Skip to content

Commit e737422

Browse files
authored
feat: ews (#1880)
fixes FTD-2227
1 parent 1d427ce commit e737422

File tree

11 files changed

+669
-462
lines changed

11 files changed

+669
-462
lines changed

components/embedded-wallets/ActiveUsersCard.tsx

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import {
2+
ApiKey,
3+
ApiKeyService,
4+
useUpdateApiKey,
5+
} from "@3rdweb-sdk/react/hooks/useApi";
6+
import { zodResolver } from "@hookform/resolvers/zod";
7+
import {
8+
Box,
9+
Divider,
10+
Flex,
11+
FormControl,
12+
HStack,
13+
Input,
14+
Switch,
15+
useColorModeValue,
16+
useToast,
17+
} from "@chakra-ui/react";
18+
import {
19+
ApiKeyEmbeddedWalletsValidationSchema,
20+
apiKeyEmbeddedWalletsValidationSchema,
21+
} from "components/settings/ApiKeys/validations";
22+
import { useForm } from "react-hook-form";
23+
import {
24+
Card,
25+
FormLabel,
26+
Heading,
27+
Text,
28+
FormErrorMessage,
29+
FormHelperText,
30+
Button,
31+
} from "tw-components";
32+
import { useTxNotifications } from "hooks/useTxNotifications";
33+
import { useTrack } from "hooks/analytics/useTrack";
34+
35+
interface ConfigureProps {
36+
apiKey: ApiKey;
37+
}
38+
39+
export const Configure: React.FC<ConfigureProps> = ({ apiKey }) => {
40+
// safe to type assert here as this component only renders
41+
// for an api key with an active embeddedWallets service
42+
const services = apiKey.services as ApiKeyService[];
43+
44+
const serviceIdx = services.findIndex(
45+
(srv) => srv.name === "embeddedWallets",
46+
);
47+
const config = services[serviceIdx];
48+
49+
const mutation = useUpdateApiKey();
50+
const trackEvent = useTrack();
51+
const toast = useToast();
52+
const bg = useColorModeValue("backgroundCardHighlight", "transparent");
53+
54+
const form = useForm<ApiKeyEmbeddedWalletsValidationSchema>({
55+
resolver: zodResolver(apiKeyEmbeddedWalletsValidationSchema),
56+
defaultValues: {
57+
recoveryShareManagement: config.recoveryShareManagement,
58+
customAuthentication: config.customAuthentication,
59+
},
60+
});
61+
62+
const { onSuccess, onError } = useTxNotifications(
63+
"Embedded Wallet API Key configuration updated",
64+
"Failed to update an API Key",
65+
);
66+
67+
const handleSubmit = form.handleSubmit((values) => {
68+
const { customAuthentication, recoveryShareManagement } = values;
69+
if (
70+
recoveryShareManagement === "USER_MANAGED" &&
71+
(!customAuthentication?.aud.length ||
72+
!customAuthentication?.jwksUri.length)
73+
) {
74+
return toast({
75+
title: "Custom JSON Web Token configuration is invalid",
76+
description:
77+
"To use Embedded Wallets with Custom JSON Web Token, provide JWKS URI and AUD.",
78+
position: "bottom",
79+
variant: "solid",
80+
status: "error",
81+
duration: 9000,
82+
isClosable: true,
83+
});
84+
}
85+
86+
trackEvent({
87+
category: "embedded-wallet",
88+
action: "configuration-update",
89+
label: "attempt",
90+
});
91+
92+
const { id, name, domains, bundleIds, redirectUrls } = apiKey;
93+
94+
// FIXME: This must match components/settings/ApiKeys/Edit/index.tsx
95+
// Make it more generic w/o me thinking of values
96+
const newServices = [...services];
97+
newServices[serviceIdx] = {
98+
...services[serviceIdx],
99+
...values,
100+
};
101+
102+
const formattedValues = {
103+
id,
104+
name,
105+
domains,
106+
bundleIds,
107+
redirectUrls,
108+
services: newServices,
109+
};
110+
111+
mutation.mutate(formattedValues, {
112+
onSuccess: () => {
113+
onSuccess();
114+
trackEvent({
115+
category: "embedded-wallet",
116+
action: "configuration-update",
117+
label: "success",
118+
});
119+
},
120+
onError: (err) => {
121+
onError(err);
122+
trackEvent({
123+
category: "embedded-wallet",
124+
action: "configuration-update",
125+
label: "error",
126+
error: err,
127+
});
128+
},
129+
});
130+
});
131+
132+
return (
133+
<Flex flexDir="column">
134+
<Flex flexDir="column" gap={6}>
135+
<Heading size="title.sm">Authentication</Heading>
136+
137+
<form
138+
onSubmit={(e) => {
139+
e.preventDefault();
140+
handleSubmit();
141+
}}
142+
autoComplete="off"
143+
>
144+
<Flex flexDir="column" gap={8}>
145+
<FormControl>
146+
<HStack justifyContent="space-between" alignItems="flex-start">
147+
<Box>
148+
<FormLabel mt={3}>Custom JSON Web Token</FormLabel>
149+
<Text>
150+
Optionally allow users to authenticate with a custom JWT.
151+
</Text>
152+
</Box>
153+
154+
<Switch
155+
colorScheme="primary"
156+
isChecked={!!form.watch("customAuthentication")}
157+
onChange={() => {
158+
form.setValue(
159+
"recoveryShareManagement",
160+
!form.watch("customAuthentication")
161+
? "USER_MANAGED"
162+
: "AWS_MANAGED",
163+
{ shouldDirty: true },
164+
);
165+
form.setValue(
166+
"customAuthentication",
167+
!form.watch("customAuthentication")
168+
? {
169+
jwksUri: "",
170+
aud: "",
171+
}
172+
: undefined,
173+
{ shouldDirty: true },
174+
);
175+
}}
176+
/>
177+
</HStack>
178+
</FormControl>
179+
180+
{form.watch("recoveryShareManagement") === "USER_MANAGED" && (
181+
<Card p={6} bg={bg}>
182+
<Flex flexDir={{ base: "column", md: "row" }} gap={4}>
183+
<FormControl
184+
isInvalid={
185+
!!form.getFieldState(
186+
"customAuthentication.jwksUri",
187+
form.formState,
188+
).error
189+
}
190+
>
191+
<FormLabel size="label.sm">JWKS URI</FormLabel>
192+
<Input
193+
placeholder="https://example.com/.well-known/jwks.json"
194+
type="text"
195+
{...form.register("customAuthentication.jwksUri")}
196+
/>
197+
{!form.getFieldState(
198+
"customAuthentication.jwksUri",
199+
form.formState,
200+
).error ? (
201+
<FormHelperText>Enter the URI of the JWKS</FormHelperText>
202+
) : (
203+
<FormErrorMessage>
204+
{
205+
form.getFieldState(
206+
"customAuthentication.jwksUri",
207+
form.formState,
208+
).error?.message
209+
}
210+
</FormErrorMessage>
211+
)}
212+
</FormControl>
213+
<FormControl
214+
isInvalid={
215+
!!form.getFieldState(
216+
`customAuthentication.aud`,
217+
form.formState,
218+
).error
219+
}
220+
>
221+
<FormLabel size="label.sm">AUD Value</FormLabel>
222+
<Input
223+
placeholder="AUD"
224+
type="text"
225+
{...form.register(`customAuthentication.aud`)}
226+
/>
227+
{!form.getFieldState(
228+
`customAuthentication.aud`,
229+
form.formState,
230+
).error ? (
231+
<FormHelperText>
232+
Enter the audience claim for the JWT
233+
</FormHelperText>
234+
) : (
235+
<FormErrorMessage>
236+
{
237+
form.getFieldState(
238+
`customAuthentication.aud`,
239+
form.formState,
240+
).error?.message
241+
}
242+
</FormErrorMessage>
243+
)}
244+
</FormControl>
245+
</Flex>
246+
</Card>
247+
)}
248+
249+
<Divider />
250+
251+
<Box alignSelf="flex-end">
252+
<Button type="submit" colorScheme="primary">
253+
Save changes
254+
</Button>
255+
</Box>
256+
</Flex>
257+
</form>
258+
</Flex>
259+
</Flex>
260+
);
261+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi";
2+
import { Box, Flex, useColorModeValue } from "@chakra-ui/react";
3+
import { UsageCard } from "components/settings/Account/UsageCard";
4+
import { useMemo } from "react";
5+
import { Card, Heading, Text, TrackedLink } from "tw-components";
6+
7+
import { toNumber, toPercent } from "utils/number";
8+
9+
interface AnalyticsProps {
10+
trackingCategory: string;
11+
}
12+
13+
export const Analytics: React.FC<AnalyticsProps> = ({ trackingCategory }) => {
14+
const bg = useColorModeValue("backgroundCardHighlight", "transparent");
15+
const usageQuery = useAccountUsage();
16+
17+
const walletsMetrics = useMemo(() => {
18+
if (!usageQuery?.data) {
19+
return {};
20+
}
21+
22+
const usageData = usageQuery.data;
23+
24+
const numOfWallets = usageData.usage.embeddedWallets.countWalletAddresses;
25+
const limitWallets = usageData.limits.embeddedWallets;
26+
const percent = toPercent(numOfWallets, limitWallets);
27+
28+
return {
29+
total: `${toNumber(numOfWallets)} / ${toNumber(
30+
limitWallets,
31+
)} (${percent}%)`,
32+
progress: percent,
33+
...(usageData.billableUsd.embeddedWallets > 0
34+
? {
35+
overage: usageData.billableUsd.embeddedWallets,
36+
}
37+
: {}),
38+
};
39+
}, [usageQuery]);
40+
41+
if (usageQuery.isLoading || !usageQuery.data) {
42+
return null;
43+
}
44+
45+
return (
46+
<Card p={{ base: 6, lg: 12 }} bg={bg}>
47+
<Flex
48+
flexDir={{ base: "column", lg: "row" }}
49+
justifyContent="space-evenly"
50+
gap={6}
51+
>
52+
<Flex flexDir="column" gap={2} justifyContent="center">
53+
<Text size="label.sm">Analytics</Text>
54+
<Heading size="title.sm" maxW="md" lineHeight={1.3}>
55+
View more insights about how users are interacting with your
56+
application
57+
</Heading>
58+
59+
<TrackedLink
60+
color="blue.500"
61+
href="/dashboard/wallets/analytics"
62+
category={trackingCategory}
63+
label="view-analytics"
64+
>
65+
View Analytics
66+
</TrackedLink>
67+
</Flex>
68+
<Box minW={280}>
69+
<UsageCard
70+
{...walletsMetrics}
71+
name="Monthly Active Users"
72+
tooltip="Email wallet (with managed recovery code) usage is calculated by monthly active wallets (i.e. active as defined by at least 1 user log-in via email or social within the billing period month)."
73+
/>
74+
</Box>
75+
</Flex>
76+
</Card>
77+
);
78+
};

0 commit comments

Comments
 (0)