diff --git a/payment_sdk/src/assets/languages/en.json b/payment_sdk/src/assets/languages/en.json index 38c850e..08c0eec 100644 --- a/payment_sdk/src/assets/languages/en.json +++ b/payment_sdk/src/assets/languages/en.json @@ -45,6 +45,8 @@ "PAYMENT_CANCELLED_MSG": "We noticed that you’ve canceled the payment process. If this was a mistake, you can try again to complete your purchase.", "BACK_TO_STORE": "Back to store", "UPDATE_PAYMENT_METHOD": "Update payment method", + "SESSION_EXPIRED": "Session Expired", + "SESSION_EXPIRED_MSG": "This session has already expired. Please try again.", "PAYMENT_VIA_ALI_PAY": "Payment via Alipay", "ALI_PAY_REDIRECT_MESSAGE": "You will be redirected to Alipay to complete the payment", diff --git a/payment_sdk/src/assets/languages/ja.json b/payment_sdk/src/assets/languages/ja.json index 3e13d62..98c0a41 100644 --- a/payment_sdk/src/assets/languages/ja.json +++ b/payment_sdk/src/assets/languages/ja.json @@ -44,7 +44,9 @@ "PAYMENT_RE_TRY_MSG": "入力していただいたカードで支払いを行うことができませんでした。別の支払い方法を試してみてください。", "PAYMENT_CANCELLED_MSG": "お支払いプロセスがキャンセルされたことを確認しました。もしこれが誤りであれば、再度お試しいただき、購入を完了してください。", "BACK_TO_STORE": "ストアに戻る", - "UPDATE_PAYMENT_METHOD": "支払い方法を変更する", + "UPDATE_PAYMENT_METHOD": "支払い方法を変更する", + "SESSION_EXPIRED": "セッションが期限切れ", + "SESSION_EXPIRED_MSG": "このセッションは既に期限切れです。再度お試しください。", "PAYMENT_VIA_ALI_PAY": "Alipayによる支払い", "ALI_PAY_REDIRECT_MESSAGE": "支払いを完了するには、Alipay にリダイレクトされます。", diff --git a/payment_sdk/src/components/PaymentModal.tsx b/payment_sdk/src/components/PaymentModal.tsx index b30995c..befbf28 100644 --- a/payment_sdk/src/components/PaymentModal.tsx +++ b/payment_sdk/src/components/PaymentModal.tsx @@ -81,6 +81,7 @@ const PaymentModal = ({ case ResponseScreenStatuses.SUCCESS: case ResponseScreenStatuses.COMPLETE: case ResponseScreenStatuses.CANCELLED: + case ResponseScreenStatuses.EXPIRED: return paymentSuccessCtaText; case ResponseScreenStatuses.FAILED: return paymentFailedCtaText; @@ -95,6 +96,8 @@ const PaymentModal = ({ case ResponseScreenStatuses.COMPLETE: case ResponseScreenStatuses.CANCELLED: return closeSheet(false); + case ResponseScreenStatuses.EXPIRED: + return closeSheet(false); case ResponseScreenStatuses.FAILED: return dispatch({ type: Actions.SET_PAYMENT_STATE, @@ -110,7 +113,8 @@ const PaymentModal = ({ !( paymentState === ResponseScreenStatuses.SUCCESS || paymentState === ResponseScreenStatuses.CANCELLED || - paymentState === ResponseScreenStatuses.COMPLETE + paymentState === ResponseScreenStatuses.COMPLETE || + paymentState === ResponseScreenStatuses.EXPIRED ) ); }; diff --git a/payment_sdk/src/components/ResponseScreen.tsx b/payment_sdk/src/components/ResponseScreen.tsx index f9a8858..6b1cddb 100644 --- a/payment_sdk/src/components/ResponseScreen.tsx +++ b/payment_sdk/src/components/ResponseScreen.tsx @@ -38,6 +38,11 @@ const statusConfigs: Partial> = { defaultMessage: "PAYMENT_CANCELLED_MSG", image: require("../assets/images/awaitingPayment.png"), }, + [ResponseScreenStatuses.EXPIRED]: { + title: "SESSION_EXPIRED", + defaultMessage: "SESSION_EXPIRED_MSG", + image: require("../assets/images/error.png"), + }, }; type Props = { @@ -111,6 +116,7 @@ const getStyles = (theme: ThemeSchemeType) => { marginBottom: responsiveScale(16), textAlign: "center", paddingHorizontal: responsiveScale(32), + color: theme.TEXT_COLOR, }, bottomButton: { position: "absolute", diff --git a/payment_sdk/src/components/SubmitButton.tsx b/payment_sdk/src/components/SubmitButton.tsx index 7b73bb5..2ce949b 100644 --- a/payment_sdk/src/components/SubmitButton.tsx +++ b/payment_sdk/src/components/SubmitButton.tsx @@ -1,9 +1,11 @@ -import React from "react"; +import React, { useContext } from "react"; import { StyleSheet, Text, TouchableOpacity } from "react-native"; import { useTranslation } from "react-i18next"; +import { Actions, DispatchContext } from "@context/state"; + import { ThemeSchemeType } from "@util/types"; import { resizeFonts, responsiveScale } from "@theme/scalling"; @@ -20,11 +22,18 @@ const SubmitButton = ({ label, labelSuffix, onPress, testID }: Props) => { const { t } = useTranslation(); const theme = useCurrentTheme(); const styles = getStyles(theme); + const dispatch = useContext(DispatchContext); + + const onSubmit = () => { + dispatch({ type: Actions.SET_PROCEED_PAYMENT, payload: true }); + onPress(); + } + return ( {labelSuffix ? `${t(label)} ${labelSuffix}` : t(label)} diff --git a/payment_sdk/src/context/MainStateProvider.tsx b/payment_sdk/src/context/MainStateProvider.tsx index f7b6dac..480f28f 100644 --- a/payment_sdk/src/context/MainStateProvider.tsx +++ b/payment_sdk/src/context/MainStateProvider.tsx @@ -7,7 +7,7 @@ import React, { useState, } from "react"; -import { Alert, Linking } from "react-native"; +import { Alert, AppState, Linking } from "react-native"; import i18next from "i18next"; @@ -30,11 +30,12 @@ import { import { validateSessionResponse } from "@util/validator"; import "@assets/languages/i18n"; -import { Actions, DispatchContext, KomojuContext } from "./state"; +import { Actions, DispatchContext, KomojuContext, StateContext } from "./state"; export const MainStateProvider = (props: KomojuProviderIprops) => { const dispatch = useContext(DispatchContext); const [modalVisible, setModalVisible] = useState(false); + const { processedPayment } = useContext(StateContext) const sheetRef = useRef(null); // ref to hold client provided onComplete callback @@ -56,6 +57,65 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { }; }, [props]); + useEffect(() => { + // Add event listener for deep links + const subscription = AppState.addEventListener( + "change", + handleBackgroundStateChange + ); + + return () => { + subscription.remove(); + }; + }, [processedPayment]); + + // This callback method is used to check the background state + const handleBackgroundStateChange = async () => { + startLoading(); + + if (processedPayment) { + // if this is a session flow, check until session response changes from 'pending' to 'completed' or 'error' + const sessionShowPayload = { + publishableKey: props.publishableKey, + sessionId: sessionIdRef.current, + }; + + // fetch session status to check if the payment is completed + let sessionResponse = await sessionShow(sessionShowPayload); + + // Polling until session verification status changes + while ( + sessionResponse?.status === PaymentStatuses.PENDING && + sessionResponse?.payment?.status !== PaymentStatuses.CANCELLED && + !sessionResponse?.expired + ) { + sessionResponse = await sessionShow(sessionShowPayload); + } + + // if payment success showing success screen or if failed showing error screen + if (sessionResponse?.status === PaymentStatuses.SUCCESS) { + if (sessionResponse?.payment?.payment_details?.instructions_url) { + openURL(sessionResponse?.payment?.payment_details?.instructions_url); + } + onPaymentSuccess(); + // calling user passed onComplete method with session response data + onCompleteCallback.current && + // TODO: Fix this type error + // @ts-expect-error - Argument of type 'PaymentSessionResponse' is not assignable to parameter of type 'string'. + onCompleteCallback.current(sessionResponse); + } else if (sessionResponse?.payment?.status === PaymentStatuses.CANCELLED) { + onPaymentCancelled(); + } else if (sessionResponse?.expired) { + onSessionExpired() + } else { + onPaymentFailed(); + } + } + dispatch({ type: Actions.SET_PROCEED_PAYMENT, payload: false }); + // after all api calls are done stopping the loading indicator + stopLoading(); + }; + const openPaymentSheet = () => { if (props?.useBottomSheet) { sheetRef?.current?.open(); @@ -111,6 +171,13 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { }); }; + // when payment is failed invoking the error screen + const onSessionExpired = () => + dispatch({ + type: Actions.SET_PAYMENT_STATE, + payload: ResponseScreenStatuses.EXPIRED, + }); + const onUserCancel = async () => { if (onDismissCallback.current) { const sessionShowPayload = { @@ -233,6 +300,7 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { onPaymentFailed(); } + dispatch({ type: Actions.SET_PROCEED_PAYMENT, payload: false }); // after all api calls are done stopping the loading indicator stopLoading(); }; @@ -302,7 +370,7 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { // TODO: Fix this type error // eslint-disable-next-line @typescript-eslint/no-unused-vars - const initializeKomoju = useCallback((params: InitPrams) => {}, []); + const initializeKomoju = useCallback((params: InitPrams) => { }, []); const renderPaymentUI = useMemo(() => { const UI = props?.useBottomSheet ? ( diff --git a/payment_sdk/src/context/state.ts b/payment_sdk/src/context/state.ts index aea5ef8..6e78765 100644 --- a/payment_sdk/src/context/state.ts +++ b/payment_sdk/src/context/state.ts @@ -32,6 +32,7 @@ export const Actions = { SET_PAYMENT_STATE: "SET_PAYMENT_STATE", SET_PAYMENT_METHODS: "SET_PAYMENT_METHODS", SESSION_PAY: "SESSION_PAY", + SET_PROCEED_PAYMENT: "SET_PROCEED_PAYMENT", }; /** @@ -123,6 +124,11 @@ export function reducer(state: State, action: ActionType) { ...state, paymentMethods: action.payload, }; + case Actions.SET_PROCEED_PAYMENT: + return { + ...state, + processedPayment: action.payload, + }; default: throw new Error(); } diff --git a/payment_sdk/src/util/types.ts b/payment_sdk/src/util/types.ts index 0d454c3..88c3ea9 100644 --- a/payment_sdk/src/util/types.ts +++ b/payment_sdk/src/util/types.ts @@ -75,6 +75,7 @@ export enum PaymentStatuses { SUCCESS = "completed", PENDING = "pending", CANCELLED = "cancelled", + EXPIRED = "expired", } export enum TokenResponseStatuses { @@ -93,6 +94,8 @@ export enum ResponseScreenStatuses { COMPLETE = "complete", /** For displaying payment instruction screens for cancelled by the user */ CANCELLED = "cancelled", + /** For displaying payment instruction screens for expired user session */ + EXPIRED = "expired", } export enum CurrencySign { @@ -219,6 +222,10 @@ export type State = CardDetailsType & * Global loading state. to display loading animation over sdk and disable buttons. */ loading: boolean; + /** + * Global processedPayment state. to indicate whether the payment was processed + */ + processedPayment: boolean; /** * Callback function to call relevant api for each payment type. */ @@ -246,6 +253,7 @@ export type State = CardDetailsType & | ResponseScreenStatuses.COMPLETE | ResponseScreenStatuses.FAILED | ResponseScreenStatuses.CANCELLED + | ResponseScreenStatuses.EXPIRED | ""; /** * States of the Bank transfer and Pay Easy fields. @@ -262,6 +270,7 @@ export type sessionPayProps = { export const initialState: State = { paymentType: PaymentType.CREDIT, loading: false, + processedPayment: false, /** credit card payment related states start */ cardholderName: "",