Skip to content

Commit

Permalink
feat: payment-management added (#392)
Browse files Browse the repository at this point in the history
Co-authored-by: Arush <arush.kapoor@juspay.in>
Co-authored-by: Pritish Budhiraja <pritish.budhiraja@gmail.com>
  • Loading branch information
3 people committed Jul 8, 2024
1 parent 70b920b commit ef1c605
Show file tree
Hide file tree
Showing 27 changed files with 1,044 additions and 91 deletions.
5 changes: 5 additions & 0 deletions public/icons/orca.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion src/App.res
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ let make = () => {
let sessionId = CardUtils.getQueryParamsDictforKey(url.search, "sessionId")
let publishableKey = CardUtils.getQueryParamsDictforKey(url.search, "publishableKey")
let endpoint = CardUtils.getQueryParamsDictforKey(url.search, "endpoint")
<PreMountLoader publishableKey sessionId clientSecret endpoint />
let ephemeralKey = CardUtils.getQueryParamsDictforKey(url.search, "ephemeralKey")
let hyperComponentName =
CardUtils.getQueryParamsDictforKey(
url.search,
"hyperComponentName",
)->Types.getHyperComponentNameFromStr

<PreMountLoader publishableKey sessionId clientSecret endpoint ephemeralKey hyperComponentName />
}
| "achBankTransfer"
| "bacsBankTransfer"
Expand Down
4 changes: 3 additions & 1 deletion src/CardTheme.res
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ let defaultConfig = {
locale: "auto",
fonts: [],
clientSecret: "",
ephemeralKey: "",
loader: Auto,
}
type recoilConfig = {
Expand Down Expand Up @@ -382,7 +383,7 @@ let itemToObjMapper = (
logger,
) => {
unknownKeysWarning(
["appearance", "fonts", "locale", "clientSecret", "loader"],
["appearance", "fonts", "locale", "clientSecret", "loader", "ephemeralKey"],
dict,
"elements",
~logger,
Expand All @@ -392,6 +393,7 @@ let itemToObjMapper = (
locale: getWarningString(dict, "locale", "auto", ~logger),
fonts: getFonts("fonts", dict, logger),
clientSecret: getWarningString(dict, "clientSecret", "", ~logger),
ephemeralKey: getWarningString(dict, "ephemeralKey", "", ~logger),
loader: getWarningString(dict, "loader", "auto", ~logger)->getShowLoader(logger),
}
}
26 changes: 26 additions & 0 deletions src/CardUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ let getCardBrandIcon = (cardType, paymentType) => {
| ApplePayElement
| KlarnaElement
| ExpressCheckoutElement
| PaymentMethodsManagement
| NONE =>
<Icon size=brandIconSize name="default-card" />
}
Expand Down Expand Up @@ -625,3 +626,28 @@ let useCardDetails = (~cvcNumber, ~isCvcValidValue, ~isCVCValid) => {
(isCardDetailsEmpty, isCardDetailsValid, isCardDetailsInvalid)
}, (cvcNumber, isCvcValidValue, isCVCValid))
}

let getWalletBrandIcon = (customerMethod: PaymentType.customerMethods) => {
let iconName = switch customerMethod.paymentMethodType {
| Some("apple_pay") => "apple_pay_saved"
| Some("google_pay") => "google_pay_saved"
| Some("paypal") => "paypal"
| _ => "default-card"
}

<Icon size=Utils.brandIconSize name=iconName />
}

let getPaymentMethodBrand = (customerMethod: PaymentType.customerMethods) => {
switch customerMethod.paymentMethod {
| "wallet" => getWalletBrandIcon(customerMethod)
| _ =>
getCardBrandIcon(
switch customerMethod.card.scheme {
| Some(ele) => ele
| None => ""
}->getCardType,
""->CardThemeType.getPaymentMode,
)
}
}
14 changes: 14 additions & 0 deletions src/Components/PaymentElementShimmer.res
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ module Shimmer = {
}
}

module SavedPaymentShimmer = {
@react.component
let make = () => {
<Shimmer>
<div className="animate-pulse w-full h-12 rounded bg-slate-200">
<div className="flex flex-row my-auto">
<div className="w-10 h-5 rounded-full m-3 bg-white bg-opacity-70" />
<div className="my-auto w-24 h-2 rounded m-3 bg-white bg-opacity-70" />
</div>
</div>
</Shimmer>
}
}

@react.component
let make = () => {
<div className="flex flex-col gap-4">
Expand Down
88 changes: 88 additions & 0 deletions src/Components/SavedMethodItem.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
@react.component
let make = (~brandIcon, ~paymentItem: PaymentType.customerMethods, ~handleDelete) => {
let {themeObj, localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
let {hideExpiredPaymentMethods} = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom)
let isCard = paymentItem.paymentMethod === "card"
let expiryMonth = paymentItem.card.expiryMonth
let expiryYear = paymentItem.card.expiryYear
let expiryDate = Date.fromString(`${expiryYear}-${expiryMonth}`)
let currentDate = Date.make()
let pickerItemClass = "PickerItem--selected"
let isCardExpired = isCard && expiryDate < currentDate
let paymentMethodType = paymentItem.paymentMethodType->Option.getOr("debit")

<RenderIf condition={!hideExpiredPaymentMethods || !isCardExpired}>
<div
className={`PickerItem ${pickerItemClass} flex flex-row items-stretch`}
style={
minWidth: "150px",
width: "100%",
padding: "1rem 0 1rem 0",
borderBottom: `1px solid ${themeObj.borderColor}`,
borderTop: "none",
borderLeft: "none",
borderRight: "none",
borderRadius: "0px",
background: "transparent",
color: themeObj.colorTextSecondary,
boxShadow: "none",
opacity: {isCardExpired ? "0.7" : "1"},
}>
<div className="w-full">
<div>
<div className="flex flex-row justify-between items-center">
<div className="flex grow justify-between">
<div
className={`flex flex-row justify-center items-center`}
style={columnGap: themeObj.spacingUnit}>
<div className={`PickerItemIcon mx-3 flex items-center `}> brandIcon </div>
<div className="flex flex-col">
<div className="flex items-center gap-4">
{if isCard {
<div className="flex flex-col items-start">
<div> {React.string(paymentItem.card.nickname)} </div>
<div className={`PickerItemLabel flex flex-row gap-3 items-center`}>
<div className="tracking-widest"> {React.string(`****`)} </div>
<div> {React.string(paymentItem.card.last4Digits)} </div>
</div>
</div>
} else {
<div> {React.string(paymentMethodType->Utils.snakeToTitleCase)} </div>
}}
</div>
</div>
</div>
<RenderIf condition={isCard}>
<div
className={`flex flex-row items-center justify-end gap-3 -mt-1`}
style={fontSize: "14px", opacity: "0.5"}>
<div className="flex">
{React.string(
`${expiryMonth} / ${expiryYear->CardUtils.formatExpiryToTwoDigit}`,
)}
</div>
</div>
</RenderIf>
</div>
<Icon
size=18
name="delete"
style={color: themeObj.colorDanger}
className="cursor-pointer ml-4 mb-[6px]"
onClick={_ => paymentItem->handleDelete}
/>
</div>
<div className="w-full">
<div className="flex flex-col items-start mx-8">
<RenderIf condition={isCardExpired}>
<div className="italic mt-3 ml-1" style={fontSize: "14px", opacity: "0.7"}>
{`*${localeString.cardExpiredText}`->React.string}
</div>
</RenderIf>
</div>
</div>
</div>
</div>
</div>
</RenderIf>
}
31 changes: 2 additions & 29 deletions src/Components/SavedMethods.res
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,10 @@ let make = (

let {paymentToken: paymentTokenVal, customerId} = paymentToken

let getWalletBrandIcon = (obj: PaymentType.customerMethods) => {
switch obj.paymentMethodType {
| Some("apple_pay") => <Icon size=brandIconSize name="apple_pay_saved" />
| Some("google_pay") => <Icon size=brandIconSize name="google_pay_saved" />
| Some("paypal") => <Icon size=brandIconSize name="paypal" />
| _ => <Icon size=brandIconSize name="default-card" />
}
}

let bottomElement = {
savedMethods
->Array.mapWithIndex((obj, i) => {
let brandIcon = switch obj.paymentMethod {
| "wallet" => getWalletBrandIcon(obj)
| "bank_debit" => <Icon size=brandIconSize name="bank" />
| _ =>
getCardBrandIcon(
switch obj.card.scheme {
| Some(ele) => ele
| None => ""
}->getCardType,
""->CardThemeType.getPaymentMode,
)
}
let brandIcon = obj->getPaymentMethodBrand
let isActive = paymentTokenVal == obj.paymentToken
<SavedCardItem
key={i->Int.toString}
Expand Down Expand Up @@ -261,14 +241,7 @@ let make = (
fontWeight: "400",
marginTop: "25px",
}>
<PaymentElementShimmer.Shimmer>
<div className="animate-pulse w-full h-12 rounded bg-slate-200">
<div className="flex flex-row my-auto">
<div className="w-10 h-5 rounded-full m-3 bg-white bg-opacity-70" />
<div className="my-auto w-24 h-2 rounded m-3 bg-white bg-opacity-70" />
</div>
</div>
</PaymentElementShimmer.Shimmer>
<PaymentElementShimmer.SavedPaymentShimmer />
</div>
} else {
<RenderIf condition={!showFields}> {bottomElement} </RenderIf>
Expand Down
70 changes: 70 additions & 0 deletions src/Components/SavedPaymentManagement.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@react.component
let make = (~savedMethods: array<PaymentType.customerMethods>, ~setSavedMethods) => {
open CardUtils
open Utils

let {iframeId} = Recoil.useRecoilValueFromAtom(RecoilAtoms.keys)
let {config} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
let switchToCustomPod = Recoil.useRecoilValueFromAtom(RecoilAtoms.switchToCustomPod)
let logger = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom)

let removeSavedMethod = (
savedMethods: array<OrcaPaymentPage.PaymentType.customerMethods>,
paymentMethodId,
) => {
savedMethods->Array.filter(savedMethod => {
savedMethod.paymentMethodId !== paymentMethodId
})
}

let handleDelete = (paymentItem: PaymentType.customerMethods) => {
handlePostMessage([
("fullscreen", true->JSON.Encode.bool),
("param", "paymentloader"->JSON.Encode.string),
("iframeId", iframeId->JSON.Encode.string),
])
open Promise
PaymentHelpers.deletePaymentMethod(
~ephemeralKey=config.ephemeralKey,
~paymentMethodId=paymentItem.paymentMethodId,
~logger,
~switchToCustomPod,
)
->then(res => {
let dict = res->getDictFromJson
let paymentMethodId = dict->getString("payment_method_id", "")
let isDeleted = dict->getBool("deleted", false)

if isDeleted {
logger.setLogInfo(
~value="Successfully Deleted Saved Payment Method",
~eventName=DELETE_SAVED_PAYMENT_METHOD,
(),
)
setSavedMethods(prev => prev->removeSavedMethod(paymentMethodId))
} else {
logger.setLogError(~value=res->JSON.stringify, ~eventName=DELETE_SAVED_PAYMENT_METHOD, ())
}
handlePostMessage([("fullscreen", false->JSON.Encode.bool)])
resolve()
})
->catch(err => {
let exceptionMessage = err->formatException->JSON.stringify
logger.setLogError(
~value=`Error Deleting Saved Payment Method: ${exceptionMessage}`,
~eventName=DELETE_SAVED_PAYMENT_METHOD,
(),
)
handlePostMessage([("fullscreen", false->JSON.Encode.bool)])
resolve()
})
->ignore
}

savedMethods
->Array.mapWithIndex((obj, i) => {
let brandIcon = obj->getPaymentMethodBrand
<SavedMethodItem key={i->Int.toString} paymentItem=obj brandIcon handleDelete />
})
->React.array
}
1 change: 1 addition & 0 deletions src/Hooks/CommonHooks.res
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type element = {
}
type keys = {
clientSecret: option<string>,
ephemeralKey?: string,
publishableKey: string,
iframeId: string,
parentURL: string,
Expand Down
Loading

0 comments on commit ef1c605

Please sign in to comment.