diff --git a/src/assets/languages/en.json b/src/assets/languages/en.json index 73057b7..f46ee07 100644 --- a/src/assets/languages/en.json +++ b/src/assets/languages/en.json @@ -27,6 +27,7 @@ "CARD_NUMBER": "Card Number", "SCAN_CARD": "Scan Card", "PAY": "Pay", + "SAVE": "Save", "NAME_SHOWN_ON_RECEIPT": "Name (shown on receipt)", "FULL_NAME_ON_RECEIPT": "Full name on receipt", "EMAIL": "Email", diff --git a/src/assets/languages/ja.json b/src/assets/languages/ja.json index c052714..ab2fc53 100644 --- a/src/assets/languages/ja.json +++ b/src/assets/languages/ja.json @@ -27,6 +27,7 @@ "CARD_NUMBER": "カード番号", "SCAN_CARD": "カードをスキャン", "PAY": "支払い", + "SAVE": "保存", "NAME_SHOWN_ON_RECEIPT": "氏名", "FULL_NAME_ON_RECEIPT": "氏名を入力(レシートで表示されます)", "EMAIL": "メールアドレス", diff --git a/src/components/CardInputGroup.tsx b/src/components/CardInputGroup.tsx index 5db86c5..dcdaec7 100644 --- a/src/components/CardInputGroup.tsx +++ b/src/components/CardInputGroup.tsx @@ -1,10 +1,4 @@ -import { - memo, - useContext, - useEffect, - useState, - useCallback, -} from "react"; +import { memo, useContext, useEffect, useState, useCallback } from "react"; import { StyleSheet, View, Image, Dimensions } from "react-native"; @@ -18,6 +12,7 @@ import { import { CardTypes, PaymentType, + sessionDataType, sessionShowPaymentMethodType, ThemeSchemeType, } from "../util/types"; @@ -50,8 +45,9 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { // const [toggleScanCard, setToggleScanCard] = useState(false); const theme = useCurrentTheme(); const styles = getStyles(theme); - const { cardCVV, cardNumber, cardExpiredDate, paymentMethods } = + const { cardCVV, cardNumber, cardExpiredDate, sessionData } = useContext(StateContext); + const SessionData = sessionData as sessionDataType; useEffect(() => { // Determine card type and set it on first render if cardNumber is not empty @@ -83,7 +79,7 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { // Create card image list const cardImage = useCallback(() => { // Select credit card payment method data from session response payment methods - const cardPaymentMethodData = paymentMethods?.find( + const cardPaymentMethodData = SessionData.paymentMethods?.find( (method: sessionShowPaymentMethodType) => method?.type === PaymentType.CREDIT ); @@ -116,7 +112,7 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { } else { return; } - }, [cardType, paymentMethods]); + }, [cardType, SessionData.paymentMethods]); // const onCardScanned = useCallback( // (cardDetails: { cardNumber?: string; expirationDate?: string }) => { diff --git a/src/components/PillContainer.tsx b/src/components/PillContainer.tsx index 54b0bd3..678bb53 100644 --- a/src/components/PillContainer.tsx +++ b/src/components/PillContainer.tsx @@ -4,7 +4,11 @@ import { StyleSheet, View, Image, FlatList } from "react-native"; import { StateContext } from "../context/state"; -import { PaymentType, sessionShowPaymentMethodType } from "../util/types"; +import { + PaymentType, + sessionDataType, + sessionShowPaymentMethodType, +} from "../util/types"; import PaymentMethodImages from "../assets/images/paymentMethodImages"; @@ -24,7 +28,9 @@ const squareSizeImages = [ ]; const PillContainer = ({ onSelect, selectedItem }: Props) => { - const { paymentMethods } = useContext(StateContext); + const { sessionData } = useContext(StateContext) as { + sessionData: sessionDataType; + }; const getIcon = (slug: PaymentType) => { return ( @@ -53,7 +59,7 @@ const PillContainer = ({ onSelect, selectedItem }: Props) => { return ( { const [inputErrors, setInputErrors] = useState(initialErrors); const { threeDSecurePayment } = useThreeDSecureHandler(); - const { - cardholderName, - cardCVV, - cardNumber, - cardExpiredDate, - amount, - currency, - } = useContext(StateContext); + const { sessionPay } = useSessionPayHandler(); + const { cardholderName, cardCVV, cardNumber, cardExpiredDate, sessionData } = + useContext(StateContext); const dispatch = useContext(DispatchContext); + const SessionData = sessionData as sessionDataType; const resetError = (type: string) => { // TODO: Fix this type error @@ -58,12 +56,24 @@ const CardSection = (): JSX.Element => { }); if (isValid) { - threeDSecurePayment({ - cardholderName, - cardCVV, - cardNumber, - cardExpiredDate, - }); + if (SessionData.mode === PaymentMode.Customer) { + sessionPay({ + paymentType: PaymentType.CREDIT, + paymentDetails: { + cardholderName, + cardCVV, + cardNumber, + cardExpiredDate, + }, + }); + } else { + threeDSecurePayment({ + cardholderName, + cardCVV, + cardNumber, + cardExpiredDate, + }); + } } }; @@ -87,8 +97,15 @@ const CardSection = (): JSX.Element => { diff --git a/src/components/sections/KonbiniSection.tsx b/src/components/sections/KonbiniSection.tsx index b39b8d8..1b8076b 100644 --- a/src/components/sections/KonbiniSection.tsx +++ b/src/components/sections/KonbiniSection.tsx @@ -9,6 +9,7 @@ import { brandType, KonbiniType, PaymentType, + sessionDataType, sessionShowPaymentMethodType, } from "../../util/types"; import { validateKonbiniFormFields } from "../../util/validator"; @@ -31,11 +32,11 @@ const KonbiniSection = (): JSX.Element => { const [inputErrors, setInputErrors] = useState(initialErrors); const { sessionPay } = useSessionPayHandler(); - const { name, email, amount, currency, paymentMethods, selectedStore } = - useContext(StateContext); + const { name, email, sessionData, selectedStore } = useContext(StateContext); const dispatch = useContext(DispatchContext); + const SessionData = sessionData as sessionDataType; - const konbiniPaymentMethodData = paymentMethods?.find( + const konbiniPaymentMethodData = SessionData.paymentMethods?.find( (method: sessionShowPaymentMethodType) => method?.type === PaymentType.KONBINI ); @@ -134,7 +135,10 @@ const KonbiniSection = (): JSX.Element => { diff --git a/src/components/sections/SingleInputFormSection.tsx b/src/components/sections/SingleInputFormSection.tsx index c4c6898..8ee5567 100644 --- a/src/components/sections/SingleInputFormSection.tsx +++ b/src/components/sections/SingleInputFormSection.tsx @@ -8,7 +8,7 @@ import Input from "../../components/Input"; import { LangKeys } from "../../util/constants"; import { formatCurrency } from "../../util/helpers"; -import { PaymentType } from "../../util/types"; +import { PaymentType, sessionDataType } from "../../util/types"; import { responsiveScale } from "../../theme/scalling"; @@ -22,7 +22,9 @@ type SingleInputFormSectionProps = { const SingleInputFormSection = ({ type }: SingleInputFormSectionProps) => { const [inputText, setInputText] = useState(""); const [inputError, setInputError] = useState(false); - const { amount, currency } = useContext(StateContext); + const { sessionData } = useContext(StateContext) as { + sessionData: sessionDataType; + }; const { sessionPay } = useSessionPayHandler(); const onPay = () => { @@ -58,7 +60,10 @@ const SingleInputFormSection = ({ type }: SingleInputFormSectionProps) => { diff --git a/src/components/sections/TransferFormSection.tsx b/src/components/sections/TransferFormSection.tsx index 1726b57..6925406 100644 --- a/src/components/sections/TransferFormSection.tsx +++ b/src/components/sections/TransferFormSection.tsx @@ -7,7 +7,7 @@ import { Actions, DispatchContext, StateContext } from "../../context/state"; import Input from "../../components/Input"; import { formatCurrency } from "../../util/helpers"; -import { PaymentType } from "../../util/types"; +import { PaymentType, sessionDataType } from "../../util/types"; import { validateTransferFormFields } from "../../util/validator"; import { responsiveScale } from "../../theme/scalling"; @@ -33,8 +33,9 @@ const TransferFormSection = ({ type }: TransferFormSectionProps) => { const [inputErrors, setInputErrors] = useState(initialErrors); - const { amount, currency, transferFormFields } = useContext(StateContext); + const { sessionData, transferFormFields } = useContext(StateContext); const { sessionPay } = useSessionPayHandler(); + const SessionData = sessionData as sessionDataType; const onPay = () => { const isValid = validateTransferFormFields({ @@ -148,7 +149,10 @@ const TransferFormSection = ({ type }: TransferFormSectionProps) => { diff --git a/src/hooks/useBackgroundHandler.tsx b/src/hooks/useBackgroundHandler.tsx index 90717a8..53cc725 100644 --- a/src/hooks/useBackgroundHandler.tsx +++ b/src/hooks/useBackgroundHandler.tsx @@ -1,6 +1,7 @@ import { Dispatch, SetStateAction, useContext, useEffect } from "react"; import { AppState, AppStateStatus } from "react-native"; import { + CreatePaymentFuncType, PaymentStatuses, PaymentType, TokenResponseStatuses, @@ -18,6 +19,8 @@ const useBackgroundHandler = ( const { paymentType, tokenId, providerPropsData, sessionData } = useContext(StateContext); + const SessionData = sessionData as CreatePaymentFuncType; + const { startLoading, stopLoading, @@ -38,13 +41,13 @@ const useBackgroundHandler = ( return () => { windowChangeListener.remove(); }; - }, [providerPropsData, paymentType, tokenId, sessionData, isDeepLinkOpened]); + }, [providerPropsData, paymentType, tokenId, SessionData, isDeepLinkOpened]); const handleSessionPaymentResponse = async () => { // if this is a session flow, check until session response changes from 'pending' to 'completed' or 'error' const sessionShowPayload = { publishableKey: providerPropsData.publishableKey, - sessionId: sessionData.sessionId, + sessionId: SessionData.sessionId, }; // fetch session status to check if the payment is completed @@ -57,7 +60,7 @@ const useBackgroundHandler = ( } else { onPaymentAwaiting(); } // calling user passed onComplete method with session response data - sessionData.onComplete && sessionData.onComplete(sessionResponse); + SessionData.onComplete && SessionData.onComplete(sessionResponse); } else if (sessionResponse?.payment?.status === PaymentStatuses.CANCELLED) { onPaymentCancelled(); } else if (sessionResponse?.expired) { @@ -84,7 +87,7 @@ const useBackgroundHandler = ( ) { const paymentResponse = await payForSession({ paymentType: PaymentType.CREDIT, - sessionId: sessionData.sessionId, + sessionId: SessionData.sessionId, publishableKey: providerPropsData.publishableKey, paymentDetails: { tokenId: tokenId }, }); diff --git a/src/hooks/useDeepLinkHandler.tsx b/src/hooks/useDeepLinkHandler.tsx index 72fa209..ca199f3 100644 --- a/src/hooks/useDeepLinkHandler.tsx +++ b/src/hooks/useDeepLinkHandler.tsx @@ -1,6 +1,7 @@ import { Linking } from "react-native"; import { Dispatch, SetStateAction, useContext, useEffect } from "react"; import { + CreatePaymentFuncType, PaymentStatuses, PaymentType, TokenResponseStatuses, @@ -12,9 +13,12 @@ import { extractParameterFromUrl } from "../util/helpers"; import payForSession from "../services/payForSessionService"; import useMainStateUtils from "./useMainStateUtils"; -const useDeepLinkHandler = (setIsDeepLinkOpened: Dispatch>) => { +const useDeepLinkHandler = ( + setIsDeepLinkOpened: Dispatch> +) => { const { paymentType, providerPropsData, sessionData } = useContext(StateContext); + const SessionData = sessionData as CreatePaymentFuncType; const { startLoading, @@ -43,7 +47,7 @@ const useDeepLinkHandler = (setIsDeepLinkOpened: Dispatch { const dispatch = useContext(DispatchContext); const { providerPropsData, sessionData } = useContext(StateContext); + const SessionData = sessionData as CreatePaymentFuncType; const resetGlobalStates = () => dispatch({ @@ -56,10 +61,10 @@ const useMainStateUtils = () => { }); const onUserCancel = async () => { - if (sessionData?.onDismiss) { + if (SessionData?.onDismiss) { const sessionShowPayload = { publishableKey: providerPropsData?.publishableKey, - sessionId: sessionData.sessionId, + sessionId: SessionData.sessionId, }; // fetch session status to check if the payment is completed @@ -67,7 +72,7 @@ const useMainStateUtils = () => { // invoking client provided onDismiss callback // TODO: Fix this type error // @ts-expect-error - Argument of type 'PaymentSessionResponse' is not assignable to parameter of type 'string'. - sessionData?.onDismiss(sessionResponse); + SessionData?.onDismiss(sessionResponse); } }; diff --git a/src/hooks/useSessionPayHandler.tsx b/src/hooks/useSessionPayHandler.tsx index ceb952f..d2665ef 100644 --- a/src/hooks/useSessionPayHandler.tsx +++ b/src/hooks/useSessionPayHandler.tsx @@ -1,6 +1,8 @@ import { useContext } from "react"; import { + CreatePaymentFuncType, + PaymentMode, PaymentStatuses, sessionPayProps, TokenResponseStatuses, @@ -12,6 +14,7 @@ import useMainStateUtils from "./useMainStateUtils"; const useSessionPayHandler = () => { const { sessionData, providerPropsData } = useContext(StateContext); + const SessionData = sessionData as CreatePaymentFuncType; const { startLoading, @@ -32,7 +35,7 @@ const useSessionPayHandler = () => { // initiate payment for the session ID with payment details const response = await payForSession({ paymentType, - sessionId: sessionData.sessionId, + sessionId: SessionData.sessionId, publishableKey: providerPropsData.publishableKey, paymentDetails, }); @@ -42,7 +45,10 @@ const useSessionPayHandler = () => { if (response?.status === PaymentStatuses.PENDING) { openURL(response.redirect_url); } else if (response?.status === PaymentStatuses.SUCCESS) { - if (response?.payment?.status === TokenResponseStatuses.CAPTURED) { + if ( + response?.payment?.status === TokenResponseStatuses.CAPTURED || + response?.customer?.resource === PaymentMode.Customer + ) { onPaymentSuccess(); } else if (response?.payment?.payment_details?.instructions_url) { openURL(response?.payment?.payment_details?.instructions_url); diff --git a/src/hooks/useThreeDSecureHandler.tsx b/src/hooks/useThreeDSecureHandler.tsx index f4d17d8..6630f47 100644 --- a/src/hooks/useThreeDSecureHandler.tsx +++ b/src/hooks/useThreeDSecureHandler.tsx @@ -1,6 +1,10 @@ import { useContext } from "react"; -import { CardDetailsType, TokenResponseStatuses } from "../util/types"; +import { + CardDetailsType, + sessionDataType, + TokenResponseStatuses, +} from "../util/types"; import { Actions, DispatchContext, StateContext } from "../context/state"; import { getMonthYearFromExpiry, openURL } from "../util/helpers"; import { generateToken } from "../services/secureTokenService"; @@ -10,8 +14,9 @@ import useMainStateUtils from "./useMainStateUtils"; const useThreeDSecureHandler = () => { const dispatch = useContext(DispatchContext); - const { amount, currency, providerPropsData } = useContext(StateContext); + const { sessionData, providerPropsData } = useContext(StateContext); const { startLoading, stopLoading, onPaymentFailed } = useMainStateUtils(); + const SessionData = sessionData as sessionDataType; const threeDSecurePayment = async (paymentDetails: CardDetailsType) => { startLoading(); @@ -22,8 +27,8 @@ const useThreeDSecureHandler = () => { const token = await generateToken({ publishableKey: providerPropsData?.publishableKey, - amount: amount, - currency: currency, + amount: SessionData.amount, + currency: SessionData.currency, return_url: providerPropsData?.urlScheme ?? BASE_URL, cardNumber: paymentDetails?.cardNumber ?? "", month: month ?? "", diff --git a/src/hooks/useValidationHandler.tsx b/src/hooks/useValidationHandler.tsx index f250bfa..c5737c3 100644 --- a/src/hooks/useValidationHandler.tsx +++ b/src/hooks/useValidationHandler.tsx @@ -2,7 +2,11 @@ import { useContext } from "react"; import { Alert } from "react-native"; import i18next from "i18next"; -import { KomojuProviderIprops } from "../util/types"; +import { + CurrencyTypes, + KomojuProviderIprops, + PaymentMode, +} from "../util/types"; import sessionShow from "../services/sessionShow"; import { validateSessionResponse } from "../util/validator"; import { Actions, DispatchContext } from "../context/state"; @@ -45,25 +49,22 @@ const useValidationHandler = ({ i18next.changeLanguage(sessionData?.default_locale); } - // if session is valid setting amount, currency type at global store for future use - dispatch({ - type: Actions.SET_AMOUNT, - payload: String(sessionData?.amount), - }); - - dispatch({ type: Actions.SET_CURRENCY, payload: sessionData?.currency }); - // if user provided explicitly payments methods via props, will give priority to that over session payment methods const paymentMethods = parsePaymentMethods( props?.paymentMethods, - sessionData?.payment_methods + sessionData?.payment_methods ?? [] ); - // setting the payment methods in global state dispatch({ - type: Actions.SET_PAYMENT_METHODS, - payload: paymentMethods, + type: Actions.SET_SESSION_DATA, + payload: { + amount: String(sessionData?.amount), + currency: sessionData?.currency ?? CurrencyTypes.JPY, + paymentMethods: paymentMethods, + mode: sessionData?.mode ?? PaymentMode.Payment, + }, }); + // setting the current selected payment method as the first payment method on the list dispatch({ type: Actions.SET_PAYMENT_OPTION, diff --git a/src/util/helpers.ts b/src/util/helpers.ts index c7a7069..64fc348 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -138,8 +138,8 @@ export const parseBrands = ( // Method to filter out payment methods export const parsePaymentMethods = ( userPaymentMethods: PaymentType[] | undefined, - sessionPaymentMethods: sessionShowPaymentMethodType[] | undefined -) => { + sessionPaymentMethods: sessionShowPaymentMethodType[] +): Array => { // check if user has provided explicit payment methods if (userPaymentMethods && userPaymentMethods?.length > 0) { const parsedPayment: sessionShowPaymentMethodType[] = []; @@ -211,9 +211,10 @@ export const isAndroid = () => Platform.OS === "android"; export const isIOS = () => Platform.OS === "ios"; export const { height: SCREEN_HEIGHT } = Dimensions.get("window"); - // Function to convert UserFriendlyTheme to ThemeSchemeType -export function fromUserFriendlyTheme(userTheme: Partial): Partial { +export function fromUserFriendlyTheme( + userTheme: Partial +): Partial { return Object.entries(userTheme).reduce((acc, [userKey, value]) => { const internalKey = themeMapping[userKey as keyof UserFriendlyTheme]; if (internalKey) { @@ -221,4 +222,4 @@ export function fromUserFriendlyTheme(userTheme: Partial): Pa } return acc; }, {} as Partial); -} \ No newline at end of file +} diff --git a/src/util/types.ts b/src/util/types.ts index 9147d3a..3fdc592 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -111,6 +111,12 @@ export enum CurrencyTypes { USD = "USD", } +export enum PaymentMode { + Payment = "payment", + Customer = "customer", + CustomerPayment = "customer_payment", +} + export type payForSessionProps = { publishableKey: string; sessionId: string; @@ -163,6 +169,9 @@ export type SessionPayResponseType = { payment_details: { instructions_url: string }; status?: string; }; + customer?: { + resource: PaymentMode; + }; }; export type sessionShowPaymentMethodType = { @@ -177,8 +186,8 @@ export type SessionShowResponseType = { expired: boolean; secure_token?: { verification_status?: string }; amount: number; - mode: string; - currency: string; + mode: PaymentMode; + currency: CurrencyTypes; session_url?: string; return_url?: string; payment_methods: Array; @@ -226,6 +235,25 @@ export type brandType = { icon: string; }; +export type sessionDataType = { + /** + * Amount for the payment + */ + amount: string; + /** + * Currency type for the payment + */ + currency: CurrencyTypes; + /** + * All payment methods which are accepting + */ + paymentMethods: Array; + /** + * session mode of payment + */ + mode: PaymentMode; +}; + export type State = CardDetailsType & KonbiniDetailsType & { /** @@ -239,23 +267,14 @@ export type State = CardDetailsType & /** * All user provided current payment session related data */ - sessionData: CreatePaymentFuncType; + sessionData: + | CreatePaymentFuncType + | sessionDataType + | (CreatePaymentFuncType & sessionDataType); /** * All user provided props under KomojuProvider */ providerPropsData: InitPrams; - /** - * Amount for the payment - */ - amount: string; - /** - * Currency type for the payment - */ - currency: CurrencyTypes; - /** - * All payment methods which are accepting - */ - paymentMethods: Array; /** * State of the current payment. * this state is used to toggle(show hide) the success and failed screens. @@ -315,13 +334,13 @@ export const initialState: State = { }, /** Bank transfer and Pay Easy related states end */ - amount: "", - currency: CurrencyTypes.JPY, paymentState: "", - paymentMethods: [], tokenId: "", sessionData: { sessionId: "", + amount: "", + currency: CurrencyTypes.JPY, + paymentMethods: [], }, providerPropsData: { publishableKey: "",