@solvapay/react 1.0.8-preview.9 → 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 ADDED
@@ -0,0 +1,209 @@
1
+ # @solvapay/react changelog
2
+
3
+ From `1.0.10` onwards this changelog is generated by
4
+ [changesets](https://github.com/changesets/changesets) — prior entries
5
+ are maintained by hand and are grouped by the first preview / stable
6
+ release that contains them.
7
+
8
+ ## Unreleased
9
+
10
+ ## 1.0.9 — peer-dep rename: `@solvapay/mcp` → `@solvapay/mcp-core`
11
+
12
+ The optional peer dependency that guards `@solvapay/react/mcp` types was
13
+ renamed from `@solvapay/mcp` → `@solvapay/mcp-core` to match the
14
+ reshuffled SolvaPay MCP package layout (framework-neutral contracts
15
+ now live in `@solvapay/mcp-core`; the official
16
+ `@modelcontextprotocol/sdk` adapter is `@solvapay/mcp`). No source
17
+ changes — this is a rename of the `peerDependencies` /
18
+ `peerDependenciesMeta` contract line only.
19
+
20
+ Integrators who install the optional peer explicitly:
21
+
22
+ ```diff
23
+ - pnpm add @solvapay/mcp
24
+ + pnpm add @solvapay/mcp-core
25
+ ```
26
+
27
+ Integrators who don't use `@solvapay/react/mcp` are unaffected.
28
+
29
+ ### `@solvapay/react/mcp` — per-view primitives + `<McpApp>` compound (minor, additive)
30
+
31
+ The MCP App reference views (`checkout`, `account`, `topup`, `activate`) that
32
+ previously lived in `examples/mcp-checkout-app` have been lifted into
33
+ `@solvapay/react/mcp` as four composable primitives plus a thin compound
34
+ wrapper:
35
+
36
+ ```tsx
37
+ import '@solvapay/react/styles.css'
38
+ import '@solvapay/react/mcp/styles.css'
39
+ import { App } from '@modelcontextprotocol/ext-apps'
40
+ import { McpApp } from '@solvapay/react/mcp'
41
+
42
+ const app = new App({ name: 'my-mcp-app', version: '1.0.0' })
43
+ createRoot(rootEl).render(<McpApp app={app} />)
44
+ ```
45
+
46
+ Integrators who want a custom shell compose the primitives directly:
47
+
48
+ ```tsx
49
+ import {
50
+ McpCheckoutView,
51
+ McpAccountView,
52
+ McpTopupView,
53
+ McpActivateView,
54
+ McpViewRouter,
55
+ fetchMcpBootstrap,
56
+ createMcpFetch,
57
+ createMcpAppAdapter,
58
+ useStripeProbe,
59
+ } from '@solvapay/react/mcp'
60
+ ```
61
+
62
+ Each view accepts a `classNames?: McpViewClassNames` partial for per-slot
63
+ overrides; defaults render the `solvapay-mcp-*` classes from the new
64
+ `@solvapay/react/mcp/styles.css` stylesheet. `<McpApp>` also takes a `views`
65
+ map for per-screen component overrides without losing routing.
66
+
67
+ ### Seam fixes on primitives that the MCP views needed
68
+
69
+ Three additive primitive changes landed alongside the lift:
70
+
71
+ - **`<AmountPicker.Root emit="minor">`** — when set, `onChange` and
72
+ `Confirm.onConfirm` deliver the amount in minor units (respects
73
+ zero-decimal currencies like JPY). Default stays `'major'` for
74
+ back-compat. `useAmountPicker().resolvedAmountMinor` is a new sibling to
75
+ `resolvedAmount` for custom layouts. A new
76
+ `getMinorUnitsPerMajor(currency)` util is exported from
77
+ `@solvapay/react` for integrators doing conversions outside the picker.
78
+ - **`<AmountPicker.Root selector={…}>`** — accepts an externally-owned
79
+ `UseTopupAmountSelectorReturn`, letting parent flows share state with
80
+ the inner picker. `ActivationFlow.AmountPicker` now threads the flow's
81
+ selector through this prop, so amounts picked in the sub-picker feed
82
+ straight into `useActivation().retry()` without the old workaround.
83
+ - **`<LaunchCustomerPortalButton asChild>`** — renders the ready-state
84
+ anchor via `Slot` so consumers can substitute a real `<button>` inside
85
+ an anchor wrapper (matches the `ActivationFlow.ActivateButton`
86
+ convention). Loading / error fallback buttons are untouched.
87
+
88
+ All three are additive — no consumer API moves or renames.
89
+
90
+ ### Plans vs balance transactions (minor, behavioural filtering)
91
+
92
+ Credit top-ups now surface as balance transactions, not plans. `PurchaseInfo`
93
+ gains a `metadata?: Record<string, unknown>` field, and two new utilities —
94
+ `isPlanPurchase` / `isTopupPurchase` — classify structurally from
95
+ `planSnapshot`. The classification is applied inside `usePurchase()` so every
96
+ plan-shaped accessor is consistent:
97
+
98
+ - `activePurchase`, `activePaidPurchase`, `hasPaidPurchase`, `hasProduct`,
99
+ and `cancelledPurchase` / `shouldShowCancelledNotice` (on
100
+ `usePurchaseStatus`) all skip balance transactions.
101
+ - A new `balanceTransactions: PurchaseInfo[]` accessor on `usePurchase()`
102
+ returns the complement.
103
+ - `purchases` (raw) is unchanged — the full ordering is still available for
104
+ integrators that classify themselves.
105
+
106
+ `PurchaseInfo.planSnapshot.name` is now surfaced as a first-class field so
107
+ `<CurrentPlanCard>` renders a real plan name ("Pro Monthly") instead of the
108
+ opaque `planRef`. Legacy purchases without a snapshot name fall back to
109
+ `productName`. The `planRef` is retained only as `data-solvapay-current-plan-ref`
110
+ on the card root for QA hooks.
111
+
112
+ `<CurrentPlanCard>` also uses `copy.currentPlan.cycleUnit` to render
113
+ "500 kr / month" rather than "500 kr / monthly". Override any of
114
+ `weekly` / `monthly` / `quarterly` / `yearly` in your copy bundle to localise
115
+ the interval label.
116
+
117
+ Behavioural note for integrators who previously relied on top-ups surfacing on
118
+ `activePurchase`: drop to `purchases` (raw) to restore the old most-recent
119
+ ordering, or read from `balanceTransactions` directly.
120
+
121
+ ### BREAKING: Unified `transport` replaces per-method provider overrides
122
+
123
+ `SolvaPayProvider` now takes a single `config.transport: SolvaPayTransport`
124
+ prop. The old per-method `SolvaPayProviderProps` overrides have been removed:
125
+
126
+ - `checkPurchase`
127
+ - `createPayment`
128
+ - `processPayment`
129
+ - `createTopupPayment`
130
+
131
+ Consumers passing any of those props must migrate to `config.transport`.
132
+
133
+ **Before:**
134
+
135
+ ```tsx
136
+ <SolvaPayProvider
137
+ checkPurchase={myCheckPurchase}
138
+ createPayment={myCreatePayment}
139
+ processPayment={myProcessPayment}
140
+ createTopupPayment={myCreateTopupPayment}
141
+ >
142
+ {children}
143
+ </SolvaPayProvider>
144
+ ```
145
+
146
+ **After:**
147
+
148
+ ```tsx
149
+ const transport: SolvaPayTransport = {
150
+ checkPurchase: myCheckPurchase,
151
+ createPayment: myCreatePayment,
152
+ processPayment: myProcessPayment,
153
+ createTopupPayment: myCreateTopupPayment,
154
+ // ...plus every other required method; see types/transport.ts
155
+ }
156
+
157
+ <SolvaPayProvider config={{ transport }}>{children}</SolvaPayProvider>
158
+ ```
159
+
160
+ HTTP consumers who never set those overrides are unaffected — the provider
161
+ falls back to `createHttpTransport(config)` automatically.
162
+
163
+ ### Added: `@solvapay/react/mcp` subpath
164
+
165
+ Ships `createMcpAppAdapter(app)` which returns a `SolvaPayTransport` that
166
+ tunnels every data call through `app.callServerTool` instead of HTTP. Use it
167
+ when hosting the React tree inside an MCP App, where direct HTTP to your
168
+ backend is blocked by the host sandbox:
169
+
170
+ ```tsx
171
+ import { createMcpAppAdapter } from '@solvapay/react/mcp'
172
+ <SolvaPayProvider config={{ transport: createMcpAppAdapter(app) }}>
173
+ {children}
174
+ </SolvaPayProvider>
175
+ ```
176
+
177
+ See the [MCP App integration guide](https://docs.solvapay.com/sdks/typescript/guides/mcp-app)
178
+ for the full walkthrough. Re-exports `MCP_TOOL_NAMES` so client and server
179
+ share a single source of truth for tool names.
180
+
181
+ ### Added: account management components
182
+
183
+ - `<CurrentPlanCard>` — plan-type-aware summary card (recurring / one-time /
184
+ usage-based), payment-method display, Update card + Cancel plan actions.
185
+ Returns `null` when there is no active purchase.
186
+ - `<LaunchCustomerPortalButton>` — pre-fetches `createCustomerSession` on
187
+ mount and renders a real `<a target="_blank">` anchor (MCP sandbox-safe).
188
+ - `<UpdatePaymentMethodButton mode="portal">` — thin wrapper around
189
+ `<LaunchCustomerPortalButton>`. The `mode` prop reserves space for a
190
+ future `"inline"` value without breaking the API.
191
+
192
+ ### Added: `usePaymentMethod` hook
193
+
194
+ Fetches the customer's default payment method through
195
+ `transport.getPaymentMethod()`. Transport-keyed single-flight cache, mirrors
196
+ the `useMerchant` pattern. Returns `null` on error (graceful hide, no
197
+ `onError` re-emit — the HTTP transport already invokes `config.onError`
198
+ before throwing).
199
+
200
+ ### Fixed
201
+
202
+ - `SolvaPayProvider` no longer flips `loading: true` for polling refetches
203
+ when the customer has no purchases yet. `usePurchase` now exposes a
204
+ separate `isRefetching` flag for background polls; gate initial skeletons
205
+ on `loading` and render subtle refresh indicators on `isRefetching`.
206
+ - `LaunchCustomerPortalButton` captures `onError` via ref so parents passing
207
+ an inline arrow no longer re-fire the pre-fetch effect on every render.
208
+ - `useMerchant`, `useProduct`, and `usePaymentMethod` no longer double-call
209
+ `config.onError` — the HTTP transport already calls it before throwing.
package/README.md CHANGED
@@ -182,12 +182,10 @@ export default function App() {
182
182
  ```tsx
183
183
  import { SolvaPayProvider } from '@solvapay/react'
184
184
  import { createSupabaseAuthAdapter } from '@solvapay/react-supabase'
185
+ import { supabase } from './lib/supabase'
185
186
 
186
187
  export default function App() {
187
- const adapter = createSupabaseAuthAdapter({
188
- supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
189
- supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
190
- })
188
+ const adapter = createSupabaseAuthAdapter({ client: supabase })
191
189
 
192
190
  return (
193
191
  <SolvaPayProvider config={{ auth: { adapter } }}>
@@ -199,32 +197,175 @@ export default function App() {
199
197
 
200
198
  ### Fully Custom Implementation
201
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
+
202
203
  ```tsx
203
- import { SolvaPayProvider } from '@solvapay/react'
204
+ import { SolvaPayProvider, createHttpTransport } from '@solvapay/react'
204
205
 
205
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
+
206
224
  return (
207
- <SolvaPayProvider
208
- createPayment={async ({ planRef, productRef }) => {
209
- const res = await fetch('/api/custom/payment', {
210
- method: 'POST',
211
- body: JSON.stringify({ planRef, productRef }),
212
- })
213
- if (!res.ok) throw new Error('Failed to create payment')
214
- return res.json()
215
- }}
216
- checkPurchase={async customerRef => {
217
- const res = await fetch(`/api/custom/purchase?customerRef=${customerRef}`)
218
- if (!res.ok) throw new Error('Failed to check purchase')
219
- return res.json()
220
- }}
221
- >
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 }}>
222
248
  <CheckoutPage />
223
249
  </SolvaPayProvider>
224
250
  )
225
251
  }
226
252
  ```
227
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
+
228
369
  ## Components
229
370
 
230
371
  ### SolvaPayProvider
@@ -371,14 +512,42 @@ Access purchase status, active purchases, and helper functions.
371
512
 
372
513
  ```tsx
373
514
  const {
374
- 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
375
517
  loading, // Loading state
376
- hasPaidPurchase, // Boolean: has any paid purchase
377
- activePurchase, // Most recent active purchase
518
+ hasPaidPurchase, // Boolean: has any paid plan purchase
519
+ activePurchase, // Most recent active plan purchase
378
520
  refetch, // Function to refetch purchases
379
521
  } = usePurchase()
380
522
  ```
381
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
+
382
551
  ### usePlans
383
552
 
384
553
  Fetch and manage available plans.
@@ -534,7 +703,7 @@ Lovable-style apps deploy React frontends against Supabase Edge Functions
534
703
  // …same pattern for every other endpoint
535
704
  },
536
705
  auth: {
537
- adapter: createSupabaseAuthAdapter({ supabaseUrl, supabaseAnonKey }),
706
+ adapter: createSupabaseAuthAdapter({ client: supabase }),
538
707
  },
539
708
  }}
540
709
  >
@@ -547,7 +716,7 @@ Lovable-style apps deploy React frontends against Supabase Edge Functions
547
716
  ```
548
717
 
549
718
  The matching Deno edge functions ship in
550
- [`@solvapay/supabase`](../../packages/supabase) — see
719
+ [`@solvapay/fetch`](../../packages/fetch) — see
551
720
  [`examples/supabase-edge`](../../examples/supabase-edge) for one-liner
552
721
  `Deno.serve(handler)` files covering every handler including the new
553
722
  `get-merchant` and `get-product` endpoints.
@@ -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
@@ -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
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  defaultAuthAdapter
3
3
  } from "../chunk-OUSEQRCT.js";
4
+ import "../chunk-MLKGABMK.js";
4
5
  export {
5
6
  defaultAuthAdapter
6
7
  };