diff --git a/account-kit/rn-signer/example/package.json b/account-kit/rn-signer/example/package.json
index e5afa19f37..8bb4829484 100644
--- a/account-kit/rn-signer/example/package.json
+++ b/account-kit/rn-signer/example/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@account-kit/signer": "^4.3.0",
+ "@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "7.0.3",
"@react-navigation/native-stack": "7.1.0",
"dotenv": "^16.4.5",
diff --git a/account-kit/rn-signer/example/redirect-server/index.ts b/account-kit/rn-signer/example/redirect-server/index.ts
index b8c6756c80..7c7f2021a9 100644
--- a/account-kit/rn-signer/example/redirect-server/index.ts
+++ b/account-kit/rn-signer/example/redirect-server/index.ts
@@ -11,7 +11,9 @@ app.get("/", (req, res) => {
const bundle = req.query.bundle;
const orgId = req.query.orgId;
- res.redirect(`${appScheme}://home?bundle=${bundle}&orgId=${orgId}`);
+ res.redirect(
+ `${appScheme}://magic-link-auth?bundle=${bundle}&orgId=${orgId}`
+ );
});
app.listen(port, () => {
diff --git a/account-kit/rn-signer/example/src/App.tsx b/account-kit/rn-signer/example/src/App.tsx
index ae41b10abe..4316a9b890 100644
--- a/account-kit/rn-signer/example/src/App.tsx
+++ b/account-kit/rn-signer/example/src/App.tsx
@@ -1,21 +1,35 @@
/* eslint-disable import/extensions */
import { createStaticNavigation } from "@react-navigation/native";
-import { createNativeStackNavigator } from "@react-navigation/native-stack";
+import { Text } from "react-native";
+import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { SafeAreaProvider } from "react-native-safe-area-context";
-import HomeScreen from "./screens/Home";
+import OtpAuthScreen from "./screens/otp-auth";
+import MagicLinkAuthScreen from "./screens/magic-link-auth";
const linking = {
enabled: "auto" as const /* Automatically generate paths for all screens */,
prefixes: ["rn-signer-demo://"],
};
-const RootStack = createNativeStackNavigator({
- initialRouteName: "Home",
+const RootStack = createBottomTabNavigator({
+ initialRouteName: "MagicLinkAuth",
screens: {
- Home: {
- screen: HomeScreen,
- linking: { path: "home" },
+ MagicLinkAuth: {
+ screen: MagicLinkAuthScreen,
+ linking: { path: "magic-link-auth" },
+ options: {
+ tabBarLabel: "Magic Link",
+ tabBarIcon: () => 🪄,
+ },
+ },
+ OtpAuth: {
+ screen: OtpAuthScreen,
+ linking: { path: "otp-auth" },
+ options: {
+ tabBarLabel: "OTP",
+ tabBarIcon: () => 🔑,
+ },
},
},
});
diff --git a/account-kit/rn-signer/example/src/screens/Home.tsx b/account-kit/rn-signer/example/src/screens/magic-link-auth.tsx
similarity index 93%
rename from account-kit/rn-signer/example/src/screens/Home.tsx
rename to account-kit/rn-signer/example/src/screens/magic-link-auth.tsx
index ccf8ef0a88..0821472b1f 100644
--- a/account-kit/rn-signer/example/src/screens/Home.tsx
+++ b/account-kit/rn-signer/example/src/screens/magic-link-auth.tsx
@@ -1,5 +1,4 @@
/* eslint-disable import/extensions */
-import { RNAlchemySigner } from "@account-kit/react-native-signer";
import type { User } from "@account-kit/signer";
import { useEffect, useState } from "react";
import {
@@ -10,15 +9,18 @@ import {
Linking,
TouchableOpacity,
} from "react-native";
+
import Config from "react-native-config";
+import { RNAlchemySigner } from "@account-kit/react-native-signer";
-export default function HomeScreen() {
- const [email, setEmail] = useState("");
- const [user, setUser] = useState(null);
- const signer = new RNAlchemySigner({
+export default function MagicLinkAuthScreen() {
+ const signer = RNAlchemySigner({
client: { connection: { apiKey: Config.API_KEY! } },
});
+ const [email, setEmail] = useState("");
+ const [user, setUser] = useState(null);
+
const handleUserAuth = ({ bundle }: { bundle: string }) => {
signer
.authenticate({
@@ -73,12 +75,16 @@ export default function HomeScreen() {
onChangeText={setEmail}
value={email}
/>
- {/* TODO: implement OTP */}
+
{
signer
- .authenticate({ email, type: "email", emailMode: "magicLink" })
+ .authenticate({
+ email,
+ type: "email",
+ emailMode: "magicLink",
+ })
.catch(console.error);
}}
>
diff --git a/account-kit/rn-signer/example/src/screens/otp-auth.tsx b/account-kit/rn-signer/example/src/screens/otp-auth.tsx
new file mode 100644
index 0000000000..426abc9571
--- /dev/null
+++ b/account-kit/rn-signer/example/src/screens/otp-auth.tsx
@@ -0,0 +1,150 @@
+/* eslint-disable import/extensions */
+import type { User } from "@account-kit/signer";
+import { useEffect, useState } from "react";
+import {
+ View,
+ Text,
+ TextInput,
+ StyleSheet,
+ TouchableOpacity,
+} from "react-native";
+
+import Config from "react-native-config";
+import { RNAlchemySigner } from "@account-kit/react-native-signer";
+export default function MagicLinkAuthScreen() {
+ const [email, setEmail] = useState("");
+ const [user, setUser] = useState(null);
+
+ const signer = RNAlchemySigner({
+ client: { connection: { apiKey: Config.API_KEY! } },
+ });
+
+ const [awaitingOtp, setAwaitingOtp] = useState(false);
+
+ const [otpCode, setOtpCode] = useState("");
+
+ const handleUserAuth = ({ otpCode }: { otpCode: string }) => {
+ setAwaitingOtp(false);
+ signer
+ .authenticate({
+ otpCode: otpCode,
+ type: "otp",
+ })
+ .then((res) => {
+ console.log("res", res);
+ setUser(res);
+ })
+ .catch(console.error);
+ };
+
+ useEffect(() => {
+ // get the user if already logged in
+ signer.getAuthDetails().then(setUser);
+ }, []);
+
+ return (
+
+ {awaitingOtp ? (
+ <>
+
+ handleUserAuth({ otpCode })}
+ >
+ Sign in
+
+ >
+ ) : !user ? (
+ <>
+
+ {
+ signer
+ .authenticate({
+ email,
+ type: "email",
+ emailMode: "otp",
+ })
+ .catch(console.error);
+ setAwaitingOtp(true);
+ }}
+ >
+ Sign in
+
+ >
+ ) : (
+ <>
+
+ Currently logged in as: {user.email}
+
+ OrgId: {user.orgId}
+ Address: {user.address}
+
+ signer.disconnect().then(() => setUser(null))}
+ >
+ Sign out
+
+ >
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#FFFFF",
+ paddingHorizontal: 20,
+ },
+ textInput: {
+ width: "100%",
+ height: 40,
+ borderColor: "gray",
+ borderWidth: 1,
+ paddingHorizontal: 10,
+ backgroundColor: "rgba(0,0,0,0.05)",
+ marginTop: 20,
+ marginBottom: 10,
+ },
+ box: {
+ width: 60,
+ height: 60,
+ marginVertical: 20,
+ },
+ button: {
+ width: 200,
+ padding: 10,
+ height: 50,
+ backgroundColor: "rgb(147, 197, 253)",
+ borderRadius: 5,
+ alignItems: "center",
+ justifyContent: "center",
+ marginTop: 20,
+ },
+ buttonText: {
+ color: "white",
+ fontWeight: "bold",
+ textAlign: "center",
+ },
+ userText: {
+ marginBottom: 10,
+ fontSize: 18,
+ },
+});
diff --git a/account-kit/rn-signer/src/client.ts b/account-kit/rn-signer/src/client.ts
index 2ba464ec6d..c9d2577a34 100644
--- a/account-kit/rn-signer/src/client.ts
+++ b/account-kit/rn-signer/src/client.ts
@@ -36,12 +36,19 @@ export class RNSignerClient extends BaseSignerClient {
connection,
});
}
- // TODO: implement OTP
+
override async submitOtpCode(
args: Omit
): Promise<{ bundle: string }> {
- console.log("submitOtpCode", args);
- throw new Error("Method not implemented.");
+ this.eventEmitter.emit("authenticating", { type: "otpVerify" });
+ const publicKey = await this.stamper.init();
+
+ const { credentialBundle } = await this.request("/v1/otp", {
+ ...args,
+ targetPublicKey: publicKey,
+ });
+
+ return { bundle: credentialBundle };
}
override async createAccount(
@@ -57,7 +64,7 @@ export class RNSignerClient extends BaseSignerClient {
const response = await this.request("/v1/signup", {
email,
- emailMode: "magicLink",
+ emailMode: params.emailMode,
targetPublicKey: publicKey,
expirationSeconds,
redirectParams: params.redirectParams?.toString(),
@@ -72,11 +79,13 @@ export class RNSignerClient extends BaseSignerClient {
this.eventEmitter.emit("authenticating", { type: "email" });
let targetPublicKey = await this.stamper.init();
- return this.request("/v1/auth", {
+ const response = await this.request("/v1/auth", {
email: params.email,
- emailMode: "magicLink",
+ emailMode: params.emailMode,
targetPublicKey,
});
+
+ return response;
}
override async completeAuthWithBundle(params: {
@@ -86,7 +95,10 @@ export class RNSignerClient extends BaseSignerClient {
authenticatingType: AuthenticatingEventMetadata["type"];
idToken?: string;
}): Promise {
- if (params.authenticatingType !== "email") {
+ if (
+ params.authenticatingType !== "email" &&
+ params.authenticatingType !== "otp"
+ ) {
throw new Error("Unsupported authenticating type");
}
diff --git a/account-kit/rn-signer/src/signer.ts b/account-kit/rn-signer/src/signer.ts
index 0fbced4403..8c6e284556 100644
--- a/account-kit/rn-signer/src/signer.ts
+++ b/account-kit/rn-signer/src/signer.ts
@@ -19,8 +19,14 @@ const RNAlchemySignerParamsSchema = z
export type RNAlchemySignerParams = z.input;
-export class RNAlchemySigner extends BaseAlchemySigner {
- constructor(params: RNAlchemySignerParams) {
+class RNAlchemySignerSingleton extends BaseAlchemySigner {
+ private static instance: BaseAlchemySigner;
+
+ private constructor(params: RNAlchemySignerParams) {
+ if (!!RNAlchemySignerSingleton.instance) {
+ return RNAlchemySignerSingleton.instance;
+ }
+
const { sessionConfig, ...params_ } =
RNAlchemySignerParamsSchema.parse(params);
@@ -36,4 +42,17 @@ export class RNAlchemySigner extends BaseAlchemySigner {
sessionConfig,
});
}
+
+ public static getInstance(params: RNAlchemySignerParams) {
+ if (!this.instance) {
+ this.instance = new RNAlchemySignerSingleton(params);
+ }
+ return this.instance;
+ }
+}
+
+export function RNAlchemySigner(params: RNAlchemySignerParams) {
+ const instance = RNAlchemySignerSingleton.getInstance(params);
+
+ return instance;
}
diff --git a/yarn.lock b/yarn.lock
index 4456645797..97ecbfec6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6239,6 +6239,14 @@
invariant "^2.2.4"
nullthrows "^1.1.1"
+"@react-navigation/bottom-tabs@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.2.0.tgz#5b336b823226647a263b4fe743655462796b6aaf"
+ integrity sha512-1LxjgnbPyFINyf9Qr5d1YE0pYhuJayg5TCIIFQmbcX4PRhX7FKUXV7cX8OzrKXEdZi/UE/VNXugtozPAR9zgvA==
+ dependencies:
+ "@react-navigation/elements" "^2.2.5"
+ color "^4.2.3"
+
"@react-navigation/core@^7.0.3":
version "7.0.3"
resolved "https://registry.npmjs.org/@react-navigation/core/-/core-7.0.3.tgz#bac011a459ae62bb91e3bc8adeb862f05ac19a88"
@@ -6266,6 +6274,13 @@
dependencies:
color "^4.2.3"
+"@react-navigation/elements@^2.2.5":
+ version "2.2.5"
+ resolved "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.2.5.tgz#0e2ca76e2003e96b417a3d7c2829bf1afd69193f"
+ integrity sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg==
+ dependencies:
+ color "^4.2.3"
+
"@react-navigation/native-stack@7.1.0":
version "7.1.0"
resolved "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.1.0.tgz#7e4df1dc25daa9832f9677ca4c7c065287e5118f"