diff options
author | polwex <polwex@sortug.com> | 2025-07-16 07:55:57 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-07-16 07:55:57 +0700 |
commit | e2e14e414de25904d791b503d2852c68b3ac9415 (patch) | |
tree | 051b3b0dbaa6252d9a1687d29b401d079dbafb6b /components | |
parent | a528bd94a6e8e25010ae26a305550b211df0ddc6 (diff) |
Diffstat (limited to 'components')
-rw-r--r-- | components/PrimaryButton.tsx | 102 | ||||
-rw-r--r-- | components/ScreenWrapper.tsx | 31 | ||||
-rw-r--r-- | components/auth/Auth.tsx | 149 |
3 files changed, 282 insertions, 0 deletions
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<SendButtonProps> = ({ + onPress, + isLoading = false, + disabled = false, + label = "Send", + style, +}) => { + const isButtonDisabled = isLoading || disabled; + const colors = useThemeColors(); + + return ( + <TouchableOpacity + style={[ + styles.sendButton, + { backgroundColor: colors.button }, + isButtonDisabled && styles.disabledButton, + style, + ]} + onPress={onPress} + disabled={isButtonDisabled} + > + {isLoading ? ( + <View + style={{ + flexDirection: "row", + alignItems: "center", + width: "100%", + flex: 1, + justifyContent: "center", + }} + > + <ActivityIndicator color="#fff" /> + </View> + ) : ( + <> + <Text + style={[ + styles.sendText, + isButtonDisabled && styles.disabledText, + { color: colors.buttonText }, + ]} + > + {label} + </Text> + <Feather + name="arrow-right" + size={16} + color={isButtonDisabled ? colors.border : colors.buttonText} + /> + </> + )} + </TouchableOpacity> + ); +}; + +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 ( + <View style={styles.outer}> + <View style={[styles.inner, style]}>{children}</View> + </View> + ); +}; + +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 ( + <PrimaryButton + label="Create Passkey" + onPress={handleCreatePasskey} + isLoading={isLoading} + style={{ marginBottom: 16 }} + /> + ); +} |