Skip to content

Commit f81fd46

Browse files
authored
add SMS country selection for in-app wallets (#6502)
1 parent d6ce3ae commit f81fd46

File tree

10 files changed

+797
-12
lines changed

10 files changed

+797
-12
lines changed

.changeset/bumpy-ducks-travel.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/service-utils": patch
3+
---
4+
5+
expose project.services.iaw.smsEnabledCountryISOs from api response

apps/dashboard/src/@/api/sms.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
2+
3+
export type SMSCountryTiers = {
4+
tier1: string[];
5+
tier2: string[];
6+
tier3: string[];
7+
tier4: string[];
8+
tier5: string[];
9+
};
10+
11+
export async function getSMSCountryTiers() {
12+
if (!THIRDWEB_API_SECRET) {
13+
throw new Error("API_SERVER_SECRET is not set");
14+
}
15+
const res = await fetch(`${API_SERVER_URL}/v1/sms/list-country-tiers`, {
16+
headers: {
17+
"Content-Type": "application/json",
18+
"x-service-api-key": THIRDWEB_API_SECRET,
19+
},
20+
next: {
21+
revalidate: 15 * 60, //15 minutes
22+
},
23+
});
24+
25+
if (!res.ok) {
26+
console.error(
27+
"Failed to fetch sms country tiers",
28+
res.status,
29+
res.statusText,
30+
);
31+
res.body?.cancel();
32+
return {
33+
tier1: [],
34+
tier2: [],
35+
tier3: [],
36+
tier4: [],
37+
tier5: [],
38+
};
39+
}
40+
41+
try {
42+
return (await res.json()).data as SMSCountryTiers;
43+
} catch (e) {
44+
console.error("Failed to parse sms country tiers", e);
45+
return {
46+
tier1: [],
47+
tier2: [],
48+
tier3: [],
49+
tier4: [],
50+
tier5: [],
51+
};
52+
}
53+
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/settings/page.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getProject } from "@/api/projects";
2+
import { getSMSCountryTiers } from "@/api/sms";
23
import { getTeamBySlug } from "@/api/team";
34
import { redirect } from "next/navigation";
45
import { InAppWalletSettingsPage } from "../../../../../../../components/embedded-wallets/Configure";
@@ -9,9 +10,10 @@ export default async function Page(props: {
910
}) {
1011
const { team_slug, project_slug } = await props.params;
1112

12-
const [team, project] = await Promise.all([
13+
const [team, project, smsCountryTiers] = await Promise.all([
1314
getTeamBySlug(team_slug),
1415
getProject(team_slug, project_slug),
16+
getSMSCountryTiers(),
1517
]);
1618

1719
if (!team) {
@@ -29,6 +31,7 @@ export default async function Page(props: {
2931
trackingCategory="in-app-wallet-project-settings"
3032
teamSlug={team_slug}
3133
validTeamPlan={getValidTeamPlan(team)}
34+
smsCountryTiers={smsCountryTiers}
3235
/>
3336
);
3437
}

apps/dashboard/src/components/embedded-wallets/Configure/InAppWalletSettingsUI.stories.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ function Variants(props: {
6464
isUpdating={false}
6565
trackingCategory="foo"
6666
updateApiKey={() => {}}
67+
smsCountryTiers={{
68+
// scaffold some countries to play around with the UI
69+
tier1: ["US", "CA"],
70+
tier2: ["GB", "AU", "NZ"],
71+
tier3: ["FR", "DE", "ES", "IT"],
72+
tier4: ["JP", "KR", "MX", "RU"],
73+
tier5: ["BR", "AR", "CO", "CL", "PE", "VE", "SA"],
74+
}}
6775
/>
6876
</div>
6977
</div>

apps/dashboard/src/components/embedded-wallets/Configure/index.tsx

+76-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import type { Project } from "@/api/projects";
4+
import type { SMSCountryTiers } from "@/api/sms";
45
import { DynamicHeight } from "@/components/ui/DynamicHeight";
56
import { Spinner } from "@/components/ui/Spinner/Spinner";
67
import { UnderlineLink } from "@/components/ui/UnderlineLink";
@@ -36,13 +37,15 @@ import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
3637
import { toast } from "sonner";
3738
import { toArrFromList } from "utils/string";
3839
import type { Team } from "../../../@/api/team";
40+
import CountrySelector from "./sms-country-select/country-selector";
3941

4042
type InAppWalletSettingsPageProps = {
4143
trackingCategory: string;
4244
project: Project;
4345
teamId: string;
4446
teamSlug: string;
4547
validTeamPlan: Team["billingPlan"];
48+
smsCountryTiers: SMSCountryTiers;
4649
};
4750

4851
const TRACKING_CATEGORY = "embedded-wallet";
@@ -108,6 +111,7 @@ export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) {
108111
canEditAdvancedFeatures={props.validTeamPlan !== "free"}
109112
updateApiKey={handleUpdateProject}
110113
isUpdating={updateProject.isPending}
114+
smsCountryTiers={props.smsCountryTiers}
111115
/>
112116
);
113117
}
@@ -120,6 +124,7 @@ const InAppWalletSettingsPageUI: React.FC<
120124
trackingData: UpdateAPIKeyTrackingData,
121125
) => void;
122126
isUpdating: boolean;
127+
smsCountryTiers: SMSCountryTiers;
123128
}
124129
> = (props) => {
125130
const embeddedWalletService = props.project.services.find(
@@ -185,6 +190,11 @@ export const InAppWalletSettingsUI: React.FC<
185190
}
186191
: undefined),
187192
redirectUrls: (config.redirectUrls || []).join("\n"),
193+
smsEnabledCountryISOs: config.smsEnabledCountryISOs
194+
? config.smsEnabledCountryISOs
195+
: canEditAdvancedFeatures
196+
? ["US", "CA"]
197+
: [],
188198
},
189199
});
190200

@@ -228,6 +238,7 @@ export const InAppWalletSettingsUI: React.FC<
228238
applicationImageUrl: branding?.applicationImageUrl,
229239
applicationName: branding?.applicationName || props.project.name,
230240
redirectUrls: toArrFromList(redirectUrls || "", true),
241+
smsEnabledCountryISOs: values.smsEnabledCountryISOs,
231242
};
232243
});
233244

@@ -256,6 +267,8 @@ export const InAppWalletSettingsUI: React.FC<
256267
canEditAdvancedFeatures={canEditAdvancedFeatures}
257268
/>
258269

270+
<NativeAppsFieldset form={form} />
271+
259272
{/* Authentication */}
260273
<Fieldset legend="Authentication">
261274
<JSONWebTokenFields
@@ -269,9 +282,15 @@ export const InAppWalletSettingsUI: React.FC<
269282
form={form}
270283
canEditAdvancedFeatures={canEditAdvancedFeatures}
271284
/>
272-
</Fieldset>
273285

274-
<NativeAppsFieldset form={form} />
286+
<div className="h-5" />
287+
288+
<SMSCountryFields
289+
form={form}
290+
canEditAdvancedFeatures={canEditAdvancedFeatures}
291+
smsCountryTiers={props.smsCountryTiers}
292+
/>
293+
</Fieldset>
275294

276295
<div className="flex justify-end">
277296
<Button type="submit" variant="primary" className="gap-2">
@@ -364,6 +383,61 @@ function BrandingFieldset(props: {
364383
);
365384
}
366385

386+
function SMSCountryFields(props: {
387+
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
388+
canEditAdvancedFeatures: boolean;
389+
smsCountryTiers: SMSCountryTiers;
390+
}) {
391+
return (
392+
<div>
393+
<SwitchContainer
394+
switchId="sms-switch"
395+
title="SMS"
396+
description="Optionally allow users in selected countries to login via SMS OTP."
397+
>
398+
<GatedSwitch
399+
id="sms-switch"
400+
trackingLabel="sms"
401+
checked={
402+
!!props.form.watch("smsEnabledCountryISOs").length &&
403+
props.canEditAdvancedFeatures
404+
}
405+
upgradeRequired={!props.canEditAdvancedFeatures}
406+
onCheckedChange={(checked) =>
407+
props.form.setValue(
408+
"smsEnabledCountryISOs",
409+
checked
410+
? // by default, enable US and CA only
411+
["US", "CA"]
412+
: [],
413+
)
414+
}
415+
/>
416+
</SwitchContainer>
417+
418+
<AdvancedConfigurationContainer
419+
className="grid grid-cols-1"
420+
show={
421+
props.canEditAdvancedFeatures &&
422+
!!props.form.watch("smsEnabledCountryISOs").length
423+
}
424+
>
425+
<FormField
426+
control={props.form.control}
427+
name="smsEnabledCountryISOs"
428+
render={({ field }) => (
429+
<CountrySelector
430+
countryTiers={props.smsCountryTiers}
431+
selected={field.value}
432+
onChange={field.onChange}
433+
/>
434+
)}
435+
/>
436+
</AdvancedConfigurationContainer>
437+
</div>
438+
);
439+
}
440+
367441
function JSONWebTokenFields(props: {
368442
form: UseFormReturn<ApiKeyEmbeddedWalletsValidationSchema>;
369443
canEditAdvancedFeatures: boolean;

0 commit comments

Comments
 (0)