Skip to content

Commit

Permalink
sPOS offline mode support (#649)
Browse files Browse the repository at this point in the history
* wip spos offline test

* offline mode limit for spos

* update database view

* fix error message

* fix reader network status reporting

* devapp improvements

* mapper

* add iOS and typescript codes.

* prettier codes.

* update amount details

* fix type

* change up date

* misc fixes

* add reader types

* p400 backend all else client

* offline transaction limits check

* order device type

* putintornull

* pass device type 🤦

* remove log

---------

Co-authored-by: Ian Lin <ian.lin@bbpos.com>
  • Loading branch information
nazli-stripe and ianlin-bbpos authored Mar 29, 2024
1 parent b9666d3 commit 1bc0c81
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 34 deletions.
55 changes: 49 additions & 6 deletions android/src/main/java/com/stripeterminalreactnative/Mappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"

}
}

Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions dev-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export type RouteParamList = {
CollectCardPayment: {
simulated: boolean;
discoveryMethod: Reader.DiscoveryMethod;
deviceType: Reader.DeviceType;
};
RefundPayment: {
simulated: boolean;
Expand Down
24 changes: 19 additions & 5 deletions dev-app/src/screens/CollectCardPaymentScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default function CollectCardPaymentScreen() {
const [tipEligibleAmount, setTipEligibleAmount] = useState('');
const { params } =
useRoute<RouteProp<RouteParamList, 'CollectCardPayment'>>();
const { simulated, discoveryMethod } = params;
const { simulated, discoveryMethod, deviceType } = params;
const { addLogs, clearLogs, setCancel } = useContext(LogContext);
const navigation = useNavigation();

Expand Down Expand Up @@ -165,7 +165,8 @@ export default function CollectCardPaymentScreen() {
};
let paymentIntent: PaymentIntent.Type | undefined;
let paymentIntentError: StripeError<CommonError> | undefined;
if (discoveryMethod === 'internet') {

if (deviceType === 'verifoneP400') {
const resp = await api.createPaymentIntent({
amount: Number(inputValues.amount),
currency: inputValues.currency,
Expand Down Expand Up @@ -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,
Expand Down
40 changes: 38 additions & 2 deletions dev-app/src/screens/DatabaseScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,43 @@ export default function DatabaseScreen() {

return (
<ScrollView style={styles.container}>
<List bolded={false} topSpacing={false} title="PUBLIC INTERFACE SUMMARY">
<List bolded={false} topSpacing={false} title="READER SUMMARY">
{offlinePaymentStatus &&
offlinePaymentStatus.reader &&
offlinePaymentStatus.reader.offlinePaymentsCount > 0 ? (
Object.keys(
offlinePaymentStatus.reader.offlinePaymentAmountsByCurrency
).map((key) => (
<ListItem
title={
getCurrencySymbols(key) +
' ' +
(
Number(
offlinePaymentStatus.reader!
.offlinePaymentAmountsByCurrency[key]
) / 100
).toFixed(2)
}
/>
))
) : (
<></>
)}
</List>
<Text style={styles.infoText}>
{' '}
{String(
offlinePaymentStatus &&
offlinePaymentStatus.reader &&
offlinePaymentStatus.reader.offlinePaymentsCount
? offlinePaymentStatus.reader.offlinePaymentsCount
: 0
) +
' payment intent(s) for ' +
account?.settings?.dashboard.display_name}{' '}
</Text>
<List bolded={false} topSpacing={false} title="SDK SUMMARY">
{offlinePaymentStatus &&
offlinePaymentStatus.sdk.offlinePaymentsCount > 0 ? (
Object.keys(
Expand All @@ -70,7 +106,7 @@ export default function DatabaseScreen() {
<Text style={styles.infoText}>
{' '}
{String(
offlinePaymentStatus
offlinePaymentStatus && offlinePaymentStatus.sdk
? offlinePaymentStatus.sdk.offlinePaymentsCount
: 0
) +
Expand Down
20 changes: 12 additions & 8 deletions dev-app/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -91,6 +93,7 @@ export default function HomeScreen() {
? '🔋' + batteryPercentage.toFixed(0) + '%'
: '';
const chargingStatus = connectedReader?.isCharging ? '🔌' : '';
const deviceType = connectedReader?.deviceType;

useEffect(() => {
const loadDiscSettings = async () => {
Expand Down Expand Up @@ -140,6 +143,7 @@ export default function HomeScreen() {
navigation.navigate('CollectCardPaymentScreen', {
simulated,
discoveryMethod,
deviceType,
});
}}
/>
Expand Down Expand Up @@ -213,7 +217,7 @@ export default function HomeScreen() {
<Image source={icon} style={styles.image} />
</View>

<Text style={styles.readerName}>{connectedReader.deviceType}</Text>
<Text style={styles.readerName}>{deviceType}</Text>
<Text style={styles.connectionStatus}>
Connected{simulated && <Text>, simulated</Text>}
</Text>
Expand Down
Loading

0 comments on commit 1bc0c81

Please sign in to comment.