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.
- package/dist/cjs/exports/react.js +4 -2
- package/dist/cjs/exports/react.js.map +1 -1
- package/dist/cjs/exports/react.native.js +4 -1
- package/dist/cjs/exports/react.native.js.map +1 -1
- package/dist/cjs/exports/wallets/in-app.js +2 -1
- package/dist/cjs/exports/wallets/in-app.js.map +1 -1
- package/dist/cjs/exports/wallets/in-app.native.js +2 -1
- package/dist/cjs/exports/wallets/in-app.native.js.map +1 -1
- package/dist/cjs/exports/wallets.js +2 -1
- package/dist/cjs/exports/wallets.js.map +1 -1
- package/dist/cjs/exports/wallets.native.js +2 -1
- package/dist/cjs/exports/wallets.native.js.map +1 -1
- package/dist/cjs/react/native/hooks/wallets/useUnlinkProfile.js +58 -0
- package/dist/cjs/react/native/hooks/wallets/useUnlinkProfile.js.map +1 -0
- package/dist/cjs/react/web/hooks/wallets/useUnlinkProfile.js +58 -0
- package/dist/cjs/react/web/hooks/wallets/useUnlinkProfile.js.map +1 -0
- package/dist/cjs/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js +22 -6
- package/dist/cjs/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js.map +1 -1
- package/dist/cjs/react/web/wallets/in-app/CountrySelector.js +11 -10
- package/dist/cjs/react/web/wallets/in-app/CountrySelector.js.map +1 -1
- package/dist/cjs/react/web/wallets/in-app/InputSelectionUI.js +3 -1
- package/dist/cjs/react/web/wallets/in-app/InputSelectionUI.js.map +1 -1
- package/dist/cjs/react/web/wallets/in-app/supported-sms-countries.js.map +1 -1
- package/dist/cjs/react/web/wallets/shared/ConnectWalletSocialOptions.js +1 -1
- package/dist/cjs/react/web/wallets/shared/ConnectWalletSocialOptions.js.map +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/wallets/in-app/core/authentication/linkAccount.js +31 -0
- package/dist/cjs/wallets/in-app/core/authentication/linkAccount.js.map +1 -1
- package/dist/cjs/wallets/in-app/native/auth/index.js +30 -3
- package/dist/cjs/wallets/in-app/native/auth/index.js.map +1 -1
- package/dist/cjs/wallets/in-app/native/native-connector.js +8 -0
- package/dist/cjs/wallets/in-app/native/native-connector.js.map +1 -1
- package/dist/cjs/wallets/in-app/web/lib/auth/index.js +30 -0
- package/dist/cjs/wallets/in-app/web/lib/auth/index.js.map +1 -1
- package/dist/cjs/wallets/in-app/web/lib/web-connector.js +8 -0
- package/dist/cjs/wallets/in-app/web/lib/web-connector.js.map +1 -1
- package/dist/esm/exports/react.js +1 -0
- package/dist/esm/exports/react.js.map +1 -1
- package/dist/esm/exports/react.native.js +1 -0
- package/dist/esm/exports/react.native.js.map +1 -1
- package/dist/esm/exports/wallets/in-app.js +1 -1
- package/dist/esm/exports/wallets/in-app.js.map +1 -1
- package/dist/esm/exports/wallets/in-app.native.js +1 -1
- package/dist/esm/exports/wallets/in-app.native.js.map +1 -1
- package/dist/esm/exports/wallets.js +1 -1
- package/dist/esm/exports/wallets.js.map +1 -1
- package/dist/esm/exports/wallets.native.js +1 -1
- package/dist/esm/exports/wallets.native.js.map +1 -1
- package/dist/esm/react/native/hooks/wallets/useUnlinkProfile.js +55 -0
- package/dist/esm/react/native/hooks/wallets/useUnlinkProfile.js.map +1 -0
- package/dist/esm/react/web/hooks/wallets/useUnlinkProfile.js +55 -0
- package/dist/esm/react/web/hooks/wallets/useUnlinkProfile.js.map +1 -0
- package/dist/esm/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js +22 -6
- package/dist/esm/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.js.map +1 -1
- package/dist/esm/react/web/wallets/in-app/CountrySelector.js +10 -10
- package/dist/esm/react/web/wallets/in-app/CountrySelector.js.map +1 -1
- package/dist/esm/react/web/wallets/in-app/InputSelectionUI.js +4 -2
- package/dist/esm/react/web/wallets/in-app/InputSelectionUI.js.map +1 -1
- package/dist/esm/react/web/wallets/in-app/supported-sms-countries.js.map +1 -1
- package/dist/esm/react/web/wallets/shared/ConnectWalletSocialOptions.js +1 -1
- package/dist/esm/react/web/wallets/shared/ConnectWalletSocialOptions.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/wallets/in-app/core/authentication/linkAccount.js +30 -0
- package/dist/esm/wallets/in-app/core/authentication/linkAccount.js.map +1 -1
- package/dist/esm/wallets/in-app/native/auth/index.js +29 -3
- package/dist/esm/wallets/in-app/native/auth/index.js.map +1 -1
- package/dist/esm/wallets/in-app/native/native-connector.js +9 -1
- package/dist/esm/wallets/in-app/native/native-connector.js.map +1 -1
- package/dist/esm/wallets/in-app/web/lib/auth/index.js +29 -0
- package/dist/esm/wallets/in-app/web/lib/auth/index.js.map +1 -1
- package/dist/esm/wallets/in-app/web/lib/web-connector.js +9 -1
- package/dist/esm/wallets/in-app/web/lib/web-connector.js.map +1 -1
- package/dist/types/exports/react.d.ts +1 -0
- package/dist/types/exports/react.d.ts.map +1 -1
- package/dist/types/exports/react.native.d.ts +1 -0
- package/dist/types/exports/react.native.d.ts.map +1 -1
- package/dist/types/exports/wallets/in-app.d.ts +1 -1
- package/dist/types/exports/wallets/in-app.d.ts.map +1 -1
- package/dist/types/exports/wallets/in-app.native.d.ts +1 -1
- package/dist/types/exports/wallets/in-app.native.d.ts.map +1 -1
- package/dist/types/exports/wallets.d.ts +1 -1
- package/dist/types/exports/wallets.d.ts.map +1 -1
- package/dist/types/exports/wallets.native.d.ts +1 -1
- package/dist/types/exports/wallets.native.d.ts.map +1 -1
- package/dist/types/react/native/hooks/wallets/useUnlinkProfile.d.ts +34 -0
- package/dist/types/react/native/hooks/wallets/useUnlinkProfile.d.ts.map +1 -0
- package/dist/types/react/web/hooks/wallets/useUnlinkProfile.d.ts +34 -0
- package/dist/types/react/web/hooks/wallets/useUnlinkProfile.d.ts.map +1 -0
- package/dist/types/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.d.ts.map +1 -1
- package/dist/types/react/web/wallets/in-app/CountrySelector.d.ts +2 -0
- package/dist/types/react/web/wallets/in-app/CountrySelector.d.ts.map +1 -1
- package/dist/types/react/web/wallets/in-app/InputSelectionUI.d.ts +2 -0
- package/dist/types/react/web/wallets/in-app/InputSelectionUI.d.ts.map +1 -1
- package/dist/types/react/web/wallets/in-app/supported-sms-countries.d.ts +946 -5
- package/dist/types/react/web/wallets/in-app/supported-sms-countries.d.ts.map +1 -1
- package/dist/types/react/web/wallets/shared/ConnectWalletSocialOptions.d.ts.map +1 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/types/wallets/ecosystem/types.d.ts +5 -0
- package/dist/types/wallets/ecosystem/types.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/authentication/linkAccount.d.ts +13 -0
- package/dist/types/wallets/in-app/core/authentication/linkAccount.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/authentication/types.d.ts +5 -0
- package/dist/types/wallets/in-app/core/authentication/types.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/interfaces/connector.d.ts +1 -0
- package/dist/types/wallets/in-app/core/interfaces/connector.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/wallet/types.d.ts +5 -0
- package/dist/types/wallets/in-app/core/wallet/types.d.ts.map +1 -1
- package/dist/types/wallets/in-app/native/auth/index.d.ts +27 -4
- package/dist/types/wallets/in-app/native/auth/index.d.ts.map +1 -1
- package/dist/types/wallets/in-app/native/native-connector.d.ts +4 -3
- package/dist/types/wallets/in-app/native/native-connector.d.ts.map +1 -1
- package/dist/types/wallets/in-app/web/lib/auth/index.d.ts +27 -1
- package/dist/types/wallets/in-app/web/lib/auth/index.d.ts.map +1 -1
- package/dist/types/wallets/in-app/web/lib/web-connector.d.ts +4 -3
- package/dist/types/wallets/in-app/web/lib/web-connector.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/exports/react.native.ts +1 -0
- package/src/exports/react.ts +1 -0
- package/src/exports/wallets/in-app.native.ts +1 -0
- package/src/exports/wallets/in-app.ts +1 -0
- package/src/exports/wallets.native.ts +1 -0
- package/src/exports/wallets.ts +1 -0
- package/src/react/native/hooks/wallets/useUnlinkProfile.test.tsx +75 -0
- package/src/react/native/hooks/wallets/useUnlinkProfile.ts +62 -0
- package/src/react/web/hooks/wallets/useUnlinkProfile.test.tsx +75 -0
- package/src/react/web/hooks/wallets/useUnlinkProfile.ts +62 -0
- package/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.test.tsx +25 -0
- package/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx +86 -44
- package/src/react/web/wallets/in-app/CountrySelector.test.tsx +45 -0
- package/src/react/web/wallets/in-app/CountrySelector.tsx +16 -13
- package/src/react/web/wallets/in-app/InputSelectionUI.test.tsx +45 -0
- package/src/react/web/wallets/in-app/InputSelectionUI.tsx +8 -2
- package/src/react/web/wallets/in-app/supported-sms-countries.ts +3 -1
- package/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx +3 -0
- package/src/version.ts +1 -1
- package/src/wallets/ecosystem/types.ts +5 -0
- package/src/wallets/in-app/core/authentication/linkAccount.test.ts +160 -0
- package/src/wallets/in-app/core/authentication/linkAccount.ts +49 -0
- package/src/wallets/in-app/core/authentication/types.ts +6 -0
- package/src/wallets/in-app/core/interfaces/connector.ts +1 -0
- package/src/wallets/in-app/core/wallet/types.ts +5 -0
- package/src/wallets/in-app/native/auth/index.ts +31 -3
- package/src/wallets/in-app/native/native-connector.ts +11 -0
- package/src/wallets/in-app/web/lib/auth/index.ts +31 -0
- 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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
>
|
82
|
-
<
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
>
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
}: {
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
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={
|
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(
|
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.
|
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
|
+
});
|