@solvapay/react 1.0.9-preview.1 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/CONTRIBUTING.md +117 -0
- package/README.md +426 -24
- package/dist/adapters/auth.d.cts +13 -0
- package/dist/adapters/auth.d.ts +13 -0
- package/dist/adapters/auth.js +1 -0
- package/dist/chunk-37R5NZGF.js +4824 -0
- package/dist/chunk-HWVJL5X6.js +429 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-R2ZPZ7VM.js +597 -0
- package/dist/chunk-ZAV7CQ4G.js +361 -0
- package/dist/index-WBjulQHf.d.cts +3656 -0
- package/dist/index-onWNU7iT.d.ts +3656 -0
- package/dist/index.cjs +5556 -1143
- package/dist/index.d.cts +618 -708
- package/dist/index.d.ts +618 -708
- package/dist/index.js +597 -1664
- package/dist/mcp/index.cjs +8096 -0
- package/dist/mcp/index.d.cts +1013 -0
- package/dist/mcp/index.d.ts +1013 -0
- package/dist/mcp/index.js +2724 -0
- package/dist/mcp/styles.css +1020 -0
- package/dist/primitives/index.cjs +5316 -0
- package/dist/primitives/index.d.cts +721 -0
- package/dist/primitives/index.d.ts +721 -0
- package/dist/primitives/index.js +232 -0
- package/dist/styles.css +1115 -0
- package/dist/useUsage-BmOYXxgG.d.cts +413 -0
- package/dist/useUsage-nD7zwSbG.d.ts +413 -0
- package/dist/webapi-K5XBCEO6.js +3775 -0
- package/package.json +37 -6
package/README.md
CHANGED
|
@@ -34,6 +34,126 @@ By default, `SolvaPayProvider` uses:
|
|
|
34
34
|
- `/api/check-purchase` for purchase checks
|
|
35
35
|
- `/api/create-payment-intent` for payment creation
|
|
36
36
|
- `/api/process-payment` for payment processing
|
|
37
|
+
- `/api/merchant` for merchant identity (`useMerchant`, `MandateText`)
|
|
38
|
+
- `/api/get-product` for single-product lookup (`useProduct`, `CheckoutSummary`)
|
|
39
|
+
|
|
40
|
+
## Golden path — `<CheckoutLayout>` one-liner
|
|
41
|
+
|
|
42
|
+
`<CheckoutLayout>` handles the full flow: plan selection, payment (paid/free),
|
|
43
|
+
and usage-based activation. For 90% of integrations this is the only
|
|
44
|
+
component you need.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { SolvaPayProvider, CheckoutLayout } from '@solvapay/react'
|
|
48
|
+
|
|
49
|
+
export function BuyNow({ email }: { email: string }) {
|
|
50
|
+
return (
|
|
51
|
+
<SolvaPayProvider>
|
|
52
|
+
<CheckoutLayout
|
|
53
|
+
productRef="prd_myapi"
|
|
54
|
+
prefillCustomer={{ email }}
|
|
55
|
+
requireTermsAcceptance
|
|
56
|
+
onResult={result => {
|
|
57
|
+
// result.kind === 'paid' | 'activated'
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
</SolvaPayProvider>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
What happens based on what `<CheckoutLayout>` resolves:
|
|
66
|
+
|
|
67
|
+
| Product shape | Flow |
|
|
68
|
+
|---|---|
|
|
69
|
+
| One active plan | Auto-skip selection → pay or activate |
|
|
70
|
+
| Multiple plans | Styled `<PlanSelector>` → pay or activate |
|
|
71
|
+
| Free plan (`requiresPayment: false`) | Skip Stripe entirely → `useActivation` |
|
|
72
|
+
| Usage-based plan | `<ActivationFlow>` (summary → top-up → retry → activated) |
|
|
73
|
+
|
|
74
|
+
Pass `planRef` explicitly to skip the selector and keep today's payment-only
|
|
75
|
+
behavior byte-for-byte (backwards compatible with pre-selector integrations).
|
|
76
|
+
|
|
77
|
+
### Skipping plan selection
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<CheckoutLayout
|
|
81
|
+
planRef="pln_premium"
|
|
82
|
+
productRef="prd_myapi"
|
|
83
|
+
prefillCustomer={{ email }}
|
|
84
|
+
requireTermsAcceptance
|
|
85
|
+
size="auto"
|
|
86
|
+
onSuccess={() => console.log('paid')}
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The section below ("Drop-in checkout with `<CheckoutLayout>`") is the original
|
|
91
|
+
single-plan form — still supported, still works the same.
|
|
92
|
+
|
|
93
|
+
## Drop-in checkout with `<CheckoutLayout>`
|
|
94
|
+
|
|
95
|
+
For a fixed plan checkout (summary, SCA mandate, Stripe PaymentElement,
|
|
96
|
+
prefilled customer echo, optional terms checkbox):
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { SolvaPayProvider, CheckoutLayout } from '@solvapay/react'
|
|
100
|
+
|
|
101
|
+
export function BuyNow({ email }: { email: string }) {
|
|
102
|
+
return (
|
|
103
|
+
<SolvaPayProvider>
|
|
104
|
+
<CheckoutLayout
|
|
105
|
+
planRef="pln_premium"
|
|
106
|
+
productRef="prd_myapi"
|
|
107
|
+
prefillCustomer={{ email }}
|
|
108
|
+
requireTermsAcceptance
|
|
109
|
+
size="auto"
|
|
110
|
+
onSuccess={() => console.log('paid')}
|
|
111
|
+
/>
|
|
112
|
+
</SolvaPayProvider>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`size="auto"` uses a `ResizeObserver`, so the same component reflows cleanly
|
|
118
|
+
in chat bubbles, phone viewports, and desktop iframes.
|
|
119
|
+
|
|
120
|
+
## Composition: slot subcomponents
|
|
121
|
+
|
|
122
|
+
When you need custom layout, compose `<PaymentForm>` with slot children:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<PaymentForm planRef="pln_premium" productRef="prd_myapi" prefillCustomer={{ email }}>
|
|
126
|
+
<PaymentForm.Summary />
|
|
127
|
+
<PaymentForm.CustomerFields />
|
|
128
|
+
<PaymentForm.PaymentElement />
|
|
129
|
+
<PaymentForm.MandateText />
|
|
130
|
+
<PaymentForm.TermsCheckbox />
|
|
131
|
+
<PaymentForm.SubmitButton />
|
|
132
|
+
</PaymentForm>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Passing no children keeps the current default tree for backwards compatibility.
|
|
136
|
+
|
|
137
|
+
## Localization
|
|
138
|
+
|
|
139
|
+
English ships by default. Swap the locale (which also flows through to Stripe
|
|
140
|
+
Elements) and override any strings you like:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { SolvaPayProvider, CheckoutLayout, type PartialSolvaPayCopy } from '@solvapay/react'
|
|
144
|
+
|
|
145
|
+
const svSECopy: PartialSolvaPayCopy = {
|
|
146
|
+
cta: { subscribe: 'Prenumerera', processing: 'Bearbetar...' },
|
|
147
|
+
terms: { checkboxLabel: 'Jag godkänner villkoren och integritetspolicyn' },
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
<SolvaPayProvider config={{ locale: 'sv-SE', copy: svSECopy }}>
|
|
151
|
+
<CheckoutLayout planRef="..." productRef="..." />
|
|
152
|
+
</SolvaPayProvider>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Currency formatting is already locale-correct — `formatPrice` uses
|
|
156
|
+
`Intl.NumberFormat` with the provider locale and the plan's `currency` field.
|
|
37
157
|
|
|
38
158
|
### Custom API Routes
|
|
39
159
|
|
|
@@ -62,12 +182,10 @@ export default function App() {
|
|
|
62
182
|
```tsx
|
|
63
183
|
import { SolvaPayProvider } from '@solvapay/react'
|
|
64
184
|
import { createSupabaseAuthAdapter } from '@solvapay/react-supabase'
|
|
185
|
+
import { supabase } from './lib/supabase'
|
|
65
186
|
|
|
66
187
|
export default function App() {
|
|
67
|
-
const adapter = createSupabaseAuthAdapter({
|
|
68
|
-
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
69
|
-
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
70
|
-
})
|
|
188
|
+
const adapter = createSupabaseAuthAdapter({ client: supabase })
|
|
71
189
|
|
|
72
190
|
return (
|
|
73
191
|
<SolvaPayProvider config={{ auth: { adapter } }}>
|
|
@@ -79,32 +197,175 @@ export default function App() {
|
|
|
79
197
|
|
|
80
198
|
### Fully Custom Implementation
|
|
81
199
|
|
|
200
|
+
Every data-access call flows through `config.transport`. Build a custom one by
|
|
201
|
+
spreading `createHttpTransport(config)` and overriding the methods you need:
|
|
202
|
+
|
|
82
203
|
```tsx
|
|
83
|
-
import { SolvaPayProvider } from '@solvapay/react'
|
|
204
|
+
import { SolvaPayProvider, createHttpTransport } from '@solvapay/react'
|
|
84
205
|
|
|
85
206
|
export default function App() {
|
|
207
|
+
const transport = {
|
|
208
|
+
...createHttpTransport(undefined),
|
|
209
|
+
createPayment: async ({ planRef, productRef }) => {
|
|
210
|
+
const res = await fetch('/api/custom/payment', {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: JSON.stringify({ planRef, productRef }),
|
|
213
|
+
})
|
|
214
|
+
if (!res.ok) throw new Error('Failed to create payment')
|
|
215
|
+
return res.json()
|
|
216
|
+
},
|
|
217
|
+
checkPurchase: async () => {
|
|
218
|
+
const res = await fetch('/api/custom/purchase')
|
|
219
|
+
if (!res.ok) throw new Error('Failed to check purchase')
|
|
220
|
+
return res.json()
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
|
|
86
224
|
return (
|
|
87
|
-
<SolvaPayProvider
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
225
|
+
<SolvaPayProvider config={{ transport }}>
|
|
226
|
+
<CheckoutPage />
|
|
227
|
+
</SolvaPayProvider>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### MCP App
|
|
233
|
+
|
|
234
|
+
For React trees hosted inside an MCP App (where Stripe.js and direct HTTP
|
|
235
|
+
to your backend are both blocked by the host sandbox), use the MCP subpath:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { App } from '@modelcontextprotocol/ext-apps'
|
|
239
|
+
import { SolvaPayProvider } from '@solvapay/react'
|
|
240
|
+
import { createMcpAppAdapter } from '@solvapay/react/mcp'
|
|
241
|
+
|
|
242
|
+
const app = new App({ name: 'solvapay', version: '1.0.0' })
|
|
243
|
+
const transport = createMcpAppAdapter(app)
|
|
244
|
+
|
|
245
|
+
export default function Root() {
|
|
246
|
+
return (
|
|
247
|
+
<SolvaPayProvider config={{ transport }}>
|
|
102
248
|
<CheckoutPage />
|
|
103
249
|
</SolvaPayProvider>
|
|
104
250
|
)
|
|
105
251
|
}
|
|
106
252
|
```
|
|
107
253
|
|
|
254
|
+
The MCP server is expected to expose tools whose names match `MCP_TOOL_NAMES`
|
|
255
|
+
from `@solvapay/react/mcp` — each transport method maps 1:1 to a tool call.
|
|
256
|
+
|
|
257
|
+
### Managing plans in an MCP App
|
|
258
|
+
|
|
259
|
+
Once a customer is paid, drop `<CurrentPlanCard />` into the UI and the
|
|
260
|
+
SDK does the rest — plan name, price, next-billing / expiry line,
|
|
261
|
+
payment-method summary (via `get_payment_method`), Update-card and
|
|
262
|
+
Cancel-plan actions. The card returns `null` when there's no active
|
|
263
|
+
purchase, so you can render it unconditionally:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import { CurrentPlanCard } from '@solvapay/react'
|
|
267
|
+
|
|
268
|
+
function Account() {
|
|
269
|
+
return <CurrentPlanCard />
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Behind the scenes:
|
|
274
|
+
|
|
275
|
+
- Plan metadata comes from `usePurchase` (provider state, no extra fetch)
|
|
276
|
+
- Payment-method line comes from `usePaymentMethod` → `transport.getPaymentMethod()`
|
|
277
|
+
- `<UpdatePaymentMethodButton>` pre-fetches `transport.createCustomerSession()`
|
|
278
|
+
on mount and renders a real `<a target="_blank">` to the hosted portal —
|
|
279
|
+
MCP host sandboxes permit direct anchor clicks even though scripted
|
|
280
|
+
`window.open` after an async round-trip is blocked.
|
|
281
|
+
- `<CancelPlanButton>` reuses Phase 1 behaviour — no new plumbing.
|
|
282
|
+
|
|
283
|
+
If you want the bare portal-launch button on its own (e.g. in a top nav),
|
|
284
|
+
use `<LaunchCustomerPortalButton />` directly.
|
|
285
|
+
|
|
286
|
+
> `<PlanSwitcher>` (plan upgrade/downgrade with proration) and
|
|
287
|
+
> `<PaymentMethodForm>` (inline Stripe Elements card update) are deferred
|
|
288
|
+
> to follow-up PRs — see the plan in the SDK repo for scope. For plan
|
|
289
|
+
> changes today, MCP apps use `<CancelPlanButton>` + `<CurrentPlanCard>`
|
|
290
|
+
> + a fresh hosted checkout flow.
|
|
291
|
+
|
|
292
|
+
## Building MCP Apps
|
|
293
|
+
|
|
294
|
+
`@solvapay/react/mcp` ships a turnkey compound plus four composable
|
|
295
|
+
primitives for SolvaPay MCP Apps built on
|
|
296
|
+
[`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps).
|
|
297
|
+
|
|
298
|
+
### Quick start — `<McpApp>`
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { createRoot } from 'react-dom/client'
|
|
302
|
+
import { App } from '@modelcontextprotocol/ext-apps'
|
|
303
|
+
import { McpApp } from '@solvapay/react/mcp'
|
|
304
|
+
import '@solvapay/react/styles.css'
|
|
305
|
+
import '@solvapay/react/mcp/styles.css'
|
|
306
|
+
|
|
307
|
+
const app = new App({ name: 'my-mcp-app', version: '1.0.0' })
|
|
308
|
+
createRoot(document.getElementById('root')!).render(<McpApp app={app} />)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`<McpApp>` handles `app.connect()`, calls the `open_*` tool matching the
|
|
312
|
+
host's invocation context, mounts `<SolvaPayProvider>` with
|
|
313
|
+
`createMcpAppAdapter(app)` + `createMcpFetch(transport)`, and routes to the
|
|
314
|
+
correct per-view primitive.
|
|
315
|
+
|
|
316
|
+
Pass `applyContext` to wire host theme / fonts / safe-area insets from the
|
|
317
|
+
`ext-apps` helpers, override individual screens via `views`, and tweak
|
|
318
|
+
per-slot styling via `classNames`:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
<McpApp
|
|
322
|
+
app={app}
|
|
323
|
+
applyContext={ctx => {
|
|
324
|
+
if (ctx?.theme) applyDocumentTheme(ctx.theme)
|
|
325
|
+
}}
|
|
326
|
+
views={{
|
|
327
|
+
account: MyCustomAccountScreen, // optional — one-off override
|
|
328
|
+
}}
|
|
329
|
+
classNames={{ card: 'my-card', button: 'my-btn' }}
|
|
330
|
+
onInitError={err => console.error('[mcp]', err)}
|
|
331
|
+
/>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
See [`examples/mcp-checkout-app`](../../examples/mcp-checkout-app) for the
|
|
335
|
+
full host integration including the server-side `open_*` tool + UI resource
|
|
336
|
+
registration.
|
|
337
|
+
|
|
338
|
+
### Per-view primitives
|
|
339
|
+
|
|
340
|
+
When you need a custom shell — your own provider mount, additional routes,
|
|
341
|
+
a bespoke layout — compose the pieces directly:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
import {
|
|
345
|
+
createMcpAppAdapter,
|
|
346
|
+
createMcpFetch,
|
|
347
|
+
fetchMcpBootstrap,
|
|
348
|
+
McpCheckoutView,
|
|
349
|
+
McpAccountView,
|
|
350
|
+
McpTopupView,
|
|
351
|
+
McpPaywallView,
|
|
352
|
+
McpNudgeView,
|
|
353
|
+
McpViewRouter,
|
|
354
|
+
useStripeProbe,
|
|
355
|
+
} from '@solvapay/react/mcp'
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Every view accepts a `classNames?: McpViewClassNames` partial. Props are
|
|
359
|
+
typed per-view (`McpCheckoutViewProps`, `McpAccountViewProps`,
|
|
360
|
+
`McpTopupViewProps`, `McpPaywallViewProps`, `McpNudgeViewProps`).
|
|
361
|
+
|
|
362
|
+
### Architecture ADR
|
|
363
|
+
|
|
364
|
+
See [`docs/mcp-app-architecture.md`](./docs/mcp-app-architecture.md) for the
|
|
365
|
+
hybrid-API decision record and the three primitive seam fixes
|
|
366
|
+
(`AmountPicker emit="minor"` / `selector={…}`, `LaunchCustomerPortalButton asChild`)
|
|
367
|
+
that landed with this lift.
|
|
368
|
+
|
|
108
369
|
## Components
|
|
109
370
|
|
|
110
371
|
### SolvaPayProvider
|
|
@@ -251,14 +512,42 @@ Access purchase status, active purchases, and helper functions.
|
|
|
251
512
|
|
|
252
513
|
```tsx
|
|
253
514
|
const {
|
|
254
|
-
purchases, // Array of all purchases
|
|
515
|
+
purchases, // Array of all purchases (raw — plans + balance transactions)
|
|
516
|
+
balanceTransactions, // Credit top-ups and other non-plan rows
|
|
255
517
|
loading, // Loading state
|
|
256
|
-
hasPaidPurchase, // Boolean: has any paid purchase
|
|
257
|
-
activePurchase, // Most recent active purchase
|
|
518
|
+
hasPaidPurchase, // Boolean: has any paid plan purchase
|
|
519
|
+
activePurchase, // Most recent active plan purchase
|
|
258
520
|
refetch, // Function to refetch purchases
|
|
259
521
|
} = usePurchase()
|
|
260
522
|
```
|
|
261
523
|
|
|
524
|
+
#### Plans vs balance
|
|
525
|
+
|
|
526
|
+
Plans and balance credits are orthogonal. A customer can hold at most one
|
|
527
|
+
active plan and any number of balance transactions (credit top-ups today; gift
|
|
528
|
+
credits, referral bonuses, refunds tomorrow).
|
|
529
|
+
|
|
530
|
+
Every plan-shaped accessor — `activePurchase`, `activePaidPurchase`,
|
|
531
|
+
`hasPaidPurchase`, `hasProduct`, plus `cancelledPurchase` /
|
|
532
|
+
`shouldShowCancelledNotice` on `usePurchaseStatus` — filters out balance
|
|
533
|
+
transactions. Top-ups surface on `balanceTransactions`, and the aggregate
|
|
534
|
+
credit balance on `useBalance()`.
|
|
535
|
+
|
|
536
|
+
Classification is structural (`isPlanPurchase` / `isTopupPurchase`): a purchase
|
|
537
|
+
with no `planSnapshot` was never a plan. `metadata.purpose === 'credit_topup'`
|
|
538
|
+
acts as a defense-in-depth guard.
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
import { isPlanPurchase, isTopupPurchase } from '@solvapay/react'
|
|
542
|
+
|
|
543
|
+
const plans = purchases.filter(isPlanPurchase)
|
|
544
|
+
const topups = purchases.filter(isTopupPurchase)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
If you previously relied on top-ups surfacing on `activePurchase` (e.g. "show
|
|
548
|
+
the most recent transaction"), read from `purchases` or `balanceTransactions`
|
|
549
|
+
directly.
|
|
550
|
+
|
|
262
551
|
### usePlans
|
|
263
552
|
|
|
264
553
|
Fetch and manage available plans.
|
|
@@ -316,9 +605,122 @@ const {
|
|
|
316
605
|
All components and hooks are fully typed. Import types as needed:
|
|
317
606
|
|
|
318
607
|
```tsx
|
|
319
|
-
import type {
|
|
608
|
+
import type {
|
|
609
|
+
PaymentFormProps,
|
|
610
|
+
PurchaseStatus,
|
|
611
|
+
PaymentIntentResult,
|
|
612
|
+
CheckoutResult,
|
|
613
|
+
PaymentResult,
|
|
614
|
+
ActivationResult,
|
|
615
|
+
} from '@solvapay/react'
|
|
320
616
|
```
|
|
321
617
|
|
|
618
|
+
## Plan lifecycle
|
|
619
|
+
|
|
620
|
+
Beyond checkout itself, three thin styled-default components cover the
|
|
621
|
+
post-purchase experience without requiring custom UI.
|
|
622
|
+
|
|
623
|
+
### `<CancelPlanButton>`
|
|
624
|
+
|
|
625
|
+
Wraps `usePurchaseActions.cancelRenewal` with a built-in confirm dialog,
|
|
626
|
+
loading state, and plan-type-aware copy (subscription vs usage-based).
|
|
627
|
+
Auto-reads the active purchase from `usePurchase()`.
|
|
628
|
+
|
|
629
|
+
```tsx
|
|
630
|
+
import { CancelPlanButton } from '@solvapay/react'
|
|
631
|
+
|
|
632
|
+
<CancelPlanButton onCancelled={() => router.push('/')} />
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Use `confirm={false}` for a single-click cancel, or pass a string to override
|
|
636
|
+
the default copy. The render-prop form exposes `{ cancel, isCancelling,
|
|
637
|
+
disabled, purchase }` for fully custom UI.
|
|
638
|
+
|
|
639
|
+
### `<CancelledPlanNotice>`
|
|
640
|
+
|
|
641
|
+
Surfaces automatically when the customer has a cancelled-but-still-active
|
|
642
|
+
purchase. Renders the expiration date, days remaining, cancellation reason,
|
|
643
|
+
and a reactivate CTA. Renders nothing when there's nothing to show.
|
|
644
|
+
|
|
645
|
+
```tsx
|
|
646
|
+
import { CancelledPlanNotice } from '@solvapay/react'
|
|
647
|
+
|
|
648
|
+
<CancelledPlanNotice onReactivated={() => refetch()} />
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### `<CreditGate>`
|
|
652
|
+
|
|
653
|
+
Companion to `<PurchaseGate>` for usage-based flows. Blocks access when the
|
|
654
|
+
customer's credit balance falls below a threshold; renders an embedded
|
|
655
|
+
`<TopupForm>` by default.
|
|
656
|
+
|
|
657
|
+
```tsx
|
|
658
|
+
import { CreditGate } from '@solvapay/react'
|
|
659
|
+
|
|
660
|
+
<CreditGate minCredits={10}>
|
|
661
|
+
<ExpensiveFeature />
|
|
662
|
+
</CreditGate>
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Customize via the `fallback` prop, or use the render-prop form
|
|
666
|
+
(`children: ({ balance, hasCredits, topup }) => …`) for fully custom UI.
|
|
667
|
+
|
|
668
|
+
## Server-side usage tracking
|
|
669
|
+
|
|
670
|
+
There is intentionally no `useTrackUsage()` client hook. Client-reported usage
|
|
671
|
+
is trivially gamed — a user can simply block the fetch to conserve credits.
|
|
672
|
+
Instead, record usage from your server when the expensive work actually runs:
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
// app/api/do-thing/route.ts
|
|
676
|
+
import { trackUsage } from '@solvapay/next'
|
|
677
|
+
|
|
678
|
+
export async function POST(request: NextRequest) {
|
|
679
|
+
const result = await doTheExpensiveThing()
|
|
680
|
+
await trackUsage(request, { units: 1 })
|
|
681
|
+
return NextResponse.json(result)
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
The same pattern works from a Supabase Edge Function — see below.
|
|
686
|
+
|
|
687
|
+
## Using with Supabase Edge Functions
|
|
688
|
+
|
|
689
|
+
Lovable-style apps deploy React frontends against Supabase Edge Functions
|
|
690
|
+
(Deno runtime). The SDK's `api` URL overrides let the exact same
|
|
691
|
+
`<CheckoutLayout>` drop-in work against `/functions/v1/*` routes:
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
<SolvaPayProvider
|
|
695
|
+
config={{
|
|
696
|
+
api: {
|
|
697
|
+
checkPurchase: `${SUPABASE_URL}/functions/v1/check-purchase`,
|
|
698
|
+
createPayment: `${SUPABASE_URL}/functions/v1/create-payment-intent`,
|
|
699
|
+
processPayment: `${SUPABASE_URL}/functions/v1/process-payment`,
|
|
700
|
+
listPlans: `${SUPABASE_URL}/functions/v1/list-plans`,
|
|
701
|
+
getMerchant: `${SUPABASE_URL}/functions/v1/get-merchant`,
|
|
702
|
+
getProduct: `${SUPABASE_URL}/functions/v1/get-product`,
|
|
703
|
+
// …same pattern for every other endpoint
|
|
704
|
+
},
|
|
705
|
+
auth: {
|
|
706
|
+
adapter: createSupabaseAuthAdapter({ client: supabase }),
|
|
707
|
+
},
|
|
708
|
+
}}
|
|
709
|
+
>
|
|
710
|
+
<CheckoutLayout
|
|
711
|
+
productRef="prd_myapi"
|
|
712
|
+
prefillCustomer={{ email }}
|
|
713
|
+
requireTermsAcceptance
|
|
714
|
+
/>
|
|
715
|
+
</SolvaPayProvider>
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
The matching Deno edge functions ship in
|
|
719
|
+
[`@solvapay/fetch`](../../packages/fetch) — see
|
|
720
|
+
[`examples/supabase-edge`](../../examples/supabase-edge) for one-liner
|
|
721
|
+
`Deno.serve(handler)` files covering every handler including the new
|
|
722
|
+
`get-merchant` and `get-product` endpoints.
|
|
723
|
+
|
|
322
724
|
## More Information
|
|
323
725
|
|
|
324
726
|
See [`docs/contributing/architecture.md`](../../docs/contributing/architecture.md) for contributor
|
package/dist/adapters/auth.d.cts
CHANGED
|
@@ -32,6 +32,19 @@ interface AuthAdapter {
|
|
|
32
32
|
* return null and let the caller decide how to handle unauthenticated requests.
|
|
33
33
|
*/
|
|
34
34
|
getUserId: () => Promise<string | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Subscribe to auth-state changes. Optional.
|
|
37
|
+
*
|
|
38
|
+
* When provided, `SolvaPayProvider` calls `subscribe` on mount and
|
|
39
|
+
* triggers `detectAuth` every time the listener fires instead of
|
|
40
|
+
* only running `detectAuth` once. This is how reactive adapters such
|
|
41
|
+
* as `@solvapay/react-supabase` push sign-in / sign-out / token-refresh
|
|
42
|
+
* events into the SDK without the app having to reload.
|
|
43
|
+
*
|
|
44
|
+
* @param listener - Called with no arguments whenever auth state changes
|
|
45
|
+
* @returns An unsubscribe function the provider calls on cleanup
|
|
46
|
+
*/
|
|
47
|
+
subscribe?: (listener: () => void) => () => void;
|
|
35
48
|
}
|
|
36
49
|
/**
|
|
37
50
|
* Default auth adapter that only checks localStorage
|
package/dist/adapters/auth.d.ts
CHANGED
|
@@ -32,6 +32,19 @@ interface AuthAdapter {
|
|
|
32
32
|
* return null and let the caller decide how to handle unauthenticated requests.
|
|
33
33
|
*/
|
|
34
34
|
getUserId: () => Promise<string | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Subscribe to auth-state changes. Optional.
|
|
37
|
+
*
|
|
38
|
+
* When provided, `SolvaPayProvider` calls `subscribe` on mount and
|
|
39
|
+
* triggers `detectAuth` every time the listener fires instead of
|
|
40
|
+
* only running `detectAuth` once. This is how reactive adapters such
|
|
41
|
+
* as `@solvapay/react-supabase` push sign-in / sign-out / token-refresh
|
|
42
|
+
* events into the SDK without the app having to reload.
|
|
43
|
+
*
|
|
44
|
+
* @param listener - Called with no arguments whenever auth state changes
|
|
45
|
+
* @returns An unsubscribe function the provider calls on cleanup
|
|
46
|
+
*/
|
|
47
|
+
subscribe?: (listener: () => void) => () => void;
|
|
35
48
|
}
|
|
36
49
|
/**
|
|
37
50
|
* Default auth adapter that only checks localStorage
|