@solvapay/react 1.0.7 → 1.0.8-preview.10
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/CONTRIBUTING.md +117 -0
- package/README.md +234 -1
- package/dist/CancelPlanButton-CieT9swn.d.cts +1102 -0
- package/dist/CancelPlanButton-f56UlQN-.d.ts +1102 -0
- package/dist/chunk-MOP3ZBGC.js +4749 -0
- package/dist/index.cjs +4393 -1036
- package/dist/index.d.cts +375 -652
- package/dist/index.d.ts +375 -652
- package/dist/index.js +570 -1665
- package/dist/primitives/index.cjs +4254 -0
- package/dist/primitives/index.d.cts +533 -0
- package/dist/primitives/index.d.ts +533 -0
- package/dist/primitives/index.js +187 -0
- package/dist/styles.css +869 -0
- package/package.json +20 -5
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Contributing to @solvapay/react
|
|
2
|
+
|
|
3
|
+
This package ships two tiers:
|
|
4
|
+
|
|
5
|
+
- **`@solvapay/react`** — default tree. Drop-in components (`CheckoutLayout`, `PaymentForm`, …) that render a golden-path UI. Internally, they are thin shims over the primitives.
|
|
6
|
+
- **`@solvapay/react/primitives`** — unstyled compound primitives. Consumers compose these to build fully custom UIs.
|
|
7
|
+
|
|
8
|
+
This file is the canonical contract for primitives until the cross-repo Mintlify docs land. Every primitive you add or edit MUST follow these rules.
|
|
9
|
+
|
|
10
|
+
## Primitive contract
|
|
11
|
+
|
|
12
|
+
### Compound API
|
|
13
|
+
|
|
14
|
+
Every primitive is a compound: a `Root` plus named subcomponents.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<PlanSelector.Root>
|
|
18
|
+
<PlanSelector.Heading />
|
|
19
|
+
<PlanSelector.Grid>
|
|
20
|
+
<PlanSelector.Card>
|
|
21
|
+
<PlanSelector.CardName />
|
|
22
|
+
<PlanSelector.CardPrice />
|
|
23
|
+
</PlanSelector.Card>
|
|
24
|
+
</PlanSelector.Grid>
|
|
25
|
+
</PlanSelector.Root>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Export subcomponents as properties on the `Root` (`PlanSelector.Card`) **and** as named exports from `./primitives` for tree-shake-friendly use.
|
|
29
|
+
- No render-prop children. No `classNames` prop. No `unstyled` flag.
|
|
30
|
+
|
|
31
|
+
### `asChild`
|
|
32
|
+
|
|
33
|
+
Every leaf subcomponent accepts `asChild?: boolean`. When `true`, it renders its single child via `<Slot>` and merges props/ref onto the child.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<PlanSelector.Card asChild>
|
|
37
|
+
<Card>{/* shadcn/ui card */}</Card>
|
|
38
|
+
</PlanSelector.Card>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use `Slot` from `./primitives/slot`. Refs are merged via `composeRefs`; event handlers are chained via `composeEventHandlers` (consumer runs first; `preventDefault()` skips the primitive handler).
|
|
42
|
+
|
|
43
|
+
### `data-state` vocabulary
|
|
44
|
+
|
|
45
|
+
Every stateful subcomponent emits a `data-state` attribute from a fixed, documented vocabulary. States are driven by context (`PlanSelectionContext`, `PaymentFormContext`), never by subcomponent-local state.
|
|
46
|
+
|
|
47
|
+
| Primitive | `data-state` values |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `PlanSelector.Card` | `idle` \| `selected` \| `current` \| `disabled` |
|
|
50
|
+
| `PaymentForm.SubmitButton` | `idle` \| `processing` \| `disabled` |
|
|
51
|
+
| `ActivationFlow.Root` | `summary` \| `activating` \| `selectAmount` \| `topupPayment` \| `retrying` \| `activated` \| `error` |
|
|
52
|
+
| `CreditGate.Root` | `allowed` \| `blocked` \| `loading` |
|
|
53
|
+
| `PurchaseGate.Root` | `allowed` \| `blocked` \| `loading` |
|
|
54
|
+
| `CancelledPlanNotice.Root` | `active` \| `expired` |
|
|
55
|
+
| `CancelPlanButton` | `idle` \| `cancelling` |
|
|
56
|
+
| `BalanceBadge` | `loading` \| `zero` \| `low` \| `ok` |
|
|
57
|
+
| `AmountPicker.Option` | `idle` \| `selected` \| `disabled` |
|
|
58
|
+
|
|
59
|
+
Secondary flags use `data-<flag>` (e.g. `data-free`, `data-popular`, `data-has-reason`).
|
|
60
|
+
|
|
61
|
+
### Opaque selectors
|
|
62
|
+
|
|
63
|
+
Every primitive root emits `data-solvapay-<primitive>` (and subcomponents emit `data-solvapay-<primitive>-<part>` when they need to be targetable). Do not rely on internal class names; those are not a public API.
|
|
64
|
+
|
|
65
|
+
### `.Loading` and `.Error`
|
|
66
|
+
|
|
67
|
+
Every async primitive exposes `.Loading` and `.Error` subcomponents that render when the context reports loading/error. They render `null` otherwise. Always provide a fallback UI path — don't rely on consumers remembering to add them.
|
|
68
|
+
|
|
69
|
+
### Errors self-heal
|
|
70
|
+
|
|
71
|
+
Throw structured errors from `src/utils/errors.ts` for wiring mistakes:
|
|
72
|
+
|
|
73
|
+
- `MissingProviderError` — primitive rendered outside `<SolvaPayProvider>`.
|
|
74
|
+
- `MissingEnvVarError` — required env var not set.
|
|
75
|
+
- `MissingApiRouteError` — backend route not installed.
|
|
76
|
+
- `MissingProductRefError` — primitive needs `productRef` and none was provided.
|
|
77
|
+
|
|
78
|
+
Each error includes `code`, `message` (fix + docs URL), and `docsUrl`.
|
|
79
|
+
|
|
80
|
+
## Prohibited patterns
|
|
81
|
+
|
|
82
|
+
When touching a primitive, these must stay gone:
|
|
83
|
+
|
|
84
|
+
- `classNames` prop, `unstyled` prop, `RenderArgs` / function-child overloads.
|
|
85
|
+
- Inline `style={{ ... }}` inside `src/primitives/**`.
|
|
86
|
+
- `next/*` imports (the package must bundle on Vite/Remix).
|
|
87
|
+
- `as any`, `as unknown as ...` except when unavoidable; justify with a comment.
|
|
88
|
+
- Implicit `enum`; use union types.
|
|
89
|
+
|
|
90
|
+
## TDD checklist per primitive
|
|
91
|
+
|
|
92
|
+
Write the primitive and its test file in the same commit. Every primitive test covers:
|
|
93
|
+
|
|
94
|
+
1. **Default render** — subcomponents render the expected DOM.
|
|
95
|
+
2. **`asChild` composition** — refs merge, handlers chain, `data-*` + `aria-*` forward to the child element.
|
|
96
|
+
3. **`data-state` transitions** — context-driven state flips the attribute correctly.
|
|
97
|
+
4. **`.Loading` / `.Error`** — rendered when context signals loading/error.
|
|
98
|
+
5. **Provider-missing + env-missing** — the correct structured error class is thrown with its docs URL.
|
|
99
|
+
|
|
100
|
+
Delete legacy tests that assert on `classNames` / function-child / `RenderArgs` **before** landing the rewrite.
|
|
101
|
+
|
|
102
|
+
## Adding a new primitive (checklist)
|
|
103
|
+
|
|
104
|
+
1. Write the test file following the TDD checklist above.
|
|
105
|
+
2. Create `src/primitives/<Name>.tsx` with `Root` + subcomponents + `asChild` + `data-state`.
|
|
106
|
+
3. Re-export from `src/primitives/index.ts`.
|
|
107
|
+
4. If the primitive is part of the default tree, update the shim in `src/components/<Name>.tsx` to render it with the golden-path children.
|
|
108
|
+
5. Update the `data-state` table in this file if you introduce a new state.
|
|
109
|
+
6. Keep the package-local README cheat sheet in sync.
|
|
110
|
+
|
|
111
|
+
## Commands
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pnpm --filter @solvapay/react test # run the primitive + component tests
|
|
115
|
+
pnpm --filter @solvapay/react build # tsup build (top-level + primitives entry)
|
|
116
|
+
pnpm --filter @solvapay/react lint # eslint
|
|
117
|
+
```
|
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
|
|
|
@@ -316,9 +436,122 @@ const {
|
|
|
316
436
|
All components and hooks are fully typed. Import types as needed:
|
|
317
437
|
|
|
318
438
|
```tsx
|
|
319
|
-
import type {
|
|
439
|
+
import type {
|
|
440
|
+
PaymentFormProps,
|
|
441
|
+
PurchaseStatus,
|
|
442
|
+
PaymentIntentResult,
|
|
443
|
+
CheckoutResult,
|
|
444
|
+
PaymentResult,
|
|
445
|
+
ActivationResult,
|
|
446
|
+
} from '@solvapay/react'
|
|
320
447
|
```
|
|
321
448
|
|
|
449
|
+
## Plan lifecycle
|
|
450
|
+
|
|
451
|
+
Beyond checkout itself, three thin styled-default components cover the
|
|
452
|
+
post-purchase experience without requiring custom UI.
|
|
453
|
+
|
|
454
|
+
### `<CancelPlanButton>`
|
|
455
|
+
|
|
456
|
+
Wraps `usePurchaseActions.cancelRenewal` with a built-in confirm dialog,
|
|
457
|
+
loading state, and plan-type-aware copy (subscription vs usage-based).
|
|
458
|
+
Auto-reads the active purchase from `usePurchase()`.
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
import { CancelPlanButton } from '@solvapay/react'
|
|
462
|
+
|
|
463
|
+
<CancelPlanButton onCancelled={() => router.push('/')} />
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Use `confirm={false}` for a single-click cancel, or pass a string to override
|
|
467
|
+
the default copy. The render-prop form exposes `{ cancel, isCancelling,
|
|
468
|
+
disabled, purchase }` for fully custom UI.
|
|
469
|
+
|
|
470
|
+
### `<CancelledPlanNotice>`
|
|
471
|
+
|
|
472
|
+
Surfaces automatically when the customer has a cancelled-but-still-active
|
|
473
|
+
purchase. Renders the expiration date, days remaining, cancellation reason,
|
|
474
|
+
and a reactivate CTA. Renders nothing when there's nothing to show.
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
import { CancelledPlanNotice } from '@solvapay/react'
|
|
478
|
+
|
|
479
|
+
<CancelledPlanNotice onReactivated={() => refetch()} />
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### `<CreditGate>`
|
|
483
|
+
|
|
484
|
+
Companion to `<PurchaseGate>` for usage-based flows. Blocks access when the
|
|
485
|
+
customer's credit balance falls below a threshold; renders an embedded
|
|
486
|
+
`<TopupForm>` by default.
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import { CreditGate } from '@solvapay/react'
|
|
490
|
+
|
|
491
|
+
<CreditGate minCredits={10}>
|
|
492
|
+
<ExpensiveFeature />
|
|
493
|
+
</CreditGate>
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Customize via the `fallback` prop, or use the render-prop form
|
|
497
|
+
(`children: ({ balance, hasCredits, topup }) => …`) for fully custom UI.
|
|
498
|
+
|
|
499
|
+
## Server-side usage tracking
|
|
500
|
+
|
|
501
|
+
There is intentionally no `useTrackUsage()` client hook. Client-reported usage
|
|
502
|
+
is trivially gamed — a user can simply block the fetch to conserve credits.
|
|
503
|
+
Instead, record usage from your server when the expensive work actually runs:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
// app/api/do-thing/route.ts
|
|
507
|
+
import { trackUsage } from '@solvapay/next'
|
|
508
|
+
|
|
509
|
+
export async function POST(request: NextRequest) {
|
|
510
|
+
const result = await doTheExpensiveThing()
|
|
511
|
+
await trackUsage(request, { units: 1 })
|
|
512
|
+
return NextResponse.json(result)
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
The same pattern works from a Supabase Edge Function — see below.
|
|
517
|
+
|
|
518
|
+
## Using with Supabase Edge Functions
|
|
519
|
+
|
|
520
|
+
Lovable-style apps deploy React frontends against Supabase Edge Functions
|
|
521
|
+
(Deno runtime). The SDK's `api` URL overrides let the exact same
|
|
522
|
+
`<CheckoutLayout>` drop-in work against `/functions/v1/*` routes:
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
<SolvaPayProvider
|
|
526
|
+
config={{
|
|
527
|
+
api: {
|
|
528
|
+
checkPurchase: `${SUPABASE_URL}/functions/v1/check-purchase`,
|
|
529
|
+
createPayment: `${SUPABASE_URL}/functions/v1/create-payment-intent`,
|
|
530
|
+
processPayment: `${SUPABASE_URL}/functions/v1/process-payment`,
|
|
531
|
+
listPlans: `${SUPABASE_URL}/functions/v1/list-plans`,
|
|
532
|
+
getMerchant: `${SUPABASE_URL}/functions/v1/get-merchant`,
|
|
533
|
+
getProduct: `${SUPABASE_URL}/functions/v1/get-product`,
|
|
534
|
+
// …same pattern for every other endpoint
|
|
535
|
+
},
|
|
536
|
+
auth: {
|
|
537
|
+
adapter: createSupabaseAuthAdapter({ supabaseUrl, supabaseAnonKey }),
|
|
538
|
+
},
|
|
539
|
+
}}
|
|
540
|
+
>
|
|
541
|
+
<CheckoutLayout
|
|
542
|
+
productRef="prd_myapi"
|
|
543
|
+
prefillCustomer={{ email }}
|
|
544
|
+
requireTermsAcceptance
|
|
545
|
+
/>
|
|
546
|
+
</SolvaPayProvider>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
The matching Deno edge functions ship in
|
|
550
|
+
[`@solvapay/supabase`](../../packages/supabase) — see
|
|
551
|
+
[`examples/supabase-edge`](../../examples/supabase-edge) for one-liner
|
|
552
|
+
`Deno.serve(handler)` files covering every handler including the new
|
|
553
|
+
`get-merchant` and `get-product` endpoints.
|
|
554
|
+
|
|
322
555
|
## More Information
|
|
323
556
|
|
|
324
557
|
See [`docs/contributing/architecture.md`](../../docs/contributing/architecture.md) for contributor
|