Skip to content

Commit

Permalink
feat: add OTP authentication capabilities to RN Signer (#1231)
Browse files Browse the repository at this point in the history
* feat: add OTP authentication capabilities to RN Signer

* fix: update redirect route path

* fix: fix linting
  • Loading branch information
iykazrji authored Dec 18, 2024
1 parent 0a6606e commit 6ebfca5
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 24 deletions.
1 change: 1 addition & 0 deletions account-kit/rn-signer/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion account-kit/rn-signer/example/redirect-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand Down
28 changes: 21 additions & 7 deletions account-kit/rn-signer/example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <Text>🪄</Text>,
},
},
OtpAuth: {
screen: OtpAuthScreen,
linking: { path: "otp-auth" },
options: {
tabBarLabel: "OTP",
tabBarIcon: () => <Text>🔑</Text>,
},
},
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<string>("");
const [user, setUser] = useState<User | null>(null);
const signer = new RNAlchemySigner({
export default function MagicLinkAuthScreen() {
const signer = RNAlchemySigner({
client: { connection: { apiKey: Config.API_KEY! } },
});

const [email, setEmail] = useState<string>("");
const [user, setUser] = useState<User | null>(null);

const handleUserAuth = ({ bundle }: { bundle: string }) => {
signer
.authenticate({
Expand Down Expand Up @@ -73,12 +75,16 @@ export default function HomeScreen() {
onChangeText={setEmail}
value={email}
/>
{/* TODO: implement OTP */}

<TouchableOpacity
style={styles.button}
onPress={() => {
signer
.authenticate({ email, type: "email", emailMode: "magicLink" })
.authenticate({
email,
type: "email",
emailMode: "magicLink",
})
.catch(console.error);
}}
>
Expand Down
150 changes: 150 additions & 0 deletions account-kit/rn-signer/example/src/screens/otp-auth.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const [user, setUser] = useState<User | null>(null);

const signer = RNAlchemySigner({
client: { connection: { apiKey: Config.API_KEY! } },
});

const [awaitingOtp, setAwaitingOtp] = useState<boolean>(false);

const [otpCode, setOtpCode] = useState<string>("");

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 (
<View style={styles.container}>
{awaitingOtp ? (
<>
<TextInput
style={styles.textInput}
placeholderTextColor="gray"
placeholder="enter your OTP code"
onChangeText={setOtpCode}
value={otpCode}
/>
<TouchableOpacity
style={styles.button}
onPress={() => handleUserAuth({ otpCode })}
>
<Text style={styles.buttonText}>Sign in</Text>
</TouchableOpacity>
</>
) : !user ? (
<>
<TextInput
style={styles.textInput}
placeholderTextColor="gray"
placeholder="enter your email"
onChangeText={setEmail}
value={email}
/>
<TouchableOpacity
style={styles.button}
onPress={() => {
signer
.authenticate({
email,
type: "email",
emailMode: "otp",
})
.catch(console.error);
setAwaitingOtp(true);
}}
>
<Text style={styles.buttonText}>Sign in</Text>
</TouchableOpacity>
</>
) : (
<>
<Text style={styles.userText}>
Currently logged in as: {user.email}
</Text>
<Text style={styles.userText}>OrgId: {user.orgId}</Text>
<Text style={styles.userText}>Address: {user.address}</Text>

<TouchableOpacity
style={styles.button}
onPress={() => signer.disconnect().then(() => setUser(null))}
>
<Text style={styles.buttonText}>Sign out</Text>
</TouchableOpacity>
</>
)}
</View>
);
}

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,
},
});
26 changes: 19 additions & 7 deletions account-kit/rn-signer/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,19 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
connection,
});
}
// TODO: implement OTP

override async submitOtpCode(
args: Omit<OtpParams, "targetPublicKey">
): 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(
Expand All @@ -57,7 +64,7 @@ export class RNSignerClient extends BaseSignerClient<undefined> {

const response = await this.request("/v1/signup", {
email,
emailMode: "magicLink",
emailMode: params.emailMode,
targetPublicKey: publicKey,
expirationSeconds,
redirectParams: params.redirectParams?.toString(),
Expand All @@ -72,11 +79,13 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
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: {
Expand All @@ -86,7 +95,10 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
authenticatingType: AuthenticatingEventMetadata["type"];
idToken?: string;
}): Promise<User> {
if (params.authenticatingType !== "email") {
if (
params.authenticatingType !== "email" &&
params.authenticatingType !== "otp"
) {
throw new Error("Unsupported authenticating type");
}

Expand Down
23 changes: 21 additions & 2 deletions account-kit/rn-signer/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ const RNAlchemySignerParamsSchema = z

export type RNAlchemySignerParams = z.input<typeof RNAlchemySignerParamsSchema>;

export class RNAlchemySigner extends BaseAlchemySigner<RNSignerClient> {
constructor(params: RNAlchemySignerParams) {
class RNAlchemySignerSingleton extends BaseAlchemySigner<RNSignerClient> {
private static instance: BaseAlchemySigner<RNSignerClient>;

private constructor(params: RNAlchemySignerParams) {
if (!!RNAlchemySignerSingleton.instance) {
return RNAlchemySignerSingleton.instance;
}

const { sessionConfig, ...params_ } =
RNAlchemySignerParamsSchema.parse(params);

Expand All @@ -36,4 +42,17 @@ export class RNAlchemySigner extends BaseAlchemySigner<RNSignerClient> {
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;
}
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 6ebfca5

Please sign in to comment.