Skip to content

Commit

Permalink
Adds new session management and way to get authenticated client via r…
Browse files Browse the repository at this point in the history
…eact-sdk
  • Loading branch information
taylorjdawson committed Oct 25, 2024
1 parent c15023c commit 3447581
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 9 deletions.
9 changes: 6 additions & 3 deletions packages/sdk-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export {
TurnkeyWalletClient,
} from "./sdk-client";

export type { User, ReadOnlySession, ReadWriteSession } from "./models";

export { getStorageValue, setStorageValue, StorageKeys } from "./storage";

export {
Expand All @@ -44,9 +46,10 @@ export {
DEFAULT_SOLANA_ACCOUNTS,
} from "./turnkey-helpers";

export type {
TurnkeySDKClientConfig,
TurnkeySDKBrowserConfig,
export {
type TurnkeySDKClientConfig,
type TurnkeySDKBrowserConfig,
AuthClient,
} from "./__types__/base";

export type * as TurnkeySDKApiTypes from "./__generated__/sdk_api_types";
19 changes: 19 additions & 0 deletions packages/sdk-browser/src/models.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import type { AuthClient } from "./__types__/base";

/**
* This interface defines the structure of user data that will be stored in local storage
* when using one of the login methods.
*/
export interface User {
// Unique identifier for the user.
userId: string;

// Username of the user.
username: string;

// Organization details associated with the user.
organization: {
// Unique identifier for the organization.
organizationId: string;

// Name of the organization.
organizationName: string;
};

// Session information for the user, which can be either read-only or read-write.
session:
| {
// Optional read-only session details.
read?: ReadOnlySession;

// Optional read-write session details.
write?: ReadWriteSession;

// Authenticated client associated with the session.
authenticatedClient: AuthClient;
}
| undefined;
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
},
"dependencies": {
"@turnkey/sdk-browser": "workspace:*",
"@turnkey/wallet-stamper": "workspace:^"
"@turnkey/wallet-stamper": "workspace:^",
"usehooks-ts": "^3.1.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
Expand Down
43 changes: 40 additions & 3 deletions packages/sdk-react/src/contexts/TurnkeyContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import {
TurnkeySDKBrowserConfig,
TurnkeyBrowserClient,
TurnkeyWalletClient,
AuthClient,
} from "@turnkey/sdk-browser";
import type { WalletInterface } from "@turnkey/wallet-stamper";
import { useUserSession } from "../hooks/use-session";

export interface TurnkeyClientType {
client: TurnkeyBrowserClient | undefined;
turnkey: Turnkey | undefined;
authIframeClient: TurnkeyIframeClient | undefined;
passkeyClient: TurnkeyPasskeyClient | undefined;
Expand All @@ -18,6 +21,7 @@ export interface TurnkeyClientType {
}

export const TurnkeyContext = createContext<TurnkeyClientType>({
client: undefined,
turnkey: undefined,
passkeyClient: undefined,
authIframeClient: undefined,
Expand All @@ -27,11 +31,13 @@ export const TurnkeyContext = createContext<TurnkeyClientType>({
},
});

type TurnkeyProviderConfig = TurnkeySDKBrowserConfig & {
wallet?: WalletInterface;
};

interface TurnkeyProviderProps {
children: ReactNode;
config: TurnkeySDKBrowserConfig & {
wallet?: WalletInterface;
};
config: TurnkeyProviderConfig;
}

export const TurnkeyProvider: React.FC<TurnkeyProviderProps> = ({
Expand All @@ -48,6 +54,13 @@ export const TurnkeyProvider: React.FC<TurnkeyProviderProps> = ({
const [authIframeClient, setAuthIframeClient] = useState<
TurnkeyIframeClient | undefined
>(undefined);

const [client, setClient] = useState<TurnkeyBrowserClient | undefined>(
undefined
);

const { session } = useUserSession();

const iframeInit = useRef<boolean>(false);

const TurnkeyAuthIframeContainerId = "turnkey-auth-iframe-container-id";
Expand Down Expand Up @@ -115,9 +128,33 @@ export const TurnkeyProvider: React.FC<TurnkeyProviderProps> = ({
})();
}, []);

/**
* Effect hook that updates the active client based on the current session's authenticated client.
*
* This hook listens for changes in the `session` object. If the `session` contains an `authenticatedClient`,
* it determines which client was used for initial authentication by checking the `authenticatedClient` key.
* It then sets the corresponding client (either `authIframeClient`, `passkeyClient`, or `walletClient`)
* as the active client using the `setClient` function.
*
* If the `session` changes, the `authenticatedClient` will be recomputed and the active client will be
* updated accordingly.
*/
useEffect(() => {
if (session?.authenticatedClient) {
const client = {
[AuthClient.Iframe]: authIframeClient,
[AuthClient.Passkey]: passkeyClient,
[AuthClient.Wallet]: walletClient,
}[session?.authenticatedClient];

setClient(client);
}
}, [session]);

return (
<TurnkeyContext.Provider
value={{
client,
turnkey,
passkeyClient,
authIframeClient,
Expand Down
64 changes: 64 additions & 0 deletions packages/sdk-react/src/hooks/use-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { StorageKeys, User } from "@turnkey/sdk-browser";
import { useSessionStorage } from "usehooks-ts";

interface UserSession {
user?: Omit<User, "session"> | undefined;
session?: User["session"];
}

/**
* Hook for managing the user session stored in local storage.
* This hook is reactive and updates whenever the value in local storage changes.
*
* @returns {UserSession | null} An object containing user details and session information.
*
* @example
* const { user, session } = useUserSession()
*
* if (user) {
* // user is defined and thus has previously logged in
* } else {
* // no user found in local storage
* }
*
* if (session?.read) {
* // session.read is defined therefore user is authenticated with a read session
* if (session.expiry && Date.now() < session.expiry) {
* // Session is still valid
* }
* }
*
* if (session?.write) {
* // session.write is defined therefore user is authenticated with a read/write session
* if (session.expiry && Date.now() < session.expiry) {
* // Session is still valid
* }
* }
*
* if (!session) {
* // no session, user is not authenticated
* }
*/
export function useUserSession(): UserSession {
// Use the StorageKeys.UserSession key to manage session storage
const [user] = useSessionStorage<User | undefined>(
StorageKeys.UserSession,
undefined
);

// Destructure user object to separate session from other user details
const { session, userId, username, organization } = user ?? {};

// Return the structured object with separated user details and session
return {
user:
userId && organization
? {
userId: userId ?? "",
username: username ?? "",
organization: organization ?? "",
}
: undefined,
session: session ?? undefined,
};
}
8 changes: 6 additions & 2 deletions packages/wallet-stamper/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface BaseWalletInterface {
* Solana wallets can directly access the public key without needing a signed message.
* @interface SolanaWalletInterface
* @extends BaseWalletInterface
* @property {function(): string} getPublicKey - Recovers the public key, which is the ED25519 hex encoded public key from your Solana wallet public key.
* @property {function(): string} getPublicKey - Returns the public key, which is the ED25519 hex encoded public key from your Solana wallet public key.
* @property {'solana'} type - The type of the wallet.
*/
export interface SolanaWalletInterface extends BaseWalletInterface {
Expand All @@ -40,9 +40,13 @@ export interface SolanaWalletInterface extends BaseWalletInterface {

/**
* EVM wallets require a signed message to derive the public key.
*
* @remarks This is the SECP256K1 public key of the EVM wallet, not the address.
* This requires that the wallet signs a message in order to derive the public key.
*
* @interface EvmWalletInterface
* @extends BaseWalletInterface
* @property {function(): Promise<string>} getPublicKey - Public key recovery from signature and message, returning a hex encoded SECP256K1 public key as a string.
* @property {function(): Promise<string>} getPublicKey - Returns the public key, which is the SECP256K1 hex encoded public key from your EVM wallet.
* @property {'evm'} type - The type of the wallet.
*/
export interface EvmWalletInterface extends BaseWalletInterface {
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3447581

Please sign in to comment.