summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md52
-rw-r--r--app.json3
-rw-r--r--app/(tabs)/_layout.tsx81
-rw-r--r--app/(tabs)/login.tsx16
-rw-r--r--bun.lock8
-rw-r--r--components/auth/Auth.tsx164
-rw-r--r--lib/passkey.ts71
-rw-r--r--lib/types.ts2
-rw-r--r--package.json2
9 files changed, 157 insertions, 242 deletions
diff --git a/README.md b/README.md
index 48dd63f..f3cd351 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,4 @@
-# Welcome to your Expo app 👋
+# Moses Client
-This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
-
-## Get started
-
-1. Install dependencies
-
- ```bash
- npm install
- ```
-
-2. Start the app
-
- ```bash
- npx expo start
- ```
-
-In the output, you'll find options to open the app in a
-
-- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
-- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
-- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
-- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
-
-You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
-
-## Get a fresh project
-
-When you're ready, run:
-
-```bash
-npm run reset-project
-```
-
-This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
-
-## Learn more
-
-To learn more about developing your project with Expo, look at the following resources:
-
-- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
-- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
-
-## Join the community
-
-Join our community of developers creating universal apps.
-
-- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
-- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
+React Server Components on Expo:
+https://docs.expo.dev/guides/server-components/
diff --git a/app.json b/app.json
index b4c63ed..5d4c9b0 100644
--- a/app.json
+++ b/app.json
@@ -37,7 +37,8 @@
],
"experiments": {
"typedRoutes": true,
- "reactServerFunctions": true
+ "reactServerFunctions": true,
+ "reactServerComponentRoutes": true
}
}
}
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index cfbc1e2..a102c45 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -1,45 +1,54 @@
-import { Tabs } from 'expo-router';
-import React from 'react';
-import { Platform } from 'react-native';
+import { Tabs } from "expo-router";
+import { Toaster } from "react-hot-toast";
+import React from "react";
+import { Platform } from "react-native";
-import { HapticTab } from '@/components/HapticTab';
-import { IconSymbol } from '@/components/ui/IconSymbol';
-import TabBarBackground from '@/components/ui/TabBarBackground';
-import { Colors } from '@/constants/Colors';
-import { useColorScheme } from '@/hooks/useColorScheme';
+import { HapticTab } from "@/components/HapticTab";
+import { IconSymbol } from "@/components/ui/IconSymbol";
+import TabBarBackground from "@/components/ui/TabBarBackground";
+import { Colors } from "@/constants/Colors";
+import { useColorScheme } from "@/hooks/useColorScheme";
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
- <Tabs
- screenOptions={{
- tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
- headerShown: false,
- tabBarButton: HapticTab,
- tabBarBackground: TabBarBackground,
- tabBarStyle: Platform.select({
- ios: {
- // Use a transparent background on iOS to show the blur effect
- position: 'absolute',
- },
- default: {},
- }),
- }}>
- <Tabs.Screen
- name="index"
- options={{
- title: 'Home',
- tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
+ <>
+ <Tabs
+ screenOptions={{
+ tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
+ headerShown: false,
+ tabBarButton: HapticTab,
+ tabBarBackground: TabBarBackground,
+ tabBarStyle: Platform.select({
+ ios: {
+ // Use a transparent background on iOS to show the blur effect
+ position: "absolute",
+ },
+ default: {},
+ }),
}}
- />
- <Tabs.Screen
- name="explore"
- options={{
- title: 'Explore',
- tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
- }}
- />
- </Tabs>
+ >
+ <Tabs.Screen
+ name="index"
+ options={{
+ title: "Home",
+ tabBarIcon: ({ color }) => (
+ <IconSymbol size={28} name="house.fill" color={color} />
+ ),
+ }}
+ />
+ <Tabs.Screen
+ name="explore"
+ options={{
+ title: "Explore",
+ tabBarIcon: ({ color }) => (
+ <IconSymbol size={28} name="paperplane.fill" color={color} />
+ ),
+ }}
+ />
+ </Tabs>
+ <Toaster position="top-center" />
+ </>
);
}
diff --git a/app/(tabs)/login.tsx b/app/(tabs)/login.tsx
index d09e613..c5f826d 100644
--- a/app/(tabs)/login.tsx
+++ b/app/(tabs)/login.tsx
@@ -19,7 +19,7 @@ import { Passkee } from "@/components/auth/Auth";
// import { createPasskey, isPasskeySupported } from "../lib/passkey";
// import { navigationRef } from "../lib/navigationRef";
-const PasskeySetupScreen = () => {
+const PasskeySetupScreen = async () => {
const [isLoading, setIsLoading] = useState(false);
// const isDarkMode = useSettingsStore((s) => s.isDarkMode);
const isDarkMode = false;
@@ -31,20 +31,6 @@ const PasskeySetupScreen = () => {
? require("../../assets/urbit-logo-dark.png")
: require("../../assets/urbit-logo-light.png");
- const handleCreatePasskey = async () => {
- console.log("creaing psskey");
- setIsLoading(true);
-
- try {
- // const res = await startRegistration({})
- } catch (error) {
- console.error("Passkey creation error:", error);
- Alert.alert("Error", "An error occurred while creating the passkey.");
- } finally {
- setIsLoading(false);
- }
- };
-
const handleSkip = () => {
// setHasSeenPasskeyPrompt(true);
// navigationRef.current?.navigate(ROUTES.LOGIN as never);
diff --git a/bun.lock b/bun.lock
index b2dd270..957164a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,6 +17,7 @@
"expo-image": "~2.3.2",
"expo-linking": "~7.1.7",
"expo-router": "~5.1.3",
+ "expo-secure-store": "^14.2.3",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
@@ -24,6 +25,7 @@
"expo-web-browser": "~14.2.0",
"react": "19.0.0",
"react-dom": "19.0.0",
+ "react-hot-toast": "^2.5.2",
"react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4",
@@ -881,6 +883,8 @@
"expo-router": ["expo-router@5.1.3", "", { "dependencies": { "@expo/metro-runtime": "5.0.4", "@expo/server": "^0.6.3", "@radix-ui/react-slot": "1.2.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/native": "^7.1.6", "@react-navigation/native-stack": "^7.3.10", "client-only": "^0.0.1", "invariant": "^2.2.4", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.1.6", "schema-utils": "^4.0.1", "semver": "~7.6.3", "server-only": "^0.0.1", "shallowequal": "^1.1.0" }, "peerDependencies": { "@react-navigation/drawer": "^7.3.9", "expo": "*", "expo-constants": "*", "expo-linking": "*", "react-native-reanimated": "*", "react-native-safe-area-context": "*", "react-native-screens": "*" }, "optionalPeers": ["@react-navigation/drawer", "react-native-reanimated"] }, "sha512-zoAU0clwEj569PpGOzc06wCcxOskHLEyonJhLNPsweJgu+vE010d6XW+yr5ODR6F3ViFJpfcjbe7u3SaTjl24Q=="],
+ "expo-secure-store": ["expo-secure-store@14.2.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-hYBbaAD70asKTFd/eZBKVu+9RTo9OSTMMLqXtzDF8ndUGjpc6tmRCoZtrMHlUo7qLtwL5jm+vpYVBWI8hxh/1Q=="],
+
"expo-splash-screen": ["expo-splash-screen@0.30.10", "", { "dependencies": { "@expo/prebuild-config": "^9.0.10" }, "peerDependencies": { "expo": "*" } }, "sha512-Tt9va/sLENQDQYeOQ6cdLdGvTZ644KR3YG9aRlnpcs2/beYjOX1LHT510EGzVN9ljUTg+1ebEo5GGt2arYtPjw=="],
"expo-status-bar": ["expo-status-bar@2.2.3", "", { "dependencies": { "react-native-edge-to-edge": "1.6.0", "react-native-is-edge-to-edge": "^1.1.6" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+c8R3AESBoduunxTJ8353SqKAKpxL6DvcD8VKBuh81zzJyUUbfB4CVjr1GufSJEKsMzNPXZU+HJwXx7Xh7lx8Q=="],
@@ -975,6 +979,8 @@
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
+ "goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="],
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -1407,6 +1413,8 @@
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
+ "react-hot-toast": ["react-hot-toast@2.5.2", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw=="],
+
"react-is": ["react-is@19.1.0", "", {}, "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg=="],
"react-native": ["react-native@0.79.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.79.5", "@react-native/codegen": "0.79.5", "@react-native/community-cli-plugin": "0.79.5", "@react-native/gradle-plugin": "0.79.5", "@react-native/js-polyfills": "0.79.5", "@react-native/normalize-colors": "0.79.5", "@react-native/virtualized-lists": "0.79.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.25.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.82.0", "metro-source-map": "^0.82.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.25.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.0.0", "react": "^19.0.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-jVihwsE4mWEHZ9HkO1J2eUZSwHyDByZOqthwnGrVZCh6kTQBCm4v8dicsyDa6p0fpWNE5KicTcpX/XXl0ASJFg=="],
diff --git a/components/auth/Auth.tsx b/components/auth/Auth.tsx
index 40512ca..2123774 100644
--- a/components/auth/Auth.tsx
+++ b/components/auth/Auth.tsx
@@ -1,149 +1,41 @@
"use client";
+import { authenticateWithPasskey, createPasskey } from "@/lib/passkey";
import PrimaryButton from "../PrimaryButton";
import { SymbolView, SymbolViewProps, SymbolWeight } from "expo-symbols";
import { useState } from "react";
+import toast from "react-hot-toast";
import { Platform, StyleProp, ViewStyle } from "react-native";
-const PASSKEY_CREDENTIAL_ID_KEY = "urbit_wallet_passkey_id";
-const RELYING_PARTY_ID = "wallet.urbit.org"; // Change this to your domain
-const RELYING_PARTY_NAME = "Urbit Wallet";
-
export function Passkee() {
const [isLoading, setIsLoading] = useState(false);
- async function handleCreatePasskey() {
- const pkok =
- await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
- console.log({ pkok });
-
- // if (Platform.OS !== "web") {
- // return false;
- // }
- console.log("creating passkey");
-
- try {
- // Generate a random user ID
- const userId = new Uint8Array(16);
- crypto.getRandomValues(userId);
-
- // Generate a random challenge
- const challenge = new Uint8Array(32);
- crypto.getRandomValues(challenge);
-
- const createCredentialOptions: CredentialCreationOptions = {
- publicKey: {
- // REQUIRED: Random challenge to prevent replay attacks
- // Must be at least 16 bytes, we use 32 for extra security
- challenge,
-
- // REQUIRED: Relying Party (your website/app)
- rp: {
- // REQUIRED: Human-readable name shown in UI prompts
- name: RELYING_PARTY_NAME,
-
- // OPTIONAL: Domain that owns this credential
- // If omitted, defaults to current domain
- // Must match or be a registrable suffix of the current domain
- id:
- window.location.hostname === "localhost"
- ? "localhost"
- : RELYING_PARTY_ID,
- },
-
- // REQUIRED: User information
- user: {
- // REQUIRED: Unique user ID (must not contain PII)
- // Used by authenticator to distinguish between accounts
- id: userId,
-
- // REQUIRED: Unique username/identifier for this RP
- // Shown in some UI contexts, should be recognizable to user
- name: "urbit-user",
-
- // REQUIRED: Human-readable name for display
- // Shown in account selectors and prompts
- displayName: "Urbit Wallet User",
- },
-
- // REQUIRED: List of acceptable public key algorithms
- // Order matters - authenticator will use first supported algorithm
- pubKeyCredParams: [
- { alg: -7, type: "public-key" }, // ES256 (ECDSA with SHA-256) - most common
- { alg: -257, type: "public-key" }, // RS256 (RSASSA-PKCS1-v1_5) - fallback
- ],
-
- // OPTIONAL: Criteria for authenticator selection
- authenticatorSelection: {
- // OPTIONAL: "platform" (built-in like Touch ID) or "cross-platform" (USB key)
- // Omitting allows both types
- // authenticatorAttachment: "platform",
-
- // OPTIONAL: User verification requirement
- // "required" - must verify user (biometric/PIN)
- // "preferred" - verify if possible, continue if not (default)
- // "discouraged" - don't verify unless required by RP
- userVerification: "preferred",
-
- // OPTIONAL: Whether to create a discoverable credential (passkey)
- // "required" - must create discoverable credential
- // "preferred" - create if possible (default)
- // "discouraged" - don't create discoverable credential
- residentKey: "preferred",
-
- // DEPRECATED: Use residentKey instead
- // Kept for backwards compatibility with older authenticators
- requireResidentKey: false,
- },
-
- // OPTIONAL: Time limit in milliseconds (default varies by browser)
- // 60 seconds is reasonable for user interaction
- timeout: 60000,
-
- // OPTIONAL: Attestation preference (proof of authenticator legitimacy)
- // "none" - no attestation needed (default, best for privacy)
- // "indirect" - RP prefers attestation but allows anonymization
- // "direct" - RP needs attestation from authenticator
- // "enterprise" - RP needs attestation and device info (requires permission)
- attestation: "none",
-
- // OPTIONAL: List of credentials to exclude (prevent duplicates)
- // excludeCredentials: [
- // { id: existingCredentialId, type: "public-key" }
- // ],
-
- // OPTIONAL: Extensions for additional features
- // extensions: {
- // credProps: true, // Request additional credential properties
- // hmacCreateSecret: true, // For symmetric key operations
- // },
- },
- };
-
- const credential = (await navigator.credentials.create(
- createCredentialOptions,
- )) as PublicKeyCredential;
- console.log({ credential });
-
- if (credential) {
- // Store the credential ID for later use
- // await SecureStore.setItemAsync(PASSKEY_CREDENTIAL_ID_KEY, credential.id);
-
- console.log("Passkey created successfully", credential.id);
- return true;
- }
-
- return false;
- } catch (error) {
- console.error("Error creating passkey:", error);
- return false;
- }
+ async function handleCreate() {
+ //
+ const ok = await createPasskey();
+ if (!ok) toast.error("Error generating passkey");
+ else toast.success("Passkey generated!");
+ }
+ async function handleCheck() {
+ //
+ const ok = await authenticateWithPasskey();
+ if (!ok) toast.error("Can't login");
+ else toast.success("Passkey auth successful");
}
+
return (
- <PrimaryButton
- label="Create Passkey"
- onPress={handleCreatePasskey}
- isLoading={isLoading}
- style={{ marginBottom: 16 }}
- />
+ <>
+ <PrimaryButton
+ label="Create Passkey"
+ onPress={handleCreate}
+ isLoading={isLoading}
+ style={{ marginBottom: 16 }}
+ />
+ <PrimaryButton
+ label="Check Passkey"
+ onPress={handleCheck}
+ isLoading={isLoading}
+ style={{ marginBottom: 16 }}
+ />
+ </>
);
}
diff --git a/lib/passkey.ts b/lib/passkey.ts
index 573c62b..81a21a5 100644
--- a/lib/passkey.ts
+++ b/lib/passkey.ts
@@ -1,8 +1,14 @@
+"use client";
+
+import { Platform } from "react-native";
+import * as SecureStore from "expo-secure-store";
+import type { AsyncRes } from "./types";
+
const PASSKEY_CREDENTIAL_ID_KEY = "urbit_wallet_passkey_id";
const RELYING_PARTY_ID = "wallet.urbit.org"; // Change this to your domain
const RELYING_PARTY_NAME = "Urbit Wallet";
-async function createPasskey() {
+export async function createPasskey(): AsyncRes<PublicKeyCredential> {
const pkok =
await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
console.log({ pkok });
@@ -117,15 +123,70 @@ async function createPasskey() {
if (credential) {
// Store the credential ID for later use
- // await SecureStore.setItemAsync(PASSKEY_CREDENTIAL_ID_KEY, credential.id);
+ await SecureStore.setItemAsync(PASSKEY_CREDENTIAL_ID_KEY, credential.id);
console.log("Passkey created successfully", credential.id);
- return true;
+ return { ok: credential };
}
- return false;
+ return { error: "Failed to create passkey" };
} catch (error) {
console.error("Error creating passkey:", error);
- return false;
+ return { error: `${error}` };
+ }
+}
+
+export async function authenticateWithPasskey(): AsyncRes<PublicKeyCredential> {
+ // if (Platform.OS !== "web") {
+ // return false;
+ // }
+
+ try {
+ const credentialId = await SecureStore.getItemAsync(
+ PASSKEY_CREDENTIAL_ID_KEY,
+ );
+
+ if (!credentialId) {
+ console.log("No passkey found");
+ return { error: "No passkey found" };
+ }
+
+ // Generate a random challenge
+ const challenge = new Uint8Array(32);
+ crypto.getRandomValues(challenge);
+
+ const getCredentialOptions: CredentialRequestOptions = {
+ publicKey: {
+ challenge,
+ rpId:
+ window.location.hostname === "localhost"
+ ? "localhost"
+ : RELYING_PARTY_ID,
+ allowCredentials: [
+ {
+ id: Uint8Array.from(atob(credentialId), (c) => c.charCodeAt(0)),
+ type: "public-key",
+ transports: ["internal", "hybrid"] as AuthenticatorTransport[],
+ },
+ ],
+ userVerification: "preferred",
+ timeout: 60000,
+ },
+ };
+
+ const credential = (await navigator.credentials.get(
+ getCredentialOptions,
+ )) as PublicKeyCredential;
+
+ if (credential) {
+ // In a production app, you would send the assertion to your server
+ // to verify it. For this demo, we're just checking if we got a credential.
+ console.log("Passkey authentication successful");
+ return { ok: credential };
+ }
+
+ return { error: "failed to generate creds" };
+ } catch (error) {
+ return { error: `${error}` };
}
}
diff --git a/lib/types.ts b/lib/types.ts
new file mode 100644
index 0000000..a22e8be
--- /dev/null
+++ b/lib/types.ts
@@ -0,0 +1,2 @@
+export type Result<T> = { ok: T } | { error: string };
+export type AsyncRes<T> = Promise<Result<T>>;
diff --git a/package.json b/package.json
index 5fed9f5..a2c0110 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"expo-image": "~2.3.2",
"expo-linking": "~7.1.7",
"expo-router": "~5.1.3",
+ "expo-secure-store": "^14.2.3",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
@@ -31,6 +32,7 @@
"expo-web-browser": "~14.2.0",
"react": "19.0.0",
"react-dom": "19.0.0",
+ "react-hot-toast": "^2.5.2",
"react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4",