Compare commits

..
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
570d71e8d6 Version Packages (#1882)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-01 13:03:47 -07:00
961ef644d1 [SmartWallet] Improved useSmartWallet hook (#1885)
Co-authored-by: ikethirdweb <[email protected]>
2023-11-01 19:54:46 +00:00
Edward SunandGitHub 6fcd09ddbd Show sign in button if user address and active wallet address are mis-matched (#1886) 2023-11-01 19:22:35 +00:00
Kien NgoandGitHub a9d32f3c90 [SDK] Reduce bundle size by lazy loading some large modules (#1871) 2023-11-01 03:35:24 +00:00
iketwandGitHub 8ec0d00c75 [RN] Fix typo in readme (#1881) 2023-10-31 22:46:59 +00:00
+3
Joaquim VergesGitHubElasticBottleManan TankWinston Yeoikethirdweb0xFirekeeperiketwvuittont60Jonas Danielsgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>github-actions[bot] <github-actions[bot]@users.noreply.github.com>yoanslvy
0acc530f8b [Wallet, React] New Embedded Wallet API (#1846)
Signed-off-by: vuittont60 <[email protected]>
Signed-off-by: Manan Tank <[email protected]>
Co-authored-by: ElasticBottle <[email protected]>
Co-authored-by: Manan Tank <[email protected]>
Co-authored-by: Winston Yeo <[email protected]>
Co-authored-by: ikethirdweb <[email protected]>
Co-authored-by: 0xFirekeeper <[email protected]>
Co-authored-by: iketw <[email protected]>
Co-authored-by: vuittont60 <[email protected]>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: yoanslvy <[email protected]>
2023-10-31 22:42:15 +00:00
86 changed files with 2996 additions and 1074 deletions
+7
View File
@@ -1,5 +1,12 @@
# @thirdweb-dev/auth
## 4.1.3
### Patch Changes
- Updated dependencies [[`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a)]:
- @thirdweb-dev/wallets@2.1.0
## 4.1.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/auth",
"version": "4.1.2",
"version": "4.1.3",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"browser": {
+9
View File
@@ -1,5 +1,14 @@
# thirdweb
## 0.13.11
### Patch Changes
- Updated dependencies [[`a9d32f3c`](https://github.com/thirdweb-dev/js/commit/a9d32f3c90251a459e17a19eca803bbfdeeaeb79), [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a)]:
- @thirdweb-dev/sdk@4.0.10
- @thirdweb-dev/wallets@2.1.0
- @thirdweb-dev/auth@4.1.3
## 0.13.10
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "thirdweb",
"main": "dist/cli/index.js",
"version": "0.13.10",
"version": "0.13.11",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+9
View File
@@ -1,5 +1,14 @@
# @thirdweb-dev/react-core
## 4.1.0
### Patch Changes
- Updated dependencies [[`a9d32f3c`](https://github.com/thirdweb-dev/js/commit/a9d32f3c90251a459e17a19eca803bbfdeeaeb79), [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a)]:
- @thirdweb-dev/sdk@4.0.10
- @thirdweb-dev/wallets@2.1.0
- @thirdweb-dev/auth@4.1.3
## 4.0.10
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-core",
"version": "4.0.10",
"version": "4.1.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-core",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -199,7 +199,7 @@ export function ThirdwebWalletProvider(
const walletInfo: LastConnectedWalletInfo = {
walletId: walletConfig.id,
connectParams,
connectParams: connectParams || wallet.getConnectParams(),
};
// if personal wallet exists, we need to replace the connectParams.personalWallet to a stringifiable version
@@ -1,5 +1,7 @@
# @thirdweb-dev/react-native-compat
## 0.4.0
## 0.3.16
## 0.3.15
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@thirdweb-dev/react-native-compat",
"description": "Shims for Thirdweb in React Native Projects",
"version": "0.3.16",
"version": "0.4.0",
"author": "thirdweb eng <[email protected]>",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native-compat",
"license": "Apache-2.0",
+87
View File
@@ -1,5 +1,92 @@
# @thirdweb-dev/react-native
## 0.4.0
### Minor Changes
- [#1846](https://github.com/thirdweb-dev/js/pull/1846) [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - New API to configure authentication options on `embeddedWallet`
```ts
// default - google sign in is enabled
embeddedWallet();
// this is same as
embeddedWallet({
auth: {
options: ["email"],
},
});
// add google sign in
embeddedWallet({
auth: {
options: ["email", "google"],
redirectUrl: "your_app://deeplink",
},
});
```
### Patch Changes
- [#1885](https://github.com/thirdweb-dev/js/pull/1885) [`961ef644`](https://github.com/thirdweb-dev/js/commit/961ef644d1b1adaad08c071903cd53aacac50bb4) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Improved `useSmartWallet()` hook
Example with metamask:
```ts
const { connect } = useSmartWallet(metamaskWallet(), {
factoryAddress: factoryAddress,
gasless: true,
});
const onClick = async () => {
// nothing to do here, all handled internally
await connect();
};
```
```ts
Example with localWallet:
const { connect } = useSmartWallet(localWallet(), {
factoryAddress: factoryAddress,
gasless: true,
});
const onClick = async () => {
// function to 'load' the local wallet before using it
await connect({
connectPersonalWallet: async (w) => {
await w.generate();
await w.connect();
}
});
}
```
```ts
Example with embeddedWallet:
const { connect } = useSmartWallet(embeddedWallet(), {
factoryAddress: factoryAddress,
gasless: true,
});
const onClick = async () => {
// function to 'auth' the embedded wallet before using it
await connect({
connectPersonalWallet: async (w) => {
const authResult = await w.authenticate({ strategy: "google" });
await w.connect({ authResult });
}
});
}
```
- Updated dependencies [[`a9d32f3c`](https://github.com/thirdweb-dev/js/commit/a9d32f3c90251a459e17a19eca803bbfdeeaeb79), [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a)]:
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
## 0.3.16
### Patch Changes
+1 -1
View File
@@ -12,7 +12,7 @@
</p>
<p><strong>Powering react-native dapps.</strong></p>
The go to librarly for seamless Web3 integration in React Native apps.
The go to library for seamless Web3 integration in React Native apps.
<br />
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react-native",
"version": "0.3.16",
"version": "0.4.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
@@ -31,7 +31,6 @@
"@magic-sdk/provider": "17.2.0",
"@magic-sdk/react-native-bare": "^18.5.0",
"@paperxyz/embedded-wallet-service-sdk": "^1.2.4",
"@react-native-community/checkbox": "^0.5.16",
"@shopify/restyle": "^2.4.2",
"@tanstack/react-query": "^4.33.0",
"@thirdweb-dev/chains": "workspace:*",
@@ -0,0 +1,17 @@
import Svg, { Path } from "react-native-svg";
import { IconStyleProp } from "./types";
const CheckedIcon = ({ width, height, color }: IconStyleProp) => {
return (
<Svg width={width} height={height} viewBox="0 0 15 15" fill="none">
<Path
d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
fill={color}
fill-rule="evenodd"
clip-rule="evenodd"
/>
</Svg>
);
};
export default CheckedIcon;
@@ -61,7 +61,7 @@ export const ConnectWalletHeader = ({
</Text>
) : null}
{subHeaderText ? (
<Text variant="subHeader" mt="md">
<Text variant="subHeader" mt="md" textAlign="center">
{subHeaderText}
</Text>
) : null}
@@ -10,7 +10,6 @@ import { SessionRequestModal } from "./ConnectWalletDetails/SessionRequestModal"
import { SessionProposalModal } from "./ConnectWalletDetails/SessionProposalModal";
import { TWModal } from "./base/modal/TWModal";
import Box from "./base/Box";
// import { useAppTheme } from "../styles/hooks";
const MODAL_HEIGHT = Dimensions.get("window").height * 0.7;
const DEVICE_WIDTH = Dimensions.get("window").width;
@@ -25,8 +24,6 @@ export const MainModal = () => {
switch (modalState?.view) {
case "ConnectWalletFlow":
return <ConnectWalletFlow />;
// case "WalletDetails":
// return <ConnectWalletDetailsModal />;
case "WalletConnectSessionRequestModal":
return <SessionRequestModal />;
case "WalletConnectSessionProposalModal":
@@ -0,0 +1,47 @@
import React, { useRef, useEffect } from "react";
import { Animated, ViewProps } from "react-native";
interface BouncingContainerProps extends ViewProps {
duration?: number;
}
const BouncingContainer: React.FC<BouncingContainerProps> = ({
children,
duration = 500,
...props
}) => {
const bounceValue = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(bounceValue, {
toValue: 10,
duration,
useNativeDriver: true,
}),
Animated.timing(bounceValue, {
toValue: 0,
duration,
useNativeDriver: true,
}),
]),
).start();
}, [bounceValue, duration]);
return (
<Animated.View
{...props}
style={[
props.style,
{
transform: [{ translateY: bounceValue }],
},
]}
>
{children}
</Animated.View>
);
};
export default BouncingContainer;
@@ -0,0 +1,43 @@
import React, { useState } from "react";
import Text from "./Text";
import CheckedIcon from "../../assets/checked-icon";
import BaseButton from "./BaseButton";
import Box from "./Box";
interface CheckboxProps {
label: string;
color: string;
value?: boolean;
onToggle?: (value: boolean) => void;
}
const Checkbox: React.FC<CheckboxProps> = ({
label,
color,
onToggle,
value = false,
}) => {
const [isChecked, setIsChecked] = useState(value);
const onPress = () => {
setIsChecked(!isChecked);
onToggle?.(!isChecked);
};
return (
<BaseButton flexDirection="row" alignItems="center" onPress={onPress}>
<Box borderRadius="md" style={{ borderColor: color }} borderWidth={1}>
{isChecked ? (
<CheckedIcon width={20} height={20} color={color} />
) : (
<Box width={20} height={20} />
)}
</Box>
<Text variant="bodySmall" ml="xxs">
{label}
</Text>
</BaseButton>
);
};
export default Checkbox;
+5
View File
@@ -78,6 +78,11 @@ export const _en = {
forgot_password: "Forgot password",
enter_account_recovery_code: "Enter account recovery code",
backup_your_account: "Backup your account",
create_password: "Create password",
enter_password: "Enter password",
set_password_message: "Set a password to secure your account",
enter_password_for_email: "Enter password for",
make_sure_you_save_it: "Make sure you save it",
},
wallet_connect: {
no_results_found: "No results found",
+2 -2
View File
@@ -14,7 +14,7 @@ export const _es = {
connect_to_app: "Conectar a la Aplicación",
guest: "Invitado",
connect: "Conectar",
new_to_wallets: "¿Nuevo en billeteras?",
new_to_wallets: "¿Nuevo en carteras?",
view_transaction_history: "Ver Historial de Transacciones",
get_started: "Comenzar",
receive_funds: "Recibir Fondos",
@@ -87,7 +87,7 @@ export const _es = {
},
wallet_connect: {
no_results_found: "No se encontraron resultados",
search_wallets: "Buscar Billeteras",
search_wallets: "Buscar Carteras",
},
common: {
connect_app: "Conectar aplicación",
+5 -1
View File
@@ -14,7 +14,6 @@ export const _ja = {
connect_to_app: "アプリに接続",
guest: "ゲスト",
connect: "接続",
kpepe: "KPEPE",
new_to_wallets: "ウォレットは初めてですか?",
view_transaction_history: "取引履歴を見る",
get_started: "開始する",
@@ -80,6 +79,11 @@ export const _ja = {
forgot_password: "パスワードを忘れた",
enter_account_recovery_code: "アカウント回復コードを入力",
backup_your_account: "アカウントのバックアップ",
create_password: "パスワードを作成する",
enter_password: "パスワードを入力",
set_password_message: "アカウントの保護のためパスワードを設定してください",
enter_password_for_email: "メールのためのパスワードを入力",
make_sure_you_save_it: "必ず保存してください",
},
wallet_connect: {
no_results_found: "結果が見つかりません",
+3
View File
@@ -30,6 +30,9 @@ export { useCoinbaseWallet } from "./wallets/hooks/useCoinbaseWallet";
export { useMetaMaskWallet } from "./wallets/hooks/useMetaMaskWallet";
export { useRainbowWallet } from "./wallets/hooks/useRainbowWallet";
export { useTrustWallet } from "./wallets/hooks/useTrustWallet";
export { useEmbeddedWallet } from "./wallets/hooks/useEmbeddedWallet";
export { useSmartWallet } from "./wallets/hooks/useSmartWallet";
export { useEmbeddedWalletSendVerificationEmail } from "./wallets/hooks/useEmbeddedWalletSendVerificationEmail";
export {
ConnectWallet,
@@ -1,5 +1,3 @@
// import { Platform } from "react-native";
export const textVariants = {
defaults: {
fontFamily: undefined as string | undefined,
@@ -1,16 +1,18 @@
import {
AuthOptions,
AuthParams,
AuthResult,
EmbeddedWalletConnectionArgs,
EmbeddedWalletConnectorOptions,
OauthOption,
SendEmailOtpReturnType,
} from "./types";
import type { Chain } from "@thirdweb-dev/chains";
import { Connector, normalizeChainId } from "@thirdweb-dev/wallets";
import { providers, Signer } from "ethers";
import { utils } from "ethers";
import {
customJwt,
sendEmailOTP,
sendVerificationEmail,
socialLogin,
validateEmailOTP,
} from "./embedded/auth";
@@ -21,7 +23,14 @@ import {
getConnectedEmail,
saveConnectedEmail,
} from "./embedded/helpers/storage/local";
import { AuthProvider } from "@paperxyz/embedded-wallet-service-sdk";
import {
AuthProvider,
Connector,
RecoveryShareManagement,
UserWalletStatus,
normalizeChainId,
} from "@thirdweb-dev/wallets";
import { isValidUserManagedEmailOtp } from "./embedded/helpers/api/fetchers";
export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionArgs> {
private options: EmbeddedWalletConnectorOptions;
@@ -37,142 +46,172 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
this.email = getConnectedEmail();
}
async connect(options?: { chainId?: number } & EmbeddedWalletConnectionArgs) {
const connected = await this.isConnected();
if (connected) {
return this.getAddress();
}
switch (options?.loginType) {
case "headless_google_oauth":
{
await socialLogin(
{
provider: AuthProvider.GOOGLE,
redirectUrl: options.redirectUrl,
},
this.options.clientId,
);
}
break;
case "headless_email_otp_verification": {
await this.validateEmailOtp({ otp: options.otp });
break;
}
case "jwt": {
await this.customJwt({
jwt: options.jwt,
password: options.password,
});
break;
}
default:
throw new Error("Invalid login type");
}
if (options?.chainId) {
this.switchChain(options.chainId);
async connect(options?: EmbeddedWalletConnectionArgs) {
try {
await this.getSigner();
} catch (error) {
throw new Error(`Error fetching the signer: ${error}`);
}
this.setupListeners();
if (options?.chainId) {
await this.switchChain(options.chainId);
}
if (this.email) {
saveConnectedEmail(this.email);
}
return this.getAddress();
}
async validateEmailOtp(options: { otp: string }) {
if (!this.email) {
throw new Error("Email is required to connect");
async authenticate(params: AuthParams): Promise<AuthResult> {
const strategy = params.strategy;
switch (strategy) {
case "email_verification": {
return await this.validateEmailOTP({
email: params.email,
otp: params.verificationCode,
recoveryCode: params.recoveryCode,
});
}
case "google": {
return this.socialLogin({
provider: AuthProvider.GOOGLE,
redirectUrl: params.redirectUrl,
});
}
case "jwt": {
return this.customJwt({
jwt: params.jwt,
password: params.encryptionKey || "",
});
}
default:
assertUnreachable(strategy);
}
}
private async validateEmailOTP(options: {
email: string;
otp: string;
recoveryCode?: string;
}): Promise<AuthResult> {
try {
await validateEmailOTP({
const { storedToken } = await validateEmailOTP({
email: options.email,
clientId: this.options.clientId,
otp: options.otp,
recoveryCode: options.recoveryCode,
});
return {
user: {
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED,
recoveryShareManagement:
storedToken.authDetails.recoveryShareManagement,
},
isNewUser: storedToken.isNewUser,
needsRecoveryCode:
storedToken.authDetails.recoveryShareManagement ===
RecoveryShareManagement.USER_MANAGED,
};
} catch (error) {
console.error(`Error while validating otp: ${error}`);
if (error instanceof Error) {
return { error: error.message };
throw new Error(`Error while validating otp: ${error.message}`);
} else {
return { error: "An unknown error occurred" };
throw new Error("An unknown error occurred while validating otp");
}
}
try {
await this.getSigner();
this.emit("connected");
} catch (error) {
if (error instanceof Error) {
return { error: error.message };
} else {
return { error: "Error getting the signer" };
}
}
return { success: true };
}
async sendEmailOtp(options: { email: string }) {
async isValidUserManagedEmailOTP(options: { otp: string }) {
try {
const result = await isValidUserManagedEmailOtp({
clientId: this.options.clientId,
email: this.email || "",
otp: options.otp,
});
if (result.isValid) {
return result;
} else {
throw new Error("Invalid otp, please try again.");
}
} catch (error) {
throw new Error(`Error validating otp: ${error}`);
}
}
async sendVerificationEmail(options: {
email: string;
}): Promise<SendEmailOtpReturnType> {
this.email = options.email;
saveConnectedEmail(options.email);
return sendEmailOTP({
return sendVerificationEmail({
email: options.email,
clientId: this.options.clientId,
});
}
async socialLogin(oauthOption: OauthOption) {
private async socialLogin(oauthOption: OauthOption): Promise<AuthResult> {
try {
const { email } = await socialLogin(oauthOption, this.options.clientId);
const { storedToken, email } = await socialLogin(
oauthOption,
this.options.clientId,
);
this.email = email;
saveConnectedEmail(email);
return {
user: {
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED,
recoveryShareManagement:
storedToken.authDetails.recoveryShareManagement,
},
isNewUser: storedToken.isNewUser,
needsRecoveryCode:
storedToken.authDetails.recoveryShareManagement ===
RecoveryShareManagement.USER_MANAGED,
};
} catch (error) {
console.error(
`Error while signing in with: ${oauthOption.provider}. ${error}`,
);
if (error instanceof Error) {
return { error: error.message };
throw new Error(
`Error logging in with ${oauthOption.provider}: ${error.message}`,
);
} else {
return { error: "An unknown error occurred" };
throw new Error(
`An unknown error occurred logging in with ${oauthOption.provider}`,
);
}
}
try {
await this.getSigner();
this.emit("connected");
} catch (error) {
if (error instanceof Error) {
return { error: error.message };
} else {
return { error: "Error getting the signer" };
}
}
return { success: true };
}
async customJwt(authOptions: AuthOptions) {
private async customJwt(authOptions: AuthOptions): Promise<AuthResult> {
try {
const resp = await customJwt(authOptions, this.options.clientId);
this.email = resp.email;
const { verifiedToken, email } = await customJwt(
authOptions,
this.options.clientId,
);
this.email = email;
return {
user: {
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED,
recoveryShareManagement:
verifiedToken.authDetails.recoveryShareManagement,
},
isNewUser: verifiedToken.isNewUser,
needsRecoveryCode:
verifiedToken.authDetails.recoveryShareManagement ===
RecoveryShareManagement.USER_MANAGED,
};
} catch (error) {
console.error(`Error while verifying auth: ${error}`);
this.disconnect();
throw error;
}
try {
await this.getSigner();
this.emit("connected");
} catch (error) {
if (error instanceof Error) {
return { error: error.message };
} else {
return { error: "Error getting the signer" };
}
}
return { success: true };
}
async disconnect(): Promise<void> {
@@ -297,3 +336,7 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
return this.email;
}
}
function assertUnreachable(x: never): never {
throw new Error("Invalid param: " + x);
}
@@ -1,14 +1,15 @@
import type { CognitoUser } from "amazon-cognito-identity-js";
import {
AuthProvider,
AuthStoredTokenWithCookieReturnType,
RecoveryShareManagement,
} from "@paperxyz/embedded-wallet-service-sdk";
import type { CognitoUser } from "amazon-cognito-identity-js";
import { Auth } from "aws-amplify";
SendEmailOtpReturnType,
} from "@thirdweb-dev/wallets";
import {
generateAuthTokenFromCognitoEmailOtp,
getEmbeddedWalletUserDetail,
sendUserManagedEmailOtp,
validateUserManagedEmailOtp,
verifyClientId,
} from "./helpers/api/fetchers";
import {
@@ -20,17 +21,17 @@ import {
postPaperAuthUserManaged,
prePaperAuth,
} from "./helpers/auth/middleware";
import { isDeviceSharePresentForUser } from "./helpers/storage/local";
import { getCognitoUser, setCognitoUser } from "./helpers/storage/state";
import { SendEmailOtpReturnType } from "@thirdweb-dev/wallets";
import { InAppBrowser } from "react-native-inappbrowser-reborn";
import { AuthOptions, OauthOption } from "../types";
import { isDeviceSharePresentForUser } from "./helpers/storage/local";
import { Auth } from "aws-amplify";
import {
ROUTE_AUTH_JWT_CALLBACK,
ROUTE_HEADLESS_GOOGLE_LOGIN,
} from "./helpers/constants";
import { AuthOptions, OauthOption, VerifiedTokenResponse } from "../types";
import { InAppBrowser } from "react-native-inappbrowser-reborn";
export async function sendEmailOTP(options: {
export async function sendVerificationEmail(options: {
email: string;
clientId: string;
}): Promise<SendEmailOtpReturnType> {
@@ -41,16 +42,6 @@ export async function sendEmailOTP(options: {
email: options.email,
});
// AWS Auth flow
let cognitoUser: CognitoUser;
try {
cognitoUser = await cognitoEmailSignIn(options.email, options.clientId);
} catch (e) {
await cognitoEmailSignUp(options.email, options.clientId);
cognitoUser = await cognitoEmailSignIn(options.email, options.clientId);
}
setCognitoUser(cognitoUser);
let result: Awaited<ReturnType<typeof getEmbeddedWalletUserDetail>>;
try {
result = await getEmbeddedWalletUserDetail({
@@ -63,11 +54,29 @@ export async function sendEmailOTP(options: {
);
}
if (result.recoveryShareManagement === "USER_MANAGED") {
try {
await sendUserManagedEmailOtp(options.email, options.clientId);
} catch (error) {
throw new Error(`Error sending user managed email otp: ${error}`);
}
} else {
// CLOUD_MANAGED
let cognitoUser: CognitoUser;
try {
cognitoUser = await cognitoEmailSignIn(options.email, options.clientId);
} catch (e) {
await cognitoEmailSignUp(options.email, options.clientId);
cognitoUser = await cognitoEmailSignIn(options.email, options.clientId);
}
setCognitoUser(cognitoUser);
}
return result.isNewUser
? {
isNewUser: result.isNewUser,
isNewDevice: true,
recoveryShareManagement: RecoveryShareManagement.AWS_MANAGED,
recoveryShareManagement: result.recoveryShareManagement,
}
: {
isNewUser: result.isNewUser,
@@ -75,60 +84,88 @@ export async function sendEmailOTP(options: {
options.clientId,
result.walletUserId ?? "",
)),
recoveryShareManagement: RecoveryShareManagement.AWS_MANAGED,
recoveryShareManagement: result.recoveryShareManagement,
};
}
export async function validateEmailOTP({
clientId,
otp,
}: {
export async function validateEmailOTP(options: {
email: string;
otp: string;
clientId: string;
recoveryCode?: string;
}): Promise<AuthStoredTokenWithCookieReturnType> {
let verifiedToken: Awaited<
ReturnType<typeof generateAuthTokenFromCognitoEmailOtp>
>["verifiedToken"];
let verifiedTokenJwtString: string;
let result: Awaited<ReturnType<typeof getEmbeddedWalletUserDetail>>;
try {
let cognitoUser = getCognitoUser();
if (!cognitoUser) {
throw new Error("MISSING COGNITO USER");
}
cognitoUser = await Auth.sendCustomChallengeAnswer(cognitoUser, otp);
// It we get here, the answer was sent successfully,
// but it might have been wrong (1st or 2nd time)
// So we should test if the user is authenticated now
const session = await Auth.currentSession();
({ verifiedToken, verifiedTokenJwtString } =
await generateAuthTokenFromCognitoEmailOtp(session, clientId));
result = await getEmbeddedWalletUserDetail({
email: options.email,
clientId: options.clientId,
});
} catch (e) {
throw new Error(`Invalid OTP ${e}`);
throw new Error(
`Malformed response from the send email OTP API: ${JSON.stringify(e)}`,
);
}
let verifiedTokenResponse: VerifiedTokenResponse;
if (result.recoveryShareManagement === "USER_MANAGED") {
try {
verifiedTokenResponse = await validateUserManagedEmailOtp({
email: options.email,
otp: options.otp,
clientId: options.clientId,
});
} catch (error) {
throw new Error(`Error validating user managed email otp: ${error}`);
}
} else {
try {
let cognitoUser = getCognitoUser();
if (!cognitoUser) {
throw new Error("MISSING COGNITO USER");
}
cognitoUser = await Auth.sendCustomChallengeAnswer(
cognitoUser,
options.otp,
);
// It we get here, the answer was sent successfully,
// but it might have been wrong (1st or 2nd time)
// So we should test if the user is authenticated now
const session = await Auth.currentSession();
verifiedTokenResponse = await generateAuthTokenFromCognitoEmailOtp(
session,
options.clientId,
);
} catch (e) {
throw new Error(`Invalid OTP ${e}`);
}
}
try {
const storedToken: AuthStoredTokenWithCookieReturnType["storedToken"] = {
jwtToken: verifiedToken.jwtToken,
authDetails: verifiedToken.authDetails,
authProvider: verifiedToken.authProvider,
developerClientId: verifiedToken.developerClientId,
cookieString: verifiedTokenJwtString,
jwtToken: verifiedTokenResponse.verifiedToken.jwtToken,
authDetails: verifiedTokenResponse.verifiedToken.authDetails,
authProvider: verifiedTokenResponse.verifiedToken.authProvider,
developerClientId: verifiedTokenResponse.verifiedToken.developerClientId,
cookieString: verifiedTokenResponse.verifiedTokenJwtString,
// we should always store the jwt cookie since there's no concept of cookie in react native
shouldStoreCookieString: true,
isNewUser: verifiedToken.isNewUser,
isNewUser: verifiedTokenResponse.verifiedToken.isNewUser,
};
await postPaperAuth(storedToken, clientId);
await postPaperAuth({
storedToken,
clientId: options.clientId,
recoveryCode: options.recoveryCode,
});
return { storedToken };
} catch (e) {
throw new Error(
`Malformed response from the verify one time password: ${JSON.stringify(
e,
)}`,
`Malformed response from the verify one time password: ${
(e as Error).message
}}`,
);
}
}
@@ -195,7 +232,7 @@ export async function socialLogin(oauthOptions: OauthOption, clientId: string) {
isNewUser: storedToken.isNewUser,
};
await postPaperAuth(toStoreToken, clientId);
await postPaperAuth({ storedToken: toStoreToken, clientId });
return { storedToken, email: storedToken.authDetails.email };
} catch (e) {
@@ -218,7 +255,7 @@ export async function customJwt(authOptions: AuthOptions, clientId: string) {
});
if (!resp.ok) {
const error = await resp.json();
throw new Error(`JWT authentication error: ${error.message} `);
throw new Error(`JWT authentication error: ${error.message}`);
}
try {
@@ -1,16 +1,23 @@
import {
AuthProvider,
RecoveryShareManagement,
} from "@paperxyz/embedded-wallet-service-sdk";
import { CognitoUserSession } from "amazon-cognito-identity-js";
import {
ROUTE_GET_EMBEDDED_WALLET_DETAILS,
ROUTE_STORE_USER_SHARES,
ROUTE_VERIFY_THIRDWEB_CLIENT_ID,
ROUTE_VERIFY_COGNITO_OTP,
ROUTE_USER_MANAGED_OTP,
ROUTE_VALIDATE_USER_MANAGED_OTP,
ROUTE_IS_VALID_USER_MANAGED_OTP,
} from "../constants";
import { getAuthTokenClient } from "../storage/local";
import * as Application from "expo-application";
import {
RecoveryShareManagement,
UserWalletStatus,
} from "@thirdweb-dev/wallets";
import {
IsValidUserManagedEmailOTPResponse,
VerifiedTokenResponse,
} from "../../../types";
const EMBEDDED_WALLET_TOKEN_HEADER = "embedded-wallet-token";
const PAPER_CLIENT_ID_HEADER = "x-thirdweb-client-id";
@@ -91,11 +98,14 @@ export async function getEmbeddedWalletUserDetail(args: {
| {
isNewUser: true;
recoveryShareManagement: RecoveryShareManagement;
status: UserWalletStatus;
walletUserId: string;
}
| {
isNewUser: false;
walletUserId: string;
recoveryShareManagement: RecoveryShareManagement;
status: UserWalletStatus;
};
return result;
}
@@ -125,22 +135,82 @@ export async function generateAuthTokenFromCognitoEmailOtp(
);
}
const respJ = await resp.json();
return respJ as {
verifiedToken: {
jwtToken: string;
authProvider: AuthProvider;
developerClientId: string;
authDetails: {
email?: string;
userWalletId: string;
recoveryCode?: string;
cookieString?: string;
recoveryShareManagement: RecoveryShareManagement;
};
isNewUser: boolean;
};
verifiedTokenJwtString: string;
};
return respJ as VerifiedTokenResponse;
}
export async function sendUserManagedEmailOtp(email: string, clientId: string) {
const resp = await fetch(ROUTE_USER_MANAGED_OTP, {
method: "POST",
headers: {
"Content-Type": "application/json",
[BUNDLE_ID_HEADER]: APP_BUNDLE_ID,
},
body: JSON.stringify({
email,
clientId,
}),
});
if (!resp.ok) {
const error = await resp.json();
throw new Error(
`Something went wrong generating auth token from user cognito email otp. ${error.message}`,
);
}
const respJ = await resp.json();
return respJ;
}
export async function validateUserManagedEmailOtp(options: {
email: string;
otp: string;
clientId: string;
}) {
const resp = await fetch(ROUTE_VALIDATE_USER_MANAGED_OTP, {
method: "POST",
headers: {
"Content-Type": "application/json",
[BUNDLE_ID_HEADER]: APP_BUNDLE_ID,
},
body: JSON.stringify({
email: options.email,
otp: options.otp,
clientId: options.clientId,
}),
});
if (!resp.ok) {
const error = await resp.json();
throw new Error(
`Something went wrong generating auth token from user cognito email otp. ${error.message}`,
);
}
const respJ = await resp.json();
return respJ as VerifiedTokenResponse;
}
export async function isValidUserManagedEmailOtp(options: {
email: string;
otp: string;
clientId: string;
}) {
const resp = await fetch(ROUTE_IS_VALID_USER_MANAGED_OTP, {
method: "POST",
headers: {
"Content-Type": "application/json",
[BUNDLE_ID_HEADER]: APP_BUNDLE_ID,
},
body: JSON.stringify({
email: options.email,
otp: options.otp,
clientId: options.clientId,
}),
});
if (!resp.ok) {
const error = await resp.json();
throw new Error(
`Something went wrong generating auth token from user cognito email otp. ${error.message}`,
);
}
const respJ = await resp.json();
return respJ as IsValidUserManagedEmailOTPResponse;
}
export async function storeUserShares({
@@ -1,7 +1,8 @@
import {
AuthProvider,
AuthStoredTokenWithCookieReturnType,
} from "@paperxyz/embedded-wallet-service-sdk";
RecoveryShareManagement,
} from "@thirdweb-dev/wallets";
import {
getDeviceShare,
setAuthTokenClient,
@@ -10,6 +11,7 @@ import {
import { setUpNewUserWallet } from "../wallet/creation";
import { getCognitoRecoveryPassword } from "../wallet/recoveryCode";
import { setUpShareForNewDevice } from "../wallet/retrieval";
import { ErrorMessages } from "../errors";
export async function prePaperAuth(args: {
authenticationMethod: AuthProvider;
@@ -19,10 +21,15 @@ export async function prePaperAuth(args: {
Promise.resolve(args);
}
export async function postPaperAuth(
storedToken: AuthStoredTokenWithCookieReturnType["storedToken"],
clientId: string,
) {
export async function postPaperAuth({
storedToken,
clientId,
recoveryCode,
}: {
storedToken: AuthStoredTokenWithCookieReturnType["storedToken"];
clientId: string;
recoveryCode?: string;
}) {
if (storedToken.shouldStoreCookieString) {
await setAuthTokenClient(storedToken.cookieString, clientId);
}
@@ -34,23 +41,32 @@ export async function postPaperAuth(
});
if (storedToken.isNewUser) {
console.log("========== New User ==========");
const recoveryCode = await getCognitoRecoveryPassword(clientId);
await setUpNewUserWallet(recoveryCode, clientId);
const _recoveryCode = await getRecoveryCode(
storedToken,
clientId,
recoveryCode,
);
if (!_recoveryCode) {
throw new Error(ErrorMessages.missingRecoveryCode);
}
await setUpNewUserWallet(_recoveryCode, clientId);
} else {
try {
// existing device share
await getDeviceShare(clientId);
console.log("========== Existing user with device share ==========");
} catch (e) {
// trying to recreate device share from recovery code to derive wallet
const recoveryCode = await getCognitoRecoveryPassword(clientId);
console.log("========== Existing user on new device ==========");
const _recoveryCode = await getRecoveryCode(
storedToken,
clientId,
recoveryCode,
);
if (!_recoveryCode) {
throw new Error(ErrorMessages.missingRecoveryCode);
}
try {
await setUpShareForNewDevice({
clientId,
recoveryCode,
recoveryCode: _recoveryCode,
});
} catch (error) {
console.error("Error setting up wallet on device", error);
@@ -67,6 +83,8 @@ export async function postPaperAuthUserManaged(
clientId: string,
password: string,
) {
const _password = await getRecoveryCode(storedToken, clientId, password);
if (storedToken.shouldStoreCookieString) {
await setAuthTokenClient(storedToken.cookieString, clientId);
}
@@ -78,21 +96,17 @@ export async function postPaperAuthUserManaged(
});
if (storedToken.isNewUser) {
console.log("========== New User ==========");
await setUpNewUserWallet(password, clientId);
await setUpNewUserWallet(_password, clientId);
} else {
try {
// existing device share
await getDeviceShare(clientId);
console.log("========== Existing user with device share ==========");
} catch (e) {
// trying to recreate device share from recovery code to derive wallet
console.log("========== Existing user on new device ==========");
try {
await setUpShareForNewDevice({
clientId,
recoveryCode: password,
recoveryCode: _password,
});
} catch (error) {
console.error("Error setting up wallet on device", error);
@@ -103,3 +117,64 @@ export async function postPaperAuthUserManaged(
return storedToken;
}
async function getRecoveryCode(
storedToken: AuthStoredTokenWithCookieReturnType["storedToken"],
clientId: string,
recoveryCode?: string,
): Promise<string> {
if (
storedToken.authDetails.recoveryShareManagement ===
RecoveryShareManagement.CLOUD_MANAGED
) {
if (storedToken.authProvider === AuthProvider.CUSTOM_JWT) {
// derive sub here
const code = extractSubFromJwt(storedToken.jwtToken);
if (!code) {
throw new Error(
`GetRecoveryCode error: ${ErrorMessages.missingRecoveryCode}`,
);
}
return code;
} else {
try {
const code = await getCognitoRecoveryPassword(clientId);
return code;
} catch (e) {
throw new Error("Something went wrong getting cognito recovery code");
}
}
} else if (
storedToken.authDetails.recoveryShareManagement ===
RecoveryShareManagement.USER_MANAGED
) {
if (recoveryCode) {
return recoveryCode;
}
throw new Error(ErrorMessages.missingRecoveryCode);
} else {
throw new Error("Invalid recovery share management option");
}
}
function extractSubFromJwt(jwtToken: string): string | undefined {
const parts = jwtToken.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format.");
}
const encodedPayload = parts[1];
if (!encodedPayload) {
throw new Error("Invalid JWT format.");
}
const decodedPayload = Buffer.from(encodedPayload, "base64").toString("utf8");
try {
const payloadObject = JSON.parse(decodedPayload);
if (payloadObject && payloadObject.sub) {
return payloadObject.sub;
}
} catch (error) {
throw new Error("Error parsing JWT payload as JSON.");
}
}
@@ -25,11 +25,16 @@ const ROUTE_2023_10_20_API_BASE_PATH = `${BASE_URL_2023}api/2023-10-20`;
export const ROUTE_GET_EMBEDDED_WALLET_DETAILS = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/embedded-wallet-user-details`;
export const ROUTE_VERIFY_COGNITO_OTP = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/validate-cognito-email-otp`;
export const ROUTE_COGNITO_IDENTITY_POOL_URL = `cognito-idp.${AWS_REGION}.amazonaws.com/${COGNITO_USER_POOL_ID}`;
export const ROUTE_STORE_USER_SHARES = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/embedded-wallet-shares`;
export const ROUTE_GET_USER_SHARES = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/embedded-wallet-shares`;
export const ROUTE_VERIFY_THIRDWEB_CLIENT_ID = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/verify-thirdweb-client-id`;
export const ROUTE_AUTH_JWT_CALLBACK = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/validate-custom-jwt`;
export const ROUTE_USER_MANAGED_OTP = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/send-user-managed-email-otp`;
export const ROUTE_VALIDATE_USER_MANAGED_OTP = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/validate-thirdweb-email-otp`;
export const ROUTE_IS_VALID_USER_MANAGED_OTP = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/is-thirdweb-email-otp-valid`;
export const ROUTE_HEADLESS_GOOGLE_LOGIN = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/headless-oauth-login-link`;
export const ROUTE_AUTH_COGNITO_ID_TOKEN = `${ROUTE_2023_10_20_API_BASE_PATH}/embedded-wallet/cognito-id-token`;
@@ -0,0 +1,5 @@
export const ErrorMessages = {
invalidOtp:
"Your OTP code is invalid or expired. Please request a new code or try again.",
missingRecoveryCode: "Missing encryption key for user",
};
@@ -1,10 +1,6 @@
import { getDefaultProvider } from "ethers";
import { getExistingUserEtherJsWallet } from "./helpers/wallet/retrieval";
export async function getEthersSigner(clientId: string) {
const wallet = await getExistingUserEtherJsWallet(clientId);
const newWallet = wallet.connect(getDefaultProvider());
await newWallet.getChainId();
return newWallet;
return wallet;
}
@@ -1,10 +1,10 @@
import { Chain } from "@thirdweb-dev/chains";
import { WalletOptions } from "@thirdweb-dev/wallets";
import type {
RecoveryShareManagement,
PaperConstructorType,
import {
AuthProvider,
} from "@paperxyz/embedded-wallet-service-sdk";
RecoveryShareManagement,
UserWalletStatus,
WalletOptions,
} from "@thirdweb-dev/wallets";
export type OauthOptions = {
providers: AuthProvider[];
@@ -29,14 +29,7 @@ export type EmbeddedWalletOptions = Omit<
>;
type EmbeddedAdvanceOptions = {
recoveryShareManagement?: "USER_MANAGED" | "AWS_MANAGED";
};
export type EmbeddedWalletAdditionalOptions = {
emailClientId: string;
chain: Pick<Chain, "chainId" | "rpc">;
advancedOptions?: EmbeddedAdvanceOptions;
styles?: PaperConstructorType<RecoveryShareManagement>["styles"];
recoveryShareManagement?: RecoveryShareManagement;
};
export interface EmbeddedWalletConnectorOptions {
@@ -44,7 +37,6 @@ export interface EmbeddedWalletConnectorOptions {
chain: Pick<Chain, "chainId" | "rpc">;
chains: Chain[];
advancedOptions?: EmbeddedAdvanceOptions;
styles?: PaperConstructorType<RecoveryShareManagement>["styles"];
}
export interface AuthOptions {
@@ -52,21 +44,70 @@ export interface AuthOptions {
password: string;
}
export type SendEmailOtpReturnType = {
isNewUser: boolean;
isNewDevice: boolean;
recoveryShareManagement: RecoveryShareManagement;
};
export type EmbeddedWalletConnectionArgs = {
chainId?: number;
} & (
| {
loginType: "headless_google_oauth";
redirectUrl: string;
}
| {
loginType: "headless_email_otp_verification";
email: string;
otp: string;
}
| {
loginType: "jwt";
jwt: string;
password: string;
}
);
authResult: AuthResult;
};
type EmailVerificationAuthParams = {
strategy: "email_verification";
email: string;
verificationCode: string;
recoveryCode?: string;
};
type GoogleAuthParams = {
strategy: "google";
redirectUrl: string;
};
type JwtAuthParams = {
strategy: "jwt";
jwt: string;
encryptionKey?: string;
};
// this is the input to 'authenticate'
export type AuthParams =
| EmailVerificationAuthParams
| GoogleAuthParams
| JwtAuthParams;
// TODO typed based off AuthParams["strategy"]
export type AuthResult = {
user?: InitializedUser;
isNewUser?: boolean;
needsRecoveryCode?: boolean;
};
export type InitializedUser = {
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED;
recoveryShareManagement: RecoveryShareManagement;
};
export type VerifiedTokenResponse = {
verifiedToken: {
jwtToken: string;
authProvider: AuthProvider;
developerClientId: string;
authDetails: {
email?: string;
userWalletId: string;
recoveryCode?: string;
cookieString?: string;
recoveryShareManagement: RecoveryShareManagement;
};
isNewUser: boolean;
};
verifiedTokenJwtString: string;
};
export type IsValidUserManagedEmailOTPResponse = {
isValid: { type: "boolean" };
};
@@ -0,0 +1,72 @@
import {
useCreateWalletInstance,
useSetConnectedWallet,
useSetConnectionStatus,
useWalletContext,
useWallets,
} from "@thirdweb-dev/react-core";
import { useCallback } from "react";
import { AuthParams } from "../connectors/embedded-wallet/types";
import { walletIds } from "@thirdweb-dev/wallets";
import { EmbeddedWallet } from "../wallets/embedded/EmbeddedWallet";
import { embeddedWallet } from "../wallets/embedded/embedded-wallet";
/**
* Hook to authenticate and connect to an embedded wallet
* @returns a function to connect to an embedded wallet
*/
export function useEmbeddedWallet() {
const create = useCreateWalletInstance();
const setStatus = useSetConnectionStatus();
const setWallet = useSetConnectedWallet();
const context = useWalletContext();
const wallets = useWallets();
const connect = useCallback(
async (authParams: AuthParams) => {
setStatus("connecting");
const config = wallets.find((w) => w.id === walletIds.embeddedWallet);
if (
!config ||
(config.personalWallets && config.personalWallets.length > 0)
) {
console.warn(
"Embedded wallet not configured in ThirdwebProvider.supportedWallets - add it to enable autoconnect on page load",
);
}
const wallet = create(embeddedWallet());
try {
const authResult = await wallet.authenticate(authParams);
await wallet.connect({ authResult });
setWallet(wallet);
setStatus("connected");
} catch (e) {
console.error("Error connecting to embedded wallet", e);
setStatus("disconnected");
}
return wallet;
},
[setStatus, wallets, create, setWallet],
);
const sendVerificationEmail = useCallback(
async ({ email }: { email: string }) => {
const clientId = context.clientId;
if (!clientId) {
throw new Error(
"clientId not found, please add it to ThirdwebProvider",
);
}
return EmbeddedWallet.sendVerificationEmail({
email,
clientId,
});
},
[context],
);
return {
connect,
sendVerificationEmail,
};
}
@@ -0,0 +1,22 @@
import { useWalletContext } from "@thirdweb-dev/react-core";
import { useCallback } from "react";
import { EmbeddedWallet } from "../wallets/embedded/EmbeddedWallet";
export const useEmbeddedWalletSendVerificationEmail = () => {
const context = useWalletContext();
const sendVerificationEmail = useCallback(
async (email: string) => {
if (!context.clientId) {
throw new Error("Please, provide a clientId in the ThirdwebProvider");
}
return EmbeddedWallet.sendVerificationEmail({
email,
clientId: context.clientId,
});
},
[context.clientId],
);
return sendVerificationEmail;
};
@@ -0,0 +1,92 @@
import {
type WalletConfig,
WalletInstance,
useCreateWalletInstance,
useSetConnectedWallet,
useSetConnectionStatus,
useWalletContext,
useWallets,
} from "@thirdweb-dev/react-core";
import {
getSmartWalletAddress,
type SmartWalletConnectionArgs,
} from "@thirdweb-dev/wallets/evm/wallets/smart-wallet";
import { useCallback } from "react";
import type { BytesLike } from "ethers";
import { SmartWalletConfig } from "../types/smart-wallet";
export function useSmartWallet<W extends WalletInstance>(
personalWallet: WalletConfig<W>,
options: SmartWalletConfig,
) {
const create = useCreateWalletInstance();
const setStatus = useSetConnectionStatus();
const setWallet = useSetConnectedWallet();
const context = useWalletContext();
const supportedWallets = useWallets();
const predictAddress = useCallback(
async (args: { personalWalletAddress: string; data?: BytesLike }) => {
return getSmartWalletAddress(
context.activeChain,
options.factoryAddress,
args.personalWalletAddress,
args.data,
);
},
[context.activeChain, options.factoryAddress],
);
const connect = useCallback(
async (args?: {
connectPersonalWallet?: (wallet: W) => Promise<void>;
connectionArgs?: Omit<SmartWalletConnectionArgs, "personalWallet">;
}) => {
if (!supportedWallets.find((w) => w.id === personalWallet.id)) {
console.warn(
"Please, add your smart wallet to the supportedWallets prop of the ThirdwebProvider to enjoy auto-connecting to the wallet.",
);
}
const { smartWallet } = await import("../wallets/smart-wallet");
setStatus("connecting");
const pw = create(personalWallet);
const sw = create(smartWallet(personalWallet, options));
try {
if (args?.connectPersonalWallet) {
// if passed in, use custom function to connect personal wallet
await args.connectPersonalWallet(pw);
} else {
// otherwise default to auto-connecting personal wallet with chainId
await pw.connect({
chainId: context.activeChain.chainId,
});
}
await sw.connect({
...args?.connectionArgs,
personalWallet: pw,
});
setStatus("connected");
setWallet(sw);
} catch (e) {
console.error("Error connecting to smart wallet", e);
setStatus("disconnected");
throw e;
}
return sw;
},
[
context.activeChain.chainId,
create,
options,
personalWallet,
setStatus,
setWallet,
supportedWallets,
],
);
return {
connect,
predictAddress,
};
}
@@ -1,91 +0,0 @@
import React, { useState } from "react";
import { ActivityIndicator } from "react-native";
import { ConnectWalletHeader } from "../../../components/ConnectWalletFlow/ConnectingWallet/ConnectingWalletHeader";
import { Box, BaseButton, Text, TextInput } from "../../../components/base";
import {
useGlobalTheme,
useLocale,
} from "../../../providers/ui-context-provider";
export type EnterPasswordProps = {
goBack: () => void;
close: () => void;
};
export const AccountRecovery = ({ close, goBack }: EnterPasswordProps) => {
const l = useLocale();
const theme = useGlobalTheme();
const [errorMessage, setErrorMessage] = useState<string>();
const [checkingRecoveryCode, setCheckingRecoveryCode] =
useState<boolean>(false);
const [recoveryCode, setRecoveryCode] = useState<string>("");
const onNextPress = async () => {
setCheckingRecoveryCode(true);
// Call enter recovery code
setErrorMessage("test");
};
return (
<Box marginHorizontal="xl">
<ConnectWalletHeader
middleContent={
<Text variant="header">
{l.embedded_wallet.enter_account_recovery_code}
</Text>
}
subHeaderText={
"You should have a copy of this in the email address associated with your account"
}
onBackPress={goBack}
onClose={close}
/>
<TextInput
textInputProps={{
placeholder: "Enter your recovery code",
placeholderTextColor: theme.colors.textSecondary,
onChangeText: setRecoveryCode,
style: {
fontSize: 14,
color: theme.colors.textPrimary,
lineHeight: 16,
padding: 0,
flex: 1,
},
value: recoveryCode,
returnKeyType: "done",
autoCapitalize: "none",
autoCorrect: false,
autoComplete: "off",
clearTextOnFocus: false,
}}
containerProps={{
paddingHorizontal: "sm",
paddingVertical: "sm",
justifyContent: "flex-start",
}}
/>
{errorMessage ? (
<Text variant="error" numberOfLines={1}>
{errorMessage}
</Text>
) : (
<Box height={20} />
)}
<BaseButton
mt="sm"
alignItems="center"
height={theme.textVariants.bodySmallSecondary.fontSize}
onPress={onNextPress}
>
{checkingRecoveryCode ? (
<ActivityIndicator size={"small"} color={theme.colors.linkPrimary} />
) : (
<Text variant="bodySmallSecondary" color="linkPrimary">
{l.common.next}
</Text>
)}
</BaseButton>
</Box>
);
};
@@ -1,112 +0,0 @@
import React, { useEffect, useState } from "react";
import { ConnectWalletHeader } from "../../../components/ConnectWalletFlow/ConnectingWallet/ConnectingWalletHeader";
import { Box, BaseButton, Text, Toast } from "../../../components/base";
import {
useGlobalTheme,
useLocale,
} from "../../../providers/ui-context-provider";
import CopyIcon from "../../../assets/copy";
import CheckBox from "@react-native-community/checkbox";
import * as Clipboard from "expo-clipboard";
export type EnterPasswordProps = {
goBack: () => void;
close: () => void;
};
export const AccountRecovery = ({ close, goBack }: EnterPasswordProps) => {
const l = useLocale();
const theme = useGlobalTheme();
const [toggleCheckBox, setToggleCheckBox] = useState(false);
const [errorMessage] = useState<string>();
const [codeCopied, setCodeCopied] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
if (codeCopied) {
setCodeCopied(false);
}
}, 2000);
return () => clearTimeout(timeout);
}, [codeCopied]);
const onNextPress = async () => {};
const onCodeCopyPress = async (code: string) => {
await Clipboard.setStringAsync(code);
setCodeCopied(true);
};
const codes = ["testing", "testing2", "testing3", "testing4", "testing5"];
return (
<Box marginHorizontal="xl">
<ConnectWalletHeader
middleContent={
<Text variant="header">{l.embedded_wallet.backup_your_account}</Text>
}
subHeaderText={
"Copy or download these codes and keep them safe. You will need these to recover access to your account if you forget your password. These have also been sent to the email address associated with your account"
}
onBackPress={goBack}
onClose={close}
/>
<Box mt="sm" flexDirection="column" marginTop="xl" mb="md">
{codes.map((code) => (
<BaseButton
key={code}
mt="xs"
flexDirection="row"
alignItems="center"
justifyContent="space-between"
borderColor="border"
borderWidth={1}
borderRadius="md"
onPress={() => {
onCodeCopyPress(code);
}}
p="md"
>
<Text variant="bodySmall">{code}</Text>
<CopyIcon width={14} height={14} color={theme.colors.iconPrimary} />
</BaseButton>
))}
</Box>
{errorMessage ? (
<Text variant="error" numberOfLines={1}>
{errorMessage}
</Text>
) : (
<Box height={20} />
)}
<Box flex={1} flexDirection="row" justifyContent="flex-end">
<BaseButton>
<CheckBox
disabled={false}
value={toggleCheckBox}
onValueChange={(newValue) => setToggleCheckBox(newValue)}
/>
<Text variant="bodySmallSecondary" color="linkPrimary">
{l.common.learn_more}
</Text>
</BaseButton>
<BaseButton
mt="sm"
flexDirection="row"
alignItems="center"
height={theme.textVariants.bodySmallSecondary.fontSize}
onPress={onNextPress}
>
<Text variant="bodySmallSecondary" color="linkPrimary">
{l.common.next}
</Text>
</BaseButton>
</Box>
{codeCopied === true ? (
<Toast text={"Address copied to clipboard"} />
) : null}
</Box>
);
};
@@ -5,9 +5,10 @@ import {
useSetConnectionStatus,
} from "@thirdweb-dev/react-core";
import { EmbeddedWallet } from "./EmbeddedWallet";
import React, { useRef, useEffect, useState } from "react";
import React, { useRef, useEffect, useState, useCallback } from "react";
import {
ActivityIndicator,
Keyboard,
NativeSyntheticEvent,
TextInput,
TextInputKeyPressEventData,
@@ -23,8 +24,14 @@ import {
useGlobalTheme,
useLocale,
} from "../../../providers/ui-context-provider";
import { EnterPassword } from "./EnterPassword";
import { RecoveryShareManagement } from "@thirdweb-dev/wallets";
const OTP_LENGTH = 6;
type ScreenToShow =
| "base"
| "create-password"
| "enter-password-or-recovery-code";
export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
connected,
@@ -44,10 +51,77 @@ export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
const [errorMessage, setErrorMessage] = useState<string>("");
const setConnectedWallet = useSetConnectedWallet();
const setConnectionStatus = useSetConnectionStatus();
const [password, setPassword] = useState("");
const [focusedIndex, setFocusedIndex] = useState<number | undefined>();
const [screen, setScreen] = useState<ScreenToShow>("base"); // TODO change
const address = useAddress();
const onError = useCallback((error: any) => {
clearCode();
console.error("Error validating otp: ", error);
setErrorMessage("Error validating the code");
setCheckingOtp(false);
setFocusedIndex(undefined);
}, []);
const postConnect = useCallback(
async (response: string) => {
if (response) {
if (onLocallyConnected) {
onLocallyConnected(selectionData.emailWallet);
} else {
await setConnectedWallet(selectionData.emailWallet);
setConnectionStatus("connected");
}
} else {
clearCode();
setErrorMessage(response || "Error validating the code");
setCheckingOtp(false);
setFocusedIndex(undefined);
}
},
[
onLocallyConnected,
selectionData.emailWallet,
setConnectedWallet,
setConnectionStatus,
],
);
const connect = useCallback(
async (_password?: string) => {
try {
const emailWallet = selectionData.emailWallet as EmbeddedWallet;
const otp = values.join("");
const authResult = await emailWallet.authenticate({
strategy: "email_verification",
verificationCode: otp,
email: selectionData.email,
recoveryCode: _password,
});
if (!authResult) {
onError(new Error("Failed to verify OTP"));
}
const response = await emailWallet.connect({
authResult,
});
postConnect(response);
} catch (error) {
onError(error);
}
},
[
onError,
postConnect,
selectionData.email,
selectionData.emailWallet,
values,
],
);
useEffect(() => {
if (address) {
setCheckingOtp(false);
@@ -55,50 +129,87 @@ export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
}
}, [address, connected]);
useEffect(() => {
const onContinuePress = useCallback(() => {
if (
values.length === OTP_LENGTH &&
values.every((v) => v.length === 1) &&
selectionData
selectionData &&
screen === "base"
) {
setCheckingOtp(true);
setErrorMessage("");
setFocusedIndex(undefined);
const otp = values.join("");
Keyboard.dismiss();
setTimeout(() => {
(selectionData.emailWallet as EmbeddedWallet)
.connect({
loginType: "headless_email_otp_verification",
otp,
email: selectionData.email,
})
.then(async (response) => {
if (response) {
if (onLocallyConnected) {
onLocallyConnected(selectionData.emailWallet);
} else {
await setConnectedWallet(selectionData.emailWallet);
setConnectionStatus("connected");
}
} else {
clearCode();
setErrorMessage(response || "Error validating the code");
setCheckingOtp(false);
setFocusedIndex(undefined);
setTimeout(async () => {
const emailWallet = selectionData.emailWallet as EmbeddedWallet;
const needsRecoveryCode =
selectionData.recoveryShareManagement ===
RecoveryShareManagement.USER_MANAGED &&
(selectionData.isNewUser || selectionData.isNewDevice);
if (needsRecoveryCode) {
if (selectionData.isNewUser) {
console.log("setPassword", setPassword);
try {
const authResult = await emailWallet.authenticate({
strategy: "email_verification",
verificationCode: otp,
email: selectionData.email,
recoveryCode: password,
});
const response = await emailWallet.connect({
authResult,
});
postConnect(response);
} catch (e: any) {
onError(e);
}
})
.catch((error) => {
clearCode();
console.error("Error validating otp: ", error);
setErrorMessage("Error validating the code");
setCheckingOtp(false);
setFocusedIndex(undefined);
});
} else {
try {
await emailWallet.authenticate({
strategy: "email_verification",
verificationCode: otp,
email: selectionData.email,
});
} catch (e: any) {
if (e instanceof Error && e.message.includes("encryption key")) {
setScreen("enter-password-or-recovery-code");
} else {
onError(e);
}
}
}
} else {
// CLOUD_MANAGED
connect();
}
}, 0);
}
}, [connect, onError, password, postConnect, screen, selectionData, values]);
useEffect(() => {
if (
values.length === OTP_LENGTH &&
values.every((v) => v.length === 1) &&
selectionData &&
screen === "base" &&
selectionData.recoveryShareManagement ===
RecoveryShareManagement.CLOUD_MANAGED
) {
onContinuePress();
}
}, [
connect,
onContinuePress,
onError,
onLocallyConnected,
password,
postConnect,
screen,
selectionData,
setConnectedWallet,
setConnectionStatus,
@@ -109,7 +220,7 @@ export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
if (value !== "") {
const copiedContent = await Clipboard.getStringAsync();
if (copiedContent.length === 6 && /^\d+$/.test(copiedContent)) {
if (copiedContent.length === 6) {
const newValues = [...copiedContent];
for (let i = 0; i < 6; i++) {
inputRefs.current[i]?.setNativeProps({ text: newValues[i] });
@@ -127,7 +238,7 @@ export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
if (value === "" && index > 0) {
inputRefs.current[index - 1]?.focus();
} else if (value.length === 1 && /^[0-9]$/.test(value)) {
} else if (value.length === 1) {
if (index < 5) {
inputRefs.current[index + 1]?.focus();
}
@@ -157,7 +268,7 @@ export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
setFocusedIndex(undefined);
setRequestingNewOtp(true);
(selectionData.emailWallet as EmbeddedWallet)
.sendEmailOTP(selectionData.email)
.sendVerificationEmail(selectionData.email)
.then(() => {})
.catch((error) => {
clearCode();
@@ -169,6 +280,37 @@ export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
});
};
if (screen === "create-password") {
return (
<EnterPassword
close={goBack}
goBack={() => {
setScreen("base");
}}
email={selectionData.email}
type="create_password"
onPassword={connect}
error={errorMessage}
/>
);
}
if (screen === "enter-password-or-recovery-code") {
return (
<EnterPassword
close={goBack}
goBack={() => {
setScreen("base");
}}
email={selectionData.email}
type="enter_password"
onPassword={connect}
error={errorMessage}
/>
);
}
// TODO (ews) - check actual auth options
if (selectionData?.oauthOptions) {
return (
<EmbeddedSocialConnection
@@ -227,12 +369,15 @@ export const EmbeddedConnectionUI: React.FC<ConnectUIProps<EmbeddedWallet>> = ({
...styles.textInput,
color: theme.colors.textPrimary,
}}
keyboardType="number-pad"
editable={!checkingOtp}
keyboardType="default"
selectTextOnFocus={true}
onFocus={() => setFocusedIndex(index)}
maxLength={1}
returnKeyType={index === 5 ? "done" : "next"}
onSubmitEditing={
index === 5 ? () => onContinuePress() : undefined
}
onChangeText={(value) => handleInputChange(value, index)}
onKeyPress={(e) => handleKeyPress(e, index)}
/>
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { EmbeddedWallet } from "./EmbeddedWallet";
import { ActivityIndicator } from "react-native";
import { ActivityIndicator, Keyboard } from "react-native";
import {
SelectUIProps,
useCreateWalletInstance,
@@ -11,22 +11,21 @@ import BaseButton from "../../../components/base/BaseButton";
import { TextInput } from "../../../components/base/TextInput";
import { GOOGLE_ICON } from "../../../assets/svgs";
import { WalletButton } from "../../../components/base/WalletButton";
import { AuthProvider } from "@paperxyz/embedded-wallet-service-sdk";
import { OauthOptions } from "../../connectors/embedded-wallet/types";
import {
useGlobalTheme,
useLocale,
} from "../../../providers/ui-context-provider";
import { EmbeddedWalletConfig } from "./embedded-wallet";
import { AuthProvider } from "@thirdweb-dev/wallets";
/**
* UI for selecting wallet - this UI is rendered in the wallet selection screen
*/
export const EmailSelectionUI: React.FC<
SelectUIProps<EmbeddedWallet> & {
oauthOptions?: OauthOptions;
email?: boolean;
auth?: EmbeddedWalletConfig["auth"];
}
> = ({ onSelect, walletConfig, oauthOptions, email }) => {
> = ({ onSelect, walletConfig, auth }) => {
const l = useLocale();
const theme = useGlobalTheme();
const [emailInput, setEmailInput] = useState<string>("");
@@ -35,7 +34,8 @@ export const EmailSelectionUI: React.FC<
const createWalletInstance = useCreateWalletInstance();
const [emailWallet, setEmailWallet] = useState<EmbeddedWallet | null>(null);
const isEmailEnabled = email === false ? false : true;
const isEmailEnabled = auth?.options.includes("email");
const isGoogleEnabled = auth?.options.includes("google");
useEffect(() => {
const emailWalletInstance = createWalletInstance(
@@ -54,15 +54,16 @@ export const EmailSelectionUI: React.FC<
if (validateEmail(emailInput)) {
setErrorMessage("");
setIsFetching(true);
Keyboard.dismiss();
emailWallet
?.sendEmailOTP(emailInput)
?.sendVerificationEmail(emailInput)
.then((response) => {
onSelect({
...response,
email: emailInput,
emailWallet,
oauthOptions: undefined,
oauthOptions: undefined, // TODO (ews) clean up types
});
})
.catch((error) => {
@@ -83,15 +84,15 @@ export const EmailSelectionUI: React.FC<
email: emailInput,
emailWallet,
oauthOptions: {
provider: oauthOptions?.providers[0],
redirectUrl: oauthOptions?.redirectUrl,
provider: AuthProvider.GOOGLE,
redirectUrl: auth?.redirectUrl,
},
});
};
return (
<Box paddingHorizontal="xl" mt="lg">
{oauthOptions?.providers.includes(AuthProvider.GOOGLE) ? (
{isGoogleEnabled ? (
<Box justifyContent="center">
<WalletButton
iconHeight={28}
@@ -142,6 +143,7 @@ export const EmailSelectionUI: React.FC<
padding: 0,
flex: 1,
},
onSubmitEditing: handleNetworkCall,
value: emailInput,
keyboardType: "email-address",
returnKeyType: "done",
@@ -21,30 +21,33 @@ export const EmbeddedSocialConnection: React.FC<
const handleGoogleLogin = useCallback(() => {
setErrorMessage("");
setTimeout(() => {
(selectionData.emailWallet as EmbeddedWallet)
.connect({
loginType: "headless_google_oauth",
setTimeout(async () => {
const emailWallet = selectionData.emailWallet as EmbeddedWallet;
try {
const authResult = await emailWallet.authenticate({
strategy: "google",
redirectUrl: selectionData.oauthOptions?.redirectUrl,
})
.then(async (response) => {
if (response) {
if (onLocallyConnected) {
onLocallyConnected(selectionData.emailWallet);
} else {
await setConnectedWallet(selectionData.emailWallet);
setConnectionStatus("connected");
}
} else {
setErrorMessage(
response || "Error login in. Please try again later.",
);
}
})
.catch((error) => {
console.error("Error logging in with google: ", error);
setErrorMessage("Error login in. Please try again later.");
});
const response = await emailWallet.connect({ authResult });
if (response) {
if (onLocallyConnected) {
onLocallyConnected(selectionData.emailWallet);
} else {
await setConnectedWallet(selectionData.emailWallet);
setConnectionStatus("connected");
}
} else {
setErrorMessage(
response || "Error login in. Please try again later.",
);
}
} catch (error) {
console.error("Error logging in with google: ", error);
setErrorMessage(`Error login in. ${error}`);
}
}, 0);
}, [
onLocallyConnected,
@@ -79,9 +82,7 @@ export const EmbeddedSocialConnection: React.FC<
{!errorMessage ? (
<ActivityIndicator size="large" />
) : (
<Text variant="error" numberOfLines={1}>
{errorMessage}
</Text>
<Text variant="error">{errorMessage}</Text>
)}
</Box>
</Box>
@@ -4,12 +4,11 @@ import {
EmbeddedWalletAdditionalOptions,
walletIds,
} from "@thirdweb-dev/wallets";
import type { EmbeddedWalletConnector as EmbeddedConnectorType } from "../../connectors/embedded-wallet/embedded-connector";
import type { EmbeddedWalletConnector } from "../../connectors/embedded-wallet/embedded-connector";
import {
AuthParams,
EmbeddedWalletConnectionArgs,
OauthOption,
} from "../../connectors/embedded-wallet/types";
import { EmbeddedWalletConnector } from "../../connectors/embedded-wallet/embedded-connector";
import { EMAIL_WALLET_ICON } from "../../../assets/svgs";
export type EmbeddedWalletOptions =
@@ -19,9 +18,19 @@ export class EmbeddedWallet extends AbstractClientWallet<
EmbeddedWalletOptions,
EmbeddedWalletConnectionArgs
> {
connector?: EmbeddedConnectorType;
connector?: EmbeddedWalletConnector;
options: EmbeddedWalletOptions;
static async sendVerificationEmail(options: {
email: string;
clientId: string;
}) {
const { sendVerificationEmail } = await import(
"../../connectors/embedded-wallet/embedded/auth"
);
return sendVerificationEmail(options);
}
static meta = {
name: "Embedded Wallet",
iconURL: EMAIL_WALLET_ICON,
@@ -39,14 +48,17 @@ export class EmbeddedWallet extends AbstractClientWallet<
this.setupListeners();
}
async getConnector(): Promise<EmbeddedConnectorType> {
async getConnector(): Promise<EmbeddedWalletConnector> {
if (!this.connector) {
return await this.initializeConnector();
}
return this.connector;
}
initializeConnector() {
async initializeConnector() {
const { EmbeddedWalletConnector } = await import(
"../../connectors/embedded-wallet/embedded-connector"
);
this.connector = new EmbeddedWalletConnector({
...this.options,
clientId: this.options.clientId,
@@ -56,16 +68,13 @@ export class EmbeddedWallet extends AbstractClientWallet<
return this.connector;
}
async validateEmailOTP(otp: string) {
return this.connector?.validateEmailOtp({ otp });
async sendVerificationEmail(email: string) {
return this.connector?.sendVerificationEmail({ email });
}
async sendEmailOTP(email: string) {
return this.connector?.sendEmailOtp({ email });
}
async socialLogin(oauthOption: OauthOption) {
return this.connector?.socialLogin(oauthOption);
async authenticate(params: AuthParams) {
const connector = (await this.getConnector()) as EmbeddedWalletConnector;
return connector.authenticate(params);
}
onConnected = () => {
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { ActivityIndicator } from "react-native";
import { ConnectWalletHeader } from "../../../components/ConnectWalletFlow/ConnectingWallet/ConnectingWalletHeader";
import { Box, BaseButton, Text } from "../../../components/base";
@@ -7,12 +7,15 @@ import {
useLocale,
} from "../../../providers/ui-context-provider";
import { PasswordInput } from "../../../components/PasswordInput";
import Checkbox from "../../../components/base/CheckBox";
export type EnterPasswordProps = {
goBack: () => void;
close: () => void;
email: string;
type: "create_password" | "enter_password";
onPassword: (password: string) => void;
error: string;
};
export const EnterPassword = ({
@@ -20,49 +23,68 @@ export const EnterPassword = ({
goBack,
email,
type,
onPassword,
error,
}: EnterPasswordProps) => {
const l = useLocale();
const theme = useGlobalTheme();
const [errorMessage, setErrorMessage] = useState<string>();
const [checkingPass, setCheckingPass] = useState<boolean>(false);
const [password, setPassword] = useState<string>("");
const [toggleCheckBox, setToggleCheckBox] = useState<boolean>(false);
const isCreatePassword = type === "create_password";
const onNextPress = async () => {
setCheckingPass(true);
if (isCreatePassword) {
// Call create password
console.log("[TODO] Implement", password);
} else {
// Call enter password
setErrorMessage("test");
useEffect(() => {
if (error) {
setCheckingPass(false);
}
}, [error]);
const onNextPress = async () => {
if (!toggleCheckBox) {
return;
}
setCheckingPass(true);
onPassword(password);
};
const onForgotPress = () => {};
const onLearnMorePress = () => {};
const onForgotPress = () => {
// console.log("[TODO] Implement", setErrorMessage);
};
return (
<Box marginHorizontal="xl">
<Box marginHorizontal="xl" mb="sm">
<ConnectWalletHeader
middleContent={
<Text variant="header">
{isCreatePassword ? "Create password" : "Enter password"}
{isCreatePassword
? l.embedded_wallet.create_password
: l.embedded_wallet.enter_password}
</Text>
}
subHeaderText={
isCreatePassword
? "Set a password for your account"
: `Enter the password for email: ${email}`
? l.embedded_wallet.set_password_message
: `${l.embedded_wallet.enter_password_for_email}: ${email}`
}
onBackPress={goBack}
onClose={close}
/>
<Box mt="sm" flexDirection="column" marginTop="xl" mb="md">
<Text variant="bodySmallBold" mt="xs">
{l.embedded_wallet.make_sure_you_save_it}
</Text>
<Box mt="sm" flexDirection="column" marginTop="xl">
<PasswordInput onChangeText={setPassword} />
<BaseButton mt="md">
<Checkbox
label="I have saved my password"
color={theme.colors.linkPrimary}
onToggle={setToggleCheckBox}
/>
</BaseButton>
{isCreatePassword ? null : (
<BaseButton
mt="sm"
@@ -76,40 +98,38 @@ export const EnterPassword = ({
</BaseButton>
)}
</Box>
{errorMessage ? (
<Text variant="error" numberOfLines={1}>
{errorMessage}
{error ? (
<Text variant="error" numberOfLines={1} mt="md">
{error}
</Text>
) : (
<Box height={20} />
)}
<Box flex={1} flexDirection="row" justifyContent="flex-end">
{isCreatePassword ? (
) : null}
{/* {isCreatePassword ? (
<BaseButton onPress={onLearnMorePress}>
<Text variant="bodySmallSecondary" color="linkPrimary">
{l.common.learn_more}
</Text>
</BaseButton>
) : null}
<BaseButton
mt="sm"
flexDirection="row"
alignItems="center"
height={theme.textVariants.bodySmallSecondary.fontSize}
onPress={onNextPress}
>
{checkingPass ? (
<ActivityIndicator
size={"small"}
color={theme.colors.linkPrimary}
/>
) : (
<Text variant="bodySmallSecondary" color="linkPrimary">
{l.common.next}
</Text>
)}
</BaseButton>
</Box>
) : null} */}
<BaseButton
mt="md"
paddingVertical="md"
borderRadius="lg"
borderWidth={1}
borderColor="border"
backgroundColor="accentButtonColor"
onPress={onNextPress}
>
{checkingPass ? (
<ActivityIndicator
size={"small"}
color={theme.colors.accentButtonTextColor}
/>
) : (
<Text variant="bodySmallBold" color="accentButtonTextColor">
{l.common.next}
</Text>
)}
</BaseButton>
</Box>
);
};
@@ -6,20 +6,14 @@ import {
} from "@thirdweb-dev/react-core";
import { EmbeddedConnectionUI } from "./EmbeddedConnectionUI";
import { EmailSelectionUI } from "./EmbeddedSelectionUI";
import { AuthProvider } from "@paperxyz/embedded-wallet-service-sdk";
type OAuthProvider = "google"; // currently we only have one
type AuthOption = "email" | "google";
export type EmbeddedWalletConfig = {
// @default true - set false to disable
email?: boolean;
oauthOptions?:
| {
providers: OAuthProvider[];
redirectUrl: string;
}
| false;
auth?: {
options: AuthOption[];
redirectUrl?: string;
};
};
export const embeddedWallet = (
@@ -28,16 +22,11 @@ export const embeddedWallet = (
const selectUI = (props: SelectUIProps<EmbeddedWallet>) => (
<EmailSelectionUI
{...props}
oauthOptions={
config?.oauthOptions
? {
providers: [AuthProvider.GOOGLE],
redirectUrl: config.oauthOptions.redirectUrl,
}
: undefined
auth={
config?.auth || {
options: ["email"],
}
}
// you cannot disable both email and oauth
email={!config?.oauthOptions && !config?.email ? true : config?.email}
/>
);
+95
View File
@@ -1,5 +1,100 @@
# @thirdweb-dev/react
## 4.1.0
### Minor Changes
- [#1846](https://github.com/thirdweb-dev/js/pull/1846) [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - New API to configure authentication options on `embeddedWallet`
```ts
// default - google sign in is enabled
embeddedWallet();
// this is same as
embeddedWallet({
auth: {
options: ["email", "google"],
},
});
// only email
embeddedWallet({
auth: {
options: ["email"],
},
});
// only google sign in
embeddedWallet({
auth: {
options: ["google"],
},
});
```
### Patch Changes
- [#1885](https://github.com/thirdweb-dev/js/pull/1885) [`961ef644`](https://github.com/thirdweb-dev/js/commit/961ef644d1b1adaad08c071903cd53aacac50bb4) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Improved `useSmartWallet()` hook
Example with metamask:
```ts
const { connect } = useSmartWallet(metamaskWallet(), {
factoryAddress: factoryAddress,
gasless: true,
});
const onClick = async () => {
// nothing to do here, all handled internally
await connect();
};
```
```ts
Example with localWallet:
const { connect } = useSmartWallet(localWallet(), {
factoryAddress: factoryAddress,
gasless: true,
});
const onClick = async () => {
// function to 'load' the local wallet before using it
await connect({
connectPersonalWallet: async (w) => {
await w.generate();
await w.connect();
}
});
}
```
```ts
Example with embeddedWallet:
const { connect } = useSmartWallet(embeddedWallet(), {
factoryAddress: factoryAddress,
gasless: true,
});
const onClick = async () => {
// function to 'auth' the embedded wallet before using it
await connect({
connectPersonalWallet: async (w) => {
const authResult = await w.authenticate({ strategy: "google" });
await w.connect({ authResult });
}
});
}
```
- [#1886](https://github.com/thirdweb-dev/js/pull/1886) [`6fcd09dd`](https://github.com/thirdweb-dev/js/commit/6fcd09ddbdad2c5e02fad9c3d2c61348ebd987de) Thanks [@edwardysun](https://github.com/edwardysun)! - Fix for user address and active wallet address out of sync
- Updated dependencies [[`a9d32f3c`](https://github.com/thirdweb-dev/js/commit/a9d32f3c90251a459e17a19eca803bbfdeeaeb79), [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a)]:
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
- @thirdweb-dev/[email protected]
## 4.0.10
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/react",
"version": "4.0.10",
"version": "4.1.0",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
+7 -12
View File
@@ -11,9 +11,9 @@ export function OTPInput(props: {
setValue: (value: string) => void;
onEnter?: () => void;
}) {
const otp = props.value.split("").map(Number);
const otp = props.value.split("");
const setOTP = (newOTP: number[]) => {
const setOTP = (newOTP: string[]) => {
props.setValue(newOTP.join(""));
};
@@ -37,18 +37,13 @@ export function OTPInput(props: {
ref={(e) => (boxEls.current[i] = e)}
key={i}
value={otp[i] ?? ""}
type="number"
pattern="[0-9]*"
type="text"
pattern="[a-zA-Z0-9]*"
variant="outline"
inputMode="numeric"
inputMode="text"
onPaste={(e) => {
const pastedData = e.clipboardData.getData("text/plain");
const newOTP = pastedData
.slice(0, props.digits)
.split("")
.filter((n) => /\d/.test(n))
.map(Number);
const newOTP = pastedData.slice(0, props.digits).split("");
setOTP(newOTP);
e.preventDefault();
}}
@@ -110,7 +105,7 @@ export function OTPInput(props: {
const newOTP = [...otp];
const index = i > inputToFocusIndex - 1 ? inputToFocusIndex : i;
newOTP[index] = Number(value);
newOTP[index] = value;
setOTP(newOTP);
}}
/>
@@ -5,6 +5,7 @@ export const Button = styled.button<{
variant: "primary" | "secondary" | "link" | "accent" | "outline";
theme?: Theme;
fullWidth?: boolean;
gap?: keyof typeof spacing;
}>`
all: unset;
cursor: pointer;
@@ -20,6 +21,7 @@ export const Button = styled.button<{
line-height: 1;
flex-shrink: 0;
transition: border 200ms ease;
gap: ${(p) => (p.gap && spacing[p.gap]) || 0};
${(p) => p.fullWidth && `width: 100%;`};
+3
View File
@@ -19,6 +19,9 @@ export const Text = styled.span<{
line-height: ${(p) => (p.multiline ? 1.5 : 1)};
${(p) => (p.center ? `text-align: center;` : "")};
text-wrap: ${(p) => (p.balance ? "balance" : "inherit")};
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
`;
export const Link = styled.a<{
@@ -0,0 +1,22 @@
import { useWalletContext } from "@thirdweb-dev/react-core";
import { EmbeddedWallet } from "@thirdweb-dev/wallets";
import { useCallback } from "react";
export const useEmbeddedWalletSendVerificationEmail = () => {
const context = useWalletContext();
const sendVerificationEmail = useCallback(
async (email: string) => {
if (!context.clientId) {
throw new Error("Please, provide a clientId in the ThirdwebProvider");
}
return EmbeddedWallet.sendVerificationEmail({
email,
clientId: context.clientId,
});
},
[context.clientId],
);
return sendVerificationEmail;
};
@@ -0,0 +1,74 @@
import {
useCreateWalletInstance,
useSetConnectedWallet,
useSetConnectionStatus,
useWalletContext,
useWallets,
} from "@thirdweb-dev/react-core";
import {
walletIds,
type AuthParams,
EmbeddedWallet,
} from "@thirdweb-dev/wallets";
import { useCallback } from "react";
import { embeddedWallet } from "../../../wallet/wallets/embeddedWallet/embeddedWallet";
/**
* Hook to authenticate and connect to an embedded wallet
* @returns a function to connect to an embedded wallet
*/
export function useEmbeddedWallet() {
const create = useCreateWalletInstance();
const setStatus = useSetConnectionStatus();
const setWallet = useSetConnectedWallet();
const context = useWalletContext();
const wallets = useWallets();
const connect = useCallback(
async (authParams: AuthParams) => {
setStatus("connecting");
const config = wallets.find((w) => w.id === walletIds.embeddedWallet);
if (
!config ||
(config.personalWallets && config.personalWallets.length > 0)
) {
console.warn(
"Embedded wallet not configured in ThirdwebProvider.supportedWallets - add it to enable autoconnect on page load",
);
}
const wallet = create(embeddedWallet());
try {
const authResult = await wallet.authenticate(authParams);
await wallet.connect({ authResult });
setWallet(wallet);
setStatus("connected");
} catch (e) {
console.error("Error connecting to embedded wallet", e);
setStatus("disconnected");
}
return wallet;
},
[setStatus, wallets, create, setWallet],
);
const sendVerificationEmail = useCallback(
async ({ email }: { email: string }) => {
const clientId = context.clientId;
if (!clientId) {
throw new Error(
"clientId not found, please add it to ThirdwebProvider",
);
}
return EmbeddedWallet.sendVerificationEmail({
email,
clientId,
});
},
[context],
);
return {
connect,
sendVerificationEmail,
};
}
@@ -1,21 +1,94 @@
import { useConnect } from "@thirdweb-dev/react-core";
import {
type WalletConfig,
WalletInstance,
useCreateWalletInstance,
useSetConnectedWallet,
useSetConnectionStatus,
useWalletContext,
useWallets,
} from "@thirdweb-dev/react-core";
import type { SmartWalletConfigOptions } from "../../../wallet/wallets/smartWallet/types";
import type { SmartWalletConnectionArgs } from "@thirdweb-dev/wallets/evm/wallets/smart-wallet";
import {
getSmartWalletAddress,
type SmartWalletConnectionArgs,
} from "@thirdweb-dev/wallets/evm/wallets/smart-wallet";
import { useCallback } from "react";
import { WalletConfig } from "@thirdweb-dev/react-core/dist/declarations/src";
import type { BytesLike } from "ethers";
export function useSmartWallet() {
const connect = useConnect();
return useCallback(
async (
wallet: WalletConfig<any>,
options: SmartWalletConnectionArgs & SmartWalletConfigOptions,
) => {
export function useSmartWallet<W extends WalletInstance>(
personalWallet: WalletConfig<W>,
options: SmartWalletConfigOptions,
) {
const create = useCreateWalletInstance();
const setStatus = useSetConnectionStatus();
const setWallet = useSetConnectedWallet();
const context = useWalletContext();
const supportedWallets = useWallets();
const predictAddress = useCallback(
async (args: { personalWalletAddress: string; data?: BytesLike }) => {
return getSmartWalletAddress(
context.activeChain,
options.factoryAddress,
args.personalWalletAddress,
args.data,
);
},
[context.activeChain, options.factoryAddress],
);
const connect = useCallback(
async (args?: {
connectPersonalWallet?: (wallet: W) => Promise<void>;
connectionArgs?: Omit<SmartWalletConnectionArgs, "personalWallet">;
}) => {
if (!supportedWallets.find((w) => w.id === personalWallet.id)) {
console.warn(
"Please, add your smart wallet to the supportedWallets prop of the ThirdwebProvider to enjoy auto-connecting to the wallet.",
);
}
const { smartWallet } = await import(
"../../../wallet/wallets/smartWallet/smartWallet"
);
return connect(smartWallet(wallet, options), options);
setStatus("connecting");
const pw = create(personalWallet);
const sw = create(smartWallet(personalWallet, options));
try {
if (args?.connectPersonalWallet) {
// if passed in, use custom function to connect personal wallet
await args.connectPersonalWallet(pw);
} else {
// otherwise default to auto-connecting personal wallet with chainId
await pw.connect({
chainId: context.activeChain.chainId,
});
}
await sw.connect({
...args?.connectionArgs,
personalWallet: pw,
});
setStatus("connected");
setWallet(sw);
} catch (e) {
console.error("Error connecting to smart wallet", e);
setStatus("disconnected");
throw e;
}
return sw;
},
[connect],
[
context.activeChain.chainId,
create,
options,
personalWallet,
setStatus,
setWallet,
supportedWallets,
],
);
return {
connect,
predictAddress,
};
}
+2
View File
@@ -33,6 +33,8 @@ export { useMetamask } from "./hooks/wallets/useMetamask";
export { useCoinbaseWallet } from "./hooks/wallets/useCoinbaseWallet";
export { useFrameWallet } from "./hooks/wallets/useFrame";
export { useBloctoWallet } from "./hooks/wallets/useBloctoWallet";
export { useEmbeddedWallet } from "./hooks/wallets/useEmbeddedWallet";
export { useEmbeddedWalletSendVerificationEmail } from "./hooks/useEmbeddedWalletSendVerificationEmail";
export {
usePaperWalletUserEmail,
@@ -6,12 +6,13 @@ import {
import { WalletUIStatesProvider } from "./wallet-ui-states-provider";
import { ConnectModal } from "../../wallet/ConnectWallet/Modal/ConnectModal";
import { ThemeObjectOrType } from "../../design-system";
import { PropsWithChildren, useMemo } from "react";
import { PropsWithChildren, useEffect, useMemo, useRef } from "react";
import type { Chain, defaultChains } from "@thirdweb-dev/chains";
import { defaultWallets } from "../../wallet/wallets/defaultWallets";
import { CustomThemeProvider } from "../../design-system/CustomThemeProvider";
import { signerWallet } from "../../wallet/wallets/signerWallet";
import { Signer } from "ethers";
import { walletIds } from "@thirdweb-dev/wallets";
interface ThirdwebProviderProps<TChains extends Chain[]>
extends Omit<
@@ -80,6 +81,29 @@ export const ThirdwebProvider = <
[signer],
);
// preload the embeddedWallet SDK if present in supportedWallets
const ewsRef = useRef(false);
useEffect(() => {
if (ewsRef.current) {
return;
}
ewsRef.current = true;
const preloadEmbeddedWallet = async () => {
const hasEmbeddedWallet = wallets.find(
(w) => w.id === walletIds.embeddedWallet,
);
if (hasEmbeddedWallet && restProps.clientId) {
// TODO only preload the iframe instead of creating the SDK
const { EmbeddedWalletSdk } = await import("@thirdweb-dev/wallets");
new EmbeddedWalletSdk({
clientId: restProps.clientId,
chain: "Ethereum",
});
}
};
preloadEmbeddedWallet();
}, [restProps.clientId, wallets]);
return (
<WalletUIStatesProvider theme={theme}>
<CustomThemeProvider theme={theme}>
@@ -169,9 +169,12 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
const { user } = useUser();
const disconnect = useDisconnect();
const shouldSignIn = !!authConfig?.authUrl && !!address && !user?.address;
const addressAndUserAddressMismatched = address !== user?.address;
const requiresSignIn = props.auth?.loginOptional
? false
: !!authConfig?.authUrl && !!address && !user?.address;
: shouldSignIn || addressAndUserAddressMismatched;
const supportedTokens = useMemo(() => {
if (!props.supportedTokens) {
@@ -192,10 +195,10 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
// if wallet gets disconnected, close the signature modal
useEffect(() => {
if (!activeWallet) {
if (!activeWallet && !addressAndUserAddressMismatched) {
setShowSignatureModal(false);
}
}, [activeWallet]);
}, [activeWallet, addressAndUserAddressMismatched]);
return (
<CustomThemeProvider theme={theme}>
@@ -12,7 +12,7 @@ import { Button } from "../../../components/buttons";
import { Theme, iconSize, spacing } from "../../../design-system";
import { GoogleIcon } from "../../ConnectWallet/icons/GoogleIcon";
import { InputSelectionUI } from "../InputSelectionUI";
import type { EmbeddedWalletLoginType } from "./types";
import type { EmbeddedWalletLoginType, AuthOption } from "./types";
import { TextDivider } from "../../../components/TextDivider";
import { openGoogleSignInWindow } from "../../utils/openGoogleSignInWindow";
import { useTheme } from "@emotion/react";
@@ -20,12 +20,16 @@ import { useTheme } from "@emotion/react";
export const EmbeddedWalletFormUI = (props: {
onSelect: (loginType: EmbeddedWalletLoginType) => void;
walletConfig: WalletConfig<EmbeddedWallet>;
authOptions: AuthOption[];
}) => {
const createWalletInstance = useCreateWalletInstance();
const setConnectionStatus = useSetConnectionStatus();
const setConnectedWallet = useSetConnectedWallet();
const themeObj = useTheme() as Theme;
const enableGoogleLogin = props.authOptions.includes("google");
const enableEmailLogin = props.authOptions.includes("email");
// Need to trigger google login on button click to avoid popup from being blocked
const googleLogin = async () => {
try {
@@ -36,14 +40,16 @@ export const EmbeddedWalletFormUI = (props: {
if (!googleWindow) {
throw new Error("Failed to open google login window");
}
await embeddedWallet.connect({
loginType: "headless_google_oauth",
const authResult = await embeddedWallet.authenticate({
strategy: "google",
openedWindow: googleWindow,
closeOpenedWindow: (openedWindow) => {
openedWindow.close();
},
});
await embeddedWallet.connect({
authResult,
});
setConnectedWallet(embeddedWallet);
} catch (e) {
setConnectionStatus("disconnected");
@@ -53,33 +59,42 @@ export const EmbeddedWalletFormUI = (props: {
return (
<div>
<SocialButton
variant="secondary"
fullWidth
onClick={() => {
googleLogin();
props.onSelect({ google: true });
}}
>
<GoogleIcon size={iconSize.md} />
Sign in with Google
</SocialButton>
<TextDivider text="OR" py="lg" />
<InputSelectionUI
onSelect={(email) => props.onSelect({ email })}
placeholder="Enter your email address"
name="email"
type="email"
errorMessage={(_input) => {
const input = _input.replace(/\+/g, "");
const emailRegex = /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,})$/g;
const isValidEmail = emailRegex.test(input);
if (!isValidEmail) {
return "Invalid email address";
}
}}
emptyErrorMessage="email address is required"
/>
{enableGoogleLogin && (
<>
<SocialButton
variant="secondary"
fullWidth
onClick={() => {
googleLogin();
props.onSelect({ google: true });
}}
>
<GoogleIcon size={iconSize.md} />
Sign in with Google
</SocialButton>
</>
)}
{enableGoogleLogin && enableEmailLogin && (
<TextDivider text="OR" py="lg" />
)}
{enableEmailLogin && (
<InputSelectionUI
onSelect={(email) => props.onSelect({ email })}
placeholder="Enter your email address"
name="email"
type="email"
errorMessage={(_input) => {
const input = _input.replace(/\+/g, "");
const emailRegex =
/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,})$/g;
const isValidEmail = emailRegex.test(input);
if (!isValidEmail) {
return "Invalid email address";
}
}}
emptyErrorMessage="email address is required"
/>
)}
</div>
);
};
@@ -89,6 +104,7 @@ export const EmbeddedWalletFormUIScreen: React.FC<{
onBack: () => void;
modalSize: "compact" | "wide";
walletConfig: WalletConfig<EmbeddedWallet>;
authOptions: AuthOption[];
}> = (props) => {
const isCompact = props.modalSize === "compact";
return (
@@ -111,6 +127,7 @@ export const EmbeddedWalletFormUIScreen: React.FC<{
p={isCompact ? undefined : "lg"}
>
<EmbeddedWalletFormUI
authOptions={props.authOptions}
walletConfig={props.walletConfig}
onSelect={props.onSelect}
/>
@@ -36,15 +36,16 @@ export const EmbeddedWalletGoogleLogin = (
if (!googleWindow) {
throw new Error("Failed to open google login window");
}
await embeddedWallet.connect({
loginType: "headless_google_oauth",
const authResult = await embeddedWallet.authenticate({
strategy: "google",
openedWindow: googleWindow,
closeOpenedWindow: (openedWindow) => {
openedWindow.close();
},
});
await embeddedWallet.connect({
authResult,
});
setConnectedWallet(embeddedWallet);
props.connected();
} catch (e) {
@@ -1,70 +1,71 @@
import styled from "@emotion/styled";
import { ConnectUIProps, useWalletContext } from "@thirdweb-dev/react-core";
import { EmbeddedWallet } from "@thirdweb-dev/wallets";
import { EmbeddedWallet, SendEmailOtpReturnType } from "@thirdweb-dev/wallets";
import { useCallback, useEffect, useRef, useState } from "react";
import { FadeIn } from "../../../components/FadeIn";
import { OTPInput } from "../../../components/OTPInput";
import { Spacer } from "../../../components/Spacer";
import { Spinner } from "../../../components/Spinner";
import { Container, Line, ModalHeader } from "../../../components/basic";
import { Button } from "../../../components/buttons";
import { Text } from "../../../components/text";
import { Theme, fontSize } from "../../../design-system";
import { Button } from "../../../components/buttons";
import { CreatePassword } from "./USER_MANAGED/CreatePassword";
import { EnterPasswordOrRecovery } from "./USER_MANAGED/EnterPassword";
type EmbeddedWalletOTPLoginUIProps = ConnectUIProps<EmbeddedWallet>;
type VerificationStatus = "verifying" | "invalid" | "valid" | "idle";
type EmailStatus = "sending" | SendEmailOtpReturnType | "error";
type ScreenToShow =
| "base"
| "create-password"
| "enter-password-or-recovery-code";
export const EmbeddedWalletOTPLoginUI: React.FC<
EmbeddedWalletOTPLoginUIProps
> = (props) => {
const email = props.selectionData;
const isWideModal = props.modalSize === "wide";
const [otpInput, setOtpInput] = useState("");
const { createWalletInstance, setConnectedWallet, setConnectionStatus } =
useWalletContext();
const [wallet, setWallet] = useState<EmbeddedWallet | null>(null);
const isWideModal = props.modalSize === "wide";
const [verifyStatus, setVerifyStatus] = useState<VerificationStatus>("idle");
const [emailStatus, setEmailStatus] = useState<EmailStatus>("sending");
const [verifyStatus, setVerifyStatus] = useState<
"verifying" | "invalid" | "valid" | "idle"
>("idle");
const [sendEmailOtpStatus, setSendEmailOtpStatus] = useState<
"sending" | "sent" | "error"
>("sending");
const [screen, setScreen] = useState<ScreenToShow>("base"); // TODO change
const sendEmail = useCallback(async () => {
setOtpInput("");
setVerifyStatus("idle");
setSendEmailOtpStatus("sending");
setEmailStatus("sending");
try {
const _wallet = createWalletInstance(props.walletConfig);
setWallet(_wallet);
const _embeddedWalletSdk = await _wallet.getEmbeddedWalletSDK();
await _embeddedWalletSdk.auth.sendEmailLoginOtp({
email: email,
});
setSendEmailOtpStatus("sent");
const status = await _wallet.sendVerificationEmail({ email });
setEmailStatus(status);
} catch (e) {
console.error(e);
setVerifyStatus("idle");
setSendEmailOtpStatus("error");
setEmailStatus("error");
}
}, [createWalletInstance, email, props.walletConfig]);
const handleSubmit = (otp: string) => {
if (sendEmailOtpStatus !== "sent" || otp.length !== 6) {
const verify = async (otp: string) => {
if (typeof emailStatus !== "object" || otp.length !== 6) {
return;
}
verifyCodes(otp);
};
const verifyCodes = async (otp: string) => {
setVerifyStatus("idle");
if (typeof emailStatus !== "object") {
return;
}
if (!wallet) {
return;
}
@@ -72,15 +73,65 @@ export const EmbeddedWalletOTPLoginUI: React.FC<
try {
setVerifyStatus("verifying");
setConnectionStatus("connecting");
await wallet.connect({
loginType: "headless_email_otp_verification",
email,
otp,
});
setConnectedWallet(wallet);
const needsRecoveryCode =
emailStatus.recoveryShareManagement === "USER_MANAGED" &&
(emailStatus.isNewUser || emailStatus.isNewDevice);
// USER_MANAGED
if (needsRecoveryCode) {
if (emailStatus.isNewUser) {
try {
// verifies otp for UI feedback
// TODO (ews) tweak the UI flow to avoid verifying otp twice - needs new endpoint or new UI
await wallet.authenticate({
strategy: "email_verification",
email,
verificationCode: otp,
});
} catch (e: any) {
if (e instanceof Error && e.message.includes("encryption key")) {
setScreen("create-password");
} else {
throw e;
}
}
} else {
try {
// verifies otp for UI feedback
await wallet.authenticate({
strategy: "email_verification",
email,
verificationCode: otp,
});
} catch (e: any) {
if (e instanceof Error && e.message.includes("encryption key")) {
setScreen("enter-password-or-recovery-code");
} else {
throw e;
}
}
}
}
// AWS_MANAGED
else {
const authResult = await wallet.authenticate({
strategy: "email_verification",
email,
verificationCode: otp,
});
if (!authResult) {
throw new Error("Failed to verify OTP");
}
await wallet.connect({
authResult,
});
setConnectedWallet(wallet);
props.connected();
}
setVerifyStatus("valid");
props.connected();
} catch (e) {
setVerifyStatus("invalid");
console.error(e);
@@ -96,115 +147,178 @@ export const EmbeddedWalletOTPLoginUI: React.FC<
}
}, [sendEmail]);
return (
<Container fullHeight flex="column" animate="fadein">
<Container p="lg">
<ModalHeader title="Sign in" onBack={props.goBack} />
</Container>
if (screen === "create-password") {
return (
<CreatePassword
modalSize={props.modalSize}
email={email}
goBack={props.goBack}
onPassword={async (password) => {
if (!wallet || typeof emailStatus !== "object") {
return;
}
const authResult = await wallet.authenticate({
strategy: "email_verification",
email,
verificationCode: otpInput,
recoveryCode: password,
});
if (!authResult) {
throw new Error("Failed to verify recovery code");
}
await wallet.connect({
authResult,
});
setConnectedWallet(wallet);
props.connected();
}}
/>
);
}
<Container expand flex="column" center="y">
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<div
style={{
textAlign: "center",
if (screen === "enter-password-or-recovery-code") {
return (
<EnterPasswordOrRecovery
modalSize={props.modalSize}
goBack={props.goBack}
email={email}
onVerify={async (passwordOrRecoveryCode) => {
if (!wallet || typeof emailStatus !== "object") {
return;
}
const authResult = await wallet.authenticate({
strategy: "email_verification",
email,
verificationCode: otpInput,
recoveryCode: passwordOrRecoveryCode,
});
if (!authResult) {
throw new Error("Failed to verify recovery code");
}
await wallet.connect({
authResult,
});
setConnectedWallet(wallet);
props.connected();
}}
/>
);
}
if (screen === "base") {
return (
<Container fullHeight flex="column" animate="fadein">
<Container p="lg">
<ModalHeader title="Sign in" onBack={props.goBack} />
</Container>
<Container expand flex="column" center="y">
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
{!isWideModal && <Spacer y="xl" />}
<Text>Enter the verification code sent to</Text>
<Spacer y="sm" />
<Text color="primaryText">{email}</Text>
<div
style={{
textAlign: "center",
}}
>
{!isWideModal && <Spacer y="xl" />}
<Text>Enter the verification code sent to</Text>
<Spacer y="sm" />
<Text color="primaryText">{email}</Text>
<Spacer y="xl" />
</div>
<OTPInput
isInvalid={verifyStatus === "invalid"}
digits={6}
value={otpInput}
setValue={(value) => {
setOtpInput(value);
setVerifyStatus("idle"); // reset error
verify(value);
}}
onEnter={() => {
verify(otpInput);
}}
/>
{verifyStatus === "invalid" && (
<FadeIn>
<Spacer y="md" />
<Text size="sm" color="danger" center>
Invalid verification code
</Text>
</FadeIn>
)}
<Spacer y="xl" />
</div>
<OTPInput
isInvalid={verifyStatus === "invalid"}
digits={6}
value={otpInput}
setValue={(value) => {
setOtpInput(value);
setVerifyStatus("idle"); // reset error
handleSubmit(value);
}}
onEnter={() => {
handleSubmit(otpInput);
}}
/>
{verifyStatus === "invalid" && (
<FadeIn>
<Spacer y="md" />
<Text size="sm" color="danger" center>
Invalid verification code
</Text>
</FadeIn>
)}
<Spacer y="xl" />
<Container px={isWideModal ? "xxl" : "lg"}>
{verifyStatus === "verifying" ? (
<>
<Container flex="row" center="x" animate="fadein">
<Spinner size="lg" color="accentText" />
<Container px={isWideModal ? "xxl" : "lg"}>
{verifyStatus === "verifying" ? (
<>
<Container flex="row" center="x" animate="fadein">
<Spinner size="lg" color="accentText" />
</Container>
</>
) : (
<Container animate="fadein" key="btn-container">
<Button
onClick={() => verify(otpInput)}
variant="accent"
type="submit"
style={{
width: "100%",
}}
>
Verify
</Button>
</Container>
</>
) : (
<Container animate="fadein" key="btn-container">
<Button
onClick={() => handleSubmit(otpInput)}
variant="accent"
type="submit"
)}
</Container>
<Spacer y="xl" />
{!isWideModal && <Line />}
<Container p={isWideModal ? undefined : "lg"}>
{emailStatus === "error" && (
<>
<Text size="sm" center color="danger">
Failed to send verification code
</Text>
</>
)}
{emailStatus === "sending" && (
<Container
flex="row"
center="both"
gap="xs"
style={{
width: "100%",
textAlign: "center",
}}
>
Verify
</Button>
</Container>
)}
</Container>
<Text size="sm">Sending verification code</Text>
<Spinner size="xs" color="secondaryText" />
</Container>
)}
<Spacer y="xl" />
{!isWideModal && <Line />}
<Container p={isWideModal ? undefined : "lg"}>
{sendEmailOtpStatus === "error" && (
<>
<Text size="sm" center color="danger">
Failed to send verification code
</Text>
</>
)}
{sendEmailOtpStatus === "sending" && (
<Container
flex="row"
center="both"
gap="xs"
style={{
textAlign: "center",
}}
>
<Text size="sm">Sending verification code</Text>
<Spinner size="xs" color="secondaryText" />
</Container>
)}
{sendEmailOtpStatus === "sent" && (
<LinkButton onClick={sendEmail} type="button">
Resend verification code
</LinkButton>
)}
</Container>
</form>
{typeof emailStatus === "object" && (
<LinkButton onClick={sendEmail} type="button">
Resend verification code
</LinkButton>
)}
</Container>
</form>
</Container>
</Container>
</Container>
);
);
}
return null;
};
const LinkButton = styled.button<{ theme?: Theme }>`
@@ -0,0 +1,245 @@
import { useState } from "react";
import { Spacer } from "../../../../components/Spacer";
import { Spinner } from "../../../../components/Spinner";
import { Container, ModalHeader, Line } from "../../../../components/basic";
import { Button, IconButton } from "../../../../components/buttons";
import { spacing, Theme, radius, iconSize } from "../../../../design-system";
import { Text } from "../../../../components/text";
import styled from "@emotion/styled";
import { useClipboard } from "../../../../evm/components/hooks/useCopyClipboard";
import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
// TODO (ews) - this is unused right now, no backup codes are generated
export function BackupAccount(props: {
recoveryCodes: string[] | undefined;
goBack: () => void;
onNext: () => void;
email: string;
modalSize: "wide" | "compact";
}) {
const [showCodes, setShowCodes] = useState(false);
const [copied, setCopied] = useState(false);
const copyContent = `${props.email}\n\n${
window.location.origin
}\n\nRecovery Codes:\n\n${props.recoveryCodes?.join("\n") || ""}`;
const { hasCopied, onCopy } = useClipboard(copyContent);
if (showCodes) {
return (
<Container fullHeight flex="column" animate="fadein">
<Container p="lg">
<ModalHeader
title="Copy Codes"
onBack={() => {
setShowCodes(false);
}}
/>
</Container>
<Line />
<Spacer y="lg" />
<Container
scrollY
px="lg"
style={
props.modalSize === "compact"
? {
maxHeight: "280px",
}
: undefined
}
>
{props.recoveryCodes ? (
<Container flex="column" gap="sm" scrollY>
{props.recoveryCodes.map((code, i) => {
return <CopyRecoveryCode key={i} code={code} />;
})}
<Spacer y="md" />
</Container>
) : (
<Container flex="row" center="x">
<Spinner size="xl" color="accentText" />
</Container>
)}
</Container>
<Line />
<Container p="lg">
<Button fullWidth variant="accent" onClick={onCopy} gap="xs">
{hasCopied ? (
<CheckIcon width={iconSize.sm} height={iconSize.sm} />
) : (
<CopyIcon width={iconSize.sm} height={iconSize.sm} />
)}
{hasCopied ? "Copied" : "Copy All"}
</Button>
</Container>
</Container>
);
}
return (
<Container fullHeight flex="column" animate="fadein">
<Container p="lg">
<ModalHeader title="Backup account" onBack={props.goBack} />
</Container>
<Line />
<Container p="lg">
<Text multiline>
Copy or download the recovery codes and keep them safe.
</Text>
<Text multiline>
<Spacer y="md" />
You will need it to recover access to your account if you forget your
password. Each code can only be used once.
</Text>
</Container>
<Container px="lg" flex="column" gap="sm" expand>
{showCodes ? (
<div>
{props.recoveryCodes ? (
<Container
flex="column"
gap="sm"
scrollY
style={{
maxHeight: "260px",
}}
>
{props.recoveryCodes.map((code, i) => {
return <CopyRecoveryCode key={i} code={code} />;
})}
</Container>
) : (
<Container flex="row" center="x">
<Spinner size="xl" color="accentText" />
</Container>
)}
</div>
) : (
<Button
variant="accent"
gap="sm"
onClick={() => {
setShowCodes(true);
}}
>
Copy Codes
</Button>
)}
<Button
fullWidth
variant="accent"
onClick={() => {
downloadRecoveryCodes(
copyContent,
`recoveryCodes-${props.email}.txt`,
);
}}
>
Download
</Button>
</Container>
<Spacer y="xl" />
<Line />
<Container p="lg" flex="column" gap="lg">
<Container flex="row" gap="sm" center="y">
<CheckboxButton
aria-checked={copied}
id="copy-checkbox"
onClick={() => {
setCopied(!copied);
}}
>
<CheckIcon
width={20}
height={20}
style={{
opacity: copied ? 1 : 0,
transform: copied ? "scale(1)" : "scale(0)",
transition: "opacity 200ms ease, transform 200ms ease",
}}
/>
</CheckboxButton>
<label
htmlFor="copy-checkbox"
style={{
cursor: "pointer",
userSelect: "none",
}}
>
<Text size="sm" color="primaryText">
I have copied the codes
</Text>
</label>
</Container>
<Button
fullWidth
disabled={!copied}
variant={copied ? "primary" : "outline"}
style={{
gap: spacing.xs,
}}
onClick={props.onNext}
>
Continue
</Button>
</Container>
</Container>
);
}
function CopyRecoveryCode(props: { code: string }) {
return (
<CopyContainer>
<Text color="primaryText" size="sm">
{props.code}
</Text>
</CopyContainer>
);
}
const CopyContainer = styled.div<{ theme?: Theme }>`
padding: ${spacing.md};
border: 1px solid ${(p) => p.theme.colors.borderColor};
border-radius: ${radius.md};
`;
const CheckboxButton = /* @__PURE__ */ styled(IconButton)<{ theme?: Theme }>`
border: 2px solid ${(p) => p.theme.colors.accentText};
color: ${(p) => p.theme.colors.accentText} !important;
padding: 0;
&[aria-checked="true"] {
background: ${(p) => p.theme.colors.accentText};
color: ${(p) => p.theme.colors.modalBg} !important;
}
`;
function downloadRecoveryCodes(codes: string, fileName: string) {
const blob = new Blob([codes], {
type: "text/plain",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.style.display = "none";
a.click();
URL.revokeObjectURL(a.href);
}
@@ -0,0 +1,197 @@
import {
EyeClosedIcon,
EyeOpenIcon,
LockOpen1Icon,
} from "@radix-ui/react-icons";
import { Spacer } from "../../../../components/Spacer";
import { Container, ModalHeader, Line } from "../../../../components/basic";
import {
Button,
IconButton,
InputButton,
} from "../../../../components/buttons";
import { Input, InputContainer } from "../../../../components/formElements";
import { Theme, iconSize } from "../../../../design-system";
import { keyframes } from "@emotion/react";
import styled from "@emotion/styled";
import { Text } from "../../../../components/text";
import { useState } from "react";
import { Spinner } from "../../../../components/Spinner";
import { CheckIcon } from "@radix-ui/react-icons";
export function CreatePassword(props: {
goBack: () => void;
email: string;
onPassword: (password: string) => Promise<void>;
modalSize: "wide" | "compact";
}) {
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [password, setPassword] = useState("");
const [error, setError] = useState(false);
const [saved, setSaved] = useState(false);
return (
<Container fullHeight flex="column" animate="fadein">
<Container p="lg">
<ModalHeader title="Create Password" onBack={props.goBack} />
</Container>
<Line />
<Container
expand
px={props.modalSize === "wide" ? "xxl" : "lg"}
flex="column"
center="y"
style={{
paddingTop: 0,
}}
>
{props.modalSize === "compact" && <Spacer y="xxl" />}
<Container flex="row" center="x" color="accentText">
<BounceContainer>
<LockOpen1Icon width={iconSize.xxl} height={iconSize.xxl} />
</BounceContainer>
</Container>
<Spacer y="xl" />
<form
onSubmit={async (e) => {
e.preventDefault();
setLoading(false);
setError(false);
if (password) {
try {
setLoading(true);
await props.onPassword(password);
} catch (err: any) {
setError(true);
console.error(err);
}
setLoading(false);
}
}}
>
<Text center multiline>
Set a password for your account. You will need this password when
connecting from a new device.
</Text>
<Spacer y="xs" />
<Text center color="primaryText">
Make sure to save it.
</Text>
<Spacer y="xl" />
<InputContainer>
<Input
variant="transparent"
required
name={"new-password"}
autoComplete="new-password"
id="new-password"
onChange={(e) => setPassword(e.target.value)}
value={password}
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
/>
<InputButton
type="button"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeClosedIcon /> : <EyeOpenIcon />}
</InputButton>
</InputContainer>
<Spacer y="lg" />
<Container flex="row" gap="sm" center="y">
<CheckboxButton
aria-checked={saved}
type="button"
id="save-checkbox"
onClick={() => {
setSaved(!saved);
}}
>
<CheckIcon
width={20}
height={20}
style={{
opacity: saved ? 1 : 0,
transform: saved ? "scale(1)" : "scale(0)",
transition: "opacity 200ms ease, transform 200ms ease",
}}
/>
</CheckboxButton>
<label
htmlFor="save-checkbox"
style={{
cursor: "pointer",
userSelect: "none",
}}
>
<Text size="sm" color="primaryText">
I have saved my password
</Text>
</label>
</Container>
<Spacer y="md" />
<Button
disabled={loading || !saved}
variant="accent"
fullWidth
style={{
minWidth: "100px",
}}
type="submit"
>
{loading ? (
<Spinner size="md" color="accentButtonText" />
) : (
"Set Password"
)}
</Button>
{error && (
<Container>
<Spacer y="lg" />
<Text size="sm" color="danger" center>
Failed to set password
</Text>
</Container>
)}
</form>
</Container>
<Spacer y="xl" />
</Container>
);
}
const CheckboxButton = /* @__PURE__ */ styled(IconButton)<{ theme?: Theme }>`
border: 2px solid ${(p) => p.theme.colors.accentText};
color: ${(p) => p.theme.colors.accentText} !important;
padding: 0;
&[aria-checked="true"] {
background: ${(p) => p.theme.colors.accentText};
color: ${(p) => p.theme.colors.modalBg} !important;
}
`;
const bounceAnimation = keyframes`
from {
transform: translateY(-3px);
}
to {
transform: translateY(3px);
}
`;
const BounceContainer = styled.div`
animation: ${bounceAnimation} 1s ease-in-out infinite alternate;
`;
@@ -0,0 +1,111 @@
import { EyeClosedIcon, EyeOpenIcon } from "@radix-ui/react-icons";
import { useState } from "react";
import { Spacer } from "../../../../components/Spacer";
import { Container, Line, ModalHeader } from "../../../../components/basic";
import { Button, InputButton } from "../../../../components/buttons";
import { InputContainer, Input } from "../../../../components/formElements";
import { Text } from "../../../../components/text";
import { Spinner } from "../../../../components/Spinner";
export function EnterPasswordOrRecovery(props: {
goBack: () => void;
onVerify: (password: string) => void;
email: string;
modalSize: "wide" | "compact";
}) {
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [password, setPassword] = useState("");
const [error, setError] = useState(false);
return (
<Container fullHeight flex="column" animate="fadein">
<Container p="lg">
<ModalHeader title="Enter Password" onBack={props.goBack} />
</Container>
<Line />
<Spacer y="xxl" />
<Container
expand
flex="column"
center="y"
p={props.modalSize === "wide" ? "xxl" : "lg"}
style={{
paddingTop: 0,
}}
>
<form
onSubmit={async (event) => {
event.preventDefault();
setLoading(true);
setError(false);
try {
await props.onVerify(password);
} catch (e) {
setError(true);
console.error(e);
}
setLoading(false);
}}
>
<Text center>Enter the password for your account</Text>
<Spacer y="sm" />
<Text center color="primaryText">
{props.email}
</Text>
<Spacer y="xl" />
<InputContainer data-error={!!error}>
<Input
variant="transparent"
required
name={"new-password"}
autoComplete="new-password"
id="new-password"
onChange={(e) => {
setPassword(e.target.value);
setError(false);
}}
value={password}
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
/>
<InputButton
type="button"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeClosedIcon /> : <EyeOpenIcon />}
</InputButton>
</InputContainer>
{error && (
<>
<Spacer y="sm" />
<Text color="danger" size="sm">
Wrong password
</Text>
</>
)}
<Spacer y="md" />
<Button fullWidth variant="accent" type="submit" disabled={loading}>
{loading ? (
<>
<Spinner size="md" color="accentButtonText" />
</>
) : (
<>Verify</>
)}
</Button>
</form>
</Container>
<Spacer y="sm" />
</Container>
);
}
@@ -17,11 +17,25 @@ import {
} from "./EmbeddedWalletFormUI";
import { EmbeddedWalletGoogleLogin } from "./EmbeddedWalletGoogleLogin";
import { EmbeddedWalletOTPLoginUI } from "./EmbeddedWalletOTPLoginUI";
import { EmbeddedWalletConfig } from "./types";
import { EmbeddedWalletConfig, AuthOption } from "./types";
const DEFAULT_AUTH_OPTIONS: AuthOption[] = ["email", "google"];
export const embeddedWallet = (
config?: EmbeddedWalletConfig,
_config?: EmbeddedWalletConfig,
): WalletConfig<EmbeddedWallet> => {
const defaultConfig: EmbeddedWalletConfig = {
auth: {
options: DEFAULT_AUTH_OPTIONS,
},
};
const config: EmbeddedWalletConfig = _config
? { ...defaultConfig, ..._config }
: defaultConfig;
const { auth } = config;
return {
category: "socialLogin",
isHeadless: true,
@@ -40,17 +54,29 @@ export const embeddedWallet = (
});
},
selectUI(props) {
return <EmbeddedWalletSelectionUI {...props} />;
return (
<EmbeddedWalletSelectionUI
{...props}
authOptions={auth ? auth?.options : DEFAULT_AUTH_OPTIONS}
/>
);
},
connectUI(props) {
return <EmbeddedWalletConnectUI {...props} />;
return (
<EmbeddedWalletConnectUI
{...props}
authOptions={auth ? auth?.options : DEFAULT_AUTH_OPTIONS}
/>
);
},
};
};
const EmbeddedWalletSelectionUI: React.FC<SelectUIProps<EmbeddedWallet>> = (
props,
) => {
const EmbeddedWalletSelectionUI: React.FC<
SelectUIProps<EmbeddedWallet> & {
authOptions: AuthOption[];
}
> = (props) => {
const screen = useScreenContext();
// show the icon + text if
@@ -75,12 +101,17 @@ const EmbeddedWalletSelectionUI: React.FC<SelectUIProps<EmbeddedWallet>> = (
<EmbeddedWalletFormUI
onSelect={props.onSelect}
walletConfig={props.walletConfig}
authOptions={props.authOptions}
/>
</div>
);
};
const EmbeddedWalletConnectUI = (props: ConnectUIProps<EmbeddedWallet>) => {
const EmbeddedWalletConnectUI = (
props: ConnectUIProps<EmbeddedWallet> & {
authOptions: AuthOption[];
},
) => {
const [loginType, setLoginType] = useState<PaperLoginType | undefined>(
props.selectionData as PaperLoginType,
);
@@ -109,9 +140,11 @@ const EmbeddedWalletConnectUI = (props: ConnectUIProps<EmbeddedWallet>) => {
}
// google
else {
else if (props.authOptions?.includes("google")) {
return <EmbeddedWalletGoogleLogin {...props} goBack={handleBack} />;
}
return null;
}
return (
@@ -122,6 +155,7 @@ const EmbeddedWalletConnectUI = (props: ConnectUIProps<EmbeddedWallet>) => {
}}
walletConfig={props.walletConfig}
onBack={props.goBack}
authOptions={props.authOptions}
/>
);
};
@@ -1,5 +1,7 @@
import type { EmbeddedWalletAdditionalOptions } from "@thirdweb-dev/wallets";
export type AuthOption = "google" | "email";
export type EmbeddedWalletConfig = Omit<
EmbeddedWalletAdditionalOptions,
"chain" | "clientId"
@@ -8,6 +10,14 @@ export type EmbeddedWalletConfig = Omit<
* If true, the wallet will be tagged as "reccomended" in ConnectWallet Modal
*/
recommended?: boolean;
/**
* Choose which auth providers to show in the wallet connection UI
* @default { providers: ["email", "google"] }
*/
auth?: {
options: AuthOption[];
};
};
export type EmbeddedWalletLoginType = { email: string } | { google: true };
@@ -8,7 +8,12 @@ import {
import { useState } from "react";
import { PaperFormUI, PaperFormUIScreen } from "./PaperFormUI";
import { PaperOTPLoginUI } from "./PaperOTPLoginUI";
import { PaperConfig, PaperLoginType, RecoveryShareManagement } from "./types";
import {
OAuthProvider,
PaperConfig,
PaperLoginType,
RecoveryShareManagement,
} from "./types";
import { WalletEntryButton } from "../../ConnectWallet/WalletSelector";
import { reservedScreens } from "../../ConnectWallet/constants";
import { useScreenContext } from "../../ConnectWallet/Modal/screen";
@@ -16,10 +21,22 @@ import { PaperGoogleLogin } from "./PaperGoogleLogin";
import { emailIcon } from "../../ConnectWallet/icons/dataUris";
export const paperWallet = (
config?: PaperConfig,
_config?: PaperConfig,
): WalletConfig<PaperWallet> => {
const defaultRecovery = "AWS_MANAGED";
const defaultConfig: PaperConfig = {
oauthOptions: {
providers: ["google"],
},
};
const config: PaperConfig = _config
? { ...defaultConfig, ..._config }
: defaultConfig;
const { oauthOptions } = config;
return {
category: "socialLogin",
isHeadless: true,
@@ -47,6 +64,7 @@ export const paperWallet = (
recoveryShareManagement={
config?.advancedOptions?.recoveryShareManagement || defaultRecovery
}
providers={oauthOptions ? oauthOptions?.providers : undefined}
/>
);
},
@@ -57,6 +75,7 @@ export const paperWallet = (
recoveryShareManagement={
config?.advancedOptions?.recoveryShareManagement || defaultRecovery
}
providers={oauthOptions ? oauthOptions?.providers : undefined}
/>
);
},
@@ -66,6 +85,7 @@ export const paperWallet = (
const PaperSelectionUI: React.FC<
SelectUIProps<PaperWallet> & {
recoveryShareManagement: RecoveryShareManagement;
providers?: OAuthProvider[];
}
> = (props) => {
const screen = useScreenContext();
@@ -91,7 +111,10 @@ const PaperSelectionUI: React.FC<
<div>
<PaperFormUI
walletConfig={props.walletConfig}
googleLoginSupported={props.recoveryShareManagement !== "USER_MANAGED"}
googleLoginSupported={
props.recoveryShareManagement !== "USER_MANAGED" &&
!!props.providers?.includes("google")
}
onSelect={props.onSelect}
/>
</div>
@@ -101,6 +124,7 @@ const PaperSelectionUI: React.FC<
const PaperConnectUI = (
props: ConnectUIProps<PaperWallet> & {
recoveryShareManagement: RecoveryShareManagement;
providers?: OAuthProvider[];
},
) => {
const [loginType, setLoginType] = useState<PaperLoginType | undefined>(
@@ -140,7 +164,10 @@ const PaperConnectUI = (
return (
<PaperFormUIScreen
walletConfig={props.walletConfig}
googleLoginSupported={props.recoveryShareManagement !== "USER_MANAGED"}
googleLoginSupported={
props.recoveryShareManagement !== "USER_MANAGED" &&
!!props.providers?.includes("google")
}
modalSize={props.modalSize}
onSelect={(_loginType) => {
setLoginType(_loginType);
@@ -1,5 +1,7 @@
import { PaperWalletAdditionalOptions } from "@thirdweb-dev/wallets";
export type OAuthProvider = "google";
export type PaperConfig = Omit<
PaperWalletAdditionalOptions,
"chain" | "clientId"
@@ -8,6 +10,16 @@ export type PaperConfig = Omit<
* If true, the wallet will be tagged as "recomended" in ConnectWallet Modal
*/
recommended?: boolean;
/**
* Enable or Disable OAuth logins and specify which providers to use for OAuth
* @default { providers: ["google"] }
*/
oauthOptions?:
| {
providers: OAuthProvider[];
}
| false;
};
export type RecoveryShareManagement = Exclude<
@@ -88,6 +88,10 @@ export const SmartWalletConnecting: React.FC<{
minHeight: "300px",
}}
>
<Text color="primaryText" multiline center>
Connecting to Smart Wallet
</Text>
<Spacer y="lg" />
<Spinner color="accentText" size="xl" />
</Container>
);
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/sdk
## 4.0.10
### Patch Changes
- [#1871](https://github.com/thirdweb-dev/js/pull/1871) [`a9d32f3c`](https://github.com/thirdweb-dev/js/commit/a9d32f3c90251a459e17a19eca803bbfdeeaeb79) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Reduce bundle size by lazy loading some large modules
## 4.0.9
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/sdk",
"version": "4.0.9",
"version": "4.0.10",
"description": "The main thirdweb SDK.",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/sdk",
"license": "Apache-2.0",
+10 -8
View File
@@ -9,7 +9,7 @@ import {
PREBUILT_CONTRACTS_MAP,
getContractTypeForRemoteName,
} from "../contracts";
import { SmartContract } from "../contracts/smart-contract";
import type { SmartContract as SmartContractType } from "../contracts/smart-contract";
import { getSignerAndProvider } from "../constants/urls";
import { Abi, AbiSchema } from "../schema/contracts/custom";
import { AddressOrEns } from "../schema/shared/AddressOrEnsSchema";
@@ -519,8 +519,8 @@ export class ThirdwebSDK extends RPCConnectionHandler {
address: TContractAddress,
): Promise<
TContractAddress extends ContractAddress
? SmartContract<BaseContractForAddress<TContractAddress>>
: SmartContract<BaseContract>
? SmartContractType<BaseContractForAddress<TContractAddress>>
: SmartContractType<BaseContract>
>;
/**
* Get an instance of a Custom ThirdwebContract
@@ -539,7 +539,7 @@ export class ThirdwebSDK extends RPCConnectionHandler {
): Promise<
TContractType extends PrebuiltContractType
? ContractForPrebuiltContractType<TContractType>
: SmartContract
: SmartContractType
>;
/**
* Get an instance of a Custom ThirdwebContract
@@ -555,7 +555,7 @@ export class ThirdwebSDK extends RPCConnectionHandler {
public async getContract(
address: AddressOrEns,
abi: ContractInterface,
): Promise<SmartContract>;
): Promise<SmartContractType>;
public async getContract(
address: AddressOrEns,
contractTypeOrABI?: PrebuiltContractType | ContractInterface,
@@ -829,10 +829,13 @@ export class ThirdwebSDK extends RPCConnectionHandler {
address: AddressOrEns,
abi: ContractInterface,
) {
const resolvedAddress = await resolveAddress(address);
const [resolvedAddress, { SmartContract }] = await Promise.all([
resolveAddress(address),
import("../contracts/smart-contract"),
]);
if (this.contractCache.has(resolvedAddress)) {
return this.contractCache.get(resolvedAddress) as SmartContract;
return this.contractCache.get(resolvedAddress) as SmartContractType;
}
const [, provider] = getSignerAndProvider(
this.getSignerOrProvider(),
@@ -840,7 +843,6 @@ export class ThirdwebSDK extends RPCConnectionHandler {
);
const parsedABI = typeof abi === "string" ? JSON.parse(abi) : abi;
// TODO we still might want to lazy-fy this
const contract = new SmartContract(
this.getSignerOrProvider(),
resolvedAddress,
@@ -1,7 +1,6 @@
import { resolveAddress } from "../common/ens/resolveAddress";
import { PREBUILT_CONTRACTS_MAP } from "../contracts";
import { SmartContract } from "../contracts/smart-contract";
import { ContractPublisher } from "../core/classes/contract-publisher";
import type { SmartContract } from "../contracts/smart-contract";
import { NetworkInput } from "../core/types";
import {
ContractForPrebuiltContractType,
@@ -41,10 +40,12 @@ export async function getContract<TContractType extends PrebuiltContractType>(
params.network,
params.sdkOptions,
);
const [resolvedAddress, { chainId }] = await Promise.all([
resolveAddress(params.address),
provider.getNetwork(),
]);
const [resolvedAddress, { chainId }, { ContractPublisher }] =
await Promise.all([
resolveAddress(params.address),
provider.getNetwork(),
import("../core/classes/contract-publisher"),
]);
if (inContractCache(resolvedAddress, chainId)) {
return getCachedContract(
@@ -1,6 +1,6 @@
import { resolveAddress } from "../common/ens/resolveAddress";
import { getCompositeABI } from "../common/plugin/getCompositePluginABI";
import { SmartContract } from "../contracts/smart-contract";
import { SmartContract as SmartContractType } from "../contracts/smart-contract";
import { NetworkInput } from "../core/types";
import { AddressOrEns } from "../schema/shared/AddressOrEnsSchema";
import { AbiSchema } from "../schema/contracts/custom";
@@ -25,18 +25,19 @@ export type GetContractFromAbiParams = {
export async function getContractFromAbi(
params: GetContractFromAbiParams,
): Promise<SmartContract> {
): Promise<SmartContractType> {
const [signer, provider] = getSignerAndProvider(
params.network,
params.sdkOptions,
);
const [resolvedAddress, { chainId }] = await Promise.all([
const [resolvedAddress, { chainId }, { SmartContract }] = await Promise.all([
resolveAddress(params.address),
provider.getNetwork(),
import("../contracts/smart-contract"),
]);
if (inContractCache(resolvedAddress, chainId)) {
return getCachedContract(resolvedAddress, chainId) as SmartContract;
return getCachedContract(resolvedAddress, chainId) as SmartContractType;
}
const parsedAbi =
+9
View File
@@ -1,5 +1,14 @@
# @thirdweb-dev/unity-js-bridge
## 0.2.97
### Patch Changes
- Updated dependencies [[`a9d32f3c`](https://github.com/thirdweb-dev/js/commit/a9d32f3c90251a459e17a19eca803bbfdeeaeb79), [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a)]:
- @thirdweb-dev/sdk@4.0.10
- @thirdweb-dev/wallets@2.1.0
- @thirdweb-dev/auth@4.1.3
## 0.2.96
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/unity-js-bridge",
"version": "0.2.96",
"version": "0.2.97",
"main": "dist/thirdweb-unity-bridge.js",
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/unity-js-bridge",
"license": "Apache-2.0",
+36 -51
View File
@@ -14,10 +14,8 @@ import { LocalWallet } from "@thirdweb-dev/wallets/evm/wallets/local-wallet";
import { EthersWallet } from "@thirdweb-dev/wallets/evm/wallets/ethers";
import { InjectedWallet } from "@thirdweb-dev/wallets/evm/wallets/injected";
import { MetaMaskWallet } from "@thirdweb-dev/wallets/evm/wallets/metamask";
import { MagicLink } from "@thirdweb-dev/wallets/evm/wallets/magic";
import { SmartWallet } from "@thirdweb-dev/wallets/evm/wallets/smart-wallet";
import { WalletConnect } from "@thirdweb-dev/wallets/evm/wallets/wallet-connect";
import { PaperWallet } from "@thirdweb-dev/wallets/evm/wallets/paper-wallet";
import { EmbeddedWallet } from "@thirdweb-dev/wallets/evm/wallets/embedded-wallet";
import { BigNumber } from "ethers";
import {
@@ -55,10 +53,8 @@ const WALLETS = [
MetaMaskWallet,
InjectedWallet,
WalletConnect,
PaperWallet,
CoinbaseWallet,
LocalWallet,
MagicLink,
SmartWallet,
EmbeddedWallet,
] as const;
@@ -77,7 +73,7 @@ interface TWBridge {
password?: string,
email?: string,
personalWallet?: PossibleWallet,
useGoogle?: string,
authOptions?: string,
) => Promise<string>;
disconnect: () => Promise<void>;
switchNetwork: (chainId: string) => Promise<void>;
@@ -211,27 +207,6 @@ class ThirdwebBridge implements TWBridge {
clientId: sdkOptions.clientId,
});
break;
case walletIds.magicLink:
walletInstance = new MagicLink({
dappMetadata,
apiKey: sdkOptions.wallet?.magicLinkApiKey,
emailLogin: true,
chains: supportedChains,
clientId: sdkOptions.clientId,
});
break;
case walletIds.paper:
walletInstance = new PaperWallet({
paperClientId: sdkOptions.wallet?.paperClientId ?? "uninitialized",
chain: Ethereum,
dappMetadata,
chains: supportedChains,
clientId: sdkOptions.clientId,
advancedOptions: {
recoveryShareManagement: "USER_MANAGED",
},
});
break;
case walletIds.smartWallet:
const config: SmartWalletConfig = {
chain: chain,
@@ -276,7 +251,7 @@ class ThirdwebBridge implements TWBridge {
password?: string,
email?: string,
personalWallet: PossibleWallet = "localWallet",
useGoogle?: string,
authOptions?: string,
) {
if (!this.activeSDK) {
throw new Error("SDK not initialized");
@@ -290,44 +265,54 @@ class ThirdwebBridge implements TWBridge {
walletInstance.connect({ chainId: chainIdNumber });
}
if (walletInstance.walletId === walletIds.magicLink) {
const magicLinkWallet = walletInstance as MagicLink;
if (!email) {
throw new Error("Email is required for Magic Link Wallet");
}
await magicLinkWallet.connect({ chainId: chainIdNumber, email: email });
} else if (walletInstance.walletId === walletIds.embeddedWallet) {
if (walletInstance.walletId === walletIds.embeddedWallet) {
const embeddedWallet = walletInstance as EmbeddedWallet;
if (useGoogle?.toLowerCase() === "true") {
const authOptionsParsed = JSON.parse(authOptions || "{}");
if (authOptionsParsed.authProvider === 0) {
// DefaultManaged
if (!email) {
throw new Error("Email is required for EmbeddedWallet");
}
const authResult = await embeddedWallet.authenticate({
strategy: "iframe_email_verification",
email,
});
await embeddedWallet.connect({
chainId: chainIdNumber,
authResult,
});
} else if (authOptionsParsed.authProvider === 1) {
// GoogleManaged
const googleWindow = this.openGoogleSignInWindow();
if (!googleWindow) {
throw new Error("Failed to open google login window");
}
await embeddedWallet.connect({
chainId: chainIdNumber,
loginType: "headless_google_oauth",
const authResult = await embeddedWallet.authenticate({
strategy: "google",
openedWindow: googleWindow,
closeOpenedWindow: (openedWindow) => {
openedWindow.close();
},
});
} else {
if (!email) {
throw new Error("Email is required for EmbeddedWallet");
}
await embeddedWallet.connect({
chainId: chainIdNumber,
email: email,
loginType: "ui_email_otp",
authResult,
});
} else if (authOptionsParsed.authProvider === 2) {
// CustomAuth
const authResult = await embeddedWallet.authenticate({
strategy: "jwt",
jwt: authOptionsParsed.authToken,
});
await embeddedWallet.connect({
chainId: chainIdNumber,
authResult,
});
} else {
throw new Error(
"Invalid auth provider: " + authOptionsParsed.authProvider,
);
}
} else if (walletInstance.walletId === walletIds.paper) {
const paperWallet = walletInstance as PaperWallet;
if (!email) {
throw new Error("Email is required for Paper Wallet");
}
await paperWallet.connect({ chainId: chainIdNumber, email: email });
} else if (walletInstance.walletId === walletIds.smartWallet) {
const smartWallet = walletInstance as SmartWallet;
const eoaWallet = this.walletMap.get(personalWallet);
+77
View File
@@ -1,5 +1,82 @@
# @thirdweb-dev/wallets
## 2.1.0
### Minor Changes
- [#1846](https://github.com/thirdweb-dev/js/pull/1846) [`0acc530f`](https://github.com/thirdweb-dev/js/commit/0acc530f8bbee59672d9705724edc278bd853d9a) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - # New Embedded Wallet API:
### Headless Email authentication
```ts
// send verification email
await embeddedWallet.sendVerificationEmail({
email,
});
// verify email
const authResult = embeddedWallet.authenticate({
strategy: "email_verification",
verificationCode: code,
});
// connect
const walletAddress = embeddedWallet.connect({
authResult,
});
```
### Google sign in authentication
```ts
// prompt google sign in
const authResult = embeddedWallet.authenticate({
strategy: "google",
});
// connect
const walletAddress = embeddedWallet.connect({
authResult,
});
```
### iframe based authentication
```ts
// open iframe to sign in
const authResult = embeddedWallet.authenticate({
strategy: "iframe",
});
// connect
const walletAddress = embeddedWallet.connect({
authResult,
});
```
### Custom Auth (JWT)
```ts
// authenticate with any JWT system
const jwt = await yourLogin();
// verify JWT
const authResult = embeddedWallet.authenticate({
strategy: "jwt",
jwt,
});
// connect
const walletAddress = embeddedWallet.connect({
authResult,
});
```
### Patch Changes
- Updated dependencies [[`a9d32f3c`](https://github.com/thirdweb-dev/js/commit/a9d32f3c90251a459e17a19eca803bbfdeeaeb79)]:
- @thirdweb-dev/[email protected]
## 2.0.10
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wallets",
"version": "2.0.10",
"version": "2.1.0",
"main": "dist/thirdweb-dev-wallets.cjs.js",
"module": "dist/thirdweb-dev-wallets.esm.js",
"types": "dist/thirdweb-dev-wallets.cjs.d.ts",
@@ -4,7 +4,10 @@ export const EMBEDDED_WALLET_PATH = "/sdk/2022-08-12/embedded-wallet";
export const HEADLESS_GOOGLE_OAUTH_ROUTE = `/auth/headless-google-login-managed`;
export const GET_IFRAME_BASE_URL = () =>
`${getPaperOriginUrl().replace("withpaper.com", "ews.thirdweb.com")}`;
`${getPaperOriginUrl().replace(
"withpaper.com",
"embedded-wallet.thirdweb.com",
)}`;
export const WALLET_USER_DETAILS_LOCAL_STORAGE_NAME = (clientId: string) =>
`thirdwebEwsWalletUserDetails-${clientId}`;
@@ -11,8 +11,9 @@ export type {
AuthLoginReturnType,
AuthStoredTokenWithCookieReturnType,
StoredTokenType,
GetHeadlessLoginLinkReturnType,
} from "./interfaces/auth";
export { UserStatus } from "./interfaces/embedded-wallets/embedded-wallets";
export { UserWalletStatus } from "./interfaces/embedded-wallets/embedded-wallets";
export type {
AuthDetails,
EmbeddedWalletConstructorType,
@@ -6,11 +6,14 @@ import type {
export enum RecoveryShareManagement {
USER_MANAGED = "USER_MANAGED",
AWS_MANAGED = "AWS_MANAGED",
CLOUD_MANAGED = "AWS_MANAGED",
}
export enum AuthProvider {
COGNITO = "Cognito",
GOOGLE = "Google",
EMAIL_OTP = "EmailOtp",
CUSTOM_JWT = "CustomJWT",
}
export type GetHeadlessLoginLinkReturnType = {
@@ -4,7 +4,7 @@ import type {
} from "@paperxyz/sdk-common-utilities";
import type { EmbeddedWallet } from "../../lib/core/embedded-wallet";
import type { EmbeddedWalletIframeCommunicator } from "../../utils/iFrameCommunication/EmbeddedWalletIframeCommunicator";
import { RecoveryShareManagement } from "../auth";
import { AuthAndWalletRpcReturnType, RecoveryShareManagement } from "../auth";
// Class constructor types
// types for class constructors still a little messy right now.
@@ -12,6 +12,7 @@ import { RecoveryShareManagement } from "../auth";
export type ClientIdConstructorType = { clientId: string };
export type EmbeddedWalletConstructorType = ClientIdConstructorType & {
chain: Chain;
onAuthSuccess?: (authResult: AuthAndWalletRpcReturnType) => void;
styles?: CustomizationOptionsType;
};
export type ClientIdWithQuerierType = ClientIdConstructorType & {
@@ -25,21 +26,19 @@ export type ClientIdWithQuerierAndChainType = ClientIdWithQuerierType & {
export type AuthDetails = {
email?: string;
userWalletId: string;
recoveryCode?: string;
encryptionKey?: string;
backupRecoveryCodes?: string[];
recoveryShareManagement: RecoveryShareManagement;
};
export type InitializedUser = {
status: UserStatus.LOGGED_IN_WALLET_INITIALIZED;
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED;
wallet: EmbeddedWallet;
walletAddress: string;
authDetails: AuthDetails;
};
// Embedded Wallet Types
export enum UserStatus {
LOGGED_OUT = "Logged Out",
LOGGED_IN_WALLET_INITIALIZED = "Logged In, Wallet Initialized",
}
export enum UserWalletStatus {
LOGGED_OUT = "Logged Out",
LOGGED_IN_WALLET_UNINITIALIZED = "Logged In, Wallet Uninitialized",
@@ -85,27 +84,17 @@ export type GetUserWalletStatusRpcReturnType =
};
// this is the return type from the EmbeddedWallet Class getUserWalletStatus method
export type GetUserWalletStatusFnReturnType =
export type GetUser =
| {
status: UserWalletStatus.LOGGED_OUT;
user: undefined;
}
| {
status: UserWalletStatus.LOGGED_IN_WALLET_UNINITIALIZED;
user: { authDetails: AuthDetails };
authDetails: AuthDetails;
}
| {
status: UserWalletStatus.LOGGED_IN_NEW_DEVICE;
user: { authDetails: AuthDetails; walletAddress: string };
}
| {
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED;
user: Omit<InitializedUser, "status">;
};
// This is returned from the getUser method in PaperEmbeddedWalletSdk
export type GetUser =
| {
status: UserStatus.LOGGED_OUT;
authDetails: AuthDetails;
walletAddress: string;
}
| InitializedUser;
@@ -8,12 +8,14 @@ import type {
} from "../../interfaces/embedded-wallets/embedded-wallets";
import type { EmbeddedWalletIframeCommunicator } from "../../utils/iFrameCommunication/EmbeddedWalletIframeCommunicator";
type LoginQuerierTypes = {
export type LoginQuerierTypes = {
loginWithCustomJwt: { jwt: string; encryptionKey?: string };
loginWithThirdwebModal: undefined | { email: string };
sendThirdwebEmailLoginOtp: { email: string };
verifyThirdwebEmailLoginOtp: {
email: string;
otp: string;
recoveryCode?: string;
};
injectDeveloperClientId: void;
getHeadlessGoogleLoginLink: void;
@@ -26,6 +28,7 @@ export abstract class AbstractLogin<
EMAIL_VERIFICATION extends { email: string; otp: string } = {
email: string;
otp: string;
recoveryCode?: string;
},
> {
protected LoginQuerier: EmbeddedWalletIframeCommunicator<LoginQuerierTypes>;
@@ -56,6 +59,10 @@ export abstract class AbstractLogin<
this.clientId = clientId;
}
abstract loginWithCustomJwt(args: {
jwt: string;
encryptionKey: string;
}): Promise<AuthLoginReturnType>;
abstract loginWithModal(args?: MODAL): Promise<AuthLoginReturnType>;
abstract loginWithEmailOtp(args: EMAIL_MODAL): Promise<AuthLoginReturnType>;
abstract loginWithGoogle(args?: {
@@ -4,14 +4,12 @@ import type {
AuthLoginReturnType,
GetHeadlessLoginLinkReturnType,
} from "../../interfaces/auth";
import { AbstractLogin } from "./abstract-login";
import { AbstractLogin, LoginQuerierTypes } from "./abstract-login";
export class BaseLogin extends AbstractLogin<
{
getRecoveryCode: (userWalletId: string) => Promise<string | undefined>;
},
void,
{ email: string },
{ email: string; otp: string }
{ email: string; otp: string; recoveryCode?: string }
> {
private async getGoogleLoginUrl(): Promise<GetHeadlessLoginLinkReturnType> {
const result = await this.LoginQuerier.call<GetHeadlessLoginLinkReturnType>(
@@ -32,6 +30,7 @@ export class BaseLogin extends AbstractLogin<
});
return this.postLogin(result);
}
override async loginWithEmailOtp({
email,
}: {
@@ -70,7 +69,6 @@ export class BaseLogin extends AbstractLogin<
openedWindow?: Window | null;
closeOpenedWindow?: (openedWindow: Window) => void;
}): Promise<AuthLoginReturnType> {
await this.preLogin();
let win = args?.openedWindow;
let isWindowOpenedByFn = false;
if (!win) {
@@ -80,10 +78,12 @@ export class BaseLogin extends AbstractLogin<
if (!win) {
throw new Error("Something went wrong opening pop-up");
}
await this.preLogin();
// logout the user
// fetch the url to open the login window from iframe
const { loginLink } = await this.getGoogleLoginUrl();
const [{ loginLink }] = await Promise.all([
this.getGoogleLoginUrl(),
this.preLogin(),
]);
win.location.href = loginLink;
// listen to result from the login window
@@ -163,16 +163,25 @@ export class BaseLogin extends AbstractLogin<
});
}
override async loginWithCustomJwt({
encryptionKey,
jwt,
}: LoginQuerierTypes["loginWithCustomJwt"]): Promise<AuthLoginReturnType> {
const result = await this.LoginQuerier.call<AuthAndWalletRpcReturnType>({
procedureName: "loginWithCustomJwt",
params: { encryptionKey, jwt },
});
return this.postLogin(result);
}
override async verifyEmailLoginOtp({
email,
otp,
}: {
email: string;
otp: string;
}): Promise<AuthLoginReturnType> {
recoveryCode,
}: LoginQuerierTypes["verifyThirdwebEmailLoginOtp"]): Promise<AuthLoginReturnType> {
const result = await this.LoginQuerier.call<AuthAndWalletRpcReturnType>({
procedureName: "verifyThirdwebEmailLoginOtp",
params: { email, otp },
params: { email, otp, recoveryCode },
});
return this.postLogin(result);
}
@@ -129,6 +129,12 @@ export class Auth {
return this.BaseLogin.loginWithEmailOtp(args);
}
async loginWithCustomJwt(
args: Parameters<BaseLogin["loginWithCustomJwt"]>[0],
): Promise<AuthLoginReturnType> {
return this.BaseLogin.loginWithCustomJwt(args);
}
async loginWithGoogle(
args?: Parameters<BaseLogin["loginWithGoogle"]>[0],
): Promise<AuthLoginReturnType> {
@@ -2,7 +2,7 @@ import type { Chain } from "@paperxyz/sdk-common-utilities";
import { ChainToPublicRpc } from "@paperxyz/sdk-common-utilities";
import type {
ClientIdWithQuerierAndChainType,
GetUserWalletStatusFnReturnType,
GetUser,
GetUserWalletStatusRpcReturnType,
SetUpWalletRpcReturnType,
WalletAddressObjectType,
@@ -105,7 +105,7 @@ export class EmbeddedWallet {
*}
* @returns {GetUserWalletStatusFnReturnType} an object to containing various information on the user statuses
*/
async getUserWalletStatus(): Promise<GetUserWalletStatusFnReturnType> {
async getUserWalletStatus(): Promise<GetUser> {
const userStatus =
await this.walletManagerQuerier.call<GetUserWalletStatusRpcReturnType>({
procedureName: "getUserStatus",
@@ -114,10 +114,25 @@ export class EmbeddedWallet {
if (userStatus.status === UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED) {
return {
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED,
user: { ...userStatus.user, wallet: this },
...userStatus.user,
wallet: this,
};
} else if (userStatus.status === UserWalletStatus.LOGGED_IN_NEW_DEVICE) {
return {
status: UserWalletStatus.LOGGED_IN_WALLET_UNINITIALIZED,
...userStatus.user,
};
} else if (
userStatus.status === UserWalletStatus.LOGGED_IN_WALLET_UNINITIALIZED
) {
return {
status: UserWalletStatus.LOGGED_IN_WALLET_UNINITIALIZED,
...userStatus.user,
};
} else {
// Logged out
return { status: userStatus.status };
}
return userStatus;
}
/**
@@ -1,10 +1,7 @@
import type {
EmbeddedWalletConstructorType,
GetUser,
} from "../interfaces/embedded-wallets/embedded-wallets";
import {
UserStatus,
UserWalletStatus,
type EmbeddedWalletConstructorType,
type GetUser,
} from "../interfaces/embedded-wallets/embedded-wallets";
import { EmbeddedWalletIframeCommunicator } from "../utils/iFrameCommunication/EmbeddedWalletIframeCommunicator";
import type { AuthQuerierTypes } from "./auth";
@@ -36,7 +33,12 @@ export class EmbeddedWalletSdk {
* @param {Chain} initParams.chain sets the default chain that the EmbeddedWallet will live on.
* @param {CustomizationOptionsType} initParams.styles sets the default style override for any modal that pops up asking for user's details when creating wallet or logging in.
*/
constructor({ clientId, chain, styles }: EmbeddedWalletConstructorType) {
constructor({
clientId,
chain,
styles,
onAuthSuccess,
}: EmbeddedWalletConstructorType) {
if (this.isClientIdLegacyPaper(clientId)) {
throw new Error(
"You are using a legacy clientId. Please use the clientId found on the thirdweb dashboard settings page",
@@ -57,6 +59,7 @@ export class EmbeddedWalletSdk {
clientId,
querier: this.querier,
onAuthSuccess: async (authResult) => {
onAuthSuccess?.(authResult);
await this.wallet.postWalletSetUp({
...authResult.walletDetails,
walletUserId: authResult.storedToken.authDetails.userWalletId,
@@ -72,7 +75,7 @@ export class EmbeddedWalletSdk {
});
return {
user: {
status: UserStatus.LOGGED_IN_WALLET_INITIALIZED,
status: UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED,
authDetails: authResult.storedToken.authDetails,
wallet: this.wallet,
walletAddress: authResult.walletDetails.walletAddress,
@@ -87,11 +90,11 @@ export class EmbeddedWalletSdk {
* @example
* const user = await thirdwebEmbeddedWallet.getUser();
* switch (user.status) {
* case UserStatus.LOGGED_OUT: {
* case UserWalletStatus.LOGGED_OUT: {
* // User is logged out, call one of the auth methods on thirdwebEmbeddedWallet.auth to authenticate the user
* break;
* }
* case UserStatus.LOGGED_IN_WALLET_INITIALIZED: {
* case UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED: {
* // user is logged in and wallet is all set up.
* // You have access to:
* user.status;
@@ -104,24 +107,6 @@ export class EmbeddedWalletSdk {
* @returns {GetUser} an object to containing various information on the user statuses
*/
async getUser(): Promise<GetUser> {
const userStatus = await this.wallet.getUserWalletStatus();
switch (userStatus.status) {
// user gets {UserWalletStatus.LOGGED_IN_NEW_DEVICE} when they log in but never complete the recovery flow and exits (close modal, refresh etc)
case UserWalletStatus.LOGGED_IN_NEW_DEVICE:
// User gets {UserWalletStatus.LOGGED_IN_WALLET_UNINITIALIZED} when they log in but manage to exit the client in the small window between auth completion and sending them their wallet recovery details
case UserWalletStatus.LOGGED_IN_WALLET_UNINITIALIZED:
// in both case, we simply log them out to reset their state
await this.auth.logout();
return this.getUser();
case UserWalletStatus.LOGGED_OUT:
return {
status: UserStatus.LOGGED_OUT,
};
case UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED:
return {
status: UserStatus.LOGGED_IN_WALLET_INITIALIZED,
...userStatus.user,
};
}
return this.wallet.getUserWalletStatus();
}
}
@@ -6,12 +6,14 @@ import { walletIds } from "../../constants/walletIds";
import { Connector } from "../../interfaces/connector";
import {
AuthLoginReturnType,
EmbeddedWalletSdk,
InitializedUser,
UserStatus,
SendEmailOtpReturnType,
UserWalletStatus,
} from "./implementations";
import {
AuthParams,
AuthResult,
EmbeddedWalletConnectionArgs,
EmbeddedWalletConnectorOptions,
} from "./types";
@@ -37,75 +39,44 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
this.#embeddedWalletSdk = new EmbeddedWalletSdk({
clientId: this.options.clientId,
chain: "Ethereum",
styles: this.options.styles,
onAuthSuccess: this.options.onAuthSuccess,
});
}
return this.#embeddedWalletSdk;
}
async connect(options?: EmbeddedWalletConnectionArgs) {
const thirdwebSDK = await this.getEmbeddedWalletSDK();
if (!thirdwebSDK) {
throw new Error("EmbeddedWallet SDK not initialized");
}
const user = await thirdwebSDK.getUser();
switch (user.status) {
case UserStatus.LOGGED_OUT: {
let authResult: AuthLoginReturnType;
switch (options?.loginType) {
case "headless_google_oauth": {
authResult = await thirdwebSDK.auth.loginWithGoogle({
closeOpenedWindow: options.closeOpenedWindow,
openedWindow: options.openedWindow,
});
break;
}
case "headless_email_otp_verification": {
authResult = await thirdwebSDK.auth.verifyEmailLoginOtp({
email: options.email,
otp: options.otp,
});
break;
}
case "ui_email_otp": {
authResult = await thirdwebSDK.auth.loginWithEmailOtp({
email: options.email,
});
break;
}
default: {
authResult = await thirdwebSDK.auth.loginWithModal();
break;
}
}
this.user = authResult.user;
break;
async connect(args?: EmbeddedWalletConnectionArgs): Promise<string> {
// backwards compatibility - options should really be required here
if (!args) {
// default to iframe flow
const result = await this.authenticate({ strategy: "iframe" });
if (!result.user) {
throw new Error("Error connecting User");
}
case UserStatus.LOGGED_IN_WALLET_INITIALIZED: {
if (options?.loginType === "headless_google_oauth") {
if (options.closeOpenedWindow && options.openedWindow) {
options.closeOpenedWindow(options.openedWindow);
}
}
this.user = user;
break;
this.user = result.user;
} else {
if (!args.authResult) {
throw new Error(
"Missing authData - call authenticate() first with your authentication strategy",
);
}
}
if (!this.user) {
throw new Error("Error connecting User");
if (!args.authResult.user) {
throw new Error(
"Missing authData.user - call authenticate() first with your authentication strategy",
);
}
this.user = args.authResult.user;
}
if (options?.chainId) {
this.switchChain(options.chainId);
if (args?.chainId) {
this.switchChain(args.chainId);
}
this.setupListeners();
return this.getAddress();
}
async disconnect(): Promise<void> {
const paper = await this.#embeddedWalletSdk;
const paper = this.#embeddedWalletSdk;
await paper?.auth.logout();
this.#signer = undefined;
this.#embeddedWalletSdk = undefined;
@@ -113,8 +84,10 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
}
async getAddress(): Promise<string> {
const signer = await this.getSigner();
return signer.getAddress();
if (!this.user) {
throw new Error("Embedded Wallet is not connected");
}
return this.user.walletAddress;
}
async isConnected(): Promise<boolean> {
@@ -139,18 +112,8 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
return this.#signer;
}
if (!this.user) {
const embeddedWalletSdk = await this.getEmbeddedWalletSDK();
const user = await embeddedWalletSdk.getUser();
switch (user.status) {
case UserStatus.LOGGED_IN_WALLET_INITIALIZED: {
this.user = user;
break;
}
}
}
const signer = await this.user?.wallet.getEthersJsSigner({
const user = await this.getUser();
const signer = await user?.wallet.getEthersJsSigner({
rpcEndpoint: this.options.chain.rpc[0] || "", // TODO: handle chain.rpc being empty array
});
@@ -185,12 +148,7 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
}
async setupListeners() {
const provider = await this.getProvider();
if (provider.on) {
provider.on("accountsChanged", this.onAccountsChanged);
provider.on("chainChanged", this.onChainChanged);
provider.on("disconnect", this.onDisconnect);
}
return Promise.resolve();
}
updateChains(chains: Chain[]) {
@@ -218,12 +176,89 @@ export class EmbeddedWalletConnector extends Connector<EmbeddedWalletConnectionA
this.emit("disconnect");
};
async getUser(): Promise<InitializedUser | null> {
if (
!this.user ||
!this.user.wallet ||
!this.user.wallet.getEthersJsSigner
) {
const embeddedWalletSdk = this.getEmbeddedWalletSDK();
const user = await embeddedWalletSdk.getUser();
switch (user.status) {
case UserWalletStatus.LOGGED_IN_WALLET_INITIALIZED: {
this.user = user;
break;
}
}
}
return this.user;
}
async getEmail() {
// implicit call to set the user
await this.getSigner();
if (!this.user) {
throw new Error("No user found, Paper Wallet is not connected");
throw new Error("No user found, Embedded Wallet is not connected");
}
return this.user.authDetails.email;
}
async getRecoveryInformation() {
// implicit call to set the user
await this.getSigner();
if (!this.user) {
throw new Error("No user found, Embedded Wallet is not connected");
}
return this.user.authDetails;
}
async sendVerificationEmail({
email,
}: {
email: string;
}): Promise<SendEmailOtpReturnType> {
const ewSDK = this.getEmbeddedWalletSDK();
return ewSDK.auth.sendEmailLoginOtp({ email });
}
async authenticate(params: AuthParams): Promise<AuthResult> {
const ewSDK = this.getEmbeddedWalletSDK();
const strategy = params.strategy;
switch (strategy) {
case "email_verification": {
return await ewSDK.auth.verifyEmailLoginOtp({
email: params.email,
otp: params.verificationCode,
recoveryCode: params.recoveryCode,
});
}
case "google": {
return ewSDK.auth.loginWithGoogle({
closeOpenedWindow: params.closeOpenedWindow,
openedWindow: params.openedWindow,
});
}
case "jwt": {
return ewSDK.auth.loginWithCustomJwt({
jwt: params.jwt,
encryptionKey: params.encryptionKey,
});
}
case "iframe_email_verification": {
return ewSDK.auth.loginWithEmailOtp({
email: params.email,
});
}
case "iframe":
case undefined: {
return ewSDK.auth.loginWithModal();
}
default:
assertUnreachable(strategy);
}
}
}
function assertUnreachable(x: never): never {
throw new Error("Invalid param: " + x);
}
@@ -1,34 +1,66 @@
import type { Chain } from "@thirdweb-dev/chains";
import { EmbeddedWalletConstructorType } from "./implementations";
import { AuthAndWalletRpcReturnType, InitializedUser } from "./implementations";
export type EmbeddedWalletAdditionalOptions = {
chain: Pick<Chain, "chainId" | "rpc">;
clientId: string;
styles?: EmbeddedWalletConstructorType["styles"];
onAuthSuccess?: (authResult: AuthAndWalletRpcReturnType) => void;
};
export interface EmbeddedWalletConnectorOptions {
clientId: string;
chains: Chain[];
chain: Pick<Chain, "chainId" | "rpc">;
styles?: EmbeddedWalletConstructorType["styles"];
onAuthSuccess?: (authResult: AuthAndWalletRpcReturnType) => void;
}
export type EmbeddedWalletConnectionArgs = {
chainId?: number;
} & (
| {
loginType: "headless_google_oauth";
openedWindow?: Window;
closeOpenedWindow?: (window: Window) => void;
}
| {
loginType: "headless_email_otp_verification";
email: string;
otp: string;
}
| {
loginType: "ui_email_otp";
email: string;
}
);
authResult: AuthResult;
};
type EmailVerificationAuthParams = {
strategy: "email_verification";
email: string;
verificationCode: string;
recoveryCode?: string;
};
type GoogleAuthParams = {
strategy: "google";
openedWindow?: Window;
closeOpenedWindow?: (window: Window) => void;
};
type JwtAuthParams = {
strategy: "jwt";
jwt: string;
encryptionKey?: string;
};
// open iFrame to send and input the OTP
type IframeOtpAuthParams = {
strategy: "iframe_email_verification";
email: string;
};
// open iFrame to enter email and OTP
type IframeAuthParams = {
strategy: "iframe";
};
// this is the input to 'authenticate'
export type AuthParams =
| EmailVerificationAuthParams
| GoogleAuthParams
| JwtAuthParams
| IframeOtpAuthParams
| IframeAuthParams;
// TODO typed based off AuthParams["strategy"]
export type AuthResult = {
user?: InitializedUser;
isNewUser?: boolean;
needsRecoveryCode?: boolean;
verifyOTP?: (otp: string, recoveryCode?: string) => Promise<AuthResult>;
};
@@ -1,6 +1,7 @@
import { getValidChainRPCs } from "@thirdweb-dev/chains";
import { EmbeddedWalletConnector } from "../connectors/embedded-wallet";
import { Ethereum, getValidChainRPCs } from "@thirdweb-dev/chains";
import type { EmbeddedWalletConnector } from "../connectors/embedded-wallet";
import {
AuthParams,
EmbeddedWalletAdditionalOptions,
EmbeddedWalletConnectionArgs,
} from "../connectors/embedded-wallet/types";
@@ -11,7 +12,12 @@ import { AbstractClientWallet, WalletOptions } from "./base";
export type EmbeddedWalletOptions =
WalletOptions<EmbeddedWalletAdditionalOptions>;
export type { EmbeddedWalletAdditionalOptions } from "../connectors/embedded-wallet/types";
export type {
EmbeddedWalletAdditionalOptions,
AuthParams,
AuthResult,
EmbeddedWalletConnectionArgs,
} from "../connectors/embedded-wallet/types";
export class EmbeddedWallet extends AbstractClientWallet<
EmbeddedWalletAdditionalOptions,
@@ -27,6 +33,31 @@ export class EmbeddedWallet extends AbstractClientWallet<
"ipfs://QmNx2evQa6tcQs9VTd3YaDm31ckfStvgRGKFGELahUmrbV/emailIcon.svg",
};
/**
* Sends a verification email to the provided email address.
*
* @param email - The email address to which the verification email will be sent.
* @param clientId - Your thirdweb client ID
* @returns Information on the user's status and whether they are a new user.
*
* @example
* ```typescript
* sendVerificationEmail({ email: '[email protected]', clientId: 'yourClientId' })
* .then(() => console.log('Verification email sent successfully.'))
* .catch(error => console.error('Failed to send verification email:', error));
* ```
*/
static async sendVerificationEmail(options: {
email: string;
clientId: string;
}) {
const wallet = new EmbeddedWallet({
chain: Ethereum,
clientId: options.clientId,
});
return wallet.sendVerificationEmail({ email: options.email });
}
public get walletName() {
return "Embedded Wallet" as const;
}
@@ -50,16 +81,31 @@ export class EmbeddedWallet extends AbstractClientWallet<
protected async getConnector(): Promise<Connector> {
if (!this.connector) {
// import the connector dynamically
const { EmbeddedWalletConnector } = await import(
"../connectors/embedded-wallet"
);
this.connector = new EmbeddedWalletConnector({
clientId: this.options?.clientId ?? "",
chain: this.chain,
chains: this.chains,
styles: this.options?.styles,
onAuthSuccess: this.options?.onAuthSuccess,
});
}
return this.connector;
}
override autoConnect(
connectOptions?: ConnectParams<EmbeddedWalletConnectionArgs> | undefined,
): Promise<string> {
if (!connectOptions) {
throw new Error("Can't autoconnect embedded wallet");
}
// override autoconnect logic for embedded wallet
// can just call connect since we should have the authResult persisted already
return this.connect(connectOptions);
}
getConnectParams(): ConnectParams<EmbeddedWalletConnectionArgs> | undefined {
const connectParams = super.getConnectParams();
@@ -67,15 +113,13 @@ export class EmbeddedWallet extends AbstractClientWallet<
return undefined;
}
// do not return non-serializable params to make auto-connect work
if (connectParams.loginType === "headless_google_oauth") {
return {
loginType: connectParams.loginType,
chainId: connectParams.chainId,
};
}
return connectParams;
// omit non serializable/autoconnect-able
return {
chainId: connectParams.chainId,
authResult: {
user: connectParams.authResult.user,
},
};
}
async getEmail() {
@@ -87,4 +131,20 @@ export class EmbeddedWallet extends AbstractClientWallet<
const connector = (await this.getConnector()) as EmbeddedWalletConnector;
return connector.getEmbeddedWalletSDK();
}
// TODO move to connect/auth callback
async getRecoveryInformation() {
const connector = (await this.getConnector()) as EmbeddedWalletConnector;
return connector.getRecoveryInformation();
}
async sendVerificationEmail({ email }: { email: string }) {
const connector = (await this.getConnector()) as EmbeddedWalletConnector;
return connector.sendVerificationEmail({ email });
}
async authenticate(params: AuthParams) {
const connector = (await this.getConnector()) as EmbeddedWalletConnector;
return connector.authenticate(params);
}
}
-17
View File
@@ -768,9 +768,6 @@ importers:
'@paperxyz/embedded-wallet-service-sdk':
specifier: ^1.2.4
version: 1.2.4
'@react-native-community/checkbox':
specifier: ^0.5.16
version: 0.5.16([email protected])([email protected])
'@shopify/restyle':
specifier: ^2.4.2
version: 2.4.2([email protected])([email protected])
@@ -10376,20 +10373,6 @@ packages:
merge-options: 3.0.4
react-native: 0.71.11(@babel/[email protected])(@babel/[email protected])([email protected])
/@react-native-community/[email protected]([email protected])([email protected]):
resolution: {integrity: sha512-j4fmWe77EAayGnKJ52BljlN8apLT3xjxG/pJOA6HZ4ew63FiXmnY7VtxTzmvDKgSPrETdQc2lmx5mdXTAufJnw==}
peerDependencies:
react: '*'
react-native: '>= 0.62'
react-native-windows: '>=0.62'
peerDependenciesMeta:
react-native-windows:
optional: true
dependencies:
react: 18.2.0
react-native: 0.71.11(@babel/[email protected])(@babel/[email protected])([email protected])
dev: false
/@react-native-community/[email protected]:
resolution: {integrity: sha512-iNsrjzjIRv9yb5y309SWJ8NDHdwYtnCpmxZouQDyOljUdC9MwdZ4ChbtA4rwQyAwgOVfS9F/j56ML3Cslmvrxg==}
dependencies: