From e2e14e414de25904d791b503d2852c68b3ac9415 Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 16 Jul 2025 07:55:57 +0700 Subject: m --- components/PrimaryButton.tsx | 102 +++++++++++++++++++++++++++++ components/ScreenWrapper.tsx | 31 +++++++++ components/auth/Auth.tsx | 149 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 components/PrimaryButton.tsx create mode 100644 components/ScreenWrapper.tsx create mode 100644 components/auth/Auth.tsx (limited to 'components') diff --git a/components/PrimaryButton.tsx b/components/PrimaryButton.tsx new file mode 100644 index 0000000..94bd8ce --- /dev/null +++ b/components/PrimaryButton.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { + TouchableOpacity, + Text, + StyleSheet, + ActivityIndicator, + ViewStyle, + View, + TextStyle, +} from "react-native"; +import { Feather } from "@expo/vector-icons"; +import { useThemeColors, ColorScheme } from "../constants"; + +interface SendButtonProps { + onPress: () => void; + isLoading?: boolean; + disabled?: boolean; + label?: string; + style?: ViewStyle; + textStyle?: TextStyle; +} + +const SendButton: React.FC = ({ + onPress, + isLoading = false, + disabled = false, + label = "Send", + style, +}) => { + const isButtonDisabled = isLoading || disabled; + const colors = useThemeColors(); + + return ( + + {isLoading ? ( + + + + ) : ( + <> + + {label} + + + + )} + + ); +}; + +const styles = StyleSheet.create({ + sendButton: { + // paddingVertical: 10, + paddingHorizontal: 16, + borderRadius: 8, + flexDirection: "row", + justifyContent: "flex-start", + alignItems: "center", + gap: 8, + height: 48, + }, + sendText: { + fontSize: 16, + fontWeight: 600, + flex: 1, + }, + disabledButton: { + backgroundColor: "#4B5563", + }, + disabledText: { + color: "#eee", + }, +}); + +export default SendButton; diff --git a/components/ScreenWrapper.tsx b/components/ScreenWrapper.tsx new file mode 100644 index 0000000..49c698f --- /dev/null +++ b/components/ScreenWrapper.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { View, StyleSheet, Platform } from "react-native"; + +type Props = { + children: React.ReactNode; + style?: object; +}; + +const ScreenWrapper = ({ children, style }: Props) => { + return ( + + {children} + + ); +}; + +const styles = StyleSheet.create({ + outer: { + flex: 1, + alignItems: "center", + justifyContent: "flex-start", + }, + inner: { + width: "100%", + + maxWidth: Platform.OS === "web" ? 420 : "100%", + flexGrow: 1, + }, +}); + +export default ScreenWrapper; diff --git a/components/auth/Auth.tsx b/components/auth/Auth.tsx new file mode 100644 index 0000000..40512ca --- /dev/null +++ b/components/auth/Auth.tsx @@ -0,0 +1,149 @@ +"use client"; + +import PrimaryButton from "../PrimaryButton"; +import { SymbolView, SymbolViewProps, SymbolWeight } from "expo-symbols"; +import { useState } from "react"; +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; + } + } + return ( + + ); +} -- cgit v1.2.3