diff --git a/src/__tests__/CardInputGroup.test.tsx b/src/__tests__/CardInputGroup.test.tsx index f177a76..7fc6997 100644 --- a/src/__tests__/CardInputGroup.test.tsx +++ b/src/__tests__/CardInputGroup.test.tsx @@ -2,7 +2,11 @@ import React, { ReactNode } from "react"; import { render, fireEvent } from "@testing-library/react-native"; -import { determineCardType, formatCreditCardNumber, formatExpiry } from "../util/helpers"; +import { + determineCardType, + formatCreditCardNumber, + formatExpiry, +} from "../util/helpers"; import { Actions, DispatchContext, StateContext } from "../context/state"; import CardInputGroup from "../components/CardInputGroup"; import { isCardNumberValid, validateCardExpiry } from "../util/validator"; @@ -25,9 +29,11 @@ jest.mock("../util/validator", () => ({ // Mocked context values const mockState = { - cardCVV: "", - cardNumber: "", - cardExpiredDate: "", + cardData: { + cardCVV: "", + cardNumber: "", + cardExpiredDate: "", + }, }; const inputGroupMockValues = { @@ -60,18 +66,20 @@ describe("CardInputGroup Component", () => { const { getByTestId } = renderWithContext( { }} + resetError={(data: string) => {}} /> ); // Check if the inputs render with the initial values from context expect(getByTestId("cardNumberInput").props.value).toBe( - mockState.cardNumber + mockState.cardData.cardNumber ); expect(getByTestId("cardExpiryInput").props.value).toBe( - mockState.cardExpiredDate + mockState.cardData.cardExpiredDate + ); + expect(getByTestId("cardCVVInput").props.value).toBe( + mockState.cardData.cardCVV ); - expect(getByTestId("cardCVVInput").props.value).toBe(mockState.cardCVV); }); it("updates card number when valid and dispatches action", () => { @@ -82,7 +90,7 @@ describe("CardInputGroup Component", () => { const { getByTestId } = renderWithContext( { }} + resetError={(data: string) => {}} /> ); @@ -96,8 +104,8 @@ describe("CardInputGroup Component", () => { // Check if the dispatch was called with the correct action expect(mockDispatch).toHaveBeenCalledWith({ - type: Actions.SET_CARD_NUMBER, - payload: "1234 1234 1234 1234", + type: Actions.SET_CARD_DATA, + payload: { cardNumber: "1234 1234 1234 1234" }, }); }); @@ -107,7 +115,7 @@ describe("CardInputGroup Component", () => { const { getByTestId } = renderWithContext( { }} + resetError={(data: string) => {}} /> ); @@ -130,7 +138,7 @@ describe("CardInputGroup Component", () => { const { getByTestId } = renderWithContext( { }} + resetError={(data: string) => {}} /> ); @@ -144,8 +152,8 @@ describe("CardInputGroup Component", () => { // Check if the dispatch was called with the correct action expect(mockDispatch).toHaveBeenCalledWith({ - type: Actions.SET_CARD_EXPIRED_DATE, - payload: "12 / 25", + type: Actions.SET_CARD_DATA, + payload: { cardExpiredDate: "12 / 25" }, }); }); @@ -153,7 +161,7 @@ describe("CardInputGroup Component", () => { const { getByTestId } = renderWithContext( { }} + resetError={(data: string) => {}} /> ); @@ -164,8 +172,8 @@ describe("CardInputGroup Component", () => { // Check if the dispatch was called with the correct action expect(mockDispatch).toHaveBeenCalledWith({ - type: Actions.SET_CARD_CVV, - payload: "123", + type: Actions.SET_CARD_DATA, + payload: { cardCVV: "123" }, }); }); }); 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..bb1530a 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,16 +45,16 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { // const [toggleScanCard, setToggleScanCard] = useState(false); const theme = useCurrentTheme(); const styles = getStyles(theme); - const { cardCVV, cardNumber, cardExpiredDate, paymentMethods } = - useContext(StateContext); + const { cardData, sessionData } = useContext(StateContext); + const SessionData = sessionData as sessionDataType; useEffect(() => { // Determine card type and set it on first render if cardNumber is not empty - if (cardNumber) { - const type = determineCardType(cardNumber); + if (cardData?.cardNumber) { + const type = determineCardType(cardData?.cardNumber); setCardType(type); } - }, [cardNumber]); + }, [cardData?.cardNumber]); //Toggle card scanner // const toggleCardScanner = () => { @@ -83,7 +78,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 +111,7 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { } else { return; } - }, [cardType, paymentMethods]); + }, [cardType, SessionData?.paymentMethods]); // const onCardScanned = useCallback( // (cardDetails: { cardNumber?: string; expirationDate?: string }) => { @@ -163,7 +158,7 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { ]} > { if (isCardNumberValid(text)) { const derivedText = formatCreditCardNumber(text); dispatch({ - type: Actions.SET_CARD_NUMBER, - payload: derivedText, + type: Actions.SET_CARD_DATA, + payload: { + cardNumber: derivedText, + }, }); // Determine card type and set it const type = determineCardType(text); @@ -193,7 +190,7 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { ]} > { resetError("expiry"); if (validateCardExpiry(text)) { dispatch({ - type: Actions.SET_CARD_EXPIRED_DATE, - payload: formatExpiry(text), + type: Actions.SET_CARD_DATA, + payload: { + cardExpiredDate: formatExpiry(text), + }, }); } }} @@ -214,7 +213,7 @@ const CardInputGroup = ({ inputErrors, resetError }: Props) => { style={[styles.itemRow, inputErrors.cvv && styles.errorContainer]} > { resetError("cvv"); if (text?.length < 11) - dispatch({ type: Actions.SET_CARD_CVV, payload: text }); + dispatch({ + type: Actions.SET_CARD_DATA, + payload: { cardCVV: text }, + }); }} inputStyle={[ styles.cvvInputStyle, 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 { cardData, sessionData } = useContext(StateContext); const dispatch = useContext(DispatchContext); + const SessionData = sessionData as sessionDataType; const resetError = (type: string) => { // TODO: Fix this type error @@ -48,35 +51,69 @@ const CardSection = (): JSX.Element => { const onPay = () => { const isValid = validateCardFormFields({ - cardholderName, - cardCVV, - cardNumber, - cardExpiredDate, + cardData, + withEmail: SessionData?.mode === PaymentMode.Customer, // TODO: Fix this type error // @ts-expect-error - Type 'object' is not assignable to type 'SetStateAction'. setInputErrors, }); if (isValid) { - threeDSecurePayment({ - cardholderName, - cardCVV, - cardNumber, - cardExpiredDate, - }); + const cardDataObj: CardDetailsType = { + cardholderName: cardData?.cardNumber, + cardCVV: cardData?.cardCVV, + cardNumber: cardData?.cardNumber, + cardExpiredDate: cardData?.cardExpiredDate, + }; + if (cardData?.cardholderEmail) { + cardDataObj.cardholderEmail = cardData?.cardholderEmail; + } + + if (SessionData?.mode === PaymentMode.Customer) { + sessionPay({ + paymentType: PaymentType.CREDIT, + paymentDetails: cardDataObj, + }); + } else { + threeDSecurePayment(cardDataObj); + } } }; return ( + {SessionData?.mode === PaymentMode.Customer ? ( + + { + resetError("email"); + dispatch({ + type: Actions.SET_CARD_DATA, + payload: { cardholderEmail: text }, + }); + }} + inputStyle={styles.inputStyle} + error={inputErrors.email} + errorText="EMAIL_ERROR" + autoCapitalize="none" + inputMode="email" + /> + + ) : null} { resetError("name"); - dispatch({ type: Actions.SET_CARDHOLDER_NAME, payload: text }); + dispatch({ + type: Actions.SET_CARD_DATA, + payload: { cardholderName: text }, + }); }} inputStyle={styles.inputStyle} error={inputErrors.name} @@ -87,8 +124,15 @@ const CardSection = (): JSX.Element => { diff --git a/src/components/sections/KonbiniSection.tsx b/src/components/sections/KonbiniSection.tsx index b39b8d8..52e9f5a 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..7b24654 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/context/state.ts b/src/context/state.ts index a054f6b..e899770 100644 --- a/src/context/state.ts +++ b/src/context/state.ts @@ -16,11 +16,8 @@ import { export const Actions = { RESET_STATES: "RESET_STATES", - SET_CARDHOLDER_NAME: "SET_CARDHOLDER_NAME", - SET_CARD_NUMBER: "SET_CARD_NUMBER", + SET_CARD_DATA: "SET_CARD_DATA", SET_PAYMENT_OPTION: "SET_PAYMENT_OPTION", - SET_CARD_EXPIRED_DATE: "SET_CARD_EXPIRED_DATE", - SET_CARD_CVV: "SET_CARD_CVV", SET_NAME: "SET_NAME", SET_EMAIL: "SET_EMAIL", SET_TRANSFER_FORM_FIELDS: "SET_TRANSFER_FORM_FIELDS", @@ -60,25 +57,10 @@ export function reducer(state: State, action: ActionType) { ...state, loading: action.payload, }; - case Actions.SET_CARDHOLDER_NAME: + case Actions.SET_CARD_DATA: return { ...state, - cardholderName: action.payload, - }; - case Actions.SET_CARD_NUMBER: - return { - ...state, - cardNumber: action.payload, - }; - case Actions.SET_CARD_EXPIRED_DATE: - return { - ...state, - cardExpiredDate: action.payload, - }; - case Actions.SET_CARD_CVV: - return { - ...state, - cardCVV: action.payload, + cardData: { ...state.cardData, ...(action.payload as object) }, }; case Actions.SET_NAME: return { diff --git a/src/hooks/useBackgroundHandler.tsx b/src/hooks/useBackgroundHandler.tsx index 90717a8..72a7579 100644 --- a/src/hooks/useBackgroundHandler.tsx +++ b/src/hooks/useBackgroundHandler.tsx @@ -1,6 +1,8 @@ import { Dispatch, SetStateAction, useContext, useEffect } from "react"; import { AppState, AppStateStatus } from "react-native"; import { + CreatePaymentFuncType, + PaymentMode, PaymentStatuses, PaymentType, TokenResponseStatuses, @@ -18,6 +20,8 @@ const useBackgroundHandler = ( const { paymentType, tokenId, providerPropsData, sessionData } = useContext(StateContext); + const SessionData = sessionData as CreatePaymentFuncType; + const { startLoading, stopLoading, @@ -38,13 +42,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 @@ -52,12 +56,15 @@ const useBackgroundHandler = ( // if payment success showing success screen or if failed showing error screen if (sessionResponse?.status === PaymentStatuses.SUCCESS) { - if (sessionResponse?.payment?.status === TokenResponseStatuses.CAPTURED) { + if ( + sessionResponse?.payment?.status === TokenResponseStatuses.CAPTURED || + sessionResponse.mode === PaymentMode.Customer + ) { onPaymentSuccess(); } 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 +91,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..317d62e 100644 --- a/src/hooks/useDeepLinkHandler.tsx +++ b/src/hooks/useDeepLinkHandler.tsx @@ -1,6 +1,8 @@ import { Linking } from "react-native"; import { Dispatch, SetStateAction, useContext, useEffect } from "react"; import { + CreatePaymentFuncType, + PaymentMode, PaymentStatuses, PaymentType, TokenResponseStatuses, @@ -12,9 +14,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 +48,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..e8ce87e 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..9515119 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/services/payForSessionService.ts b/src/services/payForSessionService.ts index bdc585f..81e5edf 100644 --- a/src/services/payForSessionService.ts +++ b/src/services/payForSessionService.ts @@ -57,6 +57,9 @@ const payForSession = async ({ month, year, verification_value: paymentDetails?.cardCVV, + ...(paymentDetails?.cardholderEmail && { + email: paymentDetails.cardholderEmail, + }), }; } diff --git a/src/util/helpers.ts b/src/util/helpers.ts index c7a7069..b8235bb 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -28,8 +28,7 @@ export const printLog = ({ export const formatExpiry = (expiry: string) => { const prevExpiry = expiry.split(" / ").join("/"); - if (!prevExpiry) return null; - // TODO: Fix this type error + if (!prevExpiry) return undefined; let expiryDate: string | string[] = prevExpiry; if (/^[2-9]$/.test(expiryDate)) { @@ -138,8 +137,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 +210,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 +221,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..c9ba9de 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; @@ -118,7 +124,9 @@ export type payForSessionProps = { paymentDetails?: CardDetailsType & KonbiniDetailsType & TransferFormFieldsType & - paymentTypeInputs; + paymentTypeInputs & { + tokenId?: string; + }; }; type paymentTypeInputs = { @@ -126,11 +134,11 @@ type paymentTypeInputs = { }; export type CardDetailsType = { + cardholderEmail?: string; cardholderName?: string; cardNumber?: string; cardExpiredDate?: string; cardCVV?: string; - tokenId?: string; }; type KonbiniDetailsType = { @@ -163,6 +171,9 @@ export type SessionPayResponseType = { payment_details: { instructions_url: string }; status?: string; }; + customer?: { + resource: PaymentMode; + }; }; export type sessionShowPaymentMethodType = { @@ -177,8 +188,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; @@ -209,7 +220,10 @@ export type setInputErrorType = { setInputErrors: Dispatch>; }; -export type cardValidationFuncProps = CardDetailsType & setInputErrorType; +export type cardValidationFuncProps = setInputErrorType & { + cardData: CardDetailsType; + withEmail?: boolean; +}; export type konbiniValidationFuncProps = KonbiniDetailsType & setInputErrorType; @@ -226,56 +240,69 @@ export type brandType = { icon: string; }; -export type State = CardDetailsType & - KonbiniDetailsType & { - /** - * Current selected payment type. - */ - paymentType: PaymentType; - /** - * Global loading state. to display loading animation over sdk and disable buttons. - */ - loading: boolean; - /** - * All user provided current payment session related data - */ - sessionData: CreatePaymentFuncType; - /** - * 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. - */ - paymentState: - | ResponseScreenStatuses.SUCCESS - | ResponseScreenStatuses.COMPLETE - | ResponseScreenStatuses.FAILED - | ResponseScreenStatuses.CANCELLED - | ResponseScreenStatuses.EXPIRED - | ""; - /** - * States of the Bank transfer and Pay Easy fields. - */ - transferFormFields?: TransferFormFieldsType; - /** - * Secure token id for 3ds payment - */ - tokenId: 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 = KonbiniDetailsType & { + /** + * Current selected payment type. + */ + paymentType: PaymentType; + /** + * Global loading state. to display loading animation over sdk and disable buttons. + */ + loading: boolean; + /** + * All credit card related data + */ + cardData: CardDetailsType; + /** + * All user provided current payment session related data + */ + sessionData: + | CreatePaymentFuncType + | sessionDataType + | (CreatePaymentFuncType & sessionDataType); + /** + * All user provided props under KomojuProvider + */ + providerPropsData: InitPrams; + /** + * State of the current payment. + * this state is used to toggle(show hide) the success and failed screens. + */ + paymentState: + | ResponseScreenStatuses.SUCCESS + | ResponseScreenStatuses.COMPLETE + | ResponseScreenStatuses.FAILED + | ResponseScreenStatuses.CANCELLED + | ResponseScreenStatuses.EXPIRED + | ""; + /** + * States of the Bank transfer and Pay Easy fields. + */ + transferFormFields?: TransferFormFieldsType; + /** + * Secure token id for 3ds payment + */ + tokenId: string; +}; export type sessionPayProps = { paymentType: PaymentType; @@ -291,10 +318,12 @@ export const initialState: State = { loading: false, /** credit card payment related states start */ - cardholderName: "", - cardCVV: "", - cardNumber: "", - cardExpiredDate: "", + cardData: { + cardholderName: "", + cardCVV: "", + cardNumber: "", + cardExpiredDate: "", + }, /** credit card payment related states end */ /** konbini pay related states start */ @@ -315,13 +344,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: "", diff --git a/src/util/validator.ts b/src/util/validator.ts index 91c01b5..3507981 100644 --- a/src/util/validator.ts +++ b/src/util/validator.ts @@ -83,31 +83,37 @@ export const validateSessionResponse = ( }; export const validateCardFormFields = ({ - cardholderName, - cardNumber, - cardExpiredDate, - cardCVV, + cardData, + withEmail, setInputErrors, }: cardValidationFuncProps): boolean => { let valid = true; - if (!cardholderName) { + if ( + withEmail && + (!cardData?.cardholderEmail || !validateEmail(cardData?.cardholderEmail)) + ) { + setInputErrors((pre: object) => ({ ...pre, email: true })); + valid = false; + } + + if (!cardData?.cardholderName) { setInputErrors((pre: object) => ({ ...pre, name: true })); valid = false; } - if (!cardNumber) { + if (!cardData?.cardNumber) { setInputErrors((pre: object) => ({ ...pre, number: true })); valid = false; } - if (!luhnCheck(cardNumber ?? "")) { + if (!luhnCheck(cardData?.cardNumber ?? "")) { setInputErrors((pre: object) => ({ ...pre, number: true })); valid = false; } - if (!expiryDateCheck(cardExpiredDate ?? "")) { + if (!expiryDateCheck(cardData?.cardExpiredDate ?? "")) { setInputErrors((pre: object) => ({ ...pre, expiry: true })); valid = false; } - if (!cardCVV) { + if (!cardData?.cardCVV) { setInputErrors((pre: object) => ({ ...pre, cvv: true })); valid = false; }