diff --git a/.github/scripts/generateCodeExamples.js b/.github/scripts/generateCodeExamples.ts similarity index 83% rename from .github/scripts/generateCodeExamples.js rename to .github/scripts/generateCodeExamples.ts index d3bbbf04..ef314a5b 100644 --- a/.github/scripts/generateCodeExamples.js +++ b/.github/scripts/generateCodeExamples.ts @@ -1,5 +1,13 @@ const fs = require('fs') +type Repo = { + organization: string + repo: string + destination: string + branch: string + files: string[] +} + const repos = [ { organization: '5afe', @@ -35,6 +43,20 @@ const repos = [ '/layouts/default.vue', '/nuxt.config.ts' ] + }, + { + organization: '5afe', + repo: 'react-native-passkeys-tutorial', + destination: './examples/react-native-passkeys', + branch: 'main', + files: [ + '/lib/passkeys.ts', + '/lib/safe.ts', + '/lib/storage.ts', + '/App.tsx', + '/app.json', + '/.env-sample' + ] } // { // organization: '5afe', @@ -59,7 +81,7 @@ const generateCodeExamples = async ({ branch, destination, files -}) => { +}: Repo) => { const fetch = await import('node-fetch') files.forEach(async filePath => { const url = `https://raw.githubusercontent.com/${organization}/${repo}/${branch}${filePath}?token=$(date +%s)` diff --git a/.github/styles/config/vocabularies/default/accept.txt b/.github/styles/config/vocabularies/default/accept.txt index c30eb78f..aa9a6814 100644 --- a/.github/styles/config/vocabularies/default/accept.txt +++ b/.github/styles/config/vocabularies/default/accept.txt @@ -60,7 +60,8 @@ [Tt]estnet [Tt]rezor [Vv]alidator -[Ww]hitepaper +[Ww]hitepaper +[Xx]code A1 AA ABI @@ -150,6 +151,7 @@ Katla Klaytn Kovan Kroma +keystore LUKSO Lightlink Lisk @@ -167,6 +169,7 @@ Moonbeam Moonriver Mordor Nova +ngrok Nuxt OAuth OP @@ -178,6 +181,7 @@ PKCE Polis Protocol Kit Starter Kit +Prebuild Protofire PublicMint README @@ -282,6 +286,7 @@ trace_filter trace_transaction undefined undeployed +unencrypted v1 viem zkLink diff --git a/assets/react-native-passkeys-app-1.png b/assets/react-native-passkeys-app-1.png new file mode 100644 index 00000000..5cba52fc Binary files /dev/null and b/assets/react-native-passkeys-app-1.png differ diff --git a/assets/react-native-passkeys-app-2.png b/assets/react-native-passkeys-app-2.png new file mode 100644 index 00000000..788f8854 Binary files /dev/null and b/assets/react-native-passkeys-app-2.png differ diff --git a/assets/react-native-passkeys-app-3.png b/assets/react-native-passkeys-app-3.png new file mode 100644 index 00000000..d42a7e99 Binary files /dev/null and b/assets/react-native-passkeys-app-3.png differ diff --git a/assets/react-native-passkeys-app.gif b/assets/react-native-passkeys-app.gif new file mode 100644 index 00000000..73bb27fb Binary files /dev/null and b/assets/react-native-passkeys-app.gif differ diff --git a/assets/react-native-passkeys-play-store.png b/assets/react-native-passkeys-play-store.png new file mode 100644 index 00000000..4ddf0551 Binary files /dev/null and b/assets/react-native-passkeys-play-store.png differ diff --git a/examples/react-native-passkeys/.env-sample b/examples/react-native-passkeys/.env-sample new file mode 100644 index 00000000..842f39a7 --- /dev/null +++ b/examples/react-native-passkeys/.env-sample @@ -0,0 +1,14 @@ +# Fill this information to configure your Safe account +# --------------------------------------------------- +# The private key of the Safe account owner that will be used to deploy the Safe or execute transactions. Should have some test Sepolia ETH. +EXPO_PUBLIC_SAFE_SIGNER_PK=add_private_key_here +# Add the Safe account owners. You can one or more public addresses that you own. +EXPO_PUBLIC_SAFE_OWNERS=["0xOwnerAddress1", "..."] +# Safe salt nonce. A random number that is used to generate to Safe account address. When you change the number a new Safe will be predicted. +# This is useful to start testing the app with a new Safe account. +EXPO_PUBLIC_SAFE_SALT_NONCE=1 + +# Others +# --------------------------------------------------- +# RPC URL for the network where the Safe is deployed +EXPO_PUBLIC_RPC_URL=https://eth-sepolia.public.blastapi.io diff --git a/examples/react-native-passkeys/App.tsx b/examples/react-native-passkeys/App.tsx new file mode 100644 index 00000000..e85c457d --- /dev/null +++ b/examples/react-native-passkeys/App.tsx @@ -0,0 +1,339 @@ +import { useEffect, useState } from "react"; +import prompt from "react-native-prompt-android"; +import Safe, { PasskeyArgType } from "@safe-global/protocol-kit"; +import { + View, + Text, + StyleSheet, + Button, + Platform, + Alert, + ActivityIndicator, + SafeAreaView, +} from "react-native"; +import { + getStoredPassKey, + removeStoredPassKey, + storePassKey, +} from "./lib/storage"; +import { createPassKey, getPassKey } from "./lib/passkeys"; +import { + activateAccount, + addPasskeyOwner, + sendDummyPasskeyTransaction, + signPasskeyMessage, +} from "./lib/safe"; + +const PASSKEY_NAME = "safe-owner"; + +export default function App() { + const [protocolKit, setProtocolKit] = useState(null); + const [passkeySignerProtocolKit, setPasskeySignerProtocolKit] = + useState(null); + const [passkeySigner, setPasskeySigner] = useState( + null + ); + const [safeAddress, setSafeAddress] = useState(null); + const [isDeployed, setIsDeployed] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + (async () => { + let protocolKitInstance = await Safe.init({ + provider: process.env.EXPO_PUBLIC_RPC_URL as string, + signer: process.env.EXPO_PUBLIC_SAFE_SIGNER_PK, + predictedSafe: { + safeAccountConfig: { + owners: JSON.parse(process.env.EXPO_PUBLIC_SAFE_OWNERS as string), + threshold: 1, + }, + safeDeploymentConfig: { + saltNonce: process.env.EXPO_PUBLIC_SAFE_SALT_NONCE, + }, + }, + }); + + const safeAddress = await protocolKitInstance.getAddress(); + const isDeployed = await protocolKitInstance.isSafeDeployed(); + + console.log("Safe address", safeAddress); + console.log("Is deployed", isDeployed); + + setSafeAddress(safeAddress); + setIsDeployed(isDeployed); + + if (isDeployed) { + protocolKitInstance = await protocolKitInstance.connect({ + provider: process.env.EXPO_PUBLIC_RPC_URL, + signer: process.env.EXPO_PUBLIC_SAFE_SIGNER_PK, + safeAddress: safeAddress, + }); + } + + setProtocolKit(protocolKitInstance); + setIsLoading(false); + })(); + }, []); + + useEffect(() => { + (async () => { + const storedPasskey = await getStoredPassKey(PASSKEY_NAME); + + setPasskeySigner(storedPasskey); + })(); + }, []); + + useEffect(() => { + if (!passkeySigner || !safeAddress) return; + + (async () => { + const passkeySignerProtocolKitInstance = await Safe.init({ + provider: process.env.EXPO_PUBLIC_RPC_URL, + signer: { ...passkeySigner, getFn: getPassKey } as PasskeyArgType, + safeAddress, + }); + + setPasskeySignerProtocolKit(passkeySignerProtocolKitInstance); + })(); + }, [safeAddress, passkeySigner]); + + const handleActivateAccount = async () => { + if (!protocolKit || !safeAddress) return; + + setIsLoading(true); + + const receipt = await activateAccount(protocolKit); + + if (receipt.transactionHash) { + setIsDeployed(true); + + const updatedProtocolKitInstance = await protocolKit.connect({ + provider: protocolKit.getSafeProvider().provider, + signer: protocolKit.getSafeProvider().signer, + safeAddress: await protocolKit.getAddress(), + }); + + setProtocolKit(updatedProtocolKitInstance); + + setIsLoading(false); + } else { + setIsLoading(false); + } + }; + + const handleAddPasskeyOwner = async () => { + if (!protocolKit) { + return; + } + + const passkeyCredential = await createPassKey(); + + if (!passkeyCredential) { + throw Error("Passkey creation failed: No credential was returned."); + } + + const signer = await Safe.createPasskeySigner(passkeyCredential); + + setIsLoading(true); + + await addPasskeyOwner(protocolKit, signer); + + await storePassKey(signer, PASSKEY_NAME); + + const passkeySignerProtocolKitInstance = await Safe.init({ + provider: process.env.EXPO_PUBLIC_RPC_URL, + signer: { ...signer, getFn: getPassKey } as PasskeyArgType, + safeAddress: safeAddress as string, + }); + + setPasskeySignerProtocolKit(passkeySignerProtocolKitInstance); + setPasskeySigner(signer); + setIsLoading(false); + }; + + const handleSignMessage = async () => { + if (!passkeySignerProtocolKit) return; + + prompt( + "Sign message", + "Enter the message to sign", + [ + { + text: "Cancel", + onPress: () => console.log("Cancel Pressed"), + style: "cancel", + }, + { + text: "Sign", + onPress: async (message: string) => { + const signedMessage = await signPasskeyMessage( + passkeySignerProtocolKit, + message + ); + + if (Platform.OS === "web") { + window.alert( + (signedMessage.data as string) + + "\n" + + signedMessage.encodedSignatures() + ); + } else { + Alert.alert( + signedMessage.data as string, + signedMessage.encodedSignatures() + ); + } + }, + }, + ], + { + type: "plain-text", + cancelable: false, + defaultValue: "", + placeholder: "placeholder", + style: "shimo", + } + ); + }; + + const handleSendTransaction = async () => { + if (!safeAddress || !protocolKit || !passkeySignerProtocolKit) return; + + setIsLoading(true); + + const receipt = await sendDummyPasskeyTransaction( + protocolKit, + passkeySignerProtocolKit, + safeAddress + ); + + setIsLoading(false); + + if (receipt.transactionHash) { + if (Platform.OS === "web") { + window.alert(receipt.transactionHash); + } else { + Alert.alert("Transaction hash", receipt.transactionHash); + } + } + }; + + const handleRemovePasskey = async () => { + removeStoredPassKey(PASSKEY_NAME); + setPasskeySigner(null); + }; + + if (isLoading) { + return ; + } + + return ( + + + Safe Passkeys Demo + + + + Safe Address + {safeAddress} + + + {!isDeployed && ( + + ⚠️ The account is not activated yet + +