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.
- package/LICENSE +17 -0
- package/README.md +135 -0
- package/android/build.gradle +46 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameCheckoutActivity.kt +40 -0
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameFlowActivity.kt +84 -0
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameSDKModule.kt +175 -0
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameSDKPackage.kt +16 -0
- package/ios/FrameReactNative.podspec +26 -0
- package/ios/FrameSDKBridge.m +26 -0
- package/ios/FrameSDKBridge.swift +161 -0
- package/lib/errors.d.ts +25 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +40 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +9 -0
- package/lib/native.d.ts +18 -0
- package/lib/native.d.ts.map +1 -0
- package/lib/native.js +49 -0
- package/lib/types.d.ts +36 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +5 -0
- package/package.json +64 -0
- package/src/__tests__/native.test.ts +116 -0
- package/src/errors.ts +53 -0
- package/src/index.ts +12 -0
- package/src/native.ts +78 -0
- package/src/types.ts +38 -0
|
@@ -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
|
package/lib/errors.d.ts
ADDED
|
@@ -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';
|
package/lib/native.d.ts
ADDED
|
@@ -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
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';
|