├── global.js ├── common ├── index.ts └── constants.ts ├── assets ├── images │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ ├── adaptive-icon.png │ └── build-a-solana-wallet-app.png └── fonts │ └── SpaceMono-Regular.ttf ├── tsconfig.json ├── tailwind.config.js ├── babel.config.js ├── .gitignore ├── metro.config.js ├── lib └── tailwind.js ├── .expo-shared └── assets.json ├── components ├── __tests__ │ └── StyledText-test.js ├── Separator.tsx ├── index.ts ├── Container.tsx ├── List.tsx ├── TextInput.tsx ├── Gradient.tsx ├── IconButton.tsx ├── Wrapper.tsx ├── Column.tsx ├── Header.tsx ├── Block.tsx ├── ListItem.tsx ├── Button.tsx └── Text.tsx ├── global.native.js ├── storage.js ├── eas.json ├── hooks ├── storeHooks.ts └── useCachedResources.ts ├── screens ├── index.tsx ├── ModalScreen.tsx ├── NotFoundScreen.tsx ├── SettingsScreen.tsx ├── ActivityScreen.tsx ├── OnboardingScreen.tsx ├── SwapScreen.tsx ├── CollectiblesScreen.tsx └── DashboardScreen.tsx ├── app.json ├── App.tsx ├── types.tsx ├── navigation ├── LinkingConfiguration.ts └── index.tsx ├── api └── index.ts ├── store └── index.ts ├── utils └── index.ts ├── package.json └── README.md /global.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /common/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants"; 2 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moviendome/solacademy-wallet/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moviendome/solacademy-wallet/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moviendome/solacademy-wallet/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moviendome/solacademy-wallet/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moviendome/solacademy-wallet/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'] 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /assets/images/build-a-solana-wallet-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moviendome/solacademy-wallet/HEAD/assets/images/build-a-solana-wallet-app.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resolver: { 3 | sourceExts: ["jsx", "js", "ts", "tsx", "cjs"], 4 | extraNodeModules: { 5 | stream: require.resolve("readable-stream"), 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /lib/tailwind.js: -------------------------------------------------------------------------------- 1 | import {create} from 'twrnc'; 2 | 3 | // create the customized version... 4 | const tw = create(require(`../tailwind.config.js`)); // <- your path may differ 5 | 6 | // ... and then this becomes the main function your app uses 7 | export default tw; 8 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true, 3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true, 4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 6 | } 7 | -------------------------------------------------------------------------------- /components/__tests__/StyledText-test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import { MonoText } from '../StyledText'; 5 | 6 | it(`renders correctly`, () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /common/constants.ts: -------------------------------------------------------------------------------- 1 | import {Dimensions, Platform} from 'react-native'; 2 | 3 | export const C = { 4 | PLATFORM: (Platform.OS === 'web') ? 'web' : 'native', 5 | 6 | IS_MOBILE: Dimensions.get('window').width < 500, 7 | 8 | THEME: { 9 | GRADIENT: ['#14F195', '#9945FF', '#FFFFFF'], 10 | PRIMARY: '#9945FF', 11 | SECONDARY: '#14F195', 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /global.native.js: -------------------------------------------------------------------------------- 1 | // Inject node globals into React Native global scope. 2 | 3 | global.Buffer = require("buffer").Buffer; 4 | global.Buffer.TYPED_ARRAY_SUPPORT = false; 5 | 6 | global.process = require("process"); 7 | global.process.env.NODE_ENV = __DEV__ ? "development" : "production"; 8 | 9 | // Needed so that 'stream-http' chooses the right default protocol. 10 | global.location = { 11 | protocol: "file:", 12 | }; 13 | -------------------------------------------------------------------------------- /storage.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage"; 2 | 3 | const storage = { 4 | async getItem(key) { 5 | return JSON.parse(await AsyncStorage.getItem(key)); 6 | }, 7 | async setItem(key, data) { 8 | AsyncStorage.setItem(key, JSON.stringify(data)); 9 | }, 10 | async removeItem(key) { 11 | AsyncStorage.removeItem(key); 12 | }, 13 | }; 14 | 15 | export default storage; 16 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 0.48.2" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal" 9 | }, 10 | "preview": { 11 | "distribution": "internal", 12 | "android": { 13 | "buildType": "apk" 14 | } 15 | }, 16 | "production": {} 17 | }, 18 | "submit": { 19 | "production": {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/Separator.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | 3 | import tw from '../lib/tailwind'; 4 | import {useDeviceContext} from 'twrnc'; 5 | 6 | const Separator = () => { 7 | useDeviceContext(tw); 8 | 9 | return ; 10 | }; 11 | 12 | const styles = StyleSheet.create({ 13 | separator: { 14 | ...tw`w-full border-b border-gray-200`, 15 | }, 16 | }); 17 | 18 | export default Separator; 19 | -------------------------------------------------------------------------------- /hooks/storeHooks.ts: -------------------------------------------------------------------------------- 1 | import { createTypedHooks } from "easy-peasy"; 2 | import { WalletModel } from "../store"; 3 | 4 | const typedHooks = createTypedHooks(); 5 | 6 | // We export the hooks from our store as they will contain the 7 | // type information on them 8 | export const useStoreActions = typedHooks.useStoreActions; 9 | export const useStoreDispatch = typedHooks.useStoreDispatch; 10 | export const useStoreState = typedHooks.useStoreState; 11 | -------------------------------------------------------------------------------- /screens/index.tsx: -------------------------------------------------------------------------------- 1 | export {default as OnboardingScreen} from "./OnboardingScreen"; 2 | export {default as DashboardScreen} from "./DashboardScreen"; 3 | export {default as CollectiblesScreen} from "./CollectiblesScreen"; 4 | export {default as SwapScreen} from "./SwapScreen"; 5 | export {default as ActivityScreen} from "./ActivityScreen"; 6 | export {default as SettingsScreen} from "./SettingsScreen"; 7 | 8 | export {default as ModalScreen} from "./ModalScreen"; 9 | export {default as NotFoundScreen} from "./NotFoundScreen"; 10 | -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Block } from "./Block"; 2 | export { default as Button } from "./Button"; 3 | export { default as Column } from "./Column"; 4 | export { default as Container } from "./Container"; 5 | export { default as Header } from "./Header"; 6 | export { default as IconButton } from "./IconButton"; 7 | export { default as List } from "./List"; 8 | export { default as ListItem } from "./ListItem"; 9 | export { default as Gradient } from "./Gradient"; 10 | export { default as Separator } from "./Separator"; 11 | export { default as Text } from "./Text"; 12 | export { default as TextInput } from "./TextInput"; 13 | export { default as Wrapper } from "./Wrapper"; 14 | -------------------------------------------------------------------------------- /components/Container.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | 3 | import tw from '../lib/tailwind'; 4 | import {useDeviceContext} from 'twrnc'; 5 | 6 | type Props = { 7 | bg?: string; 8 | children: React.ReactNode; 9 | }; 10 | 11 | const Container = ({bg, children}: Props) => { 12 | useDeviceContext(tw); 13 | 14 | return ( 15 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | const styles = StyleSheet.create({ 27 | container: { 28 | flex: 1, 29 | backgroundColor: 'white', 30 | }, 31 | }); 32 | 33 | export default Container; 34 | -------------------------------------------------------------------------------- /components/List.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | 3 | import tw from '../lib/tailwind'; 4 | import {useDeviceContext} from 'twrnc'; 5 | 6 | type Props = { 7 | children: React.ReactNode; 8 | }; 9 | 10 | const List = ({children}: Props) => { 11 | useDeviceContext(tw); 12 | 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | }; 21 | 22 | const styles = StyleSheet.create({ 23 | container: { 24 | ...tw`w-full`, 25 | }, 26 | itemsWrapper: { 27 | ...tw`bg-white`, 28 | }, 29 | items: { 30 | }, 31 | }); 32 | 33 | export default List; 34 | -------------------------------------------------------------------------------- /screens/ModalScreen.tsx: -------------------------------------------------------------------------------- 1 | import {StatusBar} from 'expo-status-bar'; 2 | import {Text, View, Platform, StyleSheet} from 'react-native'; 3 | 4 | export default function ModalScreen() { 5 | return ( 6 | 7 | Modal 8 | 9 | 10 | {/* Use a light status bar on iOS to account for the black space above the modal */} 11 | 12 | 13 | ); 14 | } 15 | 16 | const styles = StyleSheet.create({ 17 | container: { 18 | flex: 1, 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | }, 22 | title: { 23 | fontSize: 20, 24 | fontWeight: 'bold', 25 | }, 26 | separator: { 27 | marginVertical: 30, 28 | height: 1, 29 | width: '80%', 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /components/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import {TextInput as T, StyleSheet} from 'react-native'; 2 | 3 | import tw from '../lib/tailwind'; 4 | import {useDeviceContext} from 'twrnc'; 5 | 6 | type Props = React.ComponentProps & { 7 | text: string; 8 | placeholder?: string; 9 | onChangeText: void; 10 | numeric: true; 11 | }; 12 | 13 | const TextInput = ({text, placeholder, onChangeText, numeric, ...rest}: Props) => { 14 | useDeviceContext(tw); 15 | 16 | return ( 17 | 25 | ); 26 | }; 27 | 28 | const styles = StyleSheet.create({ 29 | input: { 30 | ...tw`rounded-lg p-4 items-center`, 31 | height: 50, 32 | borderWidth: 1, 33 | borderColor: '#CCCCCC', 34 | }, 35 | }); 36 | 37 | export default TextInput; 38 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Solacademy Wallet", 4 | "slug": "solacademy-wallet", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": [ 19 | "**/*" 20 | ], 21 | "ios": { 22 | "supportsTablet": true 23 | }, 24 | "android": { 25 | "adaptiveIcon": { 26 | "foregroundImage": "./assets/images/adaptive-icon.png", 27 | "backgroundColor": "#ffffff" 28 | }, 29 | "package": "com.moviendome.solacademywallet" 30 | }, 31 | "web": { 32 | "favicon": "./assets/images/favicon.png" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/Gradient.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | import {LinearGradient} from 'expo-linear-gradient'; 3 | 4 | import tw from '../lib/tailwind'; 5 | 6 | type Props = { 7 | children: React.ReactNode; 8 | horizontal?: boolean; 9 | colors: string[]; 10 | } 11 | 12 | const Gradient = ({children, horizontal, colors}: Props) => { 13 | return ( 14 | 15 | 20 | 21 | {children} 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | const styles = StyleSheet.create({ 29 | container: { 30 | ...tw`flex-1`, 31 | width: '100%', 32 | }, 33 | content: { 34 | width: '100%', 35 | maxWidth: 800, 36 | alignSelf: 'center', 37 | }, 38 | }); 39 | 40 | export default Gradient; 41 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | // IMPORTANT to avoid: https://github.com/uuidjs/uuid#getrandomvalues-not-supported 2 | import 'react-native-get-random-values'; 3 | import {v4 as uuidv4} from 'uuid'; 4 | 5 | import {StatusBar} from 'expo-status-bar'; 6 | import {SafeAreaProvider} from 'react-native-safe-area-context'; 7 | 8 | import {StoreProvider} from 'easy-peasy'; 9 | 10 | import "./global"; 11 | 12 | import 'react-native-url-polyfill/auto'; 13 | 14 | import useCachedResources from './hooks/useCachedResources'; 15 | import Navigation from './navigation'; 16 | 17 | import store from "./store"; 18 | 19 | export default function App() { 20 | const isLoadingComplete = useCachedResources(); 21 | 22 | if (!isLoadingComplete) { 23 | return null; 24 | } else { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /components/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, TouchableOpacity, View} from 'react-native'; 2 | 3 | import {MaterialCommunityIcons} from '@expo/vector-icons'; 4 | 5 | import tw from '../lib/tailwind'; 6 | import {useDeviceContext} from 'twrnc'; 7 | 8 | type Props = { 9 | icon: string; 10 | onPress(): void; 11 | color: string; 12 | }; 13 | 14 | const IconButton = ({icon, color, onPress}: Props) => { 15 | useDeviceContext(tw); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | const styles = StyleSheet.create({ 27 | container: { 28 | ...tw`justify-center items-center`, 29 | }, 30 | containedContainer: { 31 | minWidth: 50, 32 | height: 50, 33 | ...tw`rounded-lg px-4 py-3 items-center justify-center`, 34 | }, 35 | }); 36 | 37 | export default IconButton; 38 | -------------------------------------------------------------------------------- /screens/NotFoundScreen.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, TouchableOpacity} from 'react-native'; 2 | 3 | import {RootStackScreenProps} from '../types'; 4 | 5 | export default function NotFoundScreen({navigation}: RootStackScreenProps<'NotFound'>) { 6 | return ( 7 | 8 | This screen doesn't exist. 9 | navigation.replace('Root')} style={styles.link}> 10 | Go to home screen! 11 | 12 | 13 | ); 14 | } 15 | 16 | const styles = StyleSheet.create({ 17 | container: { 18 | flex: 1, 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | padding: 20, 22 | }, 23 | title: { 24 | fontSize: 20, 25 | fontWeight: 'bold', 26 | }, 27 | link: { 28 | marginTop: 15, 29 | paddingVertical: 15, 30 | }, 31 | linkText: { 32 | fontSize: 14, 33 | color: '#2e78b7', 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /hooks/useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import { FontAwesome } from '@expo/vector-icons'; 2 | import * as Font from 'expo-font'; 3 | import * as SplashScreen from 'expo-splash-screen'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | export default function useCachedResources() { 7 | const [isLoadingComplete, setLoadingComplete] = useState(false); 8 | 9 | // Load any resources or data that we need prior to rendering the app 10 | useEffect(() => { 11 | async function loadResourcesAndDataAsync() { 12 | try { 13 | SplashScreen.preventAutoHideAsync(); 14 | 15 | // Load fonts 16 | await Font.loadAsync({ 17 | ...FontAwesome.font, 18 | 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'), 19 | }); 20 | } catch (e) { 21 | // We might want to provide this error information to an error reporting service 22 | console.warn(e); 23 | } finally { 24 | setLoadingComplete(true); 25 | SplashScreen.hideAsync(); 26 | } 27 | } 28 | 29 | loadResourcesAndDataAsync(); 30 | }, []); 31 | 32 | return isLoadingComplete; 33 | } 34 | -------------------------------------------------------------------------------- /components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | 3 | import tw from '../lib/tailwind'; 4 | import {useDeviceContext} from 'twrnc'; 5 | 6 | type Props = { 7 | children: React.ReactNode; 8 | noPaddingBottom?: true; 9 | noPaddingLeft?: true; 10 | noPaddingRight?: true; 11 | noPaddingTop?: true; 12 | }; 13 | 14 | const Wrapper = ({ 15 | children, 16 | noPaddingBottom, 17 | noPaddingLeft, 18 | noPaddingRight, 19 | noPaddingTop, 20 | }: Props) => { 21 | useDeviceContext(tw); 22 | 23 | return ( 24 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | const styles = StyleSheet.create({ 39 | container: { 40 | ...tw`p-2`, 41 | }, 42 | noPt: { 43 | ...tw`pt-0`, 44 | }, 45 | noPb: { 46 | ...tw`pb-0`, 47 | }, 48 | noPl: { 49 | ...tw`pl-0`, 50 | }, 51 | noPr: { 52 | ...tw`pr-0`, 53 | }, 54 | }); 55 | 56 | export default Wrapper; 57 | -------------------------------------------------------------------------------- /types.tsx: -------------------------------------------------------------------------------- 1 | import {BottomTabScreenProps} from '@react-navigation/bottom-tabs'; 2 | import {CompositeScreenProps, NavigatorScreenParams} from '@react-navigation/native'; 3 | import {NativeStackScreenProps} from '@react-navigation/native-stack'; 4 | 5 | declare global { 6 | namespace ReactNavigation { 7 | interface RootParamList extends RootStackParamList {} 8 | } 9 | } 10 | 11 | export type RootStackParamList = { 12 | Root: NavigatorScreenParams | undefined; 13 | Modal: undefined; 14 | NotFound: undefined; 15 | Receive: undefined; 16 | Send: undefined; 17 | Scan: undefined; 18 | }; 19 | 20 | export type RootStackScreenProps = NativeStackScreenProps< 21 | RootStackParamList, 22 | Screen 23 | >; 24 | 25 | export type RootTabParamList = { 26 | Onboarding: undefined; 27 | Dashboard: undefined; 28 | Collectibles: undefined; 29 | Swap: undefined; 30 | Activity: undefined; 31 | Settings: undefined; 32 | }; 33 | 34 | export type RootTabScreenProps = CompositeScreenProps< 35 | BottomTabScreenProps, 36 | NativeStackScreenProps 37 | >; 38 | -------------------------------------------------------------------------------- /navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about deep linking with React Navigation 3 | * https://reactnavigation.org/docs/deep-linking 4 | * https://reactnavigation.org/docs/configuring-links 5 | */ 6 | 7 | import { LinkingOptions } from '@react-navigation/native'; 8 | import * as Linking from 'expo-linking'; 9 | 10 | import { RootStackParamList } from '../types'; 11 | 12 | const linking: LinkingOptions = { 13 | prefixes: [Linking.makeUrl('/')], 14 | config: { 15 | screens: { 16 | Root: { 17 | screens: { 18 | Dashboard: { 19 | screens: { 20 | DashboardScreen: 'dashboard', 21 | }, 22 | }, 23 | Collectibles: { 24 | screens: { 25 | CollectiblesScreen: 'collectibles', 26 | }, 27 | }, 28 | Swap: { 29 | screens: { 30 | SwapScreen: 'swap', 31 | }, 32 | }, 33 | Activity: { 34 | screens: { 35 | ActivityScreen: 'activity', 36 | }, 37 | }, 38 | Settings: { 39 | screens: { 40 | SettingsScreen: 'settings', 41 | }, 42 | }, 43 | }, 44 | }, 45 | Modal: 'modal', 46 | NotFound: '*', 47 | }, 48 | }, 49 | }; 50 | 51 | export default linking; 52 | -------------------------------------------------------------------------------- /screens/SettingsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Block, 3 | Container, 4 | Header, 5 | List, 6 | ListItem, 7 | Gradient, 8 | Text, 9 | Wrapper 10 | } from '../components'; 11 | 12 | import {RootTabScreenProps} from '../types'; 13 | 14 | import {C} from "../common" 15 | 16 | export default function SettingsScreen({navigation}: RootTabScreenProps<'Settings'>) { 17 | const {THEME} = C; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 |
27 | 28 | 29 | 30 | 31 | 32 | Devnet 33 | 34 | 35 | 36 | 37 | 38 | 39 | console.log('Pressed!')} 45 | /> 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import * as solanaWeb3 from "@solana/web3.js"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | 4 | const LAMPORTS_PER_SOL = solanaWeb3.LAMPORTS_PER_SOL; 5 | 6 | const createConnection = () => { 7 | return new solanaWeb3.Connection(solanaWeb3.clusterApiUrl("devnet")); 8 | }; 9 | 10 | const getBalance = async (publicKey) => { 11 | const connection = createConnection(); 12 | const _publicKey = publicKeyFromString(publicKey); 13 | 14 | const lamports = await connection.getBalance(_publicKey).catch((err) => { 15 | console.error(`Error: ${err}`); 16 | }); 17 | 18 | return lamports / LAMPORTS_PER_SOL; 19 | }; 20 | 21 | const getSolanaPrice = async () => { 22 | const response = await fetch( 23 | `https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd`, 24 | { 25 | method: "GET", 26 | } 27 | ); 28 | 29 | const data = await response.json(); 30 | return data.solana.usd; 31 | }; 32 | 33 | const publicKeyFromString = (publicKeyString: string) => { 34 | return new solanaWeb3.PublicKey(publicKeyString); 35 | }; 36 | 37 | const requestAirdrop = async (publicKeyString: string) => { 38 | const connection = createConnection(); 39 | 40 | const airdropSignature = await connection.requestAirdrop( 41 | publicKeyFromString(publicKeyString), 42 | LAMPORTS_PER_SOL 43 | ); 44 | 45 | const signature = await connection.confirmTransaction(airdropSignature); 46 | return signature; 47 | }; 48 | 49 | export { 50 | LAMPORTS_PER_SOL, 51 | createConnection, 52 | getBalance, 53 | getSolanaPrice, 54 | publicKeyFromString, 55 | requestAirdrop, 56 | }; 57 | 58 | -------------------------------------------------------------------------------- /components/Column.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | 3 | import tw from '../lib/tailwind'; 4 | import {useDeviceContext} from 'twrnc'; 5 | 6 | type Props = { 7 | first?: true; 8 | last?: true; 9 | w1?: true; 10 | w2?: true; 11 | w3?: true; 12 | w4?: true; 13 | w5?: true; 14 | half?: true; 15 | center?: true; 16 | children: React.ReactNode; 17 | }; 18 | 19 | const Column = ({first, last, w1, w2, w3, w4, w5, half, center, children}: Props) => { 20 | useDeviceContext(tw); 21 | 22 | return ( 23 | 38 | {children} 39 | 40 | ); 41 | }; 42 | 43 | const styles = StyleSheet.create({ 44 | container: { 45 | justifyContent: 'center', 46 | }, 47 | w1: { 48 | ...tw`w-1/5`, 49 | }, 50 | w2: { 51 | ...tw`w-2/5`, 52 | }, 53 | w3: { 54 | ...tw`w-3/5`, 55 | }, 56 | w4: { 57 | ...tw`w-4/5`, 58 | }, 59 | w5: { 60 | ...tw`w-5/5`, 61 | }, 62 | half: { 63 | ...tw`w-1/2`, 64 | }, 65 | center: { 66 | ...tw`items-center`, 67 | }, 68 | padding: { 69 | ...tw`pl-2 pr-2`, 70 | }, 71 | noPl: { 72 | ...tw`pl-0`, 73 | }, 74 | noPr: { 75 | ...tw`pr-0`, 76 | }, 77 | }); 78 | 79 | export default Column; 80 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | 3 | import {IconButton, Text} from '../components'; 4 | 5 | import tw from '../lib/tailwind'; 6 | import {useDeviceContext} from 'twrnc'; 7 | 8 | import {useNavigation} from '@react-navigation/native'; 9 | 10 | import {C} from '../common' 11 | 12 | type Props = { 13 | title: string; 14 | subtitle?: string; 15 | left?: object; 16 | right?: object; 17 | back?: true; 18 | }; 19 | 20 | const Header = ({back, title, subtitle, left, right}: Props) => { 21 | useDeviceContext(tw); 22 | const navigation = useNavigation(); 23 | 24 | const {THEME} = C; 25 | 26 | return ( 27 | 28 | 29 | {back && ( 30 | navigation.goBack()} 34 | /> 35 | )} 36 | {left ? left : null} 37 | 38 | 39 | 40 | {title && ( 41 | 42 | {title} 43 | 44 | )} 45 | {subtitle && {subtitle}} 46 | 47 | 48 | {right ? right : null} 49 | 50 | ); 51 | }; 52 | 53 | const styles = StyleSheet.create({ 54 | container: { 55 | ...tw`w-full flex-row justify-between items-center pt-4`, 56 | }, 57 | left: { 58 | width: 40, 59 | }, 60 | content: { 61 | ...tw`flex-1 items-center`, 62 | }, 63 | right: { 64 | ...tw`items-end`, 65 | width: 40, 66 | }, 67 | }); 68 | 69 | export default Header; 70 | -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Generic, 3 | generic, 4 | Computed, 5 | computed, 6 | createStore, 7 | action, 8 | Action, 9 | persist, 10 | } from "easy-peasy"; 11 | 12 | import storage from "../storage"; 13 | 14 | export interface Wallet { 15 | mnemonic: string[]; 16 | seed: string; 17 | } 18 | 19 | export interface Account { 20 | index: number; 21 | title: string; 22 | derivationPath: string; 23 | } 24 | 25 | export interface WalletModel { 26 | wallet: Generic; 27 | accounts: Account[]; 28 | hasWallet: Computed, Wallet | false>; 29 | addWallet: Action, K>; 30 | addDefaultAccount: Action, K>; 31 | addAccount: Action, K>; 32 | } 33 | 34 | const store = createStore( 35 | persist( 36 | { 37 | wallet: generic({}), 38 | accounts: [], 39 | hasWallet: computed( 40 | (state) => 41 | Object.keys(state.wallet).length !== 0 && state.accounts.length !== 0 42 | ), 43 | addWallet: action((state, payload) => { 44 | state.wallet = { 45 | mnemonic: payload.mnemonic, 46 | seed: payload.seed, 47 | }; 48 | }), 49 | addDefaultAccount: action((state, payload) => { 50 | state.accounts.push({ 51 | index: 0, 52 | title: "default", 53 | derivationPath: "bip44Change", 54 | }); 55 | }), 56 | addAccount: action((state, payload) => { 57 | state.accounts.push({ 58 | index: payload.index, 59 | title: payload.title, 60 | derivationPath: "bip44Change", 61 | }); 62 | }), 63 | }, 64 | { 65 | storage: storage, 66 | } 67 | ) 68 | ); 69 | 70 | export default store; 71 | -------------------------------------------------------------------------------- /components/Block.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, View} from 'react-native'; 2 | 3 | 4 | import tw from '../lib/tailwind'; 5 | import {useDeviceContext} from 'twrnc'; 6 | 7 | type Props = { 8 | bg?: string; 9 | children: React.ReactNode; 10 | justify?: true; 11 | noPaddingBottom?: true; 12 | noPaddingLeft?: true; 13 | noPaddingRight?: true; 14 | noPaddingTop?: true; 15 | roundedTop?: boolean; 16 | row?: true; 17 | }; 18 | 19 | const Block = ({ 20 | bg, 21 | children, 22 | justify, 23 | noPaddingBottom, 24 | noPaddingLeft, 25 | noPaddingRight, 26 | noPaddingTop, 27 | roundedTop, 28 | row, 29 | }: Props) => { 30 | useDeviceContext(tw); 31 | 32 | return ( 33 | 46 | {children} 47 | 48 | ); 49 | }; 50 | 51 | const styles = StyleSheet.create({ 52 | container: { 53 | ...tw`p-4`, 54 | }, 55 | row: { 56 | ...tw`flex-row`, 57 | }, 58 | justify: { 59 | ...tw`justify-between items-center`, 60 | }, 61 | noPt: { 62 | ...tw`pt-0`, 63 | }, 64 | noPr: { 65 | ...tw`pr-0`, 66 | }, 67 | noPl: { 68 | ...tw`pl-0`, 69 | }, 70 | noPb: { 71 | ...tw`pb-0`, 72 | }, 73 | roundedTop: { 74 | backgroundColor: 'white', 75 | marginTop: 20, 76 | borderTopLeftRadius: 30, 77 | borderTopRightRadius: 30, 78 | minHeight: 800, 79 | } 80 | }); 81 | 82 | export default Block; 83 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | import * as solanaWeb3 from "@solana/web3.js"; 3 | import * as Random from "expo-random"; 4 | import { ethers } from "ethers"; 5 | import { Buffer } from "buffer"; 6 | import * as ed25519 from "ed25519-hd-key"; 7 | import nacl from "tweetnacl"; 8 | 9 | export const DERIVATION_PATH = { 10 | bip44Change: "bip44Change", 11 | }; 12 | 13 | const generateMnemonic = async () => { 14 | const randomBytes = await Random.getRandomBytesAsync(32); 15 | const mnemonic = ethers.utils.entropyToMnemonic(randomBytes); 16 | return mnemonic; 17 | }; 18 | 19 | const mnemonicToSeed = async (mnemonic: string) => { 20 | const bip39 = await import("bip39"); 21 | const seed = await bip39.mnemonicToSeed(mnemonic); 22 | return Buffer.from(seed).toString("hex"); 23 | }; 24 | 25 | const accountFromSeed = ( 26 | seed: string, 27 | walletIndex: number, 28 | derivationPath: string, 29 | accountIndex: 0 30 | ) => { 31 | const derivedSeed = deriveSeed( 32 | seed, 33 | walletIndex, 34 | derivationPath, 35 | accountIndex 36 | ); 37 | const keyPair = nacl.sign.keyPair.fromSeed(derivedSeed); 38 | 39 | const acc = new solanaWeb3.Keypair(keyPair); 40 | return acc; 41 | }; 42 | 43 | const maskedAddress = (address: string) => { 44 | if (!address) return; 45 | return `${address.slice(0, 4)}...${address.slice(address.length - 4)}`; 46 | }; 47 | 48 | const deriveSeed = ( 49 | seed: string, 50 | walletIndex: number, 51 | derivationPath: string, 52 | accountIndex: number 53 | ): Buffer | undefined => { 54 | const path44Change = `m/44'/501'/${walletIndex}'/0'`; 55 | return ed25519.derivePath(path44Change, Buffer.from(seed, "hex")).key; 56 | }; 57 | 58 | export { 59 | generateMnemonic, 60 | mnemonicToSeed, 61 | accountFromSeed, 62 | maskedAddress, 63 | deriveSeed, 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wallet", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "eject": "expo eject", 11 | "test": "jest --watchAll" 12 | }, 13 | "jest": { 14 | "preset": "jest-expo" 15 | }, 16 | "dependencies": { 17 | "@expo/vector-icons": "^12.0.0", 18 | "@react-native-async-storage/async-storage": "^1.16.1", 19 | "@react-navigation/bottom-tabs": "^6.0.5", 20 | "@react-navigation/native": "^6.0.2", 21 | "@react-navigation/native-stack": "^6.1.0", 22 | "@solana/web3.js": "1.21.0", 23 | "buffer": "^6.0.3", 24 | "easy-peasy": "^5.0.4", 25 | "ed25519-hd-key": "^1.2.0", 26 | "ethers": "^5.5.4", 27 | "events": "^3.3.0", 28 | "expo": "~44.0.0", 29 | "expo-asset": "~8.4.4", 30 | "expo-barcode-scanner": "~11.2.0", 31 | "expo-constants": "~13.0.0", 32 | "expo-font": "~10.0.4", 33 | "expo-linear-gradient": "~11.0.3", 34 | "expo-linking": "~3.0.0", 35 | "expo-random": "~12.1.1", 36 | "expo-splash-screen": "~0.14.0", 37 | "expo-status-bar": "~1.2.0", 38 | "expo-updates": "~0.11.6", 39 | "expo-web-browser": "~10.1.0", 40 | "process": "^0.11.10", 41 | "react": "17.0.1", 42 | "react-dom": "17.0.1", 43 | "react-native": "0.64.3", 44 | "react-native-get-random-values": "~1.7.0", 45 | "react-native-qrcode-svg": "^6.1.2", 46 | "react-native-safe-area-context": "3.3.2", 47 | "react-native-screens": "~3.10.1", 48 | "react-native-svg": "^12.3.0", 49 | "react-native-url-polyfill": "^1.3.0", 50 | "react-native-web": "0.17.1", 51 | "readable-stream": "^3.6.0", 52 | "tweetnacl": "^1.0.3", 53 | "twrnc": "^3.0.2", 54 | "uuid": "3.4.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.12.9", 58 | "@types/react": "~17.0.21", 59 | "@types/react-native": "~0.64.12", 60 | "jest": "^26.6.3", 61 | "jest-expo": "~44.0.1", 62 | "react-test-renderer": "17.0.1", 63 | "typescript": "~4.3.5" 64 | }, 65 | "private": true 66 | } 67 | -------------------------------------------------------------------------------- /screens/ActivityScreen.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Block, 3 | Container, 4 | Header, 5 | List, 6 | ListItem, 7 | Gradient, 8 | Text, 9 | Wrapper 10 | } from '../components'; 11 | 12 | import {RootTabScreenProps} from '../types'; 13 | 14 | import {C} from "../common" 15 | 16 | export default function ActivityScreen({navigation}: RootTabScreenProps<'Activity'>) { 17 | const {THEME} = C; 18 | 19 | const activity = [ 20 | { 21 | id: 1, 22 | icon: 'arrow-collapse-down', 23 | title: 'Received 100 PIP', 24 | subtitle: '', 25 | when: '29d' 26 | }, 27 | { 28 | id: 2, 29 | icon: 'arrow-collapse-down', 30 | title: 'Received 2.10 SOL', 31 | subtitle: '', 32 | when: '1mo' 33 | }, 34 | { 35 | id: 3, 36 | icon: 'send', 37 | title: 'Sent 500 USDC', 38 | subtitle: '', 39 | when: '1mo' 40 | }, 41 | { 42 | id: 4, 43 | icon: 'check-bold', 44 | title: 'vBWp...afyn', 45 | subtitle: 'Success', 46 | when: '3mo' 47 | }, 48 | ]; 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 |
58 | 59 | 60 | 61 | 62 | 63 | Recent Activity 64 | 65 | 66 | 67 | 68 | 69 | {activity.map(item => ( 70 | console.log('Pressed!')} 76 | amount={item.when} 77 | /> 78 | ))} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ◎ solacademy.xyz 3 |

4 |

5 | 6 |
7 | 8 |

9 | 10 |

A Expo, React Native & Solana Web3 Boilerplate

11 | 12 | This is a boilerplate ready to start building a Cross-Platform Wallet App with Expo, React Native and `solana/web3`. 13 | 14 | ## What does it include? 15 | 16 | - [Expo](https://expo.dev/) 17 | - [React Native](https://reactnative.dev/) 18 | - [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/) 19 | - [React Navigation](https://reactnavigation.org/) 20 | - [Easy Peasy](https://easy-peasy.vercel.app/) 21 | - [Tailwind CSS](https://tailwindcss.com/) 22 | 23 | ## Requirements 24 | 25 | In order to run this project, install: 26 | 27 | - [NodeJS](https://nodejs.org/) 28 | - [Expo](https://expo.dev/) 29 | 30 | 31 | ## How to run the project? 32 | 33 | Clone this repository: 34 | 35 | ``` 36 | git clone https://github.com/jferrer/solacademy-wallet 37 | ``` 38 | 39 | Enter the project: 40 | 41 | ``` 42 | cd solacademy-wallet 43 | ``` 44 | 45 | Install dependencies: 46 | 47 | ``` 48 | yarn install 49 | ``` 50 | 51 | Run on web: 52 | 53 | ``` 54 | expo web 55 | ``` 56 | 57 | Run on mobile (through Expo): 58 | 59 | ``` 60 | expo start 61 | ``` 62 | 63 | ## How to run the project on Android: 64 | 65 | Download [Expo Go](https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=www) and can the QR code [here](https://expo.dev/@moviendome/solacademy-wallet). 66 | 67 | ## How to run the project on iOS: 68 | 69 | Download [Expo Go](https://apps.apple.com/app/apple-store/id982107779) and visit this [link](exp://exp.host/@moviendome/solacademy-wallet?release-channel=default) on your device. 70 | 71 | ## Learn how to build a Cross-Platform Solana Wallet App 72 | 73 | Visit [Solacademy](https://www.solacademy.xyz) 74 | 75 | ## Donate 76 | 77 | Consider donating some `SOL` or `SPL Tokens` if you find useful and want support this project. 78 | 79 | 80 |

81 |
82 | 83 |
84 | Eg2SAgUB5VsNdcSMggLpyrvqcBqJBwmBUNC6e4XC7LPv 85 |

86 | -------------------------------------------------------------------------------- /screens/OnboardingScreen.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | 3 | import { 4 | Block, 5 | Button, 6 | Column, 7 | Container, 8 | Header, 9 | Gradient, 10 | Text, 11 | Wrapper 12 | } from '../components'; 13 | 14 | import { C } from "../common" 15 | 16 | import { useStoreActions } from "../hooks/storeHooks"; 17 | 18 | import { RootTabScreenProps } from '../types'; 19 | 20 | import { generateMnemonic, mnemonicToSeed, accountFromSeed } from "../utils"; 21 | 22 | export default function DashboardScreen({ navigation }: RootTabScreenProps<'Onboarding'>) { 23 | const { THEME } = C; 24 | 25 | const addWallet = useStoreActions((actions) => actions.addWallet); 26 | 27 | const addDefaultAccount = useStoreActions( 28 | (actions) => actions.addDefaultAccount 29 | ); 30 | 31 | const createWallet = async () => { 32 | const mnemonic = await generateMnemonic(); 33 | const seed = await mnemonicToSeed(mnemonic); 34 | 35 | addWallet({ 36 | mnemonic: mnemonic, 37 | seed: seed, 38 | }); 39 | 40 | addDefaultAccount(); 41 | } 42 | 43 | return ( 44 | <> 45 | 46 | 47 | 48 | 49 | 50 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Create Wallet 61 | 62 | 63 | 64 | 65 | 66 | 67 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris varius sapien mauris, a eleifend dolor dictum in. Etiam fermentum vel mi ac posuere. 68 | 69 | 70 | 71 | 72 | 73 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris varius sapien mauris, a eleifend dolor dictum in. Etiam fermentum vel mi ac posuere. 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /components/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import {StyleSheet, TouchableOpacity, View} from 'react-native'; 2 | 3 | import {Text} from '../components'; 4 | 5 | import {MaterialCommunityIcons} from '@expo/vector-icons'; 6 | 7 | import tw from '../lib/tailwind'; 8 | import {useDeviceContext} from 'twrnc'; 9 | 10 | const ListItem = ({ 11 | last, 12 | onPress, 13 | left, 14 | right, 15 | icon, 16 | iconColor, 17 | title, 18 | subtitle, 19 | amount, 20 | }: { 21 | last?: true; 22 | onPress(): void; 23 | left?: object; 24 | right?: object; 25 | title: string; 26 | subtitle: string; 27 | icon?: string; 28 | iconColor?: string; 29 | amount: string; 30 | }) => { 31 | useDeviceContext(tw); 32 | 33 | return ( 34 | 35 | 36 | 37 | {icon && ( 38 | 39 | 40 | 41 | 42 | 43 | )} 44 | {left && ( 45 | 46 | {left} 47 | 48 | )} 49 | 50 | {title} 51 | {subtitle} 52 | 53 | {amount && ( 54 | 55 | {amount} 56 | 57 | )} 58 | {right && ( 59 | 60 | {right} 61 | 62 | )} 63 | 64 | 65 | 66 | ) 67 | }; 68 | 69 | const styles = StyleSheet.create({ 70 | container: { 71 | ...tw`w-full flex-row items-center border-b border-gray-200 pb-2 mb-2 justify-between`, 72 | height: 60, 73 | }, 74 | last: { 75 | ...tw`border-white pb-0 mb-0`, 76 | }, 77 | leftContainer: { 78 | ...tw`w-1/5 justify-center`, 79 | }, 80 | iconCircle: { 81 | ...tw`rounded-full h-8 w-8 items-center justify-center bg-gray-100`, 82 | }, 83 | content3: { 84 | ...tw`w-2.5/5`, 85 | }, 86 | content4: { 87 | ...tw`w-4/5`, 88 | }, 89 | content5: { 90 | ...tw`w-5/5`, 91 | }, 92 | right: { 93 | ...tw`w-1.5/5 items-end`, 94 | }, 95 | amount: { 96 | ...tw`text-sm font-bold`, 97 | }, 98 | 99 | row: { 100 | ...tw`flex-1 flex-row items-center` 101 | }, 102 | rowLeft: { 103 | width: 40, 104 | }, 105 | rowContent: { 106 | ...tw`flex-1`, 107 | }, 108 | rowRight: { 109 | ...tw`items-end`, 110 | width: 80, 111 | } 112 | }); 113 | 114 | export default ListItem; 115 | -------------------------------------------------------------------------------- /screens/SwapScreen.tsx: -------------------------------------------------------------------------------- 1 | import {Image} from 'react-native'; 2 | 3 | import { 4 | Block, 5 | Button, 6 | Column, 7 | Container, 8 | Header, 9 | Gradient, 10 | Separator, 11 | Text, 12 | TextInput, 13 | Wrapper 14 | } from '../components'; 15 | 16 | import {C} from '../common'; 17 | 18 | import {RootTabScreenProps} from '../types'; 19 | 20 | export default function SwapScreen({navigation}: RootTabScreenProps<'Swap'>) { 21 | const {THEME} = C; 22 | 23 | const assets = [ 24 | { 25 | id: 1, 26 | name: 'Solana', 27 | symbol: 'SOL', 28 | decimals: 9, 29 | logoURI: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png', 30 | }, 31 | { 32 | id: 2, 33 | name: 'USDC Coin', 34 | symbol: 'USDC', 35 | decimals: 6, 36 | logoURI: 'https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png', 37 | }, 38 | ]; 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | Swap From 55 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | {assets[0].symbol} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Swap to 78 | 79 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | 92 | {assets[1].symbol} 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityIndicator, 3 | StyleSheet, 4 | Text, 5 | TouchableHighlight, 6 | View 7 | } from 'react-native'; 8 | 9 | import {MaterialCommunityIcons} from '@expo/vector-icons'; 10 | 11 | import tw from '../lib/tailwind'; 12 | import {useDeviceContext} from 'twrnc'; 13 | 14 | const Button = ({ 15 | mode = 'contained', 16 | color, 17 | children, 18 | onPress, 19 | iconLeft, 20 | iconRight, 21 | loading, 22 | }: { 23 | mode?: 'text' | 'outlined' | 'contained'; 24 | color: string; 25 | children: React.ReactNode; 26 | onPress(): void; 27 | iconLeft?: string; 28 | iconRight?: string; 29 | loading?: boolean; 30 | }) => { 31 | useDeviceContext(tw); 32 | 33 | const _mode = () => { 34 | switch (mode) { 35 | case 'text': { 36 | return [styles.textContainer]; 37 | } 38 | case 'outlined': { 39 | if (color) return [styles.outlinedContainer, {borderColor: color}]; 40 | return styles.outlinedContainer; 41 | } 42 | default: { 43 | if (color) 44 | return [styles.containedContainer, {backgroundColor: color}]; 45 | return styles.containedContainer; 46 | } 47 | } 48 | }; 49 | 50 | const _text = () => { 51 | switch (mode) { 52 | case 'text': { 53 | return styles.text; 54 | } 55 | case 'outlined': { 56 | return styles.text; 57 | } 58 | default: { 59 | return styles.textContained; 60 | } 61 | } 62 | }; 63 | 64 | const _icon = () => { 65 | switch (mode) { 66 | case 'text': { 67 | return 'black'; 68 | } 69 | case 'outlined': { 70 | return 'black'; 71 | } 72 | default: { 73 | return 'white'; 74 | } 75 | } 76 | }; 77 | 78 | return ( 79 | 80 | 81 | {loading && ( 82 | 83 | 84 | 85 | )} 86 | {iconLeft && ( 87 | 88 | 89 | 90 | )} 91 | {!loading && ( 92 | {children} 93 | )} 94 | {iconRight && ( 95 | 96 | 97 | 98 | )} 99 | 100 | 101 | ); 102 | }; 103 | 104 | const styles = StyleSheet.create({ 105 | row: { 106 | ...tw`flex-row items-center`, 107 | }, 108 | iconLeft: { 109 | ...tw`pr-2`, 110 | }, 111 | iconRight: { 112 | ...tw`pl-2`, 113 | }, 114 | textContainer: { 115 | ...tw`px-4 py-3 items-center justify-center`, 116 | minWidth: 50, 117 | height: 50, 118 | }, 119 | containedContainer: { 120 | minWidth: 50, 121 | height: 50, 122 | ...tw`rounded-lg px-4 py-3 items-center justify-center`, 123 | }, 124 | outlinedContainer: { 125 | minWidth: 50, 126 | height: 50, 127 | ...tw`rounded-lg border px-4 py-3 items-center justify-center`, 128 | }, 129 | text: { 130 | ...tw`text-body font-bold text-center uppercase text-primary`, 131 | }, 132 | textContained: { 133 | ...tw`text-white text-body font-bold text-center uppercase`, 134 | }, 135 | }); 136 | 137 | export default Button; 138 | -------------------------------------------------------------------------------- /navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import {FontAwesome} from '@expo/vector-icons'; 2 | import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; 3 | import {NavigationContainer} from '@react-navigation/native'; 4 | import {createNativeStackNavigator} from '@react-navigation/native-stack'; 5 | 6 | import {C} from '../common'; 7 | 8 | import { 9 | OnboardingScreen, 10 | DashboardScreen, 11 | CollectiblesScreen, 12 | SwapScreen, 13 | ActivityScreen, 14 | SettingsScreen, 15 | ModalScreen, 16 | NotFoundScreen, 17 | } from '../screens' 18 | 19 | import {RootStackParamList, RootTabParamList, RootTabScreenProps} from '../types'; 20 | import LinkingConfiguration from './LinkingConfiguration'; 21 | 22 | import { useStoreState } from "../hooks/storeHooks"; 23 | 24 | const {THEME} = C; 25 | 26 | export default function Navigation() { 27 | return ( 28 | 31 | 32 | 33 | ); 34 | } 35 | 36 | const Stack = createNativeStackNavigator(); 37 | 38 | function RootNavigator() { 39 | const hasWallet = useStoreState((state) => state.hasWallet); 40 | 41 | if (hasWallet) { 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } else { 52 | return ( 53 | 54 | 55 | 56 | ) 57 | } 58 | } 59 | 60 | const BottomTab = createBottomTabNavigator(); 61 | 62 | function BottomTabNavigator() { 63 | return ( 64 | 70 | , 76 | }} 77 | /> 78 | , 84 | }} 85 | /> 86 | , 92 | }} 93 | /> 94 | , 100 | }} 101 | /> 102 | , 108 | }} 109 | /> 110 | 111 | ); 112 | } 113 | 114 | function TabBarIcon(props: { 115 | name: React.ComponentProps['name']; 116 | color: string; 117 | }) { 118 | return ; 119 | } 120 | -------------------------------------------------------------------------------- /components/Text.tsx: -------------------------------------------------------------------------------- 1 | import {Text as T, StyleSheet} from 'react-native'; 2 | 3 | import tw from '../lib/tailwind'; 4 | import {useDeviceContext} from 'twrnc'; 5 | 6 | type Props = React.ComponentProps & { 7 | children: React.ReactNode; 8 | size?: 9 | | 'xs' 10 | | 'sm' 11 | | 'tiny' 12 | | 'lg' 13 | | 'xl' 14 | | '2xl' 15 | | '3xl' 16 | | '4xl' 17 | | '5xl' 18 | | '6xl' 19 | | '7xl'; 20 | weight?: 21 | | 'thin' 22 | | 'extralight' 23 | | 'light' 24 | | 'medium' 25 | | 'semibold' 26 | | 'bold' 27 | | 'extrabold' 28 | | 'black'; 29 | uppercase?: true; 30 | color?: string; 31 | }; 32 | 33 | const Text = ({children, size, weight, uppercase, color, ...rest}: Props) => { 34 | useDeviceContext(tw); 35 | 36 | const _size = () => { 37 | switch (size) { 38 | case 'xs': { 39 | return styles.xs; 40 | } 41 | case 'sm': { 42 | return styles.sm; 43 | } 44 | case 'tiny': { 45 | return styles.tiny; 46 | } 47 | case 'lg': { 48 | return styles.lg; 49 | } 50 | case 'xl': { 51 | return styles.xl; 52 | } 53 | case '2xl': { 54 | return styles.s2xl; 55 | } 56 | case '3xl': { 57 | return styles.s3xl; 58 | } 59 | case '4xl': { 60 | return styles.s4xl; 61 | } 62 | case '5xl': { 63 | return styles.s5xl; 64 | } 65 | case '6xl': { 66 | return styles.s6xl; 67 | } 68 | case '7xl': { 69 | return styles.s7xl; 70 | } 71 | } 72 | }; 73 | 74 | const _weight = () => { 75 | switch (weight) { 76 | case 'thin': { 77 | return styles.thin; 78 | } 79 | case 'extralight': { 80 | return styles.extralight; 81 | } 82 | case 'light': { 83 | return styles.light; 84 | } 85 | case 'medium': { 86 | return styles.medium; 87 | } 88 | case 'semibold': { 89 | return styles.semibold; 90 | } 91 | case 'bold': { 92 | return styles.bold; 93 | } 94 | case 'extrabold': { 95 | return styles.extrabold; 96 | } 97 | case 'black': { 98 | return styles.black; 99 | } 100 | } 101 | }; 102 | 103 | const _uppercase = () => { 104 | return styles.uppercase; 105 | } 106 | 107 | return ( 108 | 117 | {children} 118 | 119 | ); 120 | }; 121 | 122 | const styles = StyleSheet.create({ 123 | xs: { 124 | ...tw`text-xs`, 125 | }, 126 | sm: { 127 | ...tw`text-sm`, 128 | }, 129 | tiny: { 130 | ...tw`text-tiny`, 131 | }, 132 | lg: { 133 | ...tw`text-lg`, 134 | }, 135 | xl: { 136 | ...tw`text-xl`, 137 | }, 138 | s2xl: { 139 | ...tw`text-2xl`, 140 | }, 141 | s3xl: { 142 | ...tw`text-3xl`, 143 | }, 144 | s4xl: { 145 | ...tw`text-4xl`, 146 | }, 147 | s5xl: { 148 | ...tw`text-5xl`, 149 | }, 150 | s6xl: { 151 | ...tw`text-6xl`, 152 | }, 153 | s7xl: { 154 | ...tw`text-7xl`, 155 | }, 156 | body: { 157 | ...tw`text-base`, 158 | }, 159 | thin: { 160 | ...tw`font-thin`, 161 | }, 162 | extralight: { 163 | ...tw`font-extralight`, 164 | }, 165 | light: { 166 | ...tw`font-light`, 167 | }, 168 | medium: { 169 | ...tw`font-medium`, 170 | }, 171 | normal: { 172 | ...tw`font-normal`, 173 | }, 174 | semibold: { 175 | ...tw`font-semibold`, 176 | }, 177 | bold: { 178 | ...tw`font-bold`, 179 | }, 180 | extrabold: { 181 | ...tw`font-extrabold`, 182 | }, 183 | black: { 184 | ...tw`font-black`, 185 | }, 186 | uppercase: { 187 | ...tw`uppercase`, 188 | }, 189 | }); 190 | 191 | export default Text; 192 | -------------------------------------------------------------------------------- /screens/CollectiblesScreen.tsx: -------------------------------------------------------------------------------- 1 | import {ScrollView, StyleSheet, TouchableWithoutFeedback, View} from 'react-native'; 2 | import {MaterialCommunityIcons} from '@expo/vector-icons'; 3 | 4 | import tw from '../lib/tailwind'; 5 | 6 | import { 7 | Block, 8 | Container, 9 | Header, 10 | Gradient, 11 | Text, 12 | Wrapper 13 | } from '../components'; 14 | 15 | import {RootTabScreenProps} from '../types'; 16 | 17 | import {C} from '../common'; 18 | 19 | const data = [ 20 | { 21 | id: '1', 22 | name: 'One', 23 | // image: require('../assets/images/') 24 | }, 25 | { 26 | id: '2', 27 | name: 'Two', 28 | // image: require('../assets/images/') 29 | }, 30 | { 31 | id: '3', 32 | name: 'Three', 33 | // image: require('../assets/images/') 34 | }, 35 | { 36 | id: '4', 37 | name: 'Four', 38 | // image: require('../assets/images/') 39 | }, 40 | { 41 | id: '5', 42 | name: 'Five', 43 | // image: require('../assets/images/') 44 | }, 45 | { 46 | id: '6', 47 | name: 'Six', 48 | // image: require('../assets/images/') 49 | }, 50 | { 51 | id: '7', 52 | name: 'Seven', 53 | // image: require('../assets/images/') 54 | }, 55 | { 56 | id: '8', 57 | name: 'Eight', 58 | // image: require('../assets/images/') 59 | }, 60 | { 61 | id: '9', 62 | name: 'Nine', 63 | // image: require('../assets/images/') 64 | }, 65 | { 66 | id: '10', 67 | name: 'Ten', 68 | // image: require('../assets/images/') 69 | }, 70 | { 71 | id: '11', 72 | name: 'Eleven', 73 | // image: require('../assets/images/') 74 | }, 75 | { 76 | id: '12', 77 | name: 'Twelve', 78 | // image: require('../assets/images/') 79 | }, 80 | ] 81 | 82 | export default function CollectiblesScreen({navigation}: RootTabScreenProps<'Collectibles'>) { 83 | const {THEME} = C; 84 | 85 | const AddIcon = () => { 86 | return ( 87 | console.log('Pressed!')}> 88 | 89 | 90 | ); 91 | }; 92 | 93 | const RenderContent = ({children}: any) => { 94 | if (C.IS_MOBILE) { 95 | return ( 96 | {children} 97 | ) 98 | } 99 | 100 | return children 101 | } 102 | 103 | const renderRow = () => { 104 | const elementsInRow = C.IS_MOBILE ? 2 : 4 105 | 106 | const chunk = (arr: object[], size: number) => 107 | Array.from({length: Math.ceil(arr.length / size)}, (v, i) => 108 | arr.slice(i * size, i * size + size) 109 | ); 110 | 111 | const rows = chunk(data, elementsInRow) 112 | 113 | return rows.map((row, index) => { 114 | return 115 | { 116 | row.map(item => ( 117 | 118 | { /* 119 | 123 | */ } 124 | 125 | )) 126 | } 127 | 128 | }) 129 | } 130 | 131 | return ( 132 | 133 | 134 | 135 | 136 |
} 139 | /> 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | {data ? renderRow() : (Loading....)} 148 | 149 | 150 | 151 | 152 | 153 | 154 | ); 155 | } 156 | 157 | const styles = StyleSheet.create({ 158 | content: { 159 | flex: 1, 160 | alignItems: 'center', 161 | margin: 20, 162 | }, 163 | row: { 164 | width: '100%', 165 | maxWidth: C.IS_MOBILE ? 320 : 860, 166 | ...tw`flex-row` 167 | }, 168 | card: { 169 | width: C.IS_MOBILE ? 150 : 174, 170 | height: C.IS_MOBILE ? 150 : 174, 171 | marginRight: 20, 172 | marginBottom: 20, 173 | backgroundColor: '#CCCCCC', 174 | justifyContent: 'center', 175 | alignItems: 'center' 176 | }, 177 | }) 178 | -------------------------------------------------------------------------------- /screens/DashboardScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useCallback, useState } from 'react'; 2 | import { Image } from 'react-native'; 3 | 4 | import { 5 | Block, 6 | Button, 7 | Column, 8 | Container, 9 | Header, 10 | List, 11 | ListItem, 12 | Gradient, 13 | Text, 14 | Wrapper 15 | } from '../components'; 16 | 17 | import { C } from '../common'; 18 | 19 | import { RootTabScreenProps } from '../types'; 20 | 21 | import { useStoreState } from '../hooks/storeHooks'; 22 | import { accountFromSeed, maskedAddress } from '../utils'; 23 | 24 | import { 25 | getBalance, 26 | getSolanaPrice, 27 | requestAirdrop, 28 | } from '../api'; 29 | import { useFocusEffect } from '@react-navigation/native'; 30 | 31 | export default function DashboardScreen({ navigation }: RootTabScreenProps<'Dashboard'>) { 32 | const { THEME } = C; 33 | 34 | const wallet = useStoreState((state) => state.wallet); 35 | const accounts = useStoreState((state) => state.accounts); 36 | 37 | const [account, setAccount] = useState({}); 38 | 39 | useEffect(() => { 40 | async function generate() { 41 | const currentAccount = accounts[0]; 42 | setAccount({ 43 | index: currentAccount.index, 44 | title: currentAccount.title, 45 | keyPair: accountFromSeed( 46 | wallet.seed, 47 | currentAccount.index, 48 | currentAccount.derivationPath, 49 | 0 50 | ), 51 | }); 52 | } 53 | 54 | generate(); 55 | }, []); 56 | 57 | const [balance, setBalance] = useState({ 58 | usd: 0.0, 59 | sol: 0 60 | }); 61 | 62 | useFocusEffect( 63 | useCallback(() => { 64 | async function getAsyncBalance() { 65 | if (account?.keyPair?.publicKey?.toString()) { 66 | const sol = await getBalance(account.keyPair.publicKey.toString()); 67 | const usdPrice = await getSolanaPrice(); 68 | 69 | setBalance({ 70 | sol, 71 | usd: (sol * usdPrice).toFixed(2), 72 | }); 73 | } 74 | } 75 | getAsyncBalance(); 76 | }, [account]) 77 | ); 78 | 79 | const assets = [ 80 | { 81 | id: 1, 82 | name: "Solana", 83 | symbol: "SOL", 84 | decimals: 9, 85 | logoURI: "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", 86 | }, 87 | { 88 | id: 2, 89 | name: "USDC Coin", 90 | symbol: "USDC", 91 | decimals: 6, 92 | logoURI: "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png", 93 | }, 94 | { 95 | id: 3, 96 | name: "Samoyed Coin", 97 | symbol: "SAMO", 98 | decimals: 9, 99 | logoURI: "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU/logo.png", 100 | }, 101 | ]; 102 | 103 | const getAirdrop = async () => { 104 | const address = account.keyPair.publicKey.toString(); 105 | 106 | console.log("Requesting Airdrop...") 107 | 108 | await requestAirdrop(address); 109 | const sol = await getBalance(address); 110 | const usdPrice = await getSolanaPrice(); 111 | 112 | console.log("Setting updated balance...") 113 | 114 | setBalance({ 115 | sol, 116 | usd: (sol * usdPrice).toFixed(2), 117 | }); 118 | } 119 | 120 | return ( 121 | 122 | 123 | 124 | 125 |
129 | 130 | 131 | 132 | 133 | 134 | 140 | 141 | 142 | 148 | 149 | 150 | 151 | 152 | 153 | Assets 154 | 155 | 156 | 157 | 158 | 159 | 168 | } 169 | title={assets[0].name} 170 | subtitle={`${balance?.sol} ${assets[0].symbol}`} 171 | onPress={() => console.log('Pressed!')} 172 | amount={balance?.usd ? `$${balance.usd}` : '-'} 173 | /> 174 | 175 | 176 | 177 | 178 | 179 | 180 | ); 181 | } 182 | --------------------------------------------------------------------------------