-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
326 additions
and
20 deletions.
There are no files selected for viewing
54 changes: 35 additions & 19 deletions
54
apps/web/app/app.dub.co/(dashboard)/[slug]/bank-accounts/page-client.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,49 @@ | ||
"use client"; | ||
|
||
import useWorkspace from "@/lib/swr/use-workspace"; | ||
import { useAddBankAccountModal } from "@/ui/modals/add-bank-account-modal"; | ||
import { Button, MaxWidthWrapper } from "@dub/ui"; | ||
|
||
export const BankAccountsClient = () => { | ||
const { slug } = useWorkspace(); | ||
const { bankAccountName, maskedAccountNumber } = useWorkspace(); | ||
|
||
return ( | ||
<div className="relative min-h-[calc(100vh-16px)]"> | ||
<MaxWidthWrapper className="grid gap-5 pb-10 pt-3"> | ||
<div className="flex items-center gap-5 rounded-lg border bg-white p-5"> | ||
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-300"> | ||
<div className="h-5 w-[41px]"></div> | ||
</div> | ||
const { AddBankAccountModal, setShowAddBankAccountModal } = | ||
useAddBankAccountModal(); | ||
|
||
<div className="flex grow flex-col gap-1"> | ||
<div className="text-base font-semibold text-gray-700"> | ||
Bank account | ||
return ( | ||
<> | ||
<AddBankAccountModal /> | ||
<div className="relative min-h-[calc(100vh-16px)]"> | ||
<MaxWidthWrapper className="grid gap-5 pb-10 pt-3"> | ||
<div className="flex items-center gap-5 rounded-lg border bg-white p-5"> | ||
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-300"> | ||
<div className="h-5 w-[41px]"></div> | ||
</div> | ||
<div className="text-sm text-neutral-500"> | ||
Add your bank account | ||
|
||
<div className="flex grow flex-col gap-1"> | ||
<div className="text-base font-semibold text-gray-700"> | ||
{bankAccountName ?? "Add your bank account"} | ||
</div> | ||
<div className="text-sm text-neutral-500"> | ||
{maskedAccountNumber | ||
? `*******${maskedAccountNumber}` | ||
: "Add your bank account"} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div> | ||
<Button text="Add bank account" /> | ||
<div> | ||
{bankAccountName ? ( | ||
<Button text="Disconnect" /> | ||
) : ( | ||
<Button | ||
text="Add bank account" | ||
onClick={() => setShowAddBankAccountModal(true)} | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
</MaxWidthWrapper> | ||
</div> | ||
</MaxWidthWrapper> | ||
</div> | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
"use server"; | ||
|
||
import { prisma } from "@/lib/prisma"; | ||
import { revalidatePath } from "next/cache"; | ||
import { z } from "zod"; | ||
import { addAppAchAccount } from "../dots/add-app-ach-account"; | ||
import { createDotsApp } from "../dots/create-dots-app"; | ||
import { addBankAccountSchema } from "../dots/schemas"; | ||
import { authActionClient } from "./safe-action"; | ||
|
||
const schema = addBankAccountSchema.extend({ workspaceId: z.string() }); | ||
|
||
export const addBankAccount = authActionClient | ||
.schema(schema) | ||
.action(async ({ parsedInput, ctx }) => { | ||
const { workspace } = ctx; | ||
const { accountNumber, accountType, routingNumber } = parsedInput; | ||
|
||
let dotsAppId: string | null = workspace.dotsAppId; | ||
|
||
// Create Dots app if it doesn't exist | ||
if (!dotsAppId) { | ||
const dotsApp = await createDotsApp({ workspace }); | ||
|
||
await prisma.project.update({ | ||
where: { | ||
id: workspace.id, | ||
}, | ||
data: { | ||
dotsAppId: dotsApp.id, | ||
}, | ||
}); | ||
|
||
dotsAppId = dotsApp.id; | ||
} | ||
|
||
// Add bank account to Dots app | ||
const achAccount = await addAppAchAccount({ | ||
dotsAppId, | ||
accountNumber, | ||
routingNumber, | ||
accountType, | ||
}); | ||
|
||
await prisma.project.update({ | ||
where: { | ||
id: workspace.id, | ||
}, | ||
data: { | ||
bankAccountName: achAccount.name, | ||
maskedAccountNumber: achAccount.mask, | ||
}, | ||
}); | ||
|
||
revalidatePath("/"); | ||
|
||
return achAccount; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { DOTS_API_URL } from "./env"; | ||
import { achAccountSchema } from "./schemas"; | ||
import { getBasicAuthToken } from "./utils"; | ||
|
||
export const addAppAchAccount = async ({ | ||
dotsAppId, | ||
accountNumber, | ||
routingNumber, | ||
accountType, | ||
}: { | ||
dotsAppId: string; | ||
accountNumber: string; | ||
routingNumber: string; | ||
accountType: "checking" | "savings"; | ||
}) => { | ||
const response = await fetch( | ||
`${DOTS_API_URL}/apps/${dotsAppId}/ach-account`, | ||
{ | ||
method: "PUT", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: `Basic ${getBasicAuthToken()}`, | ||
}, | ||
body: JSON.stringify({ | ||
account_number: accountNumber, | ||
routing_number: routingNumber, | ||
account_type: accountType, | ||
}), | ||
}, | ||
); | ||
|
||
if (!response.ok) { | ||
console.error(await response.text()); | ||
|
||
throw new Error(`Failed to add ACH account to Dots app ${dotsAppId}.`); | ||
} | ||
|
||
return achAccountSchema.parse(await response.json()); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { addBankAccount } from "@/lib/actions/add-bank-account"; | ||
import { addBankAccountSchema } from "@/lib/dots/schemas"; | ||
import useWorkspace from "@/lib/swr/use-workspace"; | ||
import z from "@/lib/zod"; | ||
import { Button, Modal } from "@dub/ui"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useAction } from "next-safe-action/hooks"; | ||
import { useCallback, useMemo, useState } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import { toast } from "sonner"; | ||
|
||
interface AddBankAccountProps { | ||
showModal: boolean; | ||
setShowModal: (showModal: boolean) => void; | ||
} | ||
|
||
type BankAccount = z.infer<typeof addBankAccountSchema>; | ||
|
||
interface AddBankAccountFormProps { | ||
onCancel: () => void; | ||
onSuccess: () => void; | ||
} | ||
|
||
const AddBankAccount = ({ showModal, setShowModal }: AddBankAccountProps) => { | ||
return ( | ||
<Modal | ||
showModal={showModal} | ||
setShowModal={setShowModal} | ||
drawerRootProps={{ repositionInputs: false }} | ||
> | ||
<h3 className="border-b border-gray-200 px-4 py-4 text-lg font-medium sm:px-6"> | ||
Add bank account | ||
</h3> | ||
<div className="scrollbar-hide mt-6 max-h-[calc(100dvh-200px)] overflow-auto overflow-y-scroll"> | ||
<AddBankAccountForm | ||
onSuccess={() => setShowModal(false)} | ||
onCancel={() => setShowModal(false)} | ||
/> | ||
</div> | ||
</Modal> | ||
); | ||
}; | ||
|
||
const AddBankAccountForm = ({ | ||
onCancel, | ||
onSuccess, | ||
}: AddBankAccountFormProps) => { | ||
const { id: workspaceId } = useWorkspace(); | ||
|
||
const { | ||
register, | ||
handleSubmit, | ||
formState: { isValid, isSubmitting }, | ||
} = useForm<BankAccount>({ | ||
resolver: zodResolver(addBankAccountSchema), | ||
}); | ||
|
||
const { executeAsync, isExecuting } = useAction(addBankAccount, { | ||
async onSuccess() { | ||
toast.success( | ||
"Bank account added successfully. Waiting for verification.", | ||
); | ||
onSuccess(); | ||
}, | ||
onError({ error }) { | ||
toast.error(error.serverError?.serverError); | ||
}, | ||
}); | ||
|
||
const onSubmit = async (data: BankAccount) => { | ||
await executeAsync({ ...data, workspaceId: workspaceId! }); | ||
}; | ||
|
||
return ( | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<div className="flex flex-col gap-y-6 px-4 text-left sm:px-6"> | ||
<div className="flex flex-col gap-3"> | ||
<div> | ||
<label | ||
htmlFor="accountNumber" | ||
className="flex items-center space-x-2" | ||
> | ||
<h2 className="text-sm font-medium text-gray-900"> | ||
Account number | ||
</h2> | ||
</label> | ||
<div className="relative mt-2 rounded-md shadow-sm"> | ||
<input | ||
{...register("accountNumber")} | ||
className="block w-full rounded-md border-gray-300 text-gray-900 placeholder-gray-400 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm" | ||
required | ||
autoFocus | ||
autoComplete="off" | ||
/> | ||
</div> | ||
</div> | ||
|
||
<div> | ||
<label | ||
htmlFor="routingNumber" | ||
className="flex items-center space-x-2" | ||
> | ||
<h2 className="text-sm font-medium text-gray-900"> | ||
Routing number | ||
</h2> | ||
</label> | ||
<div className="relative mt-2 rounded-md shadow-sm"> | ||
<input | ||
{...register("routingNumber")} | ||
className="block w-full rounded-md border-gray-300 text-gray-900 placeholder-gray-400 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm" | ||
required | ||
autoComplete="off" | ||
/> | ||
</div> | ||
</div> | ||
|
||
<div> | ||
<label | ||
htmlFor="accountType" | ||
className="flex items-center space-x-2" | ||
> | ||
<h2 className="text-sm font-medium text-gray-900"> | ||
Account type | ||
</h2> | ||
</label> | ||
<div className="relative mt-2 rounded-md shadow-sm"> | ||
<select | ||
{...register("accountType")} | ||
className="block w-full rounded-md border-gray-300 text-gray-900 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm" | ||
required | ||
> | ||
<option value="checking">Checking</option> | ||
<option value="savings">Savings</option> | ||
</select> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div className="mt-8 flex justify-end gap-2 border-t border-gray-200 px-4 py-4 sm:px-6"> | ||
<Button | ||
type="button" | ||
variant="secondary" | ||
text="Cancel" | ||
className="h-9 w-fit" | ||
onClick={onCancel} | ||
/> | ||
|
||
<Button | ||
type="submit" | ||
text="Add bank account" | ||
className="h-9 w-fit" | ||
disabled={!isValid} | ||
loading={isSubmitting || isExecuting} | ||
/> | ||
</div> | ||
</form> | ||
); | ||
}; | ||
|
||
export function useAddBankAccountModal() { | ||
const [showAddBankAccountModal, setShowAddBankAccountModal] = useState(false); | ||
|
||
const AddBankAccountModal = useCallback(() => { | ||
return ( | ||
<AddBankAccount | ||
showModal={showAddBankAccountModal} | ||
setShowModal={setShowAddBankAccountModal} | ||
/> | ||
); | ||
}, [showAddBankAccountModal, setShowAddBankAccountModal]); | ||
|
||
return useMemo( | ||
() => ({ setShowAddBankAccountModal, AddBankAccountModal }), | ||
[setShowAddBankAccountModal, AddBankAccountModal], | ||
); | ||
} |