Skip to content

Commit 9c2f3a1

Browse files
dapp-client: add sessionless snapshot restore flow
1 parent bb92e56 commit 9c2f3a1

File tree

3 files changed

+132
-4
lines changed

3 files changed

+132
-4
lines changed

packages/wallet/dapp-client/src/DappClient.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { type ExplicitSession, type ExplicitSessionConfig, type ImplicitSession,
55
import { ChainSessionManager } from './ChainSessionManager.js'
66
import { DappTransport } from './DappTransport.js'
77
import { ConnectionError, InitializationError, SigningError, TransactionError } from './utils/errors.js'
8-
import { SequenceStorage, WebStorage } from './utils/storage.js'
8+
import { SequenceStorage, WebStorage, type SessionlessConnectionData } from './utils/storage.js'
99
import {
1010
CreateNewSessionResponse,
1111
DappClientExplicitSessionEventListener,
@@ -85,6 +85,7 @@ export class DappClient {
8585

8686
private walletAddress: Address.Address | null = null
8787
private hasSessionlessConnection = false
88+
private cachedSessionlessConnection: SessionlessConnectionData | null = null
8889
private eventListeners: {
8990
[K in keyof DappClientEventMap]?: Set<DappClientEventMap[K]>
9091
} = {}
@@ -273,10 +274,14 @@ export class DappClient {
273274
private async _loadStateFromStorage(): Promise<void> {
274275
const implicitSession = await this.sequenceStorage.getImplicitSession()
275276

276-
const [explicitSessions, sessionlessConnection] = await Promise.all([
277+
const [explicitSessions, sessionlessConnection, sessionlessSnapshot] = await Promise.all([
277278
this.sequenceStorage.getExplicitSessions(),
278279
this.sequenceStorage.getSessionlessConnection(),
280+
this.sequenceStorage.getSessionlessConnectionSnapshot
281+
? this.sequenceStorage.getSessionlessConnectionSnapshot()
282+
: Promise.resolve(null),
279283
])
284+
this.cachedSessionlessConnection = sessionlessSnapshot ?? null
280285
const chainIdsToInitialize = new Set([
281286
...(implicitSession?.chainId !== undefined ? [implicitSession.chainId] : []),
282287
...explicitSessions.map((s) => s.chainId),
@@ -316,6 +321,10 @@ export class DappClient {
316321
this.userEmail = result[0]?.userEmail || null
317322
this.guard = implicitSession?.guard || explicitSessions.find((s) => !!s.guard)?.guard
318323
await this.sequenceStorage.clearSessionlessConnection()
324+
if (this.sequenceStorage.clearSessionlessConnectionSnapshot) {
325+
await this.sequenceStorage.clearSessionlessConnectionSnapshot()
326+
}
327+
this.cachedSessionlessConnection = null
319328

320329
this.isInitialized = true
321330
this.emit('sessionsUpdated')
@@ -369,6 +378,63 @@ export class DappClient {
369378
}
370379
}
371380

381+
/**
382+
* Indicates if there is cached sessionless connection data that can be restored.
383+
*/
384+
public async hasRestorableSessionlessConnection(): Promise<boolean> {
385+
if (this.cachedSessionlessConnection) return true
386+
this.cachedSessionlessConnection = this.sequenceStorage.getSessionlessConnectionSnapshot
387+
? await this.sequenceStorage.getSessionlessConnectionSnapshot()
388+
: null
389+
return this.cachedSessionlessConnection !== null
390+
}
391+
392+
/**
393+
* Returns the cached sessionless connection metadata without altering client state.
394+
* @returns The cached sessionless connection or null if none is available.
395+
*/
396+
public async getSessionlessConnectionInfo(): Promise<SessionlessConnectionData | null> {
397+
if (!this.cachedSessionlessConnection) {
398+
this.cachedSessionlessConnection = this.sequenceStorage.getSessionlessConnectionSnapshot
399+
? await this.sequenceStorage.getSessionlessConnectionSnapshot()
400+
: null
401+
}
402+
if (!this.cachedSessionlessConnection) return null
403+
return {
404+
walletAddress: this.cachedSessionlessConnection.walletAddress,
405+
loginMethod: this.cachedSessionlessConnection.loginMethod,
406+
userEmail: this.cachedSessionlessConnection.userEmail,
407+
guard: this.cachedSessionlessConnection.guard,
408+
}
409+
}
410+
411+
/**
412+
* Restores a sessionless connection that was previously persisted via {@link disconnect} or a connect flow.
413+
* @returns A promise that resolves to true if a sessionless connection was applied.
414+
*/
415+
public async restoreSessionlessConnection(): Promise<boolean> {
416+
const sessionlessConnection =
417+
this.cachedSessionlessConnection ??
418+
(this.sequenceStorage.getSessionlessConnectionSnapshot
419+
? await this.sequenceStorage.getSessionlessConnectionSnapshot()
420+
: null)
421+
if (!sessionlessConnection) {
422+
return false
423+
}
424+
425+
await this.applySessionlessConnectionState(
426+
sessionlessConnection.walletAddress,
427+
sessionlessConnection.loginMethod,
428+
sessionlessConnection.userEmail,
429+
sessionlessConnection.guard,
430+
)
431+
if (this.sequenceStorage.clearSessionlessConnectionSnapshot) {
432+
await this.sequenceStorage.clearSessionlessConnectionSnapshot()
433+
}
434+
this.cachedSessionlessConnection = null
435+
return true
436+
}
437+
372438
/**
373439
* Handles the redirect response from the Wallet.
374440
* This is called automatically on `initialize()` for web environments but can be called manually
@@ -881,17 +947,21 @@ export class DappClient {
881947
/**
882948
* Disconnects the client, clearing all session data from browser storage.
883949
* @remarks This action does not revoke the sessions on-chain. Sessions remain active until they expire or are manually revoked by the user in their wallet.
950+
* @param options Options to control the disconnection behavior.
951+
* @param options.keepSessionlessConnection When true, retains the latest wallet metadata so it can be restored later as a sessionless connection. Defaults to true.
884952
* @returns A promise that resolves when disconnection is complete.
885953
*
886954
* @example
887955
* const dappClient = new DappClient('http://localhost:5173');
888956
* await dappClient.initialize();
889957
*
890958
* if (dappClient.isInitialized) {
891-
* await dappClient.disconnect();
959+
* await dappClient.disconnect({ keepSessionlessConnection: true });
892960
* }
893961
*/
894-
async disconnect(): Promise<void> {
962+
async disconnect(options?: { keepSessionlessConnection?: boolean }): Promise<void> {
963+
const keepSessionlessConnection = options?.keepSessionlessConnection ?? true
964+
895965
const transportMode = this.transport.mode
896966

897967
this.transport.destroy()
@@ -904,7 +974,30 @@ export class DappClient {
904974
)
905975

906976
this.chainSessionManagers.clear()
977+
const sessionlessSnapshot =
978+
keepSessionlessConnection && this.walletAddress
979+
? {
980+
walletAddress: this.walletAddress,
981+
loginMethod: this.loginMethod ?? undefined,
982+
userEmail: this.userEmail ?? undefined,
983+
guard: this.guard,
984+
}
985+
: undefined
986+
907987
await this.sequenceStorage.clearAllData()
988+
989+
if (sessionlessSnapshot) {
990+
if (this.sequenceStorage.saveSessionlessConnectionSnapshot) {
991+
await this.sequenceStorage.saveSessionlessConnectionSnapshot(sessionlessSnapshot)
992+
}
993+
this.cachedSessionlessConnection = sessionlessSnapshot
994+
} else {
995+
if (this.sequenceStorage.clearSessionlessConnectionSnapshot) {
996+
await this.sequenceStorage.clearSessionlessConnectionSnapshot()
997+
}
998+
this.cachedSessionlessConnection = null
999+
}
1000+
9081001
this.isInitialized = false
9091002
this.walletAddress = null
9101003
this.loginMethod = null
@@ -939,6 +1032,7 @@ export class DappClient {
9391032
this.guard = guard
9401033
this.hasSessionlessConnection = true
9411034
this.isInitialized = true
1035+
this.cachedSessionlessConnection = null
9421036
this.emit('sessionsUpdated')
9431037
if (persist) {
9441038
await this.sequenceStorage.saveSessionlessConnection({

packages/wallet/dapp-client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type {
3939
SequenceStorage,
4040
ExplicitSessionData,
4141
ImplicitSessionData,
42+
SessionlessConnectionData,
4243
PendingRequestContext,
4344
PendingPayload,
4445
} from './utils/storage.js'

packages/wallet/dapp-client/src/utils/storage.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export interface SequenceStorage {
7777
getSessionlessConnection(): Promise<SessionlessConnectionData | null>
7878
clearSessionlessConnection(): Promise<void>
7979

80+
saveSessionlessConnectionSnapshot?(sessionData: SessionlessConnectionData): Promise<void>
81+
getSessionlessConnectionSnapshot?(): Promise<SessionlessConnectionData | null>
82+
clearSessionlessConnectionSnapshot?(): Promise<void>
83+
8084
clearAllData(): Promise<void>
8185
}
8286

@@ -86,6 +90,7 @@ const STORE_NAME = 'userKeys'
8690
const IMPLICIT_SESSIONS_IDB_KEY = 'SequenceImplicitSession'
8791
const EXPLICIT_SESSIONS_IDB_KEY = 'SequenceExplicitSession'
8892
const SESSIONLESS_CONNECTION_IDB_KEY = 'SequenceSessionlessConnection'
93+
const SESSIONLESS_CONNECTION_SNAPSHOT_IDB_KEY = 'SequenceSessionlessConnectionSnapshot'
8994

9095
const PENDING_REDIRECT_REQUEST_KEY = 'SequencePendingRedirect'
9196
const TEMP_SESSION_PK_KEY = 'SequencePendingTempSessionPk'
@@ -294,6 +299,33 @@ export class WebStorage implements SequenceStorage {
294299
}
295300
}
296301

302+
async saveSessionlessConnectionSnapshot(sessionData: SessionlessConnectionData): Promise<void> {
303+
try {
304+
await this.setIDBItem(SESSIONLESS_CONNECTION_SNAPSHOT_IDB_KEY, sessionData)
305+
} catch (error) {
306+
console.error('Failed to save sessionless connection snapshot:', error)
307+
throw error
308+
}
309+
}
310+
311+
async getSessionlessConnectionSnapshot(): Promise<SessionlessConnectionData | null> {
312+
try {
313+
return (await this.getIDBItem<SessionlessConnectionData>(SESSIONLESS_CONNECTION_SNAPSHOT_IDB_KEY)) ?? null
314+
} catch (error) {
315+
console.error('Failed to retrieve sessionless connection snapshot:', error)
316+
return null
317+
}
318+
}
319+
320+
async clearSessionlessConnectionSnapshot(): Promise<void> {
321+
try {
322+
await this.deleteIDBItem(SESSIONLESS_CONNECTION_SNAPSHOT_IDB_KEY)
323+
} catch (error) {
324+
console.error('Failed to clear sessionless connection snapshot:', error)
325+
throw error
326+
}
327+
}
328+
297329
async clearAllData(): Promise<void> {
298330
try {
299331
// Clear all session storage items
@@ -305,6 +337,7 @@ export class WebStorage implements SequenceStorage {
305337
await this.clearExplicitSessions()
306338
await this.clearImplicitSession()
307339
await this.clearSessionlessConnection()
340+
await this.clearSessionlessConnectionSnapshot()
308341
} catch (error) {
309342
console.error('Failed to clear all data:', error)
310343
throw error

0 commit comments

Comments
 (0)