@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/README.md CHANGED
@@ -8,6 +8,25 @@ The foundation for all Reevit payment SDKs. It provides the shared API client, s
8
8
  npm install @reevit/core
9
9
  ```
10
10
 
11
+ ## Compatibility
12
+
13
+ `@reevit/core` is a regular `dependencies` entry of each framework SDK, so it is
14
+ installed for you. Install it directly only when you are building against the
15
+ low-level API.
16
+
17
+ | `@reevit/core` | Required by |
18
+ |---|---|
19
+ | 0.9.x | `@reevit/react` 0.9.x–0.10.x, `@reevit/vue` 0.9.x–0.10.x, `@reevit/svelte` 0.9.x–0.10.x |
20
+
21
+ 0.9.1 is a patch release on purpose: it carries the idempotency-key and
22
+ zero-decimal-currency fixes, and the framework SDKs pick it up through their
23
+ existing `^0.9.0` range without a coordinated bump.
24
+
25
+ On a `0.x` package a caret range pins the **minor**, not the major:
26
+ `^0.9.0` resolves to `>=0.9.0 <0.10.0`. A future `@reevit/core` 0.10.0 is
27
+ therefore not picked up automatically — the React, Vue and Svelte manifests
28
+ have to be bumped together in the same release.
29
+
11
30
  ## Features
12
31
 
13
32
  - **ReevitAPIClient**: A lightweight, promise-based client for interacting with the Reevit backend.
@@ -42,16 +61,62 @@ if (data) {
42
61
  }
43
62
  ```
44
63
 
64
+ ### Loading a checkout session
65
+
66
+ Browser SDKs should prefer server-created checkout sessions. Use the session secret returned by your backend to load the payment intent without exposing private API credentials.
67
+
68
+ ```typescript
69
+ const { data, error } = await client.getCheckoutSession('cs_session_secret');
70
+
71
+ if (data) {
72
+ console.log('Ready to render:', data.payment_intent.id);
73
+ }
74
+ ```
75
+
76
+ ### Error handling
77
+
78
+ Core returns a consistent `{ data, error }` result. Errors include `code`, `message`, `recoverable`, and `details.httpStatus` when the API returns a status code.
79
+
80
+ ```typescript
81
+ const result = await client.getCheckoutSession('cs_session_secret');
82
+
83
+ if (result.error) {
84
+ if (result.error.recoverable) {
85
+ // show retry UI
86
+ }
87
+ console.error(result.error.code, result.error.message);
88
+ }
89
+ ```
90
+
45
91
  ### Using Utilities
46
92
 
47
93
  ```typescript
48
94
  import { formatAmount, validatePhone, detectNetwork } from '@reevit/core';
49
95
 
50
- console.log(formatAmount(10000, 'GHS')); // "GH₵ 100.00"
96
+ console.log(formatAmount(10000, 'GHS')); // "GH₵100.00"
51
97
  console.log(validatePhone('0241234567')); // true
52
98
  console.log(detectNetwork('0241234567')); // "mtn"
53
99
  ```
54
100
 
101
+ ### Amounts and currency exponents
102
+
103
+ Amounts are always integers in the smallest unit of the currency, and that unit
104
+ is **not** always 1/100. GHS, NGN and USD have two decimals; XOF, XAF, RWF, UGX,
105
+ JPY and KRW have none — 5,000 XOF is 5000, not 500000.
106
+
107
+ ```typescript
108
+ import { currencyExponent, formatAmount, toMinorUnits } from '@reevit/core';
109
+
110
+ currencyExponent('GHS'); // 2
111
+ currencyExponent('XOF'); // 0
112
+
113
+ formatAmount(4500, 'GHS'); // "GH₵45.00"
114
+ formatAmount(5000, 'XOF'); // "F CFA 5,000" (not "XOF 50.00")
115
+
116
+ toMinorUnits(45, 'GHS'); // 4500
117
+ toMinorUnits(5000, 'XOF'); // 5000
118
+ ```
119
+
55
120
  ### Intent Identity & Idempotency
56
121
 
57
122
  Core exports helpers to stabilize intent creation and dedupe in-flight requests.
@@ -59,7 +124,7 @@ Core exports helpers to stabilize intent creation and dedupe in-flight requests.
59
124
  ```typescript
60
125
  import { resolveIntentIdentity } from '@reevit/core';
61
126
 
62
- const { idempotencyKey, reference } = resolveIntentIdentity({
127
+ const { idempotencyKey, lookupKey, reference } = resolveIntentIdentity({
63
128
  config: {
64
129
  amount: 5000,
65
130
  currency: 'GHS',
@@ -70,9 +135,35 @@ const { idempotencyKey, reference } = resolveIntentIdentity({
70
135
  });
71
136
  ```
72
137
 
138
+ **Pass your own order-scoped `idempotencyKey` for retry safety across page
139
+ loads.** Without one, the SDK generates a per-tab attempt key: a UUID minted on
140
+ the first request and kept in `sessionStorage`, so a repeated "Continue" click
141
+ in the same tab is deduped by the API, while a reload or a different shopper
142
+ starts a new attempt.
143
+
144
+ Two keys are in play and they must not be confused:
145
+
146
+ | | Value | Where it goes |
147
+ |---|---|---|
148
+ | `idempotencyKey` | UUID from `newIdempotencyKey()` / `attemptIdempotencyKey()`, or the one you supplied | the `Idempotency-Key` request header |
149
+ | `lookupKey` | deterministic djb2 hash from `generateIdempotencyKey()` | local in-flight cache only — **never** the wire |
150
+
151
+ `generateIdempotencyKey()` stays exported for existing callers, but a 32-bit
152
+ hash is not safe as a wire key: two unrelated shoppers can collide and be handed
153
+ each other's `client_secret`. Use `newIdempotencyKey()` if you need to mint one
154
+ yourself, and `clearIdempotencyAttemptKeys()` to start a fresh attempt after a
155
+ completed checkout.
156
+
73
157
  ## Release Notes
74
158
 
75
- ### v0.7.0
159
+ ### v0.9.1
160
+
161
+ - The wire `Idempotency-Key` is now a per-attempt UUID instead of a djb2 hash
162
+ - Zero-decimal currencies (XOF, XAF, RWF, UGX, JPY, …) are no longer divided by 100
163
+ - Added `currencyExponent`, `toMinorUnits`, `newIdempotencyKey`, `attemptIdempotencyKey`, `clearIdempotencyAttemptKeys`
164
+ - First test suite for this package; CI runs `npm test`
165
+
166
+ ### v0.9.0
76
167
 
77
168
  - Version alignment across all Reevit SDKs
78
169
  - Updated shared CSS with redesigned checkout visual system
package/dist/index.d.mts CHANGED
@@ -10,10 +10,12 @@ type PaymentSource = 'payment_link' | 'api' | 'subscription';
10
10
  interface ReevitCheckoutConfig {
11
11
  /** Your Reevit public key (required for API-created intents; omit for payment links) */
12
12
  publicKey?: string;
13
- /** Amount in the smallest currency unit (e.g., pesewas for GHS) */
14
- amount: number;
15
- /** Currency code (e.g., 'GHS', 'NGN', 'USD') */
16
- currency: string;
13
+ /** Server-created checkout session secret. Prefer this for browser checkouts. */
14
+ sessionSecret?: string;
15
+ /** Amount in the smallest currency unit (e.g., pesewas for GHS). Required unless sessionSecret or initialPaymentIntent is provided. */
16
+ amount?: number;
17
+ /** Currency code (e.g., 'GHS', 'NGN', 'USD'). Required unless sessionSecret or initialPaymentIntent is provided. */
18
+ currency?: string;
17
19
  /** Customer email address */
18
20
  email?: string;
19
21
  /** Customer phone number (required for mobile money) */
@@ -247,6 +249,7 @@ interface PaymentIntentResponse {
247
249
  provider_ref_id?: string;
248
250
  status: string;
249
251
  client_secret: string;
252
+ session_secret?: string;
250
253
  psp_public_key: string;
251
254
  psp_credentials?: {
252
255
  merchantAccount?: string | number;
@@ -267,6 +270,13 @@ interface PaymentIntentResponse {
267
270
  }>;
268
271
  branding?: Record<string, unknown>;
269
272
  }
273
+ interface CheckoutSessionResponse {
274
+ id: string;
275
+ client_secret: string;
276
+ session_secret: string;
277
+ payment_intent: PaymentIntentResponse;
278
+ expires_at?: string;
279
+ }
270
280
  interface ConfirmPaymentRequest {
271
281
  provider_ref_id: string;
272
282
  provider_data?: Record<string, unknown>;
@@ -298,8 +308,15 @@ interface PaymentDetailResponse {
298
308
  interface APIErrorResponse {
299
309
  code: string;
300
310
  message: string;
301
- details?: Record<string, string>;
311
+ details?: Record<string, unknown>;
302
312
  }
313
+ type ReevitAPIResult<T> = {
314
+ data: T;
315
+ error?: never;
316
+ } | {
317
+ data?: never;
318
+ error: PaymentError;
319
+ };
303
320
  interface ReevitAPIClientConfig {
304
321
  /** Your Reevit public key */
305
322
  publicKey?: string;
@@ -309,11 +326,48 @@ interface ReevitAPIClientConfig {
309
326
  timeout?: number;
310
327
  }
311
328
  /**
312
- * Generates a deterministic idempotency key based on input parameters
313
- * Uses a simple hash function suitable for browser environments
314
- * Exported for use by SDK hooks (e.g., payment link flows)
329
+ * Creates a PaymentError from an API error response
330
+ */
331
+ declare function createPaymentError(response: Response, errorData: APIErrorResponse): PaymentError;
332
+ declare function isPaymentError(error: unknown): error is PaymentError;
333
+ /**
334
+ * Generates a deterministic **cache/lookup** key from input parameters.
335
+ *
336
+ * NEVER SEND THIS ON THE WIRE. It is a 32-bit djb2 hash bucketed into
337
+ * 5-minute windows, so two unrelated shoppers can collide and be handed each
338
+ * other's payment intent (and therefore each other's `client_secret`), and a
339
+ * shopper legitimately buying the same item twice inside one window would be
340
+ * charged once. Its only job is to identify "the same checkout attempt" inside
341
+ * a single browser tab so the in-flight intent cache can dedupe a repeated
342
+ * "Continue" click.
343
+ *
344
+ * The value actually sent as `Idempotency-Key` is produced by
345
+ * {@link newIdempotencyKey} / {@link attemptIdempotencyKey}.
346
+ *
347
+ * Exported for use by SDK hooks (e.g. payment link flows).
315
348
  */
316
349
  declare function generateIdempotencyKey(params: Record<string, unknown>): string;
350
+ /**
351
+ * Generates a fresh, globally unique `Idempotency-Key` (RFC 4122 v4 UUID).
352
+ * This is the only value that should ever be sent on the wire.
353
+ */
354
+ declare function newIdempotencyKey(): string;
355
+ /**
356
+ * Resolves the stable per-checkout-attempt wire key for a deterministic
357
+ * lookup key (see {@link generateIdempotencyKey}).
358
+ *
359
+ * The first call for a lookup key mints a UUID and stores it in
360
+ * `sessionStorage` (falling back to a module-level map when storage is
361
+ * unavailable); every later call in the same tab returns that same UUID, so a
362
+ * repeated "Continue" click is still deduped by the backend. A different tab,
363
+ * a different shopper or a cleared store yields a different UUID.
364
+ */
365
+ declare function attemptIdempotencyKey(lookupKey: string): string;
366
+ /**
367
+ * Forgets every stored per-attempt key, so the next checkout attempt gets a
368
+ * fresh `Idempotency-Key`. Call it after a completed checkout (and in tests).
369
+ */
370
+ declare function clearIdempotencyAttemptKeys(): void;
317
371
  /**
318
372
  * Reevit API Client
319
373
  */
@@ -344,6 +398,13 @@ declare class ReevitAPIClient {
344
398
  data?: PaymentDetailResponse;
345
399
  error?: PaymentError;
346
400
  }>;
401
+ /**
402
+ * Retrieves a server-created checkout session using its public session secret.
403
+ */
404
+ getCheckoutSession(sessionSecret: string): Promise<{
405
+ data?: CheckoutSessionResponse;
406
+ error?: PaymentError;
407
+ }>;
347
408
  /**
348
409
  * Confirms a payment after PSP callback
349
410
  */
@@ -389,6 +450,19 @@ declare function createReevitClient(config: ReevitAPIClientConfig): ReevitAPICli
389
450
  * Shared utilities for Reevit SDKs
390
451
  */
391
452
 
453
+ /**
454
+ * Returns how many decimal places a currency's minor unit uses: 2 for GHS and
455
+ * NGN, 0 for XOF, XAF, RWF, UGX, JPY and friends.
456
+ *
457
+ * Dividing every amount by 100 renders a 5,000 XOF charge as "XOF 50.00" while
458
+ * the shopper is actually charged 5,000 — which is why this exists.
459
+ */
460
+ declare function currencyExponent(currency: string): number;
461
+ /**
462
+ * Converts a major-unit amount (what a shopper types) into the minor units the
463
+ * API expects: `toMinorUnits(45, 'GHS') === 4500`, `toMinorUnits(5000, 'XOF') === 5000`.
464
+ */
465
+ declare function toMinorUnits(major: number, currency: string): number;
392
466
  /**
393
467
  * Formats an amount from smallest currency unit to display format
394
468
  */
@@ -424,6 +498,18 @@ declare function detectCountryFromCurrency(currency: string): string;
424
498
 
425
499
  /**
426
500
  * Intent identity + cache helpers
501
+ *
502
+ * Two different keys are in play here and mixing them up is a money bug:
503
+ *
504
+ * - the **lookup key** is the deterministic djb2 hash of the checkout
505
+ * parameters (`generateIdempotencyKey`). It identifies "the same checkout
506
+ * attempt" for the in-flight cache and is never sent to the API.
507
+ * - the **wire key** is the per-attempt UUID (`attemptIdempotencyKey`) that
508
+ * goes out as the `Idempotency-Key` header.
509
+ *
510
+ * The cache is keyed by the lookup key and remembers the wire key it minted.
511
+ * The public helpers accept either key so callers that only ever saw the
512
+ * `idempotencyKey` field keep working unchanged.
427
513
  */
428
514
 
429
515
  interface IntentIdentityOptions {
@@ -438,16 +524,21 @@ interface IntentCacheEntry {
438
524
  response?: PaymentIntentResponse;
439
525
  expiresAt: number;
440
526
  reference?: string;
527
+ /** The `Idempotency-Key` sent on the wire for this attempt. */
528
+ idempotencyKey?: string;
441
529
  }
442
530
  declare function resolveIntentIdentity(options: IntentIdentityOptions): {
531
+ /** The value to send as `Idempotency-Key`. */
443
532
  idempotencyKey: string;
533
+ /** The local cache key. Never send this on the wire. */
534
+ lookupKey: string;
444
535
  reference: string;
445
536
  cacheEntry?: IntentCacheEntry;
446
537
  };
447
- declare function getIntentCacheEntry(idempotencyKey: string): IntentCacheEntry | undefined;
448
- declare function cacheIntentPromise(idempotencyKey: string, promise: Promise<PaymentIntentResponse>): IntentCacheEntry;
449
- declare function cacheIntentResponse(idempotencyKey: string, response: PaymentIntentResponse): IntentCacheEntry;
450
- declare function clearIntentCacheEntry(idempotencyKey: string): void;
538
+ declare function getIntentCacheEntry(key: string): IntentCacheEntry | undefined;
539
+ declare function cacheIntentPromise(key: string, promise: Promise<PaymentIntentResponse>): IntentCacheEntry;
540
+ declare function cacheIntentResponse(key: string, response: PaymentIntentResponse): IntentCacheEntry;
541
+ declare function clearIntentCacheEntry(key: string): void;
451
542
 
452
543
  /**
453
544
  * Reevit State Machine
@@ -494,4 +585,4 @@ declare function createInitialState(): ReevitState;
494
585
  */
495
586
  declare function reevitReducer(state: ReevitState, action: ReevitAction): ReevitState;
496
587
 
497
- export { type APIErrorResponse, type CardFormData, type CheckoutProviderOption, type CheckoutState, type ConfirmPaymentRequest, type CreatePaymentIntentRequest, type HubtelSessionResponse, type IntentCacheEntry, type MobileMoneyFormData, type MobileMoneyNetwork, type PSPConfig, type PSPType, type PaymentDetailResponse, type PaymentError, type PaymentIntent, type PaymentIntentResponse, type PaymentMethod, type PaymentResult, type PaymentSource, ReevitAPIClient, type ReevitAPIClientConfig, type ReevitAction, type ReevitCheckoutCallbacks, type ReevitCheckoutConfig, type ReevitState, type ReevitTheme, cacheIntentPromise, cacheIntentResponse, clearIntentCacheEntry, cn, createInitialState, createReevitClient, createThemeVariables, detectCountryFromCurrency, detectNetwork, formatAmount, formatPhone, generateIdempotencyKey, generateReference, getIntentCacheEntry, reevitReducer, resolveIntentIdentity, validatePhone };
588
+ export { type APIErrorResponse, type CardFormData, type CheckoutProviderOption, type CheckoutSessionResponse, type CheckoutState, type ConfirmPaymentRequest, type CreatePaymentIntentRequest, type HubtelSessionResponse, type IntentCacheEntry, type MobileMoneyFormData, type MobileMoneyNetwork, type PSPConfig, type PSPType, type PaymentDetailResponse, type PaymentError, type PaymentIntent, type PaymentIntentResponse, type PaymentMethod, type PaymentResult, type PaymentSource, ReevitAPIClient, type ReevitAPIClientConfig, type ReevitAPIResult, type ReevitAction, type ReevitCheckoutCallbacks, type ReevitCheckoutConfig, type ReevitState, type ReevitTheme, attemptIdempotencyKey, cacheIntentPromise, cacheIntentResponse, clearIdempotencyAttemptKeys, clearIntentCacheEntry, cn, createInitialState, createPaymentError, createReevitClient, createThemeVariables, currencyExponent, detectCountryFromCurrency, detectNetwork, formatAmount, formatPhone, generateIdempotencyKey, generateReference, getIntentCacheEntry, isPaymentError, newIdempotencyKey, reevitReducer, resolveIntentIdentity, toMinorUnits, validatePhone };
package/dist/index.d.ts CHANGED
@@ -10,10 +10,12 @@ type PaymentSource = 'payment_link' | 'api' | 'subscription';
10
10
  interface ReevitCheckoutConfig {
11
11
  /** Your Reevit public key (required for API-created intents; omit for payment links) */
12
12
  publicKey?: string;
13
- /** Amount in the smallest currency unit (e.g., pesewas for GHS) */
14
- amount: number;
15
- /** Currency code (e.g., 'GHS', 'NGN', 'USD') */
16
- currency: string;
13
+ /** Server-created checkout session secret. Prefer this for browser checkouts. */
14
+ sessionSecret?: string;
15
+ /** Amount in the smallest currency unit (e.g., pesewas for GHS). Required unless sessionSecret or initialPaymentIntent is provided. */
16
+ amount?: number;
17
+ /** Currency code (e.g., 'GHS', 'NGN', 'USD'). Required unless sessionSecret or initialPaymentIntent is provided. */
18
+ currency?: string;
17
19
  /** Customer email address */
18
20
  email?: string;
19
21
  /** Customer phone number (required for mobile money) */
@@ -247,6 +249,7 @@ interface PaymentIntentResponse {
247
249
  provider_ref_id?: string;
248
250
  status: string;
249
251
  client_secret: string;
252
+ session_secret?: string;
250
253
  psp_public_key: string;
251
254
  psp_credentials?: {
252
255
  merchantAccount?: string | number;
@@ -267,6 +270,13 @@ interface PaymentIntentResponse {
267
270
  }>;
268
271
  branding?: Record<string, unknown>;
269
272
  }
273
+ interface CheckoutSessionResponse {
274
+ id: string;
275
+ client_secret: string;
276
+ session_secret: string;
277
+ payment_intent: PaymentIntentResponse;
278
+ expires_at?: string;
279
+ }
270
280
  interface ConfirmPaymentRequest {
271
281
  provider_ref_id: string;
272
282
  provider_data?: Record<string, unknown>;
@@ -298,8 +308,15 @@ interface PaymentDetailResponse {
298
308
  interface APIErrorResponse {
299
309
  code: string;
300
310
  message: string;
301
- details?: Record<string, string>;
311
+ details?: Record<string, unknown>;
302
312
  }
313
+ type ReevitAPIResult<T> = {
314
+ data: T;
315
+ error?: never;
316
+ } | {
317
+ data?: never;
318
+ error: PaymentError;
319
+ };
303
320
  interface ReevitAPIClientConfig {
304
321
  /** Your Reevit public key */
305
322
  publicKey?: string;
@@ -309,11 +326,48 @@ interface ReevitAPIClientConfig {
309
326
  timeout?: number;
310
327
  }
311
328
  /**
312
- * Generates a deterministic idempotency key based on input parameters
313
- * Uses a simple hash function suitable for browser environments
314
- * Exported for use by SDK hooks (e.g., payment link flows)
329
+ * Creates a PaymentError from an API error response
330
+ */
331
+ declare function createPaymentError(response: Response, errorData: APIErrorResponse): PaymentError;
332
+ declare function isPaymentError(error: unknown): error is PaymentError;
333
+ /**
334
+ * Generates a deterministic **cache/lookup** key from input parameters.
335
+ *
336
+ * NEVER SEND THIS ON THE WIRE. It is a 32-bit djb2 hash bucketed into
337
+ * 5-minute windows, so two unrelated shoppers can collide and be handed each
338
+ * other's payment intent (and therefore each other's `client_secret`), and a
339
+ * shopper legitimately buying the same item twice inside one window would be
340
+ * charged once. Its only job is to identify "the same checkout attempt" inside
341
+ * a single browser tab so the in-flight intent cache can dedupe a repeated
342
+ * "Continue" click.
343
+ *
344
+ * The value actually sent as `Idempotency-Key` is produced by
345
+ * {@link newIdempotencyKey} / {@link attemptIdempotencyKey}.
346
+ *
347
+ * Exported for use by SDK hooks (e.g. payment link flows).
315
348
  */
316
349
  declare function generateIdempotencyKey(params: Record<string, unknown>): string;
350
+ /**
351
+ * Generates a fresh, globally unique `Idempotency-Key` (RFC 4122 v4 UUID).
352
+ * This is the only value that should ever be sent on the wire.
353
+ */
354
+ declare function newIdempotencyKey(): string;
355
+ /**
356
+ * Resolves the stable per-checkout-attempt wire key for a deterministic
357
+ * lookup key (see {@link generateIdempotencyKey}).
358
+ *
359
+ * The first call for a lookup key mints a UUID and stores it in
360
+ * `sessionStorage` (falling back to a module-level map when storage is
361
+ * unavailable); every later call in the same tab returns that same UUID, so a
362
+ * repeated "Continue" click is still deduped by the backend. A different tab,
363
+ * a different shopper or a cleared store yields a different UUID.
364
+ */
365
+ declare function attemptIdempotencyKey(lookupKey: string): string;
366
+ /**
367
+ * Forgets every stored per-attempt key, so the next checkout attempt gets a
368
+ * fresh `Idempotency-Key`. Call it after a completed checkout (and in tests).
369
+ */
370
+ declare function clearIdempotencyAttemptKeys(): void;
317
371
  /**
318
372
  * Reevit API Client
319
373
  */
@@ -344,6 +398,13 @@ declare class ReevitAPIClient {
344
398
  data?: PaymentDetailResponse;
345
399
  error?: PaymentError;
346
400
  }>;
401
+ /**
402
+ * Retrieves a server-created checkout session using its public session secret.
403
+ */
404
+ getCheckoutSession(sessionSecret: string): Promise<{
405
+ data?: CheckoutSessionResponse;
406
+ error?: PaymentError;
407
+ }>;
347
408
  /**
348
409
  * Confirms a payment after PSP callback
349
410
  */
@@ -389,6 +450,19 @@ declare function createReevitClient(config: ReevitAPIClientConfig): ReevitAPICli
389
450
  * Shared utilities for Reevit SDKs
390
451
  */
391
452
 
453
+ /**
454
+ * Returns how many decimal places a currency's minor unit uses: 2 for GHS and
455
+ * NGN, 0 for XOF, XAF, RWF, UGX, JPY and friends.
456
+ *
457
+ * Dividing every amount by 100 renders a 5,000 XOF charge as "XOF 50.00" while
458
+ * the shopper is actually charged 5,000 — which is why this exists.
459
+ */
460
+ declare function currencyExponent(currency: string): number;
461
+ /**
462
+ * Converts a major-unit amount (what a shopper types) into the minor units the
463
+ * API expects: `toMinorUnits(45, 'GHS') === 4500`, `toMinorUnits(5000, 'XOF') === 5000`.
464
+ */
465
+ declare function toMinorUnits(major: number, currency: string): number;
392
466
  /**
393
467
  * Formats an amount from smallest currency unit to display format
394
468
  */
@@ -424,6 +498,18 @@ declare function detectCountryFromCurrency(currency: string): string;
424
498
 
425
499
  /**
426
500
  * Intent identity + cache helpers
501
+ *
502
+ * Two different keys are in play here and mixing them up is a money bug:
503
+ *
504
+ * - the **lookup key** is the deterministic djb2 hash of the checkout
505
+ * parameters (`generateIdempotencyKey`). It identifies "the same checkout
506
+ * attempt" for the in-flight cache and is never sent to the API.
507
+ * - the **wire key** is the per-attempt UUID (`attemptIdempotencyKey`) that
508
+ * goes out as the `Idempotency-Key` header.
509
+ *
510
+ * The cache is keyed by the lookup key and remembers the wire key it minted.
511
+ * The public helpers accept either key so callers that only ever saw the
512
+ * `idempotencyKey` field keep working unchanged.
427
513
  */
428
514
 
429
515
  interface IntentIdentityOptions {
@@ -438,16 +524,21 @@ interface IntentCacheEntry {
438
524
  response?: PaymentIntentResponse;
439
525
  expiresAt: number;
440
526
  reference?: string;
527
+ /** The `Idempotency-Key` sent on the wire for this attempt. */
528
+ idempotencyKey?: string;
441
529
  }
442
530
  declare function resolveIntentIdentity(options: IntentIdentityOptions): {
531
+ /** The value to send as `Idempotency-Key`. */
443
532
  idempotencyKey: string;
533
+ /** The local cache key. Never send this on the wire. */
534
+ lookupKey: string;
444
535
  reference: string;
445
536
  cacheEntry?: IntentCacheEntry;
446
537
  };
447
- declare function getIntentCacheEntry(idempotencyKey: string): IntentCacheEntry | undefined;
448
- declare function cacheIntentPromise(idempotencyKey: string, promise: Promise<PaymentIntentResponse>): IntentCacheEntry;
449
- declare function cacheIntentResponse(idempotencyKey: string, response: PaymentIntentResponse): IntentCacheEntry;
450
- declare function clearIntentCacheEntry(idempotencyKey: string): void;
538
+ declare function getIntentCacheEntry(key: string): IntentCacheEntry | undefined;
539
+ declare function cacheIntentPromise(key: string, promise: Promise<PaymentIntentResponse>): IntentCacheEntry;
540
+ declare function cacheIntentResponse(key: string, response: PaymentIntentResponse): IntentCacheEntry;
541
+ declare function clearIntentCacheEntry(key: string): void;
451
542
 
452
543
  /**
453
544
  * Reevit State Machine
@@ -494,4 +585,4 @@ declare function createInitialState(): ReevitState;
494
585
  */
495
586
  declare function reevitReducer(state: ReevitState, action: ReevitAction): ReevitState;
496
587
 
497
- export { type APIErrorResponse, type CardFormData, type CheckoutProviderOption, type CheckoutState, type ConfirmPaymentRequest, type CreatePaymentIntentRequest, type HubtelSessionResponse, type IntentCacheEntry, type MobileMoneyFormData, type MobileMoneyNetwork, type PSPConfig, type PSPType, type PaymentDetailResponse, type PaymentError, type PaymentIntent, type PaymentIntentResponse, type PaymentMethod, type PaymentResult, type PaymentSource, ReevitAPIClient, type ReevitAPIClientConfig, type ReevitAction, type ReevitCheckoutCallbacks, type ReevitCheckoutConfig, type ReevitState, type ReevitTheme, cacheIntentPromise, cacheIntentResponse, clearIntentCacheEntry, cn, createInitialState, createReevitClient, createThemeVariables, detectCountryFromCurrency, detectNetwork, formatAmount, formatPhone, generateIdempotencyKey, generateReference, getIntentCacheEntry, reevitReducer, resolveIntentIdentity, validatePhone };
588
+ export { type APIErrorResponse, type CardFormData, type CheckoutProviderOption, type CheckoutSessionResponse, type CheckoutState, type ConfirmPaymentRequest, type CreatePaymentIntentRequest, type HubtelSessionResponse, type IntentCacheEntry, type MobileMoneyFormData, type MobileMoneyNetwork, type PSPConfig, type PSPType, type PaymentDetailResponse, type PaymentError, type PaymentIntent, type PaymentIntentResponse, type PaymentMethod, type PaymentResult, type PaymentSource, ReevitAPIClient, type ReevitAPIClientConfig, type ReevitAPIResult, type ReevitAction, type ReevitCheckoutCallbacks, type ReevitCheckoutConfig, type ReevitState, type ReevitTheme, attemptIdempotencyKey, cacheIntentPromise, cacheIntentResponse, clearIdempotencyAttemptKeys, clearIntentCacheEntry, cn, createInitialState, createPaymentError, createReevitClient, createThemeVariables, currencyExponent, detectCountryFromCurrency, detectNetwork, formatAmount, formatPhone, generateIdempotencyKey, generateReference, getIntentCacheEntry, isPaymentError, newIdempotencyKey, reevitReducer, resolveIntentIdentity, toMinorUnits, validatePhone };