├── .expo-shared └── assets.json ├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── components ├── AddReviewSheet.tsx ├── Button.tsx ├── Input.tsx ├── MovieCoordinator.ts ├── MovieList.tsx └── ToastConfig.tsx ├── constants └── index.ts ├── metro.config.js ├── models └── Movie.ts ├── package.json ├── tsconfig.json ├── utils ├── buildUrl.ts ├── decryptPayload.ts ├── encryptPayload.ts └── usePrevious.ts ├── yarn-error.log └── yarn.lock /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import "react-native-get-random-values"; 2 | import "react-native-url-polyfill/auto"; 3 | import { Buffer } from "buffer"; 4 | global.Buffer = global.Buffer || Buffer; 5 | import { StatusBar } from "expo-status-bar"; 6 | import React, { useEffect, useRef, useState } from "react"; 7 | import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; 8 | import * as Linking from "expo-linking"; 9 | import nacl from "tweetnacl"; 10 | import bs58 from "bs58"; 11 | import { 12 | clusterApiUrl, 13 | Connection, 14 | PublicKey, 15 | Transaction, 16 | } from "@solana/web3.js"; 17 | import { decryptPayload } from "./utils/decryptPayload"; 18 | import { encryptPayload } from "./utils/encryptPayload"; 19 | import { buildUrl } from "./utils/buildUrl"; 20 | import MovieList from "./components/MovieList"; 21 | import Button from "./components/Button"; 22 | import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; 23 | import ActionSheet from "react-native-actions-sheet"; 24 | import AddReviewSheet from "./components/AddReviewSheet"; 25 | import Toast from "react-native-toast-message"; 26 | import { toastConfig } from "./components/ToastConfig"; 27 | import { COLORS } from "./constants"; 28 | 29 | const onConnectRedirectLink = Linking.createURL("onConnect"); 30 | const onDisconnectRedirectLink = Linking.createURL("onDisconnect"); 31 | const onSignAndSendTransactionRedirectLink = Linking.createURL( 32 | "onSignAndSendTransaction" 33 | ); 34 | 35 | const connection = new Connection(clusterApiUrl("devnet")); 36 | 37 | export default function App() { 38 | const [deeplink, setDeepLink] = useState(""); 39 | const [dappKeyPair] = useState(nacl.box.keyPair()); 40 | 41 | const [sharedSecret, setSharedSecret] = useState(); 42 | const [session, setSession] = useState(); 43 | const [phantomWalletPublicKey, setPhantomWalletPublicKey] = 44 | useState(null); 45 | 46 | const [submitting, setSubmitting] = useState(false); 47 | 48 | const actionSheetRef = useRef(null); 49 | 50 | // Initialize our app's deeplinking protocol on app start-up 51 | useEffect(() => { 52 | const initializeDeeplinks = async () => { 53 | const initialUrl = await Linking.getInitialURL(); 54 | if (initialUrl) { 55 | setDeepLink(initialUrl); 56 | } 57 | }; 58 | initializeDeeplinks(); 59 | const listener = Linking.addEventListener("url", handleDeepLink); 60 | return () => { 61 | listener.remove(); 62 | }; 63 | }, []); 64 | 65 | const handleDeepLink = ({ url }: Linking.EventType) => setDeepLink(url); 66 | 67 | // Handle in-bound links 68 | useEffect(() => { 69 | setSubmitting(false); 70 | if (!deeplink) return; 71 | 72 | const url = new URL(deeplink); 73 | const params = url.searchParams; 74 | 75 | // Handle an error response from Phantom 76 | if (params.get("errorCode")) { 77 | const error = Object.fromEntries([...params]); 78 | const message = 79 | error?.errorMessage ?? 80 | JSON.stringify(Object.fromEntries([...params]), null, 2); 81 | Toast.show({ 82 | type: "error", 83 | text1: "Error", 84 | text2: message, 85 | }); 86 | console.log("error: ", message); 87 | return; 88 | } 89 | 90 | // Handle a `connect` response from Phantom 91 | if (/onConnect/.test(url.pathname)) { 92 | const sharedSecretDapp = nacl.box.before( 93 | bs58.decode(params.get("phantom_encryption_public_key")!), 94 | dappKeyPair.secretKey 95 | ); 96 | const connectData = decryptPayload( 97 | params.get("data")!, 98 | params.get("nonce")!, 99 | sharedSecretDapp 100 | ); 101 | setSharedSecret(sharedSecretDapp); 102 | setSession(connectData.session); 103 | setPhantomWalletPublicKey(new PublicKey(connectData.public_key)); 104 | console.log(`connected to ${connectData.public_key.toString()}`); 105 | } 106 | 107 | // Handle a `disconnect` response from Phantom 108 | if (/onDisconnect/.test(url.pathname)) { 109 | setPhantomWalletPublicKey(null); 110 | console.log("disconnected"); 111 | } 112 | 113 | // Handle a `signAndSendTransaction` response from Phantom 114 | if (/onSignAndSendTransaction/.test(url.pathname)) { 115 | actionSheetRef.current?.hide(); 116 | const signAndSendTransactionData = decryptPayload( 117 | params.get("data")!, 118 | params.get("nonce")!, 119 | sharedSecret 120 | ); 121 | console.log("signAndSendTrasaction: ", signAndSendTransactionData); 122 | Toast.show({ 123 | type: "success", 124 | text1: "Review submitted 🎥", 125 | text2: signAndSendTransactionData.signature, 126 | }); 127 | } 128 | }, [deeplink]); 129 | 130 | // Initiate a new connection to Phantom 131 | const connect = async () => { 132 | const params = new URLSearchParams({ 133 | dapp_encryption_public_key: bs58.encode(dappKeyPair.publicKey), 134 | cluster: "devnet", 135 | app_url: "https://deeplink-movie-tutorial-dummy-site.vercel.app/", 136 | redirect_link: onConnectRedirectLink, 137 | }); 138 | const url = buildUrl("connect", params); 139 | Linking.openURL(url); 140 | }; 141 | 142 | // Initiate a disconnect from Phantom 143 | const disconnect = async () => { 144 | const payload = { 145 | session, 146 | }; 147 | const [nonce, encryptedPayload] = encryptPayload(payload, sharedSecret); 148 | const params = new URLSearchParams({ 149 | dapp_encryption_public_key: bs58.encode(dappKeyPair.publicKey), 150 | nonce: bs58.encode(nonce), 151 | redirect_link: onDisconnectRedirectLink, 152 | payload: bs58.encode(encryptedPayload), 153 | }); 154 | const url = buildUrl("disconnect", params); 155 | Linking.openURL(url); 156 | }; 157 | 158 | // Initiate a new transaction via Phantom. We call this in `components/AddReviewSheet.tsx` to send our movie review to the Solana network 159 | const signAndSendTransaction = async (transaction: Transaction) => { 160 | if (!phantomWalletPublicKey) return; 161 | setSubmitting(true); 162 | transaction.feePayer = phantomWalletPublicKey; 163 | transaction.recentBlockhash = ( 164 | await connection.getLatestBlockhash() 165 | ).blockhash; 166 | const serializedTransaction = transaction.serialize({ 167 | requireAllSignatures: false, 168 | }); 169 | const payload = { 170 | session, 171 | transaction: bs58.encode(serializedTransaction), 172 | }; 173 | const [nonce, encryptedPayload] = encryptPayload(payload, sharedSecret); 174 | const params = new URLSearchParams({ 175 | dapp_encryption_public_key: bs58.encode(dappKeyPair.publicKey), 176 | nonce: bs58.encode(nonce), 177 | redirect_link: onSignAndSendTransactionRedirectLink, 178 | payload: bs58.encode(encryptedPayload), 179 | }); 180 | const url = buildUrl("signAndSendTransaction", params); 181 | Linking.openURL(url); 182 | }; 183 | 184 | // Open the 'Add a Review' sheet defined in `components/AddReviewSheet.tsx` 185 | const openAddReviewSheet = () => { 186 | actionSheetRef.current?.show(); 187 | }; 188 | 189 | return ( 190 | 191 | 192 | 193 | {phantomWalletPublicKey ? ( 194 | <> 195 | 196 | 197 | 202 | {`Connected to: ${phantomWalletPublicKey.toString()}`} 203 | 204 | 205 | 206 |