bitsnap-react 1.14.0 → 1.15.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/AGENTS.md +246 -0
- package/README.md +99 -51
- package/dist/CartProvider-64QEJ7A7.js +2 -0
- package/dist/CartProvider-64QEJ7A7.js.map +1 -0
- package/dist/{CartProvider-GLUQHAG3.mjs → CartProvider-XKK7A3C2.mjs} +2 -2
- package/dist/analytics.js +1 -1
- package/dist/analytics.js.map +1 -1
- package/dist/analytics.mjs +1 -1
- package/dist/catalog.js +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/catalog.mjs +1 -1
- package/dist/{chunk-MVOJMCZG.mjs → chunk-2TLWMYXW.mjs} +2 -2
- package/dist/{chunk-5DDLYVBG.js → chunk-7DG4UPWE.js} +2 -2
- package/dist/chunk-7DG4UPWE.js.map +1 -0
- package/dist/{chunk-VY7RNSFP.mjs → chunk-7TLJKYZD.mjs} +2 -2
- package/dist/{chunk-U5SAO2SH.mjs → chunk-AZQOW73L.mjs} +2 -2
- package/dist/{chunk-U5SAO2SH.mjs.map → chunk-AZQOW73L.mjs.map} +1 -1
- package/dist/{chunk-AWG5OAK7.js → chunk-DY2S42Q6.js} +2 -2
- package/dist/chunk-DY2S42Q6.js.map +1 -0
- package/dist/{chunk-7SYNGA3R.js → chunk-FGB5FJCI.js} +2 -2
- package/dist/chunk-FGB5FJCI.js.map +1 -0
- package/dist/{chunk-JXVAT2ZE.js → chunk-GLAJPUMA.js} +2 -2
- package/dist/chunk-GLAJPUMA.js.map +1 -0
- package/dist/{chunk-FLDI3BPY.js → chunk-H4WAQQVO.js} +2 -2
- package/dist/chunk-H4WAQQVO.js.map +1 -0
- package/dist/chunk-JSDGZ4NY.js.map +1 -1
- package/dist/chunk-KOYBZORL.js.map +1 -1
- package/dist/{chunk-3N2VK426.js → chunk-MG6VV5J2.js} +2 -2
- package/dist/chunk-MG6VV5J2.js.map +1 -0
- package/dist/{chunk-3OEKRPWV.mjs → chunk-PEVB3VL4.mjs} +2 -2
- package/dist/{chunk-3OEKRPWV.mjs.map → chunk-PEVB3VL4.mjs.map} +1 -1
- package/dist/{chunk-CYLZ5GYW.mjs → chunk-Q3DSK66F.mjs} +2 -2
- package/dist/{chunk-SKFE6TUP.mjs → chunk-RWEP6IUC.mjs} +2 -2
- package/dist/{chunk-GKFET34B.mjs → chunk-S4Z4O46O.mjs} +2 -2
- package/dist/{chunk-R2M6W4LD.js → chunk-VY6CZVVA.js} +3 -3
- package/dist/chunk-VY6CZVVA.js.map +1 -0
- package/dist/chunk-W3RGQYPD.js.map +1 -1
- package/dist/{chunk-PLS5RR27.js → chunk-XZYCQ655.js} +2 -2
- package/dist/chunk-XZYCQ655.js.map +1 -0
- package/dist/{chunk-VYYUTJZB.mjs → chunk-ZIWQ57GY.mjs} +2 -2
- package/dist/consent.js +14 -7
- package/dist/consent.js.map +1 -1
- package/dist/consent.mjs +14 -7
- package/dist/consent.mjs.map +1 -1
- package/dist/course.js +1 -1
- package/dist/course.js.map +1 -1
- package/dist/course.mjs +1 -1
- package/dist/embed/analytics.js +1 -1
- package/dist/embed/consent.js +14 -7
- package/dist/errors.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/methods-NUMVR53L.js +2 -0
- package/dist/methods-NUMVR53L.js.map +1 -0
- package/dist/methods-YKZZ6OVT.mjs +2 -0
- package/dist/models.js.map +1 -1
- package/dist/returns.js +1 -1
- package/dist/returns.js.map +1 -1
- package/dist/returns.mjs +1 -1
- package/package.json +17 -16
- package/dist/CartProvider-6D5WE2HA.js +0 -2
- package/dist/CartProvider-6D5WE2HA.js.map +0 -1
- package/dist/chunk-3N2VK426.js.map +0 -1
- package/dist/chunk-5DDLYVBG.js.map +0 -1
- package/dist/chunk-7SYNGA3R.js.map +0 -1
- package/dist/chunk-AWG5OAK7.js.map +0 -1
- package/dist/chunk-FLDI3BPY.js.map +0 -1
- package/dist/chunk-JXVAT2ZE.js.map +0 -1
- package/dist/chunk-PLS5RR27.js.map +0 -1
- package/dist/chunk-R2M6W4LD.js.map +0 -1
- package/dist/methods-N35A2OGE.mjs +0 -2
- package/dist/methods-WSICR2JH.js +0 -2
- package/dist/methods-WSICR2JH.js.map +0 -1
- /package/dist/{CartProvider-GLUQHAG3.mjs.map → CartProvider-XKK7A3C2.mjs.map} +0 -0
- /package/dist/{chunk-MVOJMCZG.mjs.map → chunk-2TLWMYXW.mjs.map} +0 -0
- /package/dist/{chunk-VY7RNSFP.mjs.map → chunk-7TLJKYZD.mjs.map} +0 -0
- /package/dist/{chunk-CYLZ5GYW.mjs.map → chunk-Q3DSK66F.mjs.map} +0 -0
- /package/dist/{chunk-SKFE6TUP.mjs.map → chunk-RWEP6IUC.mjs.map} +0 -0
- /package/dist/{chunk-GKFET34B.mjs.map → chunk-S4Z4O46O.mjs.map} +0 -0
- /package/dist/{chunk-VYYUTJZB.mjs.map → chunk-ZIWQ57GY.mjs.map} +0 -0
- /package/dist/{methods-N35A2OGE.mjs.map → methods-YKZZ6OVT.mjs.map} +0 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Integrate bitsnap-react
|
|
2
|
+
|
|
3
|
+
`bitsnap-react` provides checkout, Apple Pay, returns and the customer panel.
|
|
4
|
+
Use a Next.js client component or an Astro React island. Use `bitsnap-js` on the
|
|
5
|
+
server for products, secret checkout operations and webhooks.
|
|
6
|
+
|
|
7
|
+
## Install and configure
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install bitsnap-js bitsnap-react
|
|
11
|
+
# Astro also needs @astrojs/react: npx astro add react
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use these application env names; the SDK does not load them:
|
|
15
|
+
|
|
16
|
+
- `BITSNAP_PROJECT_ID`: public. Pass it from the server as `projectID`.
|
|
17
|
+
If using a public env var, only the project ID may use `NEXT_PUBLIC_`/`PUBLIC_`.
|
|
18
|
+
- `BITSNAP_API_KEY`: secret, server-only. Never `NEXT_PUBLIC_*`/`PUBLIC_*`.
|
|
19
|
+
- `BITSNAP_WEBHOOK_SECRET`: separate webhook secret, also server-only.
|
|
20
|
+
|
|
21
|
+
Store secrets in ignored `.env.local` (Next.js), `.env` (Astro) or deployment
|
|
22
|
+
server variables. Normal checkout needs no API key. Never pass keys in props,
|
|
23
|
+
HTML, browser checkout calls or agent chat.
|
|
24
|
+
|
|
25
|
+
## Minimal checkout
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
"use client";
|
|
29
|
+
|
|
30
|
+
import { Bitsnap, BitsnapCheckout, setProjectID } from "bitsnap-react";
|
|
31
|
+
import "bitsnap-react/dist/index.css";
|
|
32
|
+
|
|
33
|
+
type Product = { id: string; name: string };
|
|
34
|
+
|
|
35
|
+
export default function Checkout({ projectID, products }: {
|
|
36
|
+
projectID: string;
|
|
37
|
+
products: Product[];
|
|
38
|
+
}) {
|
|
39
|
+
async function buy(id: string) {
|
|
40
|
+
setProjectID(projectID);
|
|
41
|
+
const error = await Bitsnap.addProductToCart(id, 1);
|
|
42
|
+
if (error) {
|
|
43
|
+
alert("Could not add this product. Try again.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
Bitsnap.showCart();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
{products.map((product) => (
|
|
52
|
+
<button key={product.id} onClick={() => buy(product.id)}>
|
|
53
|
+
Buy {product.name}
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
<BitsnapCheckout projectID={projectID} locale="pl" />
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Fetch products with `BitsnapBackend` on the server and pass only public fields.
|
|
63
|
+
Import the exported CSS. Configure the project before calling cart methods.
|
|
64
|
+
For Astro, omit `"use client"`, and mount with `client:load` or `client:visible`.
|
|
65
|
+
|
|
66
|
+
## Other components
|
|
67
|
+
|
|
68
|
+
- `ApplePayButton` requires `items` with `id`, `name`, `price` (minor units) and
|
|
69
|
+
`quantity`. It has no `projectID` prop. Call `setProjectID(projectID)` on the
|
|
70
|
+
client before mounting. Use an effect and a ready flag for standalone buttons.
|
|
71
|
+
Configure Apple Pay and test on an eligible device over HTTPS.
|
|
72
|
+
- `BitsnapReturns` takes `projectID` and optional `locale`, `appearance`, `texts`.
|
|
73
|
+
- For the panel, configure `setProjectID` and `setLoginURL("/panel/login")` on
|
|
74
|
+
the client before mounting `<PanelProvider><Panel /></PanelProvider>`.
|
|
75
|
+
Mount `<PanelProvider><PanelLogin /></PanelProvider>` at that login URL.
|
|
76
|
+
|
|
77
|
+
## Avoid these mistakes
|
|
78
|
+
|
|
79
|
+
- Neither checkout UI component has a test API key prop. For test purchases, use
|
|
80
|
+
`Bitsnap.createCheckout(projectID, { items, apiKey, testMode: true })` from
|
|
81
|
+
`bitsnap-js` on the server. Do not pass keys to React's `createCheckout`.
|
|
82
|
+
- `bitsnap-react`'s own `createCheckout` sends `apiKey` as an `Authorization:
|
|
83
|
+
Bearer` header and is server-side only. Never call it from a client component,
|
|
84
|
+
island or browser bundle. A normal checkout needs no key; omit `apiKey`.
|
|
85
|
+
- `bitsnap-react` does not export `handleWebhook`. Import it from `bitsnap-js`
|
|
86
|
+
only in a server route. Pass `request.text()` unchanged, full URL and headers.
|
|
87
|
+
Verify `X-Content-Signature` or `sig` before processing or fulfilment.
|
|
88
|
+
- In Next.js use a Node route at `app/api/bitsnap/webhook/route.ts`. In Astro use
|
|
89
|
+
`src/pages/api/bitsnap/webhook.ts`, `prerender = false` and a Node adapter;
|
|
90
|
+
check `output`. A static build alone cannot receive webhook POSTs.
|
|
91
|
+
- Check the error object returned by `addProductToCart`. Do not show a successful
|
|
92
|
+
cart state when adding the item failed.
|
|
93
|
+
- Mount one cart per page/project. Project configuration is global.
|
|
94
|
+
|
|
95
|
+
## Verify
|
|
96
|
+
|
|
97
|
+
1. Configure a test gateway and make a test purchase with a project API key used
|
|
98
|
+
for server test checkout. Current keys have no distinct test/live prefix.
|
|
99
|
+
2. Confirm a Test webhook arrives and HMAC verifies. Reject tampered bodies and
|
|
100
|
+
wrong projects. Deduplicate events and separate test/production fulfilment.
|
|
101
|
+
3. Build and grep actual secret values in the client output and source maps.
|
|
102
|
+
Set nonempty variables privately; quiet grep avoids printing keys.
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
grep -rFq -- "$BITSNAP_API_KEY" .next/static
|
|
106
|
+
grep -rFq -- "$BITSNAP_WEBHOOK_SECRET" .next/static
|
|
107
|
+
# Astro with Node: use dist/client/_astro instead of .next/static.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Exit 0 means a leak, 1 means no match, 2 means a search error. Check the adapter's
|
|
111
|
+
client output path. Fix leaks and rotate exposed keys before deploying.
|
|
112
|
+
|
|
113
|
+
## Full recipes
|
|
114
|
+
|
|
115
|
+
- [Next.js](https://docs.bitsnap.pl/en/guides/nextjs/)
|
|
116
|
+
- [Astro](https://docs.bitsnap.pl/en/guides/astro/)
|
|
117
|
+
- [SDK reference](https://docs.bitsnap.pl/en/reference/example/)
|
|
118
|
+
- [MCP](https://docs.bitsnap.pl/en/guides/mcp/)
|
|
119
|
+
- [bitsnap-integrate skill](https://docs.bitsnap.pl/skills/bitsnap-integrate/SKILL.md)
|
|
120
|
+
|
|
121
|
+
## Internals (for contributors)
|
|
122
|
+
|
|
123
|
+
## Product list (`ProductList`, `useProducts`)
|
|
124
|
+
|
|
125
|
+
Renders a shop's product catalog. Used by the Bitsnap custom-domain home page
|
|
126
|
+
and meant for embedding on merchants' own sites. Import from `bitsnap-react` or
|
|
127
|
+
from `bitsnap-react/catalog` (list and hook only; the checkout is loaded lazily
|
|
128
|
+
on the first "Add to cart" click). Load `bitsnap-react/dist/index.css` once.
|
|
129
|
+
|
|
130
|
+
Minimal embed:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { ProductList } from "bitsnap-react/catalog";
|
|
134
|
+
import "bitsnap-react/dist/index.css";
|
|
135
|
+
|
|
136
|
+
export function Shop() {
|
|
137
|
+
return (
|
|
138
|
+
<ProductList
|
|
139
|
+
projectID="PROJECT_ID"
|
|
140
|
+
locale="pl-PL"
|
|
141
|
+
productHref={(p) => `https://shop.example.com/p/${p.uniqueSlug}`}
|
|
142
|
+
onAddToCart={(p) => myCart.add(p.cartItemId ?? p.id)} // optional, default is Bitsnap.addProductToCart
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Rules:
|
|
149
|
+
|
|
150
|
+
- Endpoint: `GET {host}/api/integrations/{projectID}/public-commerce/product-list?limit&offset&locale&search`
|
|
151
|
+
(`limit` default 24, max 48). `host` defaults to `https://bitsnap.pl`.
|
|
152
|
+
- Prices are minor units (grosze, cents).
|
|
153
|
+
- Pass `initialData` (`{ items, totalCount }`, the first page) to render the
|
|
154
|
+
products in the server HTML. Without it the server renders a skeleton and the
|
|
155
|
+
browser fetches.
|
|
156
|
+
- `productHref` defaults to the relative `/p/{uniqueSlug}`. On any site other
|
|
157
|
+
than the shop's own domain, return the full URL.
|
|
158
|
+
- CORS: the embedding site's domain must be in the project's site, success or
|
|
159
|
+
cancel URLs, or in the allowed domains. Otherwise the browser blocks the
|
|
160
|
+
request. Custom domains need nothing.
|
|
161
|
+
- Add to cart: products with `addToCart: true` get a button calling
|
|
162
|
+
`onAddToCart(product)`, or by default `Bitsnap.addProductToCart(id, 1)` and
|
|
163
|
+
then `Bitsnap.showCart()` (mount `BitsnapCheckout` on the page). It adds
|
|
164
|
+
`cartItemId ?? id` (the variant ID for single-variant products). The default
|
|
165
|
+
action sets the cart's global project ID/host from the list props only when
|
|
166
|
+
the page has not set them; if the page cart belongs to another project or
|
|
167
|
+
host it changes nothing, warns and shows the error state, so such a page
|
|
168
|
+
must pass `onAddToCart`.
|
|
169
|
+
- Cart links: `Bitsnap.addProductToCart(id, qty, metadata, { url })` stores the
|
|
170
|
+
product page with the cart line (localStorage), and the cart links the
|
|
171
|
+
item's image and name to it. Relative URLs are resolved against the page
|
|
172
|
+
that adds the item; only `http`/`https` are kept (`cartItemURL`). The
|
|
173
|
+
`ADD_TO_CART` postMessage accepts `url` too. `ProductList`'s default action
|
|
174
|
+
passes `productHref(product)`.
|
|
175
|
+
- Styling: `appearance={{ theme, tokens, elements }}`, scoped to
|
|
176
|
+
`[data-bitsnap-products]`. Texts: `texts`, Polish and English defaults by
|
|
177
|
+
`locale`.
|
|
178
|
+
- Do not use React Query, `setProjectID` or `setCustomHost` for the list
|
|
179
|
+
itself; `useProducts` takes everything as arguments.
|
|
180
|
+
|
|
181
|
+
## Cart checkout (`BitsnapCheckout`)
|
|
182
|
+
|
|
183
|
+
"Next step" posts the cart to `{host}/api/integrations/{projectID}/public-commerce/buy`
|
|
184
|
+
(`createPaymentURL` in `src/components/checkout/methods.ts`, body built by
|
|
185
|
+
`cartLinkRequest` in `CartProvider.tsx`) and redirects to the returned `url`.
|
|
186
|
+
|
|
187
|
+
- Payment method choice: while the drawer shows a cart with a country,
|
|
188
|
+
`payment-methods` gets the same body (react-query, keyed by the cart lines,
|
|
189
|
+
country and locale). A non-null `choice` renders `PaymentMethodChoice`
|
|
190
|
+
(online, transfer / BLIK to the seller's phone, `cod` cash on delivery; first
|
|
191
|
+
of `methods` selected) and `/buy` gets `paymentMethod`. `cod` refused by the
|
|
192
|
+
shop comes back as `gateway-not-configured` and shows
|
|
193
|
+
`paymentMethodCodUnavailable`. Anything else (null, error, 404 of an old backend, 5 s
|
|
194
|
+
timeout) is "no choice": plain `/buy`, no `paymentMethod`. A click before the
|
|
195
|
+
answer waits for it and, if a choice arrives, shows it instead of buying.
|
|
196
|
+
While a new country or cart is asked for, the last choice stays as
|
|
197
|
+
`placeholderData` without `cod` (`withoutCod`); a click then fetches the new
|
|
198
|
+
answer and buys only if it still offers the selected method.
|
|
199
|
+
- Pending order (`pending-order.ts`): the cart is kept until a
|
|
200
|
+
`/payment/common/...` order is paid, or `placed` (a `cod` order the buyer
|
|
201
|
+
accepted on the order's details form; it is paid to the courier). A
|
|
202
|
+
`/payment/manualPayment/<project>/<order>` page is a placed order paid
|
|
203
|
+
outside Bitsnap, so the cart is cleared at once.
|
|
204
|
+
- Express buttons (Apple Pay, Google Pay) are separate online flows.
|
|
205
|
+
- Tests mock `fetch` by URL: `cart-buy-error.test.tsx`, `cart-payment-method.test.tsx`.
|
|
206
|
+
|
|
207
|
+
## Course view (`PanelCourse`)
|
|
208
|
+
|
|
209
|
+
The customer panel opens a purchased course inside the panel (no redirect to
|
|
210
|
+
kursy.bitsnap.pl). Import `PanelCourse` from `bitsnap-react/course`; the panel
|
|
211
|
+
itself loads it with a dynamic import, so the video player (hls.js), DOMPurify
|
|
212
|
+
and the tests are not in the main ESM bundle.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { PanelCourse } from "bitsnap-react/course";
|
|
216
|
+
|
|
217
|
+
<PanelCourse accessId={product.accessId} onBack={() => history.back()} />
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Rules:
|
|
221
|
+
|
|
222
|
+
- Data goes through `CourseDataSource` (`src/course/types.ts`). The default is
|
|
223
|
+
`createRpcCourseDataSource({ accessId })`: `UserPanelCourseService` on
|
|
224
|
+
`{host}/api/rpc` with the panel session token (`accessId` + `accessToken`
|
|
225
|
+
like `userPanelGetCourseDetails`; no project or course id in requests).
|
|
226
|
+
Pass `dataSource` to feed another source (the creator preview of the
|
|
227
|
+
dashboard) and `preview` to show the "Podgląd" badge and skip progress saving.
|
|
228
|
+
- Deep links: the panel keeps the course in the URL, `?state=course&course=<accessId>&item=<lessonOrTestId>`
|
|
229
|
+
(values are JSON-encoded by `useUrlSearchParams` like the other params; plain
|
|
230
|
+
values work too). Reload keeps the lesson; browser back/forward move between
|
|
231
|
+
lessons. `PanelProductGrid` opens courses directly; `PanelProductDetails` of a
|
|
232
|
+
course product shows an "open the course" button.
|
|
233
|
+
- Lesson HTML is always rendered through `sanitizeLessonHtml` (DOMPurify; no
|
|
234
|
+
iframes, forms, inline styles). It returns `""` without a DOM, so lesson HTML
|
|
235
|
+
never appears in server-rendered output.
|
|
236
|
+
- Video: `hls.js`, native HLS on Safari. Position is saved every ~15 s while
|
|
237
|
+
playing and on pause/end/leaving; a lesson completes at 80 %, as in the LMS (`logic.ts`).
|
|
238
|
+
- Texts: `texts` prop over Polish/English tables (`src/course/texts.ts`).
|
|
239
|
+
- The contract is drafted in `proto-draft/` (see its README); generated code in
|
|
240
|
+
`src/gen/proto/public/v1/user_panel_course_*` comes from
|
|
241
|
+
`./proto-course-draft.sh`.
|
|
242
|
+
|
|
243
|
+
Tests: `npm test` (unit), `pnpm test:catalog-ssr` (builds, then renders the
|
|
244
|
+
list on the server from `initialData`), `pnpm test:course-ssr` (builds, then
|
|
245
|
+
renders the course view on the server). Never run `pnpm build:publish` unless
|
|
246
|
+
asked to release: it publishes to npm.
|
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Bitsnap Checkout
|
|
2
2
|
|
|
3
|
+
## Instructions for coding agents
|
|
4
|
+
|
|
5
|
+
Read [AGENTS.md](./AGENTS.md) before integrating this package. Follow the
|
|
6
|
+
[Next.js](https://docs.bitsnap.pl/en/guides/nextjs/) or
|
|
7
|
+
[Astro](https://docs.bitsnap.pl/en/guides/astro/) recipe. Keep API keys and webhook
|
|
8
|
+
secrets on the server.
|
|
9
|
+
|
|
3
10
|
The library is designed to provide a complete e-commerce checkout solution that can be easily integrated into web applications, with focus on:
|
|
4
11
|
|
|
5
12
|
- User experience
|
|
@@ -782,19 +789,35 @@ async function legacyHandleCart() {
|
|
|
782
789
|
}
|
|
783
790
|
```
|
|
784
791
|
|
|
785
|
-
5. Creating Checkout/Payment:
|
|
792
|
+
5. Creating Checkout/Payment (server-side only):
|
|
786
793
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
794
|
+
`createCheckout` sends an `Authorization: Bearer` header, so a browser key would
|
|
795
|
+
be visible to anyone loading your page. Create the checkout in a server route or
|
|
796
|
+
server action and redirect the browser to the returned URL:
|
|
797
|
+
|
|
798
|
+
```ts
|
|
799
|
+
// app/api/bitsnap/checkout/route.ts
|
|
800
|
+
import { Bitsnap, type LinkRequest } from "bitsnap-js";
|
|
801
|
+
|
|
802
|
+
export const runtime = "nodejs";
|
|
803
|
+
|
|
804
|
+
export async function POST(request: Request) {
|
|
805
|
+
const projectID = process.env.BITSNAP_PROJECT_ID;
|
|
806
|
+
const apiKey = process.env.BITSNAP_API_KEY;
|
|
807
|
+
if (!projectID || !apiKey) {
|
|
808
|
+
return new Response("Not configured", { status: 500 });
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Pick the fields you accept. Do not forward a raw client body to the API.
|
|
812
|
+
const { items } = (await request.json()) as {
|
|
813
|
+
items?: Array<{ id: string; quantity: number }>;
|
|
814
|
+
};
|
|
815
|
+
if (!items?.length) {
|
|
816
|
+
return new Response("No items", { status: 400 });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const linkRequest: LinkRequest = {
|
|
820
|
+
items,
|
|
798
821
|
details: {
|
|
799
822
|
email: "customer@example.com",
|
|
800
823
|
name: "John Doe",
|
|
@@ -807,20 +830,27 @@ async function handleCheckout() {
|
|
|
807
830
|
},
|
|
808
831
|
askForAddress: true,
|
|
809
832
|
askForPhone: true,
|
|
833
|
+
apiKey,
|
|
834
|
+
testMode: true,
|
|
810
835
|
};
|
|
811
836
|
|
|
812
|
-
const result = await createCheckout(
|
|
813
|
-
...request,
|
|
814
|
-
apiKey: "your-api-key",
|
|
815
|
-
testMode: true,
|
|
816
|
-
});
|
|
837
|
+
const result = await Bitsnap.createCheckout(projectID, linkRequest);
|
|
817
838
|
|
|
818
|
-
|
|
819
|
-
|
|
839
|
+
// The SDK does not check the HTTP status. Check that a URL came back.
|
|
840
|
+
if (!result.redirectURL) {
|
|
841
|
+
return new Response("Checkout failed", { status: 502 });
|
|
820
842
|
}
|
|
843
|
+
return Response.json({ url: result.redirectURL });
|
|
821
844
|
}
|
|
822
845
|
```
|
|
823
846
|
|
|
847
|
+
`bitsnap-react` also exports its own `createCheckout(request)`. It takes the same
|
|
848
|
+
request and sends `apiKey` as an `Authorization: Bearer` header, so it is
|
|
849
|
+
**server-side only**: calling it from a client component, an island or any
|
|
850
|
+
browser bundle would expose the project API key to every visitor. A normal
|
|
851
|
+
checkout needs no key at all. For a keyed server checkout use
|
|
852
|
+
`Bitsnap.createCheckout(projectID, request)` from `bitsnap-js`.
|
|
853
|
+
|
|
824
854
|
On success `createCheckout` returns `{ status: "ok", redirectURL }`. A failed
|
|
825
855
|
response no longer throws and no longer yields an undefined `redirectURL`: it
|
|
826
856
|
returns an `Err` with `status: "error"` instead (`result.ERR === true`), typed
|
|
@@ -855,32 +885,56 @@ When the backend lists unavailable products (`itemIds`) under
|
|
|
855
885
|
`item-ids-not-found` and `data` is the array of ids. With any other code the
|
|
856
886
|
code is kept and the ids are in `data.itemIds`.
|
|
857
887
|
|
|
858
|
-
6. Webhook Handler:
|
|
859
|
-
|
|
860
888
|
```tsx
|
|
861
|
-
|
|
889
|
+
// Client component: no key, only the returned URL.
|
|
890
|
+
const { url } = await fetch("/api/bitsnap/checkout", {
|
|
891
|
+
method: "POST",
|
|
892
|
+
headers: { "Content-Type": "application/json" },
|
|
893
|
+
body: JSON.stringify({ items: [{ id: "product-id", quantity: 1 }] }),
|
|
894
|
+
}).then((r) => r.json());
|
|
895
|
+
window.location.href = url;
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
Without a key, the browser can still create a normal checkout for a public
|
|
899
|
+
product by adding it to the cart and calling `Bitsnap.showCart()`. Only server
|
|
900
|
+
test checkout needs `apiKey`.
|
|
901
|
+
|
|
902
|
+
6. Webhook Handler (server-side only):
|
|
903
|
+
|
|
904
|
+
`handleWebhook` is exported by `bitsnap-js`, not by `bitsnap-react`. Install
|
|
905
|
+
`bitsnap-js` and run this in a server route.
|
|
906
|
+
|
|
907
|
+
```ts
|
|
908
|
+
// app/api/bitsnap/webhook/route.ts
|
|
909
|
+
import { Event, handleWebhook } from "bitsnap-js";
|
|
910
|
+
|
|
911
|
+
export const runtime = "nodejs";
|
|
862
912
|
|
|
863
|
-
async function processWebhook(req: Request) {
|
|
913
|
+
export async function processWebhook(req: Request) {
|
|
914
|
+
const webhookSecret = process.env.BITSNAP_WEBHOOK_SECRET;
|
|
915
|
+
if (!webhookSecret) return new Response("Not configured", { status: 500 });
|
|
916
|
+
|
|
917
|
+
// Pass the raw body and the full URL. Never call req.json() first.
|
|
864
918
|
const payload = await req.text();
|
|
865
|
-
const url = req.url;
|
|
866
919
|
const headers = Object.fromEntries(req.headers);
|
|
867
|
-
const webhookSecret = "your-webhook-secret";
|
|
868
920
|
|
|
869
|
-
const result = await handleWebhook(payload, url, headers, webhookSecret);
|
|
921
|
+
const result = await handleWebhook(payload, req.url, headers, webhookSecret);
|
|
870
922
|
|
|
871
923
|
if (result.isErr) {
|
|
872
924
|
console.error("Webhook error:", result.error);
|
|
873
|
-
return;
|
|
925
|
+
return new Response("Invalid signature", { status: 401 });
|
|
874
926
|
}
|
|
875
927
|
|
|
876
928
|
// Process the webhook event
|
|
877
929
|
const { projectId, environment, eventData } = result;
|
|
878
930
|
|
|
931
|
+
// Parsed enums are numeric. Compare against the exported Event enum,
|
|
932
|
+
// not against the string "TRANSACTION_SUCCESS".
|
|
879
933
|
switch (eventData?.event) {
|
|
880
|
-
case
|
|
934
|
+
case Event.TRANSACTION_SUCCESS:
|
|
881
935
|
// Handle successful transaction
|
|
882
936
|
break;
|
|
883
|
-
case
|
|
937
|
+
case Event.TRANSACTION_FAILURE:
|
|
884
938
|
// Handle failed transaction
|
|
885
939
|
break;
|
|
886
940
|
// Handle other events...
|
|
@@ -891,14 +945,13 @@ async function processWebhook(req: Request) {
|
|
|
891
945
|
Complete Example:
|
|
892
946
|
|
|
893
947
|
```tsx
|
|
948
|
+
"use client";
|
|
949
|
+
|
|
894
950
|
import {
|
|
895
951
|
BitsnapCheckout,
|
|
896
952
|
setProjectID,
|
|
897
953
|
setCustomHost,
|
|
898
954
|
Bitsnap,
|
|
899
|
-
createCheckout,
|
|
900
|
-
handleWebhook,
|
|
901
|
-
type LinkRequest,
|
|
902
955
|
} from "bitsnap-react";
|
|
903
956
|
|
|
904
957
|
// Configure globally
|
|
@@ -914,30 +967,22 @@ function Store() {
|
|
|
914
967
|
Bitsnap.showCart();
|
|
915
968
|
}
|
|
916
969
|
|
|
970
|
+
// A hosted checkout link without an API key: create it in a server route
|
|
971
|
+
// (see "Creating Checkout/Payment" above) and follow the returned URL.
|
|
917
972
|
async function processCheckout(
|
|
918
973
|
items: Array<{ id: string; quantity: number }>,
|
|
919
974
|
) {
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
details: {
|
|
925
|
-
email: "customer@example.com",
|
|
926
|
-
},
|
|
927
|
-
redirect: {
|
|
928
|
-
successURL: "/success",
|
|
929
|
-
cancelURL: "/cancel",
|
|
930
|
-
},
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
const result = await createCheckout({
|
|
934
|
-
...checkoutRequest,
|
|
935
|
-
testMode: true,
|
|
975
|
+
const response = await fetch("/api/bitsnap/checkout", {
|
|
976
|
+
method: "POST",
|
|
977
|
+
headers: { "Content-Type": "application/json" },
|
|
978
|
+
body: JSON.stringify({ items }),
|
|
936
979
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
980
|
+
if (!response.ok) {
|
|
981
|
+
console.error("Checkout failed");
|
|
982
|
+
return;
|
|
940
983
|
}
|
|
984
|
+
const { url } = await response.json();
|
|
985
|
+
window.location.href = url;
|
|
941
986
|
}
|
|
942
987
|
|
|
943
988
|
return (
|
|
@@ -955,6 +1000,9 @@ function Store() {
|
|
|
955
1000
|
}
|
|
956
1001
|
```
|
|
957
1002
|
|
|
1003
|
+
`createCheckout` and `handleWebhook` are not part of this client bundle: the
|
|
1004
|
+
keyed checkout and webhook verification belong to `bitsnap-js` on the server.
|
|
1005
|
+
|
|
958
1006
|
---
|
|
959
1007
|
|
|
960
1008
|
## Capabilities
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkVY6CZVVAjs = require('./chunk-VY6CZVVA.js');require('./chunk-H4WAQQVO.js');require('./chunk-KOYBZORL.js');require('./chunk-7DG4UPWE.js');require('./chunk-FGB5FJCI.js');var _chunkDY2S42Q6js = require('./chunk-DY2S42Q6.js');require('./chunk-MG6VV5J2.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.MARKETING_AGREEMENT_ID = _chunkVY6CZVVAjs.L; exports.bitsnapProjectID = _chunkVY6CZVVAjs.M; exports.default = _chunkVY6CZVVAjs.T; exports.getCartLines = _chunkVY6CZVVAjs.Q; exports.getCheckoutMethods = _chunkVY6CZVVAjs.U; exports.getCheckoutSelection = _chunkVY6CZVVAjs.P; exports.getLocale = _chunkDY2S42Q6js.m; exports.getProjectID = _chunkVY6CZVVAjs.O; exports.reconcileOrderOfLastPayment = _chunkVY6CZVVAjs.R; exports.setLocale = _chunkDY2S42Q6js.l; exports.setProjectID = _chunkVY6CZVVAjs.N; exports.useCartProvider = _chunkVY6CZVVAjs.S;
|
|
2
|
+
//# sourceMappingURL=CartProvider-64QEJ7A7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/private/tmp/bsr-src/dist/CartProvider-64QEJ7A7.js"],"names":[],"mappings":"AAAA,+HAAsG,+BAA4B,+BAA4B,+BAA4B,+BAA4B,sDAA+C,+BAA4B,+BAA4B,+BAA4B,siBAAmQ","file":"/private/tmp/bsr-src/dist/CartProvider-64QEJ7A7.js"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{L as c,M as d,N as e,O as f,P as g,Q as h,R as i,S as j,T as k,U as l}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=CartProvider-
|
|
1
|
+
import{L as c,M as d,N as e,O as f,P as g,Q as h,R as i,S as j,T as k,U as l}from"./chunk-AZQOW73L.mjs";import"./chunk-Q3DSK66F.mjs";import"./chunk-KOVKUJAW.mjs";import"./chunk-RWEP6IUC.mjs";import"./chunk-2TLWMYXW.mjs";import{l as a,m as b}from"./chunk-S4Z4O46O.mjs";import"./chunk-PEVB3VL4.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{c as MARKETING_AGREEMENT_ID,d as bitsnapProjectID,k as default,h as getCartLines,l as getCheckoutMethods,g as getCheckoutSelection,b as getLocale,f as getProjectID,i as reconcileOrderOfLastPayment,a as setLocale,e as setProjectID,j as useCartProvider};
|
|
2
|
+
//# sourceMappingURL=CartProvider-XKK7A3C2.mjs.map
|
package/dist/analytics.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true});var
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkMG6VV5J2js = require('./chunk-MG6VV5J2.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.BitsnapAnalyticsProvider = _chunkMG6VV5J2js.i; exports.DEFAULT_ENDPOINT = _chunkMG6VV5J2js.c; exports.InstrumentProvider = _chunkMG6VV5J2js.l; exports.createTracker = _chunkMG6VV5J2js.d; exports.getTracker = _chunkMG6VV5J2js.f; exports.initTracker = _chunkMG6VV5J2js.e; exports.noopInstrument = _chunkMG6VV5J2js.k; exports.resetTracker = _chunkMG6VV5J2js.g; exports.scoreBucket = _chunkMG6VV5J2js.r; exports.startAutoPageViews = _chunkMG6VV5J2js.b; exports.useAnalytics = _chunkMG6VV5J2js.j; exports.useInstrument = _chunkMG6VV5J2js.n; exports.useInstrumentContext = _chunkMG6VV5J2js.m;
|
|
2
2
|
//# sourceMappingURL=analytics.js.map
|
package/dist/analytics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/
|
|
1
|
+
{"version":3,"sources":["/private/tmp/bsr-src/dist/analytics.js"],"names":[],"mappings":"AAAA,+HAAsH,+BAA4B,+BAA4B,klBAAsR","file":"/private/tmp/bsr-src/dist/analytics.js"}
|
package/dist/analytics.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{b as e,c as t,d as r,e as n,f as o,g as a,i as s,j as p,k as c,l as i,m,n as u,r as v}from"./chunk-
|
|
1
|
+
import{b as e,c as t,d as r,e as n,f as o,g as a,i as s,j as p,k as c,l as i,m,n as u,r as v}from"./chunk-PEVB3VL4.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{s as BitsnapAnalyticsProvider,t as DEFAULT_ENDPOINT,i as InstrumentProvider,r as createTracker,o as getTracker,n as initTracker,c as noopInstrument,a as resetTracker,v as scoreBucket,e as startAutoPageViews,p as useAnalytics,u as useInstrument,m as useInstrumentContext};
|
|
2
2
|
//# sourceMappingURL=analytics.mjs.map
|
package/dist/catalog.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true});var
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunkGLAJPUMAjs = require('./chunk-GLAJPUMA.js');require('./chunk-KOYBZORL.js');require('./chunk-DY2S42Q6.js');require('./chunk-MG6VV5J2.js');require('./chunk-W3RGQYPD.js');require('./chunk-JSDGZ4NY.js');exports.PRODUCT_LIST_SLOT_NAMES = _chunkGLAJPUMAjs.a; exports.ProductList = _chunkGLAJPUMAjs.d; exports.formatProductPrice = _chunkGLAJPUMAjs.b; exports.useProducts = _chunkGLAJPUMAjs.c;
|
|
2
2
|
//# sourceMappingURL=catalog.js.map
|
package/dist/catalog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/
|
|
1
|
+
{"version":3,"sources":["/private/tmp/bsr-src/dist/catalog.js"],"names":[],"mappings":"AAAA,+HAA4D,+BAA4B,+BAA4B,+BAA4B,+BAA4B,+BAA4B,0LAA+F","file":"/private/tmp/bsr-src/dist/catalog.js"}
|
package/dist/catalog.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as o,b as r,c as e,d as f}from"./chunk-
|
|
1
|
+
import{a as o,b as r,c as e,d as f}from"./chunk-ZIWQ57GY.mjs";import"./chunk-KOVKUJAW.mjs";import"./chunk-S4Z4O46O.mjs";import"./chunk-PEVB3VL4.mjs";import"./chunk-MDCP5JNP.mjs";import"./chunk-U6FLIRIV.mjs";export{o as PRODUCT_LIST_SLOT_NAMES,f as ProductList,r as formatProductPrice,e as useProducts};
|
|
2
2
|
//# sourceMappingURL=catalog.mjs.map
|