@usethrottle/payment-methods 0.1.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 +44 -0
- package/README.md +258 -0
- package/dist/Gr4vyAddCard-J3HC6KTK.js +87 -0
- package/dist/Gr4vyAddCard-J3HC6KTK.js.map +1 -0
- package/dist/chunk-T2CV3T5K.js +49 -0
- package/dist/chunk-T2CV3T5K.js.map +1 -0
- package/dist/index.cjs +511 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +338 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +121 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +29 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.js +57 -0
- package/dist/server.js.map +1 -0
- package/dist/types-Bleeq_oI.d.cts +33 -0
- package/dist/types-Bleeq_oI.d.ts +33 -0
- package/package.json +57 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# `@usethrottle/payment-methods`
|
|
2
|
+
|
|
3
|
+
Drop-in saved-card wallet (list, add, set default, remove) for Throttle storefronts. Headless core + React component.
|
|
4
|
+
|
|
5
|
+
## Package purpose
|
|
6
|
+
|
|
7
|
+
Wraps the Plan-1 `/api/v1/me/payment-methods*` browser routes and the `/api/v1/payment-methods/client-token` + `/api/v1/customers/:id/payment-methods*` server routes behind a typed SDK.
|
|
8
|
+
|
|
9
|
+
## Entry points
|
|
10
|
+
|
|
11
|
+
| Import path | Environment | What it exports |
|
|
12
|
+
|--------------------------------------------|--------------|---------------------------------------------------------------------------------|
|
|
13
|
+
| `@usethrottle/payment-methods` | browser/React | `createPaymentMethods`, `usePaymentMethods`, `<PaymentMethods>`, types, errors |
|
|
14
|
+
| `@usethrottle/payment-methods/server` | Node.js only | `mintClientToken`, `createServerPaymentMethods` |
|
|
15
|
+
|
|
16
|
+
Never import from `./server` in browser bundles — it only uses `./request`, `./types`, and `./errors`, but the path convention signals server-only intent to bundlers and reviewers.
|
|
17
|
+
|
|
18
|
+
## SSR isolation rule
|
|
19
|
+
|
|
20
|
+
`@gr4vy/embed-react` accesses browser globals at module evaluation time. It must NEVER be imported eagerly.
|
|
21
|
+
|
|
22
|
+
- `PaymentMethods.tsx` loads `Gr4vyAddCard` via `React.lazy()` only. Do not hoist that import.
|
|
23
|
+
- In Next.js, consumers must wrap `<PaymentMethods>` with `dynamic(() => import(...), { ssr: false })` at any route that SSRs.
|
|
24
|
+
- Tests use jsdom (vitest), which does not provide real browser globals — mock `@gr4vy/embed-react` as needed.
|
|
25
|
+
|
|
26
|
+
## Test conventions
|
|
27
|
+
|
|
28
|
+
- Runner: **vitest** + **jsdom** (`vitest.config.ts`).
|
|
29
|
+
- Run: `pnpm --filter @usethrottle/payment-methods test`.
|
|
30
|
+
- Typecheck: `pnpm --filter @usethrottle/payment-methods typecheck`.
|
|
31
|
+
- Build: `pnpm --filter @usethrottle/payment-methods build` (tsup, dual CJS + ESM, server subpath).
|
|
32
|
+
|
|
33
|
+
## API routes consumed
|
|
34
|
+
|
|
35
|
+
| Route | Auth | Used by |
|
|
36
|
+
|----------------------------------------------------|------------------------------|---------------------------------|
|
|
37
|
+
| `POST /api/v1/payment-methods/client-token` | `X-API-Key` (`sk_*`) | `mintClientToken` |
|
|
38
|
+
| `GET /api/v1/me/payment-methods` | `X-Throttle-PM-Token` | `client.list`, hook, component |
|
|
39
|
+
| `PATCH /api/v1/me/payment-methods/:id` | `X-Throttle-PM-Token` | `client.setDefault` |
|
|
40
|
+
| `DELETE /api/v1/me/payment-methods/:id` | `X-Throttle-PM-Token` | `client.remove` |
|
|
41
|
+
| `POST /api/v1/me/payment-methods/embed-token` | `X-Throttle-PM-Token` | `client.startAddCard` |
|
|
42
|
+
| `GET /api/v1/customers/:id/payment-methods` | `X-API-Key` (`sk_*`) | `createServerPaymentMethods.list` |
|
|
43
|
+
| `PATCH /api/v1/customers/:id/payment-methods/:id` | `X-API-Key` (`sk_*`) | `createServerPaymentMethods.setDefault` |
|
|
44
|
+
| `DELETE /api/v1/customers/:id/payment-methods/:id` | `X-API-Key` (`sk_*`) | `createServerPaymentMethods.remove` |
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# `@usethrottle/payment-methods`
|
|
2
|
+
|
|
3
|
+
Drop-in saved-card wallet for Throttle storefronts. Headless core + React component + server helpers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @usethrottle/payment-methods
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @usethrottle/payment-methods
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
React and `react-dom` are peer dependencies (>=18).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
18
|
+
|
|
19
|
+
1. **Backend** calls `mintClientToken` (from `@usethrottle/payment-methods/server`) using your secret key. It returns a short-lived JWT scoped to one customer.
|
|
20
|
+
2. **Frontend** passes that token to `<PaymentMethods clientToken={token} />` (or the headless `createPaymentMethods` / `usePaymentMethods`). The token authorises all card management operations — your secret key never reaches the browser.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Your Backend ──POST /api/v1/payment-methods/client-token──► Throttle API
|
|
24
|
+
◄── { token, expiresAt } ──────────────────────
|
|
25
|
+
Your Backend ──{ token }──────────────────────────────────► Browser
|
|
26
|
+
Browser ──<PaymentMethods clientToken={token} />──────► wallet UI
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Backend: mint a client token
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// app/api/pm-token/route.ts (Next.js App Router)
|
|
35
|
+
import { mintClientToken } from '@usethrottle/payment-methods/server';
|
|
36
|
+
|
|
37
|
+
export async function POST(request: Request) {
|
|
38
|
+
const user = await getAuthUser(request); // your own auth
|
|
39
|
+
if (!user) return new Response(null, { status: 401 });
|
|
40
|
+
|
|
41
|
+
const customerId = await resolveThrottleCustomerId(user.id); // cus_*
|
|
42
|
+
|
|
43
|
+
const { token, expiresAt } = await mintClientToken({
|
|
44
|
+
apiKey: process.env.THROTTLE_SECRET_KEY!,
|
|
45
|
+
customerId,
|
|
46
|
+
// baseUrl?: 'https://api.usethrottle.dev' (default)
|
|
47
|
+
// environmentId?: 'env_...' (omit to use key default)
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return Response.json({ token, expiresAt });
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`mintClientToken` options:
|
|
55
|
+
|
|
56
|
+
| Option | Type | Required | Description |
|
|
57
|
+
|-----------------|--------|----------|----------------------------------------------------------|
|
|
58
|
+
| `apiKey` | string | yes | Your `sk_*` secret key. |
|
|
59
|
+
| `customerId` | string | yes | Throttle customer ID (`cus_*`). |
|
|
60
|
+
| `baseUrl` | string | no | API base URL (defaults to `https://api.usethrottle.dev`).|
|
|
61
|
+
| `environmentId` | string | no | Override the environment derived from the key. |
|
|
62
|
+
|
|
63
|
+
Returns `{ token: string; expiresAt: string }`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Frontend: React component
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// components/Wallet.tsx
|
|
71
|
+
'use client';
|
|
72
|
+
|
|
73
|
+
import dynamic from 'next/dynamic';
|
|
74
|
+
|
|
75
|
+
const PaymentMethods = dynamic(
|
|
76
|
+
() => import('@usethrottle/payment-methods').then((m) => m.PaymentMethods),
|
|
77
|
+
{ ssr: false },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export function Wallet({ clientToken }: { clientToken: string }) {
|
|
81
|
+
return (
|
|
82
|
+
<PaymentMethods
|
|
83
|
+
clientToken={clientToken}
|
|
84
|
+
theme={{
|
|
85
|
+
colorPrimary: '#1D56E8',
|
|
86
|
+
colorText: '#111827',
|
|
87
|
+
colorBorder: '#E5E7EB',
|
|
88
|
+
borderRadius: '8px',
|
|
89
|
+
}}
|
|
90
|
+
onChange={(cards) => console.log('cards updated', cards)}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `<PaymentMethods>` props
|
|
97
|
+
|
|
98
|
+
| Prop | Type | Required | Description |
|
|
99
|
+
|---------------|-------------------------|----------|------------------------------------------------------------------------|
|
|
100
|
+
| `clientToken` | string | yes | Short-lived token from `mintClientToken`. |
|
|
101
|
+
| `baseUrl` | string | no | API base URL override. |
|
|
102
|
+
| `theme` | `PaymentMethodsTheme` | no | Colour + radius tokens (see table below). |
|
|
103
|
+
| `onChange` | `(cards: SavedCard[]) => void` | no | Fires after every list change (initial load, set default, remove, add card). |
|
|
104
|
+
|
|
105
|
+
### `PaymentMethodsTheme` tokens
|
|
106
|
+
|
|
107
|
+
| Token | Default | Description |
|
|
108
|
+
|-----------------|-------------|-------------------------|
|
|
109
|
+
| `colorPrimary` | `#1D56E8` | Accent / button colour. |
|
|
110
|
+
| `colorText` | `#111827` | Body text colour. |
|
|
111
|
+
| `colorBorder` | `#E5E7EB` | Card border colour. |
|
|
112
|
+
| `borderRadius` | `8px` | Corner radius. |
|
|
113
|
+
|
|
114
|
+
### SSR note
|
|
115
|
+
|
|
116
|
+
`@gr4vy/embed-react` uses browser globals. Always render `<PaymentMethods>` in a client component (`'use client'`) **and** wrap it with `dynamic(..., { ssr: false })` at any route that server-renders. The component itself lazy-loads the Gr4vy embed internally, so it is safe inside a client component; the `dynamic` guard prevents the module from being evaluated during SSR.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Frontend: headless hook
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
'use client';
|
|
124
|
+
|
|
125
|
+
import { usePaymentMethods } from '@usethrottle/payment-methods';
|
|
126
|
+
|
|
127
|
+
export function CardList({ clientToken }: { clientToken: string }) {
|
|
128
|
+
const { cards, isLoading, error, setDefault, remove, addCard } =
|
|
129
|
+
usePaymentMethods({ clientToken });
|
|
130
|
+
|
|
131
|
+
if (isLoading) return <p>Loading...</p>;
|
|
132
|
+
if (error) return <p style={{ color: 'red' }}>{error.message}</p>;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<ul>
|
|
136
|
+
{cards.map((card) => (
|
|
137
|
+
<li key={card.id}>
|
|
138
|
+
{card.cardBrand} .... {card.cardLastFour}
|
|
139
|
+
{!card.isDefault && (
|
|
140
|
+
<>
|
|
141
|
+
<button onClick={() => setDefault(card.id)}>Make default</button>
|
|
142
|
+
<button onClick={() => remove(card.id)}>Remove</button>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
</li>
|
|
146
|
+
))}
|
|
147
|
+
<li>
|
|
148
|
+
<button onClick={() => addCard().then((payload) => console.log(payload)).catch(console.error)}>
|
|
149
|
+
+ Add card
|
|
150
|
+
</button>
|
|
151
|
+
</li>
|
|
152
|
+
</ul>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`usePaymentMethods` returns:
|
|
158
|
+
|
|
159
|
+
| Key | Type | Description |
|
|
160
|
+
|-------------|---------------------------------------|---------------------------------------------------------------------|
|
|
161
|
+
| `cards` | `SavedCard[]` | Current list of saved cards. |
|
|
162
|
+
| `isLoading` | boolean | True while the initial list is fetching. |
|
|
163
|
+
| `error` | `ThrottlePaymentMethodsError \| null` | Last error, or `null`. |
|
|
164
|
+
| `setDefault`| `(id: string) => Promise<void>` | Promote a card to default; refetches list on success. |
|
|
165
|
+
| `remove` | `(id: string) => Promise<void>` | Remove a card; refetches list on success. |
|
|
166
|
+
| `addCard` | `() => Promise<EmbedPayload>` | Fetches a Gr4vy add-only embed payload for the card-add UI. Unlike `setDefault`/`remove`, it does **not** route failures into `error` — `await` it in a `try/catch` (the `<PaymentMethods>` component does this for you). |
|
|
167
|
+
| `refetch` | `() => Promise<void>` | Manually re-fetches the list. |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Frontend: headless client (no React)
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { createPaymentMethods } from '@usethrottle/payment-methods';
|
|
175
|
+
|
|
176
|
+
const client = createPaymentMethods({ clientToken: token });
|
|
177
|
+
|
|
178
|
+
const cards = await client.list();
|
|
179
|
+
await client.setDefault('cpm_...');
|
|
180
|
+
await client.remove('cpm_...');
|
|
181
|
+
const embedPayload = await client.startAddCard();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`createPaymentMethods({ clientToken, baseUrl? })` returns `{ list, setDefault, remove, startAddCard }`.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Backend: server CRUD
|
|
189
|
+
|
|
190
|
+
Use `createServerPaymentMethods` when your backend needs to manage a customer's cards directly (e.g. admin tooling), without a browser session.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { createServerPaymentMethods } from '@usethrottle/payment-methods/server';
|
|
194
|
+
|
|
195
|
+
const server = createServerPaymentMethods({
|
|
196
|
+
apiKey: process.env.THROTTLE_SECRET_KEY!,
|
|
197
|
+
customerId: 'cus_...',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const cards = await server.list();
|
|
201
|
+
await server.setDefault('cpm_...');
|
|
202
|
+
await server.remove('cpm_...');
|
|
203
|
+
// Note: startAddCard is not available on the server client —
|
|
204
|
+
// the Gr4vy embed flow is inherently browser-side.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`createServerPaymentMethods` options match `mintClientToken` (`apiKey`, `customerId`, `baseUrl?`, `environmentId?`).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Error handling
|
|
212
|
+
|
|
213
|
+
All errors thrown by this package are instances of `ThrottlePaymentMethodsError` (extends `ThrottleError`):
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { isThrottleError, ThrottlePaymentMethodsError } from '@usethrottle/payment-methods';
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await client.remove(cardId);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof ThrottlePaymentMethodsError) {
|
|
222
|
+
if (err.code === 'cannot_remove_default') {
|
|
223
|
+
// 409 — buyer must set another card as default first
|
|
224
|
+
alert('Set another card as default before removing this one.');
|
|
225
|
+
} else {
|
|
226
|
+
console.error(err.code, err.message, err.statusCode);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// isThrottleError also works (catches both ThrottleError and ThrottlePaymentMethodsError):
|
|
232
|
+
if (isThrottleError(err)) { /* … */ }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Common error codes:
|
|
236
|
+
|
|
237
|
+
| Status | Code | When |
|
|
238
|
+
|--------|-------------------------|----------------------------------------------------------------------|
|
|
239
|
+
| 401 | `unauthorized` | Token missing, malformed, or expired. Mint a fresh token and retry. |
|
|
240
|
+
| 403 | `forbidden` | PM token used outside the `/me/payment-methods` surface. |
|
|
241
|
+
| 404 | `not_found` | Card ID not found or already removed. |
|
|
242
|
+
| 409 | `cannot_remove_default` | Removing the default card while an active subscription is attached. |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## `SavedCard` shape
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
interface SavedCard {
|
|
250
|
+
id: string; // "cpm_*"
|
|
251
|
+
methodType: string; // "card" in v1
|
|
252
|
+
cardBrand: string | null; // "visa" | "mastercard" | "amex" | ...
|
|
253
|
+
cardLastFour: string | null; // "4242"
|
|
254
|
+
cardExpMonth: number | null; // 1-12
|
|
255
|
+
cardExpYear: number | null; // 4-digit year
|
|
256
|
+
isDefault: boolean;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/Gr4vyAddCard.tsx
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import Gr4vyEmbed from "@gr4vy/embed-react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
function Gr4vyAddCard({ payload, theme, onAdded, onCancel, onError }) {
|
|
6
|
+
const embedRef = useRef(null);
|
|
7
|
+
const colors = theme ? {
|
|
8
|
+
...theme.colorPrimary !== void 0 ? { primary: theme.colorPrimary } : {},
|
|
9
|
+
...theme.colorText !== void 0 ? { text: theme.colorText } : {},
|
|
10
|
+
...theme.colorBorder !== void 0 ? { inputBorder: theme.colorBorder } : {}
|
|
11
|
+
} : {};
|
|
12
|
+
const gr4vyTheme = Object.keys(colors).length ? { colors } : void 0;
|
|
13
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
14
|
+
/* @__PURE__ */ jsx(
|
|
15
|
+
Gr4vyEmbed,
|
|
16
|
+
{
|
|
17
|
+
ref: embedRef,
|
|
18
|
+
gr4vyId: payload.gr4vyId,
|
|
19
|
+
environment: payload.environment,
|
|
20
|
+
token: payload.embedToken,
|
|
21
|
+
amount: payload.amount,
|
|
22
|
+
currency: payload.currency,
|
|
23
|
+
merchantAccountId: payload.merchantAccountId,
|
|
24
|
+
intent: "capture",
|
|
25
|
+
paymentSource: "recurring",
|
|
26
|
+
store: true,
|
|
27
|
+
display: "addOnly",
|
|
28
|
+
...gr4vyTheme !== void 0 ? { theme: gr4vyTheme } : {},
|
|
29
|
+
onComplete: (txn) => {
|
|
30
|
+
onAdded(txn.paymentMethod?.id);
|
|
31
|
+
},
|
|
32
|
+
onEvent: (eventName, data) => {
|
|
33
|
+
if (eventName === "transactionFailed") {
|
|
34
|
+
onError(data);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
),
|
|
39
|
+
/* @__PURE__ */ jsx(
|
|
40
|
+
"button",
|
|
41
|
+
{
|
|
42
|
+
type: "button",
|
|
43
|
+
onClick: () => {
|
|
44
|
+
embedRef.current?.submit();
|
|
45
|
+
},
|
|
46
|
+
style: {
|
|
47
|
+
marginTop: "12px",
|
|
48
|
+
padding: "8px 16px",
|
|
49
|
+
backgroundColor: theme?.colorPrimary ?? "#1D56E8",
|
|
50
|
+
color: "#fff",
|
|
51
|
+
border: "none",
|
|
52
|
+
borderRadius: "8px",
|
|
53
|
+
fontSize: "14px",
|
|
54
|
+
fontWeight: 600,
|
|
55
|
+
cursor: "pointer",
|
|
56
|
+
width: "100%"
|
|
57
|
+
},
|
|
58
|
+
children: "Save card"
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
"button",
|
|
63
|
+
{
|
|
64
|
+
type: "button",
|
|
65
|
+
onClick: onCancel,
|
|
66
|
+
style: {
|
|
67
|
+
marginTop: "8px",
|
|
68
|
+
padding: "8px 16px",
|
|
69
|
+
backgroundColor: "transparent",
|
|
70
|
+
color: theme?.colorText ?? "#111827",
|
|
71
|
+
border: `1px solid ${theme?.colorBorder ?? "#E5E7EB"}`,
|
|
72
|
+
borderRadius: "8px",
|
|
73
|
+
fontSize: "14px",
|
|
74
|
+
cursor: "pointer",
|
|
75
|
+
width: "100%"
|
|
76
|
+
},
|
|
77
|
+
children: "Cancel"
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
] });
|
|
81
|
+
}
|
|
82
|
+
var Gr4vyAddCard_default = Gr4vyAddCard;
|
|
83
|
+
export {
|
|
84
|
+
Gr4vyAddCard,
|
|
85
|
+
Gr4vyAddCard_default as default
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=Gr4vyAddCard-J3HC6KTK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/Gr4vyAddCard.tsx"],"sourcesContent":["/**\n * Gr4vyAddCard — lazy-loaded vault-only embed wrapper.\n *\n * THIS FILE MUST ONLY BE LOADED VIA React.lazy(() => import('./Gr4vyAddCard'))\n * from PaymentMethods.tsx. Never import it at the module top-level of any file\n * that is part of the server bundle — @gr4vy/embed-react touches `window` at\n * module evaluation time and will throw `ReferenceError: window is not defined`\n * in SSR contexts.\n */\nimport React, { useRef } from 'react';\nimport Gr4vyEmbed, { type EmbedInstance } from '@gr4vy/embed-react';\nimport type { EmbedPayload, PaymentMethodsTheme } from './types';\n\ninterface Gr4vyTransaction {\n id: string;\n status: string;\n paymentMethod?: { id?: string };\n}\n\nexport interface Gr4vyAddCardProps {\n payload: EmbedPayload;\n theme?: PaymentMethodsTheme;\n onAdded: (paymentMethodId?: string) => void;\n onCancel: () => void;\n onError: (err: unknown) => void;\n}\n\nexport function Gr4vyAddCard({ payload, theme, onAdded, onCancel, onError }: Gr4vyAddCardProps) {\n const embedRef = useRef<EmbedInstance>(null);\n\n const colors = theme\n ? {\n ...(theme.colorPrimary !== undefined ? { primary: theme.colorPrimary } : {}),\n ...(theme.colorText !== undefined ? { text: theme.colorText } : {}),\n ...(theme.colorBorder !== undefined ? { inputBorder: theme.colorBorder } : {}),\n }\n : {};\n const gr4vyTheme = Object.keys(colors).length ? { colors } : undefined;\n\n return (\n <div>\n <Gr4vyEmbed\n ref={embedRef}\n gr4vyId={payload.gr4vyId}\n environment={payload.environment}\n token={payload.embedToken}\n amount={payload.amount}\n currency={payload.currency}\n merchantAccountId={payload.merchantAccountId}\n intent=\"capture\"\n paymentSource=\"recurring\"\n store={true}\n display=\"addOnly\"\n {...(gr4vyTheme !== undefined ? { theme: gr4vyTheme } : {})}\n onComplete={(txn: Gr4vyTransaction) => {\n onAdded(txn.paymentMethod?.id);\n }}\n onEvent={(eventName: string, data: unknown) => {\n if (eventName === 'transactionFailed') {\n onError(data);\n }\n }}\n />\n\n <button\n type=\"button\"\n onClick={() => {\n embedRef.current?.submit();\n }}\n style={{\n marginTop: '12px',\n padding: '8px 16px',\n backgroundColor: theme?.colorPrimary ?? '#1D56E8',\n color: '#fff',\n border: 'none',\n borderRadius: '8px',\n fontSize: '14px',\n fontWeight: 600,\n cursor: 'pointer',\n width: '100%',\n }}\n >\n Save card\n </button>\n\n <button\n type=\"button\"\n onClick={onCancel}\n style={{\n marginTop: '8px',\n padding: '8px 16px',\n backgroundColor: 'transparent',\n color: theme?.colorText ?? '#111827',\n border: `1px solid ${theme?.colorBorder ?? '#E5E7EB'}`,\n borderRadius: '8px',\n fontSize: '14px',\n cursor: 'pointer',\n width: '100%',\n }}\n >\n Cancel\n </button>\n </div>\n );\n}\n\nexport default Gr4vyAddCard;\n"],"mappings":";AASA,SAAgB,cAAc;AAC9B,OAAO,gBAAwC;AA8B3C,SACE,KADF;AAbG,SAAS,aAAa,EAAE,SAAS,OAAO,SAAS,UAAU,QAAQ,GAAsB;AAC9F,QAAM,WAAW,OAAsB,IAAI;AAE3C,QAAM,SAAS,QACX;AAAA,IACE,GAAI,MAAM,iBAAiB,SAAY,EAAE,SAAS,MAAM,aAAa,IAAI,CAAC;AAAA,IAC1E,GAAI,MAAM,cAAc,SAAY,EAAE,MAAM,MAAM,UAAU,IAAI,CAAC;AAAA,IACjE,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;AAAA,EAC9E,IACA,CAAC;AACL,QAAM,aAAa,OAAO,KAAK,MAAM,EAAE,SAAS,EAAE,OAAO,IAAI;AAE7D,SACE,qBAAC,SACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,SAAS,QAAQ;AAAA,QACjB,aAAa,QAAQ;AAAA,QACrB,OAAO,QAAQ;AAAA,QACf,QAAQ,QAAQ;AAAA,QAChB,UAAU,QAAQ;AAAA,QAClB,mBAAmB,QAAQ;AAAA,QAC3B,QAAO;AAAA,QACP,eAAc;AAAA,QACd,OAAO;AAAA,QACP,SAAQ;AAAA,QACP,GAAI,eAAe,SAAY,EAAE,OAAO,WAAW,IAAI,CAAC;AAAA,QACzD,YAAY,CAAC,QAA0B;AACrC,kBAAQ,IAAI,eAAe,EAAE;AAAA,QAC/B;AAAA,QACA,SAAS,CAAC,WAAmB,SAAkB;AAC7C,cAAI,cAAc,qBAAqB;AACrC,oBAAQ,IAAI;AAAA,UACd;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM;AACb,mBAAS,SAAS,OAAO;AAAA,QAC3B;AAAA,QACA,OAAO;AAAA,UACL,WAAW;AAAA,UACX,SAAS;AAAA,UACT,iBAAiB,OAAO,gBAAgB;AAAA,UACxC,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,QAAQ;AAAA,UACR,OAAO;AAAA,QACT;AAAA,QACD;AAAA;AAAA,IAED;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,UACL,WAAW;AAAA,UACX,SAAS;AAAA,UACT,iBAAiB;AAAA,UACjB,OAAO,OAAO,aAAa;AAAA,UAC3B,QAAQ,aAAa,OAAO,eAAe,SAAS;AAAA,UACpD,cAAc;AAAA,UACd,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,OAAO;AAAA,QACT;AAAA,QACD;AAAA;AAAA,IAED;AAAA,KACF;AAEJ;AAEA,IAAO,uBAAQ;","names":[]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
import { ThrottleError } from "@usethrottle/errors";
|
|
3
|
+
import { ThrottleError as ThrottleError2, isThrottleError } from "@usethrottle/errors";
|
|
4
|
+
var ThrottlePaymentMethodsError = class extends ThrottleError {
|
|
5
|
+
constructor(args) {
|
|
6
|
+
super(args);
|
|
7
|
+
this.name = "ThrottlePaymentMethodsError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/types.ts
|
|
12
|
+
var DEFAULT_BASE_URL = "https://api.usethrottle.dev";
|
|
13
|
+
|
|
14
|
+
// src/request.ts
|
|
15
|
+
async function request(input) {
|
|
16
|
+
const res = await fetch(`${input.baseUrl}${input.path}`, {
|
|
17
|
+
method: input.method,
|
|
18
|
+
headers: {
|
|
19
|
+
...input.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
20
|
+
...input.headers ?? {}
|
|
21
|
+
},
|
|
22
|
+
...input.body !== void 0 ? { body: JSON.stringify(input.body) } : {}
|
|
23
|
+
});
|
|
24
|
+
const text = await res.text();
|
|
25
|
+
let parsed = {};
|
|
26
|
+
try {
|
|
27
|
+
parsed = text ? JSON.parse(text) : {};
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const e = parsed?.error ?? {};
|
|
32
|
+
throw new ThrottlePaymentMethodsError({
|
|
33
|
+
code: e.code ?? "request_failed",
|
|
34
|
+
message: e.message ?? `Request failed with ${res.status}`,
|
|
35
|
+
statusCode: res.status,
|
|
36
|
+
details: e.details
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
ThrottlePaymentMethodsError,
|
|
44
|
+
ThrottleError2 as ThrottleError,
|
|
45
|
+
isThrottleError,
|
|
46
|
+
DEFAULT_BASE_URL,
|
|
47
|
+
request
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=chunk-T2CV3T5K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/types.ts","../src/request.ts"],"sourcesContent":["import { ThrottleError } from '@usethrottle/errors';\n\nexport class ThrottlePaymentMethodsError extends ThrottleError {\n constructor(args: { code: string; message: string; statusCode: number; details?: unknown }) {\n super(args);\n this.name = 'ThrottlePaymentMethodsError';\n }\n}\n\nexport { ThrottleError, isThrottleError } from '@usethrottle/errors';\n","export interface SavedCard {\n id: string;\n methodType: string;\n cardBrand: string | null;\n cardLastFour: string | null;\n cardExpMonth: number | null;\n cardExpYear: number | null;\n isDefault: boolean;\n}\n\nexport interface EmbedPayload {\n embedToken: string;\n checkoutSessionId: string;\n amount: number;\n currency: string;\n gr4vyId: string;\n merchantAccountId?: string;\n environment: 'sandbox' | 'production';\n hostedUrl?: string;\n embedUrl?: string;\n}\n\nexport interface MintedClientToken {\n token: string;\n expiresAt: string;\n}\n\nexport interface PaymentMethodsTheme {\n colorPrimary?: string;\n colorText?: string;\n colorBorder?: string;\n borderRadius?: string;\n}\n\nexport const DEFAULT_BASE_URL = 'https://api.usethrottle.dev';\n","import { ThrottlePaymentMethodsError } from './errors';\n\nexport async function request(input: {\n method: string;\n path: string;\n baseUrl: string;\n headers?: Record<string, string>;\n body?: unknown;\n}): Promise<any> {\n const res = await fetch(`${input.baseUrl}${input.path}`, {\n method: input.method,\n headers: {\n ...(input.body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...(input.headers ?? {}),\n },\n ...(input.body !== undefined ? { body: JSON.stringify(input.body) } : {}),\n });\n const text = await res.text();\n let parsed: any = {};\n try {\n parsed = text ? JSON.parse(text) : {};\n } catch {\n // Non-JSON body (e.g. an HTML 502/503 gateway page). Leave parsed as {} so\n // the non-2xx branch below still throws a typed ThrottlePaymentMethodsError\n // (code 'request_failed') rather than leaking a raw SyntaxError.\n }\n if (!res.ok) {\n const e = parsed?.error ?? {};\n throw new ThrottlePaymentMethodsError({\n code: e.code ?? 'request_failed',\n message: e.message ?? `Request failed with ${res.status}`,\n statusCode: res.status,\n details: e.details,\n });\n }\n return parsed;\n}\n"],"mappings":";AAAA,SAAS,qBAAqB;AAS9B,SAAS,iBAAAA,gBAAe,uBAAuB;AAPxC,IAAM,8BAAN,cAA0C,cAAc;AAAA,EAC7D,YAAY,MAAgF;AAC1F,UAAM,IAAI;AACV,SAAK,OAAO;AAAA,EACd;AACF;;;AC2BO,IAAM,mBAAmB;;;AChChC,eAAsB,QAAQ,OAMb;AACf,QAAM,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,GAAG,MAAM,IAAI,IAAI;AAAA,IACvD,QAAQ,MAAM;AAAA,IACd,SAAS;AAAA,MACP,GAAI,MAAM,SAAS,SAAY,EAAE,gBAAgB,mBAAmB,IAAI,CAAC;AAAA,MACzE,GAAI,MAAM,WAAW,CAAC;AAAA,IACxB;AAAA,IACA,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,KAAK,UAAU,MAAM,IAAI,EAAE,IAAI,CAAC;AAAA,EACzE,CAAC;AACD,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,SAAc,CAAC;AACnB,MAAI;AACF,aAAS,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,EACtC,QAAQ;AAAA,EAIR;AACA,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,QAAQ,SAAS,CAAC;AAC5B,UAAM,IAAI,4BAA4B;AAAA,MACpC,MAAM,EAAE,QAAQ;AAAA,MAChB,SAAS,EAAE,WAAW,uBAAuB,IAAI,MAAM;AAAA,MACvD,YAAY,IAAI;AAAA,MAChB,SAAS,EAAE;AAAA,IACb,CAAC;AAAA,EACH;AACA,SAAO;AACT;","names":["ThrottleError"]}
|