@snap-pay/js 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ Initial public release.
6
+
7
+ - `Snap` class — constructs from a publishable key (`pk_test_` or `pk_live_`). Rejects secret keys.
8
+ - `snap.elements({ clientSecret, appearance })` — creates an Elements container bound to a checkout session.
9
+ - `snap.confirmPayment({ elements, clientSecret })` — reads the customer's selection from the mounted Elements and dispatches the payment. Attaches billing/customer/otp form data automatically when those elements are mounted.
10
+ - Provider environment derived from the key prefix (`pk_live_*` → live).
11
+ - Publishable-key HTTP client with a 30s AbortController-backed timeout.
12
+ - Element registry — `@snap-pay/elements` populates it on import; `snap.elements(...).create(type)` looks up the factory.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @snap-pay/js
2
+
3
+ SNAP.js — client-side SDK for creating and confirming SNAP payments in the browser.
4
+
5
+ Pairs with `@snap-pay/elements` (UI components) and `@snap-pay/react` (React bindings).
6
+
7
+ ```bash
8
+ bun add @snap-pay/js @snap-pay/elements
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { Snap } from "@snap-pay/js";
15
+ import "@snap-pay/elements";
16
+
17
+ const snap = new Snap("pk_test_…");
18
+ const elements = snap.elements({ clientSecret: sessionId });
19
+ const payment = elements.create("payment");
20
+ payment.mount("#pay");
21
+
22
+ const result = await snap.confirmPayment({ elements, clientSecret: sessionId });
23
+ if (result.status === "succeeded") window.location.href = "/thanks";
24
+ ```
25
+
26
+ ## Options
27
+
28
+ ```ts
29
+ new Snap(publishableKey, {
30
+ apiBase: "http://localhost:3210", // for local dev / self-hosted
31
+ cardFrameUrl: "http://localhost:3000/elements/card-frame",
32
+ });
33
+ ```
34
+
35
+ - `apiBase` — defaults to the SNAP production API. Point at a local Convex deployment during development.
36
+ - `cardFrameUrl` — the origin hosting the Card iframe. Defaults to SNAP's hosted page; override for local dev or self-hosted deployments.
37
+
38
+ ## `confirmPayment`
39
+
40
+ ```ts
41
+ const result = await snap.confirmPayment({ elements, clientSecret });
42
+ // result.status: "succeeded" | "processing" | "pending" | "failed" | ...
43
+ // result.transactionId: the SNAP transaction id
44
+ ```
45
+
46
+ The SDK reads the customer's selection from the mounted Elements. Wallet selection sends `provider` directly; Card selection sends `provider: "mock"` alongside `card_token` / `card_brand` / `card_last4` metadata in sandbox. Live-mode Card tokenization throws explicitly — it's on the roadmap for a subsequent release.
47
+
48
+ Any form-collecting elements (BillingAddressElement, CustomerElement, OtpElement) that are mounted inside the same Elements set contribute their values automatically — no extra plumbing.
49
+
50
+ ## Security
51
+
52
+ - Always pass a **publishable** key (`pk_test_` or `pk_live_`). Secret keys (`sk_…`) belong on the server and must never ship to the browser — the constructor refuses them.
53
+ - The `clientSecret` is a checkout session id. It grants payment rights only for that session and its owning merchant.
54
+
55
+ ## License
56
+
57
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var SnapError = class extends Error {
5
+ constructor(message, code, type, requestId) {
6
+ super(message);
7
+ this.name = "SnapError";
8
+ this.code = code;
9
+ this.type = type;
10
+ this.requestId = requestId;
11
+ }
12
+ };
13
+ var SnapAuthenticationError = class extends SnapError {
14
+ constructor(message, requestId) {
15
+ super(message, "authentication_failed", "authentication_error", requestId);
16
+ this.name = "SnapAuthenticationError";
17
+ }
18
+ };
19
+ var SnapValidationError = class extends SnapError {
20
+ constructor(message, requestId) {
21
+ super(message, "validation_error", "invalid_request_error", requestId);
22
+ this.name = "SnapValidationError";
23
+ }
24
+ };
25
+
26
+ // src/session-client.ts
27
+ var DEFAULT_API_BASE = "https://api.snap.so";
28
+ var DEFAULT_TIMEOUT_MS = 3e4;
29
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
32
+ try {
33
+ return await fetch(url, { ...init, signal: controller.signal });
34
+ } catch (err) {
35
+ if (err instanceof DOMException && err.name === "AbortError") {
36
+ throw new SnapError(
37
+ `Request timed out after ${timeoutMs} ms.`,
38
+ "timeout",
39
+ "api_connection_error"
40
+ );
41
+ }
42
+ throw err;
43
+ } finally {
44
+ clearTimeout(timer);
45
+ }
46
+ }
47
+ async function throwFromResponse(response) {
48
+ let envelope;
49
+ try {
50
+ envelope = await response.json();
51
+ } catch {
52
+ }
53
+ const err = envelope?.error;
54
+ const message = err?.message ?? `Request failed with status ${response.status}`;
55
+ const code = err?.code ?? `http_${response.status}`;
56
+ const type = err?.type ?? "api_error";
57
+ const requestId = err?.request_id;
58
+ if (response.status === 401 || code === "authentication_failed") {
59
+ throw new SnapAuthenticationError(message, requestId);
60
+ }
61
+ if (response.status === 400 || code === "validation_error") {
62
+ throw new SnapValidationError(message, requestId);
63
+ }
64
+ throw new SnapError(message, code, type, requestId);
65
+ }
66
+ async function fetchSession(apiBase, publishableKey, clientSecret) {
67
+ const url = `${apiBase.replace(/\/+$/, "")}/v1/checkout/sessions/${encodeURIComponent(clientSecret)}`;
68
+ const response = await fetchWithTimeout(url, {
69
+ method: "GET",
70
+ headers: {
71
+ Authorization: `Bearer ${publishableKey}`,
72
+ Accept: "application/json"
73
+ },
74
+ // Elements are cross-origin by construction — the merchant's
75
+ // page is not on snap.so. `cors` is the fetch default but
76
+ // stating it makes the intent readable.
77
+ mode: "cors",
78
+ credentials: "omit"
79
+ });
80
+ if (!response.ok) await throwFromResponse(response);
81
+ const body = await response.json();
82
+ return toPublicSession(body);
83
+ }
84
+ async function startPayment(apiBase, publishableKey, clientSecret, provider, customer, metadata) {
85
+ const url = `${apiBase.replace(/\/+$/, "")}/v1/checkout/sessions/${encodeURIComponent(clientSecret)}/pay`;
86
+ const body = { provider };
87
+ if (customer) body.customer = customer;
88
+ if (metadata) Object.assign(body, metadata);
89
+ const response = await fetchWithTimeout(url, {
90
+ method: "POST",
91
+ headers: {
92
+ Authorization: `Bearer ${publishableKey}`,
93
+ "Content-Type": "application/json",
94
+ Accept: "application/json"
95
+ },
96
+ body: JSON.stringify(body),
97
+ mode: "cors",
98
+ credentials: "omit"
99
+ });
100
+ if (!response.ok) await throwFromResponse(response);
101
+ const wire = await response.json();
102
+ return { transactionId: wire.transaction_id, status: wire.status };
103
+ }
104
+ function toPublicSession(w) {
105
+ return {
106
+ id: w.id,
107
+ amount: w.amount,
108
+ currency: w.currency,
109
+ allowedProviders: w.allowed_providers,
110
+ status: w.status,
111
+ expiresAt: w.expires_at,
112
+ transactionId: w.transaction_id,
113
+ merchantName: w.merchant_name,
114
+ customer: w.customer,
115
+ successUrl: w.success_url,
116
+ cancelUrl: w.cancel_url
117
+ };
118
+ }
119
+
120
+ // src/elements.ts
121
+ var REGISTRY = /* @__PURE__ */ new Map();
122
+ function registerElement(type, factory) {
123
+ REGISTRY.set(type, factory);
124
+ }
125
+ function registeredTypes() {
126
+ return Array.from(REGISTRY.keys());
127
+ }
128
+ var SnapElements = class {
129
+ constructor(args) {
130
+ // Registration-order children — each carries a getter for its
131
+ // current selection. Insertion-order iteration on JavaScript
132
+ // Sets is stable; new elements append to the tail. Deregistration
133
+ // (via the returned cleanup fn) drops the child entirely so a
134
+ // destroyed element can't accidentally answer a later
135
+ // `_getSelection` call.
136
+ this.children = /* @__PURE__ */ new Set();
137
+ // Form-data getters, keyed by (type, opaque token). One element
138
+ // may register a getter and another element of the same type may
139
+ // register another — first-registered wins on `_getValue`, which
140
+ // matches the visual convention that the earliest-mounted
141
+ // element on the page is the primary one.
142
+ this.values = /* @__PURE__ */ new Map();
143
+ if (!args.options.clientSecret) {
144
+ throw new Error("`clientSecret` is required when creating Elements.");
145
+ }
146
+ this.clientSecret = args.options.clientSecret;
147
+ this.appearance = args.options.appearance ?? {};
148
+ this.publishableKey = args.publishableKey;
149
+ this.apiBase = args.apiBase;
150
+ this.environment = args.environment;
151
+ this.cardFrameUrl = args.cardFrameUrl;
152
+ }
153
+ create(type, options = {}) {
154
+ const factory = REGISTRY.get(type);
155
+ if (!factory) {
156
+ throw new Error(
157
+ `Element type "${type}" is not implemented in this SDK version. Supported: ${registeredTypes().map((t) => `'${t}'`).join(", ") || "(none \u2014 did you import @snap-pay/elements?)"}.`
158
+ );
159
+ }
160
+ return factory({
161
+ clientSecret: this.clientSecret,
162
+ appearance: this.appearance,
163
+ publishableKey: this.publishableKey,
164
+ apiBase: this.apiBase,
165
+ environment: this.environment,
166
+ cardFrameUrl: this.cardFrameUrl,
167
+ registerChild: (getSelection) => {
168
+ this.children.add(getSelection);
169
+ return () => this.children.delete(getSelection);
170
+ },
171
+ registerValue: (childType, getValue) => {
172
+ let bucket = this.values.get(childType);
173
+ if (!bucket) {
174
+ bucket = /* @__PURE__ */ new Set();
175
+ this.values.set(childType, bucket);
176
+ }
177
+ bucket.add(getValue);
178
+ return () => bucket.delete(getValue);
179
+ },
180
+ options
181
+ });
182
+ }
183
+ _getSelection() {
184
+ for (const getSelection of this.children) {
185
+ const s = getSelection();
186
+ if (s !== null) return s;
187
+ }
188
+ return null;
189
+ }
190
+ _getValue(type) {
191
+ const bucket = this.values.get(type);
192
+ if (!bucket) return null;
193
+ for (const getValue of bucket) {
194
+ const v = getValue();
195
+ if (v !== null && v !== void 0) return v;
196
+ }
197
+ return null;
198
+ }
199
+ };
200
+
201
+ // src/snap.ts
202
+ var DEFAULT_CARD_FRAME_URL = "https://checkout.snap.so/elements/card-frame";
203
+ var Snap = class {
204
+ constructor(publishableKey, options = {}) {
205
+ if (typeof publishableKey !== "string" || publishableKey.length === 0) {
206
+ throw new Error("Snap requires a publishable key (pk_test_\u2026 or pk_live_\u2026).");
207
+ }
208
+ if (!publishableKey.startsWith("pk_")) {
209
+ throw new Error(
210
+ "Publishable key must start with 'pk_test_' or 'pk_live_'. Secret keys (sk_\u2026) are for server code only and must NEVER be shipped to the browser."
211
+ );
212
+ }
213
+ this.publishableKey = publishableKey;
214
+ this.apiBase = options.apiBase ?? DEFAULT_API_BASE;
215
+ this.environment = publishableKey.startsWith("pk_live_") ? "live" : "sandbox";
216
+ this.cardFrameUrl = options.cardFrameUrl ?? DEFAULT_CARD_FRAME_URL;
217
+ }
218
+ // Creates an Elements set bound to a specific client secret
219
+ // (checkout session id). Returned handle stays local — the SDK
220
+ // deliberately does NOT stash it in module scope so multiple
221
+ // sessions can coexist on the same page (useful for merchants
222
+ // rendering upsell + main-cart Elements simultaneously).
223
+ elements(options) {
224
+ return new SnapElements({
225
+ publishableKey: this.publishableKey,
226
+ apiBase: this.apiBase,
227
+ environment: this.environment,
228
+ cardFrameUrl: this.cardFrameUrl,
229
+ options
230
+ });
231
+ }
232
+ // Confirms the payment by asking the server to dispatch it to
233
+ // the payment method the customer chose inside the mounted
234
+ // Elements. Two selection kinds:
235
+ // - "provider": Wallet flow (evcPlus / zaad / …). Sends the
236
+ // provider straight to the /pay endpoint.
237
+ // - "card": Card Element sandbox flow. Sends provider="mock"
238
+ // alongside `card_token` metadata so the backend can drive
239
+ // the transaction through the mock adapter (real Mastercard
240
+ // tokenization is deferred to a future PR). Live-mode card
241
+ // tokens throw explicitly.
242
+ //
243
+ // Throws if no selection is available — the UI should keep the
244
+ // confirm button disabled until an Element reports a `change`
245
+ // with `complete: true`.
246
+ async confirmPayment(options) {
247
+ const selection = options.elements._getSelection();
248
+ if (selection === null) {
249
+ throw new Error(
250
+ "No payment method selected. Ask the customer to pick a provider before confirming."
251
+ );
252
+ }
253
+ const getVal = options.elements._getValue?.bind(options.elements);
254
+ const customer = getVal?.("customer") ?? void 0;
255
+ const billing = getVal?.("billingAddress") ?? void 0;
256
+ const otp = getVal?.("otp") ?? null;
257
+ const extraMetadata = {};
258
+ if (billing) {
259
+ const billingKeyMap = {
260
+ name: "billing_name",
261
+ line1: "billing_line1",
262
+ line2: "billing_line2",
263
+ city: "billing_city",
264
+ region: "billing_region",
265
+ postalCode: "billing_postal_code",
266
+ country: "billing_country"
267
+ };
268
+ for (const [k, v] of Object.entries(billing)) {
269
+ if (typeof v === "string" && v.length > 0) {
270
+ const wireKey = billingKeyMap[k];
271
+ if (wireKey) extraMetadata[wireKey] = v;
272
+ }
273
+ }
274
+ }
275
+ if (otp && typeof otp.code === "string" && otp.code.length > 0) {
276
+ extraMetadata.otp_code = otp.code;
277
+ }
278
+ if (selection.kind === "card") {
279
+ if (this.environment === "live") {
280
+ throw new Error(
281
+ "Card Element live-mode tokenization is not yet supported. Use a `pk_test_` key or wire a wallet Element for live payments."
282
+ );
283
+ }
284
+ const result2 = await startPayment(
285
+ this.apiBase,
286
+ this.publishableKey,
287
+ options.clientSecret,
288
+ "mock",
289
+ customer,
290
+ {
291
+ card_token: selection.token,
292
+ card_brand: selection.brand,
293
+ card_last4: selection.last4,
294
+ ...extraMetadata
295
+ }
296
+ );
297
+ return { transactionId: result2.transactionId, status: result2.status };
298
+ }
299
+ const result = await startPayment(
300
+ this.apiBase,
301
+ this.publishableKey,
302
+ options.clientSecret,
303
+ selection.provider,
304
+ customer,
305
+ Object.keys(extraMetadata).length > 0 ? extraMetadata : void 0
306
+ );
307
+ return { transactionId: result.transactionId, status: result.status };
308
+ }
309
+ };
310
+
311
+ exports.DEFAULT_API_BASE = DEFAULT_API_BASE;
312
+ exports.Snap = Snap;
313
+ exports.SnapAuthenticationError = SnapAuthenticationError;
314
+ exports.SnapElements = SnapElements;
315
+ exports.SnapError = SnapError;
316
+ exports.SnapValidationError = SnapValidationError;
317
+ exports.fetchSession = fetchSession;
318
+ exports.registerElement = registerElement;
319
+ exports.startPayment = startPayment;
@@ -0,0 +1,74 @@
1
+ import * as _snap_pay_types from '@snap-pay/types';
2
+ import { Appearance, Environment, SnapSelection, ElementType, CreateElementOptions, SnapElementHandle, SnapElementsHandle, ElementsOptions, SnapOptions, ConfirmPaymentOptions, PaymentResult, PublicSession, CheckoutProvider } from '@snap-pay/types';
3
+ export { Appearance, CheckoutProvider, ConfirmPaymentOptions, CreateElementOptions, ElementEventHandler, ElementEventMap, ElementEventName, ElementType, ElementsOptions, Environment, PaymentResult, PaymentStatus, PublicSession, PublicSessionResponse, SessionStatus, SnapElementHandle, SnapElementsHandle, SnapOptions, WalletProvider } from '@snap-pay/types';
4
+
5
+ type ElementFactory = (args: {
6
+ clientSecret: string;
7
+ appearance: Appearance;
8
+ publishableKey: string;
9
+ apiBase: string;
10
+ environment: Environment;
11
+ cardFrameUrl: string | undefined;
12
+ registerChild: (getSelection: () => SnapSelection | null) => () => void;
13
+ registerValue: (type: ElementType, getValue: () => unknown | null) => () => void;
14
+ options: CreateElementOptions;
15
+ }) => SnapElementHandle;
16
+ declare function registerElement(type: ElementType, factory: ElementFactory): void;
17
+ interface SnapElementsConstructorArgs {
18
+ publishableKey: string;
19
+ apiBase: string;
20
+ environment: Environment;
21
+ cardFrameUrl: string | undefined;
22
+ options: ElementsOptions;
23
+ }
24
+ declare class SnapElements implements SnapElementsHandle {
25
+ readonly clientSecret: string;
26
+ readonly appearance: Appearance;
27
+ private children;
28
+ private values;
29
+ private readonly publishableKey;
30
+ private readonly apiBase;
31
+ private readonly environment;
32
+ private readonly cardFrameUrl;
33
+ constructor(args: SnapElementsConstructorArgs);
34
+ create(type: ElementType, options?: CreateElementOptions): SnapElementHandle;
35
+ _getSelection(): SnapSelection | null;
36
+ _getValue(type: ElementType): unknown | null;
37
+ }
38
+
39
+ declare class Snap {
40
+ private readonly publishableKey;
41
+ private readonly apiBase;
42
+ private readonly environment;
43
+ private readonly cardFrameUrl;
44
+ constructor(publishableKey: string, options?: SnapOptions);
45
+ elements(options: ElementsOptions): SnapElements;
46
+ confirmPayment(options: ConfirmPaymentOptions): Promise<PaymentResult>;
47
+ }
48
+
49
+ declare class SnapError extends Error {
50
+ readonly code: string;
51
+ readonly type: string;
52
+ readonly requestId?: string;
53
+ constructor(message: string, code: string, type: string, requestId?: string);
54
+ }
55
+ declare class SnapAuthenticationError extends SnapError {
56
+ constructor(message: string, requestId?: string);
57
+ }
58
+ declare class SnapValidationError extends SnapError {
59
+ constructor(message: string, requestId?: string);
60
+ }
61
+
62
+ declare const DEFAULT_API_BASE = "https://api.snap.so";
63
+ declare function fetchSession(apiBase: string, publishableKey: string, clientSecret: string): Promise<PublicSession>;
64
+ declare function startPayment(apiBase: string, publishableKey: string, clientSecret: string, provider: CheckoutProvider, customer?: {
65
+ name?: string;
66
+ email?: string;
67
+ phone?: string;
68
+ country?: string;
69
+ }, metadata?: Record<string, string>): Promise<{
70
+ transactionId: string;
71
+ status: _snap_pay_types.PaymentStatus;
72
+ }>;
73
+
74
+ export { DEFAULT_API_BASE, type ElementFactory, Snap, SnapAuthenticationError, SnapElements, SnapError, SnapValidationError, fetchSession, registerElement, startPayment };
@@ -0,0 +1,74 @@
1
+ import * as _snap_pay_types from '@snap-pay/types';
2
+ import { Appearance, Environment, SnapSelection, ElementType, CreateElementOptions, SnapElementHandle, SnapElementsHandle, ElementsOptions, SnapOptions, ConfirmPaymentOptions, PaymentResult, PublicSession, CheckoutProvider } from '@snap-pay/types';
3
+ export { Appearance, CheckoutProvider, ConfirmPaymentOptions, CreateElementOptions, ElementEventHandler, ElementEventMap, ElementEventName, ElementType, ElementsOptions, Environment, PaymentResult, PaymentStatus, PublicSession, PublicSessionResponse, SessionStatus, SnapElementHandle, SnapElementsHandle, SnapOptions, WalletProvider } from '@snap-pay/types';
4
+
5
+ type ElementFactory = (args: {
6
+ clientSecret: string;
7
+ appearance: Appearance;
8
+ publishableKey: string;
9
+ apiBase: string;
10
+ environment: Environment;
11
+ cardFrameUrl: string | undefined;
12
+ registerChild: (getSelection: () => SnapSelection | null) => () => void;
13
+ registerValue: (type: ElementType, getValue: () => unknown | null) => () => void;
14
+ options: CreateElementOptions;
15
+ }) => SnapElementHandle;
16
+ declare function registerElement(type: ElementType, factory: ElementFactory): void;
17
+ interface SnapElementsConstructorArgs {
18
+ publishableKey: string;
19
+ apiBase: string;
20
+ environment: Environment;
21
+ cardFrameUrl: string | undefined;
22
+ options: ElementsOptions;
23
+ }
24
+ declare class SnapElements implements SnapElementsHandle {
25
+ readonly clientSecret: string;
26
+ readonly appearance: Appearance;
27
+ private children;
28
+ private values;
29
+ private readonly publishableKey;
30
+ private readonly apiBase;
31
+ private readonly environment;
32
+ private readonly cardFrameUrl;
33
+ constructor(args: SnapElementsConstructorArgs);
34
+ create(type: ElementType, options?: CreateElementOptions): SnapElementHandle;
35
+ _getSelection(): SnapSelection | null;
36
+ _getValue(type: ElementType): unknown | null;
37
+ }
38
+
39
+ declare class Snap {
40
+ private readonly publishableKey;
41
+ private readonly apiBase;
42
+ private readonly environment;
43
+ private readonly cardFrameUrl;
44
+ constructor(publishableKey: string, options?: SnapOptions);
45
+ elements(options: ElementsOptions): SnapElements;
46
+ confirmPayment(options: ConfirmPaymentOptions): Promise<PaymentResult>;
47
+ }
48
+
49
+ declare class SnapError extends Error {
50
+ readonly code: string;
51
+ readonly type: string;
52
+ readonly requestId?: string;
53
+ constructor(message: string, code: string, type: string, requestId?: string);
54
+ }
55
+ declare class SnapAuthenticationError extends SnapError {
56
+ constructor(message: string, requestId?: string);
57
+ }
58
+ declare class SnapValidationError extends SnapError {
59
+ constructor(message: string, requestId?: string);
60
+ }
61
+
62
+ declare const DEFAULT_API_BASE = "https://api.snap.so";
63
+ declare function fetchSession(apiBase: string, publishableKey: string, clientSecret: string): Promise<PublicSession>;
64
+ declare function startPayment(apiBase: string, publishableKey: string, clientSecret: string, provider: CheckoutProvider, customer?: {
65
+ name?: string;
66
+ email?: string;
67
+ phone?: string;
68
+ country?: string;
69
+ }, metadata?: Record<string, string>): Promise<{
70
+ transactionId: string;
71
+ status: _snap_pay_types.PaymentStatus;
72
+ }>;
73
+
74
+ export { DEFAULT_API_BASE, type ElementFactory, Snap, SnapAuthenticationError, SnapElements, SnapError, SnapValidationError, fetchSession, registerElement, startPayment };
package/dist/index.js ADDED
@@ -0,0 +1,309 @@
1
+ // src/errors.ts
2
+ var SnapError = class extends Error {
3
+ constructor(message, code, type, requestId) {
4
+ super(message);
5
+ this.name = "SnapError";
6
+ this.code = code;
7
+ this.type = type;
8
+ this.requestId = requestId;
9
+ }
10
+ };
11
+ var SnapAuthenticationError = class extends SnapError {
12
+ constructor(message, requestId) {
13
+ super(message, "authentication_failed", "authentication_error", requestId);
14
+ this.name = "SnapAuthenticationError";
15
+ }
16
+ };
17
+ var SnapValidationError = class extends SnapError {
18
+ constructor(message, requestId) {
19
+ super(message, "validation_error", "invalid_request_error", requestId);
20
+ this.name = "SnapValidationError";
21
+ }
22
+ };
23
+
24
+ // src/session-client.ts
25
+ var DEFAULT_API_BASE = "https://api.snap.so";
26
+ var DEFAULT_TIMEOUT_MS = 3e4;
27
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
30
+ try {
31
+ return await fetch(url, { ...init, signal: controller.signal });
32
+ } catch (err) {
33
+ if (err instanceof DOMException && err.name === "AbortError") {
34
+ throw new SnapError(
35
+ `Request timed out after ${timeoutMs} ms.`,
36
+ "timeout",
37
+ "api_connection_error"
38
+ );
39
+ }
40
+ throw err;
41
+ } finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+ async function throwFromResponse(response) {
46
+ let envelope;
47
+ try {
48
+ envelope = await response.json();
49
+ } catch {
50
+ }
51
+ const err = envelope?.error;
52
+ const message = err?.message ?? `Request failed with status ${response.status}`;
53
+ const code = err?.code ?? `http_${response.status}`;
54
+ const type = err?.type ?? "api_error";
55
+ const requestId = err?.request_id;
56
+ if (response.status === 401 || code === "authentication_failed") {
57
+ throw new SnapAuthenticationError(message, requestId);
58
+ }
59
+ if (response.status === 400 || code === "validation_error") {
60
+ throw new SnapValidationError(message, requestId);
61
+ }
62
+ throw new SnapError(message, code, type, requestId);
63
+ }
64
+ async function fetchSession(apiBase, publishableKey, clientSecret) {
65
+ const url = `${apiBase.replace(/\/+$/, "")}/v1/checkout/sessions/${encodeURIComponent(clientSecret)}`;
66
+ const response = await fetchWithTimeout(url, {
67
+ method: "GET",
68
+ headers: {
69
+ Authorization: `Bearer ${publishableKey}`,
70
+ Accept: "application/json"
71
+ },
72
+ // Elements are cross-origin by construction — the merchant's
73
+ // page is not on snap.so. `cors` is the fetch default but
74
+ // stating it makes the intent readable.
75
+ mode: "cors",
76
+ credentials: "omit"
77
+ });
78
+ if (!response.ok) await throwFromResponse(response);
79
+ const body = await response.json();
80
+ return toPublicSession(body);
81
+ }
82
+ async function startPayment(apiBase, publishableKey, clientSecret, provider, customer, metadata) {
83
+ const url = `${apiBase.replace(/\/+$/, "")}/v1/checkout/sessions/${encodeURIComponent(clientSecret)}/pay`;
84
+ const body = { provider };
85
+ if (customer) body.customer = customer;
86
+ if (metadata) Object.assign(body, metadata);
87
+ const response = await fetchWithTimeout(url, {
88
+ method: "POST",
89
+ headers: {
90
+ Authorization: `Bearer ${publishableKey}`,
91
+ "Content-Type": "application/json",
92
+ Accept: "application/json"
93
+ },
94
+ body: JSON.stringify(body),
95
+ mode: "cors",
96
+ credentials: "omit"
97
+ });
98
+ if (!response.ok) await throwFromResponse(response);
99
+ const wire = await response.json();
100
+ return { transactionId: wire.transaction_id, status: wire.status };
101
+ }
102
+ function toPublicSession(w) {
103
+ return {
104
+ id: w.id,
105
+ amount: w.amount,
106
+ currency: w.currency,
107
+ allowedProviders: w.allowed_providers,
108
+ status: w.status,
109
+ expiresAt: w.expires_at,
110
+ transactionId: w.transaction_id,
111
+ merchantName: w.merchant_name,
112
+ customer: w.customer,
113
+ successUrl: w.success_url,
114
+ cancelUrl: w.cancel_url
115
+ };
116
+ }
117
+
118
+ // src/elements.ts
119
+ var REGISTRY = /* @__PURE__ */ new Map();
120
+ function registerElement(type, factory) {
121
+ REGISTRY.set(type, factory);
122
+ }
123
+ function registeredTypes() {
124
+ return Array.from(REGISTRY.keys());
125
+ }
126
+ var SnapElements = class {
127
+ constructor(args) {
128
+ // Registration-order children — each carries a getter for its
129
+ // current selection. Insertion-order iteration on JavaScript
130
+ // Sets is stable; new elements append to the tail. Deregistration
131
+ // (via the returned cleanup fn) drops the child entirely so a
132
+ // destroyed element can't accidentally answer a later
133
+ // `_getSelection` call.
134
+ this.children = /* @__PURE__ */ new Set();
135
+ // Form-data getters, keyed by (type, opaque token). One element
136
+ // may register a getter and another element of the same type may
137
+ // register another — first-registered wins on `_getValue`, which
138
+ // matches the visual convention that the earliest-mounted
139
+ // element on the page is the primary one.
140
+ this.values = /* @__PURE__ */ new Map();
141
+ if (!args.options.clientSecret) {
142
+ throw new Error("`clientSecret` is required when creating Elements.");
143
+ }
144
+ this.clientSecret = args.options.clientSecret;
145
+ this.appearance = args.options.appearance ?? {};
146
+ this.publishableKey = args.publishableKey;
147
+ this.apiBase = args.apiBase;
148
+ this.environment = args.environment;
149
+ this.cardFrameUrl = args.cardFrameUrl;
150
+ }
151
+ create(type, options = {}) {
152
+ const factory = REGISTRY.get(type);
153
+ if (!factory) {
154
+ throw new Error(
155
+ `Element type "${type}" is not implemented in this SDK version. Supported: ${registeredTypes().map((t) => `'${t}'`).join(", ") || "(none \u2014 did you import @snap-pay/elements?)"}.`
156
+ );
157
+ }
158
+ return factory({
159
+ clientSecret: this.clientSecret,
160
+ appearance: this.appearance,
161
+ publishableKey: this.publishableKey,
162
+ apiBase: this.apiBase,
163
+ environment: this.environment,
164
+ cardFrameUrl: this.cardFrameUrl,
165
+ registerChild: (getSelection) => {
166
+ this.children.add(getSelection);
167
+ return () => this.children.delete(getSelection);
168
+ },
169
+ registerValue: (childType, getValue) => {
170
+ let bucket = this.values.get(childType);
171
+ if (!bucket) {
172
+ bucket = /* @__PURE__ */ new Set();
173
+ this.values.set(childType, bucket);
174
+ }
175
+ bucket.add(getValue);
176
+ return () => bucket.delete(getValue);
177
+ },
178
+ options
179
+ });
180
+ }
181
+ _getSelection() {
182
+ for (const getSelection of this.children) {
183
+ const s = getSelection();
184
+ if (s !== null) return s;
185
+ }
186
+ return null;
187
+ }
188
+ _getValue(type) {
189
+ const bucket = this.values.get(type);
190
+ if (!bucket) return null;
191
+ for (const getValue of bucket) {
192
+ const v = getValue();
193
+ if (v !== null && v !== void 0) return v;
194
+ }
195
+ return null;
196
+ }
197
+ };
198
+
199
+ // src/snap.ts
200
+ var DEFAULT_CARD_FRAME_URL = "https://checkout.snap.so/elements/card-frame";
201
+ var Snap = class {
202
+ constructor(publishableKey, options = {}) {
203
+ if (typeof publishableKey !== "string" || publishableKey.length === 0) {
204
+ throw new Error("Snap requires a publishable key (pk_test_\u2026 or pk_live_\u2026).");
205
+ }
206
+ if (!publishableKey.startsWith("pk_")) {
207
+ throw new Error(
208
+ "Publishable key must start with 'pk_test_' or 'pk_live_'. Secret keys (sk_\u2026) are for server code only and must NEVER be shipped to the browser."
209
+ );
210
+ }
211
+ this.publishableKey = publishableKey;
212
+ this.apiBase = options.apiBase ?? DEFAULT_API_BASE;
213
+ this.environment = publishableKey.startsWith("pk_live_") ? "live" : "sandbox";
214
+ this.cardFrameUrl = options.cardFrameUrl ?? DEFAULT_CARD_FRAME_URL;
215
+ }
216
+ // Creates an Elements set bound to a specific client secret
217
+ // (checkout session id). Returned handle stays local — the SDK
218
+ // deliberately does NOT stash it in module scope so multiple
219
+ // sessions can coexist on the same page (useful for merchants
220
+ // rendering upsell + main-cart Elements simultaneously).
221
+ elements(options) {
222
+ return new SnapElements({
223
+ publishableKey: this.publishableKey,
224
+ apiBase: this.apiBase,
225
+ environment: this.environment,
226
+ cardFrameUrl: this.cardFrameUrl,
227
+ options
228
+ });
229
+ }
230
+ // Confirms the payment by asking the server to dispatch it to
231
+ // the payment method the customer chose inside the mounted
232
+ // Elements. Two selection kinds:
233
+ // - "provider": Wallet flow (evcPlus / zaad / …). Sends the
234
+ // provider straight to the /pay endpoint.
235
+ // - "card": Card Element sandbox flow. Sends provider="mock"
236
+ // alongside `card_token` metadata so the backend can drive
237
+ // the transaction through the mock adapter (real Mastercard
238
+ // tokenization is deferred to a future PR). Live-mode card
239
+ // tokens throw explicitly.
240
+ //
241
+ // Throws if no selection is available — the UI should keep the
242
+ // confirm button disabled until an Element reports a `change`
243
+ // with `complete: true`.
244
+ async confirmPayment(options) {
245
+ const selection = options.elements._getSelection();
246
+ if (selection === null) {
247
+ throw new Error(
248
+ "No payment method selected. Ask the customer to pick a provider before confirming."
249
+ );
250
+ }
251
+ const getVal = options.elements._getValue?.bind(options.elements);
252
+ const customer = getVal?.("customer") ?? void 0;
253
+ const billing = getVal?.("billingAddress") ?? void 0;
254
+ const otp = getVal?.("otp") ?? null;
255
+ const extraMetadata = {};
256
+ if (billing) {
257
+ const billingKeyMap = {
258
+ name: "billing_name",
259
+ line1: "billing_line1",
260
+ line2: "billing_line2",
261
+ city: "billing_city",
262
+ region: "billing_region",
263
+ postalCode: "billing_postal_code",
264
+ country: "billing_country"
265
+ };
266
+ for (const [k, v] of Object.entries(billing)) {
267
+ if (typeof v === "string" && v.length > 0) {
268
+ const wireKey = billingKeyMap[k];
269
+ if (wireKey) extraMetadata[wireKey] = v;
270
+ }
271
+ }
272
+ }
273
+ if (otp && typeof otp.code === "string" && otp.code.length > 0) {
274
+ extraMetadata.otp_code = otp.code;
275
+ }
276
+ if (selection.kind === "card") {
277
+ if (this.environment === "live") {
278
+ throw new Error(
279
+ "Card Element live-mode tokenization is not yet supported. Use a `pk_test_` key or wire a wallet Element for live payments."
280
+ );
281
+ }
282
+ const result2 = await startPayment(
283
+ this.apiBase,
284
+ this.publishableKey,
285
+ options.clientSecret,
286
+ "mock",
287
+ customer,
288
+ {
289
+ card_token: selection.token,
290
+ card_brand: selection.brand,
291
+ card_last4: selection.last4,
292
+ ...extraMetadata
293
+ }
294
+ );
295
+ return { transactionId: result2.transactionId, status: result2.status };
296
+ }
297
+ const result = await startPayment(
298
+ this.apiBase,
299
+ this.publishableKey,
300
+ options.clientSecret,
301
+ selection.provider,
302
+ customer,
303
+ Object.keys(extraMetadata).length > 0 ? extraMetadata : void 0
304
+ );
305
+ return { transactionId: result.transactionId, status: result.status };
306
+ }
307
+ };
308
+
309
+ export { DEFAULT_API_BASE, Snap, SnapAuthenticationError, SnapElements, SnapError, SnapValidationError, fetchSession, registerElement, startPayment };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@snap-pay/js",
3
+ "version": "1.0.0",
4
+ "description": "SNAP.js — client-side SDK for creating and confirming SNAP payments in the browser.",
5
+ "license": "MIT",
6
+ "author": "SNAP",
7
+ "homepage": "https://github.com/khaledhussein957/SNAP/tree/main/packages/js",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/khaledhussein957/SNAP.git",
11
+ "directory": "packages/js"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/khaledhussein957/SNAP/issues"
15
+ },
16
+ "keywords": ["snap", "payments", "checkout", "sdk", "browser", "somalia", "mobile-money"],
17
+ "sideEffects": false,
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "files": ["dist", "README.md", "CHANGELOG.md"],
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "test": "vitest run",
36
+ "typecheck": "tsc --noEmit"
37
+ },
38
+ "dependencies": {
39
+ "@snap-pay/types": "1.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "tsup": "^8.3.5",
43
+ "typescript": "^5.7.2",
44
+ "vitest": "^4.1.9"
45
+ }
46
+ }