Files
wmj 22ad077dab feat: add Support tab — donations + co-branding/white-label contact
New tab under the Giving hub, alongside Zakat/Sadaqah/Khairat. Two parts:

Donations: amount presets + custom amount, four frequencies (One-Time,
Monthly, Quarterly, Yearly), each mapped to its own live Polar.sh
checkout link (merchant of record — this app never touches payment
details, same 'log intent, don't move money' principle used everywhere
else). Design/copy modeled on the sibling moslem03.falahos.my app's
existing Support tab, per direct reference.

No donation product existed on the Polar account yet, so created one
via the Polar API with the user's explicit go-ahead: 'Support Nur
Falah' as 4 pay-what-you-want products (one-time + monthly/quarterly/
yearly recurring, quarterly via recurring_interval=month with count=3
since Polar has no native quarterly interval), plus a hosted checkout
link for each. Verified all four resolve to genuinely distinct, live
Stripe-backed checkout sessions before shipping — moslem03's own
checkout.polar.sh URL pattern turned out to be stale/non-resolving;
the current correct domain is buy.polar.sh via the /v1/checkout-links
API, discovered from Polar's live OpenAPI spec rather than guessed.

Partnership section: co-branding and white-label pitch with the VP
Sales contact info given directly — info@falahos.my and WhatsApp
+60132250691 — reachable via a 'View Partnership Opportunities' link
from the donation card, matching moslem03's UX pattern of surfacing
partnership discovery from the support flow.

Covered by e2e-support.cjs (14/14) — including opening all four
checkout links live and confirming each is a genuinely distinct,
resolving Stripe checkout session, not a dead or placeholder link.
Full regression: all suites pass (one confirmed transient flake on
rerun, unrelated to this change).
2026-08-14 16:24:42 +08:00

87 lines
4.6 KiB
JavaScript

// Verifies the Support tab: amount/frequency selection, and that "Support
// Now" actually opens a real, live Polar.sh checkout page (not a dead or
// placeholder link) — one check per frequency, since each maps to a
// different Polar product.
const { chromium } = require('playwright');
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
const BASE = 'https://moslem04.falahos.my/';
const results = [];
const consoleErrors = [];
function record(name, pass, detail = '') { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); }
async function main() {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
page.on('pageerror', e => consoleErrors.push(e.message));
await signInFreshFamily(page, BASE, 'e2e-support');
await gotoTab(page, 'Support');
await page.waitForTimeout(600);
record('Support: intro copy renders', await page.locator('h3', { hasText: 'Support Nur Falah' }).isVisible().catch(() => false));
// Amount presets
await page.locator('.pill', { hasText: '$25' }).click();
await page.waitForTimeout(200);
const presetActive = await page.locator('.pill.active', { hasText: '$25' }).isVisible().catch(() => false);
record('Support: selecting a preset amount highlights it', presetActive);
// Custom amount overrides preset
await page.locator('.custom-input').fill('17');
await page.waitForTimeout(200);
const customActive = await page.locator('.pill-custom.active').isVisible().catch(() => false);
record('Support: entering a custom amount switches selection to custom', customActive);
const btnShowsCustom = await page.locator('.donate-btn', { hasText: '$17' }).isVisible().catch(() => false);
record('Support: button reflects the custom amount', btnShowsCustom);
await page.locator('.custom-input').fill('');
// Each frequency should open a distinct, real, live Polar checkout page
const frequencies = [
{ label: 'One-Time', btnText: 'Support Now' },
{ label: 'Monthly 🌙', btnText: 'Subscribe Monthly' },
{ label: 'Quarterly', btnText: 'Subscribe Quarterly' },
{ label: 'Yearly', btnText: 'Subscribe Yearly' }
];
const openedUrls = new Set();
for (const freq of frequencies) {
await page.locator('.frequency-toggle button', { hasText: freq.label }).click();
await page.waitForTimeout(200);
await page.locator('.pill', { hasText: '$10' }).click();
await page.waitForTimeout(200);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.locator('.donate-btn').click()
]);
await popup.waitForLoadState('domcontentloaded').catch(() => {});
await page.waitForTimeout(1500);
const finalUrl = popup.url();
openedUrls.add(finalUrl);
const reachedPolar = /polar\.sh|stripe\.com/i.test(finalUrl);
record(`Support (${freq.label}): opens a real, live checkout page`, reachedPolar, finalUrl);
await popup.close();
}
record('Support: each frequency opens a genuinely distinct checkout session', openedUrls.size === frequencies.length, `${openedUrls.size} distinct URLs`);
// Partnership section
await page.locator('a', { hasText: 'View Partnership Opportunities' }).click();
await page.waitForTimeout(500);
record('Support: partnership section is reachable', await page.locator('h3', { hasText: 'Co-Branding' }).isVisible().catch(() => false));
const emailLinkVisible = await page.locator('a[href^="mailto:info@falahos.my"]').isVisible().catch(() => false);
record('Support: shows the correct VP sales email contact', emailLinkVisible);
const whatsappLinkVisible = await page.locator('a[href="https://wa.me/60132250691"]').isVisible().catch(() => false);
record('Support: shows the correct WhatsApp contact', whatsappLinkVisible);
record('Support: mentions white-labeling', (await page.locator('.partner-tiers').innerText()).toLowerCase().includes('white-label'));
record('No uncaught JS console errors during full session', consoleErrors.length === 0, consoleErrors.slice(0, 5).join(' || '));
await browser.close();
const passCount = results.filter(r => r.pass).length;
const failCount = results.length - passCount;
console.log(`\n${passCount} passed, ${failCount} failed, ${results.length} total`);
if (failCount > 0) results.filter(r => !r.pass).forEach(r => console.log(` - ${r.name}: ${r.detail}`));
process.exit(failCount > 0 ? 1 : 0);
}
main().catch(e => { console.error('SCRIPT ERROR:', e); process.exit(2); });