├── src ├── components │ ├── CoinPriceGraph │ │ ├── styles.ts │ │ └── index.tsx │ ├── StyledText.tsx │ ├── __tests__ │ │ └── StyledText-test.js │ ├── PercentageChange │ │ └── index.tsx │ ├── MarketCoin │ │ ├── styles.ts │ │ └── index.tsx │ ├── PortfolioCoin │ │ ├── styles.ts │ │ └── index.tsx │ ├── UserRankingItem │ │ ├── styles.ts │ │ └── index.tsx │ ├── Themed.tsx │ └── EditScreenInfo.tsx ├── utils │ ├── AppContext.ts │ └── formatMoney.ts ├── hooks │ ├── useColorScheme.web.ts │ ├── useColorScheme.ts │ └── useCachedResources.ts ├── constants │ ├── Layout.ts │ └── Colors.ts ├── screens │ ├── RankingsScreen │ │ ├── queries.ts │ │ ├── styles.ts │ │ └── index.tsx │ ├── CoinExchangeScreen │ │ ├── mutations.ts │ │ ├── styles.ts │ │ └── index.tsx │ ├── MarketScreen │ │ ├── styles.ts │ │ └── index.tsx │ ├── PortfolioScreen │ │ ├── queries.ts │ │ ├── styles.ts │ │ └── index.tsx │ ├── HomeScreen │ │ ├── index.tsx │ │ └── styles.ts │ ├── TabOneScreen.tsx │ ├── TabTwoScreen.tsx │ ├── WelcomeScreen │ │ ├── styles.ts │ │ └── index.tsx │ ├── ProfileScreen │ │ ├── styles.ts │ │ └── index.tsx │ ├── NotFoundScreen.tsx │ └── CoinDetailsScreen │ │ ├── styles.ts │ │ └── index.tsx ├── navigation │ ├── LinkingConfiguration.ts │ ├── index.tsx │ └── BottomTabNavigator.tsx ├── graphql │ ├── mutations.ts │ ├── subscriptions.ts │ └── queries.ts └── API.ts ├── amplify ├── backend │ ├── function │ │ ├── vcryptoe6806c17 │ │ │ ├── parameters.json │ │ │ ├── src │ │ │ │ ├── event.json │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── index.js │ │ │ ├── amplify.state │ │ │ ├── function-parameters.json │ │ │ └── vcryptoe6806c17-cloudformation-template.json │ │ ├── vcryptof289a4cd │ │ │ ├── parameters.json │ │ │ ├── function-parameters.json │ │ │ ├── src │ │ │ │ ├── event.json │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── index.js │ │ │ ├── amplify.state │ │ │ └── vcryptof289a4cd-cloudformation-template.json │ │ ├── vcrypto16d3e4c5PostConfirmation │ │ │ ├── parameters.json │ │ │ ├── src │ │ │ │ ├── event.json │ │ │ │ ├── package.json │ │ │ │ ├── index.js │ │ │ │ ├── package-lock.json │ │ │ │ └── custom.js │ │ │ ├── amplify.state │ │ │ ├── function-parameters.json │ │ │ └── vcrypto16d3e4c5PostConfirmation-cloudformation-template.json │ │ └── vcrypto5fce58ab │ │ │ ├── function-parameters.json │ │ │ ├── parameters.json │ │ │ ├── src │ │ │ ├── event.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── index.js │ │ │ ├── amplify.state │ │ │ └── vcrypto5fce58ab-cloudformation-template.json │ ├── api │ │ └── vcrypto │ │ │ ├── transform.conf.json │ │ │ ├── parameters.json │ │ │ ├── schema.graphql │ │ │ └── stacks │ │ │ └── CustomResources.json │ ├── backend-config.json │ └── auth │ │ └── vcrypto16d3e4c5 │ │ ├── parameters.json │ │ └── vcrypto16d3e4c5-cloudformation-template.yml ├── .config │ └── project-config.json └── team-provider-info.json ├── assets ├── images │ ├── icon.png │ ├── user.png │ ├── Saly-1.png │ ├── splash.png │ ├── Saly-10.png │ ├── Saly-16.png │ ├── Saly-17.png │ ├── Saly-20.png │ ├── Saly-31.png │ ├── favicon.png │ ├── adaptive-icon.png │ ├── apple-button.png │ └── google-button.png └── fonts │ └── SpaceMono-Regular.ttf ├── babel.config.js ├── rn-cli.config.js ├── amplify.json ├── .expo-shared ├── assets.json └── README.md ├── tsconfig.json ├── types.tsx ├── .graphqlconfig.yml ├── .gitignore ├── LICENSE ├── app.json ├── package.json └── App.tsx /src/components/CoinPriceGraph/styles.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/parameters.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/parameters.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/parameters.json: -------------------------------------------------------------------------------- 1 | {"modules":"custom"} -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "lambdaLayers": [] 3 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "lambdaLayers": [] 3 | } -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/user.png -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "CloudWatchRule": "rate(5 minutes)" 3 | } -------------------------------------------------------------------------------- /assets/images/Saly-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/Saly-1.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /amplify/backend/api/vcrypto/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true 4 | } -------------------------------------------------------------------------------- /assets/images/Saly-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/Saly-10.png -------------------------------------------------------------------------------- /assets/images/Saly-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/Saly-16.png -------------------------------------------------------------------------------- /assets/images/Saly-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/Saly-17.png -------------------------------------------------------------------------------- /assets/images/Saly-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/Saly-20.png -------------------------------------------------------------------------------- /assets/images/Saly-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/Saly-31.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/apple-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/apple-button.png -------------------------------------------------------------------------------- /assets/images/google-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/images/google-button.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VadimNotJustDev/VCrupto/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "value1", 3 | "key2": "value2", 4 | "key3": "value3" 5 | } 6 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "value1", 3 | "key2": "value2", 4 | "key3": "value3" 5 | } 6 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key1": "value1", 3 | "key2": "value2", 4 | "key3": "value3" 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ExchangeCoins", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FetchCoinsPrices", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NetWorthCalculator", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/AppContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default React.createContext({ 4 | userId: null, 5 | setUserId: (id: string) => {}, 6 | }) 7 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "userPoolId": "testID", 4 | "userName": "testUser" 5 | }, 6 | "response": {} 7 | } -------------------------------------------------------------------------------- /rn-cli.config.js: -------------------------------------------------------------------------------- 1 | const blacklist = require('metro-config/src/defaults/blacklist') 2 | module.exports = { 3 | resolver: { 4 | blacklistRE: blacklist([/#current-cloud-backend\/.*/]), 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /amplify.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": 3 | { 4 | "graphqltransformer": 5 | { 6 | "transformerversion": 5 7 | }, 8 | "keytransformer": 9 | { 10 | "defaultquery": true 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true, 5 | "defaultEditorFile": "src/index.js" 6 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true, 5 | "defaultEditorFile": "src/index.js" 6 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true, 5 | "defaultEditorFile": "src/index.js" 6 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ExchangeCoins", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0" 7 | } 8 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true, 5 | "defaultEditorFile": "src/index.js" 6 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FetchCoinsPrices", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0" 7 | } 8 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NetWorthCalculator", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0" 7 | } 8 | -------------------------------------------------------------------------------- /src/components/StyledText.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Text, TextProps } from './Themed'; 4 | 5 | export function MonoText(props: TextProps) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | // useColorScheme from react-native does not support web currently. You can replace 2 | // this with react-native-appearance if you would like theme support on web. 3 | export default function useColorScheme() { 4 | return 'light'; 5 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "api": { 4 | "vcrypto": [ 5 | "create", 6 | "read", 7 | "update", 8 | "delete" 9 | ] 10 | } 11 | }, 12 | "lambdaLayers": [] 13 | } -------------------------------------------------------------------------------- /src/constants/Layout.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const width = Dimensions.get('window').width; 4 | const height = Dimensions.get('window').height; 5 | 6 | export default { 7 | window: { 8 | width, 9 | height, 10 | }, 11 | isSmallDevice: width < 375, 12 | }; 13 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vcrypto16d3e4c5PostConfirmation", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "axios": "latest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true, 3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true, 4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 6 | } 7 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /amplify/backend/api/vcrypto/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "vcrypto", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": false, 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authvcrypto16d3e4c5", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "esModuleInterop": true, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/screens/RankingsScreen/queries.ts: -------------------------------------------------------------------------------- 1 | export const getUsersByNetworth = /* GraphQL */ ` 2 | query getUsersByNetworth($limit: Int) { 3 | getUsersByNetworth(type: "User", sortDirection: DESC, limit: $limit) { 4 | items { 5 | id 6 | name 7 | image 8 | networth 9 | } 10 | nextToken 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /types.tsx: -------------------------------------------------------------------------------- 1 | export type RootStackParamList = { 2 | Root: undefined; 3 | NotFound: undefined; 4 | CoinDetails: undefined; 5 | CoinExchange: undefined; 6 | Welcome: undefined; 7 | }; 8 | 9 | export type BottomTabParamList = { 10 | Home: undefined; 11 | Portfolio: undefined; 12 | Market: undefined; 13 | Rankings: undefined; 14 | Profile: undefined; 15 | }; 16 | -------------------------------------------------------------------------------- /src/screens/CoinExchangeScreen/mutations.ts: -------------------------------------------------------------------------------- 1 | export const exchangeCoins = /* GraphQL */ ` 2 | mutation ExchangeCoins($coinId: ID!, $isBuy: Boolean!, $amount: Float!, $usdPortfolioCoinId: ID, $coinPortfolioCoinId: ID) { 3 | exchangeCoins(coinId: $coinId, isBuy: $isBuy, amount: $amount, usdPortfolioCoinId: $usdPortfolioCoinId, coinPortfolioCoinId: $coinPortfolioCoinId) 4 | } 5 | `; 6 | -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | vcrypto: 3 | schemaPath: src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.ts 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: typescript 11 | generatedFileName: src/API.ts 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /src/screens/MarketScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flex: 1, 6 | alignItems: 'center', 7 | padding: 20, 8 | backgroundColor: 'white', 9 | }, 10 | image: { 11 | height: 175, 12 | resizeMode: "contain", 13 | }, 14 | label: { 15 | fontSize: 18, 16 | fontWeight: 'bold', 17 | }, 18 | }); 19 | 20 | export default styles; 21 | -------------------------------------------------------------------------------- /src/screens/RankingsScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flex: 1, 6 | alignItems: 'center', 7 | padding: 20, 8 | backgroundColor: 'white', 9 | }, 10 | image: { 11 | height: 175, 12 | resizeMode: "contain", 13 | }, 14 | label: { 15 | fontSize: 18, 16 | fontWeight: 'bold', 17 | }, 18 | }); 19 | 20 | export default styles; 21 | -------------------------------------------------------------------------------- /src/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { ColorSchemeName, useColorScheme as _useColorScheme } from 'react-native'; 2 | 3 | // The useColorScheme value is always either light or dark, but the built-in 4 | // type suggests that it can be null. This will not happen in practice, so this 5 | // makes it a bit easier to work with. 6 | export default function useColorScheme(): NonNullable { 7 | return _useColorScheme() as NonNullable; 8 | } 9 | -------------------------------------------------------------------------------- /src/constants/Colors.ts: -------------------------------------------------------------------------------- 1 | const tintColorLight = '#2f95dc'; 2 | const tintColorDark = '#fff'; 3 | 4 | export default { 5 | light: { 6 | text: '#000', 7 | background: '#fff', 8 | tint: tintColorLight, 9 | tabIconDefault: '#ccc', 10 | tabIconSelected: tintColorLight, 11 | }, 12 | dark: { 13 | text: '#fff', 14 | background: '#000', 15 | tint: tintColorDark, 16 | tabIconDefault: '#ccc', 17 | tabIconSelected: tintColorDark, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/screens/PortfolioScreen/queries.ts: -------------------------------------------------------------------------------- 1 | export const getUserPortfolio = /* GraphQL */ ` 2 | query GetUser($id: ID!) { 3 | getUser(id: $id) { 4 | id 5 | networth 6 | portfolioCoins { 7 | items { 8 | id 9 | amount 10 | coin { 11 | id 12 | name 13 | symbol 14 | image 15 | currentPrice 16 | } 17 | } 18 | nextToken 19 | } 20 | } 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "VCrypto", 3 | "version": "3.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react-native", 7 | "config": { 8 | "SourceDir": "/", 9 | "DistributionDir": "/", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /src/components/PercentageChange/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | interface PercentageChangeProps { 5 | value: number, 6 | style?: object, 7 | } 8 | 9 | const PercentageChange = ({ value, style = {} }: PercentageChangeProps ) => { 10 | return ( 11 | 0 ? '#398f0a' : '#f10000'}]}> 12 | {value > 0 ? '+' : ''}{value.toPrecision(2)}% 13 | 14 | ); 15 | }; 16 | 17 | export default PercentageChange; 18 | -------------------------------------------------------------------------------- /src/navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | import * as Linking from 'expo-linking'; 2 | 3 | export default { 4 | prefixes: [Linking.makeUrl('/')], 5 | config: { 6 | screens: { 7 | Root: { 8 | screens: { 9 | TabOne: { 10 | screens: { 11 | TabOneScreen: 'one', 12 | }, 13 | }, 14 | TabTwo: { 15 | screens: { 16 | TabTwoScreen: 'two', 17 | }, 18 | }, 19 | }, 20 | }, 21 | NotFound: '*', 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | .idea/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | #amplify 17 | amplify/\#current-cloud-backend 18 | amplify/.config/local-* 19 | amplify/mock-data 20 | amplify/backend/amplify-meta.json 21 | amplify/backend/awscloudformation 22 | build/ 23 | dist/ 24 | node_modules/ 25 | aws-exports.js 26 | awsconfiguration.json 27 | amplifyconfiguration.json 28 | amplify-build-config.json 29 | amplify-gradle-config.json 30 | amplifytools.xcconfig -------------------------------------------------------------------------------- /src/screens/HomeScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, Image} from 'react-native'; 3 | import styles from './styles'; 4 | const image = require('../../../assets/images/Saly-1.png'); 5 | 6 | const HomeScreen = () => { 7 | return ( 8 | 9 | 10 | Welcome to VCrypto 11 | Invest your virtual $100.000 and compete with others 12 | 13 | ); 14 | }; 15 | 16 | export default HomeScreen; 17 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | this file will loop through all js modules which are uploaded to the lambda resource, 3 | provided that the file names (without extension) are included in the "MODULES" env variable. 4 | "MODULES" is a comma-delimmited string. 5 | */ 6 | 7 | exports.handler = (event, context, callback) => { 8 | const modules = process.env.MODULES.split(','); 9 | for (let i = 0; i < modules.length; i += 1) { 10 | const { handler } = require(`./${modules[i]}`); 11 | handler(event, context, callback); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.expo-shared/README.md: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".expo-shared" in my project? 2 | 3 | The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize". 4 | 5 | > What does the "assets.json" file contain? 6 | 7 | The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again. 8 | 9 | > Should I commit the ".expo-shared" folder? 10 | 11 | Yes, you should share the ".expo-shared" folder with your collaborators. 12 | -------------------------------------------------------------------------------- /src/graphql/mutations.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // this is an auto generated file. This will be overwritten 4 | 5 | export const exchangeCoins = /* GraphQL */ ` 6 | mutation ExchangeCoins( 7 | $coinId: ID! 8 | $isBuy: Boolean! 9 | $amount: Float! 10 | $usdPortfolioCoinId: ID 11 | $coinPortfolioCoinId: ID 12 | ) { 13 | exchangeCoins( 14 | coinId: $coinId 15 | isBuy: $isBuy 16 | amount: $amount 17 | usdPortfolioCoinId: $usdPortfolioCoinId 18 | coinPortfolioCoinId: $coinPortfolioCoinId 19 | ) 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/screens/HomeScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | padding: 20, 6 | alignItems: 'center', 7 | flex: 1, 8 | backgroundColor: 'white', 9 | }, 10 | image: { 11 | height: '40%', 12 | aspectRatio: 1, 13 | }, 14 | header1: { 15 | fontSize: 24, 16 | fontWeight: 'bold', 17 | marginTop: 50, 18 | marginBottom: 15, 19 | }, 20 | header2: { 21 | fontSize: 20, 22 | textAlign: 'center', 23 | color: '#707070', 24 | } 25 | }); 26 | 27 | export default styles; 28 | -------------------------------------------------------------------------------- /src/screens/PortfolioScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flex: 1, 6 | alignItems: 'center', 7 | padding: 20, 8 | backgroundColor: 'white', 9 | }, 10 | image: { 11 | height: 175, 12 | resizeMode: "contain", 13 | }, 14 | balanceContainer: { 15 | marginVertical: 20, 16 | width: '100%', 17 | }, 18 | label: { 19 | fontSize: 18, 20 | fontWeight: 'bold', 21 | color: '#777777' 22 | }, 23 | balance: { 24 | fontSize: 36, 25 | fontWeight: 'bold', 26 | } 27 | }); 28 | 29 | export default styles; 30 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "trigger": true, 3 | "modules": [ 4 | "custom" 5 | ], 6 | "parentResource": "vcrypto16d3e4c5", 7 | "functionName": "vcrypto16d3e4c5PostConfirmation", 8 | "resourceName": "vcrypto16d3e4c5PostConfirmation", 9 | "parentStack": "auth", 10 | "triggerEnvs": "[]", 11 | "triggerDir": "/usr/local/lib/node_modules/@aws-amplify/cli/node_modules/amplify-category-auth/provider-utils/awscloudformation/utils/../triggers/PostConfirmation", 12 | "triggerTemplate": "PostConfirmation.json.ejs", 13 | "roleName": "vcrypto16d3e4c5PostConfirmation", 14 | "skipEdit": true, 15 | "triggerEventPath": "PostConfirmation.event.json" 16 | } -------------------------------------------------------------------------------- /src/utils/formatMoney.ts: -------------------------------------------------------------------------------- 1 | export default (amount: number, decimalCount = 2, decimal = ".", thousands = ",") => { 2 | try { 3 | decimalCount = Math.abs(decimalCount); 4 | decimalCount = isNaN(decimalCount) ? 2 : decimalCount; 5 | 6 | const negativeSign = amount < 0 ? "-" : ""; 7 | 8 | let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString(); 9 | let j = (i.length > 3) ? i.length % 3 : 0; 10 | 11 | return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : ""); 12 | } catch (e) { 13 | console.log(e) 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/MarketCoin/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flexDirection: 'row', 6 | justifyContent: 'space-between', 7 | alignItems: 'center', 8 | height: 50, 9 | marginVertical: 10, 10 | }, 11 | image: { 12 | height: 50, 13 | width: 50, 14 | marginRight: 10, 15 | }, 16 | left: { 17 | flexDirection: 'row', 18 | alignItems: 'center', 19 | }, 20 | name: { 21 | fontWeight: 'bold', 22 | marginBottom: 5, 23 | }, 24 | value: { 25 | fontSize: 18, 26 | fontWeight: '600', 27 | marginBottom: 5, 28 | }, 29 | symbol: { 30 | color: '#6b6b6b', 31 | }, 32 | }); 33 | 34 | export default styles; 35 | -------------------------------------------------------------------------------- /src/components/PortfolioCoin/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flexDirection: 'row', 6 | justifyContent: 'space-between', 7 | alignItems: 'center', 8 | height: 50, 9 | marginVertical: 10, 10 | }, 11 | image: { 12 | height: 50, 13 | width: 50, 14 | marginRight: 10, 15 | }, 16 | left: { 17 | flexDirection: 'row', 18 | alignItems: 'center', 19 | }, 20 | name: { 21 | fontWeight: 'bold', 22 | marginBottom: 5, 23 | }, 24 | symbol: { 25 | color: '#6b6b6b', 26 | }, 27 | value: { 28 | fontSize: 18, 29 | fontWeight: '600', 30 | marginBottom: 5, 31 | }, 32 | }); 33 | 34 | export default styles; 35 | -------------------------------------------------------------------------------- /src/components/UserRankingItem/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flexDirection: 'row', 6 | justifyContent: 'space-between', 7 | alignItems: 'center', 8 | height: 50, 9 | marginVertical: 10, 10 | }, 11 | image: { 12 | height: 50, 13 | width: 50, 14 | marginRight: 10, 15 | borderRadius: 50, 16 | }, 17 | left: { 18 | flexDirection: 'row', 19 | alignItems: 'center', 20 | }, 21 | name: { 22 | fontWeight: 'bold', 23 | marginBottom: 5, 24 | }, 25 | value: { 26 | fontSize: 18, 27 | fontWeight: '600', 28 | marginBottom: 5, 29 | }, 30 | symbol: { 31 | color: '#6b6b6b', 32 | }, 33 | place: { 34 | fontSize: 18, 35 | width: 20, 36 | }, 37 | }); 38 | 39 | export default styles; 40 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vcrypto16d3e4c5PostConfirmation", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.21.1", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", 10 | "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", 11 | "requires": { 12 | "follow-redirects": "^1.10.0" 13 | } 14 | }, 15 | "follow-redirects": { 16 | "version": "1.13.2", 17 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", 18 | "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/screens/TabOneScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import EditScreenInfo from '../components/EditScreenInfo'; 5 | import { Text, View } from '../components/Themed'; 6 | 7 | export default function TabOneScreen() { 8 | return ( 9 | 10 | Tab One 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flex: 1, 20 | alignItems: 'center', 21 | justifyContent: 'center', 22 | }, 23 | title: { 24 | fontSize: 20, 25 | fontWeight: 'bold', 26 | }, 27 | separator: { 28 | marginVertical: 30, 29 | height: 1, 30 | width: '80%', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/screens/TabTwoScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import EditScreenInfo from '../components/EditScreenInfo'; 5 | import { Text, View } from '../components/Themed'; 6 | 7 | export default function TabTwoScreen() { 8 | return ( 9 | 10 | Tab Two 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flex: 1, 20 | alignItems: 'center', 21 | justifyContent: 'center', 22 | }, 23 | title: { 24 | fontSize: 20, 25 | fontWeight: 'bold', 26 | }, 27 | separator: { 28 | marginVertical: 30, 29 | height: 1, 30 | width: '80%', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/screens/WelcomeScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | padding: 20, 6 | alignItems: 'center', 7 | flex: 1, 8 | backgroundColor: 'white', 9 | }, 10 | image: { 11 | height: '40%', 12 | aspectRatio: 1, 13 | }, 14 | header1: { 15 | fontSize: 24, 16 | fontWeight: 'bold', 17 | marginTop: 50, 18 | marginBottom: 15, 19 | }, 20 | header2: { 21 | fontSize: 20, 22 | textAlign: 'center', 23 | color: '#707070', 24 | }, 25 | buttonContainer: { 26 | marginTop: 'auto', 27 | width: '100%', 28 | alignItems: 'center', 29 | marginBottom: 20 30 | }, 31 | googleButton: { 32 | width: '70%', 33 | height: 70, 34 | }, 35 | buttonImage: { 36 | width: '100%', 37 | height: '100%', 38 | resizeMode: 'contain', 39 | } 40 | }); 41 | 42 | export default styles; 43 | -------------------------------------------------------------------------------- /src/screens/ProfileScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flex: 1, 6 | alignItems: 'center', 7 | padding: 20, 8 | backgroundColor: 'white', 9 | }, 10 | image: { 11 | height: 175, 12 | resizeMode: "contain", 13 | }, 14 | 15 | userContainer: { 16 | flexDirection: 'row', 17 | justifyContent: 'flex-start', 18 | alignItems: 'center', 19 | height: 50, 20 | marginVertical: 10, 21 | width: '100%', 22 | }, 23 | userImage: { 24 | height: 100, 25 | width: 100, 26 | marginRight: 10, 27 | borderRadius: 50, 28 | }, 29 | name: { 30 | fontWeight: 'bold', 31 | }, 32 | email: { 33 | marginVertical: 5, 34 | }, 35 | value: { 36 | fontSize: 18, 37 | fontWeight: '600', 38 | marginBottom: 5, 39 | }, 40 | symbol: { 41 | color: '#6b6b6b', 42 | }, 43 | }); 44 | 45 | export default styles; 46 | -------------------------------------------------------------------------------- /src/components/UserRankingItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, Image} from 'react-native'; 3 | import styles from './styles' 4 | import formatMoney from "../../utils/formatMoney"; 5 | 6 | export interface UserRankingItemProps { 7 | user: { 8 | image: string, 9 | name: string, 10 | networth: number, 11 | }, 12 | place: number, 13 | } 14 | 15 | const UserRankingItem = (props: UserRankingItemProps) => { 16 | const { 17 | user: { 18 | image, 19 | name, 20 | networth 21 | }, 22 | place 23 | } = props; 24 | return ( 25 | 26 | 27 | {place} 28 | 29 | 30 | {name} 31 | 32 | 33 | 34 | ${formatMoney(networth, 0)} 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default UserRankingItem; 42 | -------------------------------------------------------------------------------- /src/hooks/useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import { Ionicons } from '@expo/vector-icons'; 2 | import * as Font from 'expo-font'; 3 | import * as SplashScreen from 'expo-splash-screen'; 4 | import * as React from 'react'; 5 | 6 | export default function useCachedResources() { 7 | const [isLoadingComplete, setLoadingComplete] = React.useState(false); 8 | 9 | // Load any resources or data that we need prior to rendering the app 10 | React.useEffect(() => { 11 | async function loadResourcesAndDataAsync() { 12 | try { 13 | SplashScreen.preventAutoHideAsync(); 14 | 15 | // Load fonts 16 | await Font.loadAsync({ 17 | ...Ionicons.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vadim Savin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "VCrypto", 4 | "slug": "VCrypto", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "vcrypto", 9 | "userInterfaceStyle": "light", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#2F68C1" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": [ 19 | "**/*" 20 | ], 21 | "ios": { 22 | "bundleIdentifier": "dev.notjust.vcrypto", 23 | "buildNumber": "1.0.3", 24 | "supportsTablet": true 25 | }, 26 | "android": { 27 | "package": "dev.notjust.vcrypto", 28 | "versionCode": 3, 29 | "adaptiveIcon": { 30 | "foregroundImage": "./assets/images/adaptive-icon.png", 31 | "backgroundColor": "#2F68C1" 32 | } 33 | }, 34 | "web": { 35 | "favicon": "./assets/images/favicon.png" 36 | }, 37 | "description": "Virtual Crypto Investment Platform", 38 | "githubUrl": "https://github.com/Savinvadim1312/VCrupto" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/screens/NotFoundScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StackScreenProps } from '@react-navigation/stack'; 2 | import * as React from 'react'; 3 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 4 | 5 | import { RootStackParamList } from '../../types'; 6 | 7 | export default function NotFoundScreen({ 8 | navigation, 9 | }: StackScreenProps) { 10 | return ( 11 | 12 | This screen doesn't exist. 13 | navigation.replace('Root')} style={styles.link}> 14 | Go to home screen! 15 | 16 | 17 | ); 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | backgroundColor: '#fff', 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | padding: 20, 27 | }, 28 | title: { 29 | fontSize: 20, 30 | fontWeight: 'bold', 31 | }, 32 | link: { 33 | marginTop: 15, 34 | paddingVertical: 15, 35 | }, 36 | linkText: { 37 | fontSize: 14, 38 | color: '#2e78b7', 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /amplify/backend/api/vcrypto/schema.graphql: -------------------------------------------------------------------------------- 1 | type User 2 | @model(mutations: null) 3 | @key(name: "byNetworth", fields: [ "type", "networth" ], queryField: "getUsersByNetworth") 4 | { 5 | id: ID! 6 | type: String! 7 | email: String! 8 | name: String 9 | image: String 10 | networth: Float! 11 | 12 | portfolioCoins: [PortfolioCoin] @connection(keyName: "byUser", fields: ["id"]) 13 | } 14 | 15 | type PortfolioCoin 16 | @model(mutations: null) 17 | @key(name: "byUser", fields: ["userId"]){ 18 | id: ID! 19 | amount: Float! 20 | 21 | userId: ID! 22 | user: User @connection(fields: ["userId"]) 23 | 24 | coinId: ID! 25 | coin: Coin @connection(fields: ["coinId"]) 26 | } 27 | 28 | type Coin @model(mutations: null) { 29 | id: ID! 30 | cgId: String! 31 | name: String! 32 | symbol: String! 33 | image: String 34 | currentPrice: Float! 35 | valueChange24H: Float! 36 | valueChange1D: Float! 37 | valueChange7D: Float! 38 | priceHistoryString: String 39 | } 40 | 41 | type Mutation { 42 | exchangeCoins( 43 | coinId: ID! 44 | isBuy: Boolean! 45 | amount: Float! 46 | usdPortfolioCoinId: ID 47 | coinPortfolioCoinId: ID 48 | ): Boolean! @function(name: "ExchangeCoins-${env}") 49 | } 50 | -------------------------------------------------------------------------------- /src/screens/CoinExchangeScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flex: 1, 6 | alignItems: 'center', 7 | padding: 20, 8 | backgroundColor: 'white', 9 | }, 10 | title: { 11 | fontSize: 24, 12 | fontWeight: 'bold', 13 | marginTop: 20, 14 | }, 15 | subtitle: { 16 | fontSize: 18, 17 | marginVertical: 10, 18 | color: '#5f5f5f' 19 | }, 20 | image: { 21 | height: 200, 22 | resizeMode: 'contain', 23 | }, 24 | 25 | inputsContainer: { 26 | flexDirection: 'row', 27 | justifyContent: 'space-between', 28 | width: '100%', 29 | alignItems: 'center', 30 | }, 31 | inputContainer: { 32 | flexDirection: 'row', 33 | justifyContent: 'space-between', 34 | borderWidth: 1, 35 | borderColor: '#b1b1b1', 36 | padding: 15, 37 | flex: 1, 38 | margin: 20, 39 | }, 40 | button: { 41 | backgroundColor: '#0097ff', 42 | marginTop: 'auto', 43 | width: '100%', 44 | height: 50, 45 | justifyContent: 'center', 46 | alignItems: 'center', 47 | borderRadius: 50, 48 | flexDirection: 'row', 49 | marginBottom: 20, 50 | }, 51 | buttonText: { 52 | color: 'white', 53 | fontSize: 18, 54 | } 55 | }); 56 | 57 | export default styles; 58 | -------------------------------------------------------------------------------- /src/components/Themed.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text as DefaultText, View as DefaultView } from 'react-native'; 3 | 4 | import Colors from '../constants/Colors'; 5 | import useColorScheme from '../hooks/useColorScheme'; 6 | 7 | export function useThemeColor( 8 | props: { light?: string; dark?: string }, 9 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 10 | ) { 11 | const theme = useColorScheme(); 12 | const colorFromProps = props[theme]; 13 | 14 | if (colorFromProps) { 15 | return colorFromProps; 16 | } else { 17 | return Colors[theme][colorName]; 18 | } 19 | } 20 | 21 | type ThemeProps = { 22 | lightColor?: string; 23 | darkColor?: string; 24 | }; 25 | 26 | export type TextProps = ThemeProps & DefaultText['props']; 27 | export type ViewProps = ThemeProps & DefaultView['props']; 28 | 29 | export function Text(props: TextProps) { 30 | const { style, lightColor, darkColor, ...otherProps } = props; 31 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 32 | 33 | return ; 34 | } 35 | 36 | export function View(props: ViewProps) { 37 | const { style, lightColor, darkColor, ...otherProps } = props; 38 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 39 | 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /src/screens/CoinDetailsScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | const styles = StyleSheet.create({ 4 | root: { 5 | flex: 1, 6 | alignItems: 'center', 7 | padding: 20, 8 | backgroundColor: 'white', 9 | }, 10 | 11 | topContainer: { 12 | flexDirection: 'row', 13 | justifyContent: 'space-between', 14 | alignItems: 'center', 15 | height: 50, 16 | marginVertical: 10, 17 | width: '100%', 18 | }, 19 | image: { 20 | height: 50, 21 | width: 50, 22 | marginRight: 10, 23 | borderRadius: 25, 24 | }, 25 | left: { 26 | flexDirection: 'row', 27 | alignItems: 'center', 28 | }, 29 | name: { 30 | fontWeight: 'bold', 31 | marginBottom: 5, 32 | }, 33 | symbol: { 34 | color: '#6b6b6b', 35 | }, 36 | 37 | row: { 38 | flexDirection: 'row', 39 | width: '100%', 40 | justifyContent: 'space-between', 41 | marginVertical: 15, 42 | }, 43 | valueContainer: { 44 | alignItems: 'center', 45 | marginHorizontal: 5, 46 | }, 47 | label: { 48 | color: '#545454', 49 | marginBottom: 5, 50 | }, 51 | value: { 52 | fontSize: 24, 53 | }, 54 | 55 | button: { 56 | flex: 1, 57 | margin: 5, 58 | height: 50, 59 | borderRadius: 30, 60 | alignItems: 'center', 61 | justifyContent: 'center', 62 | }, 63 | buttonText: { 64 | color: 'white', 65 | fontSize: 18, 66 | } 67 | }); 68 | 69 | export default styles; 70 | -------------------------------------------------------------------------------- /src/components/MarketCoin/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, Image, Pressable} from 'react-native'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | import styles from './styles' 5 | import PercentageChange from "../PercentageChange"; 6 | import formatMoney from "../../utils/formatMoney"; 7 | 8 | export interface PortfolioCoinProps { 9 | marketCoin: { 10 | id: string, 11 | image: string, 12 | name: string, 13 | symbol: string, 14 | valueChange24H: number, 15 | currentPrice: number, 16 | } 17 | } 18 | 19 | const PortfolioCoin = (props: PortfolioCoinProps) => { 20 | const { 21 | marketCoin: { 22 | id, 23 | image, 24 | name, 25 | symbol, 26 | valueChange24H, 27 | currentPrice, 28 | }, 29 | } = props; 30 | 31 | const navigation = useNavigation(); 32 | 33 | return ( 34 | navigation.navigate('CoinDetails', { id })}> 35 | 36 | 37 | 38 | {name} 39 | {symbol} 40 | 41 | 42 | 43 | ${formatMoney(currentPrice)} 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default PortfolioCoin; 51 | -------------------------------------------------------------------------------- /src/components/PortfolioCoin/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, Image, Pressable} from 'react-native'; 3 | import {useNavigation} from "@react-navigation/native"; 4 | import styles from './styles' 5 | import formatMoney from "../../utils/formatMoney"; 6 | 7 | export interface PortfolioCoinProps { 8 | portfolioCoin: { 9 | amount: number, 10 | coin: { 11 | id: string, 12 | image: string, 13 | name: string, 14 | symbol: string, 15 | currentPrice: number, 16 | } 17 | } 18 | } 19 | 20 | const PortfolioCoin = (props: PortfolioCoinProps) => { 21 | const { 22 | portfolioCoin: { 23 | 24 | amount, 25 | coin: { 26 | id, 27 | image, 28 | name, 29 | symbol, 30 | currentPrice, 31 | } 32 | }, 33 | } = props; 34 | 35 | const navigation = useNavigation(); 36 | 37 | return ( 38 | navigation.navigate('CoinDetails', { id })}> 39 | 40 | 41 | 42 | {name} 43 | {symbol} 44 | 45 | 46 | 47 | ${formatMoney(amount * currentPrice)} 48 | {symbol} {formatMoney(amount)} 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default PortfolioCoin; 55 | -------------------------------------------------------------------------------- /src/graphql/subscriptions.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // this is an auto generated file. This will be overwritten 4 | 5 | export const onCreateUser = /* GraphQL */ ` 6 | subscription OnCreateUser { 7 | onCreateUser { 8 | id 9 | email 10 | name 11 | image 12 | networth 13 | portfolioCoins { 14 | items { 15 | id 16 | amount 17 | userId 18 | coinId 19 | createdAt 20 | updatedAt 21 | } 22 | nextToken 23 | } 24 | createdAt 25 | updatedAt 26 | } 27 | } 28 | `; 29 | export const onUpdateUser = /* GraphQL */ ` 30 | subscription OnUpdateUser { 31 | onUpdateUser { 32 | id 33 | email 34 | name 35 | image 36 | networth 37 | portfolioCoins { 38 | items { 39 | id 40 | amount 41 | userId 42 | coinId 43 | createdAt 44 | updatedAt 45 | } 46 | nextToken 47 | } 48 | createdAt 49 | updatedAt 50 | } 51 | } 52 | `; 53 | export const onDeleteUser = /* GraphQL */ ` 54 | subscription OnDeleteUser { 55 | onDeleteUser { 56 | id 57 | email 58 | name 59 | image 60 | networth 61 | portfolioCoins { 62 | items { 63 | id 64 | amount 65 | userId 66 | coinId 67 | createdAt 68 | updatedAt 69 | } 70 | nextToken 71 | } 72 | createdAt 73 | updatedAt 74 | } 75 | } 76 | `; 77 | -------------------------------------------------------------------------------- /src/screens/MarketScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import {View, Text, Image, FlatList} from 'react-native'; 3 | import { API, graphqlOperation } from 'aws-amplify'; 4 | import styles from './styles'; 5 | import MarketCoin from "../../components/MarketCoin"; 6 | import { listCoins } from '../../graphql/queries' 7 | 8 | const image = require('../../../assets/images/Saly-17.png'); 9 | 10 | const PortfolioScreen = () => { 11 | const [coins, setCoins] = useState([]) 12 | const [loading, setLoading] = useState(false) 13 | 14 | const fetchCoins = async () => { 15 | setLoading(true); 16 | try { 17 | const response = await API.graphql(graphqlOperation(listCoins)); 18 | setCoins(response.data.listCoins.items); 19 | } catch (e) { 20 | console.error(e); 21 | } finally { 22 | setLoading(false); 23 | } 24 | } 25 | 26 | useEffect(() => { 27 | fetchCoins(); 28 | }, []) 29 | 30 | return ( 31 | 32 | } 38 | showsVerticalScrollIndicator={false} 39 | ListHeaderComponentStyle={{alignItems: 'center'}} 40 | ListHeaderComponent={() => ( 41 | <> 42 | 43 | Market 44 | 45 | )} 46 | /> 47 | 48 | ); 49 | }; 50 | 51 | export default PortfolioScreen; 52 | -------------------------------------------------------------------------------- /src/screens/RankingsScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {View, Text, Image, FlatList} from 'react-native'; 3 | import { API, graphqlOperation } from 'aws-amplify'; 4 | import { getUsersByNetworth } from './queries'; 5 | import styles from './styles'; 6 | import UserRangeItem from "../../components/UserRankingItem"; 7 | const image = require('../../../assets/images/Saly-20.png'); 8 | 9 | const RankingsScreen = () => { 10 | const [users, setUsers] = useState([]); 11 | const [loading, setLoading] = useState(false); 12 | 13 | const fetchUsers = async () => { 14 | setLoading(true); 15 | try { 16 | const response = await API.graphql(graphqlOperation(getUsersByNetworth, { limit: 100 })); 17 | setUsers(response.data.getUsersByNetworth.items); 18 | } catch (e) { 19 | console.log(e); 20 | } finally { 21 | setLoading(false); 22 | } 23 | }; 24 | 25 | useEffect(() => { 26 | fetchUsers(); 27 | }, []) 28 | 29 | return ( 30 | 31 | } 35 | onRefresh={fetchUsers} 36 | refreshing={loading} 37 | showsVerticalScrollIndicator={false} 38 | ListHeaderComponentStyle={{alignItems: 'center'}} 39 | ListHeaderComponent={() => ( 40 | <> 41 | 42 | Rankings 43 | 44 | )} 45 | /> 46 | 47 | ); 48 | }; 49 | 50 | export default RankingsScreen; 51 | -------------------------------------------------------------------------------- /src/components/CoinPriceGraph/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, Dimensions} from 'react-native'; 3 | import {LineChart} from "react-native-chart-kit"; 4 | 5 | interface CoinPriceGraphProps { 6 | dataString: string; 7 | } 8 | 9 | const CoinPriceGraph = ({ dataString }: CoinPriceGraphProps) => { 10 | 11 | const data = JSON.parse(dataString); 12 | 13 | return ( 14 | 15 | `rgba(18, 85, 255, ${opacity})`, 37 | labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`, 38 | style: { 39 | borderRadius: 16 40 | }, 41 | propsForDots: { 42 | r: "0", 43 | strokeWidth: "1", 44 | stroke: "#fafafa" 45 | } 46 | }} 47 | style={{ 48 | marginVertical: 8, 49 | borderRadius: 16 50 | }} 51 | /> 52 | 53 | ); 54 | }; 55 | 56 | export default CoinPriceGraph; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject", 9 | "test": "jest --watchAll" 10 | }, 11 | "jest": { 12 | "preset": "jest-expo" 13 | }, 14 | "dependencies": { 15 | "@expo/vector-icons": "^12.0.0", 16 | "@react-native-community/masked-view": "0.1.10", 17 | "@react-native-community/netinfo": "^6.0.0", 18 | "@react-navigation/bottom-tabs": "5.11.2", 19 | "@react-navigation/native": "~5.8.10", 20 | "@react-navigation/stack": "~5.12.8", 21 | "aws-amplify": "^3.3.20", 22 | "aws-amplify-react-native": "^4.3.1", 23 | "expo": "~40.0.0", 24 | "expo-asset": "~8.2.1", 25 | "expo-constants": "~9.3.0", 26 | "expo-font": "~8.4.0", 27 | "expo-linking": "~2.0.0", 28 | "expo-splash-screen": "~0.8.0", 29 | "expo-status-bar": "~1.0.3", 30 | "expo-web-browser": "~8.6.0", 31 | "react": "16.13.1", 32 | "react-dom": "16.13.1", 33 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz", 34 | "react-native-chart-kit": "^6.11.0", 35 | "react-native-gesture-handler": "~1.8.0", 36 | "react-native-safe-area-context": "3.1.9", 37 | "react-native-screens": "~2.15.0", 38 | "react-native-svg": "^12.1.0", 39 | "react-native-web": "~0.13.12", 40 | "react-number-format": "^4.4.4" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "~7.9.0", 44 | "@types/react": "~16.9.35", 45 | "@types/react-native": "~0.63.2", 46 | "jest-expo": "~40.0.0", 47 | "typescript": "~4.0.0" 48 | }, 49 | "private": true 50 | } 51 | -------------------------------------------------------------------------------- /amplify/backend/api/vcrypto/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | } 34 | }, 35 | "Conditions": { 36 | "HasEnvironmentParameter": { 37 | "Fn::Not": [ 38 | { 39 | "Fn::Equals": [ 40 | { 41 | "Ref": "env" 42 | }, 43 | "NONE" 44 | ] 45 | } 46 | ] 47 | }, 48 | "AlwaysFalse": { 49 | "Fn::Equals": ["true", "false"] 50 | } 51 | }, 52 | "Outputs": { 53 | "EmptyOutput": { 54 | "Description": "An empty output. You may delete this if you have at least one resource above.", 55 | "Value": "" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/src/custom.js: -------------------------------------------------------------------------------- 1 | const aws = require('aws-sdk'); 2 | const ddb = new aws.DynamoDB(); 3 | 4 | exports.handler = async (event, context) => { 5 | if (!event.request.userAttributes.sub) { 6 | console.log("Error: No user was written to DynamoDB") 7 | context.done(null, event); 8 | return; 9 | } 10 | 11 | // Save the user to DynamoDB 12 | const date = new Date(); 13 | 14 | const Item = { 15 | 'id': { S: event.request.userAttributes.sub }, 16 | '__typename': { S: 'User' }, 17 | 'type': { S: 'User' }, 18 | 'email': { S: event.request.userAttributes.email }, 19 | 'createdAt': { S: date.toISOString() }, 20 | 'updatedAt': { S: date.toISOString() }, 21 | 'networth': { N: "100000.0" } 22 | } 23 | 24 | if (event.request.userAttributes.picture) { 25 | Item.image = { S: event.request.userAttributes.picture }; 26 | } 27 | 28 | if (event.request.userAttributes.name) { 29 | Item.name = { S: event.request.userAttributes.name }; 30 | } 31 | 32 | const params = { 33 | Item, 34 | TableName: process.env.USERTABLE, 35 | } 36 | 37 | try { 38 | await ddb.putItem(params).promise(); 39 | console.log("Success"); 40 | } catch (e) { 41 | console.log("Error", e); 42 | } 43 | 44 | const PortfolionCoinItem = { 45 | 'id': { S: `${event.request.userAttributes.sub}-usd` }, 46 | '__typename': { S: 'PortfolioCoin' }, 47 | 'createdAt': { S: date.toISOString() }, 48 | 'updatedAt': { S: date.toISOString() }, 49 | 'userId': { S: event.request.userAttributes.sub }, 50 | 'coinId': { S: process.env.USD_COIN_ID }, 51 | 'amount': { N: "100000.0" } 52 | } 53 | 54 | try { 55 | await ddb.putItem({ 56 | Item: PortfolionCoinItem, 57 | TableName: process.env.PORTFOLIO_COIN_TABLE, 58 | }).promise(); 59 | console.log("Success"); 60 | } catch (e) { 61 | console.log("Error", e); 62 | } 63 | context.done(null, event); 64 | } 65 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Platform, Linking } from 'react-native'; 3 | import { StatusBar } from 'expo-status-bar'; 4 | import { SafeAreaProvider } from 'react-native-safe-area-context'; 5 | import Amplify from 'aws-amplify' 6 | import * as WebBrowser from 'expo-web-browser'; 7 | 8 | import useCachedResources from './src/hooks/useCachedResources'; 9 | import useColorScheme from './src/hooks/useColorScheme'; 10 | import Navigation from './src/navigation'; 11 | import AppContext from './src/utils/AppContext'; 12 | // @ts-ignore 13 | import awsConfig from './aws-exports' 14 | 15 | const [ 16 | productionRedirectSignIn, 17 | localRedirectSignIn, 18 | ] = awsConfig.oauth.redirectSignIn.split(","); 19 | 20 | const [ 21 | productionRedirectSignOut, 22 | localRedirectSignOut, 23 | ] = awsConfig.oauth.redirectSignOut.split(","); 24 | 25 | async function urlOpener(url: string, redirectUrl: string) { 26 | const { type, url: newUrl } = await WebBrowser.openAuthSessionAsync( 27 | url, 28 | redirectUrl 29 | ); 30 | 31 | if (type === 'success' && Platform.OS === 'ios') { 32 | WebBrowser.dismissBrowser(); 33 | return Linking.openURL(newUrl); 34 | } 35 | } 36 | 37 | 38 | Amplify.configure({ 39 | ...awsConfig, 40 | oauth: { 41 | ...awsConfig.oauth, 42 | urlOpener, 43 | redirectSignIn: __DEV__ ? localRedirectSignIn : productionRedirectSignIn, 44 | redirectSignOut: __DEV__ ? localRedirectSignOut : productionRedirectSignOut, 45 | }, 46 | }); 47 | 48 | 49 | 50 | export default function App() { 51 | const [userId, setUserId] = useState(null); 52 | 53 | const isLoadingComplete = useCachedResources(); 54 | const colorScheme = useColorScheme(); 55 | 56 | if (!isLoadingComplete) { 57 | return null; 58 | } else { 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/screens/ProfileScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import {View, Text, Image, Pressable, ActivityIndicator} from 'react-native'; 3 | import styles from './styles'; 4 | import {Auth, API, graphqlOperation} from "aws-amplify"; 5 | import {CommonActions, useNavigation} from "@react-navigation/native"; 6 | import {getUser} from '../../graphql/queries'; 7 | import AppContext from "../../utils/AppContext"; 8 | import formatMoney from "../../utils/formatMoney"; 9 | const image = require('../../../assets/images/Saly-16.png'); 10 | 11 | const ProfileScreen = () => { 12 | const [user, setUser] = useState(null) 13 | const { userId } = useContext(AppContext); 14 | 15 | const navigation = useNavigation(); 16 | 17 | useEffect(() => { 18 | const fetchUser = async () => { 19 | try { 20 | const response = await API.graphql( 21 | graphqlOperation(getUser, {id: userId}) 22 | ); 23 | setUser(response.data.getUser); 24 | } catch (e) { 25 | console.log(e); 26 | } 27 | } 28 | fetchUser(); 29 | }, []) 30 | 31 | const signOut = async () => { 32 | await Auth.signOut(); 33 | navigation.dispatch( 34 | CommonActions.reset({ 35 | index: 0, 36 | routes: [ 37 | { name: 'Welcome' }, 38 | ], 39 | }) 40 | ); 41 | } 42 | 43 | if (!user) { 44 | return () 45 | } 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | {user.name} 55 | {user.email} 56 | ${formatMoney(user.networth, 0)} 57 | 58 | 59 | 60 | 61 | Sign out 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default ProfileScreen; 68 | -------------------------------------------------------------------------------- /src/navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import * as React from 'react'; 4 | import { ColorSchemeName } from 'react-native'; 5 | 6 | import NotFoundScreen from '../screens/NotFoundScreen'; 7 | import { RootStackParamList } from '../../types'; 8 | import BottomTabNavigator from './BottomTabNavigator'; 9 | import LinkingConfiguration from './LinkingConfiguration'; 10 | import CoinDetailsScreen from "../screens/CoinDetailsScreen"; 11 | import CoinExchangeScreen from "../screens/CoinExchangeScreen"; 12 | import WelcomeScreen from "../screens/WelcomeScreen"; 13 | 14 | // If you are not familiar with React Navigation, we recommend going through the 15 | // "Fundamentals" guide: https://reactnavigation.org/docs/getting-started 16 | export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) { 17 | return ( 18 | 21 | 22 | 23 | ); 24 | } 25 | 26 | // A root stack navigator is often used for displaying modals on top of all other content 27 | // Read more here: https://reactnavigation.org/docs/modal 28 | const Stack = createStackNavigator(); 29 | 30 | function RootNavigator() { 31 | return ( 32 | 33 | 40 | 47 | 54 | 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "vcrypto16d3e4c5": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [ 7 | { 8 | "category": "function", 9 | "resourceName": "vcrypto16d3e4c5PostConfirmation", 10 | "triggerProvider": "Cognito", 11 | "attributes": [ 12 | "Arn", 13 | "Name" 14 | ] 15 | } 16 | ] 17 | } 18 | }, 19 | "function": { 20 | "vcrypto16d3e4c5PostConfirmation": { 21 | "build": true, 22 | "providerPlugin": "awscloudformation", 23 | "service": "Lambda" 24 | }, 25 | "vcryptoe6806c17": { 26 | "build": true, 27 | "providerPlugin": "awscloudformation", 28 | "service": "Lambda", 29 | "dependsOn": [ 30 | { 31 | "category": "api", 32 | "resourceName": "vcrypto", 33 | "attributes": [ 34 | "GraphQLAPIIdOutput", 35 | "GraphQLAPIEndpointOutput" 36 | ] 37 | } 38 | ] 39 | }, 40 | "vcrypto5fce58ab": { 41 | "build": true, 42 | "providerPlugin": "awscloudformation", 43 | "service": "Lambda", 44 | "dependsOn": [] 45 | }, 46 | "vcryptof289a4cd": { 47 | "build": true, 48 | "providerPlugin": "awscloudformation", 49 | "service": "Lambda", 50 | "dependsOn": [] 51 | } 52 | }, 53 | "api": { 54 | "vcrypto": { 55 | "service": "AppSync", 56 | "providerPlugin": "awscloudformation", 57 | "output": { 58 | "authConfig": { 59 | "defaultAuthentication": { 60 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 61 | "userPoolConfig": { 62 | "userPoolId": "authvcrypto16d3e4c5" 63 | } 64 | }, 65 | "additionalAuthenticationProviders": [] 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/screens/PortfolioScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import {FlatList, Image, Text, View} from 'react-native'; 3 | import {API, graphqlOperation} from 'aws-amplify'; 4 | import {useNavigation} from '@react-navigation/native'; 5 | import {getUserPortfolio} from './queries'; 6 | import styles from './styles'; 7 | import PortfolioCoin from "../../components/PortfolioCoin"; 8 | import AppContext from "../../utils/AppContext"; 9 | import formatMoney from "../../utils/formatMoney"; 10 | 11 | const image = require('../../../assets/images/Saly-10.png'); 12 | 13 | const PortfolioScreen = () => { 14 | const [balance, setBalance] = useState(0); 15 | const [portfolioCoins, setPortfolioCoins] = useState([]); 16 | const [loading, setLoading] = useState(false); 17 | 18 | const navigation = useNavigation(); 19 | 20 | const { userId } = useContext(AppContext); 21 | 22 | const fetchPortfolio = async () => { 23 | setLoading(true); 24 | try { 25 | const response = await API.graphql( 26 | graphqlOperation( 27 | getUserPortfolio, 28 | { id: userId }, 29 | ) 30 | ) 31 | setBalance(response.data.getUser.networth) 32 | setPortfolioCoins(response.data.getUser.portfolioCoins.items) 33 | } catch (e) { 34 | console.log(e) 35 | } finally { 36 | setLoading(false); 37 | } 38 | }; 39 | 40 | useEffect(() => { 41 | fetchPortfolio(); 42 | }, []) 43 | 44 | React.useEffect(() => { 45 | return navigation.addListener('focus', () => { 46 | fetchPortfolio(); 47 | }); 48 | }, [navigation]); 49 | 50 | return ( 51 | 52 | } 56 | onRefresh={fetchPortfolio} 57 | refreshing={loading} 58 | showsVerticalScrollIndicator={false} 59 | ListHeaderComponentStyle={{alignItems: 'center'}} 60 | ListHeaderComponent={() => ( 61 | <> 62 | 63 | 64 | Portfolio balance 65 | ${formatMoney(balance)} 66 | 67 | 68 | )} 69 | /> 70 | 71 | ); 72 | }; 73 | 74 | export default PortfolioScreen; 75 | -------------------------------------------------------------------------------- /src/navigation/BottomTabNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesome, Entypo, AntDesign, FontAwesome5, MaterialIcons } from '@expo/vector-icons'; 2 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 3 | import * as React from 'react'; 4 | 5 | import Colors from '../constants/Colors'; 6 | import useColorScheme from '../hooks/useColorScheme'; 7 | import { BottomTabParamList } from '../../types'; 8 | import HomeScreen from "../screens/HomeScreen"; 9 | import PortfolioScreen from "../screens/PortfolioScreen"; 10 | import MarketScreen from "../screens/MarketScreen"; 11 | import RankingsScreen from "../screens/RankingsScreen"; 12 | import ProfileScreen from "../screens/ProfileScreen"; 13 | 14 | const BottomTab = createBottomTabNavigator(); 15 | 16 | export default function BottomTabNavigator() { 17 | const colorScheme = useColorScheme(); 18 | 19 | return ( 20 | 23 | , 28 | }} 29 | /> 30 | , 35 | }} 36 | /> 37 | , 42 | }} 43 | /> 44 | , 49 | }} 50 | /> 51 | , 56 | }} 57 | /> 58 | 59 | ); 60 | } 61 | 62 | // You can explore the built-in icon families and icons on the web at: 63 | // https://icons.expo.fyi/ 64 | function TabBarIcon(props: { name: React.ComponentProps['name']; color: string }) { 65 | return ; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/screens/WelcomeScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect} from 'react'; 2 | import {View, Text, Image, Pressable, Platform} from 'react-native'; 3 | import { Auth, Hub } from 'aws-amplify'; 4 | import { CognitoHostedUIIdentityProvider } from "@aws-amplify/auth/lib/types"; 5 | import styles from './styles'; 6 | import {useNavigation, CommonActions} from "@react-navigation/native"; 7 | import AppContext from "../../utils/AppContext"; 8 | 9 | const image = require('../../../assets/images/Saly-1.png'); 10 | const googleButtonImage = require('../../../assets/images/google-button.png'); 11 | const appleButtonImage = require('../../../assets/images/apple-button.png'); 12 | 13 | const WelcomeScreen = () => { 14 | const navigation = useNavigation(); 15 | const { setUserId } = useContext(AppContext); 16 | 17 | useEffect(() => { 18 | const fetchUser = async () => { 19 | const user = await Auth.currentAuthenticatedUser(); 20 | if (user) { 21 | setUserId(user.attributes.sub) 22 | navigation.dispatch( 23 | CommonActions.reset({ 24 | index: 0, 25 | routes: [ 26 | { name: 'Root' }, 27 | ], 28 | }) 29 | ); 30 | } 31 | } 32 | 33 | fetchUser(); 34 | }, []) 35 | 36 | useEffect(() => { 37 | Hub.listen("auth", ({ payload: { event, data } }) => { 38 | if (event === "signIn") { 39 | setUserId(data.signInUserSession.accessToken.payload.sub) 40 | navigation.dispatch( 41 | CommonActions.reset({ 42 | index: 0, 43 | routes: [ 44 | { name: 'Root' }, 45 | ], 46 | }) 47 | ); 48 | } 49 | }); 50 | }, []) 51 | 52 | const signInGoogle = async () => { 53 | await Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google}); 54 | } 55 | 56 | const signInApple = async () => { 57 | await Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Apple }); 58 | } 59 | 60 | return ( 61 | 62 | 63 | Welcome to VCrypto 64 | Invest your virtual $100.000 and compete with others 65 | 66 | 67 | {Platform.OS === 'ios' && ( 68 | 69 | 70 | 71 | )} 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export default WelcomeScreen; 82 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/src/index.js: -------------------------------------------------------------------------------- 1 | const { DynamoDB } = require('aws-sdk'); 2 | const ddb = new DynamoDB(); 3 | 4 | const USER_TABLE = process.env.USER_TABLE; 5 | const COIN_TABLE = process.env.COIN_TABLE; 6 | const PORTFOLIO_COIN_TABLE = process.env.PORTFOLIO_COIN_TABLE; 7 | 8 | const getAllCoins = async () => { 9 | const params = { 10 | TableName: COIN_TABLE, 11 | ProjectionExpression: 'id,currentPrice', 12 | } 13 | 14 | const coins = await ddb.scan(params).promise(); 15 | return coins.Items.map(coin => ({ 16 | id: coin.id.S, 17 | currentPrice: parseFloat(coin.currentPrice.N) 18 | })); 19 | } 20 | 21 | const getAllUsers = async () => { 22 | const params = { 23 | TableName: USER_TABLE, 24 | ProjectionExpression: 'id', 25 | } 26 | 27 | const users = await ddb.scan(params).promise(); 28 | return users.Items.map(user => ({ 29 | id: user.id.S 30 | })); 31 | } 32 | 33 | const getUserCoins = async (user) => { 34 | const params = { 35 | TableName: PORTFOLIO_COIN_TABLE, 36 | IndexName: 'byUser', 37 | KeyConditionExpression: "userId = :userId", 38 | ExpressionAttributeValues: { 39 | ":userId": { S: user.id } 40 | }, 41 | ProjectionExpression: 'coinId,amount', 42 | } 43 | 44 | const usersCoins = await ddb.query(params).promise(); 45 | return usersCoins.Items.map(usersCoin => ({ 46 | coinId: usersCoin.coinId.S, 47 | amount: parseFloat(usersCoin.amount.N) 48 | })); 49 | } 50 | 51 | const getUserCoinPrice = (userCoin, coins) => { 52 | const coin = coins.find(c => c.id === userCoin.coinId); 53 | return coin ? coin.currentPrice : 0; 54 | } 55 | 56 | const updateUserNetWorth = async (user, newNetWorth) => { 57 | console.log(`User ${user.id} new networth: ${newNetWorth}`); 58 | 59 | const params = { 60 | TableName: USER_TABLE, 61 | Key: { 62 | id: { S: user.id } 63 | }, 64 | UpdateExpression: 'SET networth = :networth', 65 | ExpressionAttributeValues: { 66 | ":networth": { N: newNetWorth.toString() } 67 | } 68 | } 69 | 70 | await ddb.updateItem(params).promise(); 71 | } 72 | 73 | const calculateUserNetWorth = async (user, coins) => { 74 | const userCoins = await getUserCoins(user); 75 | 76 | const sumUserCoins = (sum, userCoin) => sum + userCoin.amount * getUserCoinPrice(userCoin, coins); 77 | 78 | const netWorth = userCoins.reduce(sumUserCoins, 0) 79 | 80 | return updateUserNetWorth(user, netWorth); 81 | } 82 | 83 | exports.handler = async () => { 84 | const coins = await getAllCoins(); 85 | const users = await getAllUsers(); 86 | 87 | await Promise.all(users.map(user => calculateUserNetWorth(user, coins))); 88 | 89 | return true; 90 | }; 91 | -------------------------------------------------------------------------------- /amplify/backend/auth/vcrypto16d3e4c5/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "vcrypto16d3e4c5_identitypool_16d3e4c5", 3 | "allowUnauthenticatedIdentities": false, 4 | "resourceNameTruncated": "vcrypt16d3e4c5", 5 | "userPoolName": "vcrypto16d3e4c5_userpool_16d3e4c5", 6 | "autoVerifiedAttributes": [ 7 | "email" 8 | ], 9 | "mfaConfiguration": "OFF", 10 | "mfaTypes": [ 11 | "SMS Text Message" 12 | ], 13 | "smsAuthenticationMessage": "Your authentication code is {####}", 14 | "smsVerificationMessage": "Your verification code is {####}", 15 | "emailVerificationSubject": "Your verification code", 16 | "emailVerificationMessage": "Your verification code is {####}", 17 | "defaultPasswordPolicy": false, 18 | "passwordPolicyMinLength": 8, 19 | "passwordPolicyCharacters": [], 20 | "requiredAttributes": [ 21 | "email" 22 | ], 23 | "userpoolClientGenerateSecret": true, 24 | "userpoolClientRefreshTokenValidity": 30, 25 | "userpoolClientWriteAttributes": [ 26 | "email" 27 | ], 28 | "userpoolClientReadAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientLambdaRole": "vcrypt16d3e4c5_userpoolclient_lambda_role", 32 | "userpoolClientSetAttributes": false, 33 | "sharedId": "16d3e4c5", 34 | "resourceName": "vcrypto16d3e4c5", 35 | "authSelections": "identityPoolAndUserPool", 36 | "authRoleArn": { 37 | "Fn::GetAtt": [ 38 | "AuthRole", 39 | "Arn" 40 | ] 41 | }, 42 | "unauthRoleArn": { 43 | "Fn::GetAtt": [ 44 | "UnauthRole", 45 | "Arn" 46 | ] 47 | }, 48 | "useDefault": "manual", 49 | "hostedUI": true, 50 | "triggers": "{\"PostConfirmation\":[\"custom\"]}", 51 | "hostedUIDomainName": "vcrypto16d3e4c5-16d3e4c5", 52 | "authProvidersUserPool": [ 53 | "Google" 54 | ], 55 | "hostedUIProviderMeta": "[{\"ProviderName\":\"Google\",\"authorize_scopes\":\"openid email profile\",\"AttributeMapping\":{\"email\":\"email\",\"username\":\"sub\"}}]", 56 | "userPoolGroupList": [], 57 | "parentStack": { 58 | "Ref": "AWS::StackId" 59 | }, 60 | "permissions": [], 61 | "dependsOn": [ 62 | { 63 | "category": "function", 64 | "resourceName": "vcrypto16d3e4c5PostConfirmation", 65 | "triggerProvider": "Cognito", 66 | "attributes": [ 67 | "Arn", 68 | "Name" 69 | ] 70 | } 71 | ], 72 | "thirdPartyAuth": false, 73 | "userPoolGroups": false, 74 | "adminQueries": false, 75 | "oAuthMetadata": "{\"AllowedOAuthFlows\":[\"code\"],\"AllowedOAuthScopes\":[\"phone\",\"email\",\"openid\",\"profile\",\"aws.cognito.signin.user.admin\"],\"CallbackURLs\":[\"vcrypto://\",\"exp://127.0.0.1:19000/--/\"],\"LogoutURLs\":[\"vcrypto://\",\"exp://127.0.0.1:19000/--/\"]}" 76 | } -------------------------------------------------------------------------------- /amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-vcrypto-dev-132939-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::704219588443:role/amplify-vcrypto-dev-132939-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::704219588443:role/amplify-vcrypto-dev-132939-authRole", 7 | "Region": "eu-west-1", 8 | "DeploymentBucketName": "amplify-vcrypto-dev-132939-deployment", 9 | "UnauthRoleName": "amplify-vcrypto-dev-132939-unauthRole", 10 | "StackName": "amplify-vcrypto-dev-132939", 11 | "StackId": "arn:aws:cloudformation:eu-west-1:704219588443:stack/amplify-vcrypto-dev-132939/868f6190-72b6-11eb-832b-06f0a0237781", 12 | "AmplifyAppId": "dq12imswussk4" 13 | }, 14 | "categories": { 15 | "auth": { 16 | "vcrypto16d3e4c5": { 17 | "hostedUIProviderCreds": "[{\"ProviderName\":\"Google\",\"client_id\":\"851778055316-vfkk3itsq4s3cejhuc4sg51mogguntca.apps.googleusercontent.com\",\"client_secret\":\"isIeWL4D_oy2PMB0w-lVLkqg\"}]", 18 | "googleClientId": "851778055316-vfkk3itsq4s3cejhuc4sg51mogguntca.apps.googleusercontent.com" 19 | } 20 | }, 21 | "function": { 22 | "vcrypto16d3e4c5PostConfirmation": {}, 23 | "vcryptoe6806c17": {}, 24 | "vcrypto5fce58ab": {}, 25 | "vcryptof289a4cd": {} 26 | } 27 | } 28 | }, 29 | "prod": { 30 | "awscloudformation": { 31 | "AuthRoleName": "amplify-vcrypto-prod-153702-authRole", 32 | "UnauthRoleArn": "arn:aws:iam::704219588443:role/amplify-vcrypto-prod-153702-unauthRole", 33 | "AuthRoleArn": "arn:aws:iam::704219588443:role/amplify-vcrypto-prod-153702-authRole", 34 | "Region": "eu-west-1", 35 | "DeploymentBucketName": "amplify-vcrypto-prod-153702-deployment", 36 | "UnauthRoleName": "amplify-vcrypto-prod-153702-unauthRole", 37 | "StackName": "amplify-vcrypto-prod-153702", 38 | "StackId": "arn:aws:cloudformation:eu-west-1:704219588443:stack/amplify-vcrypto-prod-153702/513bfd50-777f-11eb-b48c-024ded4fa113", 39 | "AmplifyAppId": "dq12imswussk4" 40 | }, 41 | "categories": { 42 | "auth": { 43 | "vcrypto16d3e4c5": { 44 | "hostedUIProviderCreds": "[{\"ProviderName\":\"Google\",\"client_id\":\"172321385729-ml685eeocpm5888jek8pflb5v8230vig.apps.googleusercontent.com\",\"client_secret\":\"y2OBjtd3xwFOw2fwvJUEHcz8\"}]" 45 | } 46 | }, 47 | "function": { 48 | "vcrypto16d3e4c5PostConfirmation": {}, 49 | "vcryptoe6806c17": {}, 50 | "vcrypto5fce58ab": {}, 51 | "vcryptof289a4cd": {} 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/src/index.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | const aws = require('aws-sdk'); 3 | 4 | const ddb = new aws.DynamoDB(); 5 | const lambda = new aws.Lambda(); 6 | 7 | const URL = 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=USD&ids=bitcoin%2Cethereum%2Ctether%2Cpolkadot%2Ccardano%2Cbinancecoin%2Cripple%2Clitecoin%2Cchainlink%2Cbitcoin-cash%2Cstellar%2Cusd-coin%2Cdogecoin%2Cwrapped-bitcoin%2Cuniswap%2Caave%2Ccosmos%2Ceos%2Cmonero%2Cbitcoin-cash-sv%2Ciota%2Ctron%2Cnem%2Ctezos%2Cvechain%2Ctheta-token%2Chavven%2Cavalanche-2%2Cneo%2Chuobi-token%2Cterra-luna%2Cdash%2Cokb%2Ccrypto-com-chain%2Cthe-graph%2Celrond-erd-2%2Ccompound-ether%2Csolana%2Cmaker%2Cftx-token%2Ccdai%2Cdai%2Cfilecoin%2Ccelsius-degree-token%2Ckusama%2Csushi%2Ccompound-governance-token%2Czcash%2Cethereum-classic%2Ccompound-usd-coin&order=market_cap_desc&per_page=100&page=1&sparkline=true&price_change_percentage=1h%2C24h%2C7d' 8 | 9 | const getCoinData = () => { 10 | return new Promise((resolve, reject) => { 11 | https.get(URL, (resp) => { 12 | let data = ''; 13 | 14 | // A chunk of data has been received. 15 | resp.on('data', (chunk) => { 16 | data += chunk; 17 | }); 18 | // The whole response has been received. Print out the result. 19 | resp.on('end', async () => { 20 | const dataJson = JSON.parse(data); 21 | resolve(dataJson); 22 | }); 23 | 24 | }).on("error", (err) => { 25 | console.log("Error: " + err.message); 26 | reject(err); 27 | }); 28 | }) 29 | } 30 | 31 | exports.handler = async (event, context) => { 32 | const data = await getCoinData(); 33 | const date = new Date(); 34 | 35 | const Items = data.map(entry => ({ 36 | id: { S: entry.id }, 37 | cgId: { S: entry.id }, 38 | createdAt: { S: date.toISOString() }, 39 | updatedAt: { S: date.toISOString() }, 40 | currentPrice: { N: entry.current_price.toString() }, 41 | image: { S: entry.image }, 42 | name: { S: entry.name }, 43 | symbol: { S: entry.symbol }, 44 | valueChange24H: { N: entry.price_change_percentage_1h_in_currency.toString() }, 45 | valueChange1D: { N: entry.price_change_percentage_24h_in_currency.toString() }, 46 | valueChange7D: { N: entry.price_change_percentage_7d_in_currency.toString() }, 47 | priceHistoryString: { S: JSON.stringify(entry.sparkline_in_7d.price) }, 48 | })) 49 | 50 | try { 51 | await Promise.all(Items.map(Item => { 52 | const params = { 53 | Item, 54 | TableName: process.env.COIN_TABLE, 55 | } 56 | 57 | return ddb.putItem(params).promise(); 58 | })) 59 | } catch (e) { 60 | console.log(e); 61 | context.done(null, event); 62 | } 63 | 64 | lambda.invoke({ 65 | FunctionName: process.env.NET_WORTH_CALCULATOR_FUNCTION, 66 | InvocationType: "Event" 67 | }).send(); 68 | context.done(null, event); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/EditScreenInfo.tsx: -------------------------------------------------------------------------------- 1 | import * as WebBrowser from 'expo-web-browser'; 2 | import React from 'react'; 3 | import { StyleSheet, TouchableOpacity } from 'react-native'; 4 | 5 | import Colors from '../constants/Colors'; 6 | import { MonoText } from './StyledText'; 7 | import { Text, View } from './Themed'; 8 | 9 | export default function EditScreenInfo({ path }: { path: string }) { 10 | return ( 11 | 12 | 13 | 17 | Open up the code for this screen: 18 | 19 | 20 | 24 | {path} 25 | 26 | 27 | 31 | Change any of the text, save the file, and your app will automatically update. 32 | 33 | 34 | 35 | 36 | 37 | 38 | Tap here if your app doesn't automatically update after making changes 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | function handleHelpPress() { 47 | WebBrowser.openBrowserAsync( 48 | 'https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet' 49 | ); 50 | } 51 | 52 | const styles = StyleSheet.create({ 53 | container: { 54 | flex: 1, 55 | backgroundColor: '#fff', 56 | }, 57 | developmentModeText: { 58 | marginBottom: 20, 59 | fontSize: 14, 60 | lineHeight: 19, 61 | textAlign: 'center', 62 | }, 63 | contentContainer: { 64 | paddingTop: 30, 65 | }, 66 | welcomeContainer: { 67 | alignItems: 'center', 68 | marginTop: 10, 69 | marginBottom: 20, 70 | }, 71 | welcomeImage: { 72 | width: 100, 73 | height: 80, 74 | resizeMode: 'contain', 75 | marginTop: 3, 76 | marginLeft: -10, 77 | }, 78 | getStartedContainer: { 79 | alignItems: 'center', 80 | marginHorizontal: 50, 81 | }, 82 | homeScreenFilename: { 83 | marginVertical: 7, 84 | }, 85 | codeHighlightText: { 86 | color: 'rgba(96,100,109, 0.8)', 87 | }, 88 | codeHighlightContainer: { 89 | borderRadius: 3, 90 | paddingHorizontal: 4, 91 | }, 92 | getStartedText: { 93 | fontSize: 17, 94 | lineHeight: 24, 95 | textAlign: 'center', 96 | }, 97 | helpContainer: { 98 | marginTop: 15, 99 | marginHorizontal: 20, 100 | alignItems: 'center', 101 | }, 102 | helpLink: { 103 | paddingVertical: 15, 104 | }, 105 | helpLinkText: { 106 | textAlign: 'center', 107 | }, 108 | }); 109 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptof289a4cd/vcryptof289a4cd-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Lambda Function resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "CloudWatchRule": { 6 | "Type": "String", 7 | "Default": "NONE", 8 | "Description": " Schedule Expression" 9 | }, 10 | "env": { 11 | "Type": "String" 12 | } 13 | }, 14 | "Conditions": { 15 | "ShouldNotCreateEnvResources": { 16 | "Fn::Equals": [ 17 | { 18 | "Ref": "env" 19 | }, 20 | "NONE" 21 | ] 22 | } 23 | }, 24 | "Resources": { 25 | "LambdaFunction": { 26 | "Type": "AWS::Lambda::Function", 27 | "Metadata": { 28 | "aws:asset:path": "./src", 29 | "aws:asset:property": "Code" 30 | }, 31 | "Properties": { 32 | "Handler": "index.handler", 33 | "FunctionName": { 34 | "Fn::If": [ 35 | "ShouldNotCreateEnvResources", 36 | "NetWorthCalculator", 37 | { 38 | "Fn::Join": [ 39 | "", 40 | [ 41 | "NetWorthCalculator", 42 | "-", 43 | { 44 | "Ref": "env" 45 | } 46 | ] 47 | ] 48 | } 49 | ] 50 | }, 51 | "Environment": { 52 | "Variables": { 53 | "ENV": { 54 | "Ref": "env" 55 | }, 56 | "REGION": { 57 | "Ref": "AWS::Region" 58 | } 59 | } 60 | }, 61 | "Role": { 62 | "Fn::GetAtt": [ 63 | "LambdaExecutionRole", 64 | "Arn" 65 | ] 66 | }, 67 | "Runtime": "nodejs12.x", 68 | "Layers": [], 69 | "Timeout": "25", 70 | "Code": { 71 | "S3Bucket": "amplify-vcrypto-prod-153702-deployment", 72 | "S3Key": "amplify-builds/vcryptof289a4cd-7046593367375066746a-build.zip" 73 | } 74 | } 75 | }, 76 | "LambdaExecutionRole": { 77 | "Type": "AWS::IAM::Role", 78 | "Properties": { 79 | "RoleName": { 80 | "Fn::If": [ 81 | "ShouldNotCreateEnvResources", 82 | "vcryptoLambdaRolef6aa8847", 83 | { 84 | "Fn::Join": [ 85 | "", 86 | [ 87 | "vcryptoLambdaRolef6aa8847", 88 | "-", 89 | { 90 | "Ref": "env" 91 | } 92 | ] 93 | ] 94 | } 95 | ] 96 | }, 97 | "AssumeRolePolicyDocument": { 98 | "Version": "2012-10-17", 99 | "Statement": [ 100 | { 101 | "Effect": "Allow", 102 | "Principal": { 103 | "Service": [ 104 | "lambda.amazonaws.com" 105 | ] 106 | }, 107 | "Action": [ 108 | "sts:AssumeRole" 109 | ] 110 | } 111 | ] 112 | } 113 | } 114 | }, 115 | "lambdaexecutionpolicy": { 116 | "DependsOn": [ 117 | "LambdaExecutionRole" 118 | ], 119 | "Type": "AWS::IAM::Policy", 120 | "Properties": { 121 | "PolicyName": "lambda-execution-policy", 122 | "Roles": [ 123 | { 124 | "Ref": "LambdaExecutionRole" 125 | } 126 | ], 127 | "PolicyDocument": { 128 | "Version": "2012-10-17", 129 | "Statement": [ 130 | { 131 | "Effect": "Allow", 132 | "Action": [ 133 | "logs:CreateLogGroup", 134 | "logs:CreateLogStream", 135 | "logs:PutLogEvents" 136 | ], 137 | "Resource": { 138 | "Fn::Sub": [ 139 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", 140 | { 141 | "region": { 142 | "Ref": "AWS::Region" 143 | }, 144 | "account": { 145 | "Ref": "AWS::AccountId" 146 | }, 147 | "lambda": { 148 | "Ref": "LambdaFunction" 149 | } 150 | } 151 | ] 152 | } 153 | } 154 | ] 155 | } 156 | } 157 | } 158 | }, 159 | "Outputs": { 160 | "Name": { 161 | "Value": { 162 | "Ref": "LambdaFunction" 163 | } 164 | }, 165 | "Arn": { 166 | "Value": { 167 | "Fn::GetAtt": [ 168 | "LambdaFunction", 169 | "Arn" 170 | ] 171 | } 172 | }, 173 | "Region": { 174 | "Value": { 175 | "Ref": "AWS::Region" 176 | } 177 | }, 178 | "LambdaExecutionRole": { 179 | "Value": { 180 | "Ref": "LambdaExecutionRole" 181 | } 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /src/graphql/queries.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // this is an auto generated file. This will be overwritten 4 | 5 | export const getUser = /* GraphQL */ ` 6 | query GetUser($id: ID!) { 7 | getUser(id: $id) { 8 | id 9 | type 10 | email 11 | name 12 | image 13 | networth 14 | portfolioCoins { 15 | items { 16 | id 17 | amount 18 | userId 19 | coinId 20 | createdAt 21 | updatedAt 22 | } 23 | nextToken 24 | } 25 | createdAt 26 | updatedAt 27 | } 28 | } 29 | `; 30 | export const listUsers = /* GraphQL */ ` 31 | query ListUsers( 32 | $filter: ModelUserFilterInput 33 | $limit: Int 34 | $nextToken: String 35 | ) { 36 | listUsers(filter: $filter, limit: $limit, nextToken: $nextToken) { 37 | items { 38 | id 39 | type 40 | email 41 | name 42 | image 43 | networth 44 | portfolioCoins { 45 | nextToken 46 | } 47 | createdAt 48 | updatedAt 49 | } 50 | nextToken 51 | } 52 | } 53 | `; 54 | export const getPortfolioCoin = /* GraphQL */ ` 55 | query GetPortfolioCoin($id: ID!) { 56 | getPortfolioCoin(id: $id) { 57 | id 58 | amount 59 | userId 60 | user { 61 | id 62 | type 63 | email 64 | name 65 | image 66 | networth 67 | portfolioCoins { 68 | nextToken 69 | } 70 | createdAt 71 | updatedAt 72 | } 73 | coinId 74 | coin { 75 | id 76 | cgId 77 | name 78 | symbol 79 | image 80 | currentPrice 81 | valueChange24H 82 | valueChange1D 83 | valueChange7D 84 | priceHistoryString 85 | createdAt 86 | updatedAt 87 | } 88 | createdAt 89 | updatedAt 90 | } 91 | } 92 | `; 93 | export const listPortfolioCoins = /* GraphQL */ ` 94 | query ListPortfolioCoins( 95 | $filter: ModelPortfolioCoinFilterInput 96 | $limit: Int 97 | $nextToken: String 98 | ) { 99 | listPortfolioCoins(filter: $filter, limit: $limit, nextToken: $nextToken) { 100 | items { 101 | id 102 | amount 103 | userId 104 | user { 105 | id 106 | type 107 | email 108 | name 109 | image 110 | networth 111 | createdAt 112 | updatedAt 113 | } 114 | coinId 115 | coin { 116 | id 117 | cgId 118 | name 119 | symbol 120 | image 121 | currentPrice 122 | valueChange24H 123 | valueChange1D 124 | valueChange7D 125 | priceHistoryString 126 | createdAt 127 | updatedAt 128 | } 129 | createdAt 130 | updatedAt 131 | } 132 | nextToken 133 | } 134 | } 135 | `; 136 | export const getCoin = /* GraphQL */ ` 137 | query GetCoin($id: ID!) { 138 | getCoin(id: $id) { 139 | id 140 | cgId 141 | name 142 | symbol 143 | image 144 | currentPrice 145 | valueChange24H 146 | valueChange1D 147 | valueChange7D 148 | priceHistoryString 149 | createdAt 150 | updatedAt 151 | } 152 | } 153 | `; 154 | export const listCoins = /* GraphQL */ ` 155 | query ListCoins( 156 | $filter: ModelCoinFilterInput 157 | $limit: Int 158 | $nextToken: String 159 | ) { 160 | listCoins(filter: $filter, limit: $limit, nextToken: $nextToken) { 161 | items { 162 | id 163 | cgId 164 | name 165 | symbol 166 | image 167 | currentPrice 168 | valueChange24H 169 | valueChange1D 170 | valueChange7D 171 | priceHistoryString 172 | createdAt 173 | updatedAt 174 | } 175 | nextToken 176 | } 177 | } 178 | `; 179 | export const getUsersByNetworth = /* GraphQL */ ` 180 | query GetUsersByNetworth( 181 | $type: String 182 | $networth: ModelFloatKeyConditionInput 183 | $sortDirection: ModelSortDirection 184 | $filter: ModelUserFilterInput 185 | $limit: Int 186 | $nextToken: String 187 | ) { 188 | getUsersByNetworth( 189 | type: $type 190 | networth: $networth 191 | sortDirection: $sortDirection 192 | filter: $filter 193 | limit: $limit 194 | nextToken: $nextToken 195 | ) { 196 | items { 197 | id 198 | type 199 | email 200 | name 201 | image 202 | networth 203 | portfolioCoins { 204 | nextToken 205 | } 206 | createdAt 207 | updatedAt 208 | } 209 | nextToken 210 | } 211 | } 212 | `; 213 | -------------------------------------------------------------------------------- /src/screens/CoinDetailsScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import {View, Text, Image, Pressable, ActivityIndicator} from 'react-native'; 3 | import { AntDesign } from "@expo/vector-icons"; 4 | import { API, graphqlOperation } from 'aws-amplify'; 5 | import { getCoin, listPortfolioCoins } from '../../graphql/queries'; 6 | import styles from "./styles"; 7 | import PercentageChange from "../../components/PercentageChange"; 8 | import CoinPriceGraph from "../../components/CoinPriceGraph"; 9 | import {useNavigation, useRoute} from "@react-navigation/native"; 10 | import AppContext from "../../utils/AppContext"; 11 | import formatMoney from "../../utils/formatMoney"; 12 | 13 | const CoinDetailsScreen = () => { 14 | const [coin, setCoin] = useState(null); 15 | const [portfolioCoin, setPortfolioCoin] = useState(null); 16 | 17 | const { userId } = useContext(AppContext); 18 | 19 | const navigation = useNavigation(); 20 | const route = useRoute(); 21 | 22 | const fetchCoinData = async () => { 23 | if (!route.params?.id) { 24 | return; 25 | } 26 | try { 27 | const response = await API.graphql( 28 | graphqlOperation(getCoin, { id: route.params.id }) 29 | ) 30 | setCoin(response.data.getCoin); 31 | } catch (e) { 32 | console.error(e); 33 | } 34 | } 35 | 36 | const fetchPortfolioCoinData = async () => { 37 | if (!route.params?.id) { 38 | return; 39 | } 40 | try { 41 | const response = await API.graphql( 42 | graphqlOperation(listPortfolioCoins, 43 | { filter: { 44 | and: { 45 | coinId: { eq: route.params?.id}, 46 | userId: { eq: userId } 47 | } 48 | }} 49 | ) 50 | ) 51 | if (response.data.listPortfolioCoins.items.length > 0) { 52 | setPortfolioCoin(response.data.listPortfolioCoins.items[0]) 53 | } 54 | } catch (e) { 55 | console.error(e); 56 | } 57 | } 58 | 59 | useEffect(() => { 60 | fetchCoinData(); 61 | fetchPortfolioCoinData(); 62 | }, []) 63 | 64 | const onBuy = () => { 65 | navigation.navigate('CoinExchange', { isBuy: true, coin, portfolioCoin }); 66 | } 67 | 68 | const onSell = () => { 69 | navigation.navigate('CoinExchange', { isBuy: false, coin, portfolioCoin }); 70 | } 71 | 72 | if (!coin) { 73 | return 74 | } 75 | 76 | return ( 77 | 78 | 79 | 80 | 81 | 82 | {coin.name} 83 | {coin.symbol} 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | Current price 94 | ${formatMoney(coin.currentPrice)} 95 | 96 | 97 | 98 | 99 | 1 hour 100 | 101 | 102 | 103 | 104 | 1 day 105 | 106 | 107 | 108 | 109 | 7 days 110 | 111 | 112 | 113 | 114 | 115 | {coin.priceHistoryString 116 | && } 117 | 118 | 119 | Position 120 | 121 | {coin.symbol} {formatMoney(portfolioCoin?.amount || 0)} 122 | {' '} 123 | (${formatMoney(coin.currentPrice * (portfolioCoin?.amount || 0))}) 124 | 125 | 126 | 127 | 128 | 131 | Buy 132 | 133 | 134 | 137 | Sell 138 | 139 | 140 | 141 | 142 | ); 143 | }; 144 | 145 | export default CoinDetailsScreen; 146 | -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto16d3e4c5PostConfirmation/vcrypto16d3e4c5PostConfirmation-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Lambda resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "GROUP": { 6 | "Type": "String", 7 | "Default": "" 8 | }, 9 | "modules": { 10 | "Type": "String", 11 | "Default": "", 12 | "Description": "Comma-delimmited list of modules to be executed by a lambda trigger. Sent to resource as an env variable." 13 | }, 14 | "resourceName": { 15 | "Type": "String", 16 | "Default": "" 17 | }, 18 | "trigger": { 19 | "Type": "String", 20 | "Default": "true" 21 | }, 22 | "functionName": { 23 | "Type": "String", 24 | "Default": "" 25 | }, 26 | "roleName": { 27 | "Type": "String", 28 | "Default": "" 29 | }, 30 | "parentResource": { 31 | "Type": "String", 32 | "Default": "" 33 | }, 34 | "parentStack": { 35 | "Type": "String", 36 | "Default": "" 37 | }, 38 | "env": { 39 | "Type": "String" 40 | } 41 | }, 42 | "Conditions": { 43 | "ShouldNotCreateEnvResources": { 44 | "Fn::Equals": [ 45 | { 46 | "Ref": "env" 47 | }, 48 | "NONE" 49 | ] 50 | } 51 | }, 52 | "Resources": { 53 | "LambdaFunction": { 54 | "Type": "AWS::Lambda::Function", 55 | "Metadata": { 56 | "aws:asset:path": "./src", 57 | "aws:asset:property": "Code" 58 | }, 59 | "Properties": { 60 | "Handler": "index.handler", 61 | "FunctionName": { 62 | "Fn::If": [ 63 | "ShouldNotCreateEnvResources", 64 | "vcrypto16d3e4c5PostConfirmation", 65 | { 66 | "Fn::Join": [ 67 | "", 68 | [ 69 | "vcrypto16d3e4c5PostConfirmation", 70 | "-", 71 | { 72 | "Ref": "env" 73 | } 74 | ] 75 | ] 76 | } 77 | ] 78 | }, 79 | "Environment": { 80 | "Variables": { 81 | "ENV": { 82 | "Ref": "env" 83 | }, 84 | "MODULES": { 85 | "Ref": "modules" 86 | }, 87 | "REGION": { 88 | "Ref": "AWS::Region" 89 | }, 90 | "GROUP": { 91 | "Ref": "GROUP" 92 | } 93 | } 94 | }, 95 | "Role": { 96 | "Fn::GetAtt": [ 97 | "LambdaExecutionRole", 98 | "Arn" 99 | ] 100 | }, 101 | "Runtime": "nodejs10.x", 102 | "Timeout": "25", 103 | "Code": { 104 | "S3Bucket": "amplify-vcrypto-prod-153702-deployment", 105 | "S3Key": "amplify-builds/vcrypto16d3e4c5PostConfirmation-4f502f383447364d6e4f-build.zip" 106 | } 107 | } 108 | }, 109 | "LambdaExecutionRole": { 110 | "Type": "AWS::IAM::Role", 111 | "Properties": { 112 | "RoleName": { 113 | "Fn::If": [ 114 | "ShouldNotCreateEnvResources", 115 | "vcrypto16d3e4c5PostConfirmation", 116 | { 117 | "Fn::Join": [ 118 | "", 119 | [ 120 | "vcrypto16d3e4c5PostConfirmation", 121 | "-", 122 | { 123 | "Ref": "env" 124 | } 125 | ] 126 | ] 127 | } 128 | ] 129 | }, 130 | "AssumeRolePolicyDocument": { 131 | "Version": "2012-10-17", 132 | "Statement": [ 133 | { 134 | "Effect": "Allow", 135 | "Principal": { 136 | "Service": [ 137 | "lambda.amazonaws.com" 138 | ] 139 | }, 140 | "Action": [ 141 | "sts:AssumeRole" 142 | ] 143 | } 144 | ] 145 | } 146 | } 147 | }, 148 | "lambdaexecutionpolicy": { 149 | "DependsOn": [ 150 | "LambdaExecutionRole" 151 | ], 152 | "Type": "AWS::IAM::Policy", 153 | "Properties": { 154 | "PolicyName": "lambda-execution-policy", 155 | "Roles": [ 156 | { 157 | "Ref": "LambdaExecutionRole" 158 | } 159 | ], 160 | "PolicyDocument": { 161 | "Version": "2012-10-17", 162 | "Statement": [ 163 | { 164 | "Effect": "Allow", 165 | "Action": [ 166 | "logs:CreateLogGroup", 167 | "logs:CreateLogStream", 168 | "logs:PutLogEvents" 169 | ], 170 | "Resource": { 171 | "Fn::Sub": [ 172 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", 173 | { 174 | "region": { 175 | "Ref": "AWS::Region" 176 | }, 177 | "account": { 178 | "Ref": "AWS::AccountId" 179 | }, 180 | "lambda": { 181 | "Ref": "LambdaFunction" 182 | } 183 | } 184 | ] 185 | } 186 | } 187 | ] 188 | } 189 | } 190 | } 191 | }, 192 | "Outputs": { 193 | "Name": { 194 | "Value": { 195 | "Ref": "LambdaFunction" 196 | } 197 | }, 198 | "Arn": { 199 | "Value": { 200 | "Fn::GetAtt": [ 201 | "LambdaFunction", 202 | "Arn" 203 | ] 204 | } 205 | }, 206 | "LambdaExecutionRole": { 207 | "Value": { 208 | "Ref": "LambdaExecutionRole" 209 | } 210 | }, 211 | "Region": { 212 | "Value": { 213 | "Ref": "AWS::Region" 214 | } 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcrypto5fce58ab/vcrypto5fce58ab-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Lambda Function resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "CloudWatchRule": { 6 | "Type": "String", 7 | "Default": "NONE", 8 | "Description": " Schedule Expression" 9 | }, 10 | "env": { 11 | "Type": "String" 12 | } 13 | }, 14 | "Conditions": { 15 | "ShouldNotCreateEnvResources": { 16 | "Fn::Equals": [ 17 | { 18 | "Ref": "env" 19 | }, 20 | "NONE" 21 | ] 22 | } 23 | }, 24 | "Resources": { 25 | "LambdaFunction": { 26 | "Type": "AWS::Lambda::Function", 27 | "Metadata": { 28 | "aws:asset:path": "./src", 29 | "aws:asset:property": "Code" 30 | }, 31 | "Properties": { 32 | "Handler": "index.handler", 33 | "FunctionName": { 34 | "Fn::If": [ 35 | "ShouldNotCreateEnvResources", 36 | "FetchCoinsPrices", 37 | { 38 | "Fn::Join": [ 39 | "", 40 | [ 41 | "FetchCoinsPrices", 42 | "-", 43 | { 44 | "Ref": "env" 45 | } 46 | ] 47 | ] 48 | } 49 | ] 50 | }, 51 | "Environment": { 52 | "Variables": { 53 | "ENV": { 54 | "Ref": "env" 55 | }, 56 | "REGION": { 57 | "Ref": "AWS::Region" 58 | } 59 | } 60 | }, 61 | "Role": { 62 | "Fn::GetAtt": [ 63 | "LambdaExecutionRole", 64 | "Arn" 65 | ] 66 | }, 67 | "Runtime": "nodejs12.x", 68 | "Layers": [], 69 | "Timeout": "25", 70 | "Code": { 71 | "S3Bucket": "amplify-vcrypto-prod-153702-deployment", 72 | "S3Key": "amplify-builds/vcrypto5fce58ab-7576674c572b616c346d-build.zip" 73 | } 74 | } 75 | }, 76 | "LambdaExecutionRole": { 77 | "Type": "AWS::IAM::Role", 78 | "Properties": { 79 | "RoleName": { 80 | "Fn::If": [ 81 | "ShouldNotCreateEnvResources", 82 | "vcryptoLambdaRoleba7dd43d", 83 | { 84 | "Fn::Join": [ 85 | "", 86 | [ 87 | "vcryptoLambdaRoleba7dd43d", 88 | "-", 89 | { 90 | "Ref": "env" 91 | } 92 | ] 93 | ] 94 | } 95 | ] 96 | }, 97 | "AssumeRolePolicyDocument": { 98 | "Version": "2012-10-17", 99 | "Statement": [ 100 | { 101 | "Effect": "Allow", 102 | "Principal": { 103 | "Service": [ 104 | "lambda.amazonaws.com" 105 | ] 106 | }, 107 | "Action": [ 108 | "sts:AssumeRole" 109 | ] 110 | } 111 | ] 112 | } 113 | } 114 | }, 115 | "lambdaexecutionpolicy": { 116 | "DependsOn": [ 117 | "LambdaExecutionRole" 118 | ], 119 | "Type": "AWS::IAM::Policy", 120 | "Properties": { 121 | "PolicyName": "lambda-execution-policy", 122 | "Roles": [ 123 | { 124 | "Ref": "LambdaExecutionRole" 125 | } 126 | ], 127 | "PolicyDocument": { 128 | "Version": "2012-10-17", 129 | "Statement": [ 130 | { 131 | "Effect": "Allow", 132 | "Action": [ 133 | "logs:CreateLogGroup", 134 | "logs:CreateLogStream", 135 | "logs:PutLogEvents" 136 | ], 137 | "Resource": { 138 | "Fn::Sub": [ 139 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", 140 | { 141 | "region": { 142 | "Ref": "AWS::Region" 143 | }, 144 | "account": { 145 | "Ref": "AWS::AccountId" 146 | }, 147 | "lambda": { 148 | "Ref": "LambdaFunction" 149 | } 150 | } 151 | ] 152 | } 153 | } 154 | ] 155 | } 156 | } 157 | }, 158 | "CloudWatchEvent": { 159 | "Type": "AWS::Events::Rule", 160 | "Properties": { 161 | "Description": "Schedule rule for Lambda", 162 | "ScheduleExpression": { 163 | "Ref": "CloudWatchRule" 164 | }, 165 | "State": "ENABLED", 166 | "Targets": [ 167 | { 168 | "Arn": { 169 | "Fn::GetAtt": [ 170 | "LambdaFunction", 171 | "Arn" 172 | ] 173 | }, 174 | "Id": { 175 | "Ref": "LambdaFunction" 176 | } 177 | } 178 | ] 179 | } 180 | }, 181 | "PermissionForEventsToInvokeLambda": { 182 | "Type": "AWS::Lambda::Permission", 183 | "Properties": { 184 | "FunctionName": { 185 | "Ref": "LambdaFunction" 186 | }, 187 | "Action": "lambda:InvokeFunction", 188 | "Principal": "events.amazonaws.com", 189 | "SourceArn": { 190 | "Fn::GetAtt": [ 191 | "CloudWatchEvent", 192 | "Arn" 193 | ] 194 | } 195 | } 196 | } 197 | }, 198 | "Outputs": { 199 | "Name": { 200 | "Value": { 201 | "Ref": "LambdaFunction" 202 | } 203 | }, 204 | "Arn": { 205 | "Value": { 206 | "Fn::GetAtt": [ 207 | "LambdaFunction", 208 | "Arn" 209 | ] 210 | } 211 | }, 212 | "Region": { 213 | "Value": { 214 | "Ref": "AWS::Region" 215 | } 216 | }, 217 | "LambdaExecutionRole": { 218 | "Value": { 219 | "Ref": "LambdaExecutionRole" 220 | } 221 | }, 222 | "CloudWatchEventRule": { 223 | "Value": { 224 | "Ref": "CloudWatchEvent" 225 | } 226 | } 227 | } 228 | } -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/vcryptoe6806c17-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Lambda Function resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "CloudWatchRule": { 6 | "Type": "String", 7 | "Default": "NONE", 8 | "Description": " Schedule Expression" 9 | }, 10 | "env": { 11 | "Type": "String" 12 | }, 13 | "apivcryptoGraphQLAPIIdOutput": { 14 | "Type": "String", 15 | "Default": "apivcryptoGraphQLAPIIdOutput" 16 | }, 17 | "apivcryptoGraphQLAPIEndpointOutput": { 18 | "Type": "String", 19 | "Default": "apivcryptoGraphQLAPIEndpointOutput" 20 | } 21 | }, 22 | "Conditions": { 23 | "ShouldNotCreateEnvResources": { 24 | "Fn::Equals": [ 25 | { 26 | "Ref": "env" 27 | }, 28 | "NONE" 29 | ] 30 | } 31 | }, 32 | "Resources": { 33 | "LambdaFunction": { 34 | "Type": "AWS::Lambda::Function", 35 | "Metadata": { 36 | "aws:asset:path": "./src", 37 | "aws:asset:property": "Code" 38 | }, 39 | "Properties": { 40 | "Handler": "index.handler", 41 | "FunctionName": { 42 | "Fn::If": [ 43 | "ShouldNotCreateEnvResources", 44 | "ExchangeCoins", 45 | { 46 | "Fn::Join": [ 47 | "", 48 | [ 49 | "ExchangeCoins", 50 | "-", 51 | { 52 | "Ref": "env" 53 | } 54 | ] 55 | ] 56 | } 57 | ] 58 | }, 59 | "Environment": { 60 | "Variables": { 61 | "ENV": { 62 | "Ref": "env" 63 | }, 64 | "REGION": { 65 | "Ref": "AWS::Region" 66 | }, 67 | "API_VCRYPTO_GRAPHQLAPIIDOUTPUT": { 68 | "Ref": "apivcryptoGraphQLAPIIdOutput" 69 | }, 70 | "API_VCRYPTO_GRAPHQLAPIENDPOINTOUTPUT": { 71 | "Ref": "apivcryptoGraphQLAPIEndpointOutput" 72 | } 73 | } 74 | }, 75 | "Role": { 76 | "Fn::GetAtt": [ 77 | "LambdaExecutionRole", 78 | "Arn" 79 | ] 80 | }, 81 | "Runtime": "nodejs12.x", 82 | "Layers": [], 83 | "Timeout": "25", 84 | "Code": { 85 | "S3Bucket": "amplify-vcrypto-prod-153702-deployment", 86 | "S3Key": "amplify-builds/vcryptoe6806c17-58546b4d4f35776b2f7a-build.zip" 87 | } 88 | } 89 | }, 90 | "LambdaExecutionRole": { 91 | "Type": "AWS::IAM::Role", 92 | "Properties": { 93 | "RoleName": { 94 | "Fn::If": [ 95 | "ShouldNotCreateEnvResources", 96 | "vcryptoLambdaRole4d2a4dcd", 97 | { 98 | "Fn::Join": [ 99 | "", 100 | [ 101 | "vcryptoLambdaRole4d2a4dcd", 102 | "-", 103 | { 104 | "Ref": "env" 105 | } 106 | ] 107 | ] 108 | } 109 | ] 110 | }, 111 | "AssumeRolePolicyDocument": { 112 | "Version": "2012-10-17", 113 | "Statement": [ 114 | { 115 | "Effect": "Allow", 116 | "Principal": { 117 | "Service": [ 118 | "lambda.amazonaws.com" 119 | ] 120 | }, 121 | "Action": [ 122 | "sts:AssumeRole" 123 | ] 124 | } 125 | ] 126 | } 127 | } 128 | }, 129 | "lambdaexecutionpolicy": { 130 | "DependsOn": [ 131 | "LambdaExecutionRole" 132 | ], 133 | "Type": "AWS::IAM::Policy", 134 | "Properties": { 135 | "PolicyName": "lambda-execution-policy", 136 | "Roles": [ 137 | { 138 | "Ref": "LambdaExecutionRole" 139 | } 140 | ], 141 | "PolicyDocument": { 142 | "Version": "2012-10-17", 143 | "Statement": [ 144 | { 145 | "Effect": "Allow", 146 | "Action": [ 147 | "logs:CreateLogGroup", 148 | "logs:CreateLogStream", 149 | "logs:PutLogEvents" 150 | ], 151 | "Resource": { 152 | "Fn::Sub": [ 153 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", 154 | { 155 | "region": { 156 | "Ref": "AWS::Region" 157 | }, 158 | "account": { 159 | "Ref": "AWS::AccountId" 160 | }, 161 | "lambda": { 162 | "Ref": "LambdaFunction" 163 | } 164 | } 165 | ] 166 | } 167 | } 168 | ] 169 | } 170 | } 171 | }, 172 | "AmplifyResourcesPolicy": { 173 | "DependsOn": [ 174 | "LambdaExecutionRole" 175 | ], 176 | "Type": "AWS::IAM::Policy", 177 | "Properties": { 178 | "PolicyName": "amplify-lambda-execution-policy", 179 | "Roles": [ 180 | { 181 | "Ref": "LambdaExecutionRole" 182 | } 183 | ], 184 | "PolicyDocument": { 185 | "Version": "2012-10-17", 186 | "Statement": [ 187 | { 188 | "Effect": "Allow", 189 | "Action": [ 190 | "appsync:Create*", 191 | "appsync:StartSchemaCreation", 192 | "appsync:GraphQL", 193 | "appsync:Get*", 194 | "appsync:List*", 195 | "appsync:Update*", 196 | "appsync:Delete*" 197 | ], 198 | "Resource": [ 199 | { 200 | "Fn::Join": [ 201 | "", 202 | [ 203 | "arn:aws:appsync:", 204 | { 205 | "Ref": "AWS::Region" 206 | }, 207 | ":", 208 | { 209 | "Ref": "AWS::AccountId" 210 | }, 211 | ":apis/", 212 | { 213 | "Ref": "apivcryptoGraphQLAPIIdOutput" 214 | }, 215 | "/*" 216 | ] 217 | ] 218 | } 219 | ] 220 | } 221 | ] 222 | } 223 | } 224 | } 225 | }, 226 | "Outputs": { 227 | "Name": { 228 | "Value": { 229 | "Ref": "LambdaFunction" 230 | } 231 | }, 232 | "Arn": { 233 | "Value": { 234 | "Fn::GetAtt": [ 235 | "LambdaFunction", 236 | "Arn" 237 | ] 238 | } 239 | }, 240 | "Region": { 241 | "Value": { 242 | "Ref": "AWS::Region" 243 | } 244 | }, 245 | "LambdaExecutionRole": { 246 | "Value": { 247 | "Ref": "LambdaExecutionRole" 248 | } 249 | } 250 | } 251 | } -------------------------------------------------------------------------------- /src/screens/CoinExchangeScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Image, 6 | TextInput, 7 | Pressable, 8 | Alert, 9 | KeyboardAvoidingView, 10 | Platform, 11 | ActivityIndicator, 12 | } from 'react-native'; 13 | import {useNavigation, useRoute} from '@react-navigation/native'; 14 | import { API, graphqlOperation } from 'aws-amplify'; 15 | import { exchangeCoins } from './mutations'; 16 | const image = require('../../../assets/images/Saly-31.png'); 17 | import styles from './styles'; 18 | import AppContext from "../../utils/AppContext"; 19 | import {listPortfolioCoins} from "../../graphql/queries"; 20 | import formatMoney from "../../utils/formatMoney"; 21 | 22 | const USD_COIN_ID = 'usd'; 23 | 24 | const CoinExchangeScreen = () => { 25 | const [coinAmount, setCoinAmount] = useState('') 26 | const [coinUSDValue, setCoinUSDValue] = useState('') 27 | const [usdPortfolioCoin, setUsdPortfolioCoin] = useState(null); 28 | const [isLoading, setIsLoading] = useState(false); 29 | 30 | const navigation = useNavigation(); 31 | 32 | const { userId } = useContext(AppContext); 33 | 34 | const route = useRoute(); 35 | 36 | const isBuy = route?.params?.isBuy; 37 | const coin = route?.params?.coin; 38 | const portfolioCoin = route?.params?.portfolioCoin; 39 | 40 | const getUSDPortfolioCoin = async () => { 41 | try { 42 | const response = await API.graphql( 43 | graphqlOperation(listPortfolioCoins, 44 | { filter: { 45 | and: { 46 | coinId: { eq: USD_COIN_ID }, 47 | userId: { eq: userId } 48 | } 49 | }} 50 | ) 51 | ) 52 | if (response.data.listPortfolioCoins.items.length > 0) { 53 | setUsdPortfolioCoin(response.data.listPortfolioCoins.items[0]); 54 | } 55 | } catch (e) { 56 | console.error(e); 57 | } 58 | } 59 | 60 | useEffect(() => { 61 | getUSDPortfolioCoin(); 62 | }, []) 63 | 64 | useEffect(() => { 65 | const amount = parseFloat(coinAmount) 66 | if (!amount && amount !== 0) { 67 | setCoinAmount(""); 68 | setCoinUSDValue(""); 69 | return; 70 | } 71 | setCoinUSDValue((amount * coin?.currentPrice).toString()); 72 | }, [coinAmount]); 73 | 74 | useEffect(() => { 75 | const amount = parseFloat(coinUSDValue) 76 | if (!amount && amount !== 0) { 77 | setCoinAmount(""); 78 | setCoinUSDValue(""); 79 | return; 80 | } 81 | setCoinAmount((amount / coin?.currentPrice).toString()); 82 | }, [coinUSDValue]); 83 | 84 | const onSellAll = () => { 85 | setCoinAmount(portfolioCoin.amount); 86 | } 87 | 88 | const onBuyAll = () => { 89 | setCoinUSDValue(usdPortfolioCoin?.amount || 0); 90 | } 91 | 92 | const placeOrder = async () => { 93 | if (isLoading) { 94 | return; 95 | } 96 | setIsLoading(true); 97 | try { 98 | const variables = { 99 | coinId: coin.id, 100 | isBuy, 101 | amount: parseFloat(coinAmount), 102 | usdPortfolioCoinId: usdPortfolioCoin?.id, 103 | coinPortfolioCoinId: portfolioCoin?.id, 104 | } 105 | 106 | const response = await API.graphql( 107 | graphqlOperation(exchangeCoins, variables) 108 | ) 109 | if (response.data.exchangeCoins) { 110 | navigation.navigate('Portfolio'); 111 | } else { 112 | Alert.alert('Error', 'There was an error exchanging coins'); 113 | } 114 | } catch (e) { 115 | Alert.alert('Error', 'There was an error exchanging coins'); 116 | console.error(e); 117 | } 118 | setIsLoading(false); 119 | } 120 | 121 | const onPlaceOrder = async () => { 122 | const maxUsd = usdPortfolioCoin?.amount || 0; 123 | if (isBuy && parseFloat(coinUSDValue) > maxUsd) { 124 | Alert.alert('Error', `Not enough USD coins. Max: ${maxUsd}`); 125 | return; 126 | } 127 | if (!isBuy && (!portfolioCoin || parseFloat(coinAmount) > portfolioCoin.amount)) { 128 | Alert.alert('Error', `Not enough ${coin.symbol} coins. Max: ${portfolioCoin.amount || 0}`); 129 | return; 130 | } 131 | 132 | await placeOrder(); 133 | } 134 | 135 | return ( 136 | 141 | 142 | {isBuy ? 'Buy ' : "Sell "} 143 | {coin?.name} 144 | 145 | 146 | 1 {coin?.symbol} 147 | {' = '} 148 | ${formatMoney(coin?.currentPrice)} 149 | 150 | 151 | 152 | 153 | 154 | 160 | {coin?.symbol} 161 | 162 | = 163 | 164 | 165 | 171 | USD 172 | 173 | 174 | 175 | {isBuy ? ( 176 | 177 | Buy max 178 | 179 | ) : ( 180 | 181 | Sell all 182 | 183 | )} 184 | 185 | 186 | Place Order 187 | {isLoading && } 188 | 189 | 190 | ); 191 | }; 192 | 193 | export default CoinExchangeScreen; 194 | -------------------------------------------------------------------------------- /amplify/backend/function/vcryptoe6806c17/src/index.js: -------------------------------------------------------------------------------- 1 | const { DynamoDB } = require('aws-sdk'); 2 | 3 | const ddb = new DynamoDB(); 4 | 5 | const getCoinAmount = async (coinPortfolioCoinId, userId) => { 6 | const params = { 7 | Key: { 8 | id: { S: coinPortfolioCoinId }, 9 | }, 10 | TableName: process.env.PORTFOLIO_COIN_TABLE 11 | } 12 | const coinData = await ddb.getItem(params).promise(); 13 | console.log('porftolio coin data'); 14 | console.log(coinData); 15 | // TOdo CHECK if it is indeed coin, and belongs to user 16 | if (coinData && coinData.Item && coinData.Item.amount && coinData.Item.amount.N) { 17 | return parseFloat(coinData.Item.amount.N); 18 | } 19 | return 0; 20 | } 21 | 22 | const getUsdAmount = async (usdPortfolioCoinId, userId) => { 23 | const params = { 24 | Key: { 25 | id: { S: usdPortfolioCoinId } 26 | }, 27 | TableName: process.env.PORTFOLIO_COIN_TABLE 28 | } 29 | const coinData = await ddb.getItem(params).promise(); 30 | console.log('usd coin data'); 31 | console.log(coinData); 32 | // TOdo CHECK if it is indeed USD coin, and belongs to user 33 | // coinId: { S: process.env.USD_COIN_ID }, 34 | // userId: { S: userId }, 35 | if (coinData && coinData.Item && coinData.Item.amount && coinData.Item.amount.N) { 36 | return parseFloat(coinData.Item.amount.N); 37 | } 38 | return 0; 39 | } 40 | 41 | const getCoin = async (coinId) => { 42 | const params = { 43 | Key: { 44 | id: { S: coinId } 45 | }, 46 | TableName: process.env.COIN_TABLE 47 | } 48 | const coinData = await ddb.getItem(params).promise(); 49 | console.log('coin data'); 50 | console.log(coinData); 51 | return coinData; 52 | } 53 | 54 | const canBuyCoin = (coin, amountToBuy, usdAmount) => { 55 | console.log('can buycoin') 56 | console.log(usdAmount) 57 | console.log(coin.Item.currentPrice.N) 58 | console.log(amountToBuy) 59 | return usdAmount >= parseFloat(coin.Item.currentPrice.N) * amountToBuy 60 | } 61 | 62 | const canSellCoin = (amountToSell, portfolioAmount) => { 63 | return portfolioAmount >= amountToSell 64 | } 65 | 66 | const buyCoin = async ( 67 | coin, 68 | amountToBuy, 69 | usdPortfolioCoinId, 70 | usdAmount, 71 | coinAmount, 72 | userId) => { 73 | const date = new Date(); 74 | // decrease USD 75 | const newUsdAmount = usdAmount - parseFloat(coin.Item.currentPrice.N) * amountToBuy; 76 | const params = { 77 | Item: { 78 | id: { S: usdPortfolioCoinId }, 79 | '__typename': { S: 'PortfolioCoin' }, 80 | 'createdAt': { S: date.toISOString() }, 81 | 'updatedAt': { S: date.toISOString() }, 82 | 'userId': { S: userId }, 83 | 'coinId': { S: process.env.USD_COIN_ID }, 84 | 'amount': { N: newUsdAmount.toString() } 85 | }, 86 | TableName: process.env.PORTFOLIO_COIN_TABLE 87 | } 88 | await ddb.putItem(params).promise(); 89 | 90 | // ADD new portfolio coin, or update the existing one 91 | const newCoinAmount = coinAmount + amountToBuy; 92 | const params1 = { 93 | Item: { 94 | id: { S: `${userId}-${coin.Item.symbol.S}` }, 95 | '__typename': { S: 'PortfolioCoin' }, 96 | 'createdAt': { S: date.toISOString() }, 97 | 'updatedAt': { S: date.toISOString() }, 98 | 'userId': { S: userId }, 99 | 'coinId': { S: coin.Item.id.S }, 100 | 'amount': { N: newCoinAmount.toString() } 101 | }, 102 | TableName: process.env.PORTFOLIO_COIN_TABLE 103 | } 104 | await ddb.putItem(params1).promise(); 105 | } 106 | 107 | const sellCoin = async ( 108 | coin, 109 | amountToSell, 110 | usdPortfolioCoinId, 111 | usdAmount, 112 | coinAmount, 113 | userId) => { 114 | const date = new Date(); 115 | // increase USD 116 | const newUsdAmount = usdAmount + parseFloat(coin.Item.currentPrice.N) * amountToSell; 117 | const params = { 118 | Item: { 119 | id: { S: usdPortfolioCoinId }, 120 | '__typename': { S: 'PortfolioCoin' }, 121 | 'createdAt': { S: date.toISOString() }, 122 | 'updatedAt': { S: date.toISOString() }, 123 | 'userId': { S: userId }, 124 | 'coinId': { S: process.env.USD_COIN_ID }, 125 | 'amount': { N: newUsdAmount.toString() } 126 | }, 127 | TableName: process.env.PORTFOLIO_COIN_TABLE 128 | } 129 | await ddb.putItem(params).promise(); 130 | 131 | // ADD new portfolio coin, or update the existing one 132 | const newCoinAmount = coinAmount - amountToSell; 133 | const params1 = { 134 | Item: { 135 | id: { S: `${userId}-${coin.Item.symbol.S}` }, 136 | '__typename': { S: 'PortfolioCoin' }, 137 | 'createdAt': { S: date.toISOString() }, 138 | 'updatedAt': { S: date.toISOString() }, 139 | 'userId': { S: userId }, 140 | 'coinId': { S: coin.Item.id.S }, 141 | 'amount': { N: newCoinAmount.toString() } 142 | }, 143 | TableName: process.env.PORTFOLIO_COIN_TABLE 144 | } 145 | await ddb.putItem(params1).promise(); 146 | } 147 | 148 | /** 149 | * Using this as the entry point, you can use a single function to handle many resolvers. 150 | */ 151 | const resolvers = { 152 | Mutation: { 153 | exchangeCoins: async ctx => { 154 | console.log('ctx'); 155 | console.log(ctx); 156 | const { 157 | coinId, 158 | isBuy, 159 | amount, 160 | usdPortfolioCoinId, 161 | coinPortfolioCoinId, 162 | } = ctx.arguments; 163 | const userId = ctx.identity.sub; 164 | 165 | const usdAmount = !usdPortfolioCoinId ? 0 : await getUsdAmount(usdPortfolioCoinId, userId) 166 | const coinAmount = !coinPortfolioCoinId ? 0 : await getCoinAmount(coinPortfolioCoinId, userId) 167 | const coin = await getCoin(coinId); 168 | 169 | try { 170 | if (isBuy && canBuyCoin(coin, amount, usdAmount)) { 171 | await buyCoin(coin, amount, usdPortfolioCoinId, usdAmount, coinAmount, userId); 172 | } 173 | else if (!isBuy && canSellCoin(amount, coinAmount)) { 174 | await sellCoin(coin, amount, usdPortfolioCoinId, usdAmount, coinAmount, userId); 175 | } else { 176 | throw new Error(isBuy ? `Not enough USD` : 'Not enough coins to sell'); 177 | } 178 | } catch (e) { 179 | console.log(e); 180 | throw new Error('Unexpected Error exchanging coins'); 181 | } 182 | 183 | return true; 184 | } 185 | }, 186 | } 187 | 188 | exports.handler = async (event) => { 189 | const typeHandler = resolvers[event.typeName]; 190 | if (typeHandler) { 191 | const resolver = typeHandler[event.fieldName]; 192 | if (resolver) { 193 | return await resolver(event); 194 | } 195 | } 196 | throw new Error("Resolver not found."); 197 | }; 198 | -------------------------------------------------------------------------------- /src/API.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | export type ModelUserFilterInput = { 6 | id?: ModelIDInput | null, 7 | type?: ModelStringInput | null, 8 | email?: ModelStringInput | null, 9 | name?: ModelStringInput | null, 10 | image?: ModelStringInput | null, 11 | networth?: ModelFloatInput | null, 12 | and?: Array< ModelUserFilterInput | null > | null, 13 | or?: Array< ModelUserFilterInput | null > | null, 14 | not?: ModelUserFilterInput | null, 15 | }; 16 | 17 | export type ModelIDInput = { 18 | ne?: string | null, 19 | eq?: string | null, 20 | le?: string | null, 21 | lt?: string | null, 22 | ge?: string | null, 23 | gt?: string | null, 24 | contains?: string | null, 25 | notContains?: string | null, 26 | between?: Array< string | null > | null, 27 | beginsWith?: string | null, 28 | attributeExists?: boolean | null, 29 | attributeType?: ModelAttributeTypes | null, 30 | size?: ModelSizeInput | null, 31 | }; 32 | 33 | export enum ModelAttributeTypes { 34 | binary = "binary", 35 | binarySet = "binarySet", 36 | bool = "bool", 37 | list = "list", 38 | map = "map", 39 | number = "number", 40 | numberSet = "numberSet", 41 | string = "string", 42 | stringSet = "stringSet", 43 | _null = "_null", 44 | } 45 | 46 | 47 | export type ModelSizeInput = { 48 | ne?: number | null, 49 | eq?: number | null, 50 | le?: number | null, 51 | lt?: number | null, 52 | ge?: number | null, 53 | gt?: number | null, 54 | between?: Array< number | null > | null, 55 | }; 56 | 57 | export type ModelStringInput = { 58 | ne?: string | null, 59 | eq?: string | null, 60 | le?: string | null, 61 | lt?: string | null, 62 | ge?: string | null, 63 | gt?: string | null, 64 | contains?: string | null, 65 | notContains?: string | null, 66 | between?: Array< string | null > | null, 67 | beginsWith?: string | null, 68 | attributeExists?: boolean | null, 69 | attributeType?: ModelAttributeTypes | null, 70 | size?: ModelSizeInput | null, 71 | }; 72 | 73 | export type ModelFloatInput = { 74 | ne?: number | null, 75 | eq?: number | null, 76 | le?: number | null, 77 | lt?: number | null, 78 | ge?: number | null, 79 | gt?: number | null, 80 | between?: Array< number | null > | null, 81 | attributeExists?: boolean | null, 82 | attributeType?: ModelAttributeTypes | null, 83 | }; 84 | 85 | export type ModelPortfolioCoinFilterInput = { 86 | id?: ModelIDInput | null, 87 | amount?: ModelFloatInput | null, 88 | userId?: ModelIDInput | null, 89 | coinId?: ModelIDInput | null, 90 | and?: Array< ModelPortfolioCoinFilterInput | null > | null, 91 | or?: Array< ModelPortfolioCoinFilterInput | null > | null, 92 | not?: ModelPortfolioCoinFilterInput | null, 93 | }; 94 | 95 | export type ModelCoinFilterInput = { 96 | id?: ModelIDInput | null, 97 | cgId?: ModelStringInput | null, 98 | name?: ModelStringInput | null, 99 | symbol?: ModelStringInput | null, 100 | image?: ModelStringInput | null, 101 | currentPrice?: ModelFloatInput | null, 102 | valueChange24H?: ModelFloatInput | null, 103 | valueChange1D?: ModelFloatInput | null, 104 | valueChange7D?: ModelFloatInput | null, 105 | priceHistoryString?: ModelStringInput | null, 106 | and?: Array< ModelCoinFilterInput | null > | null, 107 | or?: Array< ModelCoinFilterInput | null > | null, 108 | not?: ModelCoinFilterInput | null, 109 | }; 110 | 111 | export type ModelFloatKeyConditionInput = { 112 | eq?: number | null, 113 | le?: number | null, 114 | lt?: number | null, 115 | ge?: number | null, 116 | gt?: number | null, 117 | between?: Array< number | null > | null, 118 | }; 119 | 120 | export enum ModelSortDirection { 121 | ASC = "ASC", 122 | DESC = "DESC", 123 | } 124 | 125 | 126 | export type ExchangeCoinsMutationVariables = { 127 | coinId: string, 128 | isBuy: boolean, 129 | amount: number, 130 | usdPortfolioCoinId?: string | null, 131 | coinPortfolioCoinId?: string | null, 132 | }; 133 | 134 | export type ExchangeCoinsMutation = { 135 | exchangeCoins: boolean, 136 | }; 137 | 138 | export type GetUserQueryVariables = { 139 | id: string, 140 | }; 141 | 142 | export type GetUserQuery = { 143 | getUser: { 144 | __typename: "User", 145 | id: string, 146 | type: string, 147 | email: string, 148 | name: string | null, 149 | image: string | null, 150 | networth: number, 151 | portfolioCoins: { 152 | __typename: "ModelPortfolioCoinConnection", 153 | items: Array< { 154 | __typename: "PortfolioCoin", 155 | id: string, 156 | amount: number, 157 | userId: string, 158 | coinId: string, 159 | createdAt: string, 160 | updatedAt: string, 161 | } | null > | null, 162 | nextToken: string | null, 163 | } | null, 164 | createdAt: string, 165 | updatedAt: string, 166 | } | null, 167 | }; 168 | 169 | export type ListUsersQueryVariables = { 170 | filter?: ModelUserFilterInput | null, 171 | limit?: number | null, 172 | nextToken?: string | null, 173 | }; 174 | 175 | export type ListUsersQuery = { 176 | listUsers: { 177 | __typename: "ModelUserConnection", 178 | items: Array< { 179 | __typename: "User", 180 | id: string, 181 | type: string, 182 | email: string, 183 | name: string | null, 184 | image: string | null, 185 | networth: number, 186 | portfolioCoins: { 187 | __typename: "ModelPortfolioCoinConnection", 188 | nextToken: string | null, 189 | } | null, 190 | createdAt: string, 191 | updatedAt: string, 192 | } | null > | null, 193 | nextToken: string | null, 194 | } | null, 195 | }; 196 | 197 | export type GetPortfolioCoinQueryVariables = { 198 | id: string, 199 | }; 200 | 201 | export type GetPortfolioCoinQuery = { 202 | getPortfolioCoin: { 203 | __typename: "PortfolioCoin", 204 | id: string, 205 | amount: number, 206 | userId: string, 207 | user: { 208 | __typename: "User", 209 | id: string, 210 | type: string, 211 | email: string, 212 | name: string | null, 213 | image: string | null, 214 | networth: number, 215 | portfolioCoins: { 216 | __typename: "ModelPortfolioCoinConnection", 217 | nextToken: string | null, 218 | } | null, 219 | createdAt: string, 220 | updatedAt: string, 221 | } | null, 222 | coinId: string, 223 | coin: { 224 | __typename: "Coin", 225 | id: string, 226 | cgId: string, 227 | name: string, 228 | symbol: string, 229 | image: string | null, 230 | currentPrice: number, 231 | valueChange24H: number, 232 | valueChange1D: number, 233 | valueChange7D: number, 234 | priceHistoryString: string | null, 235 | createdAt: string, 236 | updatedAt: string, 237 | } | null, 238 | createdAt: string, 239 | updatedAt: string, 240 | } | null, 241 | }; 242 | 243 | export type ListPortfolioCoinsQueryVariables = { 244 | filter?: ModelPortfolioCoinFilterInput | null, 245 | limit?: number | null, 246 | nextToken?: string | null, 247 | }; 248 | 249 | export type ListPortfolioCoinsQuery = { 250 | listPortfolioCoins: { 251 | __typename: "ModelPortfolioCoinConnection", 252 | items: Array< { 253 | __typename: "PortfolioCoin", 254 | id: string, 255 | amount: number, 256 | userId: string, 257 | user: { 258 | __typename: "User", 259 | id: string, 260 | type: string, 261 | email: string, 262 | name: string | null, 263 | image: string | null, 264 | networth: number, 265 | createdAt: string, 266 | updatedAt: string, 267 | } | null, 268 | coinId: string, 269 | coin: { 270 | __typename: "Coin", 271 | id: string, 272 | cgId: string, 273 | name: string, 274 | symbol: string, 275 | image: string | null, 276 | currentPrice: number, 277 | valueChange24H: number, 278 | valueChange1D: number, 279 | valueChange7D: number, 280 | priceHistoryString: string | null, 281 | createdAt: string, 282 | updatedAt: string, 283 | } | null, 284 | createdAt: string, 285 | updatedAt: string, 286 | } | null > | null, 287 | nextToken: string | null, 288 | } | null, 289 | }; 290 | 291 | export type GetCoinQueryVariables = { 292 | id: string, 293 | }; 294 | 295 | export type GetCoinQuery = { 296 | getCoin: { 297 | __typename: "Coin", 298 | id: string, 299 | cgId: string, 300 | name: string, 301 | symbol: string, 302 | image: string | null, 303 | currentPrice: number, 304 | valueChange24H: number, 305 | valueChange1D: number, 306 | valueChange7D: number, 307 | priceHistoryString: string | null, 308 | createdAt: string, 309 | updatedAt: string, 310 | } | null, 311 | }; 312 | 313 | export type ListCoinsQueryVariables = { 314 | filter?: ModelCoinFilterInput | null, 315 | limit?: number | null, 316 | nextToken?: string | null, 317 | }; 318 | 319 | export type ListCoinsQuery = { 320 | listCoins: { 321 | __typename: "ModelCoinConnection", 322 | items: Array< { 323 | __typename: "Coin", 324 | id: string, 325 | cgId: string, 326 | name: string, 327 | symbol: string, 328 | image: string | null, 329 | currentPrice: number, 330 | valueChange24H: number, 331 | valueChange1D: number, 332 | valueChange7D: number, 333 | priceHistoryString: string | null, 334 | createdAt: string, 335 | updatedAt: string, 336 | } | null > | null, 337 | nextToken: string | null, 338 | } | null, 339 | }; 340 | 341 | export type GetUsersByNetworthQueryVariables = { 342 | type?: string | null, 343 | networth?: ModelFloatKeyConditionInput | null, 344 | sortDirection?: ModelSortDirection | null, 345 | filter?: ModelUserFilterInput | null, 346 | limit?: number | null, 347 | nextToken?: string | null, 348 | }; 349 | 350 | export type GetUsersByNetworthQuery = { 351 | getUsersByNetworth: { 352 | __typename: "ModelUserConnection", 353 | items: Array< { 354 | __typename: "User", 355 | id: string, 356 | type: string, 357 | email: string, 358 | name: string | null, 359 | image: string | null, 360 | networth: number, 361 | portfolioCoins: { 362 | __typename: "ModelPortfolioCoinConnection", 363 | nextToken: string | null, 364 | } | null, 365 | createdAt: string, 366 | updatedAt: string, 367 | } | null > | null, 368 | nextToken: string | null, 369 | } | null, 370 | }; 371 | -------------------------------------------------------------------------------- /amplify/backend/auth/vcrypto16d3e4c5/vcrypto16d3e4c5-cloudformation-template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | env: 5 | Type: String 6 | authRoleArn: 7 | Type: String 8 | unauthRoleArn: 9 | Type: String 10 | 11 | 12 | 13 | 14 | functionvcrypto16d3e4c5PostConfirmationArn: 15 | Type: String 16 | Default: functionvcrypto16d3e4c5PostConfirmationArn 17 | 18 | functionvcrypto16d3e4c5PostConfirmationName: 19 | Type: String 20 | Default: functionvcrypto16d3e4c5PostConfirmationName 21 | 22 | 23 | 24 | 25 | 26 | identityPoolName: 27 | Type: String 28 | 29 | allowUnauthenticatedIdentities: 30 | Type: String 31 | 32 | resourceNameTruncated: 33 | Type: String 34 | 35 | userPoolName: 36 | Type: String 37 | 38 | autoVerifiedAttributes: 39 | Type: CommaDelimitedList 40 | 41 | mfaConfiguration: 42 | Type: String 43 | 44 | mfaTypes: 45 | Type: CommaDelimitedList 46 | 47 | smsAuthenticationMessage: 48 | Type: String 49 | 50 | smsVerificationMessage: 51 | Type: String 52 | 53 | emailVerificationSubject: 54 | Type: String 55 | 56 | emailVerificationMessage: 57 | Type: String 58 | 59 | defaultPasswordPolicy: 60 | Type: String 61 | 62 | passwordPolicyMinLength: 63 | Type: Number 64 | 65 | passwordPolicyCharacters: 66 | Type: CommaDelimitedList 67 | 68 | requiredAttributes: 69 | Type: CommaDelimitedList 70 | 71 | userpoolClientGenerateSecret: 72 | Type: String 73 | 74 | userpoolClientRefreshTokenValidity: 75 | Type: Number 76 | 77 | userpoolClientWriteAttributes: 78 | Type: CommaDelimitedList 79 | 80 | userpoolClientReadAttributes: 81 | Type: CommaDelimitedList 82 | 83 | userpoolClientLambdaRole: 84 | Type: String 85 | 86 | userpoolClientSetAttributes: 87 | Type: String 88 | 89 | sharedId: 90 | Type: String 91 | 92 | resourceName: 93 | Type: String 94 | 95 | authSelections: 96 | Type: String 97 | 98 | useDefault: 99 | Type: String 100 | 101 | hostedUI: 102 | Type: String 103 | 104 | triggers: 105 | Type: String 106 | 107 | hostedUIDomainName: 108 | Type: String 109 | 110 | authProvidersUserPool: 111 | Type: CommaDelimitedList 112 | 113 | hostedUIProviderMeta: 114 | Type: String 115 | 116 | userPoolGroupList: 117 | Type: CommaDelimitedList 118 | 119 | parentStack: 120 | Type: String 121 | 122 | permissions: 123 | Type: CommaDelimitedList 124 | 125 | dependsOn: 126 | Type: CommaDelimitedList 127 | 128 | thirdPartyAuth: 129 | Type: String 130 | 131 | userPoolGroups: 132 | Type: String 133 | 134 | adminQueries: 135 | Type: String 136 | 137 | hostedUIProviderCreds: 138 | Type: String 139 | 140 | oAuthMetadata: 141 | Type: String 142 | 143 | Conditions: 144 | ShouldNotCreateEnvResources: !Equals [ !Ref env, NONE ] 145 | 146 | Resources: 147 | 148 | 149 | # BEGIN SNS ROLE RESOURCE 150 | SNSRole: 151 | # Created to allow the UserPool SMS Config to publish via the Simple Notification Service during MFA Process 152 | Type: AWS::IAM::Role 153 | Properties: 154 | RoleName: !If [ShouldNotCreateEnvResources, 'vcrypt16d3e4c5_sns-role', !Join ['',[ 'sns', '16d3e4c5', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 155 | AssumeRolePolicyDocument: 156 | Version: "2012-10-17" 157 | Statement: 158 | - Sid: "" 159 | Effect: "Allow" 160 | Principal: 161 | Service: "cognito-idp.amazonaws.com" 162 | Action: 163 | - "sts:AssumeRole" 164 | Condition: 165 | StringEquals: 166 | sts:ExternalId: vcrypt16d3e4c5_role_external_id 167 | Policies: 168 | - 169 | PolicyName: vcrypt16d3e4c5-sns-policy 170 | PolicyDocument: 171 | Version: "2012-10-17" 172 | Statement: 173 | - 174 | Effect: "Allow" 175 | Action: 176 | - "sns:Publish" 177 | Resource: "*" 178 | # BEGIN USER POOL RESOURCES 179 | UserPool: 180 | # Created upon user selection 181 | # Depends on SNS Role for Arn if MFA is enabled 182 | Type: AWS::Cognito::UserPool 183 | UpdateReplacePolicy: Retain 184 | Properties: 185 | UserPoolName: !If [ShouldNotCreateEnvResources, !Ref userPoolName, !Join ['',[!Ref userPoolName, '-', !Ref env]]] 186 | 187 | Schema: 188 | 189 | - 190 | Name: email 191 | Required: true 192 | Mutable: true 193 | 194 | 195 | 196 | LambdaConfig: 197 | 198 | 199 | 200 | 201 | 202 | PostConfirmation: !Ref functionvcrypto16d3e4c5PostConfirmationArn 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | AutoVerifiedAttributes: !Ref autoVerifiedAttributes 211 | 212 | 213 | EmailVerificationMessage: !Ref emailVerificationMessage 214 | EmailVerificationSubject: !Ref emailVerificationSubject 215 | 216 | Policies: 217 | PasswordPolicy: 218 | MinimumLength: !Ref passwordPolicyMinLength 219 | RequireLowercase: false 220 | RequireNumbers: false 221 | RequireSymbols: false 222 | RequireUppercase: false 223 | 224 | MfaConfiguration: !Ref mfaConfiguration 225 | SmsVerificationMessage: !Ref smsVerificationMessage 226 | SmsConfiguration: 227 | SnsCallerArn: !GetAtt SNSRole.Arn 228 | ExternalId: vcrypt16d3e4c5_role_external_id 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | UserPoolPostConfirmationLambdaInvokePermission: 237 | Type: "AWS::Lambda::Permission" 238 | DependsOn: UserPool 239 | Properties: 240 | Action: "lambda:invokeFunction" 241 | Principal: "cognito-idp.amazonaws.com" 242 | FunctionName: !Ref functionvcrypto16d3e4c5PostConfirmationName 243 | SourceArn: !GetAtt UserPool.Arn 244 | 245 | 246 | 247 | 248 | 249 | # Updating lambda role with permissions to Cognito 250 | 251 | 252 | UserPoolClientWeb: 253 | # Created provide application access to user pool 254 | # Depends on UserPool for ID reference 255 | Type: "AWS::Cognito::UserPoolClient" 256 | Properties: 257 | ClientName: vcrypt16d3e4c5_app_clientWeb 258 | 259 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 260 | UserPoolId: !Ref UserPool 261 | DependsOn: UserPool 262 | UserPoolClient: 263 | # Created provide application access to user pool 264 | # Depends on UserPool for ID reference 265 | Type: "AWS::Cognito::UserPoolClient" 266 | Properties: 267 | ClientName: vcrypt16d3e4c5_app_client 268 | 269 | GenerateSecret: !Ref userpoolClientGenerateSecret 270 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 271 | UserPoolId: !Ref UserPool 272 | DependsOn: UserPool 273 | # BEGIN USER POOL LAMBDA RESOURCES 274 | UserPoolClientRole: 275 | # Created to execute Lambda which gets userpool app client config values 276 | Type: 'AWS::IAM::Role' 277 | Properties: 278 | RoleName: !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',['upClientLambdaRole', '16d3e4c5', !Select [3, !Split ['-', !Ref 'AWS::StackName']], '-', !Ref env]]] 279 | AssumeRolePolicyDocument: 280 | Version: '2012-10-17' 281 | Statement: 282 | - Effect: Allow 283 | Principal: 284 | Service: 285 | - lambda.amazonaws.com 286 | Action: 287 | - 'sts:AssumeRole' 288 | DependsOn: UserPoolClient 289 | UserPoolClientLambda: 290 | # Lambda which gets userpool app client config values 291 | # Depends on UserPool for id 292 | # Depends on UserPoolClientRole for role ARN 293 | Type: 'AWS::Lambda::Function' 294 | Properties: 295 | Code: 296 | ZipFile: !Join 297 | - |+ 298 | - - 'const response = require(''cfn-response'');' 299 | - 'const aws = require(''aws-sdk'');' 300 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 301 | - 'exports.handler = (event, context, callback) => {' 302 | - ' if (event.RequestType == ''Delete'') { ' 303 | - ' response.send(event, context, response.SUCCESS, {})' 304 | - ' }' 305 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 306 | - ' const params = {' 307 | - ' ClientId: event.ResourceProperties.clientId,' 308 | - ' UserPoolId: event.ResourceProperties.userpoolId' 309 | - ' };' 310 | - ' identity.describeUserPoolClient(params).promise()' 311 | - ' .then((res) => {' 312 | - ' response.send(event, context, response.SUCCESS, {''appSecret'': res.UserPoolClient.ClientSecret});' 313 | - ' })' 314 | - ' .catch((err) => {' 315 | - ' response.send(event, context, response.FAILED, {err});' 316 | - ' });' 317 | - ' }' 318 | - '};' 319 | Handler: index.handler 320 | Runtime: nodejs10.x 321 | Timeout: '300' 322 | Role: !GetAtt 323 | - UserPoolClientRole 324 | - Arn 325 | DependsOn: UserPoolClientRole 326 | UserPoolClientLambdaPolicy: 327 | # Sets userpool policy for the role that executes the Userpool Client Lambda 328 | # Depends on UserPool for Arn 329 | # Marked as depending on UserPoolClientRole for easier to understand CFN sequencing 330 | Type: 'AWS::IAM::Policy' 331 | Properties: 332 | PolicyName: vcrypt16d3e4c5_userpoolclient_lambda_iam_policy 333 | Roles: 334 | - !Ref UserPoolClientRole 335 | PolicyDocument: 336 | Version: '2012-10-17' 337 | Statement: 338 | - Effect: Allow 339 | Action: 340 | - 'cognito-idp:DescribeUserPoolClient' 341 | Resource: !GetAtt UserPool.Arn 342 | DependsOn: UserPoolClientLambda 343 | UserPoolClientLogPolicy: 344 | # Sets log policy for the role that executes the Userpool Client Lambda 345 | # Depends on UserPool for Arn 346 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 347 | Type: 'AWS::IAM::Policy' 348 | Properties: 349 | PolicyName: vcrypt16d3e4c5_userpoolclient_lambda_log_policy 350 | Roles: 351 | - !Ref UserPoolClientRole 352 | PolicyDocument: 353 | Version: 2012-10-17 354 | Statement: 355 | - Effect: Allow 356 | Action: 357 | - 'logs:CreateLogGroup' 358 | - 'logs:CreateLogStream' 359 | - 'logs:PutLogEvents' 360 | Resource: !Sub 361 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 362 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref UserPoolClientLambda} 363 | DependsOn: UserPoolClientLambdaPolicy 364 | UserPoolClientInputs: 365 | # Values passed to Userpool client Lambda 366 | # Depends on UserPool for Id 367 | # Depends on UserPoolClient for Id 368 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 369 | Type: 'Custom::LambdaCallout' 370 | Properties: 371 | ServiceToken: !GetAtt UserPoolClientLambda.Arn 372 | clientId: !Ref UserPoolClient 373 | userpoolId: !Ref UserPool 374 | DependsOn: UserPoolClientLogPolicy 375 | 376 | HostedUICustomResource: 377 | Type: 'AWS::Lambda::Function' 378 | Properties: 379 | Code: 380 | ZipFile: !Join 381 | - |+ 382 | - - 'const response = require(''cfn-response'');' 383 | - 'const aws = require(''aws-sdk'');' 384 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 385 | - 'exports.handler = (event, context, callback) => {' 386 | - ' const userPoolId = event.ResourceProperties.userPoolId;' 387 | - ' const inputDomainName = event.ResourceProperties.hostedUIDomainName;' 388 | - ' let deleteUserPoolDomain = (domainName) => {' 389 | - ' let params = { Domain: domainName, UserPoolId: userPoolId };' 390 | - ' return identity.deleteUserPoolDomain(params).promise();' 391 | - ' };' 392 | - ' if (event.RequestType == ''Delete'') {' 393 | - ' deleteUserPoolDomain(inputDomainName)' 394 | - ' .then(() => {response.send(event, context, response.SUCCESS, {})})' 395 | - ' .catch((err) => { console.log(err); response.send(event, context, response.FAILED, {err}) });' 396 | - ' }' 397 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 398 | - ' let checkDomainAvailability = (domainName) => {' 399 | - ' let params = { Domain: domainName };' 400 | - ' return identity.describeUserPoolDomain(params).promise().then((res) => {' 401 | - ' if (res.DomainDescription && res.DomainDescription.UserPool) {' 402 | - ' return false;' 403 | - ' }' 404 | - ' return true;' 405 | - ' }).catch((err) => { return false; });' 406 | - ' };' 407 | - ' let createUserPoolDomain = (domainName) => {' 408 | - ' let params = { Domain: domainName, UserPoolId: userPoolId };' 409 | - ' return identity.createUserPoolDomain(params).promise();' 410 | - ' };' 411 | - ' identity.describeUserPool({UserPoolId: userPoolId }).promise().then((result) => {' 412 | - ' if (inputDomainName) {' 413 | - ' if (result.UserPool.Domain === inputDomainName) {' 414 | - ' return;' 415 | - ' } else {' 416 | - ' if (!result.UserPool.Domain) {' 417 | - ' return checkDomainAvailability(inputDomainName).then((isDomainAvailable) => {' 418 | - ' if (isDomainAvailable) {' 419 | - ' return createUserPoolDomain(inputDomainName);' 420 | - ' } else {' 421 | - ' throw new Error(''Domain not available'');' 422 | - ' }' 423 | - ' });' 424 | - ' } else {' 425 | - ' return checkDomainAvailability(inputDomainName).then((isDomainAvailable) => {' 426 | - ' if (isDomainAvailable) {' 427 | - ' return deleteUserPoolDomain(result.UserPool.Domain).then(() => createUserPoolDomain(inputDomainName));' 428 | - ' } else {' 429 | - ' throw new Error(''Domain not available'');' 430 | - ' }' 431 | - ' });' 432 | - ' }' 433 | - ' }' 434 | - ' } else {' 435 | - ' if (result.UserPool.Domain) {' 436 | - ' return deleteUserPoolDomain(result.UserPool.Domain);' 437 | - ' }' 438 | - ' }' 439 | - ' }).then(() => {response.send(event, context, response.SUCCESS, {})}).catch((err) => {' 440 | - ' console.log(err); response.send(event, context, response.FAILED, {err});' 441 | - ' });' 442 | - '}}' 443 | 444 | 445 | Handler: index.handler 446 | Runtime: nodejs10.x 447 | Timeout: '300' 448 | Role: !GetAtt 449 | - UserPoolClientRole 450 | - Arn 451 | DependsOn: UserPoolClientRole 452 | 453 | HostedUICustomResourcePolicy: 454 | Type: 'AWS::IAM::Policy' 455 | Properties: 456 | PolicyName: !Join ['-',[!Ref UserPool, 'hostedUI']] 457 | Roles: 458 | - !Ref UserPoolClientRole 459 | PolicyDocument: 460 | Version: '2012-10-17' 461 | Statement: 462 | - Effect: Allow 463 | Action: 464 | - 'cognito-idp:CreateUserPoolDomain' 465 | - 'cognito-idp:DescribeUserPool' 466 | - 'cognito-idp:DeleteUserPoolDomain' 467 | Resource: !GetAtt UserPool.Arn 468 | - Effect: Allow 469 | Action: 470 | - 'cognito-idp:DescribeUserPoolDomain' 471 | Resource: '*' 472 | DependsOn: HostedUICustomResource 473 | HostedUICustomResourceLogPolicy: 474 | Type: 'AWS::IAM::Policy' 475 | Properties: 476 | PolicyName: !Join ['-',[!Ref UserPool, 'hostedUILogPolicy']] 477 | Roles: 478 | - !Ref UserPoolClientRole 479 | PolicyDocument: 480 | Version: 2012-10-17 481 | Statement: 482 | - Effect: Allow 483 | Action: 484 | - 'logs:CreateLogGroup' 485 | - 'logs:CreateLogStream' 486 | - 'logs:PutLogEvents' 487 | Resource: !Sub 488 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 489 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref HostedUICustomResource} 490 | DependsOn: HostedUICustomResourcePolicy 491 | HostedUICustomResourceInputs: 492 | Type: 'Custom::LambdaCallout' 493 | Properties: 494 | ServiceToken: !GetAtt HostedUICustomResource.Arn 495 | userPoolId: !Ref UserPool 496 | hostedUIDomainName: !If [ShouldNotCreateEnvResources, !Ref hostedUIDomainName, !Join ['-',[!Ref hostedUIDomainName, !Ref env]]] 497 | DependsOn: HostedUICustomResourceLogPolicy 498 | 499 | 500 | 501 | HostedUIProvidersCustomResource: 502 | Type: 'AWS::Lambda::Function' 503 | Properties: 504 | Code: 505 | ZipFile: !Join 506 | - |+ 507 | - - 'const response = require(''cfn-response'');' 508 | - 'const aws = require(''aws-sdk'');' 509 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 510 | - 'exports.handler = (event, context, callback) => {' 511 | - 'try{' 512 | - ' const userPoolId = event.ResourceProperties.userPoolId;' 513 | - ' let hostedUIProviderMeta = JSON.parse(event.ResourceProperties.hostedUIProviderMeta);' 514 | - ' let hostedUIProviderCreds = JSON.parse(event.ResourceProperties.hostedUIProviderCreds);' 515 | - ' if (event.RequestType == ''Delete'') {' 516 | - ' response.send(event, context, response.SUCCESS, {});' 517 | - ' }' 518 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 519 | - ' let getRequestParams = (providerName) => {' 520 | - ' let providerMetaIndex = hostedUIProviderMeta.findIndex((provider) => provider.ProviderName === providerName);' 521 | - ' let providerMeta = hostedUIProviderMeta[providerMetaIndex];' 522 | - ' let providerCredsIndex = hostedUIProviderCreds.findIndex((provider) => provider.ProviderName === providerName);' 523 | - ' let providerCreds = hostedUIProviderCreds[providerCredsIndex];' 524 | - ' let requestParams = {' 525 | - ' ProviderDetails: {' 526 | - ' ''client_id'': providerCreds.client_id,' 527 | - ' ''client_secret'': providerCreds.client_secret,' 528 | - ' ''authorize_scopes'': providerMeta.authorize_scopes' 529 | - ' },' 530 | - ' ProviderName: providerMeta.ProviderName,' 531 | - ' UserPoolId: userPoolId,' 532 | - ' AttributeMapping: providerMeta.AttributeMapping' 533 | - ' };' 534 | - ' return requestParams;' 535 | - ' };' 536 | - ' let createIdentityProvider = (providerName) => {' 537 | - ' let requestParams = getRequestParams(providerName);' 538 | - ' requestParams.ProviderType = requestParams.ProviderName;' 539 | - ' return identity.createIdentityProvider(requestParams).promise();' 540 | - ' };' 541 | - ' let updateIdentityProvider = (providerName) => {' 542 | - ' let requestParams = getRequestParams(providerName);' 543 | - ' return identity.updateIdentityProvider(requestParams).promise();' 544 | - ' };' 545 | - ' let deleteIdentityProvider = (providerName) => {' 546 | - ' let params = {ProviderName: providerName, UserPoolId: userPoolId};' 547 | - ' return identity.deleteIdentityProvider(params).promise();' 548 | - ' };' 549 | - ' let providerPromises = [];' 550 | - ' identity.listIdentityProviders({UserPoolId: userPoolId, MaxResults: 60}).promise()' 551 | - ' .then((result) => {' 552 | - ' let providerList = result.Providers.map(provider => provider.ProviderName);' 553 | - ' let providerListInParameters = hostedUIProviderMeta.map(provider => provider.ProviderName);' 554 | - ' hostedUIProviderMeta.forEach((providerMetadata) => {' 555 | - ' if(providerList.indexOf(providerMetadata.ProviderName) > -1) {' 556 | - ' providerPromises.push(updateIdentityProvider(providerMetadata.ProviderName));' 557 | - ' } else {' 558 | - ' providerPromises.push(createIdentityProvider(providerMetadata.ProviderName));' 559 | - ' }' 560 | - ' });' 561 | - ' providerList.forEach((provider) => {' 562 | - ' if(providerListInParameters.indexOf(provider) < 0) {' 563 | - ' providerPromises.push(deleteIdentityProvider(provider));' 564 | - ' }' 565 | - ' });' 566 | - ' return Promise.all(providerPromises);' 567 | - ' }).then(() => {response.send(event, context, response.SUCCESS, {})}).catch((err) => {' 568 | - ' console.log(err.stack); response.send(event, context, response.FAILED, {err})' 569 | - ' });' 570 | - ' } ' 571 | - ' } catch(err) { console.log(err.stack); response.send(event, context, response.FAILED, {err});};' 572 | - '} ' 573 | 574 | Handler: index.handler 575 | Runtime: nodejs10.x 576 | Timeout: '300' 577 | Role: !GetAtt 578 | - UserPoolClientRole 579 | - Arn 580 | DependsOn: UserPoolClientRole 581 | 582 | HostedUIProvidersCustomResourcePolicy: 583 | Type: 'AWS::IAM::Policy' 584 | Properties: 585 | PolicyName: !Join ['-',[!Ref UserPool, 'hostedUIProvider']] 586 | Roles: 587 | - !Ref UserPoolClientRole 588 | PolicyDocument: 589 | Version: '2012-10-17' 590 | Statement: 591 | - Effect: Allow 592 | Action: 593 | - 'cognito-idp:CreateIdentityProvider' 594 | - 'cognito-idp:UpdateIdentityProvider' 595 | - 'cognito-idp:ListIdentityProviders' 596 | - 'cognito-idp:DeleteIdentityProvider' 597 | Resource: !GetAtt UserPool.Arn 598 | DependsOn: HostedUIProvidersCustomResource 599 | 600 | HostedUIProvidersCustomResourceLogPolicy: 601 | Type: 'AWS::IAM::Policy' 602 | Properties: 603 | PolicyName: !Join ['-',[!Ref UserPool, 'hostedUIProviderLogPolicy']] 604 | Roles: 605 | - !Ref UserPoolClientRole 606 | PolicyDocument: 607 | Version: 2012-10-17 608 | Statement: 609 | - Effect: Allow 610 | Action: 611 | - 'logs:CreateLogGroup' 612 | - 'logs:CreateLogStream' 613 | - 'logs:PutLogEvents' 614 | Resource: !Sub 615 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 616 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref HostedUIProvidersCustomResource} 617 | DependsOn: HostedUIProvidersCustomResourcePolicy 618 | 619 | HostedUIProvidersCustomResourceInputs: 620 | Type: 'Custom::LambdaCallout' 621 | Properties: 622 | ServiceToken: !GetAtt HostedUIProvidersCustomResource.Arn 623 | userPoolId: !Ref UserPool 624 | hostedUIProviderMeta: !Ref hostedUIProviderMeta 625 | hostedUIProviderCreds: !Ref hostedUIProviderCreds 626 | DependsOn: HostedUIProvidersCustomResourceLogPolicy 627 | 628 | 629 | OAuthCustomResource: 630 | Type: 'AWS::Lambda::Function' 631 | Properties: 632 | Code: 633 | ZipFile: !Join 634 | - |+ 635 | - - 'const response = require(''cfn-response'');' 636 | - 'const aws = require(''aws-sdk'');' 637 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 638 | - 'exports.handler = (event, context, callback) => {' 639 | - 'try{' 640 | - ' const userPoolId = event.ResourceProperties.userPoolId;' 641 | - ' let webClientId = event.ResourceProperties.webClientId;' 642 | - ' let nativeClientId = event.ResourceProperties.nativeClientId;' 643 | - ' let hostedUIProviderMeta = JSON.parse(event.ResourceProperties.hostedUIProviderMeta);' 644 | - ' let oAuthMetadata = JSON.parse(event.ResourceProperties.oAuthMetadata);' 645 | - ' let providerList = hostedUIProviderMeta.map(provider => provider.ProviderName);' 646 | - ' providerList.push(''COGNITO'');' 647 | - ' if (event.RequestType == ''Delete'') {' 648 | - ' response.send(event, context, response.SUCCESS, {});' 649 | - ' }' 650 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 651 | - ' let params = {' 652 | - ' UserPoolId: userPoolId,' 653 | - ' AllowedOAuthFlows: oAuthMetadata.AllowedOAuthFlows,' 654 | - ' AllowedOAuthFlowsUserPoolClient: true,' 655 | - ' AllowedOAuthScopes: oAuthMetadata.AllowedOAuthScopes,' 656 | - ' CallbackURLs: oAuthMetadata.CallbackURLs,' 657 | - ' LogoutURLs: oAuthMetadata.LogoutURLs,' 658 | - ' SupportedIdentityProviders: providerList' 659 | - ' };' 660 | - ' let updateUserPoolClientPromises = [];' 661 | - ' params.ClientId = webClientId;' 662 | - ' updateUserPoolClientPromises.push(identity.updateUserPoolClient(params).promise());' 663 | - ' params.ClientId = nativeClientId;' 664 | - ' updateUserPoolClientPromises.push(identity.updateUserPoolClient(params).promise());' 665 | - ' Promise.all(updateUserPoolClientPromises)' 666 | - ' .then(() => {response.send(event, context, response.SUCCESS, {})}).catch((err) => {' 667 | - ' console.log(err.stack); response.send(event, context, response.FAILED, {err});' 668 | - ' });' 669 | - ' }' 670 | - '} catch(err) { console.log(err.stack); response.send(event, context, response.FAILED, {err});};' 671 | - '}' 672 | 673 | Handler: index.handler 674 | Runtime: nodejs10.x 675 | Timeout: '300' 676 | Role: !GetAtt 677 | - UserPoolClientRole 678 | - Arn 679 | DependsOn: HostedUIProvidersCustomResourceInputs 680 | 681 | OAuthCustomResourcePolicy: 682 | Type: 'AWS::IAM::Policy' 683 | Properties: 684 | PolicyName: !Join ['-',[!Ref UserPool, 'OAuth']] 685 | Roles: 686 | - !Ref UserPoolClientRole 687 | PolicyDocument: 688 | Version: '2012-10-17' 689 | Statement: 690 | - Effect: Allow 691 | Action: 692 | - 'cognito-idp:UpdateUserPoolClient' 693 | Resource: !GetAtt UserPool.Arn 694 | DependsOn: OAuthCustomResource 695 | 696 | OAuthCustomResourceLogPolicy: 697 | Type: 'AWS::IAM::Policy' 698 | Properties: 699 | PolicyName: !Join ['-',[!Ref UserPool, 'OAuthLogPolicy']] 700 | Roles: 701 | - !Ref UserPoolClientRole 702 | PolicyDocument: 703 | Version: 2012-10-17 704 | Statement: 705 | - Effect: Allow 706 | Action: 707 | - 'logs:CreateLogGroup' 708 | - 'logs:CreateLogStream' 709 | - 'logs:PutLogEvents' 710 | Resource: !Sub 711 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 712 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref OAuthCustomResource} 713 | DependsOn: OAuthCustomResourcePolicy 714 | 715 | OAuthCustomResourceInputs: 716 | Type: 'Custom::LambdaCallout' 717 | Properties: 718 | ServiceToken: !GetAtt OAuthCustomResource.Arn 719 | userPoolId: !Ref UserPool 720 | hostedUIProviderMeta: !Ref hostedUIProviderMeta 721 | oAuthMetadata: !Ref oAuthMetadata 722 | webClientId: !Ref 'UserPoolClientWeb' 723 | nativeClientId: !Ref 'UserPoolClient' 724 | DependsOn: OAuthCustomResourceLogPolicy 725 | 726 | 727 | 728 | 729 | # BEGIN IDENTITY POOL RESOURCES 730 | 731 | 732 | IdentityPool: 733 | # Always created 734 | Type: AWS::Cognito::IdentityPool 735 | Properties: 736 | IdentityPoolName: !If [ShouldNotCreateEnvResources, 'vcrypto16d3e4c5_identitypool_16d3e4c5', !Join ['',['vcrypto16d3e4c5_identitypool_16d3e4c5', '__', !Ref env]]] 737 | 738 | CognitoIdentityProviders: 739 | - ClientId: !Ref UserPoolClient 740 | ProviderName: !Sub 741 | - cognito-idp.${region}.amazonaws.com/${client} 742 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 743 | - ClientId: !Ref UserPoolClientWeb 744 | ProviderName: !Sub 745 | - cognito-idp.${region}.amazonaws.com/${client} 746 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 747 | 748 | AllowUnauthenticatedIdentities: !Ref allowUnauthenticatedIdentities 749 | 750 | 751 | DependsOn: UserPoolClientInputs 752 | 753 | 754 | IdentityPoolRoleMap: 755 | # Created to map Auth and Unauth roles to the identity pool 756 | # Depends on Identity Pool for ID ref 757 | Type: AWS::Cognito::IdentityPoolRoleAttachment 758 | Properties: 759 | IdentityPoolId: !Ref IdentityPool 760 | Roles: 761 | unauthenticated: !Ref unauthRoleArn 762 | authenticated: !Ref authRoleArn 763 | DependsOn: IdentityPool 764 | 765 | 766 | Outputs : 767 | 768 | IdentityPoolId: 769 | Value: !Ref 'IdentityPool' 770 | Description: Id for the identity pool 771 | IdentityPoolName: 772 | Value: !GetAtt IdentityPool.Name 773 | 774 | 775 | HostedUIDomain: 776 | Value: !If [ShouldNotCreateEnvResources, !Ref hostedUIDomainName, !Join ['-',[!Ref hostedUIDomainName, !Ref env]]] 777 | 778 | 779 | OAuthMetadata: 780 | Value: !Ref oAuthMetadata 781 | 782 | 783 | UserPoolId: 784 | Value: !Ref 'UserPool' 785 | Description: Id for the user pool 786 | UserPoolName: 787 | Value: !Ref userPoolName 788 | AppClientIDWeb: 789 | Value: !Ref 'UserPoolClientWeb' 790 | Description: The user pool app client id for web 791 | AppClientID: 792 | Value: !Ref 'UserPoolClient' 793 | Description: The user pool app client id 794 | AppClientSecret: 795 | Value: !GetAtt UserPoolClientInputs.appSecret 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | --------------------------------------------------------------------------------