├── 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 |
--------------------------------------------------------------------------------