Skip to content

[SDK] Add account deletion support when unlinking profiles #7211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/account-deletion-unlink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"thirdweb": patch
---

**Add account deletion support when unlinking profiles**

Added optional `allowAccountDeletion` parameter to `useUnlinkProfile` hook and `unlinkProfile` function. When set to `true`, this allows deleting the entire account when unlinking the last profile associated with it.

**React Hook Example:**

```tsx
import { useUnlinkProfile } from "thirdweb/react";

const { mutate: unlinkProfile } = useUnlinkProfile();

const handleUnlink = () => {
unlinkProfile({
client,
profileToUnlink: connectedProfiles[0],
allowAccountDeletion: true, // Delete account if last profile
});
};
```

**Direct Function Example:**

```ts
import { unlinkProfile } from "thirdweb/wallets/in-app";

const updatedProfiles = await unlinkProfile({
client,
profileToUnlink: profiles[0],
allowAccountDeletion: true, // Delete account if last profile
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ describe("useUnlinkProfile", () => {
client: TEST_CLIENT,
ecosystem: undefined,
profileToUnlink: mockProfile,
allowAccountDeletion: false,
});
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: ["profiles"],
});
});

it("should call unlinkProfile with allowAccountDeletion if true", async () => {
const { result } = renderHook(() => useUnlinkProfile(), {
wrapper,
});
const mutationFn = result.current.mutateAsync;

await act(async () => {
await mutationFn({
client: TEST_CLIENT,
profileToUnlink: mockProfile,
allowAccountDeletion: true,
});
});

expect(unlinkProfile).toHaveBeenCalledWith({
client: TEST_CLIENT,
ecosystem: undefined,
profileToUnlink: mockProfile,
allowAccountDeletion: true,
});
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: ["profiles"],
Expand Down Expand Up @@ -70,6 +96,7 @@ describe("useUnlinkProfile", () => {
?.partnerId,
},
profileToUnlink: mockProfile,
allowAccountDeletion: false,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ import { useConnectedWallets } from "../../../core/hooks/wallets/useConnectedWal
* };
* ```
*
* ### Unlinking an email account with account deletion
*
* ```jsx
* import { useUnlinkProfile } from "thirdweb/react";
*
* const { mutate: unlinkProfile } = useUnlinkProfile();
*
* const onClick = () => {
* unlinkProfile({
* client,
* // Select the profile you want to unlink
* profileToUnlink: connectedProfiles[0],
* allowAccountDeletion: true, // This will delete the account if it's the last profile linked to the account
* });
* };
* ```
*
* @wallet
*/
export function useUnlinkProfile() {
Expand All @@ -40,7 +57,12 @@ export function useUnlinkProfile() {
mutationFn: async ({
client,
profileToUnlink,
}: { client: ThirdwebClient; profileToUnlink: Profile }) => {
allowAccountDeletion = false,
}: {
client: ThirdwebClient;
profileToUnlink: Profile;
allowAccountDeletion?: boolean;
}) => {
const ecosystemWallet = wallets.find((w) => isEcosystemWallet(w));
const ecosystem: Ecosystem | undefined = ecosystemWallet
? {
Expand All @@ -53,6 +75,7 @@ export function useUnlinkProfile() {
client,
ecosystem,
profileToUnlink,
allowAccountDeletion,
});
},
onSuccess: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe("useUnlinkProfile", () => {
client: TEST_CLIENT,
ecosystem: undefined,
profileToUnlink: mockProfile,
allowAccountDeletion: false,
});
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: ["profiles"],
Expand Down Expand Up @@ -70,6 +71,7 @@ describe("useUnlinkProfile", () => {
?.partnerId,
},
profileToUnlink: mockProfile,
allowAccountDeletion: false,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ import { useConnectedWallets } from "../../../core/hooks/wallets/useConnectedWal
* };
* ```
*
* ### Unlinking an email account with account deletion
*
* ```jsx
* import { useUnlinkProfile } from "thirdweb/react";
*
* const { mutate: unlinkProfile } = useUnlinkProfile();
*
* const onClick = () => {
* unlinkProfile({
* client,
* // Select the profile you want to unlink
* profileToUnlink: connectedProfiles[0],
* allowAccountDeletion: true, // This will delete the account if it's the last profile linked to the account
* });
* };
* ```
*
* @wallet
*/
export function useUnlinkProfile() {
Expand All @@ -40,7 +57,12 @@ export function useUnlinkProfile() {
mutationFn: async ({
client,
profileToUnlink,
}: { client: ThirdwebClient; profileToUnlink: Profile }) => {
allowAccountDeletion = false,
}: {
client: ThirdwebClient;
profileToUnlink: Profile;
allowAccountDeletion?: boolean;
}) => {
const ecosystemWallet = wallets.find((w) => isEcosystemWallet(w));
const ecosystem: Ecosystem | undefined = ecosystemWallet
? {
Expand All @@ -53,6 +75,7 @@ export function useUnlinkProfile() {
client,
ecosystem,
profileToUnlink,
allowAccountDeletion,
});
},
onSuccess: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,37 @@ describe("Account linking functions", () => {
Authorization: "Bearer iaw-auth-token:mock-token",
"Content-Type": "application/json",
},
body: JSON.stringify(profileToUnlink),
body: JSON.stringify({
type: profileToUnlink.type,
details: profileToUnlink.details,
allowAccountDeletion: false,
}),
},
);
expect(result).toEqual(mockLinkedAccounts);
});

it("should successfully unlink an account with allowAccountDeletion", async () => {
const result = await unlinkAccount({
client: mockClient,
profileToUnlink,
storage: mockStorage,
allowAccountDeletion: true,
});

expect(mockFetch).toHaveBeenCalledWith(
"https://embedded-wallet.thirdweb.com/api/2024-05-05/account/disconnect",
{
method: "POST",
headers: {
Authorization: "Bearer iaw-auth-token:mock-token",
"Content-Type": "application/json",
},
body: JSON.stringify({
type: profileToUnlink.type,
details: profileToUnlink.details,
allowAccountDeletion: true,
}),
},
);
expect(result).toEqual(mockLinkedAccounts);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ export async function unlinkAccount({
client,
ecosystem,
profileToUnlink,
allowAccountDeletion = false,
storage,
}: {
client: ThirdwebClient;
ecosystem?: Ecosystem;
profileToUnlink: Profile;
allowAccountDeletion?: boolean;
storage: ClientScopedStorage;
}): Promise<Profile[]> {
const clientFetch = getClientFetch(client, ecosystem);
Expand All @@ -90,7 +92,11 @@ export async function unlinkAccount({
{
method: "POST",
headers,
body: stringify(profileToUnlink),
body: stringify({
type: profileToUnlink.type,
details: profileToUnlink.details,
allowAccountDeletion,
}),
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,5 @@ export type UnlinkParams = {
client: ThirdwebClient;
ecosystem?: Ecosystem;
profileToUnlink: Profile;
allowAccountDeletion?: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ export interface InAppConnector {
): Promise<AuthLoginReturnType>;
logout(): Promise<LogoutReturnType>;
linkProfile(args: AuthArgsType): Promise<Profile[]>;
unlinkProfile(args: Profile): Promise<Profile[]>;
unlinkProfile(
args: Profile,
allowAccountDeletion?: boolean,
): Promise<Profile[]>;
getProfiles(): Promise<Profile[]>;
storage: ClientScopedStorage;
}
5 changes: 4 additions & 1 deletion packages/thirdweb/src/wallets/in-app/native/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,10 @@ export async function linkProfile(args: AuthArgsType) {
*/
export async function unlinkProfile(args: UnlinkParams) {
const connector = await getInAppWalletConnector(args.client, args.ecosystem);
return await connector.unlinkProfile(args.profileToUnlink);
return await connector.unlinkProfile(
args.profileToUnlink,
args.allowAccountDeletion,
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export class InAppNativeConnector implements InAppConnector {
});
}

async unlinkProfile(profile: Profile) {
async unlinkProfile(profile: Profile, allowAccountDeletion?: boolean) {
const { unlinkAccount } = await import(
"../core/authentication/linkAccount.js"
);
Expand All @@ -376,6 +376,7 @@ export class InAppNativeConnector implements InAppConnector {
ecosystem: this.ecosystem,
storage: this.storage,
profileToUnlink: profile,
allowAccountDeletion,
});
}

Expand Down
34 changes: 33 additions & 1 deletion packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@
* @throws If the unlinking fails. This can happen if the account has no other associated profiles or if the profile that is being unlinked doesn't exists for the current logged in user.
*
* @example
* ### Unlinking an authentication method
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
*
Expand All @@ -239,11 +241,41 @@
* profileToUnlink: profiles[0],
* });
* ```
*
* ### Unlinking an authentication for ecosystems
*
* ```ts
* import { unlinkProfile } from "thirdweb/wallets/in-app";
*
* const updatedProfiles = await unlinkProfile({
* client,
* ecosystem: {
* id: "ecosystem.your-ecosystem-id",
* },
* profileToUnlink: profiles[0],
* });
* ```
*
* ### Unlinking an authentication method with account deletion
*
* ```ts
* import { unlinkProfile } from "thirdweb/wallets/in-app";
*
* const updatedProfiles = await unlinkProfile({
* client,
* profileToUnlink: profiles[0],
* allowAccountDeletion: true, // This will delete the account if it's the last profile linked to the account
* });
* ```
*
* @wallet
*/
export async function unlinkProfile(args: UnlinkParams) {
const connector = await getInAppWalletConnector(args.client, args.ecosystem);
return await connector.unlinkProfile(args.profileToUnlink);
return await connector.unlinkProfile(
args.profileToUnlink,
args.allowAccountDeletion,
);

Check warning on line 278 in packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts#L275-L278

Added lines #L275 - L278 were not covered by tests
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,12 +470,13 @@
});
}

async unlinkProfile(profile: Profile) {
async unlinkProfile(profile: Profile, allowAccountDeletion?: boolean) {
return await unlinkAccount({
client: this.client,
storage: this.storage,
ecosystem: this.ecosystem,
profileToUnlink: profile,
allowAccountDeletion,

Check warning on line 479 in packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts#L479

Added line #L479 was not covered by tests
});
}

Expand Down
Loading