Skip to content
Open
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
209 changes: 92 additions & 117 deletions features/authentication/use-logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { useAppStore } from "@/stores/app.store"
import { captureError } from "@/utils/capture-error"
import { GenericError } from "@/utils/error"
import { clearImageCache } from "@/utils/image"
import { customPromiseAllSettled } from "@/utils/promise-all-settled"
import { clearReacyQueryQueriesAndCache } from "@/utils/react-query/react-query.utils"
import { authLogger } from "../../utils/logger/logger"

Expand All @@ -35,162 +34,138 @@ export const useLogout = () => {
const hasAtLeastOneSender = senders.length > 0

if (hasAtLeastOneSender) {
const [
unsubscribeNotificationsResults,
streamingResult,
unlinkIdentitiesResults,
unregisterPushNotificationsResults,
unregisterBackgroundTaskResult,
] = await customPromiseAllSettled([
// Unsubscribe from conversations notifications
Promise.all(
senders.map((sender) =>
unsubscribeFromAllConversationsNotifications({
clientInboxId: sender.inboxId,
}),
),
),
// Stop streaming
stopStreaming(senders.map((sender) => sender.inboxId)),
// Unlink identities from device
new Promise<void>(async (resolve, reject) => {
try {
const currentUser = getCurrentUserQueryData()
if (!currentUser) {
// Ignore the flow if we don't have a current user
return resolve()
}
const currentDevice = await ensureUserDeviceQueryData({
userId: currentUser.id,
})
const currentUserIdentities = await ensureUserIdentitiesQueryData({
userId: currentUser.id,
})
await Promise.all(
currentUserIdentities.map((identity) =>
unlinkIdentityFromDeviceMutation({
identityId: identity.id,
deviceId: currentDevice.id,
}),
),
)
resolve()
} catch (error) {
reject(error)
}
}),
// Unregister push notifications
Promise.all(
senders.map((sender) =>
unregisterPushNotifications({ clientInboxId: sender.inboxId }),
),
// Fire off all cleanup operations in the background without awaiting
// Unsubscribe from conversations notifications
void Promise.all(
senders.map((sender) =>
unsubscribeFromAllConversationsNotifications({
clientInboxId: sender.inboxId,
}),
),
// Unregister background sync task
unregisterBackgroundSyncTask(),
])

if (unsubscribeNotificationsResults.status === "rejected") {
).catch((error: unknown) => {
captureError(
new GenericError({
error: unsubscribeNotificationsResults.reason,
error,
additionalMessage: "Error unsubscribing from conversations notifications",
}),
)
}
})

if (streamingResult.status === "rejected") {
// Stop streaming
void stopStreaming(senders.map((sender) => sender.inboxId)).catch((error: unknown) => {
captureError(
new GenericError({
error: streamingResult.reason,
error,
additionalMessage: "Error stopping streaming",
}),
)
}

if (unlinkIdentitiesResults.status === "rejected") {
captureError(
new GenericError({
error: unlinkIdentitiesResults.reason,
additionalMessage: "Error unregistering push notifications",
}),
)
}

if (unregisterPushNotificationsResults.status === "rejected") {
})

// Unlink identities from device
void (async () => {
try {
const currentUser = getCurrentUserQueryData()
if (!currentUser) {
return
}
const currentDevice = await ensureUserDeviceQueryData({
userId: currentUser.id,
})
const currentUserIdentities = await ensureUserIdentitiesQueryData({
userId: currentUser.id,
})
await Promise.all(
currentUserIdentities.map((identity) =>
unlinkIdentityFromDeviceMutation({
identityId: identity.id,
deviceId: currentDevice.id,
}),
),
)
} catch (error) {
captureError(
new GenericError({
error,
additionalMessage: "Error unlinking identities from device",
}),
)
}
})()

// Unregister push notifications
void Promise.all(
senders.map((sender) =>
unregisterPushNotifications({ clientInboxId: sender.inboxId }),
),
).catch((error: unknown) => {
captureError(
new GenericError({
error: unregisterPushNotificationsResults.reason,
error,
additionalMessage: "Error unregistering push notifications",
}),
)
}
})

if (unregisterBackgroundTaskResult.status === "rejected") {
// Unregister background sync task
void unregisterBackgroundSyncTask().catch((error: unknown) => {
captureError(
new GenericError({
error: unregisterBackgroundTaskResult.reason,
error,
additionalMessage: "Error unregistering background sync task",
}),
)
}

try {
await Promise.all(
senders.map((sender) =>
logoutXmtpClient({
inboxId: sender.inboxId,
ethAddress: sender.ethereumAddress,
}),
),
)
} catch (error) {
})

// Logout XMTP clients
void Promise.all(
senders.map((sender) =>
logoutXmtpClient({
inboxId: sender.inboxId,
ethAddress: sender.ethereumAddress,
}),
),
).catch((error: unknown) => {
captureError(new GenericError({ error, additionalMessage: "Error logging out xmtp" }))
}
})
}

try {
await clearTurnkeySessions()
} catch (error) {
// Clear Turnkey sessions in the background
void clearTurnkeySessions().catch((error: unknown) => {
captureError(
new GenericError({ error, additionalMessage: "Error clearing turnkey sessions" }),
)
}
})

// Doing this at the end because we want to make sure that we cleared everything before showing auth screen
useAuthenticationStore.getState().actions.setStatus("signedOut")
// Clear image cache in the background
void clearImageCache().catch((error: unknown) => {
captureError(
new GenericError({
error,
additionalMessage: "Error clearing image cache after logout",
}),
)
})

// This needs to be at the end because at many places we use useSafeCurrentSender()
// and it will throw error if we reset the store too early
// Need the setTimeout because for some reason the navigation is not updated immediately when we set auth status to signed out
// Immediately proceed with setting auth status and clearing stores
useAuthenticationStore.getState().actions.setStatus("signedOut")
useMultiInboxStore.getState().actions.reset()

useNotificationsStore.getState().actions.reset()

// Might want to only clear certain queries later but okay for now
// Put this last because otherwise some useQuery hook triggers even tho we're logging out
clearReacyQueryQueriesAndCache()

authLogger.debug("Successfully logged out")
authLogger.debug("Successfully initiated logout")

// Give a small delay before hiding the overlay to ensure smooth transition
setTimeout(() => {
useAppStore.getState().actions.setIsShowingFullScreenOverlay(false)
useAppStore.getState().actions.setIsLoggingOut(false)
}, 500)
} catch (error) {
useAppStore.getState().actions.setIsShowingFullScreenOverlay(false)
useAppStore.getState().actions.setIsLoggingOut(false)
throw new GenericError({
error,
additionalMessage: "Error logging out",
})
} finally {
// Clear expo-image cache after logout for privacy and to avoid stale images
try {
await clearImageCache()
} catch (e) {
captureError(
new GenericError({
error: e,
additionalMessage: "Error clearing image cache after logout",
}),
)
}

useAppStore.getState().actions.setIsShowingFullScreenOverlay(false)
useAppStore.getState().actions.setIsLoggingOut(false)
}
},
[clearTurnkeySessions],
Expand Down