diff --git a/.changeset/cold-dryers-dream.md b/.changeset/cold-dryers-dream.md
new file mode 100644
index 00000000000..dc8fb5ce30f
--- /dev/null
+++ b/.changeset/cold-dryers-dream.md
@@ -0,0 +1,58 @@
+---
+"@thirdweb-dev/react-native": patch
+---
+
+Custom JWT support in React Native
+
+Enables passing a custom JWT to the embeddedWallet:
+
+```javascript
+import {
+ EmbeddedWallet,
+ embeddedWallet,
+ ThirdwebProvider,
+ useCreateWalletInstance,
+ useSetConnectedWallet,
+} from '@thirdweb-dev/react-native';
+import {Button} from 'react-native';
+import React from 'react';
+
+const App = () => {
+ return (
+
+
+
+ );
+};
+
+const AppInner = () => {
+ const createInstance = useCreateWalletInstance();
+ const setConnectedWallet = useSetConnectedWallet();
+
+ const triggerConnect = async () => {
+ const embeddedWalletConfig = embeddedWallet();
+
+ if (embeddedWalletConfig) {
+ const instance = createInstance(embeddedWalletConfig);
+
+ if (instance) {
+ await (instance as EmbeddedWallet).connect({
+ loginType: 'custom_jwt_auth',
+ encryptionKey: 'hello',
+ jwtToken: 'customJwt' || '',
+ });
+ setConnectedWallet(instance); // this sets the active wallet on the provider enabling all thirdweb hooks
+ }
+ }
+ };
+
+ return ;
+};
+
+
+```
diff --git a/packages/react-native/package.json b/packages/react-native/package.json
index cb54ea4bd16..70c2d808791 100644
--- a/packages/react-native/package.json
+++ b/packages/react-native/package.json
@@ -31,6 +31,7 @@
"@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:*",
diff --git a/packages/react-native/src/evm/components/ConnectWalletFlow/ChooseWallet/ChooseWallet.tsx b/packages/react-native/src/evm/components/ConnectWalletFlow/ChooseWallet/ChooseWallet.tsx
index 7b16b87b405..9aa29c72b97 100644
--- a/packages/react-native/src/evm/components/ConnectWalletFlow/ChooseWallet/ChooseWallet.tsx
+++ b/packages/react-native/src/evm/components/ConnectWalletFlow/ChooseWallet/ChooseWallet.tsx
@@ -45,7 +45,9 @@ export function ChooseWallet({
const guestWallet = wallets.find((w) => w.id === walletIds.localWallet);
const emailWallet = wallets.find(
- (w) => w.id === walletIds.magicLink || w.id === walletIds.embeddedWallet,
+ (w) =>
+ w.id === walletIds.magicLink ||
+ (w.id === walletIds.embeddedWallet && w.selectUI),
);
const connectionWallets = wallets
.filter(
diff --git a/packages/react-native/src/evm/components/ConnectWalletFlow/SmartWallet/SmartWalletFlow.tsx b/packages/react-native/src/evm/components/ConnectWalletFlow/SmartWallet/SmartWalletFlow.tsx
index e5ad513cbef..d099c960ecc 100644
--- a/packages/react-native/src/evm/components/ConnectWalletFlow/SmartWallet/SmartWalletFlow.tsx
+++ b/packages/react-native/src/evm/components/ConnectWalletFlow/SmartWallet/SmartWalletFlow.tsx
@@ -146,7 +146,7 @@ export const SmartWalletFlow = ({
<>
{mismatch
- ? l.smart_wallet.network_mistmach
+ ? l.smart_wallet.network_mismatch
: `${l.smart_wallet.connecting} ...`}
{mismatch ? (
diff --git a/packages/react-native/src/evm/i18n/types.ts b/packages/react-native/src/evm/i18n/types.ts
index 51c30f5981b..e7dd6196195 100644
--- a/packages/react-native/src/evm/i18n/types.ts
+++ b/packages/react-native/src/evm/i18n/types.ts
@@ -31,7 +31,6 @@ export const _en = {
copy_address_or_scan:
"Copy the wallet address or scan the QR code to send funds to this wallet.",
request_testnet_funds: "Request Testnet Funds",
- view_transatcion_history: "View Transaction History",
your_address: "Your address",
qr_code: "QR Code",
select_token: "Select Token",
@@ -72,7 +71,7 @@ export const _en = {
smart_wallet: {
switch_to_smart: "Switch to Smart Wallet",
switch_to_personal: "Switch to Personal Wallet",
- network_mistmach: "Network Mismatch",
+ network_mismatch: "Network Mismatch",
connecting: "Connecting",
},
embedded_wallet: {
@@ -80,6 +79,9 @@ export const _en = {
sign_in: "Sign In",
sign_in_google: "Sign in with Google",
enter_your_email: "Enter your email address",
+ forgot_password: "Forgot password",
+ enter_account_recovery_code: "Enter account recovery code",
+ backup_your_account: "Backup your account",
},
wallet_connect: {
no_results_found: "No results found",
@@ -103,5 +105,7 @@ export const _en = {
or: "OR",
from: "from",
to: "from",
+ next: "Next",
+ learn_more: "Learn More",
},
};
diff --git a/packages/react-native/src/evm/wallets/connectors/embedded-wallet/embedded-connector.ts b/packages/react-native/src/evm/wallets/connectors/embedded-wallet/embedded-connector.ts
index d481f5ec446..d4243f749bc 100644
--- a/packages/react-native/src/evm/wallets/connectors/embedded-wallet/embedded-connector.ts
+++ b/packages/react-native/src/evm/wallets/connectors/embedded-wallet/embedded-connector.ts
@@ -1,4 +1,5 @@
import {
+ AuthOptions,
EmbeddedWalletConnectionArgs,
EmbeddedWalletConnectorOptions,
OauthOption,
@@ -7,7 +8,12 @@ import type { Chain } from "@thirdweb-dev/chains";
import { Connector, normalizeChainId } from "@thirdweb-dev/wallets";
import { providers, Signer } from "ethers";
import { utils } from "ethers";
-import { sendEmailOTP, socialLogin, validateEmailOTP } from "./embedded/auth";
+import {
+ customJwt,
+ sendEmailOTP,
+ socialLogin,
+ validateEmailOTP,
+} from "./embedded/auth";
import { getEthersSigner } from "./embedded/signer";
import { logoutUser } from "./embedded/helpers/auth/logout";
import {
@@ -15,6 +21,7 @@ import {
getConnectedEmail,
saveConnectedEmail,
} from "./embedded/helpers/storage/local";
+import { AuthProvider } from "@paperxyz/embedded-wallet-service-sdk";
export class EmbeddedWalletConnector extends Connector {
private options: EmbeddedWalletConnectorOptions;
@@ -30,11 +37,38 @@ export class EmbeddedWalletConnector extends Connector {
clearConnectedEmail();
await logoutUser(this.options.clientId);
@@ -148,12 +206,7 @@ export class EmbeddedWalletConnector extends Connector["styles"];
}
-export interface EmbeddedWalletConnectionArgs {
- email?: string;
+export interface AuthOptions {
+ jwtToken: string;
+ encryptionKey: string;
}
+
+export type EmbeddedWalletConnectionArgs = {
+ chainId?: number;
+} & (
+ | {
+ loginType: "headless_google_oauth";
+ redirectUrl: string;
+ }
+ | {
+ loginType: "headless_email_otp_verification";
+ email: string;
+ otp: string;
+ }
+ | {
+ loginType: "custom_jwt_auth";
+ jwtToken: string;
+ encryptionKey: string;
+ }
+);
diff --git a/packages/react-native/src/evm/wallets/wallets/embedded/AccountRecovery.tsx b/packages/react-native/src/evm/wallets/wallets/embedded/AccountRecovery.tsx
new file mode 100644
index 00000000000..cb513c1972d
--- /dev/null
+++ b/packages/react-native/src/evm/wallets/wallets/embedded/AccountRecovery.tsx
@@ -0,0 +1,91 @@
+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();
+ const [checkingRecoveryCode, setCheckingRecoveryCode] =
+ useState(false);
+ const [recoveryCode, setRecoveryCode] = useState("");
+
+ const onNextPress = async () => {
+ setCheckingRecoveryCode(true);
+ // Call enter recovery code
+ setErrorMessage("test");
+ };
+
+ return (
+
+
+ {l.embedded_wallet.enter_account_recovery_code}
+
+ }
+ subHeaderText={
+ "You should have a copy of this in the email address associated with your account"
+ }
+ onBackPress={goBack}
+ onClose={close}
+ />
+
+ {errorMessage ? (
+
+ {errorMessage}
+
+ ) : (
+
+ )}
+
+ {checkingRecoveryCode ? (
+
+ ) : (
+
+ {l.common.next}
+
+ )}
+
+
+ );
+};
diff --git a/packages/react-native/src/evm/wallets/wallets/embedded/BackupAccount.tsx b/packages/react-native/src/evm/wallets/wallets/embedded/BackupAccount.tsx
new file mode 100644
index 00000000000..71dcced1f76
--- /dev/null
+++ b/packages/react-native/src/evm/wallets/wallets/embedded/BackupAccount.tsx
@@ -0,0 +1,114 @@
+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();
+ 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) => {
+ console.log("code", code);
+
+ await Clipboard.setStringAsync(code);
+ setCodeCopied(true);
+ };
+
+ const codes = ["testing", "testing2", "testing3", "testing4", "testing5"];
+
+ return (
+
+ {l.embedded_wallet.backup_your_account}
+ }
+ 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}
+ />
+
+ {codes.map((code) => (
+ {
+ onCodeCopyPress(code);
+ }}
+ p="md"
+ >
+ {code}
+
+
+ ))}
+
+
+ {errorMessage ? (
+
+ {errorMessage}
+
+ ) : (
+
+ )}
+
+
+ setToggleCheckBox(newValue)}
+ />
+
+ {l.common.learn_more}
+
+
+
+
+ {l.common.next}
+
+
+
+ {codeCopied === true ? (
+
+ ) : null}
+
+ );
+};
diff --git a/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedConnectionUI.tsx b/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedConnectionUI.tsx
index a6c745c07d6..3c5a786dc4d 100644
--- a/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedConnectionUI.tsx
+++ b/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedConnectionUI.tsx
@@ -68,9 +68,13 @@ export const EmbeddedConnectionUI: React.FC> = ({
setTimeout(() => {
(selectionData.emailWallet as EmbeddedWallet)
- .validateEmailOTP(otp)
+ .connect({
+ loginType: "headless_email_otp_verification",
+ otp,
+ email: selectionData.email,
+ })
.then(async (response) => {
- if (response?.success) {
+ if (response) {
if (onLocallyConnected) {
onLocallyConnected(selectionData.emailWallet);
} else {
@@ -79,7 +83,7 @@ export const EmbeddedConnectionUI: React.FC> = ({
}
} else {
clearCode();
- setErrorMessage(response?.error || "Error validating the code");
+ setErrorMessage(response || "Error validating the code");
setCheckingOtp(false);
setFocusedIndex(undefined);
}
diff --git a/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSelectionUI.tsx b/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSelectionUI.tsx
index de95dbf39af..aec459200d6 100644
--- a/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSelectionUI.tsx
+++ b/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSelectionUI.tsx
@@ -25,8 +25,9 @@ export const EmailSelectionUI: React.FC<
SelectUIProps & {
oauthOptions?: OauthOptions;
email?: boolean;
+ custom_auth?: boolean;
}
-> = ({ onSelect, walletConfig, oauthOptions, email }) => {
+> = ({ onSelect, walletConfig, oauthOptions, email, custom_auth }) => {
const l = useLocale();
const theme = useGlobalTheme();
const [emailInput, setEmailInput] = useState("");
@@ -45,6 +46,11 @@ export const EmailSelectionUI: React.FC<
setEmailWallet(emailWalletInstance);
}, [createWalletInstance, walletConfig]);
+ if (custom_auth) {
+ // No UI for custom auth
+ return null;
+ }
+
const validateEmail = (emailToValidate: string) => {
const pattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
return pattern.test(emailToValidate);
diff --git a/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSocialConnection.tsx b/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSocialConnection.tsx
index f2492e7f84d..15bf8751d48 100644
--- a/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSocialConnection.tsx
+++ b/packages/react-native/src/evm/wallets/wallets/embedded/EmbeddedSocialConnection.tsx
@@ -23,9 +23,12 @@ export const EmbeddedSocialConnection: React.FC<
setTimeout(() => {
(selectionData.emailWallet as EmbeddedWallet)
- .socialLogin(selectionData?.oauthOptions)
+ .connect({
+ loginType: "headless_google_oauth",
+ redirectUrl: selectionData.oauthOptions?.redirectUrl,
+ })
.then(async (response) => {
- if (response?.success) {
+ if (response) {
if (onLocallyConnected) {
onLocallyConnected(selectionData.emailWallet);
} else {
@@ -34,7 +37,7 @@ export const EmbeddedSocialConnection: React.FC<
}
} else {
setErrorMessage(
- response?.error || "Error login in. Please try again later.",
+ response || "Error login in. Please try again later.",
);
}
})
diff --git a/packages/react-native/src/evm/wallets/wallets/embedded/EnterPassword.tsx b/packages/react-native/src/evm/wallets/wallets/embedded/EnterPassword.tsx
new file mode 100644
index 00000000000..05592a60820
--- /dev/null
+++ b/packages/react-native/src/evm/wallets/wallets/embedded/EnterPassword.tsx
@@ -0,0 +1,115 @@
+import React, { useState } from "react";
+import { ActivityIndicator } from "react-native";
+import { ConnectWalletHeader } from "../../../components/ConnectWalletFlow/ConnectingWallet/ConnectingWalletHeader";
+import { Box, BaseButton, Text } from "../../../components/base";
+import {
+ useGlobalTheme,
+ useLocale,
+} from "../../../providers/ui-context-provider";
+import { PasswordInput } from "../../../components/PasswordInput";
+
+export type EnterPasswordProps = {
+ goBack: () => void;
+ close: () => void;
+ email: string;
+ type: "create_password" | "enter_password";
+};
+
+export const EnterPassword = ({
+ close,
+ goBack,
+ email,
+ type,
+}: EnterPasswordProps) => {
+ const l = useLocale();
+ const theme = useGlobalTheme();
+ const [errorMessage, setErrorMessage] = useState();
+ const [checkingPass, setCheckingPass] = useState(false);
+ const [password, setPassword] = useState("");
+
+ const isCreatePassword = type === "create_password";
+
+ const onNextPress = async () => {
+ setCheckingPass(true);
+ if (isCreatePassword) {
+ // Call create password
+ console.log("password", password);
+ } else {
+ // Call enter password
+ setErrorMessage("test");
+ }
+ };
+
+ const onForgotPress = () => {};
+
+ const onLearnMorePress = () => {};
+
+ return (
+
+
+ {isCreatePassword ? "Create password" : "Enter password"}
+
+ }
+ subHeaderText={
+ isCreatePassword
+ ? "Set a password for your account"
+ : `Enter the password for email: ${email}`
+ }
+ onBackPress={goBack}
+ onClose={close}
+ />
+
+
+
+ {isCreatePassword ? null : (
+
+
+ {l.embedded_wallet.forgot_password}
+
+
+ )}
+
+ {errorMessage ? (
+
+ {errorMessage}
+
+ ) : (
+
+ )}
+
+ {isCreatePassword ? (
+
+
+ {l.common.learn_more}
+
+
+ ) : null}
+
+ {checkingPass ? (
+
+ ) : (
+
+ {l.common.next}
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/react-native/src/evm/wallets/wallets/embedded/embedded-wallet.tsx b/packages/react-native/src/evm/wallets/wallets/embedded/embedded-wallet.tsx
index b97917f6fbe..544475104ea 100644
--- a/packages/react-native/src/evm/wallets/wallets/embedded/embedded-wallet.tsx
+++ b/packages/react-native/src/evm/wallets/wallets/embedded/embedded-wallet.tsx
@@ -14,13 +14,14 @@ export type EmbeddedWalletConfig = {
// @default true - set false to disable
email?: boolean;
- // @default { providers: ['google'] } - set false to disable
oauthOptions?:
| {
providers: OAuthProvider[];
redirectUrl: string;
}
| false;
+
+ custom_auth?: boolean;
};
export const embeddedWallet = (
@@ -37,6 +38,7 @@ export const embeddedWallet = (
}
: undefined
}
+ custom_auth={config?.custom_auth}
// you cannot disable both email and oauth
email={!config?.oauthOptions && !config?.email ? true : config?.email}
/>
@@ -51,7 +53,7 @@ export const embeddedWallet = (
clientId: options.clientId || "",
});
},
- selectUI: selectUI,
+ selectUI: config?.custom_auth ? undefined : selectUI,
connectUI: EmbeddedConnectionUI,
};
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 149a12ed992..f1b1344699c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -772,6 +772,9 @@ 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(react-native@0.71.11)(react@18.2.0)
'@shopify/restyle':
specifier: ^2.4.2
version: 2.4.2(react-native@0.71.11)(react@18.2.0)
@@ -10364,6 +10367,20 @@ packages:
merge-options: 3.0.4
react-native: 0.71.11(@babel/core@7.22.9)(@babel/preset-env@7.22.9)(react@18.2.0)
+ /@react-native-community/checkbox@0.5.16(react-native@0.71.11)(react@18.2.0):
+ 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/core@7.22.9)(@babel/preset-env@7.22.9)(react@18.2.0)
+ dev: false
+
/@react-native-community/cli-clean@10.1.1:
resolution: {integrity: sha512-iNsrjzjIRv9yb5y309SWJ8NDHdwYtnCpmxZouQDyOljUdC9MwdZ4ChbtA4rwQyAwgOVfS9F/j56ML3Cslmvrxg==}
dependencies: