Skip to content
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b5d86f6
fix(dashboard): surface Google Play + App Store on the Providers list…
therajanmaurya Jul 24, 2026
e7438ae
fix(dashboard): surface store sync in products UI (bulk + badges + Re…
therajanmaurya Jul 24, 2026
4ecf6c1
chore(source): update .github/workflows/deploy-cloud.yml dashboard/ap…
therajanmaurya Jul 25, 2026
59a92f0
chore(source): update dashboard/__tests__/lib/googleplay-product-sync…
therajanmaurya Jul 25, 2026
590170f
Merge pull request #125 from therajanmaurya/development
mobilebytesenseicommunity Jul 25, 2026
095369f
chore(source): update cmp-paycraft/src/androidMain/kotlin/com/mobileb…
therajanmaurya Jul 25, 2026
c0def04
chore(release): bump paycraft 2.2.0 → 2.3.0 (commonMain-only native b…
therajanmaurya Jul 25, 2026
8c7073e
feat paywall currency fix native store price and usd fallback (#128)
therajanmaurya Jul 29, 2026
11064d5
chore: initialize session branch — chore(paycraft): reconcile + close…
therajanmaurya Jul 31, 2026
e3c8b1f
chore(source): update .gitignore CHANGELOG.md cmp-paycraft/src/common…
therajanmaurya Aug 1, 2026
e1b59a8
chore(source): update cmp-paycraft/src/commonMain/kotlin/com/mobileby…
therajanmaurya Aug 2, 2026
9098577
Merge pull request #129 from therajanmaurya/session-PayCraft-20260731…
mobilebytesenseicommunity Aug 2, 2026
b26abce
chore: initialize session branch — fix(auth): multi-tenant login call…
therajanmaurya Aug 2, 2026
fd74340
chore(source): update dashboard/app/auth/callback/route.ts supabase/.…
therajanmaurya Aug 2, 2026
6d1c5cf
Merge pull request #131 from therajanmaurya/session-PayCraft-20260802…
mobilebytesenseicommunity Aug 2, 2026
60dc053
merge main (#123/#124 store-sync) into development — keep dev's super…
therajanmaurya Aug 2, 2026
c5d1fb4
feat(dashboard): platform→provider selector panel on providers page (…
therajanmaurya Aug 3, 2026
8fd7684
feat(dashboard): platform panel to top + fees + default provider (#134)
therajanmaurya Aug 3, 2026
13cb384
merge main into development — keep dev's providers/page.tsx (panel at…
therajanmaurya Aug 3, 2026
862677c
fix(dashboard): remove duplicate platform providers panel (#136)
therajanmaurya Aug 3, 2026
4ce7e7c
merge main into development — keep dev's single-panel providers/page.…
therajanmaurya Aug 3, 2026
6883326
feat(dashboard): platform providers as separate sidebar page (#138)
therajanmaurya Aug 3, 2026
bb6cfba
merge main into development — keep dev (separate platform-providers p…
therajanmaurya Aug 3, 2026
8bb7a87
feat(dashboard): platform panel — explicit primary provider + save co…
therajanmaurya Aug 3, 2026
d9f115b
merge main into development — keep dev's platform panel (primary prov…
therajanmaurya Aug 3, 2026
eb5b055
refactor(dashboard): simplify platform providers to primary + fallbac…
therajanmaurya Aug 3, 2026
441d9b2
merge main into development — keep dev's simplified primary+fallback …
therajanmaurya Aug 3, 2026
693e055
feat(dashboard): per-platform primary + fallback provider selectors (…
therajanmaurya Aug 3, 2026
2cbf36a
merge main into development — keep dev's per-platform primary+fallbac…
therajanmaurya Aug 3, 2026
bbe5d2f
feat(dashboard): supported-providers catalog with connection status (…
therajanmaurya Aug 3, 2026
0d1f4ba
merge main into development — keep dev's providers catalog + per-plat…
therajanmaurya Aug 3, 2026
a0a357b
fix(dashboard): native store as locked primary for iOS/Android (#148)
therajanmaurya Aug 3, 2026
5287e65
merge main into development — keep dev native-primary panel
therajanmaurya Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 55 additions & 35 deletions dashboard/components/providers/platform-providers-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ const CATALOG: {
{ key: "direct_upi", lane: "web", href: "/providers/upi" },
]

const PLATFORMS: { key: string; label: string; native?: string; nativeHref?: string }[] = [
{ key: "ios", label: "iOS", native: "App Store (StoreKit 2)", nativeHref: "/providers/app-store" },
{ key: "android", label: "Android", native: "Google Play Billing", nativeHref: "/providers/google-play" },
const PLATFORMS: { key: string; label: string; nativeKey?: string; native?: string; nativeHref?: string }[] = [
{ key: "ios", label: "iOS", nativeKey: "app_store", native: "App Store (StoreKit 2)", nativeHref: "/providers/app-store" },
{ key: "android", label: "Android", nativeKey: "google_play", native: "Google Play Billing", nativeHref: "/providers/google-play" },
{ key: "desktop", label: "Desktop" },
{ key: "web", label: "Web" },
]
const isNativePlatform = (key: string) => key === "ios" || key === "android"

type Sel = { primary: string; fallback: string }

Expand Down Expand Up @@ -102,9 +103,15 @@ export function PlatformProvidersPanel({
if (!plat || plat === "any") continue
if (rule.country_code || rule.currency || rule.product_type) continue
ruleIdByPlatform[plat] = rule.id
const primary = methodToProvider.get(rule.priority_methods?.[0] ?? "") ?? ""
const fallback = methodToProvider.get(rule.priority_methods?.[1] ?? "") ?? ""
initSel[plat] = { primary, fallback }
if (isNativePlatform(plat)) {
// native store is the fixed primary; the single stored web method is the web-lane fallback
const fallback = methodToProvider.get(rule.priority_methods?.[0] ?? "") ?? ""
initSel[plat] = { primary: "", fallback }
} else {
const primary = methodToProvider.get(rule.priority_methods?.[0] ?? "") ?? ""
const fallback = methodToProvider.get(rule.priority_methods?.[1] ?? "") ?? ""
initSel[plat] = { primary, fallback }
}
}

const [sel, setSel] = useState<Record<string, Sel>>(initSel)
Expand All @@ -128,8 +135,10 @@ export function PlatformProvidersPanel({
return c
})
}
// ordered [primary, fallback], drop empties/dupes, map to methods
const providers = [next.primary, next.fallback].filter((p, i, a) => p && connected.has(p) && a.indexOf(p) === i)
// ordered providers → methods. Native platforms: only the web-lane provider (native store is the
// fixed primary, not a routable method). Desktop/Web: [primary, fallback].
const ordered = isNativePlatform(platform) ? [next.fallback] : [next.primary, next.fallback]
const providers = ordered.filter((p, i, a) => p && connected.has(p) && a.indexOf(p) === i)
const methods = providers.map((p) => providerInfo.get(p)?.method).filter(Boolean) as string[]
if (methods.length > 0) {
const res = await fetch("/api/routing-rules", {
Expand Down Expand Up @@ -228,10 +237,10 @@ export function PlatformProvidersPanel({
<Badge>primary + fallback</Badge>
</div>
<p className="text-xs text-ink-500 mb-4 max-w-2xl">
For each platform, pick a <strong>primary</strong> provider and an optional{" "}
<strong>fallback</strong> used when the primary can't serve a customer. Saves instantly.
iOS &amp; Android digital subscriptions always use the native store; the dropdowns route the
web / physical-goods lane there, and full checkout on Desktop / Web.
<strong>iOS &amp; Android</strong> use the native store (App Store / Google Play) as the
primary for digital subscriptions — mandatory per store policy — and the web-lane dropdown
handles physical goods there. <strong>Desktop &amp; Web</strong> pick a primary + optional
fallback web provider. Saves instantly.
</p>

{noProviders ? (
Expand All @@ -246,44 +255,55 @@ export function PlatformProvidersPanel({
<div className="hidden sm:grid grid-cols-[120px_1fr_1fr] gap-3 px-4 py-2 text-[10px] font-bold uppercase tracking-wider text-ink-400">
<span>Platform</span>
<span>Primary</span>
<span>Fallback</span>
<span>Fallback / web-lane</span>
</div>
{PLATFORMS.map((p) => {
const cur = sel[p.key] ?? { primary: "", fallback: "" }
const native = isNativePlatform(p.key)
const nativeConnected = p.nativeKey ? connected.has(p.nativeKey) : false
return (
<div key={p.key} className="grid grid-cols-1 sm:grid-cols-[120px_1fr_1fr] gap-3 px-4 py-3 items-center">
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center">
<span className="text-sm font-bold text-ink-900">{p.label}</span>
{p.native && (
<Link href={p.nativeHref ?? "#"} className="inline-flex items-center gap-1 text-[10px] font-semibold text-ink-600 bg-ink-50 border border-ink-200 rounded px-1.5 py-0.5">
{connected.has(p.key === "ios" ? "app_store" : "google_play") ? "" : "connect "}native
<Badge>auto</Badge>
</Link>
)}
</div>

{/* Primary */}
<ProviderSelect
label="Primary"
value={cur.primary}
placeholder="Auto — cheapest"
options={options}
pretty={pretty}
fee={fee}
disabled={saving === p.key}
onChange={(v) => save(p.key, { primary: v, fallback: cur.fallback === v ? "" : cur.fallback })}
/>
{native ? (
// native store is the mandatory primary for iOS/Android digital — locked, not routable
<div className="flex items-center gap-2 px-3 py-2 bg-ink-50 border border-ink-200 rounded-lg">
<span className="text-sm font-semibold text-ink-800">{p.native}</span>
<Badge>native · auto</Badge>
{nativeConnected ? (
<span className="ml-auto inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> connected
</span>
) : (
<Link href={p.nativeHref ?? "#"} className="ml-auto text-[10px] font-semibold text-brand-600">connect →</Link>
)}
</div>
) : (
<ProviderSelect
label="Primary"
value={cur.primary}
placeholder="Auto — cheapest"
options={options}
pretty={pretty}
fee={fee}
disabled={saving === p.key}
onChange={(v) => save(p.key, { primary: v, fallback: cur.fallback === v ? "" : cur.fallback })}
/>
)}

{/* Fallback (excludes the chosen primary) */}
{/* Fallback / web-lane provider */}
<div className="flex items-center gap-2">
<ProviderSelect
label="Fallback"
label={native ? "Web-lane provider" : "Fallback"}
value={cur.fallback}
placeholder="None"
options={options.filter((o) => o !== cur.primary)}
placeholder={native ? "Web lane — none" : "None"}
options={native ? options : options.filter((o) => o !== cur.primary)}
pretty={pretty}
fee={fee}
disabled={saving === p.key || !cur.primary}
disabled={saving === p.key || (!native && !cur.primary)}
onChange={(v) => save(p.key, { primary: cur.primary, fallback: v })}
/>
{saving === p.key && <span className="text-[11px] text-ink-400">saving…</span>}
Expand Down
Loading