@usethrottle/auth 0.1.1 → 0.2.0
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/CLAUDE.md +25 -0
- package/README.md +307 -1
- package/dist/chunk-AIXBUDOT.js +50 -0
- package/dist/chunk-AIXBUDOT.js.map +1 -0
- package/dist/chunk-HGBAIM6B.js +140 -0
- package/dist/chunk-HGBAIM6B.js.map +1 -0
- package/dist/{chunk-OYNGTNOS.js → chunk-SZ427BQ3.js} +54 -6
- package/dist/chunk-SZ427BQ3.js.map +1 -0
- package/dist/index.cjs +9 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +377 -8
- package/dist/index.js.map +1 -1
- package/dist/react/components/index.cjs +4558 -0
- package/dist/react/components/index.cjs.map +1 -0
- package/dist/react/components/index.d.cts +735 -0
- package/dist/react/components/index.d.ts +735 -0
- package/dist/react/components/index.js +3918 -0
- package/dist/react/components/index.js.map +1 -0
- package/dist/react/forms/index.cjs +3 -0
- package/dist/react/forms/index.cjs.map +1 -1
- package/dist/react/forms/index.js +5 -46
- package/dist/react/forms/index.js.map +1 -1
- package/dist/react/index.cjs +22 -4
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +278 -3
- package/dist/react/index.d.ts +278 -3
- package/dist/react/index.js +17 -122
- package/dist/react/index.js.map +1 -1
- package/dist/styles.css +422 -0
- package/package.json +5 -2
- package/dist/chunk-IJ3DNMNG.js +0 -49
- package/dist/chunk-IJ3DNMNG.js.map +0 -1
- package/dist/chunk-OYNGTNOS.js.map +0 -1
- package/dist/chunk-ZMNCURDE.js +0 -1
- package/dist/chunk-ZMNCURDE.js.map +0 -1
package/CLAUDE.md
CHANGED
|
@@ -36,6 +36,31 @@ points: `.` (core), `./react`, `./forms` — see `exports` in `package.json`.
|
|
|
36
36
|
empty `useOrders`/`useAddresses` results by adding client-side workarounds;
|
|
37
37
|
see the README's "Email verification and the claim cutoff" section.
|
|
38
38
|
|
|
39
|
+
## Components layer (`src/react/components/`, `./components` entry)
|
|
40
|
+
|
|
41
|
+
- **Step-up interception already exists in the core.** `client.ts:168`
|
|
42
|
+
intercepts `403 step_up_required` and calls `onStepUpRequired()` before
|
|
43
|
+
retrying the call once. `ThrottleProvider` supplies that callback (it opens
|
|
44
|
+
`StepUpDialog` and resolves the promise from the dialog's submit/cancel) —
|
|
45
|
+
it does not, and must not, re-implement the interception itself.
|
|
46
|
+
- **`ThrottleProvider` composes `ThrottleAuthProvider`, it does not replace
|
|
47
|
+
it.** It builds the `ThrottleAuth` client itself (so it can pass
|
|
48
|
+
`onStepUpRequired` at construction time) and hands that client to
|
|
49
|
+
`ThrottleAuthProvider` via the `auth` injection prop that already exists for
|
|
50
|
+
this purpose. Every hook from `./react` (`useAuth`, `useCustomer`,
|
|
51
|
+
`useOrders`, …) keeps working underneath unchanged.
|
|
52
|
+
- **Every rule in `styles.ts` must stay a single-class selector.** A merchant
|
|
53
|
+
className from `appearance.elements` lands on the same element as the
|
|
54
|
+
shipped class, so an override wins only via equal specificity + source
|
|
55
|
+
order. A descendant selector (e.g. `.throttle-card .throttle-button`) would
|
|
56
|
+
silently outrank a merchant's override no matter what class they add.
|
|
57
|
+
- **`THROTTLE_ELEMENT_KEYS` (in `appearance.tsx`) is public API.** It is the
|
|
58
|
+
exhaustive list of every `data-throttle-element` a component may render.
|
|
59
|
+
Adding a rendered element without adding its key here passes typecheck and
|
|
60
|
+
build but fails `element-keys.test.tsx`, which mounts a sweep of components
|
|
61
|
+
and asserts every `data-throttle-element` found is in the list — the element
|
|
62
|
+
would otherwise be unreachable through `appearance.elements` for merchants.
|
|
63
|
+
|
|
39
64
|
## Conventions
|
|
40
65
|
|
|
41
66
|
- Every exported function's public surface is asserted in `src/exports.test.ts`
|
package/README.md
CHANGED
|
@@ -82,6 +82,11 @@ framework adapter would use to re-render.
|
|
|
82
82
|
|
|
83
83
|
## API reference
|
|
84
84
|
|
|
85
|
+
Two objects. `ThrottleAuth` owns the session and the buyer's credentials;
|
|
86
|
+
`ThrottleAccount` owns the buyer's data.
|
|
87
|
+
|
|
88
|
+
### `ThrottleAuth` — session and credentials
|
|
89
|
+
|
|
85
90
|
Every method on the `ThrottleAuth` returned by `createThrottleAuth()` (also
|
|
86
91
|
reachable as `useAuth().auth` in React):
|
|
87
92
|
|
|
@@ -102,7 +107,7 @@ reachable as `useAuth().auth` in React):
|
|
|
102
107
|
| `resetPassword(token: string, password: string): Promise<void>` | Consumes a one-time reset-link token and sets a new password; revokes every session, this one included. |
|
|
103
108
|
| `changePassword(password: string): Promise<void>` | Changes the signed-in buyer's password (requires step-up). |
|
|
104
109
|
| `listSessions(): Promise<DeviceSession[]>` | Lists this buyer's active device sessions. |
|
|
105
|
-
| `revokeSession(id: string): Promise<void>` | Revokes one device session by id
|
|
110
|
+
| `revokeSession(id: string): Promise<void>` | Revokes one device session by id; revoking the current session signs this device out. |
|
|
106
111
|
|
|
107
112
|
```ts
|
|
108
113
|
interface DeviceSession {
|
|
@@ -115,6 +120,49 @@ interface DeviceSession {
|
|
|
115
120
|
}
|
|
116
121
|
```
|
|
117
122
|
|
|
123
|
+
### `ThrottleAccount` — the buyer's data
|
|
124
|
+
|
|
125
|
+
Every method on the `ThrottleAccount` returned by `createAccount(auth)` (also
|
|
126
|
+
reachable as `useAuth().account` in React). The React hooks in
|
|
127
|
+
[Account pages](#account-pages) wrap these; use these directly from the
|
|
128
|
+
headless core, or anywhere you want the call without the hook.
|
|
129
|
+
|
|
130
|
+
| Method | Calls | Description |
|
|
131
|
+
| ------ | ----- | ----------- |
|
|
132
|
+
| `getProfile(): Promise<Customer>` | `GET /me` | The signed-in buyer. While unverified after a claim, only `id` and `email` are populated — see [the claim cutoff](#email-verification-and-the-claim-cutoff). |
|
|
133
|
+
| `updateProfile(patch: ProfilePatch): Promise<Customer>` | `PATCH /me` | Name, phone, company, marketing opt-in. 403s while a claim cutoff is active. |
|
|
134
|
+
| `listAddresses(): Promise<Address[]>` | `GET /me/addresses` | All of the buyer's addresses. |
|
|
135
|
+
| `createAddress(input: AddressInput): Promise<Address>` | `POST /me/addresses` | `addressLine1`, `city` and `countryCode` are required. |
|
|
136
|
+
| `updateAddress(id, patch: Partial<AddressInput>): Promise<Address>` | `PATCH /me/addresses/:id` | 404s for an id that is not this buyer's. |
|
|
137
|
+
| `deleteAddress(id): Promise<void>` | `DELETE /me/addresses/:id` | |
|
|
138
|
+
| `listPaymentMethods(): Promise<PaymentMethod[]>` | `GET /me/payment-methods` | Requires a verified email. |
|
|
139
|
+
| `setDefaultPaymentMethod(id): Promise<PaymentMethod>` | `PATCH /me/payment-methods/:id` | How a buyer changes what they are billed on. |
|
|
140
|
+
| `deletePaymentMethod(id): Promise<void>` | `DELETE /me/payment-methods/:id` | Requires step-up. |
|
|
141
|
+
| `listOrders(page?: Page): Promise<Order[]>` | `GET /me/orders` | Cursor-paginated. |
|
|
142
|
+
| `getOrder(id): Promise<Order>` | `GET /me/orders/:id` | |
|
|
143
|
+
| `listInvoices(page?: Page): Promise<Invoice[]>` | `GET /me/invoices` | Cursor-paginated. |
|
|
144
|
+
| `getInvoice(id): Promise<Invoice>` | `GET /me/invoices/:id` | |
|
|
145
|
+
| `listSubscriptions(page?: Page): Promise<Subscription[]>` | `GET /me/subscriptions` | Cursor-paginated. |
|
|
146
|
+
| `getSubscription(id): Promise<Subscription>` | `GET /me/subscriptions/:id` | |
|
|
147
|
+
| `cancelSubscription(id, opts?: { atPeriodEnd?: boolean })` | `POST /me/subscriptions/:id/cancel` | Immediate by default; `atPeriodEnd: true` defers to the period end. Requires step-up. |
|
|
148
|
+
| `pauseSubscription(id): Promise<Subscription>` | `POST /me/subscriptions/:id/pause` | Requires step-up. |
|
|
149
|
+
| `resumeSubscription(id): Promise<Subscription>` | `POST /me/subscriptions/:id/resume` | Requires step-up. |
|
|
150
|
+
| `claimCart(cartId): Promise<{ cartId: string; customerId: string }>` | `POST /me/carts/:cartId/claim` | Attaches a guest cart to the signed-in buyer. Throws `cart_already_claimed` if it belongs to someone else. |
|
|
151
|
+
|
|
152
|
+
Paging is `{ cursor?: string; limit?: number }`; `limit` is 1-100 and defaults to
|
|
153
|
+
25. A list returns rows only — carry the last row's `id` as the next `cursor`.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const account = createAccount(auth);
|
|
157
|
+
|
|
158
|
+
const firstPage = await account.listOrders({ limit: 10 });
|
|
159
|
+
const nextPage = await account.listOrders({ limit: 10, cursor: firstPage.at(-1)?.id });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The row types — `Customer`, `Address`, `AddressInput`, `ProfilePatch`,
|
|
163
|
+
`PaymentMethod`, `Order`, `Invoice`, `Subscription` — are all exported from the
|
|
164
|
+
package root and mirror the API's responses field for field.
|
|
165
|
+
|
|
118
166
|
## Account pages
|
|
119
167
|
|
|
120
168
|
Six hooks, each a `{ data, isLoading, error, refetch }` resource plus the
|
|
@@ -141,6 +189,264 @@ function Orders() {
|
|
|
141
189
|
- `useSubscriptions(page?)` — `cancel(id, { atPeriodEnd })`, `pause(id)`,
|
|
142
190
|
`resume(id)`.
|
|
143
191
|
|
|
192
|
+
## Drop-in components
|
|
193
|
+
|
|
194
|
+
A third entry point, `@usethrottle/auth/components`, ships pre-styled React
|
|
195
|
+
components on top of everything above — a sign-in form, an account dropdown, a
|
|
196
|
+
full account dashboard — so most storefronts never touch `useAuth`/`useCustomer`
|
|
197
|
+
directly. It re-exports the whole `./react` hooks surface too, so this is the
|
|
198
|
+
only import path a component-using storefront needs.
|
|
199
|
+
|
|
200
|
+
### Quick start
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { ThrottleProvider, UserButton, SignInButton, SignedIn, SignedOut }
|
|
204
|
+
from '@usethrottle/auth/components';
|
|
205
|
+
|
|
206
|
+
export default function Layout({ children }) {
|
|
207
|
+
return (
|
|
208
|
+
<ThrottleProvider publishableKey={process.env.NEXT_PUBLIC_THROTTLE_PK}>
|
|
209
|
+
<nav>
|
|
210
|
+
<SignedIn><UserButton /></SignedIn>
|
|
211
|
+
<SignedOut><SignInButton /></SignedOut>
|
|
212
|
+
</nav>
|
|
213
|
+
{children}
|
|
214
|
+
</ThrottleProvider>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`<ThrottleProvider>` replaces `<ThrottleAuthProvider>` from `./react` — it
|
|
220
|
+
takes the same `publishableKey`/`baseUrl`/`storage`/`claimCartOnSignIn` config,
|
|
221
|
+
plus `appearance` and `localization` (below) and `injectStyles`. It composes
|
|
222
|
+
the existing provider rather than replacing it, so every hook from `./react`
|
|
223
|
+
keeps working underneath unchanged.
|
|
224
|
+
|
|
225
|
+
`<SignedIn>`, `<SignedOut>` and `<Protect requireVerified fallback={...}>` are
|
|
226
|
+
conditional-render guards — they render nothing while the session is still
|
|
227
|
+
loading, so a navbar never flashes the wrong state during session restore.
|
|
228
|
+
`<Protect>` additionally gates on `emailVerified` when `requireVerified` is set.
|
|
229
|
+
|
|
230
|
+
### Components
|
|
231
|
+
|
|
232
|
+
| Component | Purpose | Key props |
|
|
233
|
+
| --------- | ------- | --------- |
|
|
234
|
+
| `SignIn` | Email/password sign-in form. | `onSuccess()`, `onForgotPassword()`, `onSignUp()`, `appearance`, `localization` |
|
|
235
|
+
| `SignUp` | Registration form. | `onSuccess()`, `onSignIn()`, `appearance`, `localization` |
|
|
236
|
+
| `ForgotPassword` | Requests a reset-link email. | `onBack()`, `appearance`, `localization` |
|
|
237
|
+
| `ResetPassword` | Consumes a reset-link token, sets a new password. | `token`, `onSuccess()`, `appearance`, `localization` |
|
|
238
|
+
| `VerifyEmail` | Consumes a verification-link token, or offers a resend prompt with no token. | `token`, `onSuccess()`, `appearance`, `localization` |
|
|
239
|
+
| `AuthFlow` | Switches between all five auth views above, with optional hash-based routing. | `initialView` / `view` + `onViewChange` (controlled), `routing: 'virtual' \| 'hash'`, `afterSignIn()`, `afterSignUp()` |
|
|
240
|
+
| `AuthModal` | `AuthFlow` in an overlay. | `open`, `onClose()`, `initialView`, `afterSignIn()` |
|
|
241
|
+
| `SignInButton` | Opens `AuthModal` (`mode="modal"`, default) or links to a URL (`mode="redirect"`). | `mode`, `url`, `initialView`, `asChild`, `afterSignIn()` |
|
|
242
|
+
| `SignOutButton` | Signs out; `everywhere` revokes every session (may prompt step-up). | `everywhere`, `onSignedOut()` |
|
|
243
|
+
| `UserButton` | Signed-in identity dropdown: manage account, sign out. Renders nothing while signed out. | `mode: 'modal' \| 'navigate'`, `accountUrl`, `tabs`, `afterSignOut()` |
|
|
244
|
+
| `AccountDashboard` | Tabbed account shell composing all nine panels below. | `tabs` (defaults to all seven), `tab` / `onTabChange` (controlled), `routing: 'virtual' \| 'hash'` |
|
|
245
|
+
| `VerifyEmailBanner` | Persistent "verify your email" banner with a resend button. | `appearance`, `localization` |
|
|
246
|
+
| `ProfilePanel` | Name/phone/company/marketing-opt-in form. | `appearance`, `localization` |
|
|
247
|
+
| `AddressesPanel` | List, add, edit, delete, and set-default addresses. | `appearance`, `localization` |
|
|
248
|
+
| `PaymentMethodsPanel` | List saved cards, set default, remove. Read-only for adding — see Known limits. | `appearance`, `localization` |
|
|
249
|
+
| `OrdersPanel` | Order list; drills into `OrderDetail` internally. | `onViewSubscription(id)`, `appearance`, `localization` |
|
|
250
|
+
| `OrderDetail` | One order's line items, totals, and addresses. | `orderId`, `onBack()`, `onViewSubscription(id)`, `appearance`, `localization` |
|
|
251
|
+
| `InvoicesPanel` | Invoice list; drills into `InvoiceDetail` internally. | `appearance`, `localization` |
|
|
252
|
+
| `InvoiceDetail` | One invoice's totals and a PDF download button. | `appearance`, `localization` |
|
|
253
|
+
| `SubscriptionsPanel` | Subscription list: cancel (now or at period end), pause, resume. | `appearance`, `localization` |
|
|
254
|
+
| `SecurityPanel` | Change-password form and active-device session list. | `appearance`, `localization` |
|
|
255
|
+
|
|
256
|
+
Every panel behind the buyer plane's verified-only routes (`OrdersPanel`,
|
|
257
|
+
`InvoicesPanel`, `PaymentMethodsPanel`, `SubscriptionsPanel`) renders the
|
|
258
|
+
[claim-cutoff](#email-verification-and-the-claim-cutoff) empty state
|
|
259
|
+
automatically while `emailVerified` is `false` — you do not need to gate them
|
|
260
|
+
yourself, though `<Protect requireVerified>` is still useful for gating an
|
|
261
|
+
entire route. `AddressesPanel` is not claim-cutoff gated — the server does not
|
|
262
|
+
require a verified email to read or write addresses.
|
|
263
|
+
|
|
264
|
+
### Theming
|
|
265
|
+
|
|
266
|
+
Two independent knobs, both accepted as an `appearance` prop on
|
|
267
|
+
`ThrottleProvider` (applies to everything below it) or on any individual
|
|
268
|
+
component (merges with, and overrides, the provider's value — so a component
|
|
269
|
+
prop only needs to carry what's different):
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
<ThrottleProvider
|
|
273
|
+
publishableKey={pk}
|
|
274
|
+
appearance={{
|
|
275
|
+
theme: 'auto', // 'light' | 'dark' | 'auto' (follows prefers-color-scheme)
|
|
276
|
+
variables: {
|
|
277
|
+
colorPrimary: '#7c3aed',
|
|
278
|
+
colorPrimaryForeground: '#ffffff',
|
|
279
|
+
borderRadius: '6px',
|
|
280
|
+
fontFamily: 'Inter, system-ui, sans-serif',
|
|
281
|
+
},
|
|
282
|
+
elements: {
|
|
283
|
+
// key → className appended alongside the built-in class
|
|
284
|
+
'signIn.submitButton': 'my-brand-button',
|
|
285
|
+
card: 'my-brand-card',
|
|
286
|
+
},
|
|
287
|
+
components: {
|
|
288
|
+
// swap a primitive wholesale instead of just restyling it
|
|
289
|
+
Spinner: MyBrandSpinner,
|
|
290
|
+
},
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**`variables`** — CSS custom properties, set on the `.throttle-root` wrapper
|
|
296
|
+
every top-level component renders:
|
|
297
|
+
|
|
298
|
+
| Variable key | CSS custom property | Default (light) |
|
|
299
|
+
| ------------ | -------------------- | ---------------- |
|
|
300
|
+
| `colorPrimary` | `--throttle-color-primary` | `#18181b` |
|
|
301
|
+
| `colorPrimaryForeground` | `--throttle-color-primary-foreground` | `#ffffff` |
|
|
302
|
+
| `colorBackground` | `--throttle-color-background` | `#ffffff` |
|
|
303
|
+
| `colorSurface` | `--throttle-color-surface` | `#f8fafc` |
|
|
304
|
+
| `colorForeground` | `--throttle-color-foreground` | `#0e1116` |
|
|
305
|
+
| `colorMuted` | `--throttle-color-muted` | `#eceef2` |
|
|
306
|
+
| `colorMutedForeground` | `--throttle-color-muted-foreground` | `#5b6472` |
|
|
307
|
+
| `colorBorder` | `--throttle-color-border` | `#e2e8f0` |
|
|
308
|
+
| `colorDanger` | `--throttle-color-danger` | `#c2321f` |
|
|
309
|
+
| `colorDangerSurface` | `--throttle-color-danger-surface` | `#fef2f2` |
|
|
310
|
+
| `colorDangerBorder` | `--throttle-color-danger-border` | `#fecaca` |
|
|
311
|
+
| `colorSuccess` | `--throttle-color-success` | `#127a4a` |
|
|
312
|
+
| `colorSuccessSurface` | `--throttle-color-success-surface` | `#ecfdf5` |
|
|
313
|
+
| `colorSuccessBorder` | `--throttle-color-success-border` | `#a7f3d0` |
|
|
314
|
+
| `colorWarning` | `--throttle-color-warning` | `#92400e` |
|
|
315
|
+
| `colorWarningSurface` | `--throttle-color-warning-surface` | `#fffbeb` |
|
|
316
|
+
| `colorWarningBorder` | `--throttle-color-warning-border` | `#fde68a` |
|
|
317
|
+
| `fontFamily` | `--throttle-font-family` | system font stack |
|
|
318
|
+
| `fontSize` | `--throttle-font-size` | `16px` |
|
|
319
|
+
| `borderRadius` | `--throttle-radius` | `12px` |
|
|
320
|
+
| `borderWidth` | `--throttle-border-width` | `1px` |
|
|
321
|
+
| `spacing` | `--throttle-spacing` | `8px` |
|
|
322
|
+
| `shadowCard` | `--throttle-shadow-card` | `0 1px 3px rgba(15,23,42,.08), 0 1px 2px rgba(15,23,42,.04)` |
|
|
323
|
+
| `shadowPopover` | `--throttle-shadow-popover` | `0 10px 24px rgba(15,23,42,.12), 0 2px 6px rgba(15,23,42,.06)` |
|
|
324
|
+
| `transition` | `--throttle-transition` | `150ms cubic-bezier(.2,0,.2,1)` |
|
|
325
|
+
| `zModal` | `--throttle-z-modal` | `2147483000` |
|
|
326
|
+
| `zPopover` | `--throttle-z-popover` | `2147482000` |
|
|
327
|
+
|
|
328
|
+
### Set `colorPrimary` and `colorPrimaryForeground` together
|
|
329
|
+
|
|
330
|
+
`colorPrimaryForeground` is the text colour drawn on top of `colorPrimary`. They
|
|
331
|
+
are separate variables, so overriding only the first leaves the theme's own
|
|
332
|
+
foreground in place — white in light, near-black in dark — and the pair is not
|
|
333
|
+
always readable. A terracotta primary on its own measures 3.65:1 against the
|
|
334
|
+
dark theme's foreground, below the 4.5:1 WCAG asks for button text.
|
|
335
|
+
|
|
336
|
+
In development the components measure the pair and warn once in the console when
|
|
337
|
+
it falls short, naming the ratio and the theme it fails in. Nothing is logged in
|
|
338
|
+
production builds, and colours we cannot parse (`var(--brand)`, `rgb(...)`,
|
|
339
|
+
named colours) are left alone rather than guessed at.
|
|
340
|
+
|
|
341
|
+
An unrecognized key is dropped rather than passed through — a typo never
|
|
342
|
+
silently reaches the DOM as a custom property that does nothing. `theme: 'dark'`
|
|
343
|
+
swaps every color variable's default; `'auto'` follows
|
|
344
|
+
`prefers-color-scheme` and swaps only when `theme` is left unset.
|
|
345
|
+
|
|
346
|
+
**`elements`** — every element a component renders carries a stable key
|
|
347
|
+
(`'signIn.submitButton'`, `'card'`, `'field.input'`, …) via
|
|
348
|
+
`data-throttle-element` in the DOM, and `THROTTLE_ELEMENT_KEYS` is the
|
|
349
|
+
exhaustive, frozen list of them. Map any key to a className in `elements` and
|
|
350
|
+
it is appended alongside the shipped class — every shipped rule is a single
|
|
351
|
+
class selector, so an appended class of equal specificity wins on source
|
|
352
|
+
order. This means Tailwind utility classes work directly, no wrapper needed:
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
appearance={{
|
|
356
|
+
elements: {
|
|
357
|
+
'signIn.submitButton': 'bg-violet-600 hover:bg-violet-700 text-white rounded-lg',
|
|
358
|
+
card: 'shadow-xl rounded-2xl ring-1 ring-violet-100',
|
|
359
|
+
'field.input': 'focus:ring-2 focus:ring-violet-500',
|
|
360
|
+
},
|
|
361
|
+
}}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
If your bundler ever loads Tailwind's stylesheet *after* ours (order no longer
|
|
365
|
+
in your favor), add Tailwind's `!` important prefix (`!bg-violet-600`) rather
|
|
366
|
+
than fighting load order — it's a one-character fix that works regardless of
|
|
367
|
+
which stylesheet lands last.
|
|
368
|
+
|
|
369
|
+
### Localization
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
<ThrottleProvider
|
|
373
|
+
publishableKey={pk}
|
|
374
|
+
localization={{
|
|
375
|
+
signIn: { title: 'Welcome back', submitButton: 'Log in' },
|
|
376
|
+
errors: { weak_password: 'Choose a longer password.' },
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Every string in `enUS` (also exported, for reference or as a base to spread
|
|
382
|
+
from) can be overridden at any depth — a component prop only needs the keys it
|
|
383
|
+
changes, and everything else falls back to the default English copy.
|
|
384
|
+
|
|
385
|
+
`errors` is a `code → message` map layered in front of the shipped
|
|
386
|
+
`ThrottleAuthError` → copy mapping: when a call fails, a component looks up
|
|
387
|
+
`errors[err.code]` first and only falls back to the built-in message if that
|
|
388
|
+
key is absent. Use it to customize copy for `weak_password`,
|
|
389
|
+
`too_many_attempts`, or any other `ThrottleAuthError` code from the
|
|
390
|
+
[Errors](#errors) table above, without losing the built-in messages for every
|
|
391
|
+
code you don't override.
|
|
392
|
+
|
|
393
|
+
### Styles
|
|
394
|
+
|
|
395
|
+
Styles auto-inject on mount by default (`<ThrottleProvider injectStyles>`, true
|
|
396
|
+
unless set otherwise) via a single `<style>` tag, so a plain
|
|
397
|
+
`npm install` + `<ThrottleProvider>` is enough to see fully-styled components.
|
|
398
|
+
For a flash-free SSR paint, disable the runtime injection and import the
|
|
399
|
+
stylesheet directly instead, so it's present in the very first paint:
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import '@usethrottle/auth/styles.css';
|
|
403
|
+
// ...
|
|
404
|
+
<ThrottleProvider publishableKey={pk} injectStyles={false}>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Next.js App Router
|
|
408
|
+
|
|
409
|
+
Put `<ThrottleProvider>` once, in the root layout. Every drop-in component is
|
|
410
|
+
already a client component internally — nothing under it needs its own
|
|
411
|
+
`'use client'` directive:
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
// app/layout.tsx
|
|
415
|
+
import { ThrottleProvider } from '@usethrottle/auth/components';
|
|
416
|
+
|
|
417
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
418
|
+
return (
|
|
419
|
+
<html lang="en">
|
|
420
|
+
<body>
|
|
421
|
+
<ThrottleProvider publishableKey={process.env.NEXT_PUBLIC_THROTTLE_PK!}>
|
|
422
|
+
{children}
|
|
423
|
+
</ThrottleProvider>
|
|
424
|
+
</body>
|
|
425
|
+
</html>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Known limits
|
|
431
|
+
|
|
432
|
+
- **No add-card control.** There is no buyer-plane route for creating a saved
|
|
433
|
+
payment method (see [What this package does NOT do](#what-this-package-does-not-do)),
|
|
434
|
+
so `PaymentMethodsPanel` only lists, sets a default, and removes — it never
|
|
435
|
+
renders a card-entry form. Add cards through
|
|
436
|
+
[`@usethrottle/payment-methods`](https://usethrottle.dev/docs/developers/payment-methods)
|
|
437
|
+
(a merchant-minted client token, separate from this package), then let
|
|
438
|
+
`PaymentMethodsPanel` (or `useSavedCards()`) pick up the new card on its next
|
|
439
|
+
fetch.
|
|
440
|
+
- **`<SignUp/>`'s success copy is deliberately hedged.** The register endpoint
|
|
441
|
+
returns the same `pending` status, and sends no email, both for a genuinely
|
|
442
|
+
new address and for a repeat attempt on an address that already has an
|
|
443
|
+
account — the server cannot tell your UI which case happened without leaking
|
|
444
|
+
which emails are registered. So the shipped copy reads "If that email is
|
|
445
|
+
new, check your inbox…" rather than "We sent you an email" — the latter
|
|
446
|
+
would be false in the second case. Override `localization.signUp.pendingNotice`
|
|
447
|
+
if you want different wording, but keep it similarly non-committal for the
|
|
448
|
+
same reason.
|
|
449
|
+
|
|
144
450
|
## Email verification and the claim cutoff
|
|
145
451
|
|
|
146
452
|
**This is the single most surprising thing about this API — read it before
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/react/forms/messages.ts
|
|
4
|
+
function waitCopy(seconds) {
|
|
5
|
+
if (!seconds || !Number.isFinite(seconds)) return "Please wait a moment and try again.";
|
|
6
|
+
if (seconds < 60) return `Please wait ${Math.ceil(seconds)} seconds and try again.`;
|
|
7
|
+
const minutes = Math.ceil(seconds / 60);
|
|
8
|
+
return `Please wait ${minutes} ${minutes === 1 ? "minute" : "minutes"} and try again.`;
|
|
9
|
+
}
|
|
10
|
+
function messageForError(err) {
|
|
11
|
+
const e = err;
|
|
12
|
+
switch (e?.code) {
|
|
13
|
+
case "invalid_credentials":
|
|
14
|
+
return "That email or password is not correct.";
|
|
15
|
+
case "weak_password":
|
|
16
|
+
return e.message || "Please choose a stronger password.";
|
|
17
|
+
case "too_many_attempts":
|
|
18
|
+
return `Too many attempts. ${waitCopy(e.retryAfter)}`;
|
|
19
|
+
case "verification_required":
|
|
20
|
+
return "Please verify your email address to continue.";
|
|
21
|
+
case "step_up_required":
|
|
22
|
+
return "Please re-enter your password to continue.";
|
|
23
|
+
case "session_revoked":
|
|
24
|
+
return "Your session has ended. Please sign in again.";
|
|
25
|
+
case "token_expired":
|
|
26
|
+
case "token_invalid":
|
|
27
|
+
case "token_already_used":
|
|
28
|
+
return "That link is no longer valid. Please request a new link.";
|
|
29
|
+
case "auth_unavailable":
|
|
30
|
+
return "Accounts are briefly unavailable. Please try again shortly.";
|
|
31
|
+
case "origin_not_allowed":
|
|
32
|
+
case "auth_not_enabled":
|
|
33
|
+
case "invalid_api_key":
|
|
34
|
+
case "environment_mismatch":
|
|
35
|
+
return "Accounts are not available right now. Please contact support.";
|
|
36
|
+
case "cart_already_claimed":
|
|
37
|
+
return "That cart belongs to a different account.";
|
|
38
|
+
case "not_found":
|
|
39
|
+
return "We could not find that.";
|
|
40
|
+
case "payment_method_in_use":
|
|
41
|
+
return e.message || "That payment method is in use and cannot be removed.";
|
|
42
|
+
default:
|
|
43
|
+
return "Something went wrong. Please try again.";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
messageForError
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=chunk-AIXBUDOT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/forms/messages.ts"],"sourcesContent":["import { ThrottleAuthError } from '../../errors';\n\nfunction waitCopy(seconds?: number): string {\n if (!seconds || !Number.isFinite(seconds)) return 'Please wait a moment and try again.';\n if (seconds < 60) return `Please wait ${Math.ceil(seconds)} seconds and try again.`;\n const minutes = Math.ceil(seconds / 60);\n return `Please wait ${minutes} ${minutes === 1 ? 'minute' : 'minutes'} and try again.`;\n}\n\n/**\n * Buyer-facing copy for every code the storefront API returns. Two codes are\n * deliberately NOT passed through from the server: `invalid_credentials` (the\n * server is vague on purpose — enumerating accounts through our own UI would\n * undo that) and anything unrecognised.\n */\nexport function messageForError(err: unknown): string {\n const e = err as Partial<ThrottleAuthError> & { code?: string };\n switch (e?.code) {\n case 'invalid_credentials':\n return 'That email or password is not correct.';\n case 'weak_password':\n // The server's message IS the policy reason (\"Use at least 12 characters\").\n return e.message || 'Please choose a stronger password.';\n case 'too_many_attempts':\n return `Too many attempts. ${waitCopy(e.retryAfter)}`;\n case 'verification_required':\n return 'Please verify your email address to continue.';\n case 'step_up_required':\n return 'Please re-enter your password to continue.';\n case 'session_revoked':\n return 'Your session has ended. Please sign in again.';\n case 'token_expired':\n case 'token_invalid':\n case 'token_already_used':\n return 'That link is no longer valid. Please request a new link.';\n case 'auth_unavailable':\n return 'Accounts are briefly unavailable. Please try again shortly.';\n case 'origin_not_allowed':\n case 'auth_not_enabled':\n case 'invalid_api_key':\n case 'environment_mismatch':\n // Merchant misconfiguration, not something the buyer can fix.\n return 'Accounts are not available right now. Please contact support.';\n case 'cart_already_claimed':\n return 'That cart belongs to a different account.';\n case 'not_found':\n return 'We could not find that.';\n case 'payment_method_in_use':\n // The server's message IS the fix (\"set another card as default\n // first\") — same rationale as weak_password below.\n return e.message || 'That payment method is in use and cannot be removed.';\n default:\n return 'Something went wrong. Please try again.';\n }\n}\n"],"mappings":";;;AAEA,SAAS,SAAS,SAA0B;AAC1C,MAAI,CAAC,WAAW,CAAC,OAAO,SAAS,OAAO,EAAG,QAAO;AAClD,MAAI,UAAU,GAAI,QAAO,eAAe,KAAK,KAAK,OAAO,CAAC;AAC1D,QAAM,UAAU,KAAK,KAAK,UAAU,EAAE;AACtC,SAAO,eAAe,OAAO,IAAI,YAAY,IAAI,WAAW,SAAS;AACvE;AAQO,SAAS,gBAAgB,KAAsB;AACpD,QAAM,IAAI;AACV,UAAQ,GAAG,MAAM;AAAA,IACf,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAEH,aAAO,EAAE,WAAW;AAAA,IACtB,KAAK;AACH,aAAO,sBAAsB,SAAS,EAAE,UAAU,CAAC;AAAA,IACrD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAEH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAGH,aAAO,EAAE,WAAW;AAAA,IACtB;AACE,aAAO;AAAA,EACX;AACF;","names":[]}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
ThrottleAuthError,
|
|
4
|
+
useAuth
|
|
5
|
+
} from "./chunk-SZ427BQ3.js";
|
|
6
|
+
|
|
7
|
+
// src/react/hooks.ts
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
function asError(err) {
|
|
10
|
+
if (err instanceof ThrottleAuthError) return err;
|
|
11
|
+
const anyErr = err;
|
|
12
|
+
return new ThrottleAuthError({
|
|
13
|
+
code: anyErr?.code ?? "request_failed",
|
|
14
|
+
message: anyErr?.message ?? String(err),
|
|
15
|
+
statusCode: anyErr?.statusCode ?? 0
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function useResource(empty, load, deps) {
|
|
19
|
+
const { status, emailVerified } = useAuth();
|
|
20
|
+
const [data, setData] = useState(empty);
|
|
21
|
+
const [isLoading, setIsLoading] = useState(status === "loading");
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
const gen = useRef(0);
|
|
24
|
+
const alive = useRef(true);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
alive.current = true;
|
|
27
|
+
return () => {
|
|
28
|
+
alive.current = false;
|
|
29
|
+
gen.current++;
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
const refetch = useCallback(async () => {
|
|
33
|
+
const mine = ++gen.current;
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
try {
|
|
36
|
+
const next = await load();
|
|
37
|
+
if (!alive.current || gen.current !== mine) return;
|
|
38
|
+
setData(next);
|
|
39
|
+
setError(null);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (!alive.current || gen.current !== mine) return;
|
|
42
|
+
setError(asError(err));
|
|
43
|
+
} finally {
|
|
44
|
+
if (alive.current && gen.current === mine) setIsLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, deps);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (status !== "authenticated") {
|
|
49
|
+
gen.current++;
|
|
50
|
+
setData(empty);
|
|
51
|
+
setError(null);
|
|
52
|
+
setIsLoading(status === "loading");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
void refetch();
|
|
56
|
+
}, [status, emailVerified, refetch]);
|
|
57
|
+
return { data, isLoading, error, refetch };
|
|
58
|
+
}
|
|
59
|
+
function useCustomer() {
|
|
60
|
+
const { account } = useAuth();
|
|
61
|
+
const res = useResource(null, () => account.getProfile(), [account]);
|
|
62
|
+
const update = useCallback(async (patch) => {
|
|
63
|
+
await account.updateProfile(patch);
|
|
64
|
+
await res.refetch();
|
|
65
|
+
}, [account, res]);
|
|
66
|
+
return { ...res, update };
|
|
67
|
+
}
|
|
68
|
+
function useAddresses() {
|
|
69
|
+
const { account } = useAuth();
|
|
70
|
+
const res = useResource([], () => account.listAddresses(), [account]);
|
|
71
|
+
const after = async (p) => {
|
|
72
|
+
await p;
|
|
73
|
+
await res.refetch();
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
...res,
|
|
77
|
+
create: (input) => after(account.createAddress(input)),
|
|
78
|
+
update: (id, patch) => after(account.updateAddress(id, patch)),
|
|
79
|
+
remove: (id) => after(account.deleteAddress(id))
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function useSavedCards() {
|
|
83
|
+
const { account } = useAuth();
|
|
84
|
+
const res = useResource([], () => account.listPaymentMethods(), [account]);
|
|
85
|
+
const after = async (p) => {
|
|
86
|
+
await p;
|
|
87
|
+
await res.refetch();
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
...res,
|
|
91
|
+
setDefault: (id) => after(account.setDefaultPaymentMethod(id)),
|
|
92
|
+
remove: (id) => after(account.deletePaymentMethod(id))
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function useOrders(page) {
|
|
96
|
+
const { account } = useAuth();
|
|
97
|
+
return useResource([], () => account.listOrders(page), [account, page?.cursor, page?.limit]);
|
|
98
|
+
}
|
|
99
|
+
function useInvoices(page) {
|
|
100
|
+
const { account } = useAuth();
|
|
101
|
+
return useResource([], () => account.listInvoices(page), [account, page?.cursor, page?.limit]);
|
|
102
|
+
}
|
|
103
|
+
function useSubscriptions(page) {
|
|
104
|
+
const { account } = useAuth();
|
|
105
|
+
const res = useResource(
|
|
106
|
+
[],
|
|
107
|
+
() => account.listSubscriptions(page),
|
|
108
|
+
[account, page?.cursor, page?.limit]
|
|
109
|
+
);
|
|
110
|
+
const after = async (p) => {
|
|
111
|
+
await p;
|
|
112
|
+
await res.refetch();
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
...res,
|
|
116
|
+
cancel: (id, opts) => after(account.cancelSubscription(id, opts)),
|
|
117
|
+
pause: (id) => after(account.pauseSubscription(id)),
|
|
118
|
+
resume: (id) => after(account.resumeSubscription(id))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function useOrder(id) {
|
|
122
|
+
const { account } = useAuth();
|
|
123
|
+
return useResource(null, async () => id ? account.getOrder(id) : null, [account, id]);
|
|
124
|
+
}
|
|
125
|
+
function useInvoice(id) {
|
|
126
|
+
const { account } = useAuth();
|
|
127
|
+
return useResource(null, async () => id ? account.getInvoice(id) : null, [account, id]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
useCustomer,
|
|
132
|
+
useAddresses,
|
|
133
|
+
useSavedCards,
|
|
134
|
+
useOrders,
|
|
135
|
+
useInvoices,
|
|
136
|
+
useSubscriptions,
|
|
137
|
+
useOrder,
|
|
138
|
+
useInvoice
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=chunk-HGBAIM6B.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/hooks.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { ThrottleAuthError } from '../errors';\nimport type { Address, AddressInput, Customer, Invoice, Order, OrderDetail, PaymentMethod, ProfilePatch, Subscription } from '../types';\nimport type { Page } from '../account';\nimport { useAuth } from './context';\n\nexport interface Resource<T> {\n data: T;\n isLoading: boolean;\n error: ThrottleAuthError | null;\n refetch(): Promise<void>;\n}\n\nfunction asError(err: unknown): ThrottleAuthError {\n if (err instanceof ThrottleAuthError) return err;\n const anyErr = err as any;\n return new ThrottleAuthError({\n code: anyErr?.code ?? 'request_failed',\n message: anyErr?.message ?? String(err),\n statusCode: anyErr?.statusCode ?? 0,\n });\n}\n\n/**\n * One fetch-on-auth-change primitive behind every resource hook. `emailVerified`\n * is a dependency on purpose: verifying lifts the claim cutoff, and lists that\n * were legitimately empty a moment ago are not empty any more.\n */\nfunction useResource<T>(empty: T, load: () => Promise<T>, deps: unknown[]): Resource<T> {\n const { status, emailVerified } = useAuth();\n const [data, setData] = useState<T>(empty);\n const [isLoading, setIsLoading] = useState(status === 'loading');\n const [error, setError] = useState<ThrottleAuthError | null>(null);\n const gen = useRef(0);\n const alive = useRef(true);\n\n useEffect(() => {\n // Set on mount, not only cleared on unmount: React StrictMode mounts,\n // unmounts and remounts in development, and a ref that is only ever set\n // to false would silently discard every response for the rest of the\n // component's life.\n alive.current = true;\n return () => { alive.current = false; gen.current++; };\n }, []);\n\n const refetch = useCallback(async () => {\n const mine = ++gen.current;\n setIsLoading(true);\n try {\n const next = await load();\n if (!alive.current || gen.current !== mine) return;\n setData(next);\n setError(null);\n } catch (err) {\n if (!alive.current || gen.current !== mine) return;\n setError(asError(err));\n } finally {\n if (alive.current && gen.current === mine) setIsLoading(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n useEffect(() => {\n if (status !== 'authenticated') {\n gen.current++; // abandon anything in flight\n setData(empty);\n setError(null);\n setIsLoading(status === 'loading');\n return;\n }\n void refetch();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status, emailVerified, refetch]);\n\n return { data, isLoading, error, refetch };\n}\n\nexport function useCustomer(): Resource<Customer | null> & { update(patch: ProfilePatch): Promise<void> } {\n const { account } = useAuth();\n const res = useResource<Customer | null>(null, () => account.getProfile(), [account]);\n const update = useCallback(async (patch: ProfilePatch) => {\n await account.updateProfile(patch);\n await res.refetch();\n }, [account, res]);\n return { ...res, update };\n}\n\nexport function useAddresses(): Resource<Address[]> & {\n create(input: AddressInput): Promise<void>;\n update(id: string, patch: Partial<AddressInput>): Promise<void>;\n remove(id: string): Promise<void>;\n} {\n const { account } = useAuth();\n const res = useResource<Address[]>([], () => account.listAddresses(), [account]);\n const after = async (p: Promise<unknown>) => { await p; await res.refetch(); };\n return {\n ...res,\n create: (input) => after(account.createAddress(input)),\n update: (id, patch) => after(account.updateAddress(id, patch)),\n remove: (id) => after(account.deleteAddress(id)),\n };\n}\n\nexport function useSavedCards(): Resource<PaymentMethod[]> & {\n setDefault(id: string): Promise<void>;\n remove(id: string): Promise<void>;\n} {\n const { account } = useAuth();\n const res = useResource<PaymentMethod[]>([], () => account.listPaymentMethods(), [account]);\n const after = async (p: Promise<unknown>) => { await p; await res.refetch(); };\n return {\n ...res,\n setDefault: (id) => after(account.setDefaultPaymentMethod(id)),\n remove: (id) => after(account.deletePaymentMethod(id)),\n };\n}\n\nexport function useOrders(page?: Page): Resource<Order[]> {\n const { account } = useAuth();\n return useResource<Order[]>([], () => account.listOrders(page), [account, page?.cursor, page?.limit]);\n}\n\nexport function useInvoices(page?: Page): Resource<Invoice[]> {\n const { account } = useAuth();\n return useResource<Invoice[]>([], () => account.listInvoices(page), [account, page?.cursor, page?.limit]);\n}\n\nexport function useSubscriptions(page?: Page): Resource<Subscription[]> & {\n cancel(id: string, opts?: { atPeriodEnd?: boolean }): Promise<void>;\n pause(id: string): Promise<void>;\n resume(id: string): Promise<void>;\n} {\n const { account } = useAuth();\n const res = useResource<Subscription[]>([], () => account.listSubscriptions(page),\n [account, page?.cursor, page?.limit]);\n const after = async (p: Promise<unknown>) => { await p; await res.refetch(); };\n return {\n ...res,\n cancel: (id, opts) => after(account.cancelSubscription(id, opts)),\n pause: (id) => after(account.pauseSubscription(id)),\n resume: (id) => after(account.resumeSubscription(id)),\n };\n}\n\n/**\n * Single-resource reads for the detail views. A `null` id means \"nothing\n * selected\" and must not call the API — `useResource` still runs, so the\n * loader resolves to null rather than being skipped, which keeps the\n * loading/error shape identical whether or not an id is present.\n */\nexport function useOrder(id: string | null): Resource<OrderDetail | null> {\n const { account } = useAuth();\n return useResource<OrderDetail | null>(null, async () => (id ? account.getOrder(id) : null), [account, id]);\n}\n\nexport function useInvoice(id: string | null): Resource<Invoice | null> {\n const { account } = useAuth();\n return useResource<Invoice | null>(null, async () => (id ? account.getInvoice(id) : null), [account, id]);\n}\n"],"mappings":"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","names":[]}
|