@idealyst/payments 1.2.108

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 ADDED
@@ -0,0 +1,100 @@
1
+ # @idealyst/payments
2
+
3
+ Cross-platform payment provider abstractions for React and React Native. Wraps Stripe's Platform Pay API for Apple Pay and Google Pay on mobile.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ yarn add @idealyst/payments
9
+ ```
10
+
11
+ ### Native (required for mobile payments)
12
+
13
+ ```bash
14
+ yarn add @stripe/stripe-react-native
15
+ ```
16
+
17
+ Follow the [Stripe React Native setup guide](https://github.com/stripe/stripe-react-native#installation) for iOS/Android configuration.
18
+
19
+ ### Web
20
+
21
+ No additional dependencies. Web provides a stub that reports all payment methods as unavailable — use [Stripe Elements](https://stripe.com/docs/stripe-js/react) (`@stripe/react-stripe-js`) for web payments.
22
+
23
+ ## Quick Start
24
+
25
+ ```tsx
26
+ import { usePayments } from '@idealyst/payments';
27
+
28
+ function CheckoutScreen() {
29
+ const {
30
+ isReady,
31
+ isApplePayAvailable,
32
+ isGooglePayAvailable,
33
+ isProcessing,
34
+ error,
35
+ confirmPayment,
36
+ } = usePayments({
37
+ config: {
38
+ publishableKey: 'pk_test_xxx',
39
+ merchantIdentifier: 'merchant.com.myapp',
40
+ merchantName: 'My App',
41
+ merchantCountryCode: 'US',
42
+ },
43
+ });
44
+
45
+ const handlePay = async () => {
46
+ const result = await confirmPayment({
47
+ clientSecret: 'pi_xxx_secret_xxx', // from your server
48
+ amount: { amount: 1099, currencyCode: 'usd' },
49
+ lineItems: [
50
+ { label: 'Widget', amount: 999 },
51
+ { label: 'Tax', amount: 100 },
52
+ ],
53
+ });
54
+ console.log('Paid:', result.paymentIntentId);
55
+ };
56
+
57
+ if (!isReady) return null;
58
+
59
+ return (
60
+ <Button
61
+ onPress={handlePay}
62
+ disabled={isProcessing || (!isApplePayAvailable && !isGooglePayAvailable)}
63
+ label={isApplePayAvailable ? 'Pay with Apple Pay' : 'Pay with Google Pay'}
64
+ />
65
+ );
66
+ }
67
+ ```
68
+
69
+ ## Flat Function API
70
+
71
+ For more control, use the flat functions directly:
72
+
73
+ ```typescript
74
+ import {
75
+ initializePayments,
76
+ checkPaymentAvailability,
77
+ confirmPayment,
78
+ createPaymentMethod,
79
+ } from '@idealyst/payments';
80
+
81
+ await initializePayments({
82
+ publishableKey: 'pk_test_xxx',
83
+ merchantName: 'My App',
84
+ merchantCountryCode: 'US',
85
+ });
86
+
87
+ const methods = await checkPaymentAvailability();
88
+ const result = await confirmPayment({ clientSecret: '...', amount: { amount: 1099, currencyCode: 'usd' } });
89
+ ```
90
+
91
+ ## Platform Support
92
+
93
+ | Feature | iOS | Android | Web |
94
+ |---------|-----|---------|-----|
95
+ | Apple Pay | Yes | — | — |
96
+ | Google Pay | — | Yes | — |
97
+ | Card (via sheet) | Yes | Yes | — |
98
+ | usePayments hook | Yes | Yes | Stub |
99
+
100
+ Web returns all methods as unavailable. Use `@stripe/react-stripe-js` for web payments.
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@idealyst/payments",
3
+ "version": "1.2.108",
4
+ "description": "Cross-platform payment provider abstractions for React and React Native (Apple Pay, Google Pay, Stripe)",
5
+ "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/payments#readme",
6
+ "readme": "README.md",
7
+ "main": "src/index.ts",
8
+ "module": "src/index.ts",
9
+ "types": "src/index.ts",
10
+ "react-native": "src/index.native.ts",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/IdealystIO/idealyst-framework.git",
14
+ "directory": "packages/payments"
15
+ },
16
+ "author": "Idealyst <contact@idealyst.io>",
17
+ "license": "MIT",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "react-native": "./src/index.native.ts",
24
+ "browser": {
25
+ "types": "./src/index.web.ts",
26
+ "import": "./src/index.web.ts",
27
+ "require": "./src/index.web.ts"
28
+ },
29
+ "default": {
30
+ "types": "./src/index.ts",
31
+ "import": "./src/index.ts",
32
+ "require": "./src/index.ts"
33
+ }
34
+ }
35
+ },
36
+ "scripts": {
37
+ "prepublishOnly": "echo 'Publishing TypeScript source directly'",
38
+ "publish:npm": "npm publish"
39
+ },
40
+ "peerDependencies": {
41
+ "@stripe/stripe-react-native": ">=0.38.0",
42
+ "react": ">=16.8.0",
43
+ "react-native": ">=0.60.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "@stripe/stripe-react-native": {
47
+ "optional": true
48
+ },
49
+ "react-native": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "devDependencies": {
54
+ "@types/react": "^19.1.0",
55
+ "typescript": "^5.0.0"
56
+ },
57
+ "files": [
58
+ "src",
59
+ "README.md"
60
+ ],
61
+ "keywords": [
62
+ "react",
63
+ "react-native",
64
+ "payments",
65
+ "apple-pay",
66
+ "google-pay",
67
+ "stripe",
68
+ "platform-pay",
69
+ "cross-platform"
70
+ ]
71
+ }
@@ -0,0 +1,7 @@
1
+ import type { PaymentProviderStatus } from './types';
2
+
3
+ export const INITIAL_PROVIDER_STATUS: PaymentProviderStatus = {
4
+ state: 'uninitialized',
5
+ availablePaymentMethods: [],
6
+ isPaymentAvailable: false,
7
+ };
package/src/errors.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { PaymentError, PaymentErrorCode } from './types';
2
+
3
+ export function createPaymentError(
4
+ code: PaymentErrorCode,
5
+ message: string,
6
+ originalError?: unknown,
7
+ ): PaymentError {
8
+ return { code, message, originalError };
9
+ }
10
+
11
+ /**
12
+ * Normalize a Stripe error (or any thrown value) into a PaymentError.
13
+ */
14
+ export function normalizeError(error: unknown): PaymentError {
15
+ if (error && typeof error === 'object' && 'code' in error) {
16
+ const stripeError = error as { code?: string; message?: string };
17
+
18
+ switch (stripeError.code) {
19
+ case 'Canceled':
20
+ case 'cancelled':
21
+ return createPaymentError(
22
+ 'user_cancelled',
23
+ 'Payment was cancelled by user',
24
+ error,
25
+ );
26
+ case 'Failed':
27
+ return createPaymentError(
28
+ 'payment_failed',
29
+ stripeError.message || 'Payment failed',
30
+ error,
31
+ );
32
+ default:
33
+ return createPaymentError(
34
+ 'provider_error',
35
+ stripeError.message || 'An error occurred with the payment provider',
36
+ error,
37
+ );
38
+ }
39
+ }
40
+
41
+ if (error instanceof Error) {
42
+ return createPaymentError('unknown', error.message, error);
43
+ }
44
+
45
+ return createPaymentError('unknown', String(error), error);
46
+ }
@@ -0,0 +1,27 @@
1
+ export * from './types';
2
+ export { INITIAL_PROVIDER_STATUS } from './constants';
3
+ export { createPaymentError, normalizeError } from './errors';
4
+ export {
5
+ initializePayments,
6
+ checkPaymentAvailability,
7
+ confirmPayment,
8
+ createPaymentMethod,
9
+ getPaymentStatus,
10
+ } from './payments.native';
11
+
12
+ import { createUsePaymentsHook } from './usePayments';
13
+ import {
14
+ initializePayments,
15
+ checkPaymentAvailability,
16
+ confirmPayment,
17
+ createPaymentMethod,
18
+ getPaymentStatus,
19
+ } from './payments.native';
20
+
21
+ export const usePayments = createUsePaymentsHook({
22
+ initializePayments,
23
+ checkPaymentAvailability,
24
+ confirmPayment,
25
+ createPaymentMethod,
26
+ getPaymentStatus,
27
+ });
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ // Default entry — re-exports web implementation.
2
+ // Platform-specific entry points (index.native.ts, index.web.ts) are resolved by bundlers.
3
+ export * from './types';
4
+ export { INITIAL_PROVIDER_STATUS } from './constants';
5
+ export { createPaymentError, normalizeError } from './errors';
6
+ export {
7
+ initializePayments,
8
+ checkPaymentAvailability,
9
+ confirmPayment,
10
+ createPaymentMethod,
11
+ getPaymentStatus,
12
+ } from './payments.web';
13
+
14
+ import { createUsePaymentsHook } from './usePayments';
15
+ import {
16
+ initializePayments,
17
+ checkPaymentAvailability,
18
+ confirmPayment,
19
+ createPaymentMethod,
20
+ getPaymentStatus,
21
+ } from './payments.web';
22
+
23
+ export const usePayments = createUsePaymentsHook({
24
+ initializePayments,
25
+ checkPaymentAvailability,
26
+ confirmPayment,
27
+ createPaymentMethod,
28
+ getPaymentStatus,
29
+ });
@@ -0,0 +1,27 @@
1
+ export * from './types';
2
+ export { INITIAL_PROVIDER_STATUS } from './constants';
3
+ export { createPaymentError, normalizeError } from './errors';
4
+ export {
5
+ initializePayments,
6
+ checkPaymentAvailability,
7
+ confirmPayment,
8
+ createPaymentMethod,
9
+ getPaymentStatus,
10
+ } from './payments.web';
11
+
12
+ import { createUsePaymentsHook } from './usePayments';
13
+ import {
14
+ initializePayments,
15
+ checkPaymentAvailability,
16
+ confirmPayment,
17
+ createPaymentMethod,
18
+ getPaymentStatus,
19
+ } from './payments.web';
20
+
21
+ export const usePayments = createUsePaymentsHook({
22
+ initializePayments,
23
+ checkPaymentAvailability,
24
+ confirmPayment,
25
+ createPaymentMethod,
26
+ getPaymentStatus,
27
+ });
@@ -0,0 +1,301 @@
1
+ // ============================================================================
2
+ // Native Payment Implementation
3
+ // Wraps @stripe/stripe-react-native Platform Pay API for Apple Pay & Google Pay
4
+ // ============================================================================
5
+
6
+ import { Platform } from 'react-native';
7
+ import type {
8
+ PaymentConfig,
9
+ PaymentProviderStatus,
10
+ PaymentMethodAvailability,
11
+ PaymentMethodType,
12
+ PaymentSheetRequest,
13
+ PaymentResult,
14
+ } from './types';
15
+ import { INITIAL_PROVIDER_STATUS } from './constants';
16
+ import { createPaymentError, normalizeError } from './errors';
17
+
18
+ // Graceful optional import — @stripe/stripe-react-native may not be installed
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ let Stripe: any = null;
21
+ try {
22
+ Stripe = require('@stripe/stripe-react-native');
23
+ } catch {
24
+ // Will degrade gracefully when methods are called
25
+ }
26
+
27
+ // Module-level state
28
+ let _status: PaymentProviderStatus = { ...INITIAL_PROVIDER_STATUS };
29
+ let _config: PaymentConfig | null = null;
30
+
31
+ /**
32
+ * Get the current payment provider status.
33
+ */
34
+ export function getPaymentStatus(): PaymentProviderStatus {
35
+ return { ..._status };
36
+ }
37
+
38
+ /**
39
+ * Initialize the Stripe SDK for platform payments.
40
+ */
41
+ export async function initializePayments(
42
+ config: PaymentConfig,
43
+ ): Promise<void> {
44
+ if (!Stripe) {
45
+ _status = {
46
+ ..._status,
47
+ state: 'error',
48
+ error: createPaymentError(
49
+ 'not_available',
50
+ '@stripe/stripe-react-native is not installed. Run: yarn add @stripe/stripe-react-native',
51
+ ),
52
+ };
53
+ return;
54
+ }
55
+
56
+ _status = { ..._status, state: 'initializing' };
57
+ _config = config;
58
+
59
+ try {
60
+ await Stripe.initStripe({
61
+ publishableKey: config.publishableKey,
62
+ merchantIdentifier: config.merchantIdentifier,
63
+ urlScheme: config.urlScheme,
64
+ });
65
+
66
+ const availability = await checkPaymentAvailability();
67
+
68
+ _status = {
69
+ state: 'ready',
70
+ availablePaymentMethods: availability,
71
+ isPaymentAvailable: availability.some((m) => m.isAvailable),
72
+ };
73
+ } catch (error) {
74
+ _status = {
75
+ ..._status,
76
+ state: 'error',
77
+ error: normalizeError(error),
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check which payment methods (Apple Pay, Google Pay, card) are available.
84
+ */
85
+ export async function checkPaymentAvailability(): Promise<
86
+ PaymentMethodAvailability[]
87
+ > {
88
+ if (!Stripe) {
89
+ return [
90
+ {
91
+ type: 'apple_pay',
92
+ isAvailable: false,
93
+ unavailableReason: 'Stripe SDK not installed',
94
+ },
95
+ {
96
+ type: 'google_pay',
97
+ isAvailable: false,
98
+ unavailableReason: 'Stripe SDK not installed',
99
+ },
100
+ { type: 'card', isAvailable: false, unavailableReason: 'Stripe SDK not installed' },
101
+ ];
102
+ }
103
+
104
+ const results: PaymentMethodAvailability[] = [];
105
+ const isIOS = Platform.OS === 'ios';
106
+
107
+ try {
108
+ const isSupported = await Stripe.isPlatformPaySupported({
109
+ googlePay: {
110
+ testEnv: _config?.testEnvironment ?? false,
111
+ },
112
+ });
113
+
114
+ results.push({
115
+ type: 'apple_pay',
116
+ isAvailable: isIOS && isSupported,
117
+ unavailableReason: !isIOS
118
+ ? 'Apple Pay is only available on iOS'
119
+ : !isSupported
120
+ ? 'Apple Pay is not configured on this device'
121
+ : undefined,
122
+ });
123
+
124
+ results.push({
125
+ type: 'google_pay',
126
+ isAvailable: !isIOS && isSupported,
127
+ unavailableReason: isIOS
128
+ ? 'Google Pay is only available on Android'
129
+ : !isSupported
130
+ ? 'Google Pay is not configured on this device'
131
+ : undefined,
132
+ });
133
+ } catch (error) {
134
+ results.push(
135
+ {
136
+ type: 'apple_pay',
137
+ isAvailable: false,
138
+ unavailableReason: String(error),
139
+ },
140
+ {
141
+ type: 'google_pay',
142
+ isAvailable: false,
143
+ unavailableReason: String(error),
144
+ },
145
+ );
146
+ }
147
+
148
+ // Card payments are always conceptually available via Stripe
149
+ results.push({ type: 'card', isAvailable: true });
150
+
151
+ _status = {
152
+ ..._status,
153
+ availablePaymentMethods: results,
154
+ isPaymentAvailable: results.some((m) => m.isAvailable),
155
+ };
156
+
157
+ return results;
158
+ }
159
+
160
+ /**
161
+ * Present the platform payment sheet and confirm a PaymentIntent.
162
+ * Requires `request.clientSecret` from a server-created PaymentIntent.
163
+ */
164
+ export async function confirmPayment(
165
+ request: PaymentSheetRequest,
166
+ ): Promise<PaymentResult> {
167
+ assertReady();
168
+
169
+ if (!request.clientSecret) {
170
+ throw createPaymentError(
171
+ 'invalid_request',
172
+ 'clientSecret is required for confirmPayment. Create a PaymentIntent on your server first.',
173
+ );
174
+ }
175
+
176
+ try {
177
+ const { error, paymentIntent } =
178
+ await Stripe!.confirmPlatformPayPayment(request.clientSecret, {
179
+ googlePay: buildGooglePayConfig(request),
180
+ applePay: buildApplePayConfig(request),
181
+ });
182
+
183
+ if (error) {
184
+ throw error;
185
+ }
186
+
187
+ return {
188
+ paymentMethodType: detectPlatformPayType(),
189
+ paymentIntentId: paymentIntent?.id,
190
+ status: paymentIntent?.status,
191
+ };
192
+ } catch (error) {
193
+ throw normalizeError(error);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Present the platform payment sheet and create a payment method
199
+ * without confirming. Returns a payment method ID for server-side use.
200
+ */
201
+ export async function createPaymentMethod(
202
+ request: PaymentSheetRequest,
203
+ ): Promise<PaymentResult> {
204
+ assertReady();
205
+
206
+ try {
207
+ const { error, paymentMethod } =
208
+ await Stripe!.createPlatformPayPaymentMethod({
209
+ googlePay: {
210
+ ...buildGooglePayConfig(request),
211
+ amount: request.amount.amount,
212
+ },
213
+ applePay: buildApplePayConfig(request),
214
+ });
215
+
216
+ if (error) {
217
+ throw error;
218
+ }
219
+
220
+ return {
221
+ paymentMethodType: detectPlatformPayType(),
222
+ paymentMethodId: paymentMethod?.id,
223
+ };
224
+ } catch (error) {
225
+ throw normalizeError(error);
226
+ }
227
+ }
228
+
229
+ // ============================================================================
230
+ // Internal Helpers
231
+ // ============================================================================
232
+
233
+ function assertReady(): void {
234
+ if (!Stripe) {
235
+ throw createPaymentError(
236
+ 'not_available',
237
+ '@stripe/stripe-react-native is not installed',
238
+ );
239
+ }
240
+ if (_status.state !== 'ready') {
241
+ throw createPaymentError(
242
+ 'not_initialized',
243
+ 'Payment provider not initialized. Call initializePayments() first.',
244
+ );
245
+ }
246
+ }
247
+
248
+ function detectPlatformPayType(): PaymentMethodType {
249
+ return Platform.OS === 'ios' ? 'apple_pay' : 'google_pay';
250
+ }
251
+
252
+ function buildGooglePayConfig(request: PaymentSheetRequest) {
253
+ return {
254
+ testEnv: _config?.testEnvironment ?? false,
255
+ merchantName: _config!.merchantName,
256
+ merchantCountryCode: _config!.merchantCountryCode,
257
+ currencyCode: request.amount.currencyCode,
258
+ billingAddressConfig: request.billingAddress
259
+ ? {
260
+ isRequired: request.billingAddress.isRequired ?? false,
261
+ isPhoneNumberRequired:
262
+ request.billingAddress.isPhoneNumberRequired ?? false,
263
+ format:
264
+ request.billingAddress.format === 'FULL'
265
+ ? Stripe!.PlatformPay.BillingAddressFormat.Full
266
+ : Stripe!.PlatformPay.BillingAddressFormat.Min,
267
+ }
268
+ : undefined,
269
+ };
270
+ }
271
+
272
+ function buildApplePayConfig(request: PaymentSheetRequest) {
273
+ const cartItems: Array<{
274
+ label: string;
275
+ amount: string;
276
+ paymentType: number;
277
+ }> = [];
278
+
279
+ if (request.lineItems) {
280
+ for (const item of request.lineItems) {
281
+ cartItems.push({
282
+ label: item.label,
283
+ amount: (item.amount / 100).toFixed(2),
284
+ paymentType: Stripe!.PlatformPay.PaymentType.Immediate,
285
+ });
286
+ }
287
+ }
288
+
289
+ // Total line item (required by Apple Pay)
290
+ cartItems.push({
291
+ label: _config?.merchantName ?? 'Total',
292
+ amount: (request.amount.amount / 100).toFixed(2),
293
+ paymentType: Stripe!.PlatformPay.PaymentType.Immediate,
294
+ });
295
+
296
+ return {
297
+ cartItems,
298
+ merchantCountryCode: _config!.merchantCountryCode,
299
+ currencyCode: request.amount.currencyCode,
300
+ };
301
+ }
@@ -0,0 +1,91 @@
1
+ // ============================================================================
2
+ // Web Payment Stub
3
+ //
4
+ // Native payment sheets (Apple Pay, Google Pay) are mobile-only concepts.
5
+ // On web, use Stripe Elements (@stripe/react-stripe-js) or Stripe Checkout.
6
+ //
7
+ // This stub initializes and reports availability so the hook works,
8
+ // but payment operations throw descriptive errors with guidance.
9
+ // ============================================================================
10
+
11
+ import type {
12
+ PaymentConfig,
13
+ PaymentProviderStatus,
14
+ PaymentMethodAvailability,
15
+ PaymentSheetRequest,
16
+ PaymentResult,
17
+ } from './types';
18
+ import { INITIAL_PROVIDER_STATUS } from './constants';
19
+ import { createPaymentError } from './errors';
20
+
21
+ const WEB_NOT_SUPPORTED_MESSAGE =
22
+ '@idealyst/payments does not provide a web payment UI. ' +
23
+ 'Use Stripe Elements (@stripe/react-stripe-js) or Stripe Checkout for web payments.';
24
+
25
+ const WEB_UNAVAILABLE_METHODS: PaymentMethodAvailability[] = [
26
+ {
27
+ type: 'apple_pay',
28
+ isAvailable: false,
29
+ unavailableReason: 'Apple Pay requires native iOS integration',
30
+ },
31
+ {
32
+ type: 'google_pay',
33
+ isAvailable: false,
34
+ unavailableReason: 'Google Pay requires native Android integration',
35
+ },
36
+ {
37
+ type: 'card',
38
+ isAvailable: false,
39
+ unavailableReason:
40
+ 'Use Stripe Elements (@stripe/react-stripe-js) for web card payments',
41
+ },
42
+ ];
43
+
44
+ let _status: PaymentProviderStatus = { ...INITIAL_PROVIDER_STATUS };
45
+
46
+ /**
47
+ * Get the current payment provider status.
48
+ */
49
+ export function getPaymentStatus(): PaymentProviderStatus {
50
+ return { ..._status };
51
+ }
52
+
53
+ /**
54
+ * Initialize — succeeds on web but reports all methods as unavailable.
55
+ */
56
+ export async function initializePayments(
57
+ _config: PaymentConfig,
58
+ ): Promise<void> {
59
+ _status = {
60
+ state: 'ready',
61
+ availablePaymentMethods: WEB_UNAVAILABLE_METHODS,
62
+ isPaymentAvailable: false,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Check availability — all methods are unavailable on web.
68
+ */
69
+ export async function checkPaymentAvailability(): Promise<
70
+ PaymentMethodAvailability[]
71
+ > {
72
+ return WEB_UNAVAILABLE_METHODS;
73
+ }
74
+
75
+ /**
76
+ * Not supported on web — throws with guidance to use Stripe Elements.
77
+ */
78
+ export async function confirmPayment(
79
+ _request: PaymentSheetRequest,
80
+ ): Promise<PaymentResult> {
81
+ throw createPaymentError('not_supported', WEB_NOT_SUPPORTED_MESSAGE);
82
+ }
83
+
84
+ /**
85
+ * Not supported on web — throws with guidance to use Stripe Elements.
86
+ */
87
+ export async function createPaymentMethod(
88
+ _request: PaymentSheetRequest,
89
+ ): Promise<PaymentResult> {
90
+ throw createPaymentError('not_supported', WEB_NOT_SUPPORTED_MESSAGE);
91
+ }
package/src/types.ts ADDED
@@ -0,0 +1,179 @@
1
+ // ============================================================================
2
+ // Payment Method Types
3
+ // ============================================================================
4
+
5
+ export type PaymentMethodType = 'apple_pay' | 'google_pay' | 'card';
6
+
7
+ export interface PaymentMethodAvailability {
8
+ /** The payment method type. */
9
+ type: PaymentMethodType;
10
+ /** Whether this method is available on the current device. */
11
+ isAvailable: boolean;
12
+ /** Reason if unavailable. */
13
+ unavailableReason?: string;
14
+ }
15
+
16
+ // ============================================================================
17
+ // Configuration
18
+ // ============================================================================
19
+
20
+ export interface PaymentConfig {
21
+ /** Stripe publishable key (pk_test_xxx or pk_live_xxx). */
22
+ publishableKey: string;
23
+
24
+ /**
25
+ * Apple Pay merchant identifier (e.g., "merchant.com.yourapp").
26
+ * Required for Apple Pay on iOS.
27
+ */
28
+ merchantIdentifier?: string;
29
+
30
+ /** Merchant display name shown on the payment sheet. */
31
+ merchantName: string;
32
+
33
+ /** ISO 3166-1 alpha-2 country code (e.g., "US", "GB"). */
34
+ merchantCountryCode: string;
35
+
36
+ /**
37
+ * URL scheme for 3D Secure redirects (e.g., "com.yourapp").
38
+ * Required on native for 3D Secure / bank redirect flows.
39
+ */
40
+ urlScheme?: string;
41
+
42
+ /** Use test environment for Google Pay. Default: false. */
43
+ testEnvironment?: boolean;
44
+ }
45
+
46
+ // ============================================================================
47
+ // Payment Request
48
+ // ============================================================================
49
+
50
+ export interface PaymentAmount {
51
+ /** Amount in the smallest currency unit (e.g., cents for USD). */
52
+ amount: number;
53
+ /** ISO 4217 currency code (e.g., "usd", "eur"). */
54
+ currencyCode: string;
55
+ }
56
+
57
+ export interface PaymentLineItem {
58
+ /** Label displayed on the payment sheet (e.g., "Subtotal", "Tax"). */
59
+ label: string;
60
+ /** Amount in the smallest currency unit. */
61
+ amount: number;
62
+ }
63
+
64
+ export interface BillingAddressConfig {
65
+ /** Whether billing address is required. */
66
+ isRequired?: boolean;
67
+ /** Whether phone number is required. */
68
+ isPhoneNumberRequired?: boolean;
69
+ /** Address format: 'MIN' for name+postal, 'FULL' for complete address. */
70
+ format?: 'MIN' | 'FULL';
71
+ }
72
+
73
+ export interface PaymentSheetRequest {
74
+ /**
75
+ * PaymentIntent client secret from your server.
76
+ * Required for confirmPayment(), not needed for createPaymentMethod().
77
+ */
78
+ clientSecret?: string;
79
+
80
+ /** Total payment amount. Used for display and for createPaymentMethod flow. */
81
+ amount: PaymentAmount;
82
+
83
+ /** Line items displayed on the payment sheet. */
84
+ lineItems?: PaymentLineItem[];
85
+
86
+ /** Billing address requirements. */
87
+ billingAddress?: BillingAddressConfig;
88
+
89
+ /** Preferred payment methods. If empty, all available methods are shown. */
90
+ allowedPaymentMethods?: PaymentMethodType[];
91
+ }
92
+
93
+ // ============================================================================
94
+ // Payment Result
95
+ // ============================================================================
96
+
97
+ export interface PaymentResult {
98
+ /** The payment method type that was used. */
99
+ paymentMethodType: PaymentMethodType;
100
+
101
+ /** Stripe payment method ID (pm_xxx). Present for createPaymentMethod flow. */
102
+ paymentMethodId?: string;
103
+
104
+ /** Stripe PaymentIntent ID (pi_xxx). Present for confirmPayment flow. */
105
+ paymentIntentId?: string;
106
+
107
+ /** PaymentIntent status (e.g., 'succeeded', 'requires_capture'). */
108
+ status?: string;
109
+ }
110
+
111
+ // ============================================================================
112
+ // Error Types
113
+ // ============================================================================
114
+
115
+ export type PaymentErrorCode =
116
+ | 'not_initialized'
117
+ | 'not_available'
118
+ | 'not_supported'
119
+ | 'user_cancelled'
120
+ | 'payment_failed'
121
+ | 'network_error'
122
+ | 'invalid_config'
123
+ | 'invalid_request'
124
+ | 'provider_error'
125
+ | 'unknown';
126
+
127
+ export interface PaymentError {
128
+ code: PaymentErrorCode;
129
+ message: string;
130
+ /** Original error from the underlying provider. */
131
+ originalError?: unknown;
132
+ }
133
+
134
+ // ============================================================================
135
+ // Provider State
136
+ // ============================================================================
137
+
138
+ export type PaymentProviderState =
139
+ | 'uninitialized'
140
+ | 'initializing'
141
+ | 'ready'
142
+ | 'error';
143
+
144
+ export interface PaymentProviderStatus {
145
+ state: PaymentProviderState;
146
+ availablePaymentMethods: PaymentMethodAvailability[];
147
+ isPaymentAvailable: boolean;
148
+ error?: PaymentError;
149
+ }
150
+
151
+ // ============================================================================
152
+ // Hook Types
153
+ // ============================================================================
154
+
155
+ export interface UsePaymentsOptions {
156
+ /** If provided, auto-initializes the payment provider. */
157
+ config?: PaymentConfig;
158
+ /** Automatically check availability after init. Default: true. */
159
+ autoCheckAvailability?: boolean;
160
+ }
161
+
162
+ export interface UsePaymentsResult {
163
+ // State
164
+ status: PaymentProviderStatus;
165
+ isReady: boolean;
166
+ isProcessing: boolean;
167
+ availablePaymentMethods: PaymentMethodAvailability[];
168
+ isApplePayAvailable: boolean;
169
+ isGooglePayAvailable: boolean;
170
+ isPaymentAvailable: boolean;
171
+ error: PaymentError | null;
172
+
173
+ // Actions
174
+ initialize: (config: PaymentConfig) => Promise<void>;
175
+ checkAvailability: () => Promise<PaymentMethodAvailability[]>;
176
+ confirmPayment: (request: PaymentSheetRequest) => Promise<PaymentResult>;
177
+ createPaymentMethod: (request: PaymentSheetRequest) => Promise<PaymentResult>;
178
+ clearError: () => void;
179
+ }
@@ -0,0 +1,137 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react';
2
+ import type {
3
+ UsePaymentsOptions,
4
+ UsePaymentsResult,
5
+ PaymentProviderStatus,
6
+ PaymentConfig,
7
+ PaymentError,
8
+ PaymentMethodAvailability,
9
+ PaymentSheetRequest,
10
+ PaymentResult,
11
+ } from './types';
12
+ import { INITIAL_PROVIDER_STATUS } from './constants';
13
+
14
+ /**
15
+ * Factory that creates a usePayments hook bound to platform-specific functions.
16
+ * Each platform entry point calls this with the correct implementations.
17
+ */
18
+ export function createUsePaymentsHook(fns: {
19
+ initializePayments: (config: PaymentConfig) => Promise<void>;
20
+ checkPaymentAvailability: () => Promise<PaymentMethodAvailability[]>;
21
+ confirmPayment: (request: PaymentSheetRequest) => Promise<PaymentResult>;
22
+ createPaymentMethod: (request: PaymentSheetRequest) => Promise<PaymentResult>;
23
+ getPaymentStatus: () => PaymentProviderStatus;
24
+ }) {
25
+ return function usePayments(
26
+ options: UsePaymentsOptions = {},
27
+ ): UsePaymentsResult {
28
+ const { config, autoCheckAvailability = true } = options;
29
+
30
+ const [status, setStatus] = useState<PaymentProviderStatus>(
31
+ INITIAL_PROVIDER_STATUS,
32
+ );
33
+ const [isProcessing, setIsProcessing] = useState(false);
34
+ const [error, setError] = useState<PaymentError | null>(null);
35
+ const initializedRef = useRef(false);
36
+
37
+ // Auto-initialize if config provided
38
+ useEffect(() => {
39
+ if (!config || initializedRef.current) return;
40
+ initializedRef.current = true;
41
+
42
+ const init = async () => {
43
+ await fns.initializePayments(config);
44
+ const currentStatus = fns.getPaymentStatus();
45
+ setStatus(currentStatus);
46
+ setError(currentStatus.error ?? null);
47
+
48
+ if (autoCheckAvailability && currentStatus.state === 'ready') {
49
+ await fns.checkPaymentAvailability();
50
+ setStatus(fns.getPaymentStatus());
51
+ }
52
+ };
53
+ init();
54
+ }, [config, autoCheckAvailability]);
55
+
56
+ const initialize = useCallback(
57
+ async (initConfig: PaymentConfig) => {
58
+ setError(null);
59
+ await fns.initializePayments(initConfig);
60
+ const currentStatus = fns.getPaymentStatus();
61
+ setStatus(currentStatus);
62
+ setError(currentStatus.error ?? null);
63
+
64
+ if (autoCheckAvailability && currentStatus.state === 'ready') {
65
+ await fns.checkPaymentAvailability();
66
+ setStatus(fns.getPaymentStatus());
67
+ }
68
+ },
69
+ [autoCheckAvailability],
70
+ );
71
+
72
+ const checkAvailability = useCallback(async () => {
73
+ const result = await fns.checkPaymentAvailability();
74
+ setStatus(fns.getPaymentStatus());
75
+ return result;
76
+ }, []);
77
+
78
+ const confirmPaymentAction = useCallback(
79
+ async (request: PaymentSheetRequest) => {
80
+ setIsProcessing(true);
81
+ setError(null);
82
+ try {
83
+ return await fns.confirmPayment(request);
84
+ } catch (err) {
85
+ const paymentError = err as PaymentError;
86
+ setError(paymentError);
87
+ throw paymentError;
88
+ } finally {
89
+ setIsProcessing(false);
90
+ }
91
+ },
92
+ [],
93
+ );
94
+
95
+ const createPaymentMethodAction = useCallback(
96
+ async (request: PaymentSheetRequest) => {
97
+ setIsProcessing(true);
98
+ setError(null);
99
+ try {
100
+ return await fns.createPaymentMethod(request);
101
+ } catch (err) {
102
+ const paymentError = err as PaymentError;
103
+ setError(paymentError);
104
+ throw paymentError;
105
+ } finally {
106
+ setIsProcessing(false);
107
+ }
108
+ },
109
+ [],
110
+ );
111
+
112
+ const clearError = useCallback(() => setError(null), []);
113
+
114
+ const { availablePaymentMethods } = status;
115
+
116
+ return {
117
+ status,
118
+ isReady: status.state === 'ready',
119
+ isProcessing,
120
+ availablePaymentMethods,
121
+ isApplePayAvailable: availablePaymentMethods.some(
122
+ (m) => m.type === 'apple_pay' && m.isAvailable,
123
+ ),
124
+ isGooglePayAvailable: availablePaymentMethods.some(
125
+ (m) => m.type === 'google_pay' && m.isAvailable,
126
+ ),
127
+ isPaymentAvailable: status.isPaymentAvailable,
128
+ error,
129
+
130
+ initialize,
131
+ checkAvailability,
132
+ confirmPayment: confirmPaymentAction,
133
+ createPaymentMethod: createPaymentMethodAction,
134
+ clearError,
135
+ };
136
+ };
137
+ }