From 458939921526c7d8d94760540e6b6ec9446e3493 Mon Sep 17 00:00:00 2001 From: "aritro.ghosh" Date: Fri, 13 Mar 2026 12:45:51 +0530 Subject: [PATCH 1/3] feat: subscription based events for web --- src/CardUtils.res | 22 ++- src/Components/AccordionContainer.res | 8 + src/Components/LoaderPaymentShimmer.res | 3 +- src/Components/SavedCardItem.res | 23 ++- src/Components/SavedMethods.res | 37 +++- src/Hooks/CommonCardProps.res | 4 +- src/Hooks/SubscriptionEventHooks.res | 138 ++++++++++++++ src/Hooks/UtilityHooks.res | 12 +- src/Payment.res | 2 + src/PaymentOptions.res | 8 + src/Payments/ACHBankDebit.res | 5 +- src/Payments/ACHBankTransfer.res | 3 +- src/Payments/ApplePay.res | 20 ++ src/Payments/BacsBankDebit.res | 1 + src/Payments/BacsBankTransfer.res | 4 +- src/Payments/BecsBankDebit.res | 2 + src/Payments/Boleto.res | 2 + src/Payments/CardPayment.res | 31 +++- src/Payments/GPay.res | 15 +- src/Payments/InstantBankTransfer.res | 10 +- src/Payments/InstantBankTransferFinland.res | 10 +- src/Payments/InstantBankTransferPoland.res | 10 +- src/Payments/KlarnaCheckout.res | 12 ++ src/Payments/KlarnaSDK.res | 20 ++ src/Payments/PayPal.res | 26 ++- src/Payments/PaymentMethodsWrapper.res | 2 + src/Payments/PaypalSDK.res | 6 + src/Payments/PaypalSDKHelpers.res | 15 ++ src/Payments/PazeButton.res | 21 +++ src/Payments/SamsungPayComponent.res | 20 +- src/Payments/SepaBankDebit.res | 10 +- src/Payments/SepaBankTransfer.res | 10 +- src/Types/PaymentType.res | 3 + src/Types/SubscriptionEventTypes.res | 193 ++++++++++++++++++++ src/Utilities/DynamicFieldsUtils.res | 2 + src/Utilities/PaymentUtils.res | 148 ++++++++------- src/Utilities/Utils.res | 46 +++-- src/hyper-loader/Elements.res | 26 ++- src/hyper-loader/LoaderPaymentElement.res | 2 +- src/hyper-loader/Types.res | 2 +- 40 files changed, 793 insertions(+), 141 deletions(-) create mode 100644 src/Hooks/SubscriptionEventHooks.res create mode 100644 src/Types/SubscriptionEventTypes.res diff --git a/src/CardUtils.res b/src/CardUtils.res index f0400b087..675d7f3d2 100644 --- a/src/CardUtils.res +++ b/src/CardUtils.res @@ -743,13 +743,23 @@ let getCardBrandInvalidError = (~cardBrand, ~localeString: LocaleStringTypes.loc } } -let emitExpiryDate = formattedExpiry => - Utils.messageParentWindow([("expiryDate", formattedExpiry->JSON.Encode.string)]) +let emitExpiryDate = (formattedExpiry, ~subscriptionEvents) => { + if ( + SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + ) { + Utils.messageParentWindow([("expiryDate", formattedExpiry->JSON.Encode.string)]) + } +} -let emitIsFormReadyForSubmission = isFormReadyForSubmission => - Utils.messageParentWindow([ - ("isFormReadyForSubmission", isFormReadyForSubmission->JSON.Encode.bool), - ]) +let emitIsFormReadyForSubmission = (isFormReadyForSubmission, ~subscriptionEvents) => { + if ( + SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + ) { + Utils.messageParentWindow([ + ("isFormReadyForSubmission", isFormReadyForSubmission->JSON.Encode.bool), + ]) + } +} let getCardBin = cardNumber => cardNumber->CardValidations.clearSpaces->String.substring(~start=0, ~end=6) diff --git a/src/Components/AccordionContainer.res b/src/Components/AccordionContainer.res index ffcea383e..7ad67584e 100644 --- a/src/Components/AccordionContainer.res +++ b/src/Components/AccordionContainer.res @@ -77,6 +77,14 @@ let make = ( ~cardProps, ~expiryProps, ) + SubscriptionEventHooks.usePaymentMethodStatus( + ~paymentMethodName=selectedOption, + ~paymentMethods=paymentMethodListValue.payment_methods, + ~isSavedPaymentMethod=false, + ~isOneClickWallet=false, + ) + + SubscriptionEventHooks.useBillingAddress() let cardOptionDetails = paymentOptions diff --git a/src/Components/LoaderPaymentShimmer.res b/src/Components/LoaderPaymentShimmer.res index 5e2097423..eb441ff01 100644 --- a/src/Components/LoaderPaymentShimmer.res +++ b/src/Components/LoaderPaymentShimmer.res @@ -2,6 +2,7 @@ let make = () => { open RecoilAtoms let selectedOption = Recoil.useRecoilValueFromAtom(selectedOptionAtom) - UtilityHooks.useHandlePostMessages(~complete=false, ~empty=false, ~paymentType=selectedOption) + SubscriptionEventHooks.useFormStatus(~empty=true, ~complete=false) + UtilityHooks.useHandlePostMessages(~complete=false, ~empty=true, ~paymentType=selectedOption) } diff --git a/src/Components/SavedCardItem.res b/src/Components/SavedCardItem.res index f03e9ee86..492aa9796 100644 --- a/src/Components/SavedCardItem.res +++ b/src/Components/SavedCardItem.res @@ -99,7 +99,7 @@ let make = ( | None => "debit" } let {country, state, pinCode} = PaymentUtils.useNonPiiAddressData() - + let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) React.useEffect(() => { open CardUtils @@ -111,7 +111,7 @@ let make = ( // * Sending card expiry to handle cases where the card expires before the use date. `${expiryMonth}${String.substring(~start=2, ~end=4, expiryYear)}` ->CardValidations.formatCardExpiryNumber - ->emitExpiryDate + ->emitExpiryDate(~subscriptionEvents=options.subscriptionEvents) PaymentUtils.emitPaymentMethodInfo( ~paymentMethod=paymentItem.paymentMethod, @@ -125,13 +125,30 @@ let make = ( ~cardLast4, ~cardBin, ~isSavedPaymentMethod=true, + ~subscriptionEvents=options.subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod=paymentItem.paymentMethod, + ~paymentMethodType, + ~isSavedPaymentMethod=true, + ~isOneClickWallet=false, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) } None }, (isActive, paymentItem, country, state, pinCode)) React.useEffect(() => { - CardUtils.emitIsFormReadyForSubmission(isCVCValid->Option.getOr(false)) + CardUtils.emitIsFormReadyForSubmission( + isCVCValid->Option.getOr(false), + ~subscriptionEvents=options.subscriptionEvents, + ) None }, [isCVCValid]) diff --git a/src/Components/SavedMethods.res b/src/Components/SavedMethods.res index 11b3d1940..a20f41f7c 100644 --- a/src/Components/SavedMethods.res +++ b/src/Components/SavedMethods.res @@ -78,6 +78,8 @@ let make = ( let (selectedInstallmentPlan, setSelectedInstallmentPlan) = React.useState(_ => None) let (showInstallments, setShowInstallments) = React.useState(_ => false) + let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) + let shouldShowClickToPaySection = clickToPayConfig.isReady == Some(true) && (!groupSavedMethodsWithPaymentMethods || selectedOption == "card") @@ -153,8 +155,41 @@ let make = ( let paymentMethodType = customerMethod.paymentMethodType->Option.getOr(customerMethod.paymentMethod) + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + UtilityHooks.useHandlePostMessages( + ~complete, + ~empty, + ~paymentType=paymentMethodType, + ~savedMethod=true, + ) - useHandlePostMessages(~complete, ~empty, ~paymentType=paymentMethodType, ~savedMethod=true) + // Emit card info for saved card methods + React.useEffect(() => { + if isCardPaymentMethod && !isUnknownPaymentMethod { + let card = customerMethod.card + SubscriptionEventHooks.emitCardInfo( + ~subscriptionEvents=options.subscriptionEvents, + ~bin=card.cardBin, + ~last4=card.last4Digits, + ~brand=card.scheme->Option.getOr(""), + ~expiryMonth=card.expiryMonth, + ~expiryYear=card.expiryYear, + ~formattedExpiry=`${card.expiryMonth}${String.substring( + ~start=2, + ~end=4, + card.expiryYear, + )}`->CardValidations.formatCardExpiryNumber, + ~isCardNumberComplete=true, + ~isCvcComplete=complete, + ~isExpiryComplete=true, + ~isCardNumberValid=true, + ~isExpiryValid=true, + ~isCvcValid=isCVCValid->Option.getOr(false), + ~isSavedCard=true, + ) + } + None + }, (customerMethod, isCardPaymentMethod, isUnknownPaymentMethod, complete, isCVCValid)) GooglePayHelpers.useHandleGooglePayResponse(~connectors=[], ~intent, ~isSavedMethodsFlow=true) diff --git a/src/Hooks/CommonCardProps.res b/src/Hooks/CommonCardProps.res index a342dffc0..6d2d1c0a9 100644 --- a/src/Hooks/CommonCardProps.res +++ b/src/Hooks/CommonCardProps.res @@ -139,13 +139,15 @@ let useCardForm = (~logger, ~paymentType) => { } } + let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) + let changeCardExpiry = ev => { let val = ReactEvent.Form.target(ev)["value"] logInputChangeInfo("cardExpiry", logger) let formattedExpiry = val->CardValidations.formatCardExpiryNumber if isExipryValid(formattedExpiry) { handleInputFocus(~currentRef=expiryRef, ~destinationRef=cvcRef) - emitExpiryDate(formattedExpiry) + CardUtils.emitExpiryDate(formattedExpiry, ~subscriptionEvents=options.subscriptionEvents) } setExpiryValid(formattedExpiry, setIsExpiryValid) setCardExpiry(_ => formattedExpiry) diff --git a/src/Hooks/SubscriptionEventHooks.res b/src/Hooks/SubscriptionEventHooks.res new file mode 100644 index 000000000..1265f4499 --- /dev/null +++ b/src/Hooks/SubscriptionEventHooks.res @@ -0,0 +1,138 @@ +open SubscriptionEventTypes + +let emitCardInfo = ( + ~subscriptionEvents, + ~bin, + ~last4, + ~brand, + ~expiryMonth, + ~expiryYear, + ~formattedExpiry, + ~isCardNumberComplete, + ~isCvcComplete, + ~isExpiryComplete, + ~isCardNumberValid, + ~isExpiryValid, + ~isCvcValid, + ~isSavedCard, +) => { + if shouldEmitEvent(subscriptionEvents, CARD_INFO) { + let payload = createCardInfoPayload( + ~bin, + ~last4, + ~brand, + ~expiryMonth, + ~expiryYear, + ~formattedExpiry, + ~isCardNumberComplete, + ~isCvcComplete, + ~isExpiryComplete, + ~isCardNumberValid, + ~isExpiryValid, + ~isCvcValid, + ~isSavedCard, + ) + Utils.messageParentWindow(payload) + } +} + +let emitPaymentMethodStatus = ( + ~subscriptionEvents, + ~paymentMethod, + ~paymentMethodType, + ~isSavedPaymentMethod, + ~isOneClickWallet=false, +) => { + if shouldEmitEvent(subscriptionEvents, PAYMENT_METHOD_STATUS) { + let payload = createPaymentMethodStatusPayload( + ~paymentMethod, + ~paymentMethodType, + ~isSavedPaymentMethod, + ~isOneClickWallet, + ) + Utils.messageParentWindow(payload) + } +} + +let emitBillingAddress = (~subscriptionEvents, ~country, ~state, ~postalCode) => { + if shouldEmitEvent(subscriptionEvents, PAYMENT_METHOD_INFO_BILLING_ADDRESS) { + let payload = createBillingAddressPayload(~country, ~state, ~postalCode) + Utils.messageParentWindow(payload) + } +} + +let useFormStatus = (~empty: bool, ~complete: bool, ~isOneClickWallet: bool=false) => { + let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) + let subscriptionEvents = options.subscriptionEvents + + React.useEffect(() => { + if !isOneClickWallet { + let formStatusValue = if complete { + Complete + } else if empty { + Empty + } else { + Filling + } + if shouldEmitEvent(subscriptionEvents, FORM_STATUS) { + let payload = createFormStatusPayload(~status=formStatusValue->formStatusValueToString) + Utils.messageParentWindow(payload) + } + } + None + }, (empty, complete, isOneClickWallet, subscriptionEvents)) +} + +let useBillingAddress = () => { + let country = Recoil.useRecoilValueFromAtom(RecoilAtoms.userCountry) + let state = Recoil.useRecoilValueFromAtom(RecoilAtoms.userAddressState).value + let pinCode = Recoil.useRecoilValueFromAtom(RecoilAtoms.userAddressPincode).value + let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) + + React.useEffect(() => { + emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, + ) + + None + }, (country, state, pinCode, options.subscriptionEvents)) +} + +let usePaymentMethodStatus = ( + ~paymentMethodName: string, + ~paymentMethods: array, + ~isSavedPaymentMethod: bool, + ~isOneClickWallet: bool, +) => { + let loggerState = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom) + let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) + + React.useEffect(() => { + switch PaymentUtils.getPaymentMethodAndType( + ~paymentMethodName, + ~paymentMethods, + ~logger=loggerState, + ) { + | Some((paymentMethod, paymentMethodType)) => + emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod, + ~paymentMethodType, + ~isSavedPaymentMethod, + ~isOneClickWallet, + ) + | None => () + } + + None + }, ( + paymentMethodName, + paymentMethods, + isSavedPaymentMethod, + isOneClickWallet, + options.subscriptionEvents, + )) +} diff --git a/src/Hooks/UtilityHooks.res b/src/Hooks/UtilityHooks.res index b0a0faed8..5066b5431 100644 --- a/src/Hooks/UtilityHooks.res +++ b/src/Hooks/UtilityHooks.res @@ -25,11 +25,19 @@ let useHandlePostMessages = (~complete, ~empty, ~paymentType, ~savedMethod=false open RecoilAtoms let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom) + let options = Recoil.useRecoilValueFromAtom(optionAtom) React.useEffect(() => { - Utils.handlePostMessageEvents(~complete, ~empty, ~paymentType, ~loggerState, ~savedMethod) + Utils.handlePostMessageEvents( + ~complete, + ~empty, + ~paymentType, + ~loggerState, + ~savedMethod, + ~subscriptionEvents=options.subscriptionEvents, + ) None - }, (complete, empty, paymentType)) + }, (complete, empty, paymentType, options.subscriptionEvents)) } let useIsCustomerAcceptanceRequired = ( diff --git a/src/Payment.res b/src/Payment.res index d31e7e61d..8527d69bb 100644 --- a/src/Payment.res +++ b/src/Payment.res @@ -12,6 +12,7 @@ let setUserError = message => { @react.component let make = (~paymentMode, ~integrateError, ~logger) => { let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) + let options = Recoil.useRecoilValueFromAtom(optionAtom) let {iframeId} = Recoil.useRecoilValueFromAtom(keys) let isManualRetryEnabled = Recoil.useRecoilValueFromAtom(isManualRetryEnabled) let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(areRequiredFieldsValid) @@ -39,6 +40,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { | (Some(cardValid), Some(expiryValid), Some(cvcValid)) => CardUtils.emitIsFormReadyForSubmission( cardValid && expiryValid && cvcValid && areRequiredFieldsValid, + ~subscriptionEvents=options.subscriptionEvents, ) | _ => () } diff --git a/src/PaymentOptions.res b/src/PaymentOptions.res index ef8b3129b..fbd6e6869 100644 --- a/src/PaymentOptions.res +++ b/src/PaymentOptions.res @@ -122,6 +122,14 @@ let make = ( ~expiryProps, ) + SubscriptionEventHooks.useBillingAddress() + SubscriptionEventHooks.usePaymentMethodStatus( + ~paymentMethodName=selectedPaymentOption.paymentMethodName, + ~paymentMethods=paymentMethodListValue.payment_methods, + ~isSavedPaymentMethod=false, + ~isOneClickWallet=false, + ) + let displayIcon = ele => { ele } diff --git a/src/Payments/ACHBankDebit.res b/src/Payments/ACHBankDebit.res index bc14d311d..122c83eff 100644 --- a/src/Payments/ACHBankDebit.res +++ b/src/Payments/ACHBankDebit.res @@ -1,6 +1,5 @@ open RecoilAtoms open Utils -open PaymentModeType @react.component let make = () => { @@ -61,7 +60,9 @@ let make = () => { fullName.value != "" && email.isValid->Option.getOr(false) && modalData->Option.isSome - let empty = email.value == "" || fullName.value != "" + let empty = email.value == "" || fullName.value == "" + + SubscriptionEventHooks.useFormStatus(~complete, ~empty) UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="ach_bank_debit") diff --git a/src/Payments/ACHBankTransfer.res b/src/Payments/ACHBankTransfer.res index aa82bbcd0..32dff8de5 100644 --- a/src/Payments/ACHBankTransfer.res +++ b/src/Payments/ACHBankTransfer.res @@ -16,7 +16,8 @@ let make = () => { let complete = email.value != "" && email.isValid->Option.getOr(false) let empty = email.value == "" - UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="bank_transfer") + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="ach_bank_transfer") React.useEffect(() => { setComplete(_ => complete) diff --git a/src/Payments/ApplePay.res b/src/Payments/ApplePay.res index bb4a4b71e..38ea3ec04 100644 --- a/src/Payments/ApplePay.res +++ b/src/Payments/ApplePay.res @@ -36,6 +36,12 @@ let make = (~sessionObj: option, ~walletOptions) => { | _ => 48 } + SubscriptionEventHooks.useFormStatus( + ~empty=areRequiredFieldsEmpty, + ~complete=areRequiredFieldsValid, + ~isOneClickWallet=isWallet, + ) + UtilityHooks.useHandlePostMessages( ~complete=areRequiredFieldsValid, ~empty=areRequiredFieldsEmpty, @@ -236,6 +242,20 @@ let make = (~sessionObj: option, ~walletOptions) => { ~country, ~state, ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod="wallet", + ~paymentMethodType="apple_pay", + ~isSavedPaymentMethod=false, + ~isOneClickWallet=isWallet, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) setApplePayClicked(_ => true) makeOneClickHandlerPromise(sdkHandleIsThere) diff --git a/src/Payments/BacsBankDebit.res b/src/Payments/BacsBankDebit.res index 1d97cb76d..423b77dc1 100644 --- a/src/Payments/BacsBankDebit.res +++ b/src/Payments/BacsBankDebit.res @@ -77,6 +77,7 @@ let make = () => { country.value == "" || state.value == "" + SubscriptionEventHooks.useFormStatus(~empty, ~complete) UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="bacs_bank_debit") React.useEffect(() => { diff --git a/src/Payments/BacsBankTransfer.res b/src/Payments/BacsBankTransfer.res index 08969b570..47a4720e2 100644 --- a/src/Payments/BacsBankTransfer.res +++ b/src/Payments/BacsBankTransfer.res @@ -20,7 +20,9 @@ let default = () => { let paymentMethodType = "bacs" let paymentMethod = "bank_transfer" - UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType=paymentMethod) + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType=paymentMethodType) React.useEffect(() => { setComplete(_ => complete) diff --git a/src/Payments/BecsBankDebit.res b/src/Payments/BecsBankDebit.res index 7ce15d5a2..ad3c76fb8 100644 --- a/src/Payments/BecsBankDebit.res +++ b/src/Payments/BecsBankDebit.res @@ -42,6 +42,8 @@ let make = () => { | None => true } + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="becs_bank_debit") React.useEffect(() => { diff --git a/src/Payments/Boleto.res b/src/Payments/Boleto.res index 24ef3e1ea..42956eb9f 100644 --- a/src/Payments/Boleto.res +++ b/src/Payments/Boleto.res @@ -43,6 +43,8 @@ let make = () => { ) }, [socialSecurityNumber]) + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="boleto") React.useEffect(() => { diff --git a/src/Payments/CardPayment.res b/src/Payments/CardPayment.res index 481241ac7..939ce2973 100644 --- a/src/Payments/CardPayment.res +++ b/src/Payments/CardPayment.res @@ -136,14 +136,41 @@ let make = ( isInstallmentValid let empty = cardNumber == "" || cardExpiry == "" || cvcNumber == "" + + SubscriptionEventHooks.useFormStatus(~empty, ~complete=complete && areRequiredFieldsValid) + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="card") + + React.useEffect(() => { + let (month, year) = CardUtils.getExpiryDates(cardExpiry) + let isCardNumberComplete = cardNumber->String.length > 0 && isCardValid->Option.getOr(false) + let isExpiryComplete = cardExpiry->String.length > 0 && isExpiryValid->Option.getOr(false) + let isCvcComplete = cvcNumber->String.length > 0 && isCVCValid->Option.getOr(false) + + SubscriptionEventHooks.emitCardInfo( + ~subscriptionEvents=options.subscriptionEvents, + ~bin=cardNumber->CardUtils.getCardBin, + ~last4=cardNumber->CardUtils.getCardLast4, + ~brand=cardBrand, + ~expiryMonth=month, + ~expiryYear=isExpiryComplete ? year : "", // Return empty year if expiry incomplete since getExpiryDates adds 20xx prefix by default + ~formattedExpiry=cardExpiry, + ~isCardNumberComplete, + ~isCvcComplete, + ~isExpiryComplete, + ~isCardNumberValid=isCardValid->Option.getOr(false), + ~isExpiryValid=isExpiryValid->Option.getOr(false), + ~isCvcValid=isCVCValid->Option.getOr(false), + ~isSavedCard=false, + ) + None + }, (cardNumber, cardExpiry, cvcNumber, isCardValid, isExpiryValid, isCVCValid, cardBrand)) + React.useEffect(() => { setComplete(_ => complete) setShowPaymentMethodsScreen(_ => true) None }, [complete]) - useHandlePostMessages(~complete=complete && areRequiredFieldsValid, ~empty, ~paymentType="card") - let isGuestCustomer = useIsGuestCustomer() let isCvcValidValue = CardUtils.getBoolOptionVal(isCVCValid) let (cardEmpty, cardComplete, cardInvalid) = CardUtils.useCardDetails( diff --git a/src/Payments/GPay.res b/src/Payments/GPay.res index 2c54d529d..610ed203a 100644 --- a/src/Payments/GPay.res +++ b/src/Payments/GPay.res @@ -41,7 +41,6 @@ let make = ( let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) let isWallet = walletOptions->Array.includes("google_pay") let isTestMode = Recoil.useRecoilValueFromAtom(RecoilAtoms.isTestMode) - UtilityHooks.useHandlePostMessages( ~complete=areRequiredFieldsValid, ~empty=areRequiredFieldsEmpty, @@ -114,6 +113,20 @@ let make = ( ~country, ~state, ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod="wallet", + ~paymentMethodType="google_pay", + ~isSavedPaymentMethod=false, + ~isOneClickWallet=isWallet, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) makeOneClickHandlerPromise(isSDKHandleClick) ->then(result => { diff --git a/src/Payments/InstantBankTransfer.res b/src/Payments/InstantBankTransfer.res index f7da43909..948cae1b4 100644 --- a/src/Payments/InstantBankTransfer.res +++ b/src/Payments/InstantBankTransfer.res @@ -17,11 +17,11 @@ let make = () => { let paymentMethodType = "instant_bank_transfer" let paymentMethod = "bank_transfer" - UtilityHooks.useHandlePostMessages( - ~complete=areRequiredFieldsValid && !areRequiredFieldsEmpty, - ~empty=areRequiredFieldsEmpty, - ~paymentType=paymentMethod, - ) + let complete = areRequiredFieldsValid && !areRequiredFieldsEmpty + let empty = areRequiredFieldsEmpty + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType=paymentMethod) let submitCallback = React.useCallback((ev: Window.event) => { let json = ev.data->safeParse diff --git a/src/Payments/InstantBankTransferFinland.res b/src/Payments/InstantBankTransferFinland.res index 10f1985cb..3ae9ffbed 100644 --- a/src/Payments/InstantBankTransferFinland.res +++ b/src/Payments/InstantBankTransferFinland.res @@ -17,11 +17,11 @@ let make = () => { let paymentMethodType = "instant_bank_transfer_finland" let paymentMethod = "bank_transfer" - UtilityHooks.useHandlePostMessages( - ~complete=areRequiredFieldsValid && !areRequiredFieldsEmpty, - ~empty=areRequiredFieldsEmpty, - ~paymentType=paymentMethod, - ) + let complete = areRequiredFieldsValid && !areRequiredFieldsEmpty + let empty = areRequiredFieldsEmpty + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType=paymentMethod) let submitCallback = React.useCallback((ev: Window.event) => { let json = ev.data->safeParse diff --git a/src/Payments/InstantBankTransferPoland.res b/src/Payments/InstantBankTransferPoland.res index 11367b8f4..40216350e 100644 --- a/src/Payments/InstantBankTransferPoland.res +++ b/src/Payments/InstantBankTransferPoland.res @@ -17,11 +17,11 @@ let make = () => { let paymentMethodType = "instant_bank_transfer_poland" let paymentMethod = "bank_transfer" - UtilityHooks.useHandlePostMessages( - ~complete=areRequiredFieldsValid && !areRequiredFieldsEmpty, - ~empty=areRequiredFieldsEmpty, - ~paymentType=paymentMethod, - ) + let complete = areRequiredFieldsValid && !areRequiredFieldsEmpty + let empty = areRequiredFieldsEmpty + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType=paymentMethod) let submitCallback = React.useCallback((ev: Window.event) => { let json = ev.data->safeParse diff --git a/src/Payments/KlarnaCheckout.res b/src/Payments/KlarnaCheckout.res index 972b225c3..f764bf9af 100644 --- a/src/Payments/KlarnaCheckout.res +++ b/src/Payments/KlarnaCheckout.res @@ -22,6 +22,18 @@ let make = () => { let (buttonColor, textColor) = options.wallets.style.theme == Light ? ("#0070ba", "#ffffff") : ("#0b051d", "#000000") + SubscriptionEventHooks.useFormStatus( + ~empty=!klarnaClicked, + ~complete=klarnaClicked, + ~isOneClickWallet=true, + ) + + UtilityHooks.useHandlePostMessages( + ~complete=klarnaClicked, + ~empty=!klarnaClicked, + ~paymentType="klarna_checkout", + ) + let onKlarnaClick = async _ev => { try { if isTestMode { diff --git a/src/Payments/KlarnaSDK.res b/src/Payments/KlarnaSDK.res index bace399b0..9fcefb6e8 100644 --- a/src/Payments/KlarnaSDK.res +++ b/src/Payments/KlarnaSDK.res @@ -41,6 +41,12 @@ let make = (~sessionObj: SessionsType.token) => { ~paymentMethodType="klarna", ) + SubscriptionEventHooks.useFormStatus( + ~complete=isCompleted, + ~empty=!isCompleted, + ~isOneClickWallet=true, + ) + UtilityHooks.useHandlePostMessages( ~complete=isCompleted, ~empty=!isCompleted, @@ -82,6 +88,20 @@ let make = (~sessionObj: SessionsType.token) => { ~country, ~state, ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod="wallet", + ~paymentMethodType="klarna", + ~isSavedPaymentMethod=false, + ~isOneClickWallet=true, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) makeOneClickHandlerPromise(sdkHandleIsThere)->then( result => { diff --git a/src/Payments/PayPal.res b/src/Payments/PayPal.res index f055621ac..ab9cd1c98 100644 --- a/src/Payments/PayPal.res +++ b/src/Payments/PayPal.res @@ -42,11 +42,13 @@ let make = (~walletOptions) => { let isManualRetryEnabled = Recoil.useRecoilValueFromAtom(RecoilAtoms.isManualRetryEnabled) let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), Paypal) - UtilityHooks.useHandlePostMessages( - ~complete=paypalClicked, - ~empty=!paypalClicked, - ~paymentType=paymentMethodType, - ) + + let complete = paypalClicked + let empty = !paypalClicked + SubscriptionEventHooks.useFormStatus(~empty, ~complete, ~isOneClickWallet=isWallet) + + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType=paymentMethodType) + let onPaypalClick = _ev => { if isTestMode { Console.warn("PayPal button clicked in test mode - interaction disabled") @@ -67,6 +69,20 @@ let make = (~walletOptions) => { ~country, ~state, ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod, + ~paymentMethodType, + ~isSavedPaymentMethod=false, + ~isOneClickWallet=isWallet, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) setPaypalClicked(_ => true) open Promise diff --git a/src/Payments/PaymentMethodsWrapper.res b/src/Payments/PaymentMethodsWrapper.res index ad0bfab84..dd4d36ac3 100644 --- a/src/Payments/PaymentMethodsWrapper.res +++ b/src/Payments/PaymentMethodsWrapper.res @@ -49,6 +49,8 @@ let make = (~paymentMethodName: string) => { let empty = areRequiredFieldsEmpty + SubscriptionEventHooks.useFormStatus(~empty, ~complete=areRequiredFieldsValid) + UtilityHooks.useHandlePostMessages( ~complete=areRequiredFieldsValid, ~empty, diff --git a/src/Payments/PaypalSDK.res b/src/Payments/PaypalSDK.res index a09f708cd..a93961617 100644 --- a/src/Payments/PaypalSDK.res +++ b/src/Payments/PaypalSDK.res @@ -64,6 +64,11 @@ let make = (~sessionObj: SessionsType.token) => { ~paymentMethod="wallet", ~paymentMethodType="paypal", ) + SubscriptionEventHooks.useFormStatus( + ~empty=!isCompleted, + ~complete=isCompleted, + ~isOneClickWallet=true, + ) UtilityHooks.useHandlePostMessages( ~complete=isCompleted, @@ -110,6 +115,7 @@ let make = (~sessionObj: SessionsType.token) => { ~isTestMode, ~nonPiiAdderessData, ~sdkAuthorization, + ~subscriptionEvents=options.subscriptionEvents, ) }) Window.body->Window.appendChild(paypalScript) diff --git a/src/Payments/PaypalSDKHelpers.res b/src/Payments/PaypalSDKHelpers.res index 1d679dcc7..b7fca1da0 100644 --- a/src/Payments/PaypalSDKHelpers.res +++ b/src/Payments/PaypalSDKHelpers.res @@ -28,6 +28,7 @@ let loadPaypalSDK = ( ~isTestMode=false, ~nonPiiAdderessData: PaymentUtils.nonPiiAdderessData, ~sdkAuthorization, + ~subscriptionEvents: option>, ) => { open Promise @@ -70,6 +71,20 @@ let loadPaypalSDK = ( ~country, ~state, ~pinCode, + ~subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents, + ~paymentMethod="wallet", + ~paymentMethodType="paypal", + ~isSavedPaymentMethod=false, + ~isOneClickWallet=true, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) makeOneClickHandlerPromise(sdkHandleIsThere)->then(result => { let result = result->JSON.Decode.bool->Option.getOr(false) diff --git a/src/Payments/PazeButton.res b/src/Payments/PazeButton.res index dbed84cea..27454cea0 100644 --- a/src/Payments/PazeButton.res +++ b/src/Payments/PazeButton.res @@ -18,6 +18,13 @@ let make = (~token: SessionsType.token) => { let (showLoader, setShowLoader) = React.useState(() => false) let isTestMode = Recoil.useRecoilValueFromAtom(RecoilAtoms.isTestMode) let {country, state, pinCode} = PaymentUtils.useNonPiiAddressData() + SubscriptionEventHooks.useFormStatus( + ~empty=!showLoader, + ~complete=showLoader, + ~isOneClickWallet=true, + ) + + UtilityHooks.useHandlePostMessages(~complete=showLoader, ~empty=!showLoader, ~paymentType="paze") let onClick = _ => { if isTestMode { @@ -39,6 +46,20 @@ let make = (~token: SessionsType.token) => { ~country, ~state, ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod="wallet", + ~paymentMethodType="paze", + ~isSavedPaymentMethod=false, + ~isOneClickWallet=true, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) setShowLoader(_ => true) let metadata = diff --git a/src/Payments/SamsungPayComponent.res b/src/Payments/SamsungPayComponent.res index 40dd17242..4cd3c1143 100644 --- a/src/Payments/SamsungPayComponent.res +++ b/src/Payments/SamsungPayComponent.res @@ -17,14 +17,18 @@ let make = (~sessionObj: option, ~walletOptions) => { let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), Samsungpay) let isTestMode = Recoil.useRecoilValueFromAtom(RecoilAtoms.isTestMode) let {country, state, pinCode} = PaymentUtils.useNonPiiAddressData() + let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(areRequiredFieldsValid) + let areRequiredFieldsEmpty = Recoil.useRecoilValueFromAtom(areRequiredFieldsEmpty) let (_, _, _, _, heightType) = options.wallets.style.height let height = switch heightType { | SamsungPay(val) => val | _ => 48 } - + let empty = areRequiredFieldsEmpty + let complete = areRequiredFieldsValid && !areRequiredFieldsEmpty SamsungPayHelpers.useHandleSamsungPayResponse(~intent, ~isWallet) + SubscriptionEventHooks.useFormStatus(~complete, ~empty, ~isOneClickWallet=true) let getSamsungPaymentsClient = _ => SamsungPayType.samsung({ @@ -51,6 +55,20 @@ let make = (~sessionObj: option, ~walletOptions) => { ~country, ~state, ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, + ) + SubscriptionEventHooks.emitPaymentMethodStatus( + ~subscriptionEvents=options.subscriptionEvents, + ~paymentMethod="wallet", + ~paymentMethodType="samsung_pay", + ~isSavedPaymentMethod=false, + ~isOneClickWallet=true, + ) + SubscriptionEventHooks.emitBillingAddress( + ~subscriptionEvents=options.subscriptionEvents, + ~country, + ~state, + ~postalCode=pinCode, ) SamsungPayHelpers.handleSamsungPayClicked( ~sessionObj=sessionObj->Option.getOr(JSON.Encode.null)->getDictFromJson, diff --git a/src/Payments/SepaBankDebit.res b/src/Payments/SepaBankDebit.res index 4b99b164a..b90422c02 100644 --- a/src/Payments/SepaBankDebit.res +++ b/src/Payments/SepaBankDebit.res @@ -26,11 +26,11 @@ let make = () => { let isVerifyPMAuthConnectorConfigured = displaySavedPaymentMethods && pmAuthMapper->Dict.get(paymentMethodType)->Option.isSome - UtilityHooks.useHandlePostMessages( - ~complete=areRequiredFieldsValid, - ~empty=areRequiredFieldsEmpty, - ~paymentType="sepa_bank_debit", - ) + let empty = areRequiredFieldsEmpty + let complete = areRequiredFieldsValid + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="sepa_bank_debit") let submitCallback = React.useCallback((ev: Window.event) => { let json = ev.data->safeParse diff --git a/src/Payments/SepaBankTransfer.res b/src/Payments/SepaBankTransfer.res index b98e4eadf..2881c4f3f 100644 --- a/src/Payments/SepaBankTransfer.res +++ b/src/Payments/SepaBankTransfer.res @@ -13,11 +13,11 @@ let make = () => { let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) - UtilityHooks.useHandlePostMessages( - ~complete=areRequiredFieldsValid && !areRequiredFieldsEmpty, - ~empty=areRequiredFieldsEmpty, - ~paymentType="bank_transfer", - ) + let complete = areRequiredFieldsValid && !areRequiredFieldsEmpty + let empty = areRequiredFieldsEmpty + SubscriptionEventHooks.useFormStatus(~empty, ~complete) + + UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="sepa_bank_transfer") let submitCallback = React.useCallback((ev: Window.event) => { let json = ev.data->safeParse diff --git a/src/Types/PaymentType.res b/src/Types/PaymentType.res index 4dc2afd0c..bbad5f732 100644 --- a/src/Types/PaymentType.res +++ b/src/Types/PaymentType.res @@ -207,6 +207,7 @@ type options = { business: business, customerPaymentMethods: savedCardsLoadState, paymentMethodOrder: option>, + subscriptionEvents: option>, displaySavedPaymentMethodsCheckbox: bool, displaySavedPaymentMethods: bool, savedPaymentMethodsCheckboxCheckedByDefault: bool, @@ -385,6 +386,7 @@ let defaultOptions = { customerPaymentMethods: LoadingSavedCards, layout: ObjectLayout(defaultLayout), paymentMethodOrder: None, + subscriptionEvents: None, fields: defaultFields, displaySavedPaymentMethodsCheckbox: true, displaySavedPaymentMethods: true, @@ -1286,6 +1288,7 @@ let itemToObjMapper = (dict, logger: HyperLoggerTypes.loggerMake) => { layout: getLayout(dict, "layout", logger), customerPaymentMethods: getCustomerMethods(dict, "customerPaymentMethods"), paymentMethodOrder: getOptionalStrArray(dict, "paymentMethodOrder"), + subscriptionEvents: SubscriptionEventTypes.getSubscriptionEvents(dict, "subscriptionEvents"), fields: getFields(dict, "fields", logger), branding: getWarningString(dict, "branding", "auto", ~logger)->getShowType("options.branding"), displaySavedPaymentMethodsCheckbox: getBoolWithWarning( diff --git a/src/Types/SubscriptionEventTypes.res b/src/Types/SubscriptionEventTypes.res new file mode 100644 index 000000000..bb0d85b91 --- /dev/null +++ b/src/Types/SubscriptionEventTypes.res @@ -0,0 +1,193 @@ +open ErrorUtils + +type events = + | FORM_STATUS + | PAYMENT_METHOD_STATUS + | CARD_INFO + | PAYMENT_METHOD_INFO_BILLING_ADDRESS + | UNKNOWN_EVENT + +let validSubscriptionEvents = [ + "FORM_STATUS", + "PAYMENT_METHOD_STATUS", + "CARD_INFO", + "PAYMENT_METHOD_INFO_BILLING_ADDRESS", +] + +let eventToString = event => { + switch event { + | FORM_STATUS => "FORM_STATUS" + | PAYMENT_METHOD_STATUS => "PAYMENT_METHOD_STATUS" + | CARD_INFO => "CARD_INFO" + | PAYMENT_METHOD_INFO_BILLING_ADDRESS => "PAYMENT_METHOD_INFO_BILLING_ADDRESS" + | UNKNOWN_EVENT => "UNKNOWN_EVENT" + } +} + +let stringToEvent = (str, key) => + switch str { + | "FORM_STATUS" => FORM_STATUS + | "PAYMENT_METHOD_STATUS" => PAYMENT_METHOD_STATUS + | "CARD_INFO" => CARD_INFO + | "PAYMENT_METHOD_INFO_BILLING_ADDRESS" => PAYMENT_METHOD_INFO_BILLING_ADDRESS + | _ => { + str->unknownPropValueWarning(validSubscriptionEvents, key) + UNKNOWN_EVENT + } + } + +let getSubscriptionEvents = (dict, key) => { + let context = `options.${key}` + let subscriptionList = + dict + ->Dict.get(key) + ->Option.flatMap(JSON.Decode.array) + ->Option.getOr([]) + + let mappedSubscriptionEvents = + subscriptionList + ->Array.map(item => + switch JSON.Decode.string(item) { + | Some(str) => stringToEvent(str, context) + | None => UNKNOWN_EVENT + } + ) + ->Array.filter(opt => opt != UNKNOWN_EVENT) + + if mappedSubscriptionEvents->Array.length === 0 { + None + } else { + Some(mappedSubscriptionEvents) + } +} + +let shouldEmitEvent = (subscriptionEvents: option>, event: events) => { + switch subscriptionEvents { + | None => true // No subscription list provided, emit all events (backward compatible) + | Some(events) => events->Array.includes(event) // Only emit if event is in the subscription list + } +} + +type formStatusValue = + | Empty + | Filling + | Complete + +let formStatusValueToString = (status: formStatusValue): string => { + switch status { + | Empty => "EMPTY" + | Filling => "FILLING" + | Complete => "COMPLETE" + } +} + +type formStatus = {status: string} // "EMPTY" | "FILLING" | "COMPLETE" + +type cardInfo = { + bin: string, + last4: string, + brand: string, + expiryMonth: string, + expiryYear: string, + formattedExpiry: string, + isCardNumberComplete: bool, + isCvcComplete: bool, + isExpiryComplete: bool, + isCardNumberValid: bool, + isExpiryValid: bool, + isCvcValid: bool, + isSavedCard: bool, +} + +type paymentMethodStatus = { + paymentMethod: string, + paymentMethodType: string, + isSavedPaymentMethod: bool, + isOneClickWallet: bool, +} + +type billingAddress = { + country: string, + state: string, + postalCode: string, +} + +let createFormStatusPayload = (~status) => { + let payloadDict = Dict.make() + payloadDict->Dict.set("status", status->JSON.Encode.string) + + [ + ("elementType", "payment"->JSON.Encode.string), + ("eventName", "FORM_STATUS"->JSON.Encode.string), + ("payload", payloadDict->JSON.Encode.object), + ] +} + +let createCardInfoPayload = ( + ~bin, + ~last4, + ~brand, + ~expiryMonth, + ~expiryYear, + ~formattedExpiry, + ~isCardNumberComplete, + ~isCvcComplete, + ~isExpiryComplete, + ~isCardNumberValid, + ~isExpiryValid, + ~isCvcValid, + ~isSavedCard, +) => { + let payloadDict = Dict.make() + payloadDict->Dict.set("bin", bin->JSON.Encode.string) + payloadDict->Dict.set("last4", last4->JSON.Encode.string) + payloadDict->Dict.set("brand", brand->JSON.Encode.string) + payloadDict->Dict.set("expiryMonth", expiryMonth->JSON.Encode.string) + payloadDict->Dict.set("expiryYear", expiryYear->JSON.Encode.string) + payloadDict->Dict.set("formattedExpiry", formattedExpiry->JSON.Encode.string) + payloadDict->Dict.set("isCardNumberComplete", isCardNumberComplete->JSON.Encode.bool) + payloadDict->Dict.set("isCvcComplete", isCvcComplete->JSON.Encode.bool) + payloadDict->Dict.set("isExpiryComplete", isExpiryComplete->JSON.Encode.bool) + payloadDict->Dict.set("isCardNumberValid", isCardNumberValid->JSON.Encode.bool) + payloadDict->Dict.set("isExpiryValid", isExpiryValid->JSON.Encode.bool) + payloadDict->Dict.set("isCvcValid", isCvcValid->JSON.Encode.bool) + payloadDict->Dict.set("isSavedCard", isSavedCard->JSON.Encode.bool) + + [ + ("elementType", "payment"->JSON.Encode.string), + ("eventName", "CARD_INFO"->JSON.Encode.string), + ("payload", payloadDict->JSON.Encode.object), + ] +} + +let createPaymentMethodStatusPayload = ( + ~paymentMethod, + ~paymentMethodType, + ~isSavedPaymentMethod, + ~isOneClickWallet=false, +) => { + let payloadDict = Dict.make() + payloadDict->Dict.set("paymentMethod", paymentMethod->JSON.Encode.string) + payloadDict->Dict.set("paymentMethodType", paymentMethodType->JSON.Encode.string) + payloadDict->Dict.set("isSavedPaymentMethod", isSavedPaymentMethod->JSON.Encode.bool) + payloadDict->Dict.set("isOneClickWallet", isOneClickWallet->JSON.Encode.bool) + + [ + ("elementType", "payment"->JSON.Encode.string), + ("eventName", "PAYMENT_METHOD_STATUS"->JSON.Encode.string), + ("payload", payloadDict->JSON.Encode.object), + ] +} + +let createBillingAddressPayload = (~country, ~state, ~postalCode) => { + let payloadDict = Dict.make() + payloadDict->Dict.set("country", country->JSON.Encode.string) + payloadDict->Dict.set("state", state->JSON.Encode.string) + payloadDict->Dict.set("postalCode", postalCode->JSON.Encode.string) + + [ + ("elementType", "payment"->JSON.Encode.string), + ("eventName", "PAYMENT_METHOD_INFO_BILLING_ADDRESS"->JSON.Encode.string), + ("payload", payloadDict->JSON.Encode.object), + ] +} diff --git a/src/Utilities/DynamicFieldsUtils.res b/src/Utilities/DynamicFieldsUtils.res index ab309e69e..6a90fbc7a 100644 --- a/src/Utilities/DynamicFieldsUtils.res +++ b/src/Utilities/DynamicFieldsUtils.res @@ -195,6 +195,7 @@ let useRequiredFieldsEmptyAndValid = ( ~cvcNumber, ~isSavedCardFlow, ) => { + let options = Recoil.useRecoilValueFromAtom(optionAtom) let email = Recoil.useRecoilValueFromAtom(userEmailAddress) let vpaId = Recoil.useRecoilValueFromAtom(userVpaId) let pixCNPJ = Recoil.useRecoilValueFromAtom(userPixCNPJ) @@ -379,6 +380,7 @@ let useRequiredFieldsEmptyAndValid = ( | (Some(cardValid), Some(expiryValid), Some(cvcValid)) => CardUtils.emitIsFormReadyForSubmission( cardValid && expiryValid && cvcValid && areRequiredFieldsValid, + ~subscriptionEvents=options.subscriptionEvents, ) | _ => () } diff --git a/src/Utilities/PaymentUtils.res b/src/Utilities/PaymentUtils.res index 6b46633b2..36bff2caf 100644 --- a/src/Utilities/PaymentUtils.res +++ b/src/Utilities/PaymentUtils.res @@ -611,41 +611,46 @@ let emitPaymentMethodInfo = ( ~state="", ~pinCode="", ~isSavedPaymentMethod=false, + ~subscriptionEvents, ) => { - let baseCardsFields = [ - ("cardBrand", cardBrand->CardUtils.getCardStringFromType->JSON.Encode.string), - ("cardLast4", cardLast4->JSON.Encode.string), - ("cardBin", cardBin->JSON.Encode.string), - ("cardExpiryMonth", cardExpiryMonth->JSON.Encode.string), - ("cardExpiryYear", cardExpiryYear->JSON.Encode.string), - ] + if ( + SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + ) { + let baseCardsFields = [ + ("cardBrand", cardBrand->CardUtils.getCardStringFromType->JSON.Encode.string), + ("cardLast4", cardLast4->JSON.Encode.string), + ("cardBin", cardBin->JSON.Encode.string), + ("cardExpiryMonth", cardExpiryMonth->JSON.Encode.string), + ("cardExpiryYear", cardExpiryYear->JSON.Encode.string), + ] - let baseAddressFields = [ - ("country", country->JSON.Encode.string), - ("state", state->JSON.Encode.string), - ("pincode", pinCode->JSON.Encode.string), - ] + let baseAddressFields = [ + ("country", country->JSON.Encode.string), + ("state", state->JSON.Encode.string), + ("pincode", pinCode->JSON.Encode.string), + ] - let baseSavedPaymentField = [("isSavedPaymentMethod", isSavedPaymentMethod->JSON.Encode.bool)] + let baseSavedPaymentField = [("isSavedPaymentMethod", isSavedPaymentMethod->JSON.Encode.bool)] - let basePaymentInfoFields = switch paymentMethod { - | "card" => [("paymentMethod", paymentMethod->JSON.Encode.string)] - | _ => [ - ("paymentMethod", paymentMethod->JSON.Encode.string), - ("paymentMethodType", paymentMethodType->JSON.Encode.string), - ] - } + let basePaymentInfoFields = switch paymentMethod { + | "card" => [("paymentMethod", paymentMethod->JSON.Encode.string)] + | _ => [ + ("paymentMethod", paymentMethod->JSON.Encode.string), + ("paymentMethodType", paymentMethodType->JSON.Encode.string), + ] + } - let msg = if cardBrand === CardUtils.NOTFOUND || paymentMethod !== "card" { - [...basePaymentInfoFields, ...baseAddressFields] - } else { - [...basePaymentInfoFields, ...baseAddressFields, ...baseCardsFields] - } + let msg = if cardBrand === CardUtils.NOTFOUND || paymentMethod !== "card" { + [...basePaymentInfoFields, ...baseAddressFields] + } else { + [...basePaymentInfoFields, ...baseAddressFields, ...baseCardsFields] + } - let finalMsg = - msg->Array.filter(((_, value)) => value->JSON.Decode.string->Option.getOr("") != "") + let finalMsg = + msg->Array.filter(((_, value)) => value->JSON.Decode.string->Option.getOr("") != "") - emitMessage(finalMsg->Array.concat(baseSavedPaymentField)->Dict.fromArray) + emitMessage(finalMsg->Array.concat(baseSavedPaymentField)->Dict.fromArray) + } } type nonPiiAdderessData = { @@ -666,6 +671,42 @@ let useNonPiiAddressData = () => { } } +let getPaymentMethodAndType = ( + ~paymentMethodName: string, + ~paymentMethods: array, + ~logger: HyperLoggerTypes.loggerMake, +) => { + if paymentMethodName->String.includes("_debit") { + Some(("bank_debit", paymentMethodName)) + } else if paymentMethodName->String.includes("_transfer") { + Some(("bank_transfer", paymentMethodName)) + } else if paymentMethodName === "card" { + Some(("card", "debit")) + } else { + let finalOptionalPaymentMethodTypeValue = + paymentMethods + ->Array.filter(paymentMethodData => + paymentMethodData.payment_method_types + ->Array.filter(paymentMethodType => + paymentMethodType.payment_method_type === paymentMethodName + ) + ->Array.length > 0 + ) + ->Array.get(0) + + switch finalOptionalPaymentMethodTypeValue { + | Some(finalPaymentMethodType) => + Some((finalPaymentMethodType.payment_method, paymentMethodName)) + | None => + logger.setLogError( + ~value="Payment method type not found", + ~eventName=PAYMENT_METHOD_TYPE_DETECTION_FAILED, + ) + None + } + } +} + let useEmitPaymentMethodInfo = ( ~paymentMethodName, ~paymentMethods: array, @@ -674,6 +715,7 @@ let useEmitPaymentMethodInfo = ( ) => { let loggerState = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom) let {country, state, pinCode} = useNonPiiAddressData() + let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) let {cardNumber, cardBrand} = cardProps let cardBin = cardNumber->CardUtils.getCardBin @@ -697,49 +739,25 @@ let useEmitPaymentMethodInfo = ( ~country, ~state, ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, ) } else { - emitPaymentMethodInfo(~paymentMethod, ~paymentMethodType, ~country, ~state, ~pinCode) + emitPaymentMethodInfo( + ~paymentMethod, + ~paymentMethodType, + ~country, + ~state, + ~pinCode, + ~subscriptionEvents=options.subscriptionEvents, + ) } } React.useEffect(() => { - if paymentMethodName->String.includes("_debit") { - emitPaymentMethodInfoWrapper( - ~paymentMethod="bank_debit", - ~paymentMethodType=paymentMethodName, - ) - } else if paymentMethodName->String.includes("_transfer") { - emitPaymentMethodInfoWrapper( - ~paymentMethod="bank_transfer", - ~paymentMethodType=paymentMethodName, - ) - } else if paymentMethodName === "card" { - emitPaymentMethodInfoWrapper(~paymentMethod="card", ~paymentMethodType="debit") - } else { - let finalOptionalPaymentMethodTypeValue = - paymentMethods - ->Array.filter(paymentMethodData => - paymentMethodData.payment_method_types - ->Array.filter( - paymentMethodType => paymentMethodType.payment_method_type === paymentMethodName, - ) - ->Array.length > 0 - ) - ->Array.get(0) - - switch finalOptionalPaymentMethodTypeValue { - | Some(finalPaymentMethodType) => - emitPaymentMethodInfoWrapper( - ~paymentMethod=finalPaymentMethodType.payment_method, - ~paymentMethodType=paymentMethodName, - ) - | None => - loggerState.setLogError( - ~value="Payment method type not found", - ~eventName=PAYMENT_METHOD_TYPE_DETECTION_FAILED, - ) - } + switch getPaymentMethodAndType(~paymentMethodName, ~paymentMethods, ~logger=loggerState) { + | Some((paymentMethod, paymentMethodType)) => + emitPaymentMethodInfoWrapper(~paymentMethod, ~paymentMethodType) + | None => () } None diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index 4d1e7a674..9e76ba5e7 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -779,19 +779,24 @@ let handlePostMessageEvents = ( ~paymentType, ~loggerState: HyperLoggerTypes.loggerMake, ~savedMethod=false, + ~subscriptionEvents, ) => { - if complete && paymentType !== "" { - let value = `Payment Data Filled: ${savedMethod - ? "Saved Payment Method" - : "New Payment Method"}` - loggerState.setLogInfo(~value, ~eventName=PAYMENT_DATA_FILLED, ~paymentMethod=paymentType) + if ( + SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + ) { + if complete && paymentType !== "" { + let value = `Payment Data Filled: ${savedMethod + ? "Saved Payment Method" + : "New Payment Method"}` + loggerState.setLogInfo(~value, ~eventName=PAYMENT_DATA_FILLED, ~paymentMethod=paymentType) + } + messageParentWindow([ + ("elementType", "payment"->JSON.Encode.string), + ("complete", complete->JSON.Encode.bool), + ("empty", empty->JSON.Encode.bool), + ("value", [("type", paymentType->JSON.Encode.string)]->getJsonFromArrayOfJson), + ]) } - messageParentWindow([ - ("elementType", "payment"->JSON.Encode.string), - ("complete", complete->JSON.Encode.bool), - ("empty", empty->JSON.Encode.bool), - ("value", [("type", paymentType->JSON.Encode.string)]->getJsonFromArrayOfJson), - ]) } let onlyDigits = str => str->String.replaceRegExp(%re(`/\D/g`), "") @@ -1449,7 +1454,24 @@ let eventHandlerFunc = ( | OneClickConfirmPayment | Blur => switch eventHandler { - | Some(eH) => eH(Some(ev.data)) + | Some(eH) => { + let data = ev.data->Identity.anyTypeToJson + let dataDict = data->getDictFromJson + + // Check if this is a subscription event by looking for eventName key + let isSubscriptionEvent = dataDict->Dict.get("eventName")->Option.isSome + + // For subscription events, remove elementType from the data before sending to merchant + let sanitizedData = if isSubscriptionEvent { + let dataCopy = dataDict->deepCopyDict + dataCopy->Dict.delete("elementType") + dataCopy->JSON.Encode.object + } else { + data + } + + eH(Some(sanitizedData)) + } | None => () } | _ => () diff --git a/src/hyper-loader/Elements.res b/src/hyper-loader/Elements.res index 77bfc7ad7..070bffd77 100644 --- a/src/hyper-loader/Elements.res +++ b/src/hyper-loader/Elements.res @@ -401,16 +401,22 @@ let make = ( ), ]->Dict.fromArray - let widgetOptions = - [ - ("clientSecret", clientSecret->JSON.Encode.string), - ("sdkAuthorization", sdkAuthorization->JSON.Encode.string), - ("appearance", appearance), - ("locale", locale), - ("loader", loader), - ("fonts", fonts), - ("redirectionFlags", redirectionFlagsDict->JSON.Encode.object), - ]->getJsonFromArrayOfJson + let widgetOptions = [ + ("clientSecret", clientSecret->JSON.Encode.string), + ("sdkAuthorization", sdkAuthorization->JSON.Encode.string), + ("appearance", appearance), + ("locale", locale), + ("loader", loader), + ("fonts", fonts), + ("redirectionFlags", redirectionFlagsDict->JSON.Encode.object), + ( + "subscriptionEvents", + newOptions + ->getDictFromJson + ->Dict.get("subscriptionEvents") + ->Option.getOr(JSON.Encode.null), + ), + ]->getJsonFromArrayOfJson let message = [ ( "paymentElementCreate", diff --git a/src/hyper-loader/LoaderPaymentElement.res b/src/hyper-loader/LoaderPaymentElement.res index 768e5eed3..7b4982b44 100644 --- a/src/hyper-loader/LoaderPaymentElement.res +++ b/src/hyper-loader/LoaderPaymentElement.res @@ -101,7 +101,7 @@ let make = ( (ev: Types.event) => { if ev.key === "Escape" { switch eventHandler { - | Some(eH) => eH(Some(ev.data)) + | Some(eH) => eH(Some(ev.data->Identity.anyTypeToJson)) | None => () } } diff --git a/src/hyper-loader/Types.res b/src/hyper-loader/Types.res index 4a901a9c5..30f92130a 100644 --- a/src/hyper-loader/Types.res +++ b/src/hyper-loader/Types.res @@ -26,7 +26,7 @@ module This = { } type paymentElement = { - on: (string, option => unit>) => unit, + on: (string, option => unit>) => unit, collapse: unit => unit, blur: unit => unit, update: JSON.t => unit, From 11616ea574fe257ea65bbb68ccc7ddd14799d3e7 Mon Sep 17 00:00:00 2001 From: "aritro.ghosh" Date: Tue, 17 Mar 2026 11:15:47 +0530 Subject: [PATCH 2/3] refactor: used types and functions from shared codebase --- src/CardUtils.res | 12 +++- src/Hooks/SubscriptionEventHooks.res | 42 ++++++++++---- src/Payments/PaypalSDKHelpers.res | 2 +- src/Types/PaymentType.res | 2 +- src/Types/SubscriptionEventTypes.res | 83 ++++++++++------------------ src/Utilities/PaymentUtils.res | 6 +- src/Utilities/Utils.res | 6 +- 7 files changed, 80 insertions(+), 73 deletions(-) diff --git a/src/CardUtils.res b/src/CardUtils.res index 675d7f3d2..203afe4e5 100644 --- a/src/CardUtils.res +++ b/src/CardUtils.res @@ -745,7 +745,11 @@ let getCardBrandInvalidError = (~cardBrand, ~localeString: LocaleStringTypes.loc let emitExpiryDate = (formattedExpiry, ~subscriptionEvents) => { if ( - SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.UnknownEvent, + ) ) { Utils.messageParentWindow([("expiryDate", formattedExpiry->JSON.Encode.string)]) } @@ -753,7 +757,11 @@ let emitExpiryDate = (formattedExpiry, ~subscriptionEvents) => { let emitIsFormReadyForSubmission = (isFormReadyForSubmission, ~subscriptionEvents) => { if ( - SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.UnknownEvent, + ) ) { Utils.messageParentWindow([ ("isFormReadyForSubmission", isFormReadyForSubmission->JSON.Encode.bool), diff --git a/src/Hooks/SubscriptionEventHooks.res b/src/Hooks/SubscriptionEventHooks.res index 1265f4499..552dcbe85 100644 --- a/src/Hooks/SubscriptionEventHooks.res +++ b/src/Hooks/SubscriptionEventHooks.res @@ -16,7 +16,13 @@ let emitCardInfo = ( ~isCvcValid, ~isSavedCard, ) => { - if shouldEmitEvent(subscriptionEvents, CARD_INFO) { + if ( + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.PaymentMethodInfoCard, + ) + ) { let payload = createCardInfoPayload( ~bin, ~last4, @@ -43,7 +49,13 @@ let emitPaymentMethodStatus = ( ~isSavedPaymentMethod, ~isOneClickWallet=false, ) => { - if shouldEmitEvent(subscriptionEvents, PAYMENT_METHOD_STATUS) { + if ( + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.PaymentMethodStatus, + ) + ) { let payload = createPaymentMethodStatusPayload( ~paymentMethod, ~paymentMethodType, @@ -55,7 +67,13 @@ let emitPaymentMethodStatus = ( } let emitBillingAddress = (~subscriptionEvents, ~country, ~state, ~postalCode) => { - if shouldEmitEvent(subscriptionEvents, PAYMENT_METHOD_INFO_BILLING_ADDRESS) { + if ( + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.PaymentMethodInfoBillingAddress, + ) + ) { let payload = createBillingAddressPayload(~country, ~state, ~postalCode) Utils.messageParentWindow(payload) } @@ -67,15 +85,15 @@ let useFormStatus = (~empty: bool, ~complete: bool, ~isOneClickWallet: bool=fals React.useEffect(() => { if !isOneClickWallet { - let formStatusValue = if complete { - Complete - } else if empty { - Empty - } else { - Filling - } - if shouldEmitEvent(subscriptionEvents, FORM_STATUS) { - let payload = createFormStatusPayload(~status=formStatusValue->formStatusValueToString) + let formStatusValue = PaymentEventData.computeFormStatus(~isComplete=complete, ~isEmpty=empty) + if ( + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.FormStatus, + ) + ) { + let payload = SubscriptionEventTypes.createFormStatusPayload(~status=formStatusValue) Utils.messageParentWindow(payload) } } diff --git a/src/Payments/PaypalSDKHelpers.res b/src/Payments/PaypalSDKHelpers.res index b7fca1da0..eafada960 100644 --- a/src/Payments/PaypalSDKHelpers.res +++ b/src/Payments/PaypalSDKHelpers.res @@ -28,7 +28,7 @@ let loadPaypalSDK = ( ~isTestMode=false, ~nonPiiAdderessData: PaymentUtils.nonPiiAdderessData, ~sdkAuthorization, - ~subscriptionEvents: option>, + ~subscriptionEvents: option>, ) => { open Promise diff --git a/src/Types/PaymentType.res b/src/Types/PaymentType.res index bbad5f732..b3c9afea0 100644 --- a/src/Types/PaymentType.res +++ b/src/Types/PaymentType.res @@ -207,7 +207,7 @@ type options = { business: business, customerPaymentMethods: savedCardsLoadState, paymentMethodOrder: option>, - subscriptionEvents: option>, + subscriptionEvents: option>, displaySavedPaymentMethodsCheckbox: bool, displaySavedPaymentMethods: bool, savedPaymentMethodsCheckboxCheckedByDefault: bool, diff --git a/src/Types/SubscriptionEventTypes.res b/src/Types/SubscriptionEventTypes.res index bb0d85b91..996a21300 100644 --- a/src/Types/SubscriptionEventTypes.res +++ b/src/Types/SubscriptionEventTypes.res @@ -1,38 +1,22 @@ open ErrorUtils - -type events = - | FORM_STATUS - | PAYMENT_METHOD_STATUS - | CARD_INFO - | PAYMENT_METHOD_INFO_BILLING_ADDRESS - | UNKNOWN_EVENT +open PaymentEventTypes let validSubscriptionEvents = [ - "FORM_STATUS", + "PAYMENT_METHOD_INFO_CARD", "PAYMENT_METHOD_STATUS", - "CARD_INFO", + "FORM_STATUS", "PAYMENT_METHOD_INFO_BILLING_ADDRESS", ] -let eventToString = event => { - switch event { - | FORM_STATUS => "FORM_STATUS" - | PAYMENT_METHOD_STATUS => "PAYMENT_METHOD_STATUS" - | CARD_INFO => "CARD_INFO" - | PAYMENT_METHOD_INFO_BILLING_ADDRESS => "PAYMENT_METHOD_INFO_BILLING_ADDRESS" - | UNKNOWN_EVENT => "UNKNOWN_EVENT" - } -} - let stringToEvent = (str, key) => switch str { - | "FORM_STATUS" => FORM_STATUS - | "PAYMENT_METHOD_STATUS" => PAYMENT_METHOD_STATUS - | "CARD_INFO" => CARD_INFO - | "PAYMENT_METHOD_INFO_BILLING_ADDRESS" => PAYMENT_METHOD_INFO_BILLING_ADDRESS + | "PAYMENT_METHOD_INFO_CARD" => PaymentMethodInfoCard + | "PAYMENT_METHOD_STATUS" => PaymentMethodStatus + | "FORM_STATUS" => FormStatus + | "PAYMENT_METHOD_INFO_BILLING_ADDRESS" => PaymentMethodInfoBillingAddress | _ => { str->unknownPropValueWarning(validSubscriptionEvents, key) - UNKNOWN_EVENT + UnknownEvent } } @@ -49,10 +33,10 @@ let getSubscriptionEvents = (dict, key) => { ->Array.map(item => switch JSON.Decode.string(item) { | Some(str) => stringToEvent(str, context) - | None => UNKNOWN_EVENT + | None => UnknownEvent } ) - ->Array.filter(opt => opt != UNKNOWN_EVENT) + ->Array.filter(opt => opt != UnknownEvent) if mappedSubscriptionEvents->Array.length === 0 { None @@ -68,21 +52,6 @@ let shouldEmitEvent = (subscriptionEvents: option>, event: events) } } -type formStatusValue = - | Empty - | Filling - | Complete - -let formStatusValueToString = (status: formStatusValue): string => { - switch status { - | Empty => "EMPTY" - | Filling => "FILLING" - | Complete => "COMPLETE" - } -} - -type formStatus = {status: string} // "EMPTY" | "FILLING" | "COMPLETE" - type cardInfo = { bin: string, last4: string, @@ -113,13 +82,13 @@ type billingAddress = { } let createFormStatusPayload = (~status) => { - let payloadDict = Dict.make() - payloadDict->Dict.set("status", status->JSON.Encode.string) + let formStatusEvent = PaymentEventData.buildFormStatusEvent(~status) + let payload = formStatusEvent->PaymentEventData.formStatusEventToJson [ ("elementType", "payment"->JSON.Encode.string), ("eventName", "FORM_STATUS"->JSON.Encode.string), - ("payload", payloadDict->JSON.Encode.object), + ("payload", payload), ] } @@ -166,28 +135,32 @@ let createPaymentMethodStatusPayload = ( ~isSavedPaymentMethod, ~isOneClickWallet=false, ) => { - let payloadDict = Dict.make() - payloadDict->Dict.set("paymentMethod", paymentMethod->JSON.Encode.string) - payloadDict->Dict.set("paymentMethodType", paymentMethodType->JSON.Encode.string) - payloadDict->Dict.set("isSavedPaymentMethod", isSavedPaymentMethod->JSON.Encode.bool) - payloadDict->Dict.set("isOneClickWallet", isOneClickWallet->JSON.Encode.bool) + let paymentMethodStatusEvent = PaymentEventData.buildPaymentMethodStatusEvent( + ~paymentMethod, + ~paymentMethodType, + ~isSavedPaymentMethod, + ~isOneClickWallet, + ) + let payload = paymentMethodStatusEvent->PaymentEventData.paymentMethodStatusEventToJson [ ("elementType", "payment"->JSON.Encode.string), ("eventName", "PAYMENT_METHOD_STATUS"->JSON.Encode.string), - ("payload", payloadDict->JSON.Encode.object), + ("payload", payload), ] } let createBillingAddressPayload = (~country, ~state, ~postalCode) => { - let payloadDict = Dict.make() - payloadDict->Dict.set("country", country->JSON.Encode.string) - payloadDict->Dict.set("state", state->JSON.Encode.string) - payloadDict->Dict.set("postalCode", postalCode->JSON.Encode.string) + let paymentMethodInfoAddress = PaymentEventData.buildPaymentMethodInfoAddress( + ~country, + ~state, + ~postalCode, + ) + let payload = paymentMethodInfoAddress->PaymentEventData.paymentMethodInfoAddressToJson [ ("elementType", "payment"->JSON.Encode.string), ("eventName", "PAYMENT_METHOD_INFO_BILLING_ADDRESS"->JSON.Encode.string), - ("payload", payloadDict->JSON.Encode.object), + ("payload", payload), ] } diff --git a/src/Utilities/PaymentUtils.res b/src/Utilities/PaymentUtils.res index 36bff2caf..28d829078 100644 --- a/src/Utilities/PaymentUtils.res +++ b/src/Utilities/PaymentUtils.res @@ -614,7 +614,11 @@ let emitPaymentMethodInfo = ( ~subscriptionEvents, ) => { if ( - SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.UnknownEvent, + ) ) { let baseCardsFields = [ ("cardBrand", cardBrand->CardUtils.getCardStringFromType->JSON.Encode.string), diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index 9e76ba5e7..8167018dd 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -782,7 +782,11 @@ let handlePostMessageEvents = ( ~subscriptionEvents, ) => { if ( - SubscriptionEventTypes.shouldEmitEvent(subscriptionEvents, SubscriptionEventTypes.UNKNOWN_EVENT) + subscriptionEvents->Option.isNone || + PaymentEventData.shouldEmitEvent( + ~subscribedEvents=subscriptionEvents->Option.getOr([]), + ~eventType=PaymentEventTypes.UnknownEvent, + ) ) { if complete && paymentType !== "" { let value = `Payment Data Filled: ${savedMethod From 48c5d06e5aba5521a0a05c4bcc7971759ed60ad8 Mon Sep 17 00:00:00 2001 From: "aritro.ghosh" Date: Tue, 17 Mar 2026 13:18:52 +0530 Subject: [PATCH 3/3] refactor: used types and functions from shared codebase --- src/Components/SavedMethods.res | 17 +----- src/Hooks/SubscriptionEventHooks.res | 53 +++++------------- src/Payments/CardPayment.res | 27 +++------ src/Types/SubscriptionEventTypes.res | 82 ++++------------------------ 4 files changed, 34 insertions(+), 145 deletions(-) diff --git a/src/Components/SavedMethods.res b/src/Components/SavedMethods.res index a20f41f7c..532194e2e 100644 --- a/src/Components/SavedMethods.res +++ b/src/Components/SavedMethods.res @@ -167,29 +167,18 @@ let make = ( React.useEffect(() => { if isCardPaymentMethod && !isUnknownPaymentMethod { let card = customerMethod.card - SubscriptionEventHooks.emitCardInfo( - ~subscriptionEvents=options.subscriptionEvents, + let cardInfo = PaymentEventData.buildCardInfoFromSavedCard( ~bin=card.cardBin, ~last4=card.last4Digits, ~brand=card.scheme->Option.getOr(""), ~expiryMonth=card.expiryMonth, ~expiryYear=card.expiryYear, - ~formattedExpiry=`${card.expiryMonth}${String.substring( - ~start=2, - ~end=4, - card.expiryYear, - )}`->CardValidations.formatCardExpiryNumber, - ~isCardNumberComplete=true, ~isCvcComplete=complete, - ~isExpiryComplete=true, - ~isCardNumberValid=true, - ~isExpiryValid=true, - ~isCvcValid=isCVCValid->Option.getOr(false), - ~isSavedCard=true, ) + SubscriptionEventHooks.emitCardInfo(~subscriptionEvents=options.subscriptionEvents, ~cardInfo) } None - }, (customerMethod, isCardPaymentMethod, isUnknownPaymentMethod, complete, isCVCValid)) + }, (customerMethod, isCardPaymentMethod, isUnknownPaymentMethod, complete)) GooglePayHelpers.useHandleGooglePayResponse(~connectors=[], ~intent, ~isSavedMethodsFlow=true) diff --git a/src/Hooks/SubscriptionEventHooks.res b/src/Hooks/SubscriptionEventHooks.res index 552dcbe85..be3b9004f 100644 --- a/src/Hooks/SubscriptionEventHooks.res +++ b/src/Hooks/SubscriptionEventHooks.res @@ -1,43 +1,16 @@ open SubscriptionEventTypes +open PaymentEventData +open PaymentEventTypes -let emitCardInfo = ( - ~subscriptionEvents, - ~bin, - ~last4, - ~brand, - ~expiryMonth, - ~expiryYear, - ~formattedExpiry, - ~isCardNumberComplete, - ~isCvcComplete, - ~isExpiryComplete, - ~isCardNumberValid, - ~isExpiryValid, - ~isCvcValid, - ~isSavedCard, -) => { +let emitCardInfo = (~subscriptionEvents, ~cardInfo: cardInfo) => { if ( subscriptionEvents->Option.isNone || - PaymentEventData.shouldEmitEvent( + shouldEmitEvent( ~subscribedEvents=subscriptionEvents->Option.getOr([]), - ~eventType=PaymentEventTypes.PaymentMethodInfoCard, + ~eventType=PaymentMethodInfoCard, ) ) { - let payload = createCardInfoPayload( - ~bin, - ~last4, - ~brand, - ~expiryMonth, - ~expiryYear, - ~formattedExpiry, - ~isCardNumberComplete, - ~isCvcComplete, - ~isExpiryComplete, - ~isCardNumberValid, - ~isExpiryValid, - ~isCvcValid, - ~isSavedCard, - ) + let payload = createCardInfoPayload(cardInfo) Utils.messageParentWindow(payload) } } @@ -51,9 +24,9 @@ let emitPaymentMethodStatus = ( ) => { if ( subscriptionEvents->Option.isNone || - PaymentEventData.shouldEmitEvent( + shouldEmitEvent( ~subscribedEvents=subscriptionEvents->Option.getOr([]), - ~eventType=PaymentEventTypes.PaymentMethodStatus, + ~eventType=PaymentMethodStatus, ) ) { let payload = createPaymentMethodStatusPayload( @@ -69,9 +42,9 @@ let emitPaymentMethodStatus = ( let emitBillingAddress = (~subscriptionEvents, ~country, ~state, ~postalCode) => { if ( subscriptionEvents->Option.isNone || - PaymentEventData.shouldEmitEvent( + shouldEmitEvent( ~subscribedEvents=subscriptionEvents->Option.getOr([]), - ~eventType=PaymentEventTypes.PaymentMethodInfoBillingAddress, + ~eventType=PaymentMethodInfoBillingAddress, ) ) { let payload = createBillingAddressPayload(~country, ~state, ~postalCode) @@ -85,12 +58,12 @@ let useFormStatus = (~empty: bool, ~complete: bool, ~isOneClickWallet: bool=fals React.useEffect(() => { if !isOneClickWallet { - let formStatusValue = PaymentEventData.computeFormStatus(~isComplete=complete, ~isEmpty=empty) + let formStatusValue = computeFormStatus(~isComplete=complete, ~isEmpty=empty) if ( subscriptionEvents->Option.isNone || - PaymentEventData.shouldEmitEvent( + shouldEmitEvent( ~subscribedEvents=subscriptionEvents->Option.getOr([]), - ~eventType=PaymentEventTypes.FormStatus, + ~eventType=FormStatus, ) ) { let payload = SubscriptionEventTypes.createFormStatusPayload(~status=formStatusValue) diff --git a/src/Payments/CardPayment.res b/src/Payments/CardPayment.res index 939ce2973..94aa30b76 100644 --- a/src/Payments/CardPayment.res +++ b/src/Payments/CardPayment.res @@ -141,29 +141,16 @@ let make = ( UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="card") React.useEffect(() => { - let (month, year) = CardUtils.getExpiryDates(cardExpiry) - let isCardNumberComplete = cardNumber->String.length > 0 && isCardValid->Option.getOr(false) - let isExpiryComplete = cardExpiry->String.length > 0 && isExpiryValid->Option.getOr(false) - let isCvcComplete = cvcNumber->String.length > 0 && isCVCValid->Option.getOr(false) - - SubscriptionEventHooks.emitCardInfo( - ~subscriptionEvents=options.subscriptionEvents, - ~bin=cardNumber->CardUtils.getCardBin, - ~last4=cardNumber->CardUtils.getCardLast4, + let cardInfo = PaymentEventData.buildCardInfo( + ~cardNumber, + ~expiry=cardExpiry, + ~cvc=cvcNumber, ~brand=cardBrand, - ~expiryMonth=month, - ~expiryYear=isExpiryComplete ? year : "", // Return empty year if expiry incomplete since getExpiryDates adds 20xx prefix by default - ~formattedExpiry=cardExpiry, - ~isCardNumberComplete, - ~isCvcComplete, - ~isExpiryComplete, - ~isCardNumberValid=isCardValid->Option.getOr(false), - ~isExpiryValid=isExpiryValid->Option.getOr(false), - ~isCvcValid=isCVCValid->Option.getOr(false), - ~isSavedCard=false, ) + + SubscriptionEventHooks.emitCardInfo(~subscriptionEvents=options.subscriptionEvents, ~cardInfo) None - }, (cardNumber, cardExpiry, cvcNumber, isCardValid, isExpiryValid, isCVCValid, cardBrand)) + }, (cardNumber, cardExpiry, cvcNumber, cardBrand)) React.useEffect(() => { setComplete(_ => complete) diff --git a/src/Types/SubscriptionEventTypes.res b/src/Types/SubscriptionEventTypes.res index 996a21300..d0c039e68 100644 --- a/src/Types/SubscriptionEventTypes.res +++ b/src/Types/SubscriptionEventTypes.res @@ -45,29 +45,6 @@ let getSubscriptionEvents = (dict, key) => { } } -let shouldEmitEvent = (subscriptionEvents: option>, event: events) => { - switch subscriptionEvents { - | None => true // No subscription list provided, emit all events (backward compatible) - | Some(events) => events->Array.includes(event) // Only emit if event is in the subscription list - } -} - -type cardInfo = { - bin: string, - last4: string, - brand: string, - expiryMonth: string, - expiryYear: string, - formattedExpiry: string, - isCardNumberComplete: bool, - isCvcComplete: bool, - isExpiryComplete: bool, - isCardNumberValid: bool, - isExpiryValid: bool, - isCvcValid: bool, - isSavedCard: bool, -} - type paymentMethodStatus = { paymentMethod: string, paymentMethodType: string, @@ -80,52 +57,21 @@ type billingAddress = { state: string, postalCode: string, } - -let createFormStatusPayload = (~status) => { - let formStatusEvent = PaymentEventData.buildFormStatusEvent(~status) - let payload = formStatusEvent->PaymentEventData.formStatusEventToJson - +let createCardInfoPayload = (cardInfo: PaymentEventData.cardInfo) => { + let payload = PaymentEventData.cardInfoToJson(cardInfo) [ ("elementType", "payment"->JSON.Encode.string), - ("eventName", "FORM_STATUS"->JSON.Encode.string), + ("eventName", PaymentMethodInfoCard->PaymentEventTypes.eventToString->JSON.Encode.string), ("payload", payload), ] } -let createCardInfoPayload = ( - ~bin, - ~last4, - ~brand, - ~expiryMonth, - ~expiryYear, - ~formattedExpiry, - ~isCardNumberComplete, - ~isCvcComplete, - ~isExpiryComplete, - ~isCardNumberValid, - ~isExpiryValid, - ~isCvcValid, - ~isSavedCard, -) => { - let payloadDict = Dict.make() - payloadDict->Dict.set("bin", bin->JSON.Encode.string) - payloadDict->Dict.set("last4", last4->JSON.Encode.string) - payloadDict->Dict.set("brand", brand->JSON.Encode.string) - payloadDict->Dict.set("expiryMonth", expiryMonth->JSON.Encode.string) - payloadDict->Dict.set("expiryYear", expiryYear->JSON.Encode.string) - payloadDict->Dict.set("formattedExpiry", formattedExpiry->JSON.Encode.string) - payloadDict->Dict.set("isCardNumberComplete", isCardNumberComplete->JSON.Encode.bool) - payloadDict->Dict.set("isCvcComplete", isCvcComplete->JSON.Encode.bool) - payloadDict->Dict.set("isExpiryComplete", isExpiryComplete->JSON.Encode.bool) - payloadDict->Dict.set("isCardNumberValid", isCardNumberValid->JSON.Encode.bool) - payloadDict->Dict.set("isExpiryValid", isExpiryValid->JSON.Encode.bool) - payloadDict->Dict.set("isCvcValid", isCvcValid->JSON.Encode.bool) - payloadDict->Dict.set("isSavedCard", isSavedCard->JSON.Encode.bool) - +let createFormStatusPayload = (~status) => { + let payload = PaymentEventData.formStatusEventToJson(~status) [ ("elementType", "payment"->JSON.Encode.string), - ("eventName", "CARD_INFO"->JSON.Encode.string), - ("payload", payloadDict->JSON.Encode.object), + ("eventName", FormStatus->eventToString->JSON.Encode.string), + ("payload", payload), ] } @@ -135,32 +81,26 @@ let createPaymentMethodStatusPayload = ( ~isSavedPaymentMethod, ~isOneClickWallet=false, ) => { - let paymentMethodStatusEvent = PaymentEventData.buildPaymentMethodStatusEvent( + let payload = PaymentEventData.paymentMethodStatusEventToJson( ~paymentMethod, ~paymentMethodType, ~isSavedPaymentMethod, ~isOneClickWallet, ) - let payload = paymentMethodStatusEvent->PaymentEventData.paymentMethodStatusEventToJson [ ("elementType", "payment"->JSON.Encode.string), - ("eventName", "PAYMENT_METHOD_STATUS"->JSON.Encode.string), + ("eventName", PaymentMethodStatus->eventToString->JSON.Encode.string), ("payload", payload), ] } let createBillingAddressPayload = (~country, ~state, ~postalCode) => { - let paymentMethodInfoAddress = PaymentEventData.buildPaymentMethodInfoAddress( - ~country, - ~state, - ~postalCode, - ) - let payload = paymentMethodInfoAddress->PaymentEventData.paymentMethodInfoAddressToJson + let payload = PaymentEventData.paymentMethodInfoAddressToJson(~country, ~state, ~postalCode) [ ("elementType", "payment"->JSON.Encode.string), - ("eventName", "PAYMENT_METHOD_INFO_BILLING_ADDRESS"->JSON.Encode.string), + ("eventName", PaymentMethodInfoBillingAddress->eventToString->JSON.Encode.string), ("payload", payload), ] }