Fix ladl.link CORS for Mini payment assets and pay URL.
Deploy Ladill Mini / deploy (push) Successful in 44s

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.
This commit is contained in:
isaacclad
2026-07-21 22:05:36 +00:00
parent e03a5dc30f
commit 75d201bcae
4 changed files with 124 additions and 5 deletions
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* Public Mini payment pages are HTML-proxied through ladl.link while Vite assets
* and Mini pay endpoints live on mini.ladill.com. Browsers require CORS for those
* cross-origin module scripts and JSON pay posts.
*/
return [
'paths' => [
'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,
];
@@ -2,9 +2,13 @@
$content = $qrCode->content(); $content = $qrCode->content();
$businessName = $content['business_name'] ?? $qrCode->label; $businessName = $content['business_name'] ?? $qrCode->label;
$currency = $content['currency'] ?? 'GHS'; $currency = $content['currency'] ?? 'GHS';
// Same-origin pay endpoint — ladl.link publicPath is cross-origin from // Public scans stay on ladl.link/{code} (HTML proxied to Mini). Prefer a
// mini.ladill.com and has no CORS headers, so fetch() never reaches showSheet. // same-origin pay path so fetch() does not cross to mini.ladill.com.
$payUrl = url('/q/'.$qrCode->short_code.'/pay'); // 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(); $csrf = csrf_token();
@endphp @endphp
<!DOCTYPE html> <!DOCTYPE html>
+3 -2
View File
@@ -46,8 +46,9 @@ class MiniPaymentCheckoutTest extends TestCase
->assertSee('data-ladill-pay-sheet', false) ->assertSee('data-ladill-pay-sheet', false)
->assertSee('data-ladill-pay-panel', false) ->assertSee('data-ladill-pay-panel', false)
->assertSee('resumeTransaction', false) ->assertSee('resumeTransaction', false)
->assertSee('/q/'.$qr->short_code.'/pay', false) // Proxied ladl.link scans use same-origin /{code}/pay (not mini absolute /q/...).
->assertDontSee('https://ladl.link/'.$qr->short_code.'/pay', false) ->assertSee('/'.$qr->short_code.'/pay', false)
->assertDontSee('https://mini.ladill.com/q/'.$qr->short_code.'/pay', false)
->assertDontSee('MTN MoMo number', false) ->assertDontSee('MTN MoMo number', false)
->assertDontSee('Pay with MTN MoMo', false) ->assertDontSee('Pay with MTN MoMo', false)
->assertDontSee('Continue to Paystack', false) ->assertDontSee('Continue to Paystack', false)
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace Tests\Feature;
use App\Models\QrCode;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
class MiniPaymentCorsTest extends TestCase
{
use RefreshDatabase;
public function test_proxied_payment_landing_uses_same_origin_pay_path(): void
{
$user = User::create([
'public_id' => (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();
}
}