Skip to content

Commit

Permalink
feat(wallet/frontend): add card settings (#1564)
Browse files Browse the repository at this point in the history
* Initial card settings - spending limit, card pin

* Add dialogs

* Add change PIN dialog + card front scaling

* Add change card PIN form functionality

* Remove console logs

* Add spending limit settings

* Remove leftover settings button

* Add comments

* Remove comments

* Fix build

* Move dialogs
  • Loading branch information
raducristianpopa authored Sep 12, 2024
1 parent 3745071 commit 7bc7189
Show file tree
Hide file tree
Showing 9 changed files with 670 additions and 95 deletions.
142 changes: 142 additions & 0 deletions packages/wallet/frontend/src/components/dialogs/UserCardPINDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import type { DialogProps } from '@/lib/types/dialog'
import { cardServiceMock, changePinSchema, IUserCard } from '@/lib/api/card'
import { UserCardFront } from '@/components/userCards/UserCard'
import { Button } from '@/ui/Button'
import { useZodForm } from '@/lib/hooks/useZodForm'
import { Form } from '@/ui/forms/Form'
import { useRouter } from 'next/router'
import { getObjectKeys } from '@/utils/helpers'
import { Input } from '@/ui/forms/Input'

type UserCardPINDialogProos = Pick<DialogProps, 'onClose'> & {
card: IUserCard
}

export const UserCardPINDialog = ({
card,
onClose
}: UserCardPINDialogProos) => {
return (
<Transition.Root show={true} as={Fragment} appear={true}>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-green-modal/75 transition-opacity dark:bg-black/75" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
>
<Dialog.Panel className="relative w-full max-w-sm space-y-4 overflow-hidden rounded-lg bg-white p-8 shadow-xl dark:bg-purple">
<Dialog.Title
as="h3"
className="text-center text-2xl font-bold"
>
Card PIN
</Dialog.Title>
<div className="flex justify-between space-x-5">
<UserCardFront
card={card}
className="origin-top-left scale-[.3] [margin:0_calc(-20rem*(1-.3))_calc(-13rem*(1-0.3))_0] "
/>
<div>
<p className="text-base pt-2">Physical Debit Card</p>
<p className="text-black/50 dark:text-white/50 text-sm">
{card.number}
</p>
</div>
</div>
<ChangePinForm />
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

const ChangePinForm = () => {
const [showForm, setShowForm] = useState(false)
const router = useRouter()
const form = useZodForm({
schema: changePinSchema
})

if (!showForm) {
return (
<Button
className="w-full"
aria-label="show change pin form"
onClick={() => setShowForm(true)}
>
Change PIN
</Button>
)
}
return (
<Form
form={form}
onSubmit={async (data) => {
const response = await cardServiceMock.changePin(data)

if (response.success) {
router.replace(router.asPath)
} else {
const { errors, message } = response
form.setError('root', {
message
})
if (errors) {
getObjectKeys(errors).map((field) =>
form.setError(field, {
message: errors[field]
})
)
}
}
}}
>
<Input
type="password"
inputMode="numeric"
maxLength={4}
placeholder="Enter PIN"
error={form.formState?.errors?.pin?.message}
{...form.register('pin')}
/>
<Input
type="password"
inputMode="numeric"
maxLength={4}
placeholder="Repeat PIN"
error={form.formState?.errors?.confirmPin?.message}
{...form.register('confirmPin')}
/>
<Button
aria-label="change pin"
type="submit"
loading={form.formState.isSubmitting}
>
Confirm PIN change
</Button>
</Form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import type { DialogProps } from '@/lib/types/dialog'
import {
cardServiceMock,
dailySpendingLimitSchema,
monthlySpendingLimitSchema
} from '@/lib/api/card'
import { useRouter } from 'next/router'
import { useZodForm } from '@/lib/hooks/useZodForm'
import { getObjectKeys } from '@/utils/helpers'
import { Form } from '@/ui/forms/Form'
import { Input } from '@/ui/forms/Input'
import { Button } from '@/ui/Button'

type UserCardSpendingLimitDialogProos = Pick<DialogProps, 'onClose'>

export const UserCardSpendingLimitDialog = ({
onClose
}: UserCardSpendingLimitDialogProos) => {
return (
<Transition.Root show={true} as={Fragment} appear={true}>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-green-modal/75 transition-opacity dark:bg-black/75" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
>
<Dialog.Panel className="relative w-full max-w-xl space-y-4 overflow-hidden rounded-lg bg-white p-8 shadow-xl dark:bg-purple">
<Dialog.Title
as="h3"
className="text-center text-2xl font-bold"
>
Spending Limit
</Dialog.Title>
<div className="space-y-10">
<DailySpendingLimitForm />
<MonthlySpendingLimitForm />
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

// TODO: We will probably need to fetch the existing limitt
const DailySpendingLimitForm = () => {
const router = useRouter()
const form = useZodForm({
schema: dailySpendingLimitSchema
})

return (
<Form
form={form}
onSubmit={async (data) => {
const response = await cardServiceMock.setDailySpendingLimit(data)

if (response.success) {
router.replace(router.asPath)
} else {
const { errors, message } = response
form.setError('root', {
message
})
if (errors) {
getObjectKeys(errors).map((field) =>
form.setError(field, {
message: errors[field]
})
)
}
}
}}
>
<div className="flex gap-x-5">
<div className="flex-1">
<Input
type="password"
inputMode="numeric"
className="w-full"
maxLength={4}
label="Daily Spending Limit"
placeholder="Set daily spending limit"
error={form.formState?.errors?.amount?.message}
{...form.register('amount')}
/>
</div>
<Button
aria-label="change pin"
className="self-end"
type="submit"
loading={form.formState.isSubmitting}
>
Save
</Button>
</div>
</Form>
)
}

// TODO: We will probably need to fetch the existing limitt
const MonthlySpendingLimitForm = () => {
const router = useRouter()
const form = useZodForm({
schema: monthlySpendingLimitSchema
})

return (
<Form
form={form}
onSubmit={async (data) => {
const response = await cardServiceMock.setMonthlySpendingLimit(data)

if (response.success) {
router.replace(router.asPath)
} else {
const { errors, message } = response
form.setError('root', {
message
})
if (errors) {
getObjectKeys(errors).map((field) =>
form.setError(field, {
message: errors[field]
})
)
}
}
}}
>
<div className="flex gap-x-5">
<div className="flex-1">
<Input
type="password"
inputMode="numeric"
className="w-full"
maxLength={4}
label="Monthly Spending Limit"
placeholder="Set monthly spending limit"
error={form.formState?.errors?.amount?.message}
{...form.register('amount')}
/>
</div>
<Button
aria-label="change pin"
className="self-end"
type="submit"
loading={form.formState.isSubmitting}
>
Save
</Button>
</div>
</Form>
)
}
35 changes: 35 additions & 0 deletions packages/wallet/frontend/src/components/icons/Key.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,38 @@ export const Key = (props: SVGProps<SVGSVGElement>) => {
</svg>
)
}

export const CardKey = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_9_1590)">
<path
d="M9.41809 17.3731V15.9591H11.1861L12.6871 14.4581L14.7591 15.3381C15.5101 15.6571 16.3791 15.4881 16.9551 14.9111L19.8981 11.9681C20.4751 11.3911 20.6431 10.5221 20.3251 9.77205L18.8231 6.23605C18.6211 5.75905 18.2411 5.38005 17.7641 5.17705L14.2281 3.67505C13.4771 3.35605 12.6091 3.52505 12.0321 4.10205L9.08909 7.04505C8.51309 7.62105 8.34409 8.49005 8.66309 9.24105L9.52009 11.2601L3.49609 17.2841V20.4661H6.67809L8.00409 19.1401V17.3721L9.41809 17.3731Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.3759 8.89404C14.1369 8.89504 13.9429 9.08904 13.9439 9.32804C13.9439 9.56704 14.1389 9.76104 14.3779 9.76004C14.6169 9.76004 14.8109 9.56604 14.8109 9.32704C14.8109 9.08804 14.6169 8.89404 14.3779 8.89404"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_9_1590">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
)
}
Loading

0 comments on commit 7bc7189

Please sign in to comment.