Skip to content

Commit 79fc76e

Browse files
committed
build the create panel
1 parent 7c3fd98 commit 79fc76e

File tree

5 files changed

+221
-17
lines changed

5 files changed

+221
-17
lines changed

src/api/functions/stripe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const createStripeLink = async ({
2828
url: string;
2929
}> => {
3030
const stripe = new Stripe(stripeApiKey);
31-
const description = `Created For: ${contactName} (${contactEmail}) by ${createdBy}.`;
31+
const description = `Created for ${contactName} (${contactEmail}) by ${createdBy}.`;
3232
const product = await stripe.products.create({
3333
name: `Payment for Invoice: ${invoiceId}`,
3434
description,

src/common/types/stripe.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
1-
import { z } from 'zod';
1+
import { z } from "zod";
22

33
export const invoiceLinkPostResponseSchema = z.object({
44
id: z.string().min(1),
55
link: z.string().url(),
6-
})
6+
});
77

88
export const invoiceLinkPostRequestSchema = z.object({
99
invoiceId: z.string().min(1),
1010
invoiceAmountUsd: z.number().min(50),
1111
contactName: z.string().min(1),
12-
contactEmail: z.string().email()
13-
})
12+
contactEmail: z.string().email(),
13+
});
1414

15-
export const invoiceLinkGetResponseSchema = z.array(z.object({
16-
id: z.string().min(1),
17-
userId: z.string().email(),
18-
link: z.string().url(),
19-
active: z.boolean(),
20-
invoiceId: z.string().min(1),
21-
invoiceAmountUsd: z.number().min(50)
22-
}))
15+
export type PostInvoiceLinkRequest = z.infer<
16+
typeof invoiceLinkPostRequestSchema
17+
>;
18+
19+
export type PostInvoiceLinkResponse = z.infer<
20+
typeof invoiceLinkPostResponseSchema
21+
>;
22+
23+
export const invoiceLinkGetResponseSchema = z.array(
24+
z.object({
25+
id: z.string().min(1),
26+
userId: z.string().email(),
27+
link: z.string().url(),
28+
active: z.boolean(),
29+
invoiceId: z.string().min(1),
30+
invoiceAmountUsd: z.number().min(50),
31+
}),
32+
);
33+
34+
export type GetInvoiceLinksResponse = z.infer<
35+
typeof invoiceLinkGetResponseSchema
36+
>;

src/ui/pages/stripe/CreateLink.tsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {
2+
Box,
3+
Button,
4+
Card,
5+
Divider,
6+
Text,
7+
TextInput,
8+
NumberInput,
9+
Title,
10+
Modal,
11+
Anchor,
12+
CopyButton,
13+
Group,
14+
} from '@mantine/core';
15+
import { useForm } from '@mantine/form';
16+
import { showNotification } from '@mantine/notifications';
17+
import { IconAlertCircle } from '@tabler/icons-react';
18+
import React, { useState } from 'react';
19+
import { PostInvoiceLinkRequest, PostInvoiceLinkResponse } from '@common/types/stripe';
20+
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
21+
22+
interface StripeCreateLinkPanelProps {
23+
createLink: (payload: PostInvoiceLinkRequest) => Promise<PostInvoiceLinkResponse>;
24+
isLoading: boolean;
25+
}
26+
27+
export const StripeCreateLinkPanel: React.FC<StripeCreateLinkPanelProps> = ({
28+
createLink,
29+
isLoading,
30+
}) => {
31+
const [modalOpened, setModalOpened] = useState(false);
32+
const [returnedLink, setReturnedLink] = useState<string | null>(null);
33+
34+
const form = useForm({
35+
initialValues: {
36+
invoiceId: '',
37+
invoiceAmountUsd: 100,
38+
contactName: '',
39+
contactEmail: '',
40+
},
41+
validate: {
42+
invoiceId: (value) => (value.length < 1 ? 'Invoice ID is required' : null),
43+
invoiceAmountUsd: (value) => (value < 0.5 ? 'Amount must be at least $0.50' : null),
44+
contactName: (value) => (value.length < 1 ? 'Contact Name is required' : null),
45+
contactEmail: (value) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email'),
46+
},
47+
});
48+
49+
const handleSubmit = async (values: typeof form.values) => {
50+
try {
51+
const response = await createLink(values);
52+
setReturnedLink(response.link);
53+
setModalOpened(true);
54+
form.reset();
55+
} catch (err) {
56+
showNotification({
57+
title: 'Error',
58+
message: 'Failed to create payment link. Please try again or contact support.',
59+
color: 'red',
60+
icon: <IconAlertCircle size={16} />,
61+
});
62+
}
63+
};
64+
65+
if (isLoading) {
66+
return <FullScreenLoader />;
67+
}
68+
69+
return (
70+
<Box mt="xl" mb="xl">
71+
<Title order={2} mb="sm">
72+
Create a Payment Link
73+
</Title>
74+
<form onSubmit={form.onSubmit(handleSubmit)}>
75+
<TextInput
76+
label="Invoice ID"
77+
placeholder="ACM100"
78+
description="Make sure the Invoice ID is prefixed with a unique string for your group to avoid processing delays."
79+
{...form.getInputProps('invoiceId')}
80+
required
81+
/>
82+
<NumberInput
83+
label="Invoice Amount"
84+
leftSectionPointerEvents="none"
85+
leftSection={<Text>$</Text>}
86+
placeholder="100"
87+
min={0.5}
88+
{...form.getInputProps('invoiceAmountUsd')}
89+
required
90+
/>
91+
<TextInput
92+
label="Invoice Recipient Name"
93+
placeholder="John Doe"
94+
{...form.getInputProps('contactName')}
95+
required
96+
/>
97+
<TextInput
98+
label="Invoice Recipient Email"
99+
placeholder="[email protected]"
100+
{...form.getInputProps('contactEmail')}
101+
required
102+
/>
103+
104+
<Button type="submit" fullWidth mt="md">
105+
Create Link
106+
</Button>
107+
</form>
108+
109+
<Modal
110+
opened={modalOpened}
111+
size="xl"
112+
onClose={() => setModalOpened(false)}
113+
title="Payment Link Created!"
114+
>
115+
{returnedLink && (
116+
<Box mt="md">
117+
<Group>
118+
<Text style={{ color: 'blue' }}>{returnedLink}</Text>
119+
<CopyButton value={returnedLink}>
120+
{({ copied, copy }) => (
121+
<Button color={copied ? 'teal' : 'blue'} onClick={copy}>
122+
{copied ? 'Copied!' : 'Copy Link'}
123+
</Button>
124+
)}
125+
</CopyButton>
126+
</Group>
127+
<Text mt="sm">Provide this link to your billing contact for payment.</Text>
128+
</Box>
129+
)}
130+
</Modal>
131+
</Box>
132+
);
133+
};
134+
135+
export default StripeCreateLinkPanel;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Box, Card, Divider, Text, Title } from '@mantine/core';
2+
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react';
3+
import React, { useState } from 'react';
4+
import { GetInvoiceLinksResponse } from '@common/types/stripe';
5+
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
6+
7+
interface StripeCurrentLinksPanelProps {
8+
links: GetInvoiceLinksResponse;
9+
isLoading: boolean;
10+
}
11+
12+
export const StripeCurrentLinksPanel: React.FC<StripeCurrentLinksPanelProps> = ({
13+
links,
14+
isLoading,
15+
}) => {
16+
if (isLoading) {
17+
return <FullScreenLoader />;
18+
}
19+
return (
20+
<div>
21+
<Title order={2} mb="sm">
22+
Current Links
23+
</Title>
24+
<Text>Coming soon!</Text>
25+
</div>
26+
);
27+
};
28+
29+
export default StripeCurrentLinksPanel;

src/ui/pages/stripe/ViewLinks.page.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
1-
import React from 'react';
2-
import { Container, Title } from '@mantine/core';
1+
import React, { useState } from 'react';
2+
import { Card, Container, Divider, Title, Text } from '@mantine/core';
33
import { AuthGuard } from '@ui/components/AuthGuard';
44
import { AppRoles } from '@common/roles';
5+
import StripeCurrentLinksPanel from './CurrentLinks';
6+
import StripeCreateLinkPanel from './CreateLink';
7+
import { PostInvoiceLinkRequest, PostInvoiceLinkResponse } from '@common/types/stripe';
8+
import { useApi } from '@ui/util/api';
59

610
export const ManageStripeLinksPage: React.FC = () => {
11+
const [isLoading, setIsLoading] = useState<boolean>(false);
12+
const api = useApi('core');
13+
14+
const createLink = async (payload: PostInvoiceLinkRequest): Promise<PostInvoiceLinkResponse> => {
15+
setIsLoading(true);
16+
const modifiedPayload = { ...payload, invoiceAmountUsd: payload.invoiceAmountUsd * 100 };
17+
try {
18+
const response = await api.post('/api/v1/stripe/paymentLinks', modifiedPayload);
19+
setIsLoading(false);
20+
return response.data;
21+
} catch (e) {
22+
setIsLoading(false);
23+
throw e;
24+
}
25+
};
26+
727
return (
828
<AuthGuard
929
resourceDef={{ service: 'core', validRoles: [AppRoles.STRIPE_LINK_CREATOR] }}
1030
showSidebar={true}
1131
>
12-
<Container fluid>
13-
<Title>Stripe Links</Title>
32+
<Container>
33+
<Title>Stripe Link Creator</Title>
34+
<Text>Create a Stripe Payment Link to accept credit card payments.</Text>
35+
<StripeCreateLinkPanel
36+
createLink={createLink}
37+
isLoading={isLoading}
38+
></StripeCreateLinkPanel>
39+
<StripeCurrentLinksPanel links={[]} isLoading={isLoading} />
1440
</Container>
1541
</AuthGuard>
1642
);

0 commit comments

Comments
 (0)