From 75d201bcae1463c6ddef4c52afac29faf7c9f662 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Tue, 21 Jul 2026 22:05:36 +0000 Subject: [PATCH] Fix ladl.link CORS for Mini payment assets and pay URL. Allow cross-origin module loads of Mini build assets and use a same-origin pay path when HTML is proxied through ladl.link so fetch and Vite JS work. --- config/cors.php | 41 +++++++++++ .../views/public/qr/payment-landing.blade.php | 10 ++- tests/Feature/MiniPaymentCheckoutTest.php | 5 +- tests/Feature/MiniPaymentCorsTest.php | 73 +++++++++++++++++++ 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 config/cors.php create mode 100644 tests/Feature/MiniPaymentCorsTest.php diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..c0a7cd4 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,41 @@ + [ + 'q/*', + 'build/*', + 'images/*', + 'sanctum/csrf-cookie', + ], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => array_values(array_filter([ + 'https://ladl.link', + 'https://www.ladl.link', + 'https://mini.ladill.com', + env('APP_URL') ? rtrim((string) env('APP_URL'), '/') : null, + ])), + + 'allowed_origins_patterns' => [ + // Public short-link host and optional subdomains. + '#^https://([a-z0-9-]+\.)?ladl\.link$#i', + // Platform family (custom / satellite hosts that may proxy Mini HTML). + '#^https://([a-z0-9-]+\.)?ladill\.com$#i', + ], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 60 * 60, + + 'supports_credentials' => false, + +]; diff --git a/resources/views/public/qr/payment-landing.blade.php b/resources/views/public/qr/payment-landing.blade.php index 26b119e..b0bb5a0 100644 --- a/resources/views/public/qr/payment-landing.blade.php +++ b/resources/views/public/qr/payment-landing.blade.php @@ -2,9 +2,13 @@ $content = $qrCode->content(); $businessName = $content['business_name'] ?? $qrCode->label; $currency = $content['currency'] ?? 'GHS'; - // Same-origin pay endpoint — ladl.link publicPath is cross-origin from - // mini.ladill.com and has no CORS headers, so fetch() never reaches showSheet. - $payUrl = url('/q/'.$qrCode->short_code.'/pay'); + // Public scans stay on ladl.link/{code} (HTML proxied to Mini). Prefer a + // same-origin pay path so fetch() does not cross to mini.ladill.com. + // Direct visits on mini.ladill.com keep the local /q/{code}/pay route. + $isProxiedPublicScan = request()->headers->get('X-Ladill-Internal') === '1'; + $payUrl = $isProxiedPublicScan + ? '/'.$qrCode->short_code.'/pay' + : url('/q/'.$qrCode->short_code.'/pay'); $csrf = csrf_token(); @endphp diff --git a/tests/Feature/MiniPaymentCheckoutTest.php b/tests/Feature/MiniPaymentCheckoutTest.php index 61e1d7e..6017988 100644 --- a/tests/Feature/MiniPaymentCheckoutTest.php +++ b/tests/Feature/MiniPaymentCheckoutTest.php @@ -46,8 +46,9 @@ class MiniPaymentCheckoutTest extends TestCase ->assertSee('data-ladill-pay-sheet', false) ->assertSee('data-ladill-pay-panel', false) ->assertSee('resumeTransaction', false) - ->assertSee('/q/'.$qr->short_code.'/pay', false) - ->assertDontSee('https://ladl.link/'.$qr->short_code.'/pay', false) + // Proxied ladl.link scans use same-origin /{code}/pay (not mini absolute /q/...). + ->assertSee('/'.$qr->short_code.'/pay', false) + ->assertDontSee('https://mini.ladill.com/q/'.$qr->short_code.'/pay', false) ->assertDontSee('MTN MoMo number', false) ->assertDontSee('Pay with MTN MoMo', false) ->assertDontSee('Continue to Paystack', false) diff --git a/tests/Feature/MiniPaymentCorsTest.php b/tests/Feature/MiniPaymentCorsTest.php new file mode 100644 index 0000000..dff87a4 --- /dev/null +++ b/tests/Feature/MiniPaymentCorsTest.php @@ -0,0 +1,73 @@ + (string) Str::uuid(), + 'name' => 'Vendor', + 'email' => 'vendor-cors@example.com', + ]); + + $qr = QrCode::query()->create([ + 'user_id' => $user->id, + 'short_code' => 'paycors01', + 'type' => QrCode::TYPE_PAYMENT, + 'label' => 'Till', + 'payload' => [ + 'content' => [ + 'business_name' => 'Accra Kiosk', + 'currency' => 'GHS', + ], + 'style' => [], + ], + 'is_active' => true, + ]); + + $html = $this->withHeader(\App\Support\LadillLink::INTERNAL_HEADER, '1') + ->get('/q/'.$qr->short_code) + ->assertOk() + ->getContent(); + + $this->assertStringContainsString('paycors01/pay', $html); + $this->assertStringNotContainsString('https://mini.ladill.com/q/paycors01/pay', $html); + } + + public function test_payment_landing_redirects_non_internal_to_public_host(): void + { + $user = User::create([ + 'public_id' => (string) Str::uuid(), + 'name' => 'Vendor', + 'email' => 'vendor-mode@example.com', + ]); + + $qr = QrCode::query()->create([ + 'user_id' => $user->id, + 'short_code' => 'paymode1', + 'type' => QrCode::TYPE_PAYMENT, + 'label' => 'Till', + 'payload' => [ + 'content' => [ + 'business_name' => 'Accra Kiosk', + 'currency' => 'GHS', + ], + 'style' => [], + ], + 'is_active' => true, + ]); + + // Direct hit (no internal header) is redirected to ladl.link by legacy middleware. + $this->get('/q/'.$qr->short_code)->assertRedirect(); + } +}