From 04f355045fd605026b8c56c28a2255086e8a2139 Mon Sep 17 00:00:00 2001 From: Cesar Sosa <101717495+cesar-sosa-hol@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:11:17 +0100 Subject: [PATCH] upgrade to mobilepaymentsdk to 2.1.0 and add tap to pay methods for ios (#39) * upgrade to 2.1.0 and add tap to pay methods * fix Freeland and Plinio comments * remove console.log --- example/ios/Podfile.lock | 16 ++-- example/src/App.tsx | 4 +- example/src/Screens/HomeScreen.tsx | 1 + example/src/Screens/PermissionsScreen.tsx | 14 ++-- example/src/components/LoadingButton.tsx | 8 +- example/src/components/PermissionRow.tsx | 7 +- ios/MobilePaymentsSdkReactNative.mm | 12 +++ ios/MobilePaymentsSdkReactNative.swift | 95 +++++++++++++++++++++++ ios/Podfile | 2 +- ios/Podfile.lock | 8 +- package.json | 2 +- src/managers/reader.ts | 35 +++++++++ 12 files changed, 178 insertions(+), 26 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 1007296..4b8628b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -7,11 +7,11 @@ PODS: - hermes-engine (0.75.3): - hermes-engine/Pre-built (= 0.75.3) - hermes-engine/Pre-built (0.75.3) - - mobile-payments-sdk-react-native (2025.2.0): + - mobile-payments-sdk-react-native (2025.2.1): - DoubleConversion - glog - hermes-engine - - MockReaderUI (~> 2.0.2) + - MockReaderUI (~> 2.1.0) - RCT-Folly (= 2024.01.01.00) - RCTRequired - RCTTypeSafety @@ -28,9 +28,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - SquareMobilePaymentsSDK (~> 2.0.2) + - SquareMobilePaymentsSDK (~> 2.1.0) - Yoga - - MockReaderUI (2.0.2) + - MockReaderUI (2.1.0) - Permission-BluetoothPeripheral (3.10.1): - RNPermissions - Permission-LocationAccuracy (3.10.1): @@ -1579,7 +1579,7 @@ PODS: - ReactCommon/turbomodule/core - Yoga - SocketRocket (0.7.0) - - SquareMobilePaymentsSDK (2.0.2) + - SquareMobilePaymentsSDK (2.1.0) - Yoga (0.0.0) DEPENDENCIES: @@ -1818,8 +1818,8 @@ SPEC CHECKSUMS: fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 glog: 69ef571f3de08433d766d614c73a9838a06bf7eb hermes-engine: 8d2103d6c0176779aea4e25df6bb1410f9946680 - mobile-payments-sdk-react-native: f8b78eb939714c7368b6268a8f3591b7785d6219 - MockReaderUI: 859bff6aaab222b59c1d8839efad2d7fca22135a + mobile-payments-sdk-react-native: 267ce60e2818bf09c363e2d201e3c506d94a250b + MockReaderUI: 141fe6bb56f63d22e375800ee6a5c5da69ffb55a Permission-BluetoothPeripheral: 34ab829f159c6cf400c57bac05f5ba1b0af7a86e Permission-LocationAccuracy: 30c5421911024b28d8916db5cbd728097da54434 Permission-LocationAlways: af165dee8a5a5888df6764f9f6ba98b112893709 @@ -1885,7 +1885,7 @@ SPEC CHECKSUMS: RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9 RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - SquareMobilePaymentsSDK: 1964169130cab2f447d767adf9c18f8b5d212ff6 + SquareMobilePaymentsSDK: b437afc89530142af80ed818f4db02e07c47457c Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63 PODFILE CHECKSUM: 439764333dadb7461aea4b653e55d92f832f43a0 diff --git a/example/src/App.tsx b/example/src/App.tsx index 9b30596..e83a6d8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -9,12 +9,12 @@ export default function App() { return ( - - { const presentMockReader = async () => { try { const result = await showMockReaderUI(); + console.log(result); setMockReaderPresented(true); } catch (error) { console.log('Mock Reader UI error:', error); diff --git a/example/src/Screens/PermissionsScreen.tsx b/example/src/Screens/PermissionsScreen.tsx index 86ce9a9..0026e6e 100644 --- a/example/src/Screens/PermissionsScreen.tsx +++ b/example/src/Screens/PermissionsScreen.tsx @@ -144,8 +144,7 @@ const PermissionsView = () => { useState(false); const [bluetoothPermissionGranted, setBluetoothPermissionGranted] = useState(false); - const [readPhoneStateGranted, setReadPhoneStateGranted] = - useState(isIos); + const [readPhoneStateGranted, setReadPhoneStateGranted] = useState(isIos); const [isLoading, setIsLoading] = useState(false); const [isAuthorized, setIsAuthorized] = useState(false); const navigation = useNavigation(); @@ -158,6 +157,7 @@ const PermissionsView = () => { 'MOBILE_PAYMENT_SDK_ACCESS_TOKEN', 'MOBILE_PAYMENT_SDK_LOCATION_ID' ); + console.log(auth); let authorizedLocation = await getAuthorizedLocation(); let authorizationState = await getAuthorizationState(); setIsAuthorized(true); @@ -169,9 +169,7 @@ const PermissionsView = () => { ); } catch (error) { setIsAuthorized(false); - console.log( - 'Authorization error: ', JSON.stringify(error) - ); + console.log('Authorization error: ', JSON.stringify(error)); Alert.alert('Error Authenticating', error.message); } setIsLoading(false); @@ -206,7 +204,7 @@ const PermissionsView = () => { // Remember to remove your observer once the component has been removed from the DOM stopObservingAuthorizationChanges(); }; - }); + }, []); return ( @@ -248,7 +246,9 @@ const PermissionsView = () => { activeLabel="Sign in" inactiveLabel="Sign out" /> - + {isAuthorized ? 'This device is Authorized' : 'Device not Authorized'} diff --git a/example/src/components/LoadingButton.tsx b/example/src/components/LoadingButton.tsx index 802c04c..f588d80 100644 --- a/example/src/components/LoadingButton.tsx +++ b/example/src/components/LoadingButton.tsx @@ -25,7 +25,13 @@ const LoadingButton = ({ {isLoading ? ( ) : ( - + {isActive ? activeLabel : inactiveLabel} )} diff --git a/example/src/components/PermissionRow.tsx b/example/src/components/PermissionRow.tsx index d8cbd00..36742c2 100644 --- a/example/src/components/PermissionRow.tsx +++ b/example/src/components/PermissionRow.tsx @@ -14,8 +14,8 @@ const PermissionRow = ({ title, description, isGranted, onRequest }) => ( size={25} fillColor="gray" unFillColor="#FFFFFF" - innerIconStyle={{ borderWidth: 2 }} - onPress={(isChecked: boolean) => { + innerIconStyle={styles.permissionCheckBox} + onPress={() => { onRequest(); }} /> @@ -29,6 +29,9 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingBottom: 5, }, + permissionCheckBox: { + borderWidth: 2, + }, permissionTitle: { fontSize: 18, fontWeight: '600', diff --git a/ios/MobilePaymentsSdkReactNative.mm b/ios/MobilePaymentsSdkReactNative.mm index 1bcbad9..24a5026 100644 --- a/ios/MobilePaymentsSdkReactNative.mm +++ b/ios/MobilePaymentsSdkReactNative.mm @@ -44,6 +44,18 @@ @interface RCT_EXTERN_MODULE(MobilePaymentsSdkReactNative, NSObject) RCT_EXTERN_METHOD(getSDKVersion:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(linkAppleAccount:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(relinkAppleAccount:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(isAppleAccountLinked:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(isDeviceCapable:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + + (BOOL)requiresMainQueueSetup { return YES; diff --git a/ios/MobilePaymentsSdkReactNative.swift b/ios/MobilePaymentsSdkReactNative.swift index c10646c..e068207 100644 --- a/ios/MobilePaymentsSdkReactNative.swift +++ b/ios/MobilePaymentsSdkReactNative.swift @@ -126,6 +126,101 @@ class MobilePaymentsSdkReactNative: RCTEventEmitter { return resolve(mobilePaymentsSDK.settingsManager.sdkSettings.version) } + func parseTapToPayError(error: NSError, defaultError: String) -> String { + + let tapToPayReaderError = TapToPayReaderError(rawValue: (error).code) + + let errorMessage: String + switch tapToPayReaderError { + case .alreadyLinked: + errorMessage = "Apple Tap to Pay Terms and Conditions have already been accepted." + case .banned: + errorMessage = "This device is banned from using the Tap To Pay reader." + case .linkingFailed: + errorMessage = "The Tap To Pay reader could not link/relink using the provided Apple ID." + case .linkingCanceled: + errorMessage = "User has canceled the linking/relinking operation." + case .invalidToken: + errorMessage = "The Tap To Pay reader generated an invalid token." + case .notAuthorized: + errorMessage = "This device must be authorized with a Square account in order to use Tap To Pay." + case .notAvailable: + errorMessage = "The Tap To Pay reader is not available on this device or device's operating system." + case .noNetwork: + errorMessage = "The Tap To Pay reader could not connect to the network. Please reconnect to the Internet and try again." + case .networkError: + errorMessage = "The network responded with an error." + case .other: + errorMessage = "An error with the Tap To Pay reader has occurred. Please try again." + case .passcodeDisabled: + errorMessage = "This device does not currently have an active passcode set." + case .unexpected: + errorMessage = "Mobile Payments SDK encountered an unexpected error. Please try again." + case .unsupportedOSVersion: + errorMessage = "The device's OS version does not meet the minimum requirement of iOS 16.7 for Tap to Pay on iPhone." + case .unsupportedDeviceModel: + errorMessage = "This device model is not currently supported to use the Tap To Pay reader." + default: + errorMessage = defaultError + } + + return errorMessage + + } + + @objc(linkAppleAccount:withRejecter:) + func linkAppleAccount(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + DispatchQueue.main.async { [weak self] in + + + self?.mobilePaymentsSDK.readerManager.tapToPaySettings.linkAppleAccount {error in + + if (error != nil) { + let errorMessage = self?.parseTapToPayError(error: (error! as NSError), defaultError: "There has been an error linking apple account.") + reject("LINK_APPLE_ACCOUNT_ERROR", errorMessage, (error as? NSError)?.reactNativeError); + } else { + resolve("Apple account has been linked.") + } + + } + } + } + + @objc(relinkAppleAccount:withRejecter:) + func relinkAppleAccount(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + DispatchQueue.main.async { [weak self] in + self?.mobilePaymentsSDK.readerManager.tapToPaySettings.relinkAppleAccount {error in + + if (error != nil) { + let errorMessage = self?.parseTapToPayError(error: (error! as NSError), defaultError: "There has been an error re-linking apple account.") + reject("RE_LINK_APPLE_ACCOUNT_ERROR", errorMessage, (error as? NSError)?.reactNativeError); + } else { + resolve("Apple account has been re-linked.") + } + + } + } + } + + @objc(isAppleAccountLinked:withRejecter:) + func isAppleAccountLinked(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + DispatchQueue.main.async { [weak self] in + self?.mobilePaymentsSDK.readerManager.tapToPaySettings.isAppleAccountLinked { isLinked, error in + if (error != nil) { + let errorMessage = self?.parseTapToPayError(error: (error! as NSError), defaultError: "There has been an error checking if Apple Account is Linked.") + reject("IS_APPLE_ACCOUNT_LINKED_ERROR", errorMessage, (error as? NSError)?.reactNativeError); + } else { + resolve(isLinked) + } + } + } + } + + @objc(isDeviceCapable:withRejecter:) + func isDeviceCapable(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + return resolve(mobilePaymentsSDK.readerManager.tapToPaySettings.isDeviceCapable) + } + /// Mock Readers, available only in Sandbox: https://developer.squareup.com/docs/mobile-payments-sdk/ios#mock-readers private lazy var mockReaderUI: MockReaderUI? = { guard mobilePaymentsSDK.settingsManager.sdkSettings.environment == .sandbox else { diff --git a/ios/Podfile b/ios/Podfile index b4eaa8d..b4c904d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -6,7 +6,7 @@ target 'RNMobilePaymentSDK' do use_frameworks! # Pods for RNMobilePaymentSDK - pod "SquareMobilePaymentsSDK", "~> 2.0.2" + pod "SquareMobilePaymentsSDK", "~> 2.1.0" target 'RNMobilePaymentSDKTests' do inherit! :search_paths # Pods for testing diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8615ec2..d4f0c1e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - SquareMobilePaymentsSDK (2.0.2) + - SquareMobilePaymentsSDK (2.1.0) DEPENDENCIES: - - SquareMobilePaymentsSDK (~> 2.0.2) + - SquareMobilePaymentsSDK (~> 2.1.0) SPEC REPOS: trunk: - SquareMobilePaymentsSDK SPEC CHECKSUMS: - SquareMobilePaymentsSDK: 1964169130cab2f447d767adf9c18f8b5d212ff6 + SquareMobilePaymentsSDK: b437afc89530142af80ed818f4db02e07c47457c -PODFILE CHECKSUM: 1c0f22efdb235538a7a49119eaced2170ab7e369 +PODFILE CHECKSUM: 36ec71d8ea5709b9c8c2277616eae29f2769b341 COCOAPODS: 1.16.2 diff --git a/package.json b/package.json index 56ce332..5354ac0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mobile-payments-sdk-react-native", "version": "2025.2.1", "description": "Mobile Payments SDK plug-in for React Native. Enables developers to build secure in-person payment solutions.", - "mobilePaymentsSdkVersion": "2.0.2", + "mobilePaymentsSdkVersion": "2.1.0", "source": "./src/index.tsx", "main": "./lib/commonjs/index.js", "module": "./lib/module/index.js", diff --git a/src/managers/reader.ts b/src/managers/reader.ts index b04dd60..a688d9e 100644 --- a/src/managers/reader.ts +++ b/src/managers/reader.ts @@ -1,3 +1,4 @@ +import { Platform } from 'react-native'; import MobilePaymentsSdkReactNative from '../base_sdk'; export const startPairing = (): Promise => { @@ -15,3 +16,37 @@ export const showMockReaderUI = (): Promise => { export const hideMockReaderUI = (): Promise => { return MobilePaymentsSdkReactNative.hideMockReaderUI(); }; + +export namespace TapToPaySettings { + const notSupportedError = (): never => { + throw new Error('This feature is only available on iOS.'); + }; + + export const linkAppleAccount = (): Promise => { + return Platform.select({ + ios: () => MobilePaymentsSdkReactNative.linkAppleAccount(), + android: () => Promise.reject(notSupportedError()), + })!(); + }; + + export const relinkAppleAccount = (): Promise => { + return Platform.select({ + ios: () => MobilePaymentsSdkReactNative.relinkAppleAccount(), + android: () => Promise.reject(notSupportedError()), + })!(); + }; + + export const isAppleAccountLinked = (): Promise => { + return Platform.select({ + ios: () => MobilePaymentsSdkReactNative.isAppleAccountLinked(), + android: () => Promise.reject(notSupportedError()), + })!(); + }; + + export const isDeviceCapable = (): Promise => { + return Platform.select({ + ios: () => MobilePaymentsSdkReactNative.isDeviceCapable(), + android: () => Promise.reject(notSupportedError()), + })!(); + }; +}