Skip to content

Commit

Permalink
stripe: add viewing current links
Browse files Browse the repository at this point in the history
  • Loading branch information
devksingh4 committed Feb 7, 2025
1 parent 2781411 commit 266c04b
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 41 deletions.
2 changes: 2 additions & 0 deletions src/api/routes/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
active: item.active,
invoiceId: item.invoiceId,
invoiceAmountUsd: item.amount,
createdAt: item.createdAt || null,
}),
);
reply.status(200).send(parsed);
Expand Down Expand Up @@ -131,6 +132,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
url,
amount: request.body.invoiceAmountUsd,
active: true,
createdAt: new Date().toISOString(),
}),
});
await fastify.dynamoClient.send(dynamoCommand);
Expand Down
16 changes: 10 additions & 6 deletions src/common/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { allAppRoles, AppRoles, RunEnvironment } from "./roles.js";
import { AppRoles, RunEnvironment } from "./roles.js";
import { OriginFunction } from "@fastify/cors";

// From @fastify/cors
Expand Down Expand Up @@ -75,9 +75,11 @@ const environmentConfig: EnvironmentConfigType = {
AadValidClientId: "39c28870-94e4-47ee-b4fb-affe0bf96c9f",
PasskitIdentifier: "pass.org.acmuiuc.qa.membership",
PasskitSerialNumber: "0",
MembershipApiEndpoint: "https://infra-membership-api.aws.qa.acmuiuc.org/api/v1/checkMembership",
MembershipApiEndpoint:
"https://infra-membership-api.aws.qa.acmuiuc.org/api/v1/checkMembership",
EmailDomain: "aws.qa.acmuiuc.org",
SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/427040638965/infra-core-api-sqs"
SqsQueueUrl:
"https://sqs.us-east-1.amazonaws.com/427040638965/infra-core-api-sqs",
},
prod: {
AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] },
Expand All @@ -90,10 +92,12 @@ const environmentConfig: EnvironmentConfigType = {
AadValidClientId: "5e08cf0f-53bb-4e09-9df2-e9bdc3467296",
PasskitIdentifier: "pass.edu.illinois.acm.membership",
PasskitSerialNumber: "0",
MembershipApiEndpoint: "https://infra-membership-api.aws.acmuiuc.org/api/v1/checkMembership",
MembershipApiEndpoint:
"https://infra-membership-api.aws.acmuiuc.org/api/v1/checkMembership",
EmailDomain: "acm.illinois.edu",
SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/298118738376/infra-core-api-sqs"
}
SqsQueueUrl:
"https://sqs.us-east-1.amazonaws.com/298118738376/infra-core-api-sqs",
},
};

export type SecretConfig = {
Expand Down
1 change: 1 addition & 0 deletions src/common/types/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const invoiceLinkGetResponseSchema = z.array(
active: z.boolean(),
invoiceId: z.string().min(1),
invoiceAmountUsd: z.number().min(50),
createdAt: z.union([z.string().date(), z.null()]),
}),
);

Expand Down
13 changes: 7 additions & 6 deletions src/ui/pages/stripe/CreateLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,11 @@ import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';

interface StripeCreateLinkPanelProps {
createLink: (payload: PostInvoiceLinkRequest) => Promise<PostInvoiceLinkResponse>;
isLoading: boolean;
}

export const StripeCreateLinkPanel: React.FC<StripeCreateLinkPanelProps> = ({
createLink,
isLoading,
}) => {
export const StripeCreateLinkPanel: React.FC<StripeCreateLinkPanelProps> = ({ createLink }) => {
const [modalOpened, setModalOpened] = useState(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [returnedLink, setReturnedLink] = useState<string | null>(null);

const form = useForm({
Expand All @@ -49,17 +46,21 @@ export const StripeCreateLinkPanel: React.FC<StripeCreateLinkPanelProps> = ({

const handleSubmit = async (values: typeof form.values) => {
try {
setIsLoading(true);
const response = await createLink(values);
setReturnedLink(response.link);
setIsLoading(false);
setModalOpened(true);
form.reset();
} catch (err) {
} catch (e) {
setIsLoading(false);
notifications.show({
title: 'Error',
message: 'Failed to create payment link. Please try again or contact support.',
color: 'red',
icon: <IconAlertCircle size={16} />,
});
throw e;
}
};

Expand Down
164 changes: 151 additions & 13 deletions src/ui/pages/stripe/CurrentLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,162 @@
import { Box, Card, Divider, Text, Title } from '@mantine/core';
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react';
import React, { useState } from 'react';
import {
Badge,
Box,
Button,
Card,
Checkbox,
CopyButton,
Divider,
Group,
Loader,
NumberFormatter,
Table,
Text,
Title,
} from '@mantine/core';
import { IconAlertCircle, IconAlertTriangle, IconCircleCheck } from '@tabler/icons-react';
import React, { useEffect, useState } from 'react';
import { GetInvoiceLinksResponse } from '@common/types/stripe';
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
import { notifications } from '@mantine/notifications';
import { useAuth } from '@ui/components/AuthContext';
import pluralize from 'pluralize';

interface StripeCurrentLinksPanelProps {
links: GetInvoiceLinksResponse;
isLoading: boolean;
getLinks: () => Promise<GetInvoiceLinksResponse>;
}

export const StripeCurrentLinksPanel: React.FC<StripeCurrentLinksPanelProps> = ({
links,
isLoading,
}) => {
export const StripeCurrentLinksPanel: React.FC<StripeCurrentLinksPanelProps> = ({ getLinks }) => {
const [links, setLinks] = useState<GetInvoiceLinksResponse | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const { userData } = useAuth();
useEffect(() => {
const getLinksOnLoad = async () => {
try {
setIsLoading(true);
const data = await getLinks();
setLinks(data);
setIsLoading(false);
} catch (e) {
setIsLoading(false);
notifications.show({
title: 'Error',
message: 'Failed to get payment links. Please try again or contact support.',
color: 'red',
icon: <IconAlertCircle size={16} />,
});
}
};
getLinksOnLoad();
}, []);
const createTableRow = (data: GetInvoiceLinksResponse[number]) => {
return (
<Table.Tr
key={data.id}
bg={selectedRows.includes(data.id) ? 'var(--mantine-color-blue-light)' : undefined}
>
<Table.Td>
<Checkbox
aria-label="Select row"
checked={selectedRows.includes(data.id)}
onChange={(event) =>
setSelectedRows(
event.currentTarget.checked
? [...selectedRows, data.id]
: selectedRows.filter((id) => id !== data.id)
)
}
/>
</Table.Td>
<Table.Td>
{data.active && (
<Badge color="green" variant="light">
Active
</Badge>
)}
{!data.active && (
<Badge color="red" variant="light">
Inactive
</Badge>
)}
</Table.Td>
<Table.Td>{data.invoiceId}</Table.Td>
<Table.Td>
<NumberFormatter prefix="$" value={data.invoiceAmountUsd / 100} thousandSeparator />
</Table.Td>
<Table.Td>{data.userId.replace(userData!.email!, 'You')}</Table.Td>
<Table.Td>{data.createdAt === null ? 'Unknown' : data.createdAt}</Table.Td>
<Table.Td>
<CopyButton value={data.link}>
{({ copied, copy }) => (
<Button color={copied ? 'teal' : 'blue'} onClick={copy}>
{copied ? 'Copied!' : 'Copy'}
</Button>
)}
</CopyButton>
</Table.Td>
</Table.Tr>
);
};
const deactivateLinks = (linkIds: string[]) => {
notifications.show({
title: 'Feature not available',
message: 'Coming soon!',
color: 'yellow',
icon: <IconAlertTriangle size={16} />,
});
};

return (
<div>
<Title order={2} mb="sm">
Current Links
</Title>
<Text>Coming soon!</Text>
<Group justify="space-between">
<Title order={2} mb="sm">
Current Links
</Title>
{selectedRows.length > 0 && (
<Button
color="red"
onClick={() => {
deactivateLinks(selectedRows);
}}
>
Deactivate {pluralize('links', selectedRows.length, true)}
</Button>
)}
</Group>

{isLoading && <Loader color="blue" />}
{!isLoading && links && (
<Table.ScrollContainer minWidth={500}>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>
<Checkbox
aria-label="Select all rows"
checked={selectedRows.length === links.length}
onChange={(event) =>
setSelectedRows(() => {
if (selectedRows.length === links.length) {
return [];
}
return links.map((x) => x.id);
})
}
/>
</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Invoice ID</Table.Th>
<Table.Th>Invoice Amount</Table.Th>
<Table.Th>Created By</Table.Th>
<Table.Th>Created At</Table.Th>
<Table.Th>Payment Link</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{links.map(createTableRow)}</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</div>
);
};
Expand Down
30 changes: 14 additions & 16 deletions src/ui/pages/stripe/ViewLinks.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@ import { AuthGuard } from '@ui/components/AuthGuard';
import { AppRoles } from '@common/roles';
import StripeCurrentLinksPanel from './CurrentLinks';
import StripeCreateLinkPanel from './CreateLink';
import { PostInvoiceLinkRequest, PostInvoiceLinkResponse } from '@common/types/stripe';
import {
GetInvoiceLinksResponse,
PostInvoiceLinkRequest,
PostInvoiceLinkResponse,
} from '@common/types/stripe';
import { useApi } from '@ui/util/api';

export const ManageStripeLinksPage: React.FC = () => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const api = useApi('core');

const createLink = async (payload: PostInvoiceLinkRequest): Promise<PostInvoiceLinkResponse> => {
const modifiedPayload = { ...payload, invoiceAmountUsd: payload.invoiceAmountUsd * 100 };
try {
setIsLoading(true);
const response = await api.post('/api/v1/stripe/paymentLinks', modifiedPayload);
setIsLoading(false);
return response.data;
} catch (e) {
setIsLoading(false);
throw e;
}
const response = await api.post('/api/v1/stripe/paymentLinks', modifiedPayload);
return response.data;
};

const getLinks = async (): Promise<GetInvoiceLinksResponse> => {
const response = await api.get('/api/v1/stripe/paymentLinks');
return response.data;
};

return (
Expand All @@ -32,11 +33,8 @@ export const ManageStripeLinksPage: React.FC = () => {
<Container>
<Title>Stripe Link Creator</Title>
<Text>Create a Stripe Payment Link to accept credit card payments.</Text>
<StripeCreateLinkPanel
createLink={createLink}
isLoading={isLoading}
></StripeCreateLinkPanel>
<StripeCurrentLinksPanel links={[]} isLoading={isLoading} />
<StripeCreateLinkPanel createLink={createLink}></StripeCreateLinkPanel>
<StripeCurrentLinksPanel getLinks={getLinks} />
</Container>
</AuthGuard>
);
Expand Down

0 comments on commit 266c04b

Please sign in to comment.