@reevit/core 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,13 +21,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ReevitAPIClient: () => ReevitAPIClient,
24
+ attemptIdempotencyKey: () => attemptIdempotencyKey,
24
25
  cacheIntentPromise: () => cacheIntentPromise,
25
26
  cacheIntentResponse: () => cacheIntentResponse,
27
+ clearIdempotencyAttemptKeys: () => clearIdempotencyAttemptKeys,
26
28
  clearIntentCacheEntry: () => clearIntentCacheEntry,
27
29
  cn: () => cn,
28
30
  createInitialState: () => createInitialState,
31
+ createPaymentError: () => createPaymentError,
29
32
  createReevitClient: () => createReevitClient,
30
33
  createThemeVariables: () => createThemeVariables,
34
+ currencyExponent: () => currencyExponent,
31
35
  detectCountryFromCurrency: () => detectCountryFromCurrency,
32
36
  detectNetwork: () => detectNetwork,
33
37
  formatAmount: () => formatAmount,
@@ -35,8 +39,11 @@ __export(index_exports, {
35
39
  generateIdempotencyKey: () => generateIdempotencyKey,
36
40
  generateReference: () => generateReference,
37
41
  getIntentCacheEntry: () => getIntentCacheEntry,
42
+ isPaymentError: () => isPaymentError,
43
+ newIdempotencyKey: () => newIdempotencyKey,
38
44
  reevitReducer: () => reevitReducer,
39
45
  resolveIntentIdentity: () => resolveIntentIdentity,
46
+ toMinorUnits: () => toMinorUnits,
40
47
  validatePhone: () => validatePhone
41
48
  });
42
49
  module.exports = __toCommonJS(index_exports);
@@ -44,16 +51,25 @@ module.exports = __toCommonJS(index_exports);
44
51
  // src/api/client.ts
45
52
  var API_BASE_URL_PRODUCTION = "https://api.reevit.io";
46
53
  var DEFAULT_TIMEOUT = 3e4;
54
+ var hasWarnedAboutLiveBrowserIntents = false;
47
55
  function createPaymentError(response, errorData) {
48
56
  return {
49
57
  code: errorData.code || "api_error",
50
58
  message: errorData.message || "An unexpected error occurred",
59
+ recoverable: isRecoverableStatus(response.status),
51
60
  details: {
52
61
  httpStatus: response.status,
62
+ requestId: response.headers.get("x-request-id") || response.headers.get("x-reevit-request-id") || void 0,
53
63
  ...errorData.details
54
64
  }
55
65
  };
56
66
  }
67
+ function isPaymentError(error) {
68
+ return typeof error === "object" && error !== null && "code" in error && "message" in error;
69
+ }
70
+ function isRecoverableStatus(status) {
71
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
72
+ }
57
73
  function generateIdempotencyKey(params) {
58
74
  const sortedKeys = Object.keys(params).sort();
59
75
  const stableString = sortedKeys.map((key) => `${key}:${JSON.stringify(params[key])}`).join("|");
@@ -66,6 +82,95 @@ function generateIdempotencyKey(params) {
66
82
  const timeBucket = Math.floor(Date.now() / (5 * 60 * 1e3));
67
83
  return `reevit_${timeBucket}_${hashHex}`;
68
84
  }
85
+ var IDEMPOTENCY_STORE_PREFIX = "reevit:idem:";
86
+ var memoryAttemptKeys = /* @__PURE__ */ new Map();
87
+ function getSessionStore() {
88
+ try {
89
+ const storage = globalThis.sessionStorage;
90
+ if (!storage) {
91
+ return null;
92
+ }
93
+ const probe = `${IDEMPOTENCY_STORE_PREFIX}probe`;
94
+ storage.setItem(probe, "1");
95
+ storage.removeItem(probe);
96
+ return storage;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ function newIdempotencyKey() {
102
+ const cryptoObj = globalThis.crypto;
103
+ if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
104
+ try {
105
+ return cryptoObj.randomUUID();
106
+ } catch {
107
+ }
108
+ }
109
+ const bytes = new Uint8Array(16);
110
+ if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
111
+ cryptoObj.getRandomValues(bytes);
112
+ } else {
113
+ for (let i = 0; i < bytes.length; i++) {
114
+ bytes[i] = Math.floor(Math.random() * 256);
115
+ }
116
+ }
117
+ bytes[6] = bytes[6] & 15 | 64;
118
+ bytes[8] = bytes[8] & 63 | 128;
119
+ const hex = [];
120
+ for (let i = 0; i < bytes.length; i++) {
121
+ hex.push(bytes[i].toString(16).padStart(2, "0"));
122
+ }
123
+ return [
124
+ hex.slice(0, 4).join(""),
125
+ hex.slice(4, 6).join(""),
126
+ hex.slice(6, 8).join(""),
127
+ hex.slice(8, 10).join(""),
128
+ hex.slice(10, 16).join("")
129
+ ].join("-");
130
+ }
131
+ function attemptIdempotencyKey(lookupKey) {
132
+ const storageKey = `${IDEMPOTENCY_STORE_PREFIX}${lookupKey}`;
133
+ const store = getSessionStore();
134
+ if (store) {
135
+ try {
136
+ const existing2 = store.getItem(storageKey);
137
+ if (existing2) {
138
+ return existing2;
139
+ }
140
+ const created2 = newIdempotencyKey();
141
+ store.setItem(storageKey, created2);
142
+ return created2;
143
+ } catch {
144
+ }
145
+ }
146
+ const existing = memoryAttemptKeys.get(storageKey);
147
+ if (existing) {
148
+ return existing;
149
+ }
150
+ const created = newIdempotencyKey();
151
+ memoryAttemptKeys.set(storageKey, created);
152
+ return created;
153
+ }
154
+ function clearIdempotencyAttemptKeys() {
155
+ memoryAttemptKeys.clear();
156
+ const store = getSessionStore();
157
+ if (!store) {
158
+ return;
159
+ }
160
+ try {
161
+ const keys = [];
162
+ for (let i = 0; i < store.length; i++) {
163
+ const key = store.key(i);
164
+ if (key && key.startsWith(IDEMPOTENCY_STORE_PREFIX)) {
165
+ keys.push(key);
166
+ }
167
+ }
168
+ for (const key of keys) {
169
+ store.removeItem(key);
170
+ }
171
+ } catch {
172
+ }
173
+ }
69
174
  var ReevitAPIClient = class {
70
175
  constructor(config) {
71
176
  this.publicKey = config.publicKey || "";
@@ -82,13 +187,13 @@ var ReevitAPIClient = class {
82
187
  const headers = {
83
188
  "Content-Type": "application/json",
84
189
  "X-Reevit-Client": "@reevit/core",
85
- "X-Reevit-Client-Version": "0.8.1"
190
+ "X-Reevit-Client-Version": "0.9.1"
86
191
  };
87
192
  if (this.publicKey) {
88
193
  headers["X-Reevit-Key"] = this.publicKey;
89
194
  }
90
195
  if (method === "POST" || method === "PATCH" || method === "PUT") {
91
- headers["Idempotency-Key"] = idempotencyKey || (body ? generateIdempotencyKey(body) : `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`);
196
+ headers["Idempotency-Key"] = idempotencyKey || newIdempotencyKey();
92
197
  }
93
198
  try {
94
199
  const response = await fetch(`${this.baseUrl}${path}`, {
@@ -112,7 +217,8 @@ var ReevitAPIClient = class {
112
217
  return {
113
218
  error: {
114
219
  code: "request_timeout",
115
- message: "The request timed out. Please try again."
220
+ message: "The request timed out. Please try again.",
221
+ recoverable: true
116
222
  }
117
223
  };
118
224
  }
@@ -120,7 +226,8 @@ var ReevitAPIClient = class {
120
226
  return {
121
227
  error: {
122
228
  code: "network_error",
123
- message: "Unable to connect to Reevit. Please check your internet connection."
229
+ message: "Unable to connect to Reevit. Please check your internet connection.",
230
+ recoverable: true
124
231
  }
125
232
  };
126
233
  }
@@ -128,7 +235,8 @@ var ReevitAPIClient = class {
128
235
  return {
129
236
  error: {
130
237
  code: "unknown_error",
131
- message: "An unexpected error occurred. Please try again."
238
+ message: "An unexpected error occurred. Please try again.",
239
+ recoverable: true
132
240
  }
133
241
  };
134
242
  }
@@ -137,6 +245,21 @@ var ReevitAPIClient = class {
137
245
  * Creates a payment intent
138
246
  */
139
247
  async createPaymentIntent(config, method, country = "GH", options) {
248
+ if (this.publicKey.startsWith("pfk_live_") && !hasWarnedAboutLiveBrowserIntents && typeof console !== "undefined") {
249
+ hasWarnedAboutLiveBrowserIntents = true;
250
+ console.warn(
251
+ "Creating live payment intents from the browser is deprecated. Create a checkout session on your server and pass sessionSecret to the browser SDK instead."
252
+ );
253
+ }
254
+ if (typeof config.amount !== "number" || !config.currency) {
255
+ return {
256
+ error: {
257
+ code: "invalid_checkout_config",
258
+ message: "amount and currency are required when creating a payment intent in the browser.",
259
+ recoverable: false
260
+ }
261
+ };
262
+ }
140
263
  const metadata = { ...config.metadata };
141
264
  if (config.email) {
142
265
  metadata.customer_email = config.email;
@@ -160,7 +283,7 @@ var ReevitAPIClient = class {
160
283
  allowed_providers: options?.allowedProviders
161
284
  };
162
285
  }
163
- const idempotencyKey = config.idempotencyKey || generateIdempotencyKey({
286
+ const idempotencyKey = config.idempotencyKey || attemptIdempotencyKey(generateIdempotencyKey({
164
287
  amount: config.amount,
165
288
  currency: config.currency,
166
289
  customer: config.email || config.metadata?.customerId || "",
@@ -168,7 +291,7 @@ var ReevitAPIClient = class {
168
291
  method: method || "",
169
292
  provider: options?.preferredProviders?.[0] || options?.allowedProviders?.[0] || "",
170
293
  publicKey: this.publicKey
171
- });
294
+ }));
172
295
  return this.request("POST", "/v1/payments/intents", request, idempotencyKey);
173
296
  }
174
297
  /**
@@ -177,6 +300,15 @@ var ReevitAPIClient = class {
177
300
  async getPaymentIntent(paymentId) {
178
301
  return this.request("GET", `/v1/payments/${paymentId}`);
179
302
  }
303
+ /**
304
+ * Retrieves a server-created checkout session using its public session secret.
305
+ */
306
+ async getCheckoutSession(sessionSecret) {
307
+ return this.request(
308
+ "GET",
309
+ `/v1/checkout/sessions/${encodeURIComponent(sessionSecret)}`
310
+ );
311
+ }
180
312
  /**
181
313
  * Confirms a payment after PSP callback
182
314
  */
@@ -228,25 +360,62 @@ function createReevitClient(config) {
228
360
  }
229
361
 
230
362
  // src/utils.ts
363
+ var CURRENCY_LOCALES = {
364
+ GHS: "en-GH",
365
+ NGN: "en-NG",
366
+ KES: "en-KE",
367
+ USD: "en-US",
368
+ EUR: "de-DE",
369
+ GBP: "en-GB"
370
+ };
371
+ var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
372
+ "XOF",
373
+ "XAF",
374
+ "RWF",
375
+ "UGX",
376
+ "JPY",
377
+ "KRW",
378
+ "BIF",
379
+ "GNF",
380
+ "VND",
381
+ "CLP",
382
+ "ISK",
383
+ "KMF",
384
+ "DJF",
385
+ "PYG",
386
+ "MGA"
387
+ ]);
388
+ function currencyExponent(currency) {
389
+ const code = (currency || "").toUpperCase();
390
+ try {
391
+ const digits = new Intl.NumberFormat("en", {
392
+ style: "currency",
393
+ currency: code
394
+ }).resolvedOptions().maximumFractionDigits;
395
+ if (typeof digits === "number" && Number.isFinite(digits)) {
396
+ return digits;
397
+ }
398
+ } catch {
399
+ }
400
+ return ZERO_DECIMAL_CURRENCIES.has(code) ? 0 : 2;
401
+ }
402
+ function toMinorUnits(major, currency) {
403
+ return Math.round(major * 10 ** currencyExponent(currency));
404
+ }
231
405
  function formatAmount(amount, currency) {
232
- const majorUnit = amount / 100;
233
- const currencyFormats = {
234
- GHS: { locale: "en-GH", minimumFractionDigits: 2 },
235
- NGN: { locale: "en-NG", minimumFractionDigits: 2 },
236
- KES: { locale: "en-KE", minimumFractionDigits: 2 },
237
- USD: { locale: "en-US", minimumFractionDigits: 2 },
238
- EUR: { locale: "de-DE", minimumFractionDigits: 2 },
239
- GBP: { locale: "en-GB", minimumFractionDigits: 2 }
240
- };
241
- const format = currencyFormats[currency.toUpperCase()] || { locale: "en-US", minimumFractionDigits: 2 };
406
+ const code = (currency || "").toUpperCase();
407
+ const exponent = currencyExponent(code);
408
+ const majorUnit = amount / 10 ** exponent;
409
+ const locale = CURRENCY_LOCALES[code] || "en-US";
242
410
  try {
243
- return new Intl.NumberFormat(format.locale, {
411
+ return new Intl.NumberFormat(locale, {
244
412
  style: "currency",
245
- currency: currency.toUpperCase(),
246
- minimumFractionDigits: format.minimumFractionDigits
413
+ currency: code,
414
+ minimumFractionDigits: exponent,
415
+ maximumFractionDigits: exponent
247
416
  }).format(majorUnit);
248
417
  } catch {
249
- return `${currency} ${majorUnit.toFixed(2)}`;
418
+ return `${code} ${majorUnit.toFixed(exponent)}`;
250
419
  }
251
420
  }
252
421
  function generateReference(prefix = "reevit") {
@@ -364,37 +533,60 @@ function detectCountryFromCurrency(currency) {
364
533
  // src/intent.ts
365
534
  var INTENT_CACHE_TTL_MS = 10 * 60 * 1e3;
366
535
  var intentCache = /* @__PURE__ */ new Map();
536
+ var lookupKeyByWireKey = /* @__PURE__ */ new Map();
537
+ function forgetKey(lookupKey) {
538
+ const entry = intentCache.get(lookupKey);
539
+ if (entry?.idempotencyKey) {
540
+ lookupKeyByWireKey.delete(entry.idempotencyKey);
541
+ }
542
+ intentCache.delete(lookupKey);
543
+ }
544
+ function toLookupKey(key) {
545
+ return lookupKeyByWireKey.get(key) ?? key;
546
+ }
367
547
  function pruneIntentCache(now = Date.now()) {
368
548
  for (const [key, entry] of intentCache) {
369
549
  if (entry.expiresAt <= now) {
370
- intentCache.delete(key);
550
+ forgetKey(key);
371
551
  }
372
552
  }
373
553
  }
374
- function getIntentCacheEntryInternal(key) {
375
- const entry = intentCache.get(key);
554
+ function getIntentCacheEntryInternal(lookupKey) {
555
+ const entry = intentCache.get(lookupKey);
376
556
  if (!entry) {
377
557
  return void 0;
378
558
  }
379
559
  if (entry.expiresAt <= Date.now()) {
380
- intentCache.delete(key);
560
+ forgetKey(lookupKey);
381
561
  return void 0;
382
562
  }
383
563
  return entry;
384
564
  }
385
- function setIntentCacheEntryInternal(key, update) {
565
+ function setIntentCacheEntryInternal(lookupKey, update) {
386
566
  const now = Date.now();
387
- const existing = getIntentCacheEntryInternal(key);
567
+ const existing = getIntentCacheEntryInternal(lookupKey);
388
568
  const next = {
389
569
  ...existing,
390
570
  ...update,
391
571
  expiresAt: now + INTENT_CACHE_TTL_MS
392
572
  };
393
- intentCache.set(key, next);
573
+ if (existing?.idempotencyKey && existing.idempotencyKey !== next.idempotencyKey) {
574
+ lookupKeyByWireKey.delete(existing.idempotencyKey);
575
+ }
576
+ intentCache.set(lookupKey, next);
577
+ if (next.idempotencyKey && next.idempotencyKey !== lookupKey) {
578
+ lookupKeyByWireKey.set(next.idempotencyKey, lookupKey);
579
+ }
394
580
  return next;
395
581
  }
396
582
  function buildIdempotencyPayload(options) {
397
583
  const { config, method, preferredProvider, allowedProviders, publicKey } = options;
584
+ if (config.sessionSecret) {
585
+ return {
586
+ sessionSecret: config.sessionSecret,
587
+ publicKey: publicKey || config.publicKey || ""
588
+ };
589
+ }
398
590
  const payload = {
399
591
  amount: config.amount,
400
592
  currency: config.currency,
@@ -417,24 +609,26 @@ function buildIdempotencyPayload(options) {
417
609
  }
418
610
  function resolveIntentIdentity(options) {
419
611
  pruneIntentCache();
420
- const idempotencyKey = options.config.idempotencyKey || generateIdempotencyKey(buildIdempotencyPayload(options));
421
- const existing = getIntentCacheEntryInternal(idempotencyKey);
612
+ const explicitKey = options.config.idempotencyKey;
613
+ const lookupKey = explicitKey || generateIdempotencyKey(buildIdempotencyPayload(options));
614
+ const idempotencyKey = explicitKey || attemptIdempotencyKey(lookupKey);
615
+ const existing = getIntentCacheEntryInternal(lookupKey);
422
616
  const reference = options.config.reference || existing?.reference || generateReference();
423
- const cacheEntry = setIntentCacheEntryInternal(idempotencyKey, { reference });
424
- return { idempotencyKey, reference, cacheEntry };
617
+ const cacheEntry = setIntentCacheEntryInternal(lookupKey, { reference, idempotencyKey });
618
+ return { idempotencyKey, lookupKey, reference, cacheEntry };
425
619
  }
426
- function getIntentCacheEntry(idempotencyKey) {
620
+ function getIntentCacheEntry(key) {
427
621
  pruneIntentCache();
428
- return getIntentCacheEntryInternal(idempotencyKey);
622
+ return getIntentCacheEntryInternal(toLookupKey(key));
429
623
  }
430
- function cacheIntentPromise(idempotencyKey, promise) {
431
- return setIntentCacheEntryInternal(idempotencyKey, { promise });
624
+ function cacheIntentPromise(key, promise) {
625
+ return setIntentCacheEntryInternal(toLookupKey(key), { promise });
432
626
  }
433
- function cacheIntentResponse(idempotencyKey, response) {
434
- return setIntentCacheEntryInternal(idempotencyKey, { response, promise: void 0 });
627
+ function cacheIntentResponse(key, response) {
628
+ return setIntentCacheEntryInternal(toLookupKey(key), { response, promise: void 0 });
435
629
  }
436
- function clearIntentCacheEntry(idempotencyKey) {
437
- intentCache.delete(idempotencyKey);
630
+ function clearIntentCacheEntry(key) {
631
+ forgetKey(toLookupKey(key));
438
632
  }
439
633
 
440
634
  // src/state.ts
@@ -479,13 +673,17 @@ function reevitReducer(state, action) {
479
673
  // Annotate the CommonJS export names for ESM import in node:
480
674
  0 && (module.exports = {
481
675
  ReevitAPIClient,
676
+ attemptIdempotencyKey,
482
677
  cacheIntentPromise,
483
678
  cacheIntentResponse,
679
+ clearIdempotencyAttemptKeys,
484
680
  clearIntentCacheEntry,
485
681
  cn,
486
682
  createInitialState,
683
+ createPaymentError,
487
684
  createReevitClient,
488
685
  createThemeVariables,
686
+ currencyExponent,
489
687
  detectCountryFromCurrency,
490
688
  detectNetwork,
491
689
  formatAmount,
@@ -493,8 +691,11 @@ function reevitReducer(state, action) {
493
691
  generateIdempotencyKey,
494
692
  generateReference,
495
693
  getIntentCacheEntry,
694
+ isPaymentError,
695
+ newIdempotencyKey,
496
696
  reevitReducer,
497
697
  resolveIntentIdentity,
698
+ toMinorUnits,
498
699
  validatePhone
499
700
  });
500
701
  //# sourceMappingURL=index.js.map