From 4dac84643b00a918382841f1b7e27e72b5ac9e17 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 24 Mar 2026 21:52:22 +0000 Subject: [PATCH 1/2] Add country and payout currency selection to developer onboarding Collect the developer's country and preferred payout currency during Stripe Connect onboarding. This ensures Express accounts are created with the correct country parameter and enables multi-currency payouts for European/EEA countries. - Add country (108 Stripe-supported countries) and payout_currency fields to developer_accounts table - Add StripeConnectCountries support class with country data, currency names, and validation methods - Update Livewire Onboarding component with reactive country/currency selection - Use Flux Pro searchable listbox for the country dropdown - Display currency names (e.g. "US Dollar (USD)") in the currency select - Request card_payments capability alongside transfers to support all countries including cross-border (fixes JP account creation error) - Add comprehensive validation in DeveloperOnboardingController - Pass country to Stripe Connect account creation - Use developer's payout_currency for transfers instead of hardcoded USD - Install livewire/flux-pro for searchable select component - Update CLAUDE.md with correct dependency versions (Filament v5, Livewire v4, PHP 8.4.19) Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 6 +- .../DeveloperOnboardingController.php | 21 +- .../Customer/Developer/Onboarding.php | 52 ++++ app/Services/StripeConnectService.php | 8 +- app/Support/StripeConnectCountries.php | 251 ++++++++++++++++++ composer.json | 8 +- composer.lock | 75 +++++- .../factories/DeveloperAccountFactory.php | 2 + ...t_currency_to_developer_accounts_table.php | 29 ++ polyscope.json | 5 +- .../customer/developer/onboarding.blade.php | 43 ++- tests/Feature/DeveloperTermsTest.php | 124 +++++++++ .../Livewire/Customer/DeveloperPagesTest.php | 1 + tests/Unit/StripeConnectCountriesTest.php | 145 ++++++++++ 14 files changed, 760 insertions(+), 10 deletions(-) create mode 100644 app/Support/StripeConnectCountries.php create mode 100644 database/migrations/2026_03_24_000001_add_country_and_payout_currency_to_developer_accounts_table.php create mode 100644 tests/Unit/StripeConnectCountriesTest.php diff --git a/CLAUDE.md b/CLAUDE.md index 126421d1..45e08cd3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,8 +8,8 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.18 -- filament/filament (FILAMENT) - v3 +- php - 8.4.19 +- filament/filament (FILAMENT) - v5 - laravel/cashier (CASHIER) - v15 - laravel/framework (LARAVEL) - v12 - laravel/horizon (HORIZON) - v5 @@ -18,7 +18,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - laravel/prompts (PROMPTS) - v0 - laravel/sanctum (SANCTUM) - v4 - laravel/socialite (SOCIALITE) - v5 -- livewire/livewire (LIVEWIRE) - v3 +- livewire/livewire (LIVEWIRE) - v4 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 diff --git a/app/Http/Controllers/DeveloperOnboardingController.php b/app/Http/Controllers/DeveloperOnboardingController.php index d1f218ac..e0cc8169 100644 --- a/app/Http/Controllers/DeveloperOnboardingController.php +++ b/app/Http/Controllers/DeveloperOnboardingController.php @@ -4,8 +4,10 @@ use App\Models\DeveloperAccount; use App\Services\StripeConnectService; +use App\Support\StripeConnectCountries; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Validation\Rule; use Illuminate\View\View; class DeveloperOnboardingController extends Controller @@ -32,16 +34,33 @@ public function start(Request $request): RedirectResponse { $request->validate([ 'accepted_plugin_terms' => ['required', 'accepted'], + 'country' => ['required', 'string', 'size:2', Rule::in(StripeConnectCountries::supportedCountryCodes())], + 'payout_currency' => ['required', 'string', 'size:3'], ], [ 'accepted_plugin_terms.required' => 'You must accept the Plugin Developer Terms and Conditions.', 'accepted_plugin_terms.accepted' => 'You must accept the Plugin Developer Terms and Conditions.', + 'country.required' => 'Please select your country.', + 'country.in' => 'The selected country is not supported for Stripe Connect.', + 'payout_currency.required' => 'Please select a payout currency.', ]); + $country = strtoupper($request->input('country')); + $payoutCurrency = strtoupper($request->input('payout_currency')); + + if (! StripeConnectCountries::isValidCurrencyForCountry($country, $payoutCurrency)) { + return back()->withErrors(['payout_currency' => 'The selected currency is not available for your country.']); + } + $user = $request->user(); $developerAccount = $user->developerAccount; if (! $developerAccount) { - $developerAccount = $this->stripeConnectService->createConnectAccount($user); + $developerAccount = $this->stripeConnectService->createConnectAccount($user, $country, $payoutCurrency); + } else { + $developerAccount->update([ + 'country' => $country, + 'payout_currency' => $payoutCurrency, + ]); } if (! $developerAccount->hasAcceptedCurrentTerms()) { diff --git a/app/Livewire/Customer/Developer/Onboarding.php b/app/Livewire/Customer/Developer/Onboarding.php index 0c51ca6a..337d59ef 100644 --- a/app/Livewire/Customer/Developer/Onboarding.php +++ b/app/Livewire/Customer/Developer/Onboarding.php @@ -2,6 +2,7 @@ namespace App\Livewire\Customer\Developer; +use App\Support\StripeConnectCountries; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; @@ -11,6 +12,10 @@ #[Title('Developer Onboarding')] class Onboarding extends Component { + public string $country = ''; + + public string $payoutCurrency = ''; + public function mount(): void { $developerAccount = auth()->user()->developerAccount; @@ -18,6 +23,20 @@ public function mount(): void if ($developerAccount && $developerAccount->hasCompletedOnboarding()) { $this->redirect(route('customer.developer.dashboard'), navigate: true); } + + if ($developerAccount) { + $this->country = $developerAccount->country ?? ''; + $this->payoutCurrency = $developerAccount->payout_currency ?? ''; + } + } + + public function updatedCountry(string $value): void + { + if (StripeConnectCountries::isSupported($value)) { + $this->payoutCurrency = StripeConnectCountries::defaultCurrency($value); + } else { + $this->payoutCurrency = ''; + } } #[Computed] @@ -32,6 +51,39 @@ public function hasExistingAccount(): bool return $this->developerAccount !== null; } + /** + * @return array}> + */ + #[Computed] + public function countries(): array + { + $countries = StripeConnectCountries::all(); + + uasort($countries, fn (array $a, array $b) => $a['name'] <=> $b['name']); + + return $countries; + } + + /** + * @return array + */ + #[Computed] + public function availableCurrencies(): array + { + if (! $this->country || ! StripeConnectCountries::isSupported($this->country)) { + return []; + } + + $currencies = StripeConnectCountries::availableCurrencies($this->country); + + $named = []; + foreach ($currencies as $code) { + $named[$code] = StripeConnectCountries::currencyName($code); + } + + return $named; + } + public function render() { return view('livewire.customer.developer.onboarding'); diff --git a/app/Services/StripeConnectService.php b/app/Services/StripeConnectService.php index e521187f..925b2ea0 100644 --- a/app/Services/StripeConnectService.php +++ b/app/Services/StripeConnectService.php @@ -19,15 +19,17 @@ */ class StripeConnectService { - public function createConnectAccount(User $user): DeveloperAccount + public function createConnectAccount(User $user, string $country, string $payoutCurrency): DeveloperAccount { $account = Cashier::stripe()->accounts->create([ 'type' => 'express', + 'country' => $country, 'email' => $user->email, 'metadata' => [ 'user_id' => $user->id, ], 'capabilities' => [ + 'card_payments' => ['requested' => true], 'transfers' => ['requested' => true], ], ]); @@ -38,6 +40,8 @@ public function createConnectAccount(User $user): DeveloperAccount 'stripe_connect_status' => StripeConnectStatus::Pending, 'payouts_enabled' => false, 'charges_enabled' => false, + 'country' => $country, + 'payout_currency' => $payoutCurrency, ]); } @@ -109,7 +113,7 @@ public function processTransfer(PluginPayout $payout): bool try { $transferParams = [ 'amount' => $payout->developer_amount, - 'currency' => 'usd', + 'currency' => strtolower($developerAccount->payout_currency ?? 'usd'), 'destination' => $developerAccount->stripe_connect_account_id, 'metadata' => [ 'payout_id' => $payout->id, diff --git a/app/Support/StripeConnectCountries.php b/app/Support/StripeConnectCountries.php new file mode 100644 index 00000000..824029f1 --- /dev/null +++ b/app/Support/StripeConnectCountries.php @@ -0,0 +1,251 @@ +}> + */ + public const SUPPORTED_COUNTRIES = [ + 'AE' => ['name' => 'United Arab Emirates', 'flag' => "\u{1F1E6}\u{1F1EA}", 'default_currency' => 'AED', 'currencies' => ['AED']], + 'AG' => ['name' => 'Antigua & Barbuda', 'flag' => "\u{1F1E6}\u{1F1EC}", 'default_currency' => 'XCD', 'currencies' => ['XCD']], + 'AL' => ['name' => 'Albania', 'flag' => "\u{1F1E6}\u{1F1F1}", 'default_currency' => 'ALL', 'currencies' => ['ALL']], + 'AM' => ['name' => 'Armenia', 'flag' => "\u{1F1E6}\u{1F1F2}", 'default_currency' => 'AMD', 'currencies' => ['AMD']], + 'AR' => ['name' => 'Argentina', 'flag' => "\u{1F1E6}\u{1F1F7}", 'default_currency' => 'ARS', 'currencies' => ['ARS']], + 'AT' => ['name' => 'Austria', 'flag' => "\u{1F1E6}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'AU' => ['name' => 'Australia', 'flag' => "\u{1F1E6}\u{1F1FA}", 'default_currency' => 'AUD', 'currencies' => ['AUD', 'USD']], + 'BA' => ['name' => 'Bosnia & Herzegovina', 'flag' => "\u{1F1E7}\u{1F1E6}", 'default_currency' => 'BAM', 'currencies' => ['BAM']], + 'BE' => ['name' => 'Belgium', 'flag' => "\u{1F1E7}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'BG' => ['name' => 'Bulgaria', 'flag' => "\u{1F1E7}\u{1F1EC}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'BH' => ['name' => 'Bahrain', 'flag' => "\u{1F1E7}\u{1F1ED}", 'default_currency' => 'BHD', 'currencies' => ['BHD']], + 'BJ' => ['name' => 'Benin', 'flag' => "\u{1F1E7}\u{1F1EF}", 'default_currency' => 'XOF', 'currencies' => ['XOF']], + 'BN' => ['name' => 'Brunei', 'flag' => "\u{1F1E7}\u{1F1F3}", 'default_currency' => 'BND', 'currencies' => ['BND']], + 'BO' => ['name' => 'Bolivia', 'flag' => "\u{1F1E7}\u{1F1F4}", 'default_currency' => 'BOB', 'currencies' => ['BOB']], + 'BS' => ['name' => 'Bahamas', 'flag' => "\u{1F1E7}\u{1F1F8}", 'default_currency' => 'BSD', 'currencies' => ['BSD']], + 'BW' => ['name' => 'Botswana', 'flag' => "\u{1F1E7}\u{1F1FC}", 'default_currency' => 'BWP', 'currencies' => ['BWP']], + 'CA' => ['name' => 'Canada', 'flag' => "\u{1F1E8}\u{1F1E6}", 'default_currency' => 'CAD', 'currencies' => ['CAD', 'USD']], + 'CH' => ['name' => 'Switzerland', 'flag' => "\u{1F1E8}\u{1F1ED}", 'default_currency' => 'CHF', 'currencies' => ['CHF', 'EUR', 'GBP', 'USD', 'DKK', 'NOK', 'SEK']], + 'CI' => ['name' => "C\u{00F4}te d'Ivoire", 'flag' => "\u{1F1E8}\u{1F1EE}", 'default_currency' => 'XOF', 'currencies' => ['XOF']], + 'CL' => ['name' => 'Chile', 'flag' => "\u{1F1E8}\u{1F1F1}", 'default_currency' => 'CLP', 'currencies' => ['CLP']], + 'CO' => ['name' => 'Colombia', 'flag' => "\u{1F1E8}\u{1F1F4}", 'default_currency' => 'COP', 'currencies' => ['COP']], + 'CR' => ['name' => 'Costa Rica', 'flag' => "\u{1F1E8}\u{1F1F7}", 'default_currency' => 'CRC', 'currencies' => ['CRC']], + 'CY' => ['name' => 'Cyprus', 'flag' => "\u{1F1E8}\u{1F1FE}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'CZ' => ['name' => 'Czech Republic', 'flag' => "\u{1F1E8}\u{1F1FF}", 'default_currency' => 'CZK', 'currencies' => ['CZK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'DE' => ['name' => 'Germany', 'flag' => "\u{1F1E9}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'DK' => ['name' => 'Denmark', 'flag' => "\u{1F1E9}\u{1F1F0}", 'default_currency' => 'DKK', 'currencies' => ['DKK', 'EUR', 'GBP', 'USD', 'CHF', 'NOK', 'SEK']], + 'DO' => ['name' => 'Dominican Republic', 'flag' => "\u{1F1E9}\u{1F1F4}", 'default_currency' => 'DOP', 'currencies' => ['DOP']], + 'EC' => ['name' => 'Ecuador', 'flag' => "\u{1F1EA}\u{1F1E8}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'EE' => ['name' => 'Estonia', 'flag' => "\u{1F1EA}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'EG' => ['name' => 'Egypt', 'flag' => "\u{1F1EA}\u{1F1EC}", 'default_currency' => 'EGP', 'currencies' => ['EGP']], + 'ES' => ['name' => 'Spain', 'flag' => "\u{1F1EA}\u{1F1F8}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'ET' => ['name' => 'Ethiopia', 'flag' => "\u{1F1EA}\u{1F1F9}", 'default_currency' => 'ETB', 'currencies' => ['ETB']], + 'FI' => ['name' => 'Finland', 'flag' => "\u{1F1EB}\u{1F1EE}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'FR' => ['name' => 'France', 'flag' => "\u{1F1EB}\u{1F1F7}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'GB' => ['name' => 'United Kingdom', 'flag' => "\u{1F1EC}\u{1F1E7}", 'default_currency' => 'GBP', 'currencies' => ['GBP', 'EUR', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'GH' => ['name' => 'Ghana', 'flag' => "\u{1F1EC}\u{1F1ED}", 'default_currency' => 'GHS', 'currencies' => ['GHS']], + 'GM' => ['name' => 'Gambia', 'flag' => "\u{1F1EC}\u{1F1F2}", 'default_currency' => 'GMD', 'currencies' => ['GMD']], + 'GR' => ['name' => 'Greece', 'flag' => "\u{1F1EC}\u{1F1F7}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'GT' => ['name' => 'Guatemala', 'flag' => "\u{1F1EC}\u{1F1F9}", 'default_currency' => 'GTQ', 'currencies' => ['GTQ']], + 'GY' => ['name' => 'Guyana', 'flag' => "\u{1F1EC}\u{1F1FE}", 'default_currency' => 'GYD', 'currencies' => ['GYD']], + 'HK' => ['name' => 'Hong Kong', 'flag' => "\u{1F1ED}\u{1F1F0}", 'default_currency' => 'HKD', 'currencies' => ['HKD', 'USD']], + 'HR' => ['name' => 'Croatia', 'flag' => "\u{1F1ED}\u{1F1F7}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'HU' => ['name' => 'Hungary', 'flag' => "\u{1F1ED}\u{1F1FA}", 'default_currency' => 'HUF', 'currencies' => ['HUF', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'ID' => ['name' => 'Indonesia', 'flag' => "\u{1F1EE}\u{1F1E9}", 'default_currency' => 'IDR', 'currencies' => ['IDR']], + 'IE' => ['name' => 'Ireland', 'flag' => "\u{1F1EE}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'IL' => ['name' => 'Israel', 'flag' => "\u{1F1EE}\u{1F1F1}", 'default_currency' => 'ILS', 'currencies' => ['ILS']], + 'IN' => ['name' => 'India', 'flag' => "\u{1F1EE}\u{1F1F3}", 'default_currency' => 'INR', 'currencies' => ['INR']], + 'IS' => ['name' => 'Iceland', 'flag' => "\u{1F1EE}\u{1F1F8}", 'default_currency' => 'ISK', 'currencies' => ['ISK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'IT' => ['name' => 'Italy', 'flag' => "\u{1F1EE}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'JM' => ['name' => 'Jamaica', 'flag' => "\u{1F1EF}\u{1F1F2}", 'default_currency' => 'JMD', 'currencies' => ['JMD']], + 'JO' => ['name' => 'Jordan', 'flag' => "\u{1F1EF}\u{1F1F4}", 'default_currency' => 'JOD', 'currencies' => ['JOD']], + 'JP' => ['name' => 'Japan', 'flag' => "\u{1F1EF}\u{1F1F5}", 'default_currency' => 'JPY', 'currencies' => ['JPY']], + 'KE' => ['name' => 'Kenya', 'flag' => "\u{1F1F0}\u{1F1EA}", 'default_currency' => 'KES', 'currencies' => ['KES']], + 'KH' => ['name' => 'Cambodia', 'flag' => "\u{1F1F0}\u{1F1ED}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'KR' => ['name' => 'South Korea', 'flag' => "\u{1F1F0}\u{1F1F7}", 'default_currency' => 'KRW', 'currencies' => ['KRW']], + 'KW' => ['name' => 'Kuwait', 'flag' => "\u{1F1F0}\u{1F1FC}", 'default_currency' => 'KWD', 'currencies' => ['KWD']], + 'LC' => ['name' => 'St. Lucia', 'flag' => "\u{1F1F1}\u{1F1E8}", 'default_currency' => 'XCD', 'currencies' => ['XCD']], + 'LI' => ['name' => 'Liechtenstein', 'flag' => "\u{1F1F1}\u{1F1EE}", 'default_currency' => 'CHF', 'currencies' => ['CHF', 'EUR', 'GBP', 'USD', 'DKK', 'NOK', 'SEK']], + 'LK' => ['name' => 'Sri Lanka', 'flag' => "\u{1F1F1}\u{1F1F0}", 'default_currency' => 'LKR', 'currencies' => ['LKR']], + 'LT' => ['name' => 'Lithuania', 'flag' => "\u{1F1F1}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'LU' => ['name' => 'Luxembourg', 'flag' => "\u{1F1F1}\u{1F1FA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'LV' => ['name' => 'Latvia', 'flag' => "\u{1F1F1}\u{1F1FB}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'MA' => ['name' => 'Morocco', 'flag' => "\u{1F1F2}\u{1F1E6}", 'default_currency' => 'MAD', 'currencies' => ['MAD']], + 'MC' => ['name' => 'Monaco', 'flag' => "\u{1F1F2}\u{1F1E8}", 'default_currency' => 'EUR', 'currencies' => ['EUR']], + 'MD' => ['name' => 'Moldova', 'flag' => "\u{1F1F2}\u{1F1E9}", 'default_currency' => 'MDL', 'currencies' => ['MDL']], + 'MG' => ['name' => 'Madagascar', 'flag' => "\u{1F1F2}\u{1F1EC}", 'default_currency' => 'MGA', 'currencies' => ['MGA']], + 'MK' => ['name' => 'North Macedonia', 'flag' => "\u{1F1F2}\u{1F1F0}", 'default_currency' => 'MKD', 'currencies' => ['MKD']], + 'MN' => ['name' => 'Mongolia', 'flag' => "\u{1F1F2}\u{1F1F3}", 'default_currency' => 'MNT', 'currencies' => ['MNT']], + 'MO' => ['name' => 'Macao', 'flag' => "\u{1F1F2}\u{1F1F4}", 'default_currency' => 'MOP', 'currencies' => ['MOP']], + 'MT' => ['name' => 'Malta', 'flag' => "\u{1F1F2}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'MU' => ['name' => 'Mauritius', 'flag' => "\u{1F1F2}\u{1F1FA}", 'default_currency' => 'MUR', 'currencies' => ['MUR']], + 'MX' => ['name' => 'Mexico', 'flag' => "\u{1F1F2}\u{1F1FD}", 'default_currency' => 'MXN', 'currencies' => ['MXN']], + 'MY' => ['name' => 'Malaysia', 'flag' => "\u{1F1F2}\u{1F1FE}", 'default_currency' => 'MYR', 'currencies' => ['MYR']], + 'NA' => ['name' => 'Namibia', 'flag' => "\u{1F1F3}\u{1F1E6}", 'default_currency' => 'NAD', 'currencies' => ['NAD']], + 'NG' => ['name' => 'Nigeria', 'flag' => "\u{1F1F3}\u{1F1EC}", 'default_currency' => 'NGN', 'currencies' => ['NGN']], + 'NL' => ['name' => 'Netherlands', 'flag' => "\u{1F1F3}\u{1F1F1}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'NO' => ['name' => 'Norway', 'flag' => "\u{1F1F3}\u{1F1F4}", 'default_currency' => 'NOK', 'currencies' => ['NOK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'SEK']], + 'NZ' => ['name' => 'New Zealand', 'flag' => "\u{1F1F3}\u{1F1FF}", 'default_currency' => 'NZD', 'currencies' => ['NZD']], + 'OM' => ['name' => 'Oman', 'flag' => "\u{1F1F4}\u{1F1F2}", 'default_currency' => 'OMR', 'currencies' => ['OMR']], + 'PA' => ['name' => 'Panama', 'flag' => "\u{1F1F5}\u{1F1E6}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'PE' => ['name' => 'Peru', 'flag' => "\u{1F1F5}\u{1F1EA}", 'default_currency' => 'PEN', 'currencies' => ['PEN']], + 'PH' => ['name' => 'Philippines', 'flag' => "\u{1F1F5}\u{1F1ED}", 'default_currency' => 'PHP', 'currencies' => ['PHP']], + 'PK' => ['name' => 'Pakistan', 'flag' => "\u{1F1F5}\u{1F1F0}", 'default_currency' => 'PKR', 'currencies' => ['PKR']], + 'PL' => ['name' => 'Poland', 'flag' => "\u{1F1F5}\u{1F1F1}", 'default_currency' => 'PLN', 'currencies' => ['PLN', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'PT' => ['name' => 'Portugal', 'flag' => "\u{1F1F5}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'PY' => ['name' => 'Paraguay', 'flag' => "\u{1F1F5}\u{1F1FE}", 'default_currency' => 'PYG', 'currencies' => ['PYG']], + 'QA' => ['name' => 'Qatar', 'flag' => "\u{1F1F6}\u{1F1E6}", 'default_currency' => 'QAR', 'currencies' => ['QAR']], + 'RO' => ['name' => 'Romania', 'flag' => "\u{1F1F7}\u{1F1F4}", 'default_currency' => 'RON', 'currencies' => ['RON', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'RS' => ['name' => 'Serbia', 'flag' => "\u{1F1F7}\u{1F1F8}", 'default_currency' => 'RSD', 'currencies' => ['RSD']], + 'RW' => ['name' => 'Rwanda', 'flag' => "\u{1F1F7}\u{1F1FC}", 'default_currency' => 'RWF', 'currencies' => ['RWF']], + 'SA' => ['name' => 'Saudi Arabia', 'flag' => "\u{1F1F8}\u{1F1E6}", 'default_currency' => 'SAR', 'currencies' => ['SAR']], + 'SE' => ['name' => 'Sweden', 'flag' => "\u{1F1F8}\u{1F1EA}", 'default_currency' => 'SEK', 'currencies' => ['SEK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK']], + 'SG' => ['name' => 'Singapore', 'flag' => "\u{1F1F8}\u{1F1EC}", 'default_currency' => 'SGD', 'currencies' => ['SGD', 'USD']], + 'SI' => ['name' => 'Slovenia', 'flag' => "\u{1F1F8}\u{1F1EE}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'SK' => ['name' => 'Slovakia', 'flag' => "\u{1F1F8}\u{1F1F0}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'SN' => ['name' => 'Senegal', 'flag' => "\u{1F1F8}\u{1F1F3}", 'default_currency' => 'XOF', 'currencies' => ['XOF']], + 'SV' => ['name' => 'El Salvador', 'flag' => "\u{1F1F8}\u{1F1FB}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'TH' => ['name' => 'Thailand', 'flag' => "\u{1F1F9}\u{1F1ED}", 'default_currency' => 'THB', 'currencies' => ['THB']], + 'TN' => ['name' => 'Tunisia', 'flag' => "\u{1F1F9}\u{1F1F3}", 'default_currency' => 'TND', 'currencies' => ['TND']], + 'TR' => ['name' => "T\u{00FC}rkiye", 'flag' => "\u{1F1F9}\u{1F1F7}", 'default_currency' => 'TRY', 'currencies' => ['TRY']], + 'TT' => ['name' => 'Trinidad & Tobago', 'flag' => "\u{1F1F9}\u{1F1F9}", 'default_currency' => 'TTD', 'currencies' => ['TTD']], + 'TW' => ['name' => 'Taiwan', 'flag' => "\u{1F1F9}\u{1F1FC}", 'default_currency' => 'TWD', 'currencies' => ['TWD']], + 'TZ' => ['name' => 'Tanzania', 'flag' => "\u{1F1F9}\u{1F1FF}", 'default_currency' => 'TZS', 'currencies' => ['TZS']], + 'US' => ['name' => 'United States', 'flag' => "\u{1F1FA}\u{1F1F8}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'UY' => ['name' => 'Uruguay', 'flag' => "\u{1F1FA}\u{1F1FE}", 'default_currency' => 'UYU', 'currencies' => ['UYU']], + 'UZ' => ['name' => 'Uzbekistan', 'flag' => "\u{1F1FA}\u{1F1FF}", 'default_currency' => 'UZS', 'currencies' => ['UZS']], + 'VN' => ['name' => 'Vietnam', 'flag' => "\u{1F1FB}\u{1F1F3}", 'default_currency' => 'VND', 'currencies' => ['VND']], + 'ZA' => ['name' => 'South Africa', 'flag' => "\u{1F1FF}\u{1F1E6}", 'default_currency' => 'ZAR', 'currencies' => ['ZAR']], + ]; + + /** + * @var array + */ + public const CURRENCY_NAMES = [ + 'AED' => 'UAE Dirham', + 'ALL' => 'Albanian Lek', + 'AMD' => 'Armenian Dram', + 'ARS' => 'Argentine Peso', + 'AUD' => 'Australian Dollar', + 'BAM' => 'Convertible Mark', + 'BHD' => 'Bahraini Dinar', + 'BND' => 'Brunei Dollar', + 'BOB' => 'Boliviano', + 'BSD' => 'Bahamian Dollar', + 'BWP' => 'Botswana Pula', + 'CAD' => 'Canadian Dollar', + 'CHF' => 'Swiss Franc', + 'CLP' => 'Chilean Peso', + 'COP' => 'Colombian Peso', + 'CRC' => 'Costa Rican Colon', + 'CZK' => 'Czech Koruna', + 'DKK' => 'Danish Krone', + 'DOP' => 'Dominican Peso', + 'EGP' => 'Egyptian Pound', + 'ETB' => 'Ethiopian Birr', + 'EUR' => 'Euro', + 'GBP' => 'British Pound', + 'GHS' => 'Ghanaian Cedi', + 'GMD' => 'Gambian Dalasi', + 'GTQ' => 'Guatemalan Quetzal', + 'GYD' => 'Guyanese Dollar', + 'HKD' => 'Hong Kong Dollar', + 'HUF' => 'Hungarian Forint', + 'IDR' => 'Indonesian Rupiah', + 'ILS' => 'Israeli Shekel', + 'INR' => 'Indian Rupee', + 'ISK' => 'Icelandic Krona', + 'JMD' => 'Jamaican Dollar', + 'JOD' => 'Jordanian Dinar', + 'JPY' => 'Japanese Yen', + 'KES' => 'Kenyan Shilling', + 'KRW' => 'South Korean Won', + 'KWD' => 'Kuwaiti Dinar', + 'LKR' => 'Sri Lankan Rupee', + 'MAD' => 'Moroccan Dirham', + 'MDL' => 'Moldovan Leu', + 'MGA' => 'Malagasy Ariary', + 'MKD' => 'Macedonian Denar', + 'MNT' => 'Mongolian Tugrik', + 'MOP' => 'Macanese Pataca', + 'MUR' => 'Mauritian Rupee', + 'MXN' => 'Mexican Peso', + 'MYR' => 'Malaysian Ringgit', + 'NAD' => 'Namibian Dollar', + 'NGN' => 'Nigerian Naira', + 'NOK' => 'Norwegian Krone', + 'NZD' => 'New Zealand Dollar', + 'OMR' => 'Omani Rial', + 'PEN' => 'Peruvian Sol', + 'PHP' => 'Philippine Peso', + 'PKR' => 'Pakistani Rupee', + 'PLN' => 'Polish Zloty', + 'PYG' => 'Paraguayan Guarani', + 'QAR' => 'Qatari Riyal', + 'RON' => 'Romanian Leu', + 'RSD' => 'Serbian Dinar', + 'RWF' => 'Rwandan Franc', + 'SAR' => 'Saudi Riyal', + 'SEK' => 'Swedish Krona', + 'SGD' => 'Singapore Dollar', + 'THB' => 'Thai Baht', + 'TND' => 'Tunisian Dinar', + 'TRY' => 'Turkish Lira', + 'TTD' => 'Trinidad & Tobago Dollar', + 'TWD' => 'New Taiwan Dollar', + 'TZS' => 'Tanzanian Shilling', + 'USD' => 'US Dollar', + 'UYU' => 'Uruguayan Peso', + 'UZS' => 'Uzbekistani Som', + 'VND' => 'Vietnamese Dong', + 'XCD' => 'East Caribbean Dollar', + 'XOF' => 'West African CFA Franc', + 'ZAR' => 'South African Rand', + ]; + + public static function currencyName(string $code): string + { + return self::CURRENCY_NAMES[strtoupper($code)] ?? strtoupper($code); + } + + /** + * @return array}> + */ + public static function all(): array + { + return self::SUPPORTED_COUNTRIES; + } + + public static function isSupported(string $code): bool + { + return isset(self::SUPPORTED_COUNTRIES[strtoupper($code)]); + } + + public static function defaultCurrency(string $countryCode): ?string + { + return self::SUPPORTED_COUNTRIES[strtoupper($countryCode)]['default_currency'] ?? null; + } + + /** + * @return list + */ + public static function availableCurrencies(string $countryCode): array + { + return self::SUPPORTED_COUNTRIES[strtoupper($countryCode)]['currencies'] ?? []; + } + + public static function isValidCurrencyForCountry(string $countryCode, string $currencyCode): bool + { + return in_array(strtoupper($currencyCode), self::availableCurrencies($countryCode), true); + } + + /** + * @return list + */ + public static function supportedCountryCodes(): array + { + return array_keys(self::SUPPORTED_COUNTRIES); + } +} diff --git a/composer.json b/composer.json index bdf6a617..aa8d99a9 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "league/flysystem-aws-s3-v3": "^3.30", "livewire/blaze": "^1.0", "livewire/flux": "^2.13", + "livewire/flux-pro": "^2.13", "livewire/livewire": "^4.0", "sentry/sentry-laravel": "^4.13", "simonhamp/the-og": "^0.7.0", @@ -100,5 +101,10 @@ } }, "minimum-stability": "stable", - "prefer-stable": true + "prefer-stable": true, + "repositories": [{ + "name": "flux-pro", + "type": "composer", + "url": "https://composer.fluxui.dev" + }] } diff --git a/composer.lock b/composer.lock index fdcee1a1..905b04d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "65176a8ba5da980556da411f0a6d6354", + "content-hash": "0cc77dd249810289ccbf001bbaec7aa5", "packages": [ { "name": "artesaos/seotools", @@ -4734,6 +4734,79 @@ }, "time": "2026-03-03T03:32:35+00:00" }, + { + "name": "livewire/flux-pro", + "version": "2.13.0", + "dist": { + "type": "zip", + "url": "https://composer.fluxui.dev/download/a1357bcd-0c9f-48ec-a532-2aced09459e7/flux-pro-2.13.0.zip", + "reference": "3d91059d053ac94a2f522944f581c3876963656c", + "shasum": "dc3d4e5baedc6e525f41f25474885413e5c1e13e" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/view": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "livewire/flux": "2.13.0|dev-main", + "livewire/livewire": "^3.7.4|^4.0", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "livewire/volt": "*", + "orchestra/testbench": "^10.8|^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Flux": "FluxPro\\FluxPro" + }, + "providers": [ + "FluxPro\\FluxProServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "FluxPro\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\": "workbench/app/" + } + }, + "scripts": { + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@php vendor/bin/testbench workbench:build --ansi", + "@php vendor/bin/testbench serve --port 3000 --ansi" + ] + }, + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "The pro version of Flux, the official UI component library for Livewire.", + "keywords": [ + "components", + "flux", + "laravel", + "livewire", + "ui" + ], + "time": "2026-03-03T04:07:04+00:00" + }, { "name": "livewire/livewire", "version": "v4.2.1", diff --git a/database/factories/DeveloperAccountFactory.php b/database/factories/DeveloperAccountFactory.php index 5dc72735..16fec8b5 100644 --- a/database/factories/DeveloperAccountFactory.php +++ b/database/factories/DeveloperAccountFactory.php @@ -26,6 +26,8 @@ public function definition(): array 'payouts_enabled' => true, 'charges_enabled' => true, 'onboarding_completed_at' => now(), + 'country' => 'US', + 'payout_currency' => 'USD', ]; } diff --git a/database/migrations/2026_03_24_000001_add_country_and_payout_currency_to_developer_accounts_table.php b/database/migrations/2026_03_24_000001_add_country_and_payout_currency_to_developer_accounts_table.php new file mode 100644 index 00000000..6fa682ae --- /dev/null +++ b/database/migrations/2026_03_24_000001_add_country_and_payout_currency_to_developer_accounts_table.php @@ -0,0 +1,29 @@ +string('country', 2)->nullable()->after('stripe_connect_status'); + $table->string('payout_currency', 3)->nullable()->after('country'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('developer_accounts', function (Blueprint $table) { + $table->dropColumn(['country', 'payout_currency']); + }); + } +}; diff --git a/polyscope.json b/polyscope.json index fb535e68..8fe67fd7 100644 --- a/polyscope.json +++ b/polyscope.json @@ -2,7 +2,10 @@ "scripts": { "setup": [ "herd link", - "herd secure" + "herd secure", + "composer install", + "npm i", + "npm run build" ], "archive": [ "herd unsecure", diff --git a/resources/views/livewire/customer/developer/onboarding.blade.php b/resources/views/livewire/customer/developer/onboarding.blade.php index b5de5996..527ab9b9 100644 --- a/resources/views/livewire/customer/developer/onboarding.blade.php +++ b/resources/views/livewire/customer/developer/onboarding.blade.php @@ -77,11 +77,52 @@ @endif + {{-- Country & Currency Selection --}} +
+ Your Country + + Select the country where your bank account is located. This determines which currencies are available for payouts. + + +
+
+ + @foreach ($this->countries as $code => $details) + + {{ $details['flag'] }} {{ $details['name'] }} + + @endforeach + + @error('country') + {{ $message }} + @enderror +
+ + @if (count($this->availableCurrencies) > 0) +
+ + @foreach ($this->availableCurrencies as $code => $name) + + {{ $name }} ({{ $code }}) + + @endforeach + + @error('payout_currency') + {{ $message }} + @enderror +
+ @endif +
+
+ {{-- Developer Terms Agreement & CTA Button --}}
@csrf + + + @if ($this->developerAccount?->hasAcceptedCurrentTerms()) @@ -137,7 +178,7 @@ class="mt-0.5 size-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-5
@endif - + @if ($this->hasExistingAccount) Continue Onboarding @else diff --git a/tests/Feature/DeveloperTermsTest.php b/tests/Feature/DeveloperTermsTest.php index 06d5284a..f7986f2e 100644 --- a/tests/Feature/DeveloperTermsTest.php +++ b/tests/Feature/DeveloperTermsTest.php @@ -80,6 +80,7 @@ public function onboarding_start_records_terms_acceptance(): void $mockService = Mockery::mock(StripeConnectService::class); $mockService->shouldReceive('createConnectAccount') ->once() + ->with($user, 'US', 'USD') ->andReturnUsing(fn () => DeveloperAccount::factory()->pending()->create(['user_id' => $user->id])); $mockService->shouldReceive('createOnboardingLink') ->once() @@ -90,6 +91,8 @@ public function onboarding_start_records_terms_acceptance(): void $response = $this->actingAs($user) ->post(route('customer.developer.onboarding.start'), [ 'accepted_plugin_terms' => '1', + 'country' => 'US', + 'payout_currency' => 'USD', ]); $response->assertRedirect('https://connect.stripe.com/setup/test'); @@ -124,6 +127,8 @@ public function onboarding_start_does_not_overwrite_existing_terms_acceptance(): $this->actingAs($user) ->post(route('customer.developer.onboarding.start'), [ 'accepted_plugin_terms' => '1', + 'country' => 'GB', + 'payout_currency' => 'GBP', ]); $developerAccount->refresh(); @@ -131,6 +136,8 @@ public function onboarding_start_does_not_overwrite_existing_terms_acceptance(): $originalTime->toDateTimeString(), $developerAccount->accepted_plugin_terms_at->toDateTimeString() ); + $this->assertEquals('GB', $developerAccount->country); + $this->assertEquals('GBP', $developerAccount->payout_currency); } /** @test */ @@ -240,4 +247,121 @@ public function plugin_create_page_shows_github_required_for_non_connected_user( ->assertStatus(200) ->assertSee('GitHub Connection Required'); } + + /** @test */ + public function onboarding_start_requires_country(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'payout_currency' => 'USD', + ]); + + $response->assertSessionHasErrors('country'); + } + + /** @test */ + public function onboarding_start_rejects_invalid_country_code(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'XX', + 'payout_currency' => 'USD', + ]); + + $response->assertSessionHasErrors('country'); + } + + /** @test */ + public function onboarding_start_requires_payout_currency(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'US', + ]); + + $response->assertSessionHasErrors('payout_currency'); + } + + /** @test */ + public function onboarding_start_rejects_invalid_currency_for_country(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'US', + 'payout_currency' => 'EUR', + ]); + + $response->assertSessionHasErrors('payout_currency'); + } + + /** @test */ + public function onboarding_start_stores_country_and_currency_on_developer_account(): void + { + $user = User::factory()->create(); + + $mockService = Mockery::mock(StripeConnectService::class); + $mockService->shouldReceive('createConnectAccount') + ->once() + ->with($user, 'FR', 'EUR') + ->andReturnUsing(fn () => DeveloperAccount::factory()->pending()->create([ + 'user_id' => $user->id, + 'country' => 'FR', + 'payout_currency' => 'EUR', + ])); + $mockService->shouldReceive('createOnboardingLink') + ->once() + ->andReturn('https://connect.stripe.com/setup/test'); + + $this->app->instance(StripeConnectService::class, $mockService); + + $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'FR', + 'payout_currency' => 'EUR', + ]); + + $developerAccount = $user->fresh()->developerAccount; + $this->assertEquals('FR', $developerAccount->country); + $this->assertEquals('EUR', $developerAccount->payout_currency); + } + + /** @test */ + public function onboarding_page_shows_country_and_currency_fields(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Your Country') + ->assertSee('Select your country') + ->assertStatus(200); + } + + /** @test */ + public function onboarding_component_updates_currency_when_country_changes(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->set('country', 'FR') + ->assertSet('payoutCurrency', 'EUR') + ->set('country', 'US') + ->assertSet('payoutCurrency', 'USD') + ->set('country', 'GB') + ->assertSet('payoutCurrency', 'GBP'); + } } diff --git a/tests/Feature/Livewire/Customer/DeveloperPagesTest.php b/tests/Feature/Livewire/Customer/DeveloperPagesTest.php index 8b5c6946..688f6c6d 100644 --- a/tests/Feature/Livewire/Customer/DeveloperPagesTest.php +++ b/tests/Feature/Livewire/Customer/DeveloperPagesTest.php @@ -53,6 +53,7 @@ public function test_onboarding_component_shows_start_selling_for_new_user(): vo ->assertSee('Start Selling Plugins') ->assertSee('Connect with Stripe') ->assertSee('Plugin Developer Terms and Conditions') + ->assertSee('Your Country') ->assertStatus(200); } diff --git a/tests/Unit/StripeConnectCountriesTest.php b/tests/Unit/StripeConnectCountriesTest.php new file mode 100644 index 00000000..a8d1fb30 --- /dev/null +++ b/tests/Unit/StripeConnectCountriesTest.php @@ -0,0 +1,145 @@ +assertCount(108, $countries); + $this->assertArrayHasKey('US', $countries); + $this->assertArrayHasKey('GB', $countries); + $this->assertArrayHasKey('DE', $countries); + } + + /** @test */ + public function is_supported_returns_true_for_valid_country(): void + { + $this->assertTrue(StripeConnectCountries::isSupported('US')); + $this->assertTrue(StripeConnectCountries::isSupported('GB')); + $this->assertTrue(StripeConnectCountries::isSupported('FR')); + } + + /** @test */ + public function is_supported_returns_false_for_invalid_country(): void + { + $this->assertFalse(StripeConnectCountries::isSupported('XX')); + $this->assertFalse(StripeConnectCountries::isSupported('ZZ')); + } + + /** @test */ + public function is_supported_is_case_insensitive(): void + { + $this->assertTrue(StripeConnectCountries::isSupported('us')); + $this->assertTrue(StripeConnectCountries::isSupported('gb')); + } + + /** @test */ + public function default_currency_returns_correct_currency(): void + { + $this->assertEquals('USD', StripeConnectCountries::defaultCurrency('US')); + $this->assertEquals('GBP', StripeConnectCountries::defaultCurrency('GB')); + $this->assertEquals('EUR', StripeConnectCountries::defaultCurrency('DE')); + $this->assertEquals('AUD', StripeConnectCountries::defaultCurrency('AU')); + $this->assertEquals('JPY', StripeConnectCountries::defaultCurrency('JP')); + } + + /** @test */ + public function default_currency_returns_null_for_unsupported_country(): void + { + $this->assertNull(StripeConnectCountries::defaultCurrency('XX')); + } + + /** @test */ + public function available_currencies_returns_array(): void + { + $currencies = StripeConnectCountries::availableCurrencies('US'); + + $this->assertIsArray($currencies); + $this->assertContains('USD', $currencies); + } + + /** @test */ + public function available_currencies_returns_multiple_for_european_countries(): void + { + $currencies = StripeConnectCountries::availableCurrencies('DE'); + + $this->assertContains('EUR', $currencies); + $this->assertContains('GBP', $currencies); + $this->assertContains('USD', $currencies); + } + + /** @test */ + public function available_currencies_returns_empty_for_unsupported_country(): void + { + $this->assertEmpty(StripeConnectCountries::availableCurrencies('XX')); + } + + /** @test */ + public function is_valid_currency_for_country_validates_correctly(): void + { + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('US', 'USD')); + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('DE', 'EUR')); + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('DE', 'USD')); + $this->assertFalse(StripeConnectCountries::isValidCurrencyForCountry('US', 'EUR')); + $this->assertFalse(StripeConnectCountries::isValidCurrencyForCountry('JP', 'USD')); + } + + /** @test */ + public function supported_country_codes_returns_array_of_codes(): void + { + $codes = StripeConnectCountries::supportedCountryCodes(); + + $this->assertContains('US', $codes); + $this->assertContains('GB', $codes); + $this->assertCount(108, $codes); + } + + /** @test */ + public function currency_name_returns_correct_name(): void + { + $this->assertEquals('US Dollar', StripeConnectCountries::currencyName('USD')); + $this->assertEquals('Euro', StripeConnectCountries::currencyName('EUR')); + $this->assertEquals('British Pound', StripeConnectCountries::currencyName('GBP')); + $this->assertEquals('Japanese Yen', StripeConnectCountries::currencyName('JPY')); + } + + /** @test */ + public function currency_name_returns_code_for_unknown_currency(): void + { + $this->assertEquals('ZZZ', StripeConnectCountries::currencyName('ZZZ')); + } + + /** @test */ + public function every_used_currency_has_a_name(): void + { + $countries = StripeConnectCountries::all(); + $currencies = collect($countries)->pluck('currencies')->flatten()->unique(); + + foreach ($currencies as $code) { + $name = StripeConnectCountries::currencyName($code); + $this->assertNotEquals($code, $name, "Currency {$code} is missing a name in CURRENCY_NAMES"); + } + } + + /** @test */ + public function each_country_has_required_keys(): void + { + foreach (StripeConnectCountries::all() as $code => $details) { + $this->assertArrayHasKey('name', $details, "Country {$code} missing 'name'"); + $this->assertArrayHasKey('flag', $details, "Country {$code} missing 'flag'"); + $this->assertArrayHasKey('default_currency', $details, "Country {$code} missing 'default_currency'"); + $this->assertArrayHasKey('currencies', $details, "Country {$code} missing 'currencies'"); + $this->assertNotEmpty($details['currencies'], "Country {$code} has empty currencies"); + $this->assertContains($details['default_currency'], $details['currencies'], "Country {$code} default currency not in currencies list"); + $this->assertEquals(2, strlen($code), "Country code {$code} is not 2 characters"); + $this->assertEquals(3, strlen($details['default_currency']), "Country {$code} default currency is not 3 characters"); + } + } +} From 53cd380e9425e405ab1aa28d7b7e5b4c80a08486 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 24 Mar 2026 22:02:06 +0000 Subject: [PATCH 2/2] Add Flux Pro CI auth and consolidate onboarding status messages - Configure Flux Pro Composer auth in GitHub Actions workflows using FLUX_EMAIL and FLUX_LICENSE_KEY secrets - Remove duplicate onboarding incomplete message from hero card text - Move warning callout to top of page for better visibility Co-Authored-By: Claude Opus 4.6 --- .github/workflows/linting.yaml | 3 +++ .github/workflows/tests.yaml | 3 +++ .../customer/developer/onboarding.blade.php | 22 ++++++++----------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index 99019438..1cd305a9 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -37,6 +37,9 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + - name: Configure Flux Pro auth + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_EMAIL }}" "${{ secrets.FLUX_LICENSE_KEY }}" + - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c1928c15..eedef6a6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -47,6 +47,9 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + - name: Configure Flux Pro auth + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_EMAIL }}" "${{ secrets.FLUX_LICENSE_KEY }}" + - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist diff --git a/resources/views/livewire/customer/developer/onboarding.blade.php b/resources/views/livewire/customer/developer/onboarding.blade.php index 527ab9b9..71a322c2 100644 --- a/resources/views/livewire/customer/developer/onboarding.blade.php +++ b/resources/views/livewire/customer/developer/onboarding.blade.php @@ -24,6 +24,14 @@ @endif
+ {{-- Status for existing account --}} + @if ($this->hasExistingAccount && $this->developerAccount) + + Onboarding Incomplete + Your Stripe account requires additional information before you can receive payouts. + + @endif + {{-- Hero Card --}}
@@ -38,11 +46,7 @@ @endif - @if ($this->hasExistingAccount) - You've started the onboarding process. Complete the remaining steps to start receiving payouts. - @else - Connect your Stripe account to receive payments when users purchase your plugins. - @endif + Connect your Stripe account to receive payments when users purchase your plugins.
@@ -69,14 +73,6 @@
- {{-- Status for existing account --}} - @if ($this->hasExistingAccount && $this->developerAccount) - - Onboarding Incomplete - Your Stripe account requires additional information before you can receive payouts. - - @endif - {{-- Country & Currency Selection --}}
Your Country