diff --git a/example/PaymentScreen.tsx b/example/PaymentScreen.tsx index ab798e4..72a7a8e 100644 --- a/example/PaymentScreen.tsx +++ b/example/PaymentScreen.tsx @@ -34,6 +34,7 @@ const PaymentScreen = () => { createPayment({ sessionId, onComplete: onPaymentComplete, + onDismiss: onPaymentComplete, }); }; @@ -41,6 +42,7 @@ const PaymentScreen = () => { createPayment({ sessionId: '', onComplete: onPaymentComplete, + onDismiss: onPaymentComplete, secretKey: SECRET_KEY, enablePayWithoutSession: true, }); @@ -48,7 +50,7 @@ const PaymentScreen = () => { // when the payment is complete pass a callback to get the final results of response const onPaymentComplete = (response: any) => { - console.log(response); + console.log(`Transaction Status: ${response?.status}`); setAmount(''); }; diff --git a/payment_sdk/src/KomojuProvider.tsx b/payment_sdk/src/KomojuProvider.tsx index 3da3088..9feb032 100644 --- a/payment_sdk/src/KomojuProvider.tsx +++ b/payment_sdk/src/KomojuProvider.tsx @@ -19,7 +19,6 @@ import { TokenResponseStatuses, webViewDataInitialState, } from "./util/types"; -import { PaymentSheetRefProps } from "./components/PaymentSheet"; import { Actions, DispatchContext, KomojuContext } from "./state"; import StateProvider from "./components/paymentState/stateProvider"; @@ -29,9 +28,9 @@ import { validateSessionResponse } from "./util/validator"; import secureTokenService, { checkSecureTokenStatus, } from "./services/secureTokenService"; -import { parameterName } from "./util/constants"; +import { sessionParameterName, tokenParameterName } from "./util/constants"; import paymentService from "./services/paymentService"; -import Sheet from "./components/Sheet"; +import Sheet, { SheetRefProps } from "./components/Sheet"; type KomojuProviderIprops = { children?: ReactNode | ReactNode[]; @@ -53,7 +52,11 @@ export const KomojuProvider = (props: KomojuProviderIprops) => { export const MainStateProvider = (props: KomojuProviderIprops) => { const dispatch = useContext(DispatchContext); - const sheetRef = useRef(null); + const sheetRef = useRef(null); + // ref to hold client provided onDismiss callback + const onDismissCallback = useRef(null); + // ref to hold client provided session Id + const sessionIdRef = useRef(""); // when payment is success global state is rest and invoking the success screen const onPaymentSuccess = () => { @@ -74,6 +77,20 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { payload: ResponseScreenStatuses.FAILED, }); + const onUserCancel = async () => { + if (onDismissCallback.current) { + const sessionShowPayload = { + publicKey: props?.publicKey, + sessionId: sessionIdRef.current, + }; + + // fetch session status to check if the payment is completed + let sessionResponse = await sessionShow(sessionShowPayload); + // invoking client provided onDismiss callback + onDismissCallback.current(sessionResponse); + } + }; + // showing overlay loading indicator disabling all interactions const startLoading = () => dispatch({ @@ -100,7 +117,7 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { // validating the session data and closing the payment gateway if data is not valid if (validateSessionResponse(sessionData)) { - sheetRef?.current?.close(); + sheetRef?.current?.close(false); Alert.alert("Error", "Session expired"); } else { // if session is valid setting amount, currency type at global store for future use @@ -125,8 +142,11 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { const { url } = newNavState; if (!url) return; - // Check if URL includes secure_token_id= - if (url.includes(parameterName)) { + // Check if its credit card method URL includes secure_token_id= + if ( + url.includes(tokenParameterName) && + paymentType === PaymentType.CREDIT + ) { // CLose web view and start loading dispatch({ type: Actions.SET_WEBVIEW_LINK, @@ -159,18 +179,17 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { TokenResponseStatuses.SUCCESS ) { onPaymentSuccess(); + // calling user passed onComplete method with session response data + onComplete && onComplete(sessionResponse); } else { onPaymentFailed(); } - - // calling user passed onComplete method with session response data - onComplete && onComplete(sessionResponse); } else { //This is for manual 3D secure token handling flow // getting secure_token_id parameter from the web view URL const token = url.substring( - url.indexOf(parameterName) + parameterName.length + url.indexOf(tokenParameterName) + tokenParameterName.length ); // checking token status if the 3D secure has passed @@ -207,6 +226,34 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { // after all api calls are done stopping the loading indicator stopLoading(); + + // if paypay payment method web view redirection flow + } else if ( + url.includes(sessionParameterName) && + paymentType === PaymentType.PAY_PAY + ) { + // if this is a session flow, check until session response changes from 'pending' to 'completed' or 'error' + const sessionShowPayload = { + publicKey: props.publicKey, + sessionId: sessionId, + }; + + // 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 = await sessionShow(sessionShowPayload); + } + + // if payment success showing success screen or if failed showing error screen + if (sessionResponse?.status === PaymentStatuses.SUCCESS) { + onPaymentSuccess(); + // calling user passed onComplete method with session response data + onComplete && onComplete(sessionResponse); + } else { + onPaymentFailed(); + } } }; @@ -271,6 +318,7 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { ({ sessionId, onComplete, + onDismiss, secretKey, enablePayWithoutSession, }: CreatePaymentFuncType) => { @@ -279,6 +327,11 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { payload: initialState, }); + // setting client provided onDismiss callback into a ref + onDismissCallback.current = onDismiss; + // setting client provided session Id and into a ref + sessionIdRef.current = sessionId; + if (!enablePayWithoutSession) validateSession(sessionId); dispatch({ @@ -302,7 +355,7 @@ export const MainStateProvider = (props: KomojuProviderIprops) => { const initializeKomoju = useCallback((params: InitPrams) => {}, []); const renderPaymentUI = useMemo(() => { - const UI = ; + const UI = ; return UI; }, [sheetRef]); diff --git a/payment_sdk/src/components/PaymentSheet.tsx b/payment_sdk/src/components/PaymentSheet.tsx deleted file mode 100644 index e577f4f..0000000 --- a/payment_sdk/src/components/PaymentSheet.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useRef, forwardRef, useImperativeHandle } from "react"; -import { StyleSheet } from "react-native"; - -import Sheet, { SheetRefProps } from "./Sheet"; -import SheetContent from "./SheetContent"; - -export type PaymentSheetRefProps = { - open: () => void; -}; - -const PaymentSheet = forwardRef((props, ref) => { - const sheetRef = useRef(null); - - useImperativeHandle( - ref, - () => ({ - open: () => { - sheetRef.current?.open(); - }, - close: () => { - sheetRef.current?.close(); - }, - }), - [sheetRef] - ); - - return ( - - - - ); -}); - -export default PaymentSheet; diff --git a/payment_sdk/src/components/Sheet.tsx b/payment_sdk/src/components/Sheet.tsx index 953909c..1125adf 100644 --- a/payment_sdk/src/components/Sheet.tsx +++ b/payment_sdk/src/components/Sheet.tsx @@ -1,13 +1,13 @@ import React, { useCallback, useImperativeHandle, - forwardRef, useContext, useRef, useEffect, ForwardRefRenderFunction, } from "react"; import { + Alert, Dimensions, Image, StyleSheet, @@ -32,6 +32,7 @@ const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50; type SheetProps = { children?: React.ReactNode; swipeClose?: boolean; + onDismiss?: () => void; }; export type SheetRefProps = { @@ -41,7 +42,7 @@ export type SheetRefProps = { }; const Sheet: ForwardRefRenderFunction = ( - { children, swipeClose }, + { swipeClose, onDismiss }, ref ) => { const translateY = useRef(new RNAnimated.Value(0)).current; @@ -86,15 +87,38 @@ const Sheet: ForwardRefRenderFunction = ( return activeState.current; }, []); + const closeSheet = (showAlert = true) => { + if (showAlert) { + // showing an alert when user try to close the SDK modal + Alert.alert("Cancel Payment?", "", [ + { + text: "No", + onPress: () => scrollTo(MAX_TRANSLATE_Y + 50), + style: "cancel", + }, + { + text: "Yes", + onPress: () => { + // invoking client provided onDismiss() callback when closing the SDK modal + onDismiss && onDismiss(); + scrollTo(0); + }, + }, + ]); + } else { + // invoking client provided callback when closing the SDK modal + onDismiss && onDismiss(); + scrollTo(0); + } + }; + useImperativeHandle( ref, () => ({ open: () => { scrollTo(MAX_TRANSLATE_Y + 50); }, - close: () => { - scrollTo(0); - }, + close: closeSheet, scrollTo, isActive, }), @@ -113,7 +137,7 @@ const Sheet: ForwardRefRenderFunction = ( }, onPanResponderRelease: () => { if (translateYState.current > -SCREEN_HEIGHT / 1.5) { - scrollTo(0); + closeSheet(false); } else if (translateYState.current < -SCREEN_HEIGHT / 1.5) { scrollTo(MAX_TRANSLATE_Y + 50); } @@ -135,7 +159,7 @@ const Sheet: ForwardRefRenderFunction = ( const ctaOnPress = () => { switch (paymentState) { case ResponseScreenStatuses.SUCCESS: - return scrollTo(0); + return closeSheet(false); case ResponseScreenStatuses.FAILED: return dispatch({ type: Actions.SET_PAYMENT_STATE, @@ -150,7 +174,7 @@ const Sheet: ForwardRefRenderFunction = ( <> { - if (swipeClose) scrollTo(0); + if (swipeClose) closeSheet(false); }} pointerEvents="none" style={[styles.backDrop, { opacity: active }]} @@ -167,9 +191,11 @@ const Sheet: ForwardRefRenderFunction = ( Payment Options scrollTo(0)} + onPress={() => + closeSheet(paymentState !== ResponseScreenStatuses.SUCCESS) + } > - + diff --git a/payment_sdk/src/util/constants.ts b/payment_sdk/src/util/constants.ts index e9bf1b5..950c86f 100644 --- a/payment_sdk/src/util/constants.ts +++ b/payment_sdk/src/util/constants.ts @@ -1,7 +1,10 @@ export const noop = () => {}; export const BASE_URL = "https://komoju.com/api/v1"; -export const parameterName = "secure_token_id="; +// redirect url parameter to identify if token validation is completed +export const tokenParameterName = "secure_token_id="; +// redirect url parameter to identify if session payment is completed +export const sessionParameterName = "sessions"; export const paymentSuccessCtaText = "Back to store"; export const paymentFailedCtaText = "Update payment method"; diff --git a/payment_sdk/src/util/types.ts b/payment_sdk/src/util/types.ts index 2071f46..c04706e 100644 --- a/payment_sdk/src/util/types.ts +++ b/payment_sdk/src/util/types.ts @@ -7,6 +7,7 @@ export type CreatePaymentFuncType = { sessionId: string; secretKey?: string; onComplete?: (response: any) => void; // callback when transaction is complete status is passed as a prop + onDismiss?: (response: any) => void; // callback when user closes the SDK modal enablePayWithoutSession?: boolean; };