framepayments-react-native 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.
@@ -0,0 +1,161 @@
1
+ //
2
+ // FrameSDKBridge.swift
3
+ // FrameReactNative
4
+ //
5
+ // Bridges Frame iOS SDK to React Native.
6
+ //
7
+
8
+ import Foundation
9
+ import React
10
+ import UIKit
11
+ import SwiftUI
12
+ import Frame
13
+
14
+ @objc(FrameSDK)
15
+ class FrameSDKBridge: NSObject {
16
+
17
+ @objc static func requiresMainQueueSetup() -> Bool {
18
+ return true
19
+ }
20
+
21
+ @objc
22
+ func initialize(_ apiKey: String, debugMode: Bool, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
23
+ DispatchQueue.main.async {
24
+ FrameNetworking.shared.initializeWithAPIKey(apiKey, debugMode: debugMode)
25
+ resolve(nil)
26
+ }
27
+ }
28
+
29
+ @objc
30
+ func presentCheckout(_ customerId: NSObject, amount: NSNumber, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
31
+ let cId = customerId as? String
32
+ let amountInt = amount.intValue
33
+ DispatchQueue.main.async { [weak self] in
34
+ self?.presentCheckoutOnMain(customerId: cId, amount: amountInt, resolve: resolve, reject: reject)
35
+ }
36
+ }
37
+
38
+ private func presentCheckoutOnMain(customerId: String?, amount: Int, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
39
+ guard let windowScene = UIApplication.shared.connectedScenes
40
+ .compactMap({ $0 as? UIWindowScene })
41
+ .first(where: { $0.activationState == .foregroundActive }),
42
+ let rootVC = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController else {
43
+ reject("NO_ROOT_VC", "Could not find root view controller to present checkout", nil)
44
+ return
45
+ }
46
+ var top = rootVC
47
+ while let presented = top.presentedViewController { top = presented }
48
+ let delegate = CheckoutDismissDelegate(resolve: resolve, reject: reject)
49
+ let checkoutView = FrameCheckoutView(
50
+ customerId: customerId,
51
+ paymentAmount: amount,
52
+ checkoutCallback: { [weak top, weak delegate] chargeIntent in
53
+ delegate?.didComplete = true
54
+ top?.dismiss(animated: true)
55
+ if let dict = Self.encodeChargeIntent(chargeIntent) {
56
+ resolve(dict)
57
+ } else {
58
+ reject("ENCODE_ERROR", "Failed to encode charge intent", nil)
59
+ }
60
+ }
61
+ )
62
+ let hosting = UIHostingController(rootView: checkoutView)
63
+ hosting.modalPresentationStyle = .pageSheet
64
+ if let sheet = hosting.sheetPresentationController {
65
+ sheet.detents = [.large()]
66
+ }
67
+ objc_setAssociatedObject(hosting, &checkoutDismissKey, delegate, .OBJC_ASSOCIATION_RETAIN)
68
+ hosting.presentationController?.delegate = delegate
69
+ top.present(hosting, animated: true)
70
+ }
71
+
72
+ private static func encodeChargeIntent(_ intent: FrameObjects.ChargeIntent) -> [String: Any]? {
73
+ guard let data = try? JSONEncoder().encode(intent) else { return nil }
74
+ return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
75
+ }
76
+
77
+ @objc
78
+ func presentCart(_ customerId: NSObject, items: NSArray, shippingAmountInCents: NSNumber, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
79
+ let cId = customerId as? String
80
+ let shipping = shippingAmountInCents.intValue
81
+ guard let cartItems = parseCartItems(items) else {
82
+ reject("INVALID_ITEMS", "Invalid cart items array", nil)
83
+ return
84
+ }
85
+ DispatchQueue.main.async { [weak self] in
86
+ self?.presentCartOnMain(customerId: cId, cartItems: cartItems, shippingAmountInCents: shipping, resolve: resolve, reject: reject)
87
+ }
88
+ }
89
+
90
+ private struct RNFrameCartItem: FrameCartItem {
91
+ var id: String
92
+ var imageURL: String
93
+ var title: String
94
+ var amountInCents: Int
95
+ }
96
+
97
+ private func parseCartItems(_ items: NSArray) -> [RNFrameCartItem]? {
98
+ var result: [RNFrameCartItem] = []
99
+ for item in items {
100
+ guard let dict = item as? NSDictionary,
101
+ let id = dict["id"] as? String,
102
+ let title = dict["title"] as? String,
103
+ let amountInCents = dict["amountInCents"] as? Int else { return nil }
104
+ let imageURL = (dict["imageUrl"] as? String) ?? (dict["imageURL"] as? String) ?? ""
105
+ result.append(RNFrameCartItem(id: id, imageURL: imageURL, title: title, amountInCents: amountInCents))
106
+ }
107
+ return result
108
+ }
109
+
110
+ private func presentCartOnMain(customerId: String?, cartItems: [RNFrameCartItem], shippingAmountInCents: Int, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
111
+ guard let windowScene = UIApplication.shared.connectedScenes
112
+ .compactMap({ $0 as? UIWindowScene })
113
+ .first(where: { $0.activationState == .foregroundActive }),
114
+ let rootVC = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController else {
115
+ reject("NO_ROOT_VC", "Could not find root view controller to present cart", nil)
116
+ return
117
+ }
118
+ var top = rootVC
119
+ while let presented = top.presentedViewController { top = presented }
120
+ let cartView = FrameCartView(
121
+ customer: nil,
122
+ cartItems: cartItems,
123
+ shippingAmountInCents: shippingAmountInCents
124
+ )
125
+ let hosting = UIHostingController(rootView: cartView)
126
+ hosting.modalPresentationStyle = .pageSheet
127
+ if let sheet = hosting.sheetPresentationController {
128
+ sheet.detents = [.large()]
129
+ }
130
+ // When the sheet is dismissed (swipe or close), resolve. Note: FrameCartView does not expose ChargeIntent from nested checkout.
131
+ let delegate = CartDismissDelegate(resolve: resolve)
132
+ objc_setAssociatedObject(hosting, &cartDismissKey, delegate, .OBJC_ASSOCIATION_RETAIN)
133
+ hosting.presentationController?.delegate = delegate
134
+ top.present(hosting, animated: true)
135
+ }
136
+ }
137
+
138
+ private final class CheckoutDismissDelegate: NSObject, UIAdaptivePresentationControllerDelegate {
139
+ let resolve: RCTPromiseResolveBlock
140
+ let reject: RCTPromiseRejectBlock
141
+ var didComplete = false
142
+ init(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
143
+ self.resolve = resolve
144
+ self.reject = reject
145
+ }
146
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
147
+ guard !didComplete else { return }
148
+ reject("USER_CANCELED", "User dismissed checkout without completing payment", nil)
149
+ }
150
+ }
151
+
152
+ private final class CartDismissDelegate: NSObject, UIAdaptivePresentationControllerDelegate {
153
+ let resolve: RCTPromiseResolveBlock
154
+ init(resolve: @escaping RCTPromiseResolveBlock) { self.resolve = resolve }
155
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
156
+ resolve([String: Any]())
157
+ }
158
+ }
159
+
160
+ private var cartDismissKey: UInt8 = 0
161
+ private var checkoutDismissKey: UInt8 = 0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Standard error codes and shape for the Frame React Native SDK.
3
+ */
4
+ export declare const ErrorCodes: {
5
+ readonly NOT_INITIALIZED: "NOT_INITIALIZED";
6
+ readonly USER_CANCELED: "USER_CANCELED";
7
+ readonly NO_ROOT_VC: "NO_ROOT_VC";
8
+ readonly NO_ACTIVITY: "NO_ACTIVITY";
9
+ readonly INVALID_ITEMS: "INVALID_ITEMS";
10
+ readonly NETWORK_ERROR: "NETWORK_ERROR";
11
+ readonly API_ERROR: "API_ERROR";
12
+ readonly PARSE_ERROR: "PARSE_ERROR";
13
+ readonly NO_RESULT: "NO_RESULT";
14
+ readonly INIT_FAILED: "INIT_FAILED";
15
+ readonly ENCODE_ERROR: "ENCODE_ERROR";
16
+ };
17
+ export type FrameErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
18
+ export interface FrameErrorShape {
19
+ code: string;
20
+ message: string;
21
+ nativeError?: string;
22
+ }
23
+ export declare function isFrameError(error: unknown): error is FrameErrorShape;
24
+ export declare function normalizeToFrameError(reject: unknown): FrameErrorShape;
25
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,UAAU;;;;;;;;;;;;CAYb,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CASrE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAetE"}
package/lib/errors.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Standard error codes and shape for the Frame React Native SDK.
3
+ */
4
+ export const ErrorCodes = {
5
+ NOT_INITIALIZED: 'NOT_INITIALIZED',
6
+ USER_CANCELED: 'USER_CANCELED',
7
+ NO_ROOT_VC: 'NO_ROOT_VC',
8
+ NO_ACTIVITY: 'NO_ACTIVITY',
9
+ INVALID_ITEMS: 'INVALID_ITEMS',
10
+ NETWORK_ERROR: 'NETWORK_ERROR',
11
+ API_ERROR: 'API_ERROR',
12
+ PARSE_ERROR: 'PARSE_ERROR',
13
+ NO_RESULT: 'NO_RESULT',
14
+ INIT_FAILED: 'INIT_FAILED',
15
+ ENCODE_ERROR: 'ENCODE_ERROR',
16
+ };
17
+ export function isFrameError(error) {
18
+ return (typeof error === 'object' &&
19
+ error !== null &&
20
+ 'code' in error &&
21
+ 'message' in error &&
22
+ typeof error.code === 'string' &&
23
+ typeof error.message === 'string');
24
+ }
25
+ export function normalizeToFrameError(reject) {
26
+ if (isFrameError(reject)) {
27
+ return reject;
28
+ }
29
+ if (reject instanceof Error) {
30
+ const code = reject.code ?? 'UNKNOWN_ERROR';
31
+ return { code, message: reject.message, nativeError: reject.stack };
32
+ }
33
+ if (typeof reject === 'object' && reject !== null && 'code' in reject && 'message' in reject) {
34
+ return reject;
35
+ }
36
+ return {
37
+ code: 'UNKNOWN_ERROR',
38
+ message: String(reject),
39
+ };
40
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @framepayments/react-native-frame
3
+ *
4
+ * React Native SDK for Frame Payments.
5
+ * - Initialize the SDK, then use presentCheckout / presentCart for payment UI.
6
+ * - For API calls (customers, charge intents, refunds), use the framepayments (frame-node) package from JS.
7
+ */
8
+ export { initialize, presentCheckout, presentCart } from './native';
9
+ export type { FrameCartItem, ChargeIntent, FrameError } from './types';
10
+ export { ErrorCodes } from './errors';
11
+ export type { FrameErrorShape, FrameErrorCode } from './errors';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACpE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @framepayments/react-native-frame
3
+ *
4
+ * React Native SDK for Frame Payments.
5
+ * - Initialize the SDK, then use presentCheckout / presentCart for payment UI.
6
+ * - For API calls (customers, charge intents, refunds), use the framepayments (frame-node) package from JS.
7
+ */
8
+ export { initialize, presentCheckout, presentCart } from './native';
9
+ export { ErrorCodes } from './errors';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Native module bridge. Uses NativeModules for classic React Native bridge.
3
+ */
4
+ import type { ChargeIntent, FrameCartItem } from './types';
5
+ export declare function initialize(options: {
6
+ apiKey: string;
7
+ debugMode?: boolean;
8
+ }): void;
9
+ export declare function presentCheckout(options: {
10
+ customerId?: string | null;
11
+ amount: number;
12
+ }): Promise<ChargeIntent>;
13
+ export declare function presentCart(options: {
14
+ customerId?: string | null;
15
+ items: FrameCartItem[];
16
+ shippingAmountInCents: number;
17
+ }): Promise<ChargeIntent>;
18
+ //# sourceMappingURL=native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAmB3D,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAMjF;AAwBD,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,YAAY,CAAC,CAKxB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACnC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;CAC/B,GAAG,OAAO,CAAC,YAAY,CAAC,CASxB"}
package/lib/native.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Native module bridge. Uses NativeModules for classic React Native bridge.
3
+ */
4
+ import { NativeModules } from 'react-native';
5
+ import { ErrorCodes } from './errors';
6
+ const LINKING_ERROR = `The package '@framepayments/react-native-frame' doesn't seem to be linked. Make sure you have run 'pod install' (iOS) or rebuilt the app (Android).`;
7
+ const FrameSDK = NativeModules.FrameSDK
8
+ ? NativeModules.FrameSDK
9
+ : new Proxy({}, {
10
+ get() {
11
+ throw new Error(LINKING_ERROR);
12
+ },
13
+ });
14
+ let isInitialized = false;
15
+ export function initialize(options) {
16
+ if (!options?.apiKey) {
17
+ throw new Error('Frame.initialize requires apiKey');
18
+ }
19
+ FrameSDK.initialize(options.apiKey, options.debugMode ?? false);
20
+ isInitialized = true;
21
+ }
22
+ function guardInitialized() {
23
+ if (!isInitialized) {
24
+ const message = 'Frame SDK must be initialized before calling presentCheckout or presentCart. Call Frame.initialize({ apiKey }) first.';
25
+ const err = new Error(message);
26
+ err.code = ErrorCodes.NOT_INITIALIZED;
27
+ throw err;
28
+ }
29
+ }
30
+ function wrapPromise(p) {
31
+ return p.catch((err) => {
32
+ let code = 'UNKNOWN_ERROR';
33
+ let message = String(err?.message ?? err);
34
+ if (err?.code)
35
+ code = err.code;
36
+ if (typeof err === 'object' && err !== null && 'message' in err) {
37
+ message = String(err.message);
38
+ }
39
+ throw Object.assign(new Error(message), { code, message });
40
+ });
41
+ }
42
+ export function presentCheckout(options) {
43
+ guardInitialized();
44
+ return wrapPromise(FrameSDK.presentCheckout(options.customerId ?? null, options.amount));
45
+ }
46
+ export function presentCart(options) {
47
+ guardInitialized();
48
+ return wrapPromise(FrameSDK.presentCart(options.customerId ?? null, options.items, options.shippingAmountInCents));
49
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Types for the Frame React Native SDK modal APIs.
3
+ * For other types (Customer, Refund, etc.), use the framepayments (frame-node) package when calling APIs from JS.
4
+ */
5
+ /** Cart item for presentCart({ items }) */
6
+ export interface FrameCartItem {
7
+ id: string;
8
+ title: string;
9
+ amountInCents: number;
10
+ imageUrl: string;
11
+ }
12
+ /** Charge intent returned from presentCheckout / presentCart */
13
+ export interface ChargeIntent {
14
+ id: string;
15
+ currency: string;
16
+ amount: number;
17
+ status: string;
18
+ created: number;
19
+ updated: number;
20
+ livemode: boolean;
21
+ object: string;
22
+ description?: string;
23
+ customer?: Record<string, unknown>;
24
+ payment_method?: Record<string, unknown>;
25
+ latest_charge?: Record<string, unknown>;
26
+ authorization_mode?: string;
27
+ failure_description?: string;
28
+ shipping?: Record<string, unknown>;
29
+ }
30
+ /** Error shape when native module rejects (same as FrameErrorShape from errors.ts) */
31
+ export interface FrameError {
32
+ code: string;
33
+ message: string;
34
+ nativeError?: string;
35
+ }
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
package/lib/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Types for the Frame React Native SDK modal APIs.
3
+ * For other types (Customer, Refund, etc.), use the framepayments (frame-node) package when calling APIs from JS.
4
+ */
5
+ export {};
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "framepayments-react-native",
3
+ "version": "1.0.0",
4
+ "description": "React Native SDK for Frame Payments - modal checkout and cart, with API usage via frame-node.",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "typecheck": "tsc --noEmit",
10
+ "lint": "eslint src --ext .ts,.tsx",
11
+ "test": "jest",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "react-native",
16
+ "frame",
17
+ "payments",
18
+ "framepayments"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/Frame-Payments/frame-react-native.git"
23
+ },
24
+ "author": "Frame Payments <engineering@framepayments.com>",
25
+ "license": "Apache-2.0",
26
+ "bugs": {
27
+ "url": "https://github.com/Frame-Payments/frame-react-native/issues"
28
+ },
29
+ "homepage": "https://github.com/Frame-Payments/frame-react-native#readme",
30
+ "peerDependencies": {
31
+ "react": "*",
32
+ "react-native": ">=0.72.0",
33
+ "framepayments": "*"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "framepayments": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "eslint": "^8.57.0",
42
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
43
+ "@typescript-eslint/parser": "^6.21.0",
44
+ "@types/jest": "^29.5.0",
45
+ "@types/react": "^18.2.0",
46
+ "@types/react-native": "^0.72.0",
47
+ "jest": "^29.7.0",
48
+ "react": "18.2.0",
49
+ "react-native": "0.72.0",
50
+ "ts-jest": "^29.1.0",
51
+ "typescript": "^5.0.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=16"
55
+ },
56
+ "files": [
57
+ "src",
58
+ "lib",
59
+ "ios",
60
+ "android",
61
+ "README.md",
62
+ "LICENSE"
63
+ ]
64
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Unit tests for the native module bridge (initialize, presentCheckout, presentCart).
3
+ * NativeModules.FrameSDK is mocked.
4
+ */
5
+
6
+ const mockInitialize = jest.fn((_apiKey: string, _debugMode: boolean) => Promise.resolve());
7
+ const mockPresentCheckout = jest.fn((_customerId: unknown, _amount: number) => Promise.resolve({ id: 'ci_1', amount: 10000 }));
8
+ const mockPresentCart = jest.fn((_customerId: unknown, _items: unknown[], _shipping: number) => Promise.resolve({ id: 'ci_2', amount: 15000 }));
9
+
10
+ jest.mock('react-native', () => ({
11
+ NativeModules: {
12
+ FrameSDK: {
13
+ initialize: mockInitialize,
14
+ presentCheckout: mockPresentCheckout,
15
+ presentCart: mockPresentCart,
16
+ },
17
+ },
18
+ }));
19
+
20
+ // Re-import after mock so we get the mocked NativeModules
21
+ let initialize: (opts: { apiKey: string; debugMode?: boolean }) => void;
22
+ let presentCheckout: (opts: { customerId?: string | null; amount: number }) => Promise<unknown>;
23
+ let presentCart: (opts: {
24
+ customerId?: string | null;
25
+ items: Array<{ id: string; title: string; amountInCents: number; imageUrl: string }>;
26
+ shippingAmountInCents: number;
27
+ }) => Promise<unknown>;
28
+
29
+ beforeEach(() => {
30
+ jest.resetModules();
31
+ mockInitialize.mockClear();
32
+ mockPresentCheckout.mockClear();
33
+ mockPresentCart.mockClear();
34
+ const native = require('../native');
35
+ initialize = native.initialize;
36
+ presentCheckout = native.presentCheckout;
37
+ presentCart = native.presentCart;
38
+ });
39
+
40
+ describe('initialize', () => {
41
+ it('calls native FrameSDK.initialize with apiKey and debugMode', () => {
42
+ initialize({ apiKey: 'sk_test_xxx', debugMode: true });
43
+ expect(mockInitialize).toHaveBeenCalledTimes(1);
44
+ expect(mockInitialize).toHaveBeenCalledWith('sk_test_xxx', true);
45
+ });
46
+
47
+ it('defaults debugMode to false', () => {
48
+ initialize({ apiKey: 'sk_test_yyy' });
49
+ expect(mockInitialize).toHaveBeenCalledWith('sk_test_yyy', false);
50
+ });
51
+
52
+ it('throws if apiKey is missing', () => {
53
+ expect(() => initialize({ apiKey: '' })).toThrow();
54
+ expect(() => (initialize as any)({})).toThrow();
55
+ expect(mockInitialize).not.toHaveBeenCalled();
56
+ });
57
+ });
58
+
59
+ describe('presentCheckout', () => {
60
+ it('throws NOT_INITIALIZED if initialize was not called', async () => {
61
+ try {
62
+ await presentCheckout({ amount: 10000 });
63
+ expect(true).toBe(false);
64
+ } catch (e: any) {
65
+ expect(e.code).toBe('NOT_INITIALIZED');
66
+ expect(e.message).toContain('initialized');
67
+ }
68
+ expect(mockPresentCheckout).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it('calls native presentCheckout with customerId and amount after initialize', async () => {
72
+ initialize({ apiKey: 'sk_xxx' });
73
+ const result = await presentCheckout({ customerId: 'cus_1', amount: 10000 });
74
+ expect(mockPresentCheckout).toHaveBeenCalledWith('cus_1', 10000);
75
+ expect(result).toEqual({ id: 'ci_1', amount: 10000 });
76
+ });
77
+
78
+ it('passes null for customerId when not provided', async () => {
79
+ initialize({ apiKey: 'sk_xxx' });
80
+ await presentCheckout({ amount: 5000 });
81
+ expect(mockPresentCheckout).toHaveBeenCalledWith(null, 5000);
82
+ });
83
+ });
84
+
85
+ describe('presentCart', () => {
86
+ const items = [
87
+ { id: '1', title: 'Item A', amountInCents: 1000, imageUrl: 'https://example.com/a.jpg' },
88
+ ];
89
+
90
+ it('throws NOT_INITIALIZED if initialize was not called', async () => {
91
+ try {
92
+ await presentCart({ items, shippingAmountInCents: 500 });
93
+ expect(true).toBe(false);
94
+ } catch (e: any) {
95
+ expect(e.code).toBe('NOT_INITIALIZED');
96
+ }
97
+ expect(mockPresentCart).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('calls native presentCart with customerId, items, shipping after initialize', async () => {
101
+ initialize({ apiKey: 'sk_xxx' });
102
+ const result = await presentCart({
103
+ customerId: 'cus_2',
104
+ items,
105
+ shippingAmountInCents: 500,
106
+ });
107
+ expect(mockPresentCart).toHaveBeenCalledWith('cus_2', items, 500);
108
+ expect(result).toEqual({ id: 'ci_2', amount: 15000 });
109
+ });
110
+
111
+ it('passes null for customerId when not provided', async () => {
112
+ initialize({ apiKey: 'sk_xxx' });
113
+ await presentCart({ items, shippingAmountInCents: 0 });
114
+ expect(mockPresentCart).toHaveBeenCalledWith(null, items, 0);
115
+ });
116
+ });
package/src/errors.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Standard error codes and shape for the Frame React Native SDK.
3
+ */
4
+
5
+ export const ErrorCodes = {
6
+ NOT_INITIALIZED: 'NOT_INITIALIZED',
7
+ USER_CANCELED: 'USER_CANCELED',
8
+ NO_ROOT_VC: 'NO_ROOT_VC',
9
+ NO_ACTIVITY: 'NO_ACTIVITY',
10
+ INVALID_ITEMS: 'INVALID_ITEMS',
11
+ NETWORK_ERROR: 'NETWORK_ERROR',
12
+ API_ERROR: 'API_ERROR',
13
+ PARSE_ERROR: 'PARSE_ERROR',
14
+ NO_RESULT: 'NO_RESULT',
15
+ INIT_FAILED: 'INIT_FAILED',
16
+ ENCODE_ERROR: 'ENCODE_ERROR',
17
+ } as const;
18
+
19
+ export type FrameErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
20
+
21
+ export interface FrameErrorShape {
22
+ code: string;
23
+ message: string;
24
+ nativeError?: string;
25
+ }
26
+
27
+ export function isFrameError(error: unknown): error is FrameErrorShape {
28
+ return (
29
+ typeof error === 'object' &&
30
+ error !== null &&
31
+ 'code' in error &&
32
+ 'message' in error &&
33
+ typeof (error as FrameErrorShape).code === 'string' &&
34
+ typeof (error as FrameErrorShape).message === 'string'
35
+ );
36
+ }
37
+
38
+ export function normalizeToFrameError(reject: unknown): FrameErrorShape {
39
+ if (isFrameError(reject)) {
40
+ return reject;
41
+ }
42
+ if (reject instanceof Error) {
43
+ const code = (reject as Error & { code?: string }).code ?? 'UNKNOWN_ERROR';
44
+ return { code, message: reject.message, nativeError: reject.stack };
45
+ }
46
+ if (typeof reject === 'object' && reject !== null && 'code' in reject && 'message' in reject) {
47
+ return reject as FrameErrorShape;
48
+ }
49
+ return {
50
+ code: 'UNKNOWN_ERROR',
51
+ message: String(reject),
52
+ };
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @framepayments/react-native-frame
3
+ *
4
+ * React Native SDK for Frame Payments.
5
+ * - Initialize the SDK, then use presentCheckout / presentCart for payment UI.
6
+ * - For API calls (customers, charge intents, refunds), use the framepayments (frame-node) package from JS.
7
+ */
8
+
9
+ export { initialize, presentCheckout, presentCart } from './native';
10
+ export type { FrameCartItem, ChargeIntent, FrameError } from './types';
11
+ export { ErrorCodes } from './errors';
12
+ export type { FrameErrorShape, FrameErrorCode } from './errors';