thirdweb 5.77.0 → 5.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/cjs/exports/react.js +4 -2
  2. package/dist/cjs/exports/react.js.map +1 -1
  3. package/dist/cjs/exports/react.native.js +4 -1
  4. package/dist/cjs/exports/react.native.js.map +1 -1
  5. package/dist/cjs/exports/wallets/in-app.js +2 -1
  6. package/dist/cjs/exports/wallets/in-app.js.map +1 -1
  7. package/dist/cjs/exports/wallets/in-app.native.js +2 -1
  8. package/dist/cjs/exports/wallets/in-app.native.js.map +1 -1
  9. package/dist/cjs/exports/wallets.js +2 -1
  10. package/dist/cjs/exports/wallets.js.map +1 -1
  11. package/dist/cjs/exports/wallets.native.js +2 -1
  12. package/dist/cjs/exports/wallets.native.js.map +1 -1
  13. package/dist/cjs/react/native/hooks/wallets/useUnlinkProfile.js +58 -0
  14. package/dist/cjs/react/native/hooks/wallets/useUnlinkProfile.js.map +1 -0
  15. package/dist/cjs/react/web/hooks/wallets/useUnlinkProfile.js +58 -0
  16. package/dist/cjs/react/web/hooks/wallets/useUnlinkProfile.js.map +1 -0
  17. package/dist/cjs/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js +22 -6
  18. package/dist/cjs/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js.map +1 -1
  19. package/dist/cjs/react/web/wallets/in-app/CountrySelector.js +11 -10
  20. package/dist/cjs/react/web/wallets/in-app/CountrySelector.js.map +1 -1
  21. package/dist/cjs/react/web/wallets/in-app/InputSelectionUI.js +3 -1
  22. package/dist/cjs/react/web/wallets/in-app/InputSelectionUI.js.map +1 -1
  23. package/dist/cjs/react/web/wallets/in-app/supported-sms-countries.js.map +1 -1
  24. package/dist/cjs/react/web/wallets/shared/ConnectWalletSocialOptions.js +1 -1
  25. package/dist/cjs/react/web/wallets/shared/ConnectWalletSocialOptions.js.map +1 -1
  26. package/dist/cjs/version.js +1 -1
  27. package/dist/cjs/wallets/in-app/core/authentication/linkAccount.js +31 -0
  28. package/dist/cjs/wallets/in-app/core/authentication/linkAccount.js.map +1 -1
  29. package/dist/cjs/wallets/in-app/native/auth/index.js +30 -3
  30. package/dist/cjs/wallets/in-app/native/auth/index.js.map +1 -1
  31. package/dist/cjs/wallets/in-app/native/native-connector.js +8 -0
  32. package/dist/cjs/wallets/in-app/native/native-connector.js.map +1 -1
  33. package/dist/cjs/wallets/in-app/web/lib/auth/index.js +30 -0
  34. package/dist/cjs/wallets/in-app/web/lib/auth/index.js.map +1 -1
  35. package/dist/cjs/wallets/in-app/web/lib/web-connector.js +8 -0
  36. package/dist/cjs/wallets/in-app/web/lib/web-connector.js.map +1 -1
  37. package/dist/esm/exports/react.js +1 -0
  38. package/dist/esm/exports/react.js.map +1 -1
  39. package/dist/esm/exports/react.native.js +1 -0
  40. package/dist/esm/exports/react.native.js.map +1 -1
  41. package/dist/esm/exports/wallets/in-app.js +1 -1
  42. package/dist/esm/exports/wallets/in-app.js.map +1 -1
  43. package/dist/esm/exports/wallets/in-app.native.js +1 -1
  44. package/dist/esm/exports/wallets/in-app.native.js.map +1 -1
  45. package/dist/esm/exports/wallets.js +1 -1
  46. package/dist/esm/exports/wallets.js.map +1 -1
  47. package/dist/esm/exports/wallets.native.js +1 -1
  48. package/dist/esm/exports/wallets.native.js.map +1 -1
  49. package/dist/esm/react/native/hooks/wallets/useUnlinkProfile.js +55 -0
  50. package/dist/esm/react/native/hooks/wallets/useUnlinkProfile.js.map +1 -0
  51. package/dist/esm/react/web/hooks/wallets/useUnlinkProfile.js +55 -0
  52. package/dist/esm/react/web/hooks/wallets/useUnlinkProfile.js.map +1 -0
  53. package/dist/esm/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js +22 -6
  54. package/dist/esm/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js.map +1 -1
  55. package/dist/esm/react/web/wallets/in-app/CountrySelector.js +10 -10
  56. package/dist/esm/react/web/wallets/in-app/CountrySelector.js.map +1 -1
  57. package/dist/esm/react/web/wallets/in-app/InputSelectionUI.js +4 -2
  58. package/dist/esm/react/web/wallets/in-app/InputSelectionUI.js.map +1 -1
  59. package/dist/esm/react/web/wallets/in-app/supported-sms-countries.js.map +1 -1
  60. package/dist/esm/react/web/wallets/shared/ConnectWalletSocialOptions.js +1 -1
  61. package/dist/esm/react/web/wallets/shared/ConnectWalletSocialOptions.js.map +1 -1
  62. package/dist/esm/version.js +1 -1
  63. package/dist/esm/wallets/in-app/core/authentication/linkAccount.js +30 -0
  64. package/dist/esm/wallets/in-app/core/authentication/linkAccount.js.map +1 -1
  65. package/dist/esm/wallets/in-app/native/auth/index.js +29 -3
  66. package/dist/esm/wallets/in-app/native/auth/index.js.map +1 -1
  67. package/dist/esm/wallets/in-app/native/native-connector.js +9 -1
  68. package/dist/esm/wallets/in-app/native/native-connector.js.map +1 -1
  69. package/dist/esm/wallets/in-app/web/lib/auth/index.js +29 -0
  70. package/dist/esm/wallets/in-app/web/lib/auth/index.js.map +1 -1
  71. package/dist/esm/wallets/in-app/web/lib/web-connector.js +9 -1
  72. package/dist/esm/wallets/in-app/web/lib/web-connector.js.map +1 -1
  73. package/dist/types/exports/react.d.ts +1 -0
  74. package/dist/types/exports/react.d.ts.map +1 -1
  75. package/dist/types/exports/react.native.d.ts +1 -0
  76. package/dist/types/exports/react.native.d.ts.map +1 -1
  77. package/dist/types/exports/wallets/in-app.d.ts +1 -1
  78. package/dist/types/exports/wallets/in-app.d.ts.map +1 -1
  79. package/dist/types/exports/wallets/in-app.native.d.ts +1 -1
  80. package/dist/types/exports/wallets/in-app.native.d.ts.map +1 -1
  81. package/dist/types/exports/wallets.d.ts +1 -1
  82. package/dist/types/exports/wallets.d.ts.map +1 -1
  83. package/dist/types/exports/wallets.native.d.ts +1 -1
  84. package/dist/types/exports/wallets.native.d.ts.map +1 -1
  85. package/dist/types/react/native/hooks/wallets/useUnlinkProfile.d.ts +34 -0
  86. package/dist/types/react/native/hooks/wallets/useUnlinkProfile.d.ts.map +1 -0
  87. package/dist/types/react/web/hooks/wallets/useUnlinkProfile.d.ts +34 -0
  88. package/dist/types/react/web/hooks/wallets/useUnlinkProfile.d.ts.map +1 -0
  89. package/dist/types/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.d.ts.map +1 -1
  90. package/dist/types/react/web/wallets/in-app/CountrySelector.d.ts +2 -0
  91. package/dist/types/react/web/wallets/in-app/CountrySelector.d.ts.map +1 -1
  92. package/dist/types/react/web/wallets/in-app/InputSelectionUI.d.ts +2 -0
  93. package/dist/types/react/web/wallets/in-app/InputSelectionUI.d.ts.map +1 -1
  94. package/dist/types/react/web/wallets/in-app/supported-sms-countries.d.ts +946 -5
  95. package/dist/types/react/web/wallets/in-app/supported-sms-countries.d.ts.map +1 -1
  96. package/dist/types/react/web/wallets/shared/ConnectWalletSocialOptions.d.ts.map +1 -1
  97. package/dist/types/version.d.ts +1 -1
  98. package/dist/types/wallets/ecosystem/types.d.ts +5 -0
  99. package/dist/types/wallets/ecosystem/types.d.ts.map +1 -1
  100. package/dist/types/wallets/in-app/core/authentication/linkAccount.d.ts +13 -0
  101. package/dist/types/wallets/in-app/core/authentication/linkAccount.d.ts.map +1 -1
  102. package/dist/types/wallets/in-app/core/authentication/types.d.ts +5 -0
  103. package/dist/types/wallets/in-app/core/authentication/types.d.ts.map +1 -1
  104. package/dist/types/wallets/in-app/core/interfaces/connector.d.ts +1 -0
  105. package/dist/types/wallets/in-app/core/interfaces/connector.d.ts.map +1 -1
  106. package/dist/types/wallets/in-app/core/wallet/types.d.ts +5 -0
  107. package/dist/types/wallets/in-app/core/wallet/types.d.ts.map +1 -1
  108. package/dist/types/wallets/in-app/native/auth/index.d.ts +27 -4
  109. package/dist/types/wallets/in-app/native/auth/index.d.ts.map +1 -1
  110. package/dist/types/wallets/in-app/native/native-connector.d.ts +4 -3
  111. package/dist/types/wallets/in-app/native/native-connector.d.ts.map +1 -1
  112. package/dist/types/wallets/in-app/web/lib/auth/index.d.ts +27 -1
  113. package/dist/types/wallets/in-app/web/lib/auth/index.d.ts.map +1 -1
  114. package/dist/types/wallets/in-app/web/lib/web-connector.d.ts +4 -3
  115. package/dist/types/wallets/in-app/web/lib/web-connector.d.ts.map +1 -1
  116. package/package.json +1 -1
  117. package/src/exports/react.native.ts +1 -0
  118. package/src/exports/react.ts +1 -0
  119. package/src/exports/wallets/in-app.native.ts +1 -0
  120. package/src/exports/wallets/in-app.ts +1 -0
  121. package/src/exports/wallets.native.ts +1 -0
  122. package/src/exports/wallets.ts +1 -0
  123. package/src/react/native/hooks/wallets/useUnlinkProfile.test.tsx +75 -0
  124. package/src/react/native/hooks/wallets/useUnlinkProfile.ts +62 -0
  125. package/src/react/web/hooks/wallets/useUnlinkProfile.test.tsx +75 -0
  126. package/src/react/web/hooks/wallets/useUnlinkProfile.ts +62 -0
  127. package/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.test.tsx +25 -0
  128. package/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx +86 -44
  129. package/src/react/web/wallets/in-app/CountrySelector.test.tsx +45 -0
  130. package/src/react/web/wallets/in-app/CountrySelector.tsx +16 -13
  131. package/src/react/web/wallets/in-app/InputSelectionUI.test.tsx +45 -0
  132. package/src/react/web/wallets/in-app/InputSelectionUI.tsx +8 -2
  133. package/src/react/web/wallets/in-app/supported-sms-countries.ts +3 -1
  134. package/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx +3 -0
  135. package/src/version.ts +1 -1
  136. package/src/wallets/ecosystem/types.ts +5 -0
  137. package/src/wallets/in-app/core/authentication/linkAccount.test.ts +160 -0
  138. package/src/wallets/in-app/core/authentication/linkAccount.ts +49 -0
  139. package/src/wallets/in-app/core/authentication/types.ts +6 -0
  140. package/src/wallets/in-app/core/interfaces/connector.ts +1 -0
  141. package/src/wallets/in-app/core/wallet/types.ts +5 -0
  142. package/src/wallets/in-app/native/auth/index.ts +31 -3
  143. package/src/wallets/in-app/native/native-connector.ts +11 -0
  144. package/src/wallets/in-app/web/lib/auth/index.ts +31 -0
  145. package/src/wallets/in-app/web/lib/web-connector.ts +11 -0
@@ -0,0 +1,62 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import type { ThirdwebClient } from "../../../../client/client.js";
3
+ import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
4
+ import type { Profile } from "../../../../wallets/in-app/core/authentication/types.js";
5
+ import type { Ecosystem } from "../../../../wallets/in-app/core/wallet/types.js";
6
+ import { unlinkProfile } from "../../../../wallets/in-app/web/lib/auth/index.js";
7
+ import { useConnectedWallets } from "../../../core/hooks/wallets/useConnectedWallets.js";
8
+
9
+ /**
10
+ * Unlinks a web2 or web3 profile currently connected in-app or ecosystem account.
11
+ * **When a profile is unlinked from the account, it will no longer be able to be used to sign into the account.**
12
+ *
13
+ * @example
14
+ *
15
+ * ### Unlinking an email account
16
+ *
17
+ * ```jsx
18
+ * import { useUnlinkProfile } from "thirdweb/react";
19
+ *
20
+ * const { data: connectedProfiles, isLoading } = useProfiles({
21
+ * client: props.client,
22
+ * });
23
+ * const { mutate: unlinkProfile } = useUnlinkProfile();
24
+ *
25
+ * const onClick = () => {
26
+ * unlinkProfile({
27
+ * client,
28
+ * // Select any other profile you want to unlink
29
+ * profileToUnlink: connectedProfiles[1]
30
+ * });
31
+ * };
32
+ * ```
33
+ *
34
+ * @wallet
35
+ */
36
+ export function useUnlinkProfile() {
37
+ const wallets = useConnectedWallets();
38
+ const queryClient = useQueryClient();
39
+ return useMutation({
40
+ mutationFn: async ({
41
+ client,
42
+ profileToUnlink,
43
+ }: { client: ThirdwebClient; profileToUnlink: Profile }) => {
44
+ const ecosystemWallet = wallets.find((w) => isEcosystemWallet(w));
45
+ const ecosystem: Ecosystem | undefined = ecosystemWallet
46
+ ? {
47
+ id: ecosystemWallet.id,
48
+ partnerId: ecosystemWallet.getConfig()?.partnerId,
49
+ }
50
+ : undefined;
51
+
52
+ await unlinkProfile({
53
+ client,
54
+ ecosystem,
55
+ profileToUnlink,
56
+ });
57
+ },
58
+ onSuccess: () => {
59
+ queryClient.invalidateQueries({ queryKey: ["profiles"] });
60
+ },
61
+ });
62
+ }
@@ -131,5 +131,30 @@ describe("LinkedProfilesScreen", () => {
131
131
  render(<LinkedProfilesScreen {...mockProps} />);
132
132
  expect(screen.queryByText("Guest")).not.toBeInTheDocument();
133
133
  });
134
+
135
+ it("should render unlink button when there are multiple profiles", () => {
136
+ vi.mocked(useProfiles).mockReturnValue({
137
+ data: [
138
+ { type: "email", details: { email: "test@example.com" } },
139
+ { type: "google", details: { email: "google@example.com" } },
140
+ ],
141
+ isLoading: false,
142
+ // biome-ignore lint/suspicious/noExplicitAny: Mocking data
143
+ } as any);
144
+
145
+ render(<LinkedProfilesScreen {...mockProps} />);
146
+ expect(screen.getAllByLabelText("Unlink")).toHaveLength(2);
147
+ });
148
+
149
+ it("should not render unlink button when there is only one profile", () => {
150
+ vi.mocked(useProfiles).mockReturnValue({
151
+ data: [{ type: "email", details: { email: "test@example.com" } }],
152
+ isLoading: false,
153
+ // biome-ignore lint/suspicious/noExplicitAny: Mocking data
154
+ } as any);
155
+
156
+ render(<LinkedProfilesScreen {...mockProps} />);
157
+ expect(screen.queryByLabelText("Unlink")).not.toBeInTheDocument();
158
+ });
134
159
  });
135
160
  });
@@ -1,5 +1,7 @@
1
1
  "use client";
2
+ import { Cross2Icon } from "@radix-ui/react-icons";
2
3
  import type { ThirdwebClient } from "../../../../../client/client.js";
4
+ import { useUnlinkProfile } from "../../../../../react/web/hooks/wallets/useUnlinkProfile.js";
3
5
  import { shortenAddress } from "../../../../../utils/address.js";
4
6
  import type { Profile } from "../../../../../wallets/in-app/core/authentication/types.js";
5
7
  import { fontSize, iconSize } from "../../../../core/design-system/index.js";
@@ -10,6 +12,7 @@ import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
10
12
  import { Img } from "../../components/Img.js";
11
13
  import { Spacer } from "../../components/Spacer.js";
12
14
  import { Container, Line, ModalHeader } from "../../components/basic.js";
15
+ import { IconButton } from "../../components/buttons.js";
13
16
  import { Text } from "../../components/text.js";
14
17
  import { Blobbie } from "../Blobbie.js";
15
18
  import { MenuButton } from "../MenuButton.js";
@@ -70,57 +73,61 @@ export function LinkedProfilesScreen(props: {
70
73
  />
71
74
  </Container>
72
75
  <Line />
73
- {isLoading ? (
74
- <LoadingScreen />
75
- ) : (
76
- <Container
77
- scrollY
78
- style={{
79
- height: "300px",
80
- }}
81
- >
82
- <Spacer y="md" />
83
- <Container px="sm">
84
- <MenuButton
85
- onClick={() => {
86
- props.setScreen("link-profile");
87
- }}
88
- style={{
89
- fontSize: fontSize.sm,
90
- }}
91
- >
92
- <AddUserIcon size={iconSize.lg} />
93
- <Text color="primaryText">
94
- {props.locale.manageWallet.linkProfile}
95
- </Text>
96
- </MenuButton>
97
- <Spacer y="xs" />
98
- {/* Exclude guest as a profile */}
99
- {connectedProfiles
100
- ?.filter((profile) => profile.type !== "guest")
101
- .map((profile) => (
102
- <LinkedProfile
103
- key={`${profile.type}-${getProfileDisplayName(profile)}`}
104
- profile={profile}
105
- client={props.client}
106
- />
107
- ))}
108
- </Container>
109
- <Spacer y="md" />
76
+
77
+ <Container
78
+ scrollY
79
+ style={{
80
+ height: "300px",
81
+ }}
82
+ >
83
+ <Spacer y="md" />
84
+ <Container px="sm">
85
+ <MenuButton
86
+ onClick={() => {
87
+ props.setScreen("link-profile");
88
+ }}
89
+ style={{
90
+ fontSize: fontSize.sm,
91
+ }}
92
+ >
93
+ <AddUserIcon size={iconSize.lg} />
94
+ <Text color="primaryText">
95
+ {props.locale.manageWallet.linkProfile}
96
+ </Text>
97
+ </MenuButton>
98
+ <Spacer y="xs" />
99
+ {/* Exclude guest as a profile */}
100
+ {connectedProfiles
101
+ ?.filter((profile) => profile.type !== "guest")
102
+ .map((profile) => (
103
+ <LinkedProfile
104
+ key={`${JSON.stringify(profile)}`}
105
+ enableUnlinking={connectedProfiles.length > 1}
106
+ profile={profile}
107
+ client={props.client}
108
+ />
109
+ ))}
110
110
  </Container>
111
- )}
111
+ <Spacer y="md" />
112
+ </Container>
112
113
  </Container>
113
114
  );
114
115
  }
115
116
 
116
117
  function LinkedProfile({
117
118
  profile,
119
+ enableUnlinking,
118
120
  client,
119
- }: { profile: Profile; client: ThirdwebClient }) {
121
+ }: {
122
+ profile: Profile;
123
+ enableUnlinking: boolean;
124
+ client: ThirdwebClient;
125
+ }) {
120
126
  const { data: socialProfiles } = useSocialProfiles({
121
127
  client,
122
128
  address: profile.details.address,
123
129
  });
130
+ const { mutate: unlinkProfileMutation, isPending } = useUnlinkProfile();
124
131
 
125
132
  return (
126
133
  <MenuButton
@@ -128,6 +135,7 @@ function LinkedProfile({
128
135
  fontSize: fontSize.sm,
129
136
  cursor: "default",
130
137
  }}
138
+ as={"div"}
131
139
  disabled // disabled until we have more data to show on a dedicated profile screen
132
140
  >
133
141
  {socialProfiles?.some((p) => p.avatar) ? (
@@ -180,12 +188,46 @@ function LinkedProfile({
180
188
  {socialProfiles?.find((p) => p.avatar)?.name ||
181
189
  getProfileDisplayName(profile)}
182
190
  </Text>
183
- {socialProfiles?.find((p) => p.avatar)?.name &&
184
- profile.details.address && (
185
- <Text color="secondaryText" size="sm">
186
- {shortenAddress(profile.details.address, 4)}
187
- </Text>
191
+ <div
192
+ style={{
193
+ display: "flex",
194
+ flexDirection: "row",
195
+ alignItems: "center",
196
+ gap: "8px",
197
+ }}
198
+ >
199
+ {socialProfiles?.find((p) => p.avatar)?.name &&
200
+ profile.details.address && (
201
+ <Text color="secondaryText" size="sm">
202
+ {shortenAddress(profile.details.address, 4)}
203
+ </Text>
204
+ )}
205
+ {enableUnlinking && (
206
+ <IconButton
207
+ autoFocus
208
+ type="button"
209
+ aria-label="Unlink"
210
+ onClick={() =>
211
+ unlinkProfileMutation({
212
+ client,
213
+ profileToUnlink: profile,
214
+ })
215
+ }
216
+ style={{
217
+ pointerEvents: "auto",
218
+ }}
219
+ disabled={isPending}
220
+ >
221
+ <Cross2Icon
222
+ width={iconSize.md}
223
+ height={iconSize.md}
224
+ style={{
225
+ color: "inherit",
226
+ }}
227
+ />
228
+ </IconButton>
188
229
  )}
230
+ </div>
189
231
  </div>
190
232
  </MenuButton>
191
233
  );
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ } from "../../../../../test/src/react-render.js";
7
+ import { CountrySelector } from "./CountrySelector.js";
8
+
9
+ describe("CountrySelector", () => {
10
+ it("renders with default country code", () => {
11
+ const setCountryCode = vi.fn();
12
+ render(
13
+ <CountrySelector countryCode="US +1" setCountryCode={setCountryCode} />,
14
+ );
15
+
16
+ const selectElement = screen.getByRole("combobox");
17
+ expect(selectElement).toBeTruthy();
18
+ expect(selectElement).toHaveValue("US +1");
19
+ });
20
+
21
+ it("changes country code on selection", () => {
22
+ const setCountryCode = vi.fn();
23
+ render(
24
+ <CountrySelector countryCode="US +1" setCountryCode={setCountryCode} />,
25
+ );
26
+
27
+ const selectElement = screen.getByRole("combobox");
28
+ fireEvent.change(selectElement, { target: { value: "CA +1" } });
29
+
30
+ expect(setCountryCode).toHaveBeenCalledWith("CA +1");
31
+ });
32
+
33
+ it("displays all supported countries", () => {
34
+ const setCountryCode = vi.fn();
35
+ render(
36
+ <CountrySelector countryCode="US +1" setCountryCode={setCountryCode} />,
37
+ );
38
+
39
+ const options = screen.getAllByRole("option");
40
+ expect(options.length).toBeGreaterThan(0);
41
+ expect(
42
+ options.some((option) => option.textContent?.includes("United States")),
43
+ ).toBe(true);
44
+ });
45
+ });
@@ -1,9 +1,22 @@
1
1
  "use client";
2
- import { useQuery } from "@tanstack/react-query";
3
2
  import { useRef } from "react";
4
3
  import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js";
5
4
  import { radius, spacing } from "../../../core/design-system/index.js";
6
5
  import { StyledOption, StyledSelect } from "../../ui/design-system/elements.js";
6
+ import {
7
+ type SupportedSmsCountry,
8
+ supportedSmsCountries,
9
+ } from "./supported-sms-countries.js";
10
+
11
+ export function getCountrySelector(countryIsoCode: SupportedSmsCountry) {
12
+ const country = supportedSmsCountries.find(
13
+ (country) => country.countryIsoCode === countryIsoCode,
14
+ );
15
+ if (!country) {
16
+ return "US +1";
17
+ }
18
+ return `${country.countryIsoCode} +${country.phoneNumberCode}`;
19
+ }
7
20
 
8
21
  export function CountrySelector({
9
22
  countryCode,
@@ -14,17 +27,7 @@ export function CountrySelector({
14
27
  }) {
15
28
  const selectRef = useRef<HTMLSelectElement>(null);
16
29
 
17
- const { data: supportedCountries } = useQuery({
18
- queryKey: ["supported-sms-countries"],
19
- queryFn: async () => {
20
- const { supportedSmsCountries } = await import(
21
- "./supported-sms-countries.js"
22
- );
23
- return supportedSmsCountries;
24
- },
25
- });
26
-
27
- const supportedCountriesForSms = supportedCountries ?? [
30
+ const supportedCountriesForSms = supportedSmsCountries ?? [
28
31
  {
29
32
  countryIsoCode: "US",
30
33
  countryName: "United States",
@@ -58,7 +61,7 @@ export function CountrySelector({
58
61
  return (
59
62
  <Option
60
63
  key={country.countryIsoCode}
61
- value={`${country.countryIsoCode} +${country.phoneNumberCode}`}
64
+ value={getCountrySelector(country.countryIsoCode)}
62
65
  >
63
66
  {country.countryName} +{country.phoneNumberCode}
64
67
  </Option>
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen } from "../../../../../test/src/react-render.js";
3
+ import { getCountrySelector } from "./CountrySelector.js";
4
+ import { InputSelectionUI } from "./InputSelectionUI.js";
5
+
6
+ vi.mock("./CountrySelector.js", async (importOriginal) => ({
7
+ ...(await importOriginal()),
8
+ getCountrySelector: vi.fn(),
9
+ }));
10
+
11
+ describe("InputSelectionUI", () => {
12
+ it("should initialize countryCodeInfo with defaultSmsCountryCode", () => {
13
+ const mockGetCountrySelector = vi.mocked(getCountrySelector);
14
+ mockGetCountrySelector.mockReturnValue("CA +1");
15
+
16
+ render(
17
+ <InputSelectionUI
18
+ defaultSmsCountryCode="CA"
19
+ onSelect={vi.fn()}
20
+ placeholder=""
21
+ name=""
22
+ type=""
23
+ submitButtonText=""
24
+ format="phone"
25
+ />,
26
+ );
27
+
28
+ expect(screen.getByRole("combobox")).toHaveValue("CA +1");
29
+ });
30
+
31
+ it('should initialize countryCodeInfo with "US +1" if defaultSmsCountryCode is not provided', () => {
32
+ render(
33
+ <InputSelectionUI
34
+ onSelect={vi.fn()}
35
+ placeholder=""
36
+ name=""
37
+ type=""
38
+ submitButtonText=""
39
+ format="phone"
40
+ />,
41
+ );
42
+
43
+ expect(screen.getByRole("combobox")).toHaveValue("US +1");
44
+ });
45
+ });
@@ -10,7 +10,8 @@ import { Spacer } from "../../ui/components/Spacer.js";
10
10
  import { IconButton } from "../../ui/components/buttons.js";
11
11
  import { Input, InputContainer } from "../../ui/components/formElements.js";
12
12
  import { Text } from "../../ui/components/text.js";
13
- import { CountrySelector } from "./CountrySelector.js";
13
+ import { CountrySelector, getCountrySelector } from "./CountrySelector.js";
14
+ import type { SupportedSmsCountry } from "./supported-sms-countries.js";
14
15
 
15
16
  export function InputSelectionUI(props: {
16
17
  onSelect: (data: string) => void;
@@ -22,8 +23,13 @@ export function InputSelectionUI(props: {
22
23
  submitButtonText: string;
23
24
  format?: "phone";
24
25
  disabled?: boolean;
26
+ defaultSmsCountryCode?: SupportedSmsCountry;
25
27
  }) {
26
- const [countryCodeInfo, setCountryCodeInfo] = useState("US +1");
28
+ const [countryCodeInfo, setCountryCodeInfo] = useState(
29
+ props.defaultSmsCountryCode
30
+ ? getCountrySelector(props.defaultSmsCountryCode)
31
+ : "US +1",
32
+ );
27
33
  const [input, setInput] = useState("");
28
34
  const [error, setError] = useState<string | undefined>();
29
35
  const [showError, setShowError] = useState(false);
@@ -1,3 +1,5 @@
1
+ export type SupportedSmsCountry =
2
+ (typeof supportedSmsCountries)[number]["countryIsoCode"];
1
3
  export const supportedSmsCountries = [
2
4
  {
3
5
  countryIsoCode: "AD",
@@ -1183,4 +1185,4 @@ export const supportedSmsCountries = [
1183
1185
  countryName: "Zimbabwe",
1184
1186
  phoneNumberCode: "263",
1185
1187
  },
1186
- ];
1188
+ ] as const;
@@ -448,6 +448,9 @@ export const ConnectWalletSocialOptions = (
448
448
  disabled={props.disabled}
449
449
  emptyErrorMessage={emptyErrorMessage}
450
450
  submitButtonText={locale.submitEmail}
451
+ defaultSmsCountryCode={
452
+ wallet.getConfig()?.auth?.defaultSmsCountryCode
453
+ }
451
454
  />
452
455
  ) : (
453
456
  <WalletTypeRowButton
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = "5.77.0";
1
+ export const version = "5.78.0";
@@ -1,3 +1,4 @@
1
+ import type { SupportedSmsCountry } from "../../react/web/wallets/in-app/supported-sms-countries.js";
1
2
  import type {
2
3
  InAppWalletAutoConnectOptions,
3
4
  InAppWalletConnectionOptions,
@@ -13,6 +14,10 @@ export type EcosystemWalletCreationOptions = {
13
14
  * Optional url to redirect to after authentication
14
15
  */
15
16
  redirectUrl?: string;
17
+ /**
18
+ * The default country code to use for SMS authentication
19
+ */
20
+ defaultSmsCountryCode?: SupportedSmsCountry;
16
21
  };
17
22
  /**
18
23
  * The partnerId of the ecosystem wallet to connect to
@@ -0,0 +1,160 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createThirdwebClient } from "../../../../client/client.js";
3
+ import { getClientFetch } from "../../../../utils/fetch.js";
4
+ import type { ClientScopedStorage } from "./client-scoped-storage.js";
5
+ import {
6
+ getLinkedProfilesInternal,
7
+ linkAccount,
8
+ unlinkAccount,
9
+ } from "./linkAccount.js";
10
+ import type { Profile } from "./types.js";
11
+
12
+ vi.mock("../../../../utils/fetch.js");
13
+
14
+ describe("Account linking functions", () => {
15
+ const mockClient = createThirdwebClient({ clientId: "mock-client-id" });
16
+ const mockStorage = {
17
+ getAuthCookie: vi.fn(),
18
+ } as unknown as ClientScopedStorage;
19
+ const mockFetch = vi.fn();
20
+ const mockLinkedAccounts = [
21
+ { type: "email", details: { email: "user@example.com" } },
22
+ { type: "phone", details: { phone: "1234567890" } },
23
+ { type: "wallet", details: { address: "0x123456789" } },
24
+ ] satisfies Profile[];
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ vi.mocked(getClientFetch).mockReturnValue(mockFetch);
29
+ vi.mocked(mockStorage.getAuthCookie).mockResolvedValue("mock-token");
30
+ mockFetch.mockResolvedValue({
31
+ ok: true,
32
+ json: () => Promise.resolve({ linkedAccounts: mockLinkedAccounts }),
33
+ });
34
+ });
35
+
36
+ describe("linkAccount", () => {
37
+ it("should successfully link an account", async () => {
38
+ const result = await linkAccount({
39
+ client: mockClient,
40
+ tokenToLink: "token-to-link",
41
+ storage: mockStorage,
42
+ });
43
+
44
+ expect(mockFetch).toHaveBeenCalledWith(
45
+ "https://embedded-wallet.thirdweb.com/api/2024-05-05/account/connect",
46
+ {
47
+ method: "POST",
48
+ headers: {
49
+ Authorization: "Bearer iaw-auth-token:mock-token",
50
+ "Content-Type": "application/json",
51
+ },
52
+ body: JSON.stringify({
53
+ accountAuthTokenToConnect: "token-to-link",
54
+ }),
55
+ },
56
+ );
57
+ expect(result).toEqual(mockLinkedAccounts);
58
+ });
59
+
60
+ it("should throw error when no user is logged in", async () => {
61
+ vi.mocked(mockStorage.getAuthCookie).mockResolvedValue(null);
62
+
63
+ await expect(
64
+ linkAccount({
65
+ client: mockClient,
66
+ tokenToLink: "token-to-link",
67
+ storage: mockStorage,
68
+ }),
69
+ ).rejects.toThrow("Failed to link account, no user logged in");
70
+ });
71
+ });
72
+
73
+ describe("unlinkAccount", () => {
74
+ const profileToUnlink = {
75
+ type: "email",
76
+ details: { email: "user@example.com" },
77
+ } satisfies Profile;
78
+ it("should successfully unlink an account", async () => {
79
+ const result = await unlinkAccount({
80
+ client: mockClient,
81
+ profileToUnlink,
82
+ storage: mockStorage,
83
+ });
84
+
85
+ expect(mockFetch).toHaveBeenCalledWith(
86
+ "https://embedded-wallet.thirdweb.com/api/2024-05-05/account/disconnect",
87
+ {
88
+ method: "POST",
89
+ headers: {
90
+ Authorization: "Bearer iaw-auth-token:mock-token",
91
+ "Content-Type": "application/json",
92
+ },
93
+ body: JSON.stringify(profileToUnlink),
94
+ },
95
+ );
96
+ expect(result).toEqual(mockLinkedAccounts);
97
+ });
98
+
99
+ it("should throw error when no user is logged in", async () => {
100
+ vi.mocked(mockStorage.getAuthCookie).mockResolvedValue(null);
101
+
102
+ await expect(
103
+ unlinkAccount({
104
+ client: mockClient,
105
+ profileToUnlink,
106
+ storage: mockStorage,
107
+ }),
108
+ ).rejects.toThrow("Failed to unlink account, no user logged in");
109
+ });
110
+ it("should handle API errors", async () => {
111
+ mockFetch.mockResolvedValue({
112
+ ok: false,
113
+ json: () => Promise.resolve({ message: "API Error" }),
114
+ });
115
+
116
+ await expect(
117
+ unlinkAccount({
118
+ client: mockClient,
119
+ profileToUnlink,
120
+ storage: mockStorage,
121
+ }),
122
+ ).rejects.toThrow("API Error");
123
+ });
124
+ });
125
+
126
+ describe("getLinkedProfilesInternal", () => {
127
+ it("should successfully get linked profiles", async () => {
128
+ const result = await getLinkedProfilesInternal({
129
+ client: mockClient,
130
+ storage: mockStorage,
131
+ });
132
+
133
+ expect(mockFetch).toHaveBeenCalledWith(
134
+ "https://embedded-wallet.thirdweb.com/api/2024-05-05/accounts",
135
+ {
136
+ method: "GET",
137
+ headers: {
138
+ Authorization: "Bearer iaw-auth-token:mock-token",
139
+ "Content-Type": "application/json",
140
+ },
141
+ },
142
+ );
143
+ expect(result).toEqual(mockLinkedAccounts);
144
+ });
145
+
146
+ it("should handle API errors", async () => {
147
+ mockFetch.mockResolvedValue({
148
+ ok: false,
149
+ json: () => Promise.resolve({ message: "API Error" }),
150
+ });
151
+
152
+ await expect(
153
+ getLinkedProfilesInternal({
154
+ client: mockClient,
155
+ storage: mockStorage,
156
+ }),
157
+ ).rejects.toThrow("API Error");
158
+ });
159
+ });
160
+ });