@usethrottle/auth 0.1.2 → 0.2.1
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 +258 -0
- 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-SOPTBXF3.js → chunk-SZ427BQ3.js} +48 -3
- package/dist/chunk-SZ427BQ3.js.map +1 -0
- package/dist/index.cjs +3 -1
- 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 +16 -1
- 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 +49 -11
- package/dist/chunk-5GLLPF6L.js +0 -49
- package/dist/chunk-5GLLPF6L.js.map +0 -1
- package/dist/chunk-SOPTBXF3.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
|
@@ -189,6 +189,264 @@ function Orders() {
|
|
|
189
189
|
- `useSubscriptions(page?)` — `cancel(id, { atPeriodEnd })`, `pause(id)`,
|
|
190
190
|
`resume(id)`.
|
|
191
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
|
+
|
|
192
450
|
## Email verification and the claim cutoff
|
|
193
451
|
|
|
194
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":[]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// src/errors.ts
|
|
2
4
|
var ThrottleAuthError = class extends Error {
|
|
3
5
|
code;
|
|
@@ -184,7 +186,8 @@ function createThrottleAuth(config) {
|
|
|
184
186
|
const data = await request({ ...base, method: "POST", path: "/auth/refresh", body: { refreshToken } });
|
|
185
187
|
await persist(data);
|
|
186
188
|
} catch (err) {
|
|
187
|
-
|
|
189
|
+
const refused = err instanceof ThrottleAuthError && (err.statusCode === 401 || err.statusCode === 403);
|
|
190
|
+
if (refused) await forget();
|
|
188
191
|
throw err;
|
|
189
192
|
}
|
|
190
193
|
});
|
|
@@ -365,6 +368,7 @@ function createAccount(auth) {
|
|
|
365
368
|
getOrder: (id) => call("GET", `/me/orders/${seg(id)}`),
|
|
366
369
|
listInvoices: (page) => call("GET", `/me/invoices${query(page)}`),
|
|
367
370
|
getInvoice: (id) => call("GET", `/me/invoices/${seg(id)}`),
|
|
371
|
+
getInvoicePdfUrl: (id) => call("GET", `/me/invoices/${seg(id)}/pdf`),
|
|
368
372
|
listSubscriptions: (page) => call("GET", `/me/subscriptions${query(page)}`),
|
|
369
373
|
getSubscription: (id) => call("GET", `/me/subscriptions/${seg(id)}`),
|
|
370
374
|
cancelSubscription: (id, opts) => call("POST", `/me/subscriptions/${seg(id)}/cancel`, opts),
|
|
@@ -374,11 +378,52 @@ function createAccount(auth) {
|
|
|
374
378
|
};
|
|
375
379
|
}
|
|
376
380
|
|
|
381
|
+
// src/react/context.tsx
|
|
382
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
383
|
+
import { jsx } from "react/jsx-runtime";
|
|
384
|
+
var Ctx = createContext(null);
|
|
385
|
+
function ThrottleAuthProvider(props) {
|
|
386
|
+
const { auth: injected, publishableKey, baseUrl, storage, onStepUpRequired, children } = props;
|
|
387
|
+
const value = useMemo(() => {
|
|
388
|
+
const auth = injected ?? createThrottleAuth({ publishableKey, baseUrl, storage, onStepUpRequired });
|
|
389
|
+
return { auth, account: createAccount(auth) };
|
|
390
|
+
}, [injected, publishableKey, baseUrl]);
|
|
391
|
+
const restored = useRef(null);
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (restored.current === value.auth) return;
|
|
394
|
+
restored.current = value.auth;
|
|
395
|
+
void value.auth.restore();
|
|
396
|
+
}, [value]);
|
|
397
|
+
return /* @__PURE__ */ jsx(Ctx.Provider, { value, children });
|
|
398
|
+
}
|
|
399
|
+
function useCtx() {
|
|
400
|
+
const ctx = useContext(Ctx);
|
|
401
|
+
if (!ctx) throw new Error("useAuth must be used inside a <ThrottleAuthProvider>");
|
|
402
|
+
return ctx;
|
|
403
|
+
}
|
|
404
|
+
function useAuthContext() {
|
|
405
|
+
return useCtx();
|
|
406
|
+
}
|
|
407
|
+
function useAuth() {
|
|
408
|
+
const { auth, account } = useCtx();
|
|
409
|
+
const snapshot = useSyncExternalStore(auth.subscribe, auth.getSnapshot, auth.getSnapshot);
|
|
410
|
+
return {
|
|
411
|
+
...snapshot,
|
|
412
|
+
isLoading: snapshot.status === "loading",
|
|
413
|
+
isAuthenticated: snapshot.status === "authenticated",
|
|
414
|
+
auth,
|
|
415
|
+
account
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
377
419
|
export {
|
|
378
420
|
ThrottleAuthError,
|
|
379
421
|
memoryStorage,
|
|
380
422
|
localStorageAdapter,
|
|
381
423
|
createThrottleAuth,
|
|
382
|
-
createAccount
|
|
424
|
+
createAccount,
|
|
425
|
+
ThrottleAuthProvider,
|
|
426
|
+
useAuthContext,
|
|
427
|
+
useAuth
|
|
383
428
|
};
|
|
384
|
-
//# sourceMappingURL=chunk-
|
|
429
|
+
//# sourceMappingURL=chunk-SZ427BQ3.js.map
|