diff --git a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt index b9670b79..2c9e7493 100644 --- a/android/src/main/java/com/stripeterminalreactnative/Mappers.kt +++ b/android/src/main/java/com/stripeterminalreactnative/Mappers.kt @@ -6,7 +6,9 @@ import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.stripe.stripeterminal.external.CollectInputs +import com.stripe.stripeterminal.external.OfflineMode import com.stripe.stripeterminal.external.models.Address +import com.stripe.stripeterminal.external.models.AmountDetails import com.stripe.stripeterminal.external.models.CardDetails import com.stripe.stripeterminal.external.models.CardPresentDetails import com.stripe.stripeterminal.external.models.CartLineItem @@ -20,6 +22,8 @@ import com.stripe.stripeterminal.external.models.Location import com.stripe.stripeterminal.external.models.LocationStatus import com.stripe.stripeterminal.external.models.NetworkStatus import com.stripe.stripeterminal.external.models.NumericResult +import com.stripe.stripeterminal.external.models.OfflineCardPresentDetails +import com.stripe.stripeterminal.external.models.OfflineDetails import com.stripe.stripeterminal.external.models.OfflineStatus import com.stripe.stripeterminal.external.models.PaymentIntent import com.stripe.stripeterminal.external.models.PaymentIntentStatus @@ -132,15 +136,16 @@ internal fun mapFromDeviceType(type: DeviceType): String { DeviceType.COTS_DEVICE -> "cotsDevice" DeviceType.ETNA -> "etna" DeviceType.STRIPE_M2 -> "stripeM2" + DeviceType.STRIPE_S700 -> "stripeS700" + DeviceType.STRIPE_S700_DEVKIT -> "stripeS700Devkit" DeviceType.UNKNOWN -> "unknown" DeviceType.VERIFONE_P400 -> "verifoneP400" - DeviceType.WISEPAD_3 -> "wisePad3" - DeviceType.WISEPOS_E -> "wisePosE" DeviceType.WISECUBE -> "wisecube" - DeviceType.STRIPE_S700 -> "stripeS700" + DeviceType.WISEPAD_3 -> "wisePad3" DeviceType.WISEPAD_3S -> "wisePad3s" + DeviceType.WISEPOS_E -> "wisePosE" DeviceType.WISEPOS_E_DEVKIT -> "wisePosEDevkit" - DeviceType.STRIPE_S700_DEVKIT -> "stripeS700Devkit" + } } @@ -163,6 +168,7 @@ internal fun mapToDiscoveryMethod(method: String?): DiscoveryMethod? { } } +@OptIn(OfflineMode::class) internal fun mapFromPaymentIntent(paymentIntent: PaymentIntent, uuid: String): ReadableMap = nativeMapOf { putInt("amount", paymentIntent.amount.toInt()) putString("currency", paymentIntent.currency) @@ -173,6 +179,7 @@ internal fun mapFromPaymentIntent(paymentIntent: PaymentIntent, uuid: String): R putString("created", convertToUnixTimestamp(paymentIntent.created)) putString("sdkUuid", uuid) putString("paymentMethodId", paymentIntent.paymentMethodId) + putMap("offlineDetails", mapFromOfflineDetails(paymentIntent?.offlineDetails)) } internal fun mapFromSetupIntent(setupIntent: SetupIntent, uuid: String): ReadableMap = nativeMapOf { @@ -517,6 +524,42 @@ private fun mapFromCardPresentDetails(cardPresentDetails: CardPresentDetails?): ) } +private fun mapFromOfflineDetails(offlineDetails: OfflineDetails?): ReadableMap? = + offlineDetails?.let { + nativeMapOf { + putString("storedAt", offlineDetails.storedAt.toString()) + putBoolean("requiresUpload", offlineDetails.requiresUpload) + putMap( + "cardPresentDetails", + mapFromOfflineCardPresentDetails(offlineDetails.cardPresentDetails) + ) + putMap("amountDetails", mapFromAmountDetails(offlineDetails.amountDetails)) + } + } + +private fun mapFromAmountDetails(amountDetails: AmountDetails?): ReadableMap? = + amountDetails?.let { + nativeMapOf { + putMap("tip", nativeMapOf { putIntOrNull(this, "amount", amountDetails.tip?.amount?.toInt())}) + } + } + +private fun mapFromOfflineCardPresentDetails(offlineCardPresentDetails: OfflineCardPresentDetails?): ReadableMap? = + offlineCardPresentDetails?.let { + nativeMapOf { + putString("brand", offlineCardPresentDetails?.brand) + putString("cardholderName", offlineCardPresentDetails?.cardholderName) + putIntOrNull(this, "expMonth", offlineCardPresentDetails?.expMonth) + putIntOrNull(this, "expYear", offlineCardPresentDetails?.expYear) + putString("last4", offlineCardPresentDetails?.last4) + putString("readMethod", offlineCardPresentDetails?.readMethod) + putMap( + "receiptDetails", + mapFromReceiptDetails(offlineCardPresentDetails?.receiptDetails) + ) + } + } + internal fun mapFromWallet(wallet: Wallet?): ReadableMap = nativeMapOf { putString("type", wallet?.type) @@ -537,8 +580,8 @@ fun mapFromReceiptDetails(receiptDetails: ReceiptDetails?): ReadableMap = putString("authorizationResponseCode", receiptDetails?.authorizationResponseCode) putString("cvm", receiptDetails?.cvm) putString("dedicatedFileName", receiptDetails?.dedicatedFileName) - putString("tsi", receiptDetails?.tsi) - putString("tvr", receiptDetails?.tvr) + putString("transactionStatusInformation", receiptDetails?.tsi) + putString("terminalVerificationResult", receiptDetails?.tvr) } internal fun mapFromNetworkStatus(status: NetworkStatus): String { diff --git a/dev-app/src/App.tsx b/dev-app/src/App.tsx index 1b534dbf..31e122f3 100644 --- a/dev-app/src/App.tsx +++ b/dev-app/src/App.tsx @@ -70,6 +70,7 @@ export type RouteParamList = { CollectCardPayment: { simulated: boolean; discoveryMethod: Reader.DiscoveryMethod; + deviceType: Reader.DeviceType; }; RefundPayment: { simulated: boolean; diff --git a/dev-app/src/screens/CollectCardPaymentScreen.tsx b/dev-app/src/screens/CollectCardPaymentScreen.tsx index a92355bc..8cbb0c81 100644 --- a/dev-app/src/screens/CollectCardPaymentScreen.tsx +++ b/dev-app/src/screens/CollectCardPaymentScreen.tsx @@ -85,7 +85,7 @@ export default function CollectCardPaymentScreen() { const [tipEligibleAmount, setTipEligibleAmount] = useState(''); const { params } = useRoute>(); - const { simulated, discoveryMethod } = params; + const { simulated, discoveryMethod, deviceType } = params; const { addLogs, clearLogs, setCancel } = useContext(LogContext); const navigation = useNavigation(); @@ -165,7 +165,8 @@ export default function CollectCardPaymentScreen() { }; let paymentIntent: PaymentIntent.Type | undefined; let paymentIntentError: StripeError | undefined; - if (discoveryMethod === 'internet') { + + if (deviceType === 'verifoneP400') { const resp = await api.createPaymentIntent({ amount: Number(inputValues.amount), currency: inputValues.currency, @@ -205,21 +206,34 @@ export default function CollectCardPaymentScreen() { paymentIntentError = response.error; } else { const offlineStatus = await getOfflineStatus(); - let storedPaymentAmount = 0; + let sdkStoredPaymentAmount = 0; for (let currency in offlineStatus.sdk.offlinePaymentAmountsByCurrency) { if (currency === inputValues.currency) { - storedPaymentAmount = + sdkStoredPaymentAmount = offlineStatus.sdk.offlinePaymentAmountsByCurrency[currency]; } } + let readerStoredPaymentAmount = 0; + if (offlineStatus.reader) { + for (let currency in offlineStatus.reader + .offlinePaymentAmountsByCurrency) { + if (currency === inputValues.currency) { + readerStoredPaymentAmount = + offlineStatus.reader.offlinePaymentAmountsByCurrency[currency]; + } + } + } if ( Number(inputValues.amount) > Number(inputValues.offlineModeTransactionLimit) || - storedPaymentAmount > + sdkStoredPaymentAmount > + Number(inputValues.offlineModeStoredTransactionLimit) || + readerStoredPaymentAmount > Number(inputValues.offlineModeStoredTransactionLimit) ) { inputValues.offlineBehavior = 'require_online'; } + const response = await createPaymentIntent({ amount: Number(inputValues.amount), currency: inputValues.currency, diff --git a/dev-app/src/screens/DatabaseScreen.tsx b/dev-app/src/screens/DatabaseScreen.tsx index b11f0a5f..b3923dea 100644 --- a/dev-app/src/screens/DatabaseScreen.tsx +++ b/dev-app/src/screens/DatabaseScreen.tsx @@ -43,7 +43,43 @@ export default function DatabaseScreen() { return ( - + + {offlinePaymentStatus && + offlinePaymentStatus.reader && + offlinePaymentStatus.reader.offlinePaymentsCount > 0 ? ( + Object.keys( + offlinePaymentStatus.reader.offlinePaymentAmountsByCurrency + ).map((key) => ( + + )) + ) : ( + <> + )} + + + {' '} + {String( + offlinePaymentStatus && + offlinePaymentStatus.reader && + offlinePaymentStatus.reader.offlinePaymentsCount + ? offlinePaymentStatus.reader.offlinePaymentsCount + : 0 + ) + + ' payment intent(s) for ' + + account?.settings?.dashboard.display_name}{' '} + + {offlinePaymentStatus && offlinePaymentStatus.sdk.offlinePaymentsCount > 0 ? ( Object.keys( @@ -70,7 +106,7 @@ export default function DatabaseScreen() { {' '} {String( - offlinePaymentStatus + offlinePaymentStatus && offlinePaymentStatus.sdk ? offlinePaymentStatus.sdk.offlinePaymentsCount : 0 ) + diff --git a/dev-app/src/screens/HomeScreen.tsx b/dev-app/src/screens/HomeScreen.tsx index a21e7d9c..5cdbfa85 100644 --- a/dev-app/src/screens/HomeScreen.tsx +++ b/dev-app/src/screens/HomeScreen.tsx @@ -57,13 +57,15 @@ export default function HomeScreen() { }, 3000); }, onDidForwardPaymentIntent(paymentIntent, error) { - let toastMsg = - 'Payment Intent ' + - paymentIntent.id + - ' forwarded. ErrorCode' + - error?.code + - '. ErrorMsg = ' + - error?.message; + let toastMsg = 'Payment Intent ' + paymentIntent.id + ' forwarded. '; + if (error) { + toastMsg + + 'ErrorCode = ' + + error.code + + '. ErrorMsg = ' + + error.message; + } + console.log(toastMsg); let toast = Toast.show(toastMsg, { duration: Toast.durations.LONG, position: Toast.positions.BOTTOM, @@ -91,6 +93,7 @@ export default function HomeScreen() { ? '🔋' + batteryPercentage.toFixed(0) + '%' : ''; const chargingStatus = connectedReader?.isCharging ? '🔌' : ''; + const deviceType = connectedReader?.deviceType; useEffect(() => { const loadDiscSettings = async () => { @@ -140,6 +143,7 @@ export default function HomeScreen() { navigation.navigate('CollectCardPaymentScreen', { simulated, discoveryMethod, + deviceType, }); }} /> @@ -213,7 +217,7 @@ export default function HomeScreen() { - {connectedReader.deviceType} + {deviceType} Connected{simulated && , simulated} diff --git a/ios/Mappers.swift b/ios/Mappers.swift index a181f51c..3d7c6f83 100644 --- a/ios/Mappers.swift +++ b/ios/Mappers.swift @@ -62,16 +62,18 @@ class Mappers { class func mapFromDeviceType(_ type: DeviceType) -> String { switch type { + case DeviceType.appleBuiltIn: return "appleBuiltIn" case DeviceType.chipper1X: return "chipper1X" case DeviceType.chipper2X: return "chipper2X" + case DeviceType.etna: return "etna" case DeviceType.stripeM2: return "stripeM2" + case DeviceType.stripeS700: return "stripeS700" + case DeviceType.stripeS700DevKit: return "stripeS700Devkit" case DeviceType.verifoneP400: return "verifoneP400" case DeviceType.wiseCube: return "wiseCube" case DeviceType.wisePad3: return "wisePad3" case DeviceType.wisePosE: return "wisePosE" case DeviceType.wisePosEDevKit: return "wisePosEDevkit" - case DeviceType.stripeS700DevKit: return "stripeS700Devkit" - case DeviceType.appleBuiltIn: return "appleBuiltIn" default: return "unknown" } } @@ -138,6 +140,10 @@ class Mappers { class func mapFromPaymentIntent(_ paymentIntent: PaymentIntent, uuid: String) -> NSDictionary { + var offlineDetailsMap: NSDictionary? + if let offlineDetails = paymentIntent.offlineDetails { + offlineDetailsMap = mapFromOfflineDetails(offlineDetails) + } let result: NSDictionary = [ "amount": paymentIntent.amount, "charges": mapFromCharges(paymentIntent.charges), @@ -147,6 +153,7 @@ class Mappers { "id": paymentIntent.stripeId, "sdkUuid": uuid, "paymentMethodId": paymentIntent.paymentMethodId, + "offlineDetails": offlineDetailsMap ?? NSNull() ] return result } @@ -421,6 +428,58 @@ class Mappers { return result } + class func mapFromOfflineDetails(_ offlineDetails: OfflineDetails) -> NSDictionary { + var offlineCardPresentDetails: NSDictionary? + if let cardPresentDetails = offlineDetails.cardPresentDetails { + offlineCardPresentDetails = mapFromOfflineCardPresentDetails(cardPresentDetails) + } + + var amountDetails: NSDictionary? + if let offlineAmountDetails = offlineDetails.amountDetails { + amountDetails = mapFromAmountDetails(offlineAmountDetails) + } + + let result: NSDictionary = [ + "storedAt": convertDateToUnixTimestamp(date: offlineDetails.collectedAt) ?? NSNull(), + "requiresUpload": offlineDetails.requiresUpload, + "cardPresentDetails": offlineCardPresentDetails ?? NSNull(), + "amountDetails": amountDetails ?? NSNull() + ] + + return result + } + + class func mapFromAmountDetails(_ amountDetails: SCPAmountDetails?) -> NSDictionary { + let amount: NSDictionary = [ + "amount": amountDetails?.tip ?? NSNull(), + ] + + let result: NSDictionary = [ + "tip": amount + ] + + return result + } + + class func mapFromOfflineCardPresentDetails(_ offlineCardPresentDetails: OfflineCardPresentDetails) -> NSDictionary { + var receiptDetailsMap: NSDictionary? + if let receiptDetails = offlineCardPresentDetails.receiptDetails { + receiptDetailsMap = mapFromReceiptDetails(receiptDetails) + } + + let result: NSDictionary = [ + "brand": offlineCardPresentDetails.brand, + "cardholderName": offlineCardPresentDetails.cardholderName ?? NSNull(), + "expMonth": offlineCardPresentDetails.expMonth, + "expYear": offlineCardPresentDetails.expYear, + "last4": offlineCardPresentDetails.last4 ?? NSNull(), + "readMethod": mapFromReadMethod(offlineCardPresentDetails.readMethod), + "receiptDetails": receiptDetailsMap ?? NSNull() + ] + + return result + } + class func mapFromCardPresentDetailsWallet(_ wallet: SCPWallet) -> NSDictionary { let result: NSDictionary = [ "type": wallet.type @@ -600,7 +659,7 @@ class Mappers { return(["sdk": sdkDict, "reader": readerDict]) } - + class func mapFromReaderTextToSpeechStatus(_ status: ReaderTextToSpeechStatus) -> String { switch status { case ReaderTextToSpeechStatus.off: return "off" @@ -609,12 +668,12 @@ class Mappers { default: return "unknown" } } - + class func mapFromReaderSettings(_ readerSettings: ReaderSettings) -> NSDictionary { var accessibility: [String : Any] = [ "textToSpeechStatus": mapFromReaderTextToSpeechStatus(readerSettings.accessibility.textToSpeechStatus), ] - + let errorDic: NSDictionary if let error = readerSettings.accessibility.error as NSError? { errorDic = [ @@ -626,7 +685,7 @@ class Mappers { return(["accessibility": accessibility]) } - + class func mapFromReaderDisconnectReason(_ reason: DisconnectReason) -> String { switch reason { case DisconnectReason.disconnectRequested: return "disconnectRequested" @@ -638,7 +697,18 @@ class Mappers { default: return "unknown" } } - + + class func mapFromReadMethod(_ readMethod: SCPReadMethod) -> String { + switch readMethod { + case SCPReadMethod.contactEMV: return "contactEMV" + case SCPReadMethod.contactlessEMV: return "contactlessEMV" + case SCPReadMethod.contactlessMagstripeMode: return "contactlessMagstripeMode" + case SCPReadMethod.magneticStripeFallback: return "magneticStripeFallback" + case SCPReadMethod.magneticStripeTrack2: return "magneticStripeTrack2" + default: return "unknown" + } + } + class func mapFromCollectInputs(_ results: [CollectInputsResult]) -> NSDictionary { var collectInputResults: [String : Any] = [:] for result in results { @@ -668,7 +738,7 @@ class Mappers { collectInputResults["selectionResult"] = selectionResult } } - + return (["collectInputResults": collectInputResults]) } } diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index d3e63a41..6440393f 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -869,7 +869,7 @@ export function useStripeTerminal(props?: Props) { throw Error(NOT_INITIALIZED_ERROR_MESSAGE); } const response = await getOfflineStatus(); - if (response.reader?.networkStatus) { + if (!response.reader?.networkStatus) { response.reader = undefined; } return response; diff --git a/src/types/PaymentIntent.ts b/src/types/PaymentIntent.ts index f3d807b0..192d2495 100644 --- a/src/types/PaymentIntent.ts +++ b/src/types/PaymentIntent.ts @@ -1,4 +1,4 @@ -import type { Charge, PaymentMethod } from './'; +import type { Charge, OfflineDetails, PaymentMethod } from './'; export namespace PaymentIntent { export interface Type { @@ -11,6 +11,7 @@ export namespace PaymentIntent { sdkUuid: string; paymentMethodId: string; paymentMethod: PaymentMethod.Type; + offlineDetails: OfflineDetails; } export type Status = diff --git a/src/types/Reader.ts b/src/types/Reader.ts index 3e7bb8a3..71bca3ea 100644 --- a/src/types/Reader.ts +++ b/src/types/Reader.ts @@ -90,8 +90,10 @@ export namespace Reader { | 'wisePad3s' | 'wisePadEDevkit' | 'stripeS700Devkit' + | 'stripeS700' | 'cotsDevice' - | 'appleBuiltIn'; + | 'appleBuiltIn' + | 'etna'; export type InputOptions = 'insertCard' | 'swipeCard' | 'tapCard'; diff --git a/src/types/index.ts b/src/types/index.ts index 22188f91..283ad6c7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -281,8 +281,8 @@ export type ReceiptDetails = { authorizationResponseCode: string; cvm: string; dedicatedFileName: string; - tsi: string; - tvr: string; + terminalVerificationResult: string; + transactionStatusInformation: string; }; export type Wallet = { @@ -427,3 +427,28 @@ export enum SelectionButtonStyle { PRIMARY = 'PRIMARY', SECONDARY = 'CanSECONDARYceled', } + +export type OfflineDetails = { + storedAt: string; + requiresUpload: boolean; + cardPresentDetails: OfflineCardPresentDetails; + amountDetails: AmountDetails; +}; + +export type OfflineCardPresentDetails = { + brand: string; + cardholderName: string; + expMonth: number; + expYear: number; + last4: string; + readMethod: string; + receiptDetails: ReceiptDetails; +}; + +export type AmountDetails = { + tip: Amount; +}; + +export type Amount = { + amount: number; +};