diff --git a/packages/enoki/src/wallet/features.ts b/packages/enoki/src/wallet/features.ts index abb5c5ee0..9fbd18d51 100644 --- a/packages/enoki/src/wallet/features.ts +++ b/packages/enoki/src/wallet/features.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import type { AuthProvider, EnokiNetwork } from '../EnokiClient/type.js'; -import type { ZkLoginSession } from './types.js'; +import type { EnokiSessionContext, PKCEContext, ZkLoginSession } from './types.js'; +import type { StandardConnect, StandardConnectMethod } from '@mysten/wallet-standard'; /** Name of the feature for retrieving basic wallet metadata. */ export const EnokiGetMetadata = 'enoki:getMetadata'; @@ -65,3 +66,57 @@ export type EnokiGetSessionInput = { /** Output of retrieving the Enoki session. */ export type EnokiGetSessionOutput = ZkLoginSession | null; + +/** Extended connect method for manual auth flow. */ +export type EnokiConnectMethod = ( + input?: { + /** Whether to disable popup and return URL for manual handling. */ + disablePopup?: boolean; + /** Standard connect input fields. */ + } & Parameters[0], +) => ReturnType | (ReturnType & Promise<{ + authorizationUrl: string; + pkceContext: PKCEContext | undefined; + sessionContext: EnokiSessionContext; +}>); + + + +/** Name of the feature for auth flow. */ +export const EnokiHandleAuthCallback = 'enoki:handleAuthCallback'; + +/** The latest API version of the auth API. */ +export type EnokiHandleAuthCallbackVersion = '1.0.0'; + +export type EnokiHandleAuthCallbackInput = { + hash: string; + sessionContext: EnokiSessionContext; + pkceContext?: PKCEContext; + search: string; +}; + +export type EnokiHandleAuthCallbackMethod = (input: EnokiHandleAuthCallbackInput) => Promise; + + +/** + * A Wallet Standard feature for authentication flow. + */ +export type EnokiHandleAuthCallbackFeature = { + /** Namespace for the feature. */ + [EnokiHandleAuthCallback]: { + /** Version of the feature API. */ + version: EnokiHandleAuthCallbackVersion; + handleAuthCallback: EnokiHandleAuthCallbackMethod; + }; +}; + +// TODO: maybe move to standart package and remove here +/** + * Extended connect method for manual auth flow. + */ +export type EnokiConnectFeature = { + [StandardConnect]: { + version: '1.0.0'; + connect: EnokiConnectMethod; + }; +}; \ No newline at end of file diff --git a/packages/enoki/src/wallet/types.ts b/packages/enoki/src/wallet/types.ts index ab0905fc3..b98f809f2 100644 --- a/packages/enoki/src/wallet/types.ts +++ b/packages/enoki/src/wallet/types.ts @@ -88,15 +88,18 @@ export type RegisterEnokiWalletsOptions = { ( | ClientConfig | { - /** - * The SuiClient instance to use when building and executing transactions. - */ - client: SuiClient; - - /** - * The network to use when building and executing transactions. - * @default 'mainnet' - */ - network?: EnokiNetwork; - } + /** + * The SuiClient instance to use when building and executing transactions. + */ + client: SuiClient; + + /** + * The network to use when building and executing transactions. + * @default 'mainnet' + */ + network?: EnokiNetwork; + } ); + + +export type PKCEContext = { codeChallenge: string; codeVerifier: string }; diff --git a/packages/enoki/src/wallet/wallet.ts b/packages/enoki/src/wallet/wallet.ts index 32b3684b6..5132d04f0 100644 --- a/packages/enoki/src/wallet/wallet.ts +++ b/packages/enoki/src/wallet/wallet.ts @@ -5,8 +5,6 @@ import { Transaction } from '@mysten/sui/transactions'; import { fromBase64, toBase64 } from '@mysten/sui/utils'; import type { IdentifierString, - StandardConnectFeature, - StandardConnectMethod, StandardDisconnectFeature, StandardDisconnectMethod, StandardEventsFeature, @@ -32,14 +30,22 @@ import type { Emitter } from 'mitt'; import mitt from 'mitt'; import type { AuthProvider } from '../EnokiClient/type.js'; -import type { EnokiWalletOptions, WalletEventsMap, EnokiSessionContext } from './types.js'; +import type { EnokiWalletOptions, WalletEventsMap, EnokiSessionContext, PKCEContext } from './types.js'; import type { EnokiGetMetadataFeature, EnokiGetMetadataMethod, EnokiGetSessionFeature, EnokiGetSessionMethod, + EnokiHandleAuthCallbackFeature, + EnokiConnectMethod, + EnokiHandleAuthCallbackInput, + EnokiConnectFeature, +} from './features.js'; +import { + EnokiGetMetadata, + EnokiGetSession, + EnokiHandleAuthCallback, } from './features.js'; -import { EnokiGetMetadata, EnokiGetSession } from './features.js'; import type { Experimental_SuiClientTypes } from '@mysten/sui/experimental'; import { decodeJwt } from '@mysten/sui/zklogin'; import type { ExportedWebCryptoKeypair } from '@mysten/signers/webcrypto'; @@ -59,8 +65,6 @@ const pkceFlowProviders: Partial }, }; -type PKCEContext = { codeChallenge: string; codeVerifier: string }; - export class EnokiWallet implements Wallet { #events: Emitter; #accounts: ReadonlyWalletAccount[]; @@ -101,14 +105,15 @@ export class EnokiWallet implements Wallet { return this.#accounts; } - get features(): StandardConnectFeature & + get features(): EnokiConnectFeature & StandardDisconnectFeature & StandardEventsFeature & SuiSignTransactionFeature & SuiSignAndExecuteTransactionFeature & SuiSignPersonalMessageFeature & EnokiGetMetadataFeature & - EnokiGetSessionFeature { + EnokiGetSessionFeature & + EnokiHandleAuthCallbackFeature { return { [StandardConnect]: { version: '1.0.0', @@ -142,6 +147,12 @@ export class EnokiWallet implements Wallet { version: '1.0.0', getSession: this.#getSession, }, + [EnokiHandleAuthCallback]: { + version: '1.0.0', + handleAuthCallback: async (input) => { + return this.#handleAuthCallback(input); + } + }, }; } @@ -260,7 +271,7 @@ export class EnokiWallet implements Wallet { return () => this.#events.off(event, listener); }; - #connect: StandardConnectMethod = async (input) => { + #connect: EnokiConnectMethod = async (input) => { // NOTE: This is a hackfix for the old version of dApp Kit where auto-connection logic // only fires on initial mount of the WalletProvider component. Since hydrating the // zkLogin state from IndexedDB is an asynchronous process, we need to make sure it @@ -272,9 +283,9 @@ export class EnokiWallet implements Wallet { } const currentNetwork = this.#getCurrentNetwork(); - await this.#createSession({ network: currentNetwork }); + const session = await this.#createSession({ network: currentNetwork, disablePopup: input?.disablePopup }); - return { accounts: this.#accounts }; + return { accounts: this.#accounts, ...session }; }; #disconnect: StandardDisconnectMethod = async () => { @@ -301,8 +312,9 @@ export class EnokiWallet implements Wallet { async #getKeypair(sessionContext: EnokiSessionContext) { const session = await this.#state.getSession(sessionContext); + if (!session?.jwt || Date.now() > session.expiresAt) { - await this.#createSession({ network: sessionContext.client.network }); + await this.#createSession({ network: sessionContext.client.network, disablePopup: false }); } const storedNativeSigner = await get( @@ -352,7 +364,19 @@ export class EnokiWallet implements Wallet { return { client: sessionContext.client, keypair }; } - async #createSession({ network }: { network: Experimental_SuiClientTypes.Network }) { + async #createSession({ network, disablePopup = false }: { network: Experimental_SuiClientTypes.Network; disablePopup?: boolean }) { + const sessionContext = this.#state.getSessionContext(network); + const pkceContext = await this.#getPKCEFlowContext(); + const authorizationUrl = await this.#createAuthorizationURL(sessionContext, pkceContext); + + if (disablePopup) { + return { + authorizationUrl, + pkceContext, + sessionContext, + }; + } + const popup = window.open( undefined, '_blank', @@ -363,10 +387,7 @@ export class EnokiWallet implements Wallet { throw new Error('Failed to open popup'); } - const sessionContext = this.#state.getSessionContext(network); - const pkceContext = await this.#getPKCEFlowContext(); - - popup.location = await this.#createAuthorizationURL(sessionContext, pkceContext); + popup.location = authorizationUrl; return await new Promise((resolve, reject) => { const interval = setInterval(() => { @@ -440,10 +461,10 @@ export class EnokiWallet implements Wallet { .join(' '), ...(pkceContext ? { - response_type: 'code', - code_challenge_method: 'S256', - code_challenge: pkceContext.codeChallenge, - } + response_type: 'code', + code_challenge_method: 'S256', + code_challenge: pkceContext.codeChallenge, + } : undefined), }); @@ -484,12 +505,7 @@ export class EnokiWallet implements Wallet { sessionContext, pkceContext, search, - }: { - hash: string; - sessionContext: EnokiSessionContext; - pkceContext?: PKCEContext; - search: string; - }) { + }: EnokiHandleAuthCallbackInput) { const params = new URLSearchParams(hash.startsWith('#') ? hash.slice(1) : hash); const zkp = await this.#state.getSession(sessionContext);