Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add OTP authentication capabilities to RN Signer #1231

Merged
merged 4 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@ -53,14 +55,14 @@
useEffect(() => {
// get the user if already logged in
signer.getAuthDetails().then(setUser);
}, []);

Check warning on line 58 in account-kit/rn-signer/example/src/screens/magic-link-auth.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'signer'. Either include it or remove the dependency array

// Add listener for incoming links
useEffect(() => {
const subscription = Linking.addEventListener("url", handleIncomingURL);

return () => subscription.remove();
}, []);

Check warning on line 65 in account-kit/rn-signer/example/src/screens/magic-link-auth.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'handleIncomingURL'. Either include it or remove the dependency array

return (
<View style={styles.container}>
Expand All @@ -73,12 +75,16 @@
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);
}, []);

Check warning on line 43 in account-kit/rn-signer/example/src/screens/otp-auth.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'signer'. Either include it or remove the dependency array
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <react-hooks/exhaustive-deps> reported by reviewdog 🐶
React Hook useEffect has a missing dependency: 'signer'. Either include it or remove the dependency array.


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
Loading