├── packages
├── server
│ ├── .gitignore
│ ├── .env.example
│ ├── src
│ │ ├── modules
│ │ │ ├── store
│ │ │ │ ├── mutations
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── StoreCreate.ts
│ │ │ │ ├── StoreLoader.ts
│ │ │ │ ├── StoreModel.ts
│ │ │ │ └── StoreType.ts
│ │ │ ├── product
│ │ │ │ ├── mutations
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── ProductCreate.ts
│ │ │ │ ├── ProductLoader.ts
│ │ │ │ ├── ProductModel.ts
│ │ │ │ └── ProductType.ts
│ │ │ ├── userStore
│ │ │ │ ├── mutations
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── UserStoreCreate.ts
│ │ │ │ ├── UserStoreLoader.ts
│ │ │ │ ├── UserStoreModel.ts
│ │ │ │ └── UserStoreType.ts
│ │ │ ├── userPoints
│ │ │ │ ├── mutations
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── UserPointsCreateOrUpdate.ts
│ │ │ │ ├── UserPointsLoader.ts
│ │ │ │ ├── UserPointsModel.ts
│ │ │ │ └── UserPointsType.ts
│ │ │ ├── user
│ │ │ │ ├── mutations
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── UserLoginWithEmailMutation.ts
│ │ │ │ │ └── UserRegisterWithEmailMutation.ts
│ │ │ │ ├── UserLoader.ts
│ │ │ │ ├── UserModel.ts
│ │ │ │ └── UserType.ts
│ │ │ └── node
│ │ │ │ └── typeRegister.ts
│ │ ├── shared
│ │ │ ├── logger.config.ts
│ │ │ ├── server.config.ts
│ │ │ └── index.ts
│ │ ├── schema
│ │ │ ├── schema.ts
│ │ │ ├── MutationType.ts
│ │ │ └── QueryType.ts
│ │ ├── graphql
│ │ │ ├── errorField.ts
│ │ │ ├── successField.ts
│ │ │ ├── index.ts
│ │ │ ├── withFilter.ts
│ │ │ ├── withConnectionCursor.ts
│ │ │ ├── loaderRegister.ts
│ │ │ ├── createLoader.ts
│ │ │ └── connectionDefinitions.ts
│ │ ├── config.ts
│ │ ├── database
│ │ │ └── database.ts
│ │ ├── auth.ts
│ │ ├── index.ts
│ │ └── app.ts
│ ├── tsconfig.json
│ ├── scripts
│ │ ├── updateSchema.ts
│ │ └── updateSchema.js
│ ├── package.json
│ └── schema
│ │ └── schema.graphql
├── app
│ ├── .env.example
│ ├── src
│ │ ├── types
│ │ │ └── env.d.ts
│ │ ├── core
│ │ │ └── auth
│ │ │ │ ├── useAuth.tsx
│ │ │ │ ├── security.ts
│ │ │ │ ├── userType.ts
│ │ │ │ └── AuthContext.tsx
│ │ ├── components
│ │ │ ├── qrCode
│ │ │ │ ├── QrCodeMeQuery.tsx
│ │ │ │ ├── ScanQrCodeStoreQuery.tsx
│ │ │ │ ├── ScanQrCodeUserPointsCreateOrUpdateMutation.tsx
│ │ │ │ ├── QrCode.tsx
│ │ │ │ └── ScanQrCode.tsx
│ │ │ ├── settings
│ │ │ │ ├── SettingsMeQuery.tsx
│ │ │ │ └── Settings.tsx
│ │ │ ├── Home
│ │ │ │ ├── CreateStoreMutation.tsx
│ │ │ │ ├── NewProductMutation.tsx
│ │ │ │ ├── HomeStoreListQuery.tsx
│ │ │ │ ├── Store.tsx
│ │ │ │ ├── Home.tsx
│ │ │ │ ├── CreateStore.tsx
│ │ │ │ └── NewProduct.tsx
│ │ │ ├── auth
│ │ │ │ ├── SignInUserLoginWithEmailMutation.tsx
│ │ │ │ ├── SignUpUserRegisterWithEmailMutation.tsx
│ │ │ │ ├── SignIn.tsx
│ │ │ │ └── SignUp.tsx
│ │ │ ├── ui
│ │ │ │ ├── Alert.tsx
│ │ │ │ ├── TextInput.tsx
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── RadioButton.tsx
│ │ │ │ └── Product.tsx
│ │ │ └── storeDetails
│ │ │ │ ├── StoreDetailsQuery.tsx
│ │ │ │ ├── Header.tsx
│ │ │ │ └── StoreDetails.tsx
│ │ ├── relay
│ │ │ ├── Environment.ts
│ │ │ └── fetchQuery.ts
│ │ ├── Providers.tsx
│ │ └── routes.tsx
│ ├── assets
│ │ ├── icon.png
│ │ ├── favicon.png
│ │ ├── splash.png
│ │ └── adaptive-icon.png
│ ├── relay.config.js
│ ├── metro.config.js
│ ├── tsconfig.json
│ ├── .expo-shared
│ │ └── assets.json
│ ├── .gitignore
│ ├── babel.config.js
│ ├── App.tsx
│ ├── app.json
│ └── package.json
├── .DS_Store
└── website
│ ├── amora_emoji.png
│ ├── package.json
│ └── index.html
├── .gitignore
├── README.md
├── .prettierrc.json
├── package.json
└── .eslintrc.json
/packages/server/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | lib/
3 | dist/
--------------------------------------------------------------------------------
/packages/app/.env.example:
--------------------------------------------------------------------------------
1 | API_URL=http://localhost:9001
--------------------------------------------------------------------------------
/packages/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akinncar/amora/HEAD/packages/.DS_Store
--------------------------------------------------------------------------------
/packages/app/src/types/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@env' {
2 | export const API_URL: string;
3 | }
--------------------------------------------------------------------------------
/packages/app/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akinncar/amora/HEAD/packages/app/assets/icon.png
--------------------------------------------------------------------------------
/packages/app/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akinncar/amora/HEAD/packages/app/assets/favicon.png
--------------------------------------------------------------------------------
/packages/app/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akinncar/amora/HEAD/packages/app/assets/splash.png
--------------------------------------------------------------------------------
/packages/website/amora_emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akinncar/amora/HEAD/packages/website/amora_emoji.png
--------------------------------------------------------------------------------
/packages/app/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akinncar/amora/HEAD/packages/app/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/packages/server/.env.example:
--------------------------------------------------------------------------------
1 | PORT=9001
2 | NODE_ENV=development
3 |
4 | MONGO_URI=mongodb://localhost/rbaf
5 | JWT_SECRET=jwt_secret
--------------------------------------------------------------------------------
/packages/server/src/modules/store/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import StoreCreate from './StoreCreate';
2 |
3 | export default {
4 | StoreCreate,
5 | };
--------------------------------------------------------------------------------
/packages/server/src/modules/product/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import ProductCreate from './ProductCreate';
2 |
3 | export default {
4 | ProductCreate,
5 | };
--------------------------------------------------------------------------------
/packages/app/relay.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | src: './src',
3 | schema: '../server/schema/schema.graphql',
4 | language: 'typescript'
5 | };
6 |
--------------------------------------------------------------------------------
/packages/app/metro.config.js:
--------------------------------------------------------------------------------
1 | const { createMetroConfiguration } = require('expo-yarn-workspaces');
2 |
3 | module.exports = createMetroConfiguration(__dirname);
--------------------------------------------------------------------------------
/packages/server/src/modules/userStore/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import UserStoreCreate from './UserStoreCreate';
2 |
3 | export default {
4 | UserStoreCreate,
5 | };
--------------------------------------------------------------------------------
/packages/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "typeRoots": ["./src/types"],
4 | "compilerOptions": {
5 | "strict": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userPoints/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import UserPointsCreateOrUpdate from './UserPointsCreateOrUpdate';
2 |
3 | export default {
4 | UserPointsCreateOrUpdate,
5 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Monorepo
2 | node_modules/
3 | apps/*/credentials.json
4 | packages/*/build
5 | yarn-error.log
6 | .DS_Store
7 |
8 | # Expo
9 | .expo
10 | __generated__
11 | web-build
--------------------------------------------------------------------------------
/packages/app/src/core/auth/useAuth.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { AuthContext } from './AuthContext';
4 |
5 | export const useAuth = () => useContext(AuthContext);
6 |
--------------------------------------------------------------------------------
/packages/app/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/packages/app/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 | .env
13 |
14 | # macOS
15 | .DS_Store
16 |
--------------------------------------------------------------------------------
/packages/server/src/shared/logger.config.ts:
--------------------------------------------------------------------------------
1 | const LOG_ENV = {
2 | 'production': { level: 'info' },
3 | 'preproduction': { level: 'info' },
4 | 'development': { level: 'all' }
5 | };
6 |
7 | export default LOG_ENV;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :construction: 🫐 Amora
2 | Amora is a mobile app to help establishments to retain customers made with React Native.
3 |
4 |
5 | # Deploy web app
6 |
7 | https://docs.expo.dev/distribution/publishing-websites/#vercel
--------------------------------------------------------------------------------
/packages/app/src/components/qrCode/QrCodeMeQuery.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const QrCodeMeQuery = graphql`
4 | query QrCodeMeQuery {
5 | me {
6 | _id
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/packages/app/src/components/settings/SettingsMeQuery.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const SettingsMeQuery = graphql`
4 | query SettingsMeQuery {
5 | me {
6 | _id
7 | name
8 | email
9 | }
10 | }
11 | `;
12 |
--------------------------------------------------------------------------------
/packages/server/src/modules/user/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import UserLoginWithEmail from './UserLoginWithEmailMutation';
2 | import UserRegisterWithEmail from './UserRegisterWithEmailMutation';
3 |
4 | export default {
5 | UserLoginWithEmail,
6 | UserRegisterWithEmail,
7 | };
--------------------------------------------------------------------------------
/packages/server/src/schema/schema.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLSchema } from 'graphql'
2 |
3 | import QueryType from './QueryType'
4 | import MutationType from './MutationType'
5 |
6 | export const schema = new GraphQLSchema({
7 | query: QueryType,
8 | mutation: MutationType
9 | })
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "jsxSingleQuote": false,
7 | "bracketSpacing": true,
8 | "jsxBracketSameLine": false,
9 | "endOfLine": "auto",
10 | "arrowParens": "avoid"
11 | }
--------------------------------------------------------------------------------
/packages/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@amora/website",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "deploy": "gh-pages -d ./"
8 | },
9 | "dependencies": {
10 | "gh-pages": "^3.2.3"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/server/src/shared/server.config.ts:
--------------------------------------------------------------------------------
1 |
2 | const SERVER_ENV = {
3 | production: {
4 | SERVER_PORT: process.env.PORT || 9001,
5 | },
6 | preproduction: {
7 | SERVER_PORT: 9001,
8 | },
9 | development: {
10 | SERVER_PORT: 9001,
11 | },
12 | };
13 |
14 | export default SERVER_ENV;
--------------------------------------------------------------------------------
/packages/server/src/graphql/errorField.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLString } from 'graphql';
2 |
3 | export const errorField = {
4 | error: {
5 | type: GraphQLString,
6 | // TODO check if this resolver is actually needed. graphqljs defaults to something along these lines
7 | resolve: ({ error }: { readonly error: string }) => error,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/packages/server/src/shared/index.ts:
--------------------------------------------------------------------------------
1 | import SERVER_ENV from './server.config';
2 | import LOG_ENV from './logger.config';
3 |
4 | const environment = process.env.NODE_ENV || 'development';
5 |
6 | const serverConf = SERVER_ENV[environment];
7 | const logConf = LOG_ENV[environment];
8 |
9 | export {
10 | environment,
11 | serverConf,
12 | logConf
13 | };
--------------------------------------------------------------------------------
/packages/server/src/graphql/successField.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLString } from 'graphql';
2 |
3 | export const successField = {
4 | success: {
5 | type: GraphQLString,
6 | // TODO check if this resolver is actually needed. graphqljs defaults to something along these lines
7 | resolve: ({ success }: { readonly success: string }) => success,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/packages/app/src/components/qrCode/ScanQrCodeStoreQuery.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const ScanQrCodeStoreQuery = graphql`
4 | query ScanQrCodeStoreQuery {
5 | userStoreByUserId {
6 | edges {
7 | node {
8 | _id
9 | storeId
10 | userId
11 | }
12 | }
13 | }
14 | }
15 | `;
16 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/CreateStoreMutation.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const CreateStoreMutation = graphql`
4 | mutation CreateStoreMutation($input: StoreCreateInput!) {
5 | StoreCreate(input: $input) {
6 | store {
7 | name
8 | description
9 | pictureUrl
10 | }
11 | error
12 | }
13 | }
14 | `;
15 |
--------------------------------------------------------------------------------
/packages/server/src/graphql/index.ts:
--------------------------------------------------------------------------------
1 | export { errorField } from './errorField';
2 | export { successField } from './successField';
3 | export { createLoader } from './createLoader';
4 | export { withConnectionCursor } from './withConnectionCursor';
5 | export { registerLoader, getDataloaders } from './loaderRegister';
6 | export { connectionArgs, connectionDefinitions } from './connectionDefinitions';
7 |
8 |
9 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/NewProductMutation.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const NewProductMutation = graphql`
4 | mutation NewProductMutation($input: ProductCreateInput!) {
5 | ProductCreate(input: $input) {
6 | product {
7 | name
8 | description
9 | pictureUrl
10 | points
11 | storeId
12 | }
13 | error
14 | }
15 | }
16 | `;
17 |
--------------------------------------------------------------------------------
/packages/server/src/graphql/withFilter.ts:
--------------------------------------------------------------------------------
1 | import { ConnectionArguments } from 'graphql-relay';
2 |
3 | export type ArgsWithFilter = ConnectionArguments & {
4 | filters: { [key: string]: string };
5 | [key: string]: any;
6 | };
7 |
8 | export const withFilter = (args: ArgsWithFilter | { [key: string]: any }, filters: object) => ({
9 | ...args,
10 | filters: {
11 | ...(args.filters || {}),
12 | ...filters,
13 | },
14 | });
--------------------------------------------------------------------------------
/packages/app/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [
6 | 'relay',
7 | ["module:react-native-dotenv", {
8 | "moduleName": "@env",
9 | "path": ".env",
10 | "blacklist": null,
11 | "whitelist": null,
12 | "safe": false,
13 | "allowUndefined": true
14 | }],
15 | ],
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/packages/app/src/core/auth/security.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage';
2 |
3 | const JWT_TOKEN_KEY = '@user/TOKEN';
4 |
5 | export const getAuthToken = () => AsyncStorage.getItem(JWT_TOKEN_KEY);
6 |
7 | export const updateAuthToken = (token?: any) => {
8 | if (!token) {
9 | return AsyncStorage.removeItem(JWT_TOKEN_KEY);
10 | } else {
11 | return AsyncStorage.setItem(JWT_TOKEN_KEY, token);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/packages/server/src/config.ts:
--------------------------------------------------------------------------------
1 | import dotenvSafe from 'dotenv-safe';
2 | import path from 'path';
3 |
4 | const cwd = process.cwd();
5 |
6 | const root = path.join.bind(cwd);
7 |
8 | dotenvSafe.config({
9 | path: root('.env'),
10 | sample: root('.env.example'),
11 | });
12 |
13 | export const config = {
14 | PORT: process.env.PORT || 9001,
15 | NODE_ENV: process.env.NODE_ENV,
16 | MONGO_URI: process.env.MONGO_URI,
17 | JWT_SECRET: process.env.JWT_SECRET,
18 | };
19 |
--------------------------------------------------------------------------------
/packages/app/src/core/auth/userType.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage';
2 |
3 | const USER_TYPE_KEY = '@user/TYPE';
4 |
5 | export const getUserType = () => AsyncStorage.getItem(USER_TYPE_KEY);
6 |
7 | export const updateUserType = (userType?: 'customer' | 'provider') => {
8 | if (!userType) {
9 | return AsyncStorage.removeItem(USER_TYPE_KEY);
10 | } else {
11 | return AsyncStorage.setItem(USER_TYPE_KEY, userType);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/packages/app/src/components/auth/SignInUserLoginWithEmailMutation.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const SignInUserLoginWithEmailMutation = graphql`
4 | mutation SignInUserLoginWithEmailMutation($input: UserLoginWithEmailInput!) {
5 | UserLoginWithEmail(input: $input) {
6 | token
7 | error
8 | success
9 | me {
10 | _id
11 | name
12 | username
13 | email
14 | type
15 | }
16 | }
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/packages/app/src/components/auth/SignUpUserRegisterWithEmailMutation.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const SignUpUserRegisterWithEmailMutation = graphql`
4 | mutation SignUpUserRegisterWithEmailMutation(
5 | $input: UserRegisterWithEmailInput!
6 | ) {
7 | UserRegisterWithEmail(input: $input) {
8 | token
9 | error
10 | success
11 | me {
12 | id
13 | name
14 | username
15 | email
16 | type
17 | }
18 | }
19 | }
20 | `;
21 |
--------------------------------------------------------------------------------
/packages/server/src/modules/user/UserLoader.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { createLoader } from '../../graphql/createLoader';
3 |
4 | import { registerLoader } from '../../graphql/loaderRegister';
5 |
6 | import UserModel from './UserModel';
7 |
8 | const { Wrapper: User, getLoader, clearCache, load, loadAll } = createLoader({
9 | model: UserModel,
10 | loaderName: 'UserLoader',
11 | });
12 |
13 | export { getLoader, clearCache, load, loadAll };
14 | export default User;
15 |
16 | registerLoader('UserLoader', getLoader);
17 |
--------------------------------------------------------------------------------
/packages/app/src/components/qrCode/ScanQrCodeUserPointsCreateOrUpdateMutation.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const ScanQrCodeUserPointsCreateOrUpdateMutation = graphql`
4 | mutation ScanQrCodeUserPointsCreateOrUpdateMutation(
5 | $input: UserPointsCreateOrUpdateInput!
6 | ) {
7 | UserPointsCreateOrUpdate(input: $input) {
8 | userPoints {
9 | _id
10 | points
11 | storeId
12 | userId
13 | }
14 | success
15 | error
16 | }
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/packages/app/src/relay/Environment.ts:
--------------------------------------------------------------------------------
1 | import { Environment, Network, QueryResponseCache, RecordSource, Store } from 'relay-runtime';
2 |
3 | import { fetchQuery } from './fetchQuery'
4 |
5 | const __DEV__ = process.env.NODE_ENV === 'development';
6 |
7 | const oneMinute = 60 * 1000;
8 | const network = Network.create(fetchQuery);
9 |
10 | const environment = new Environment({
11 | network,
12 | store: new Store(new RecordSource(), {
13 | gcReleaseBufferSize: 10,
14 | }),
15 | });
16 |
17 | export default environment;
18 |
--------------------------------------------------------------------------------
/packages/server/src/modules/store/StoreLoader.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { createLoader } from '../../graphql/createLoader';
3 |
4 | import { registerLoader } from '../../graphql/loaderRegister';
5 |
6 | import StoreModel from './StoreModel';
7 |
8 | const { Wrapper: Store, getLoader, clearCache, load, loadAll } = createLoader({
9 | model: StoreModel,
10 | loaderName: 'StoreLoader',
11 | });
12 |
13 | export { getLoader, clearCache, load, loadAll };
14 | export default Store;
15 |
16 | registerLoader('StoreLoader', getLoader);
17 |
--------------------------------------------------------------------------------
/packages/server/src/modules/product/ProductLoader.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { createLoader } from '../../graphql/createLoader';
3 |
4 | import { registerLoader } from '../../graphql/loaderRegister';
5 |
6 | import ProductModel from './ProductModel';
7 |
8 | const { Wrapper: Product, getLoader, clearCache, load, loadAll } = createLoader({
9 | model: ProductModel,
10 | loaderName: 'ProductLoader',
11 | });
12 |
13 | export { getLoader, clearCache, load, loadAll };
14 | export default Product;
15 |
16 | registerLoader('ProductLoader', getLoader);
17 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userStore/UserStoreLoader.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { createLoader } from '../../graphql/createLoader';
3 |
4 | import { registerLoader } from '../../graphql/loaderRegister';
5 |
6 | import UserStoreModel from './UserStoreModel';
7 |
8 | const { Wrapper: UserStore, getLoader, clearCache, load, loadAll } = createLoader({
9 | model: UserStoreModel,
10 | loaderName: 'UserStoreLoader',
11 | });
12 |
13 | export { getLoader, clearCache, load, loadAll };
14 | export default UserStore;
15 |
16 | registerLoader('UserStoreLoader', getLoader);
17 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userPoints/UserPointsLoader.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { createLoader } from '../../graphql/createLoader';
3 |
4 | import { registerLoader } from '../../graphql/loaderRegister';
5 |
6 | import UserPointsModel from './UserPointsModel';
7 |
8 | const { Wrapper: UserPoints, getLoader, clearCache, load, loadAll } = createLoader({
9 | model: UserPointsModel,
10 | loaderName: 'UserPointsLoader',
11 | });
12 |
13 | export { getLoader, clearCache, load, loadAll };
14 | export default UserPoints;
15 |
16 | registerLoader('UserPointsLoader', getLoader);
17 |
--------------------------------------------------------------------------------
/packages/app/src/Providers.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RelayEnvironmentProvider } from 'react-relay';
3 |
4 | import Environment from './relay/Environment';
5 | import { AuthProvider } from './core/auth/AuthContext';
6 |
7 | type Props = {
8 | readonly children: React.ReactNode;
9 | readonly environment: typeof Environment;
10 | };
11 | const Providers = ({ children, environment = Environment }: Props) => {
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
19 | export default Providers;
20 |
--------------------------------------------------------------------------------
/packages/server/src/database/database.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import mongoose from 'mongoose';
3 |
4 | // import { config } from "../config";
5 |
6 | const initDB = () => {
7 | mongoose.connect(process.env.MONGO_URI, {
8 | useNewUrlParser: true,
9 | useUnifiedTopology: true,
10 | });
11 |
12 | mongoose.connection
13 | .on('error', (error) => console.log(error))
14 | .once('open', () => {
15 | // const info = mongoose.connections[0];
16 | // console.log(`Connected to ${info.host}:${info.port}/${info.name}`)
17 | console.log('Connected to Database');
18 | });
19 | };
20 |
21 | export default initDB;
22 |
--------------------------------------------------------------------------------
/packages/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "outDir": "./dist",
7 | "rootDir": "./src",
8 | "lib": [ /* Specify library files to be included in the compilation. */
9 | "esnext",
10 | "dom",
11 | "dom.iterable"
12 | ],
13 | "strict": true,
14 | "resolveJsonModule": true,
15 | "esModuleInterop": true,
16 | "skipLibCheck": true,
17 | "checkJs": false,
18 | "noImplicitAny": false
19 | },
20 | "include": ["src/**/*"],
21 | "exclude": ["node_modules"]
22 | }
--------------------------------------------------------------------------------
/packages/app/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { ActivityIndicator, StatusBar } from 'react-native';
3 |
4 | import AsyncStorage from '@react-native-async-storage/async-storage';
5 |
6 | import Providers from './src/Providers';
7 | import Environment from './src/relay/Environment';
8 | import { Routes } from './src/routes';
9 |
10 | // AsyncStorage.clear();
11 |
12 | export default function App() {
13 | return (
14 |
15 | }>
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/packages/app/src/components/ui/Alert.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Platform } from 'react-native';
2 |
3 | const alertPolyfill = (title, description, options, extra) => {
4 | const result = window.confirm(
5 | [title, description].filter(Boolean).join('\n')
6 | );
7 |
8 | if (result) {
9 | const confirmOption = options.find(({ style }) => style !== 'cancel');
10 | confirmOption && confirmOption.onPress();
11 | } else {
12 | const cancelOption = options.find(({ style }) => style === 'cancel');
13 | cancelOption && cancelOption.onPress();
14 | }
15 | };
16 |
17 | const alert = Platform.OS === 'web' ? alertPolyfill : Alert.alert;
18 |
19 | export default { alert };
20 |
--------------------------------------------------------------------------------
/packages/server/src/schema/MutationType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType } from 'graphql';
2 |
3 | import UserMutations from '../modules/user/mutations';
4 | import StoreMutations from '../modules/store/mutations';
5 | import ProductMutations from '../modules/product/mutations';
6 | import UserPointsMutations from '../modules/userPoints/mutations';
7 | import UserStoreMutations from '../modules/userStore/mutations';
8 |
9 | export default new GraphQLObjectType({
10 | name: 'Mutation',
11 | description: 'Root of ... mutations',
12 | fields: () => ({
13 | ...UserMutations,
14 | ...StoreMutations,
15 | ...ProductMutations,
16 | ...UserPointsMutations,
17 | ...UserStoreMutations,
18 | }),
19 | });
--------------------------------------------------------------------------------
/packages/server/scripts/updateSchema.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { promisify } from 'util';
4 |
5 | import { printSchema } from 'graphql/utilities';
6 |
7 | import { schema as schemaGraphql } from '../src/schema/schema';
8 |
9 | const writeFileAsync = promisify(fs.writeFile);
10 |
11 | const cwd = process.cwd();
12 |
13 | (async () => {
14 | const configs = [
15 | {
16 | schema: schemaGraphql,
17 | path: path.join(cwd, './schema/schema.graphql'),
18 | },
19 | ];
20 |
21 | await Promise.all([
22 | ...configs.map(async config => {
23 | await writeFileAsync(config.path, printSchema(config.schema));
24 | }),
25 | ]);
26 |
27 | process.exit(0);
28 | })();
29 |
--------------------------------------------------------------------------------
/packages/server/src/auth.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import jwt from 'jsonwebtoken';
3 |
4 | // import { User } from './model';
5 | import { config } from './config';
6 | import User, { IUser } from './modules/user/UserModel';
7 |
8 | export const getUser = async (token: string | null | undefined) => {
9 | if (!token) return { user: null };
10 |
11 | try {
12 | const decodedToken = jwt.verify(token.substring(4), config.JWT_SECRET);
13 |
14 | const user = await User.findOne({ _id: (decodedToken as { readonly id: string }).id });
15 |
16 | return {
17 | user,
18 | };
19 | } catch (err) {
20 | return { user: null };
21 | }
22 | };
23 |
24 | export const generateToken = (user: IUser) => {
25 | return `JWT ${jwt.sign({ id: user._id }, config.JWT_SECRET)}`;
26 | };
--------------------------------------------------------------------------------
/packages/app/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "amora",
4 | "slug": "amora",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "splash": {
9 | "image": "./assets/splash.png",
10 | "resizeMode": "contain",
11 | "backgroundColor": "#ffffff"
12 | },
13 | "updates": {
14 | "fallbackToCacheTimeout": 0
15 | },
16 | "assetBundlePatterns": [
17 | "**/*"
18 | ],
19 | "ios": {
20 | "supportsTablet": true
21 | },
22 | "android": {
23 | "adaptiveIcon": {
24 | "foregroundImage": "./assets/adaptive-icon.png",
25 | "backgroundColor": "#FFFFFF"
26 | }
27 | },
28 | "web": {
29 | "favicon": "./assets/favicon.png"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/app/src/components/ui/TextInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | TextInput as RNTextInput,
4 | TextInputProps as RNTextInputProps,
5 | StyleProp,
6 | Text,
7 | View,
8 | ViewStyle,
9 | } from 'react-native';
10 |
11 | type TextInputProps = {
12 | readonly label?: string;
13 | readonly style?: StyleProp;
14 | } & RNTextInputProps;
15 |
16 | export function TextInput({ label, style, ...props }: TextInputProps) {
17 | return (
18 |
19 | {label && {label}}
20 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/HomeStoreListQuery.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const HomeStoreListQuery = graphql`
4 | query HomeStoreListQuery {
5 | stores {
6 | edges {
7 | node {
8 | _id
9 | name
10 | description
11 | pictureUrl
12 | }
13 | }
14 | }
15 | userStoreByUserId {
16 | edges {
17 | node {
18 | _id
19 | storeId
20 | userId
21 | store {
22 | _id
23 | name
24 | products {
25 | edges {
26 | node {
27 | _id
28 | name
29 | pictureUrl
30 | points
31 | }
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 | `;
40 |
--------------------------------------------------------------------------------
/packages/app/src/components/storeDetails/StoreDetailsQuery.tsx:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-relay';
2 |
3 | export const StoreDetailsQuery = graphql`
4 | query StoreDetailsQuery($storeId: ID!) {
5 | me {
6 | _id
7 | email
8 | }
9 | store: storeByStoreId(id: $storeId) {
10 | _id
11 | name
12 | description
13 | pictureUrl
14 | }
15 | products: productsByStoreId(storeId: $storeId) {
16 | edges {
17 | node {
18 | _id
19 | name
20 | description
21 | pictureUrl
22 | points
23 | storeId
24 | }
25 | }
26 | }
27 | userPoints: userPointsByStoreIdAndUserId(storeId: $storeId) {
28 | edges {
29 | node {
30 | _id
31 | points
32 | storeId
33 | userId
34 | }
35 | }
36 | }
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/packages/server/src/graphql/withConnectionCursor.ts:
--------------------------------------------------------------------------------
1 | import { connectionFromMongoCursor } from '@entria/graphql-mongoose-loader';
2 | import { Model } from 'mongoose';
3 |
4 | import { DataLoaderKey } from './createLoader';
5 |
6 | export type LoaderFn = (ctx: Context, id: DataLoaderKey) => any;
7 |
8 | export const withConnectionCursor =
9 | (
10 | model: Model,
11 | loader: LoaderFn,
12 | condFn: (...p: readonly any[]) => { readonly conditions?: object; readonly sort?: object },
13 | ) =>
14 | (...params: readonly any[]) => {
15 | const { conditions = {}, sort = {} } = condFn(...params);
16 |
17 | const [context, args] = params;
18 |
19 | const cursor = model.find(conditions).sort(sort);
20 |
21 | return connectionFromMongoCursor({
22 | cursor,
23 | context,
24 | args,
25 | loader: loader as any,
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userStore/UserStoreModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Document, Model } from 'mongoose';
2 |
3 | const { ObjectId } = mongoose.Schema.Types;
4 | const Schema = mongoose.Schema;
5 |
6 | const UserStoreSchema = new Schema(
7 | {
8 | storeId: {
9 | type: ObjectId,
10 | ref: 'Store',
11 | required: false,
12 | },
13 | userId: {
14 | type: ObjectId,
15 | ref: 'User',
16 | required: false,
17 | },
18 | },
19 | {
20 | collection: 'UserStore',
21 | timestamps: {
22 | createdAt: 'createdAt',
23 | updatedAt: 'updatedAt',
24 | },
25 | }
26 | );
27 |
28 | export interface IUserStore extends Document {
29 | readonly storeId: string;
30 | readonly userId: string;
31 | readonly createdAt: Date;
32 | readonly updatedAt: Date;
33 | }
34 |
35 | const UserStoreModel: Model = mongoose.model('UserStore', UserStoreSchema);
36 |
37 | export default UserStoreModel;
38 |
--------------------------------------------------------------------------------
/packages/app/src/components/qrCode/QrCode.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | import { useLazyLoadQuery } from 'react-relay';
5 | import QRCode from 'react-native-qrcode-svg';
6 | import { useRoute } from '@react-navigation/native';
7 |
8 | import { QrCodeMeQuery } from './QrCodeMeQuery';
9 | import type { QrCodeMeQuery as QrCodeMeQueryType } from './__generated__/QrCodeMeQuery.graphql';
10 |
11 | export function QrCode() {
12 | const { params = { points: 0 } } = useRoute();
13 | const { points } = params;
14 |
15 | const data = useLazyLoadQuery(
16 | QrCodeMeQuery,
17 | {},
18 | undefined
19 | );
20 |
21 | const code = {
22 | userId: data.me?._id,
23 | points: points || 0,
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/server/src/graphql/loaderRegister.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | export interface DataLoaders {
3 | // UserLoader: ReturnType<
4 | // typeof import("../server/modules/user/UserLoader").getLoader
5 | // >;
6 | // TeamLoader: ReturnType<
7 | // typeof import("../server/modules/team/TeamLoader").getLoader
8 | // >;
9 | }
10 |
11 | const loaders: { readonly
12 | [Name in keyof DataLoaders]: () => DataLoaders[Name];
13 | } = {} as any;
14 |
15 | const registerLoader = (
16 | key: Name,
17 | getLoader: () => DataLoaders[Name]
18 | ) => {
19 | loaders[key] = getLoader as any;
20 | };
21 |
22 | const getDataloaders = (): DataLoaders =>
23 | (Object.keys(loaders) as readonly (keyof DataLoaders)[]).reduce(
24 | (prev, loaderKey) => ({
25 | ...prev,
26 | [loaderKey]: loaders[loaderKey](),
27 | }),
28 | {}
29 | ) as any;
30 |
31 | export { registerLoader, getDataloaders };
--------------------------------------------------------------------------------
/packages/website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Amora APP
11 |
12 |
13 |
14 |

15 |
Amora App
16 |
Coming Soon...
17 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/server/scripts/updateSchema.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const fs_1 = __importDefault(require("fs"));
7 | const path_1 = __importDefault(require("path"));
8 | const util_1 = require("util");
9 | const utilities_1 = require("graphql/utilities");
10 | const schema_1 = require("../src/schema/schema");
11 | const writeFileAsync = (0, util_1.promisify)(fs_1.default.writeFile);
12 | const cwd = process.cwd();
13 | (async () => {
14 | const configs = [
15 | {
16 | schema: schema_1.schema,
17 | path: path_1.default.join(cwd, './schema/schema.graphql'),
18 | },
19 | ];
20 | await Promise.all([
21 | ...configs.map(async (config) => {
22 | await writeFileAsync(config.path, (0, utilities_1.printSchema)(config.schema));
23 | }),
24 | ]);
25 | process.exit(0);
26 | })();
27 |
--------------------------------------------------------------------------------
/packages/server/src/modules/store/StoreModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Document, Model, Types } from 'mongoose';
2 | import bcrypt from 'bcrypt';
3 |
4 | const { ObjectId } = mongoose.Schema.Types;
5 | const Schema = mongoose.Schema;
6 |
7 | const StoreSchema = new Schema(
8 | {
9 | name: {
10 | type: String,
11 | required: 'name is required',
12 | },
13 | description: {
14 | type: String,
15 | required: false,
16 | },
17 | pictureUrl: {
18 | type: String,
19 | required: false,
20 | },
21 | },
22 | {
23 | collection: 'Store',
24 | timestamps: {
25 | createdAt: 'createdAt',
26 | updatedAt: 'updatedAt',
27 | },
28 | }
29 | );
30 |
31 | export interface IStore extends Document {
32 | readonly name: string;
33 | readonly description: string;
34 | readonly pictureUrl: string;
35 | readonly createdAt: Date;
36 | readonly updatedAt: Date;
37 | }
38 |
39 | const StoreModel: Model = mongoose.model('Store', StoreSchema);
40 |
41 | export default StoreModel;
42 |
--------------------------------------------------------------------------------
/packages/app/src/relay/fetchQuery.ts:
--------------------------------------------------------------------------------
1 | import Constants from 'expo-constants';
2 | const { manifest } = Constants;
3 |
4 | import { API_URL } from '@env'
5 |
6 | import { getAuthToken } from '../core/auth/security'
7 |
8 | // Define a function that fetches the results of an operation (query/mutation/etc)
9 | // and returns its results as a Promise:
10 | export async function fetchQuery(
11 | operation: any,
12 | variables: any,
13 | cacheConfig: any,
14 | uploadables: any,
15 | ) {
16 | const authorization = await getAuthToken();
17 |
18 | const headers = {
19 | 'content-type': 'application/json',
20 | authorization,
21 | };
22 |
23 | // const uri = manifest?.debuggerHost ? `http://${manifest.debuggerHost.split(':').shift().concat(':9001')}` : API_URL;
24 | const uri = API_URL;
25 |
26 | const response = await fetch(`${uri}/graphql`, {
27 | method: 'POST',
28 | headers,
29 | body: JSON.stringify({
30 | query: operation.text, // GraphQL text from input
31 | variables,
32 | }),
33 | })
34 |
35 | return response.json()
36 | }
37 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userPoints/UserPointsModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Document, Model } from 'mongoose';
2 |
3 | const { ObjectId } = mongoose.Schema.Types;
4 | const Schema = mongoose.Schema;
5 |
6 | const UserPointsSchema = new Schema(
7 | {
8 | points: {
9 | type: Number,
10 | required: false,
11 | },
12 | storeId: {
13 | type: ObjectId,
14 | ref: 'Store',
15 | required: false,
16 | },
17 | userId: {
18 | type: ObjectId,
19 | ref: 'User',
20 | required: false,
21 | },
22 | },
23 | {
24 | collection: 'UserPoints',
25 | timestamps: {
26 | createdAt: 'createdAt',
27 | updatedAt: 'updatedAt',
28 | },
29 | }
30 | );
31 |
32 | export interface IUserPoints extends Document {
33 | readonly points: number;
34 | readonly storeId: string;
35 | readonly userId: string;
36 | readonly createdAt: Date;
37 | readonly updatedAt: Date;
38 | }
39 |
40 | const UserPointsModel: Model = mongoose.model('UserPoints', UserPointsSchema);
41 |
42 | export default UserPointsModel;
43 |
--------------------------------------------------------------------------------
/packages/app/src/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ActivityIndicator,
4 | GestureResponderEvent,
5 | StyleProp,
6 | Text,
7 | TouchableOpacity,
8 | ViewStyle,
9 | } from 'react-native';
10 |
11 | type ButtonProps = {
12 | readonly title: string;
13 | readonly onPress?: (event: GestureResponderEvent) => void;
14 | readonly style?: StyleProp;
15 | readonly isLoading?: boolean;
16 | readonly disabled?: boolean;
17 | };
18 |
19 | export function Button({
20 | title,
21 | onPress,
22 | style,
23 | isLoading = false,
24 | disabled = false,
25 | }: ButtonProps) {
26 | return (
27 |
40 | {isLoading ? (
41 |
42 | ) : (
43 | {title}
44 | )}
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/packages/server/src/modules/product/ProductModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Document, Model } from 'mongoose';
2 |
3 | const { ObjectId } = mongoose.Schema.Types;
4 | const Schema = mongoose.Schema;
5 |
6 | const ProductSchema = new Schema(
7 | {
8 | name: {
9 | type: String,
10 | required: 'name is required',
11 | },
12 | description: {
13 | type: String,
14 | required: false,
15 | },
16 | pictureUrl: {
17 | type: String,
18 | required: false,
19 | },
20 | points: {
21 | type: Number,
22 | required: false,
23 | },
24 | storeId: {
25 | type: ObjectId,
26 | ref: 'Store',
27 | required: false,
28 | },
29 | },
30 | {
31 | collection: 'Product',
32 | timestamps: {
33 | createdAt: 'createdAt',
34 | updatedAt: 'updatedAt',
35 | },
36 | }
37 | );
38 |
39 | export interface IProduct extends Document {
40 | readonly name: string;
41 | readonly description: string;
42 | readonly pictureUrl: string;
43 | readonly points: number;
44 | readonly storeId: string;
45 | readonly createdAt: Date;
46 | readonly updatedAt: Date;
47 | }
48 |
49 | const ProductModel: Model = mongoose.model('Product', ProductSchema);
50 |
51 | export default ProductModel;
52 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userStore/mutations/UserStoreCreate.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLString } from 'graphql';
2 | import { mutationWithClientMutationId } from 'graphql-relay';
3 |
4 | import { errorField, successField } from '../../../graphql';
5 |
6 | import * as UserStoreLoader from '../UserStoreLoader';
7 | import UserStore from '../UserStoreModel';
8 | import UserStoreType from '../UserStoreType';
9 |
10 | export default mutationWithClientMutationId({
11 | name: 'UserStoreCreate',
12 | inputFields: {
13 | storeId: {
14 | type: new GraphQLNonNull(GraphQLID),
15 | },
16 | userId: {
17 | type: new GraphQLNonNull(GraphQLID),
18 | },
19 | },
20 | mutateAndGetPayload: async ({ id, storeId, userId }) => {
21 | const userStore = await new UserStore({
22 | id,
23 | storeId,
24 | userId
25 | }).save();
26 |
27 | return {
28 | id: userStore.id,
29 | success: 'User points register success',
30 | };
31 | },
32 | outputFields: {
33 | userStore: {
34 | type: UserStoreType,
35 | resolve: async ({ id }, _, context) => {
36 | return await UserStoreLoader.load(context, id);
37 | },
38 | },
39 | ...errorField,
40 | ...successField,
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/Store.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image, Text, View } from 'react-native';
3 | import { useNavigation } from '@react-navigation/native';
4 |
5 | import { Button } from '../ui/Button';
6 |
7 | export function Store({ store }) {
8 | const { _id, pictureUrl, name, description } = store;
9 |
10 | const { navigate } = useNavigation();
11 |
12 | return (
13 |
25 |
26 |
30 |
31 | {name}
32 | {description}
33 |
34 |
35 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/packages/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import { createServer } from 'http';
3 | // import log4js from "./shared/logger";
4 |
5 | import app from './app';
6 | import initDB from './database/database';
7 | import { environment, serverConf } from './shared/index';
8 |
9 | // var logger = log4js.getLogger();
10 |
11 | (async () => {
12 | console.log( 'start')
13 | dotenv.config();
14 | // starting db
15 | try {
16 | await initDB();
17 | } catch (error) {
18 | console.error('Unable to connect to database');
19 | process.exit(1);
20 | }
21 |
22 | const server = createServer(app.callback());
23 |
24 | console.log('process.env.PORT', process.env.PORT || 9001);
25 |
26 | server.listen(process.env.PORT || 9001, () => {
27 | console.log('##########################################################');
28 | console.log('##### STARTING SERVER #####');
29 | console.log('##########################################################\n');
30 | console.log(
31 | `App running on ${environment.toUpperCase()} mode and listening on port ${
32 | serverConf.SERVER_PORT
33 | } ...`
34 | );
35 | console.log(
36 | `GraphQL Server is now running on http://localhost:${process.env.PORT || 9001}/graphql`
37 | );
38 | });
39 | })();
--------------------------------------------------------------------------------
/packages/server/src/modules/userPoints/UserPointsType.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { GraphQLID, GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql';
3 | import { globalIdField } from 'graphql-relay';
4 | import { objectIdResolver } from '@entria/graphql-mongo-helpers';
5 |
6 | import { nodeInterface, registerTypeLoader } from '../node/typeRegister';
7 |
8 | import { load } from './UserPointsLoader';
9 | import { connectionDefinitions } from '../../graphql/connectionDefinitions';
10 |
11 | const UserPointsType = new GraphQLObjectType({
12 | name: 'UserPoints',
13 | description: 'UserPoints data',
14 | fields: () => ({
15 | id: globalIdField('UserPoints'),
16 | ...objectIdResolver,
17 | points: {
18 | type: GraphQLInt,
19 | resolve: (userPoints) => userPoints.points,
20 | },
21 | storeId: {
22 | type: GraphQLID,
23 | resolve: (userPoints) => userPoints.storeId,
24 | },
25 | userId: {
26 | type: GraphQLID,
27 | resolve: (userPoints) => userPoints.userId,
28 | },
29 | }),
30 | interfaces: () => [nodeInterface],
31 | });
32 |
33 | registerTypeLoader(UserPointsType, load);
34 |
35 | export default UserPointsType;
36 |
37 | export const UserPointsConnection = connectionDefinitions({
38 | name: 'UserPoints',
39 | nodeType: UserPointsType,
40 | });
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "amora",
3 | "private": true,
4 | "workspaces": {
5 | "packages": [
6 | "packages/*"
7 | ]
8 | },
9 | "scripts": {
10 | "server": "yarn workspace @amora/server start",
11 | "build:server": "yarn workspace @amora/server build",
12 | "app:ios": "yarn workspace @amora/app ios",
13 | "app:android": "yarn workspace @amora/app android",
14 | "app:web": "yarn workspace @amora/app web",
15 | "env:example": "yarn workspace @amora/app env:example && yarn workspace @amora/server env:example",
16 | "lint": "yarn eslint --fix"
17 | },
18 | "version": "1.0.0",
19 | "description": "Amora Monorepo",
20 | "main": "index.js",
21 | "repository": "https://github.com/akinncar/amora",
22 | "author": "Akinn Rosa ",
23 | "license": "MIT",
24 | "devDependencies": {
25 | "@typescript-eslint/eslint-plugin": "^5.10.1",
26 | "@typescript-eslint/parser": "^5.10.1",
27 | "eslint": "^8.8.0",
28 | "eslint-config-prettier": "^8.3.0",
29 | "eslint-plugin-eslint-comments": "^3.2.0",
30 | "eslint-plugin-functional": "^4.1.1",
31 | "eslint-plugin-import": "^2.25.4",
32 | "eslint-plugin-prettier": "^4.0.0",
33 | "eslint-plugin-react": "^7.28.0",
34 | "eslint-plugin-react-native": "^4.0.0",
35 | "prettier": "^2.5.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userStore/UserStoreType.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { GraphQLID, GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql';
3 | import { globalIdField } from 'graphql-relay';
4 | import { objectIdResolver } from '@entria/graphql-mongo-helpers';
5 |
6 | import { nodeInterface, registerTypeLoader } from '../node/typeRegister';
7 |
8 | import { load } from './UserStoreLoader';
9 | import { connectionDefinitions } from '../../graphql/connectionDefinitions';
10 |
11 | import StoreType from '../../modules/store/StoreType';
12 | import Store from '../store/StoreModel';
13 |
14 | const UserStoreType = new GraphQLObjectType({
15 | name: 'UserStore',
16 | description: 'UserStore data',
17 | fields: () => ({
18 | id: globalIdField('UserStore'),
19 | ...objectIdResolver,
20 | storeId: {
21 | type: GraphQLID,
22 | resolve: (userStore) => userStore.storeId,
23 | },
24 | userId: {
25 | type: GraphQLID,
26 | resolve: (userStore) => userStore.userId,
27 | },
28 | store: {
29 | type: StoreType,
30 | resolve: async (userStore) => {
31 | return await Store.findOne({
32 | _id: userStore.storeId,
33 | });
34 | },
35 | },
36 | }),
37 | interfaces: () => [nodeInterface],
38 | });
39 |
40 | registerTypeLoader(UserStoreType, load);
41 |
42 | export default UserStoreType;
43 |
44 | export const UserStoreConnection = connectionDefinitions({
45 | name: 'UserStore',
46 | nodeType: UserStoreType,
47 | });
48 |
--------------------------------------------------------------------------------
/packages/app/src/components/ui/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | GestureResponderEvent,
4 | StyleProp,
5 | Text,
6 | TouchableOpacity,
7 | View,
8 | ViewStyle,
9 | } from 'react-native';
10 |
11 | type RadioButtonProps = {
12 | readonly checked: boolean;
13 | readonly label: string;
14 | readonly onPress?: (event: GestureResponderEvent) => void;
15 | readonly style?: StyleProp;
16 | };
17 |
18 | export function RadioButton({
19 | checked,
20 | label,
21 | onPress,
22 | style,
23 | }: RadioButtonProps) {
24 | return (
25 |
35 |
36 |
47 |
48 |
56 | {label}
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/packages/app/src/components/storeDetails/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image, Text, View } from 'react-native';
3 | import { useNavigation, useRoute } from '@react-navigation/native';
4 |
5 | import { graphql, useLazyLoadQuery } from 'react-relay';
6 |
7 | import { useAuth } from '../../core/auth/useAuth';
8 |
9 | export function Header({ store, userPoints }) {
10 | const { token } = useAuth();
11 |
12 | return (
13 |
25 |
26 |
30 |
31 | {store?.name}
32 | {store?.description}
33 |
34 |
35 | {token && (
36 |
37 | Você tem
38 |
39 | {userPoints?.points || 0} pontos
40 |
41 |
42 | )}
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/packages/server/src/modules/product/ProductType.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { GraphQLID, GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql';
3 | import { globalIdField } from 'graphql-relay';
4 | import { objectIdResolver } from '@entria/graphql-mongo-helpers';
5 |
6 | // import TeamType from "../team/TeamType";
7 | // import Team from "../team/TeamModel";
8 | import { nodeInterface, registerTypeLoader } from '../node/typeRegister';
9 |
10 | import { load } from './ProductLoader';
11 | import { connectionDefinitions } from '../../graphql/connectionDefinitions';
12 |
13 | const ProductType = new GraphQLObjectType({
14 | name: 'Product',
15 | description: 'Product data',
16 | fields: () => ({
17 | id: globalIdField('Product'),
18 | ...objectIdResolver,
19 | name: {
20 | type: GraphQLString,
21 | resolve: (product) => product.name,
22 | },
23 | description: {
24 | type: GraphQLString,
25 | resolve: (product) => product.description,
26 | },
27 | pictureUrl: {
28 | type: GraphQLString,
29 | resolve: (product) => product.pictureUrl,
30 | },
31 | points: {
32 | type: GraphQLInt,
33 | resolve: (product) => product.points,
34 | },
35 | storeId: {
36 | type: GraphQLID,
37 | resolve: (product) => product.storeId,
38 | },
39 | }),
40 | interfaces: () => [nodeInterface],
41 | });
42 |
43 | registerTypeLoader(ProductType, load);
44 |
45 | export default ProductType;
46 |
47 | export const ProductConnection = connectionDefinitions({
48 | name: 'Product',
49 | nodeType: ProductType,
50 | });
51 |
--------------------------------------------------------------------------------
/packages/server/src/modules/product/mutations/ProductCreate.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLNonNull, GraphQLString, GraphQLInt, GraphQLID } from 'graphql';
2 | import { mutationWithClientMutationId } from 'graphql-relay';
3 |
4 | import { errorField, successField } from '../../../graphql';
5 |
6 | import * as ProductLoader from '../ProductLoader';
7 | import Product from '../ProductModel';
8 | import ProductType from '../ProductType';
9 |
10 | export default mutationWithClientMutationId({
11 | name: 'ProductCreate',
12 | inputFields: {
13 | name: {
14 | type: new GraphQLNonNull(GraphQLString),
15 | },
16 | description: {
17 | type: new GraphQLNonNull(GraphQLString),
18 | },
19 | pictureUrl: {
20 | type: new GraphQLNonNull(GraphQLString),
21 | },
22 | points: {
23 | type: new GraphQLNonNull(GraphQLInt),
24 | },
25 | storeId: {
26 | type: new GraphQLNonNull(GraphQLID),
27 | },
28 | },
29 | mutateAndGetPayload: async ({ id, name, description, pictureUrl, points, storeId }) => {
30 | const product = await new Product({
31 | id,
32 | name,
33 | description,
34 | pictureUrl,
35 | points,
36 | storeId
37 | }).save();
38 |
39 | return {
40 | id: product.id,
41 | success: 'Product register success',
42 | };
43 | },
44 | outputFields: {
45 | product: {
46 | type: ProductType,
47 | resolve: async ({ id }, _, context) => {
48 | return await ProductLoader.load(context, id);
49 | },
50 | },
51 | ...errorField,
52 | ...successField,
53 | },
54 | });
55 |
--------------------------------------------------------------------------------
/packages/app/src/components/ui/Product.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image, Text, View } from 'react-native';
3 | import { useNavigation } from '@react-navigation/native';
4 |
5 | import { Button } from './Button';
6 | import { useAuth } from '../../core/auth/useAuth';
7 |
8 | export function Product({ product, userPoints = { points: 0 } }) {
9 | const { _id, pictureUrl, name, description, points } = product;
10 | const { token, type } = useAuth();
11 |
12 | const { navigate } = useNavigation();
13 |
14 | console.log(userPoints);
15 |
16 | return (
17 |
28 |
29 |
33 |
34 | {name}
35 | {points} pontos
36 |
37 |
38 | {token && type === 'customer' && (
39 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/packages/server/src/modules/user/UserModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Document, Model, Types } from 'mongoose';
2 | import bcrypt from 'bcrypt';
3 |
4 | const { ObjectId } = mongoose.Schema.Types;
5 | const Schema = mongoose.Schema;
6 |
7 | const UserSchema = new Schema(
8 | {
9 | name: {
10 | type: String,
11 | required: 'name is required',
12 | },
13 | username: {
14 | type: String,
15 | required: false,
16 | },
17 | email: {
18 | type: String,
19 | required: 'email is required',
20 | },
21 | password: {
22 | type: String,
23 | required: 'password is required',
24 | },
25 | type: {
26 | type: String,
27 | required: 'type is required, can be provider or customer',
28 | }
29 | },
30 | {
31 | collection: 'User',
32 | timestamps: {
33 | createdAt: 'createdAt',
34 | updatedAt: 'updatedAt',
35 | },
36 | }
37 | );
38 |
39 | export interface IUser extends Document {
40 | readonly name: string;
41 | readonly email: string;
42 | readonly password: string;
43 | readonly type: string; // provider or customer
44 | readonly authenticate: (plainTextPassword: string) => boolean;
45 | readonly encryptPassword: (password: string | undefined) => string;
46 | readonly createdAt: Date;
47 | readonly updatedAt: Date;
48 | }
49 |
50 | UserSchema.methods = {
51 | authenticate(plainTextPassword) {
52 | return bcrypt.compareSync(plainTextPassword, this.password);
53 | },
54 | encryptPassword(password) {
55 | return bcrypt.hashSync(password, 8);
56 | },
57 | };
58 |
59 | const UserModel: Model = mongoose.model('User', UserSchema);
60 |
61 | export default UserModel;
62 |
--------------------------------------------------------------------------------
/packages/app/src/core/auth/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useState } from 'react';
2 |
3 | import { getAuthToken, updateAuthToken } from './security';
4 | import { getUserType, updateUserType } from './userType';
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
7 | export const AuthContext = React.createContext(null!);
8 |
9 | export const AuthProvider = ({
10 | children,
11 | }: {
12 | readonly children: React.ReactNode;
13 | }) => {
14 | const [userToken, setUserToken] = useState();
15 | const [userType, setUserType] = useState('customer'); // customer or provider
16 |
17 | const signIn = useCallback((token, type, callback) => {
18 | updateAuthToken(token);
19 | setUserToken(token);
20 | updateUserType(type);
21 | setUserType(type);
22 | return callback();
23 | }, []);
24 |
25 | const signOut = useCallback(callback => {
26 | setUserToken(null);
27 | updateAuthToken();
28 | updateUserType('customer');
29 | setUserType('customer');
30 | return callback();
31 | }, []);
32 |
33 | async function init() {
34 | const authToken = await getAuthToken();
35 | setUserToken(authToken);
36 | console.log({ authToken });
37 | const type = await getUserType();
38 | setUserType(type);
39 | }
40 |
41 | useEffect(() => {
42 | init();
43 | }, []);
44 |
45 | const value = useMemo(
46 | () => ({
47 | token: userToken,
48 | type: userType,
49 | signIn,
50 | signOut,
51 | }),
52 | [userToken, userType, signIn, signOut]
53 | );
54 |
55 | return {children};
56 | };
57 |
--------------------------------------------------------------------------------
/packages/server/src/modules/node/typeRegister.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { GraphQLObjectType } from 'graphql';
3 |
4 | import { fromGlobalId, nodeDefinitions } from 'graphql-relay';
5 |
6 | // import { GraphQLContext } from '../../graphql/types';
7 |
8 | type GraphQLContext = {};
9 |
10 | type Load = (context: GraphQLContext, id: string) => any;
11 | type TypeLoaders = {
12 | readonly [key: string]: {
13 | readonly type: GraphQLObjectType;
14 | readonly load: Load;
15 | };
16 | };
17 |
18 | const getTypeRegister = () => {
19 | const typesLoaders: TypeLoaders = {};
20 |
21 | const getTypesLoaders = () => typesLoaders;
22 |
23 | const registerTypeLoader = (type: GraphQLObjectType, load: Load) => {
24 | typesLoaders[type.name] = {
25 | type,
26 | load,
27 | };
28 |
29 | return type;
30 | };
31 |
32 | const idFetcher = (globalId, context: GraphQLContext) => {
33 | const { type, id } = fromGlobalId(globalId);
34 |
35 | const { load } = typesLoaders[type] || { load: null };
36 |
37 | return (load && load(context, id)) || null;
38 | };
39 |
40 | const typeResolver = (obj) => {
41 | const { type } = typesLoaders[obj.constructor.name] || { type: null };
42 |
43 | return type;
44 | };
45 |
46 | const { nodeField, nodesField, nodeInterface } = nodeDefinitions(
47 | idFetcher,
48 | typeResolver
49 | );
50 |
51 | return {
52 | registerTypeLoader,
53 | getTypesLoaders,
54 | nodeField,
55 | nodesField,
56 | nodeInterface,
57 | };
58 | };
59 |
60 | const { registerTypeLoader, nodeInterface, nodeField, nodesField } =
61 | getTypeRegister();
62 |
63 | export { registerTypeLoader, nodeInterface, nodeField, nodesField };
64 |
--------------------------------------------------------------------------------
/packages/server/src/modules/user/mutations/UserLoginWithEmailMutation.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLNonNull, GraphQLString } from 'graphql';
2 | import { mutationWithClientMutationId } from 'graphql-relay';
3 |
4 | import { errorField, successField } from '../../../graphql';
5 |
6 | import { generateToken } from '../../../auth';
7 |
8 | import UserModel from '../UserModel';
9 |
10 | import * as UserLoader from '../UserLoader';
11 | import UserType from '../UserType';
12 |
13 | export default mutationWithClientMutationId({
14 | name: 'UserLoginWithEmail',
15 | inputFields: {
16 | email: {
17 | type: new GraphQLNonNull(GraphQLString),
18 | },
19 | password: {
20 | type: new GraphQLNonNull(GraphQLString),
21 | },
22 | },
23 | mutateAndGetPayload: async ({ email, password }) => {
24 | const user = await UserModel.findOne({ email: email.trim().toLowerCase() });
25 |
26 | const defaultErrorMessage = 'Invalid credentials';
27 | if (!user) {
28 | return {
29 | error: defaultErrorMessage,
30 | };
31 | }
32 |
33 | const correctPassword = user.authenticate(password);
34 |
35 | if (!correctPassword) {
36 | return {
37 | error: defaultErrorMessage,
38 | };
39 | }
40 |
41 | return {
42 | token: generateToken(user),
43 | id: user._id,
44 | success: 'Logged with success',
45 | };
46 | },
47 | outputFields: {
48 | token: {
49 | type: GraphQLString,
50 | resolve: ({ token }) => token,
51 | },
52 | me: {
53 | type: UserType,
54 | resolve: async ({ id }, _, context) => {
55 | return await UserLoader.load(context, id);
56 | },
57 | },
58 | ...errorField,
59 | ...successField,
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/packages/server/src/modules/store/mutations/StoreCreate.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLNonNull, GraphQLString } from 'graphql';
2 | import { mutationWithClientMutationId } from 'graphql-relay';
3 | import bcrypt from 'bcrypt';
4 |
5 | import { errorField, successField } from '../../../graphql';
6 |
7 | import * as StoreLoader from '../StoreLoader';
8 | import Store from '../StoreModel';
9 | import StoreType from '../StoreType';
10 |
11 | import * as UserStoreLoader from '../../userStore/UserStoreLoader';
12 | import UserStore from '../../userStore/UserStoreModel';
13 | import UserStoreType from '../../userStore/UserStoreType';
14 |
15 | export default mutationWithClientMutationId({
16 | name: 'StoreCreate',
17 | inputFields: {
18 | name: {
19 | type: new GraphQLNonNull(GraphQLString),
20 | },
21 | description: {
22 | type: new GraphQLNonNull(GraphQLString),
23 | },
24 | pictureUrl: {
25 | type: new GraphQLNonNull(GraphQLString),
26 | },
27 | },
28 | mutateAndGetPayload: async ({ id, name, description, pictureUrl }, props) => {
29 | const store = await new Store({
30 | id,
31 | name,
32 | description,
33 | pictureUrl
34 | }).save();
35 |
36 | if (props.user._id) {
37 | await new UserStore({
38 | id,
39 | storeId: store.id,
40 | userId: props.user._id
41 | }).save();
42 | }
43 |
44 | return {
45 | id: store.id,
46 | success: 'Store register success',
47 | };
48 | },
49 | outputFields: {
50 | store: {
51 | type: StoreType,
52 | resolve: async ({ id }, _, context) => {
53 | return await StoreLoader.load(context, id);
54 | },
55 | },
56 | ...errorField,
57 | ...successField,
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/packages/server/src/app.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { getDataloaders } from './graphql/loaderRegister';
3 |
4 | require('dotenv').config();
5 | import Koa from 'koa';
6 | import GraphQLHTTP from 'koa-graphql';
7 | import Router from 'koa-router';
8 | import koaPlayground from 'graphql-playground-middleware-koa';
9 | import cors from '@koa/cors';
10 | import bodyParser from 'koa-bodyparser';
11 |
12 | import { getUser } from './auth';
13 | import { schema } from './schema/schema';
14 |
15 | // import { auth } from "./auth";
16 |
17 | const app = new Koa();
18 | const router = new Router();
19 |
20 | // router.use(auth);
21 |
22 | const graphqlSettingsPerReq = async (req, ctx, koaContext) => {
23 | const { user } = await getUser(req.header.authorization);
24 | const dataloaders = getDataloaders();
25 |
26 | return {
27 | graphiql: process.env.NODE_ENV !== 'production',
28 | schema,
29 | context: {
30 | user,
31 | req,
32 | dataloaders,
33 | },
34 | formatError: (error) => {
35 | console.log(error.message);
36 | console.log(error.locations);
37 | console.log(error.stack);
38 |
39 | return {
40 | message: error.message,
41 | locations: error.locations,
42 | stack: error.stack,
43 | };
44 | },
45 | };
46 | };
47 |
48 | const graphqlServer = GraphQLHTTP(graphqlSettingsPerReq);
49 |
50 | router.all('/graphql', graphqlServer);
51 | router.all(
52 | '/graphiql',
53 | koaPlayground({
54 | endpoint: '/graphql',
55 | subscriptionEndpoint: '/subscriptions',
56 | // subscriptionsEndpoint: `ws://localhost:${port}/subscriptions`
57 | }),
58 | );
59 | app.use(bodyParser());
60 | app.use(cors());
61 | app.use(router.routes()).use(router.allowedMethods());
62 |
63 | export default app;
--------------------------------------------------------------------------------
/packages/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@amora/app",
3 | "version": "1.0.0",
4 | "main": "__generated__/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web",
10 | "eject": "expo eject",
11 | "relay": "relay-compiler",
12 | "env:example": "cp .env.example .env",
13 | "postinstall": "expo-yarn-workspaces postinstall"
14 | },
15 | "dependencies": {
16 | "@react-native-async-storage/async-storage": "~1.15.0",
17 | "@react-navigation/bottom-tabs": "^6.0.9",
18 | "@react-navigation/native": "^6.0.6",
19 | "@react-navigation/stack": "^6.0.11",
20 | "expo": "~43.0.0",
21 | "expo-barcode-scanner": "~11.1.2",
22 | "expo-camera": "~12.0.3",
23 | "expo-status-bar": "~1.1.0",
24 | "expo-vector-icons": "^10.0.1",
25 | "react": "^17.0.2",
26 | "react-dom": "^17.0.2",
27 | "react-native": "0.64.2",
28 | "react-native-dotenv": "^3.3.1",
29 | "react-native-gesture-handler": "~1.10.2",
30 | "react-native-qrcode-svg": "^6.1.2",
31 | "react-native-safe-area-context": "3.3.2",
32 | "react-native-screens": "~3.8.0",
33 | "react-native-svg": "^12.1.1",
34 | "react-native-web": "0.17.1",
35 | "react-relay": "^12.0.0"
36 | },
37 | "devDependencies": {
38 | "@babel/core": "^7.12.9",
39 | "@types/react": "~17.0.21",
40 | "@types/react-native": "~0.64.12",
41 | "@types/react-relay": "^13.0.0",
42 | "babel-plugin-relay": "^12.0.0",
43 | "expo-yarn-workspaces": "^1.6.0",
44 | "graphql": "^15.0.0",
45 | "metro-config": "^0.66.2",
46 | "relay-compiler": "^13.0.0",
47 | "relay-config": "^12.0.0",
48 | "typescript": "~4.3.5"
49 | },
50 | "private": true
51 | }
52 |
--------------------------------------------------------------------------------
/packages/server/src/modules/user/UserType.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { GraphQLList, GraphQLObjectType, GraphQLString } from 'graphql';
3 | import { globalIdField } from 'graphql-relay';
4 | import { objectIdResolver } from '@entria/graphql-mongo-helpers';
5 |
6 | // import TeamType from "../team/TeamType";
7 | // import Team from "../team/TeamModel";
8 | import { nodeInterface, registerTypeLoader } from '../node/typeRegister';
9 |
10 | import { load } from './UserLoader';
11 | import { connectionDefinitions } from '../../graphql/connectionDefinitions';
12 |
13 | const UserType = new GraphQLObjectType({
14 | name: 'User',
15 | description: 'User data',
16 | fields: () => ({
17 | id: globalIdField('User'),
18 | ...objectIdResolver,
19 | name: {
20 | type: GraphQLString,
21 | resolve: (user) => user.name,
22 | },
23 | username: {
24 | type: GraphQLString,
25 | resolve: (user) => user.username,
26 | },
27 | email: {
28 | type: GraphQLString,
29 | resolve: (user) => user.email,
30 | },
31 | type: {
32 | type: GraphQLString, // provider or customer
33 | resolve: (user) => user.type,
34 | },
35 | // team: {
36 | // type: TeamType,
37 | // resolve: async (user) => {
38 | // return await Team.findOne({
39 | // _id: user.team,
40 | // });
41 | // },
42 | // },
43 | // kind: {
44 | // type: GraphQLList(GraphQLString),
45 | // description: "Kind that this user belongs: coach or player",
46 | // resolve: (user) => user?.kind || [],
47 | // },
48 | }),
49 | interfaces: () => [nodeInterface],
50 | });
51 |
52 | registerTypeLoader(UserType, load);
53 |
54 | export default UserType;
55 |
56 | export const UserConnection = connectionDefinitions({
57 | name: 'User',
58 | nodeType: UserType,
59 | });
60 |
--------------------------------------------------------------------------------
/packages/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@amora/server",
3 | "version": "1.0.0",
4 | "description": "Amora Server",
5 | "main": "index.js",
6 | "module": "src/index.ts",
7 | "repository": "https://github.com/akinncar/amora",
8 | "author": "Akinn Rosa ",
9 | "license": "MIT",
10 | "engines": {
11 | "node": ">=10.16.0 <=14.x.x",
12 | "npm": "6.14.9"
13 | },
14 | "scripts": {
15 | "es": "node -r esbuild-register",
16 | "dev": "yarn es ./src/index.ts -w",
17 | "start": "node ./dist/index.js",
18 | "build": "rimraf ./dist && tsc",
19 | "tsc": "tsc",
20 | "env:example": "cp .env.example .env",
21 | "update-schema": "yarn es ./scripts/updateSchema.ts --extensions \".es6,.js,.es,.jsx,.mjs,.ts\""
22 | },
23 | "dependencies": {
24 | "@entria/graphql-mongo-helpers": "^1.0.1",
25 | "@entria/graphql-mongoose-loader": "^4.3.2",
26 | "@koa/cors": "^3.1.0",
27 | "bcrypt": "^5.0.1",
28 | "dataloader": "^2.0.0",
29 | "dotenv": "^10.0.0",
30 | "dotenv-safe": "^8.2.0",
31 | "esbuild": "^0.13.12",
32 | "esbuild-register": "^3.1.0",
33 | "graphql": "15",
34 | "graphql-playground-middleware-koa": "^1.6.22",
35 | "graphql-relay": "^0.9.0",
36 | "jsonwebtoken": "^8.5.1",
37 | "koa": "^2.13.4",
38 | "koa-bodyparser": "^4.3.0",
39 | "koa-graphql": "^0.9.0",
40 | "koa-router": "^10.1.1",
41 | "mongoose": "^6.0.12",
42 | "typescript": "^4.4.4"
43 | },
44 | "devDependencies": {
45 | "@koa/cors": "^3.1.0",
46 | "@types/bcrypt": "^5.0.0",
47 | "@types/dotenv-safe": "^8.1.2",
48 | "@types/jsonwebtoken": "^8.5.8",
49 | "@types/koa": "^2.13.4",
50 | "@types/koa-bodyparser": "^4.3.4",
51 | "@types/koa-graphql": "^0.8.5",
52 | "@types/koa-router": "^7.4.4",
53 | "@types/koa__cors": "^3.0.3"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": {
5 | "es6": true
6 | },
7 | "ignorePatterns": [
8 | "node_modules",
9 | "build",
10 | "coverage",
11 | "babel.config.js",
12 | "metro.config.js",
13 | "hardhat.config.js",
14 | "__tests__/contracts"
15 | ],
16 | "plugins": [
17 | "import",
18 | "eslint-comments",
19 | "functional",
20 | "react",
21 | "react-native"
22 | ],
23 | "extends": [
24 | "eslint:recommended",
25 | "plugin:eslint-comments/recommended",
26 | "plugin:@typescript-eslint/recommended",
27 | "plugin:import/typescript",
28 | "plugin:functional/lite",
29 | "prettier"
30 | ],
31 | "globals": {
32 | "console": true,
33 | "__DEV__": true
34 | },
35 | "rules": {
36 | "@typescript-eslint/explicit-module-boundary-types": "off",
37 | "eslint-comments/disable-enable-pair": [
38 | "error",
39 | {
40 | "allowWholeFile": true
41 | }
42 | ],
43 | "eslint-comments/no-unused-disable": "error",
44 | // "import/order": [
45 | // "error",
46 | // {
47 | // "newlines-between": "always",
48 | // "alphabetize": {
49 | // "order": "asc"
50 | // }
51 | // }
52 | // ],
53 | "sort-imports": [
54 | "error",
55 | {
56 | "ignoreDeclarationSort": true,
57 | "ignoreCase": true
58 | }
59 | ],
60 | "quotes": [
61 | "error",
62 | "single"
63 | ],
64 | "functional/no-let": "off",
65 | "functional/immutable-data": "off",
66 | "functional/prefer-type-literal": "off",
67 | "react-native/no-unused-styles": 2,
68 | "react-native/split-platform-components": 2,
69 | "react-native/no-color-literals": "off",
70 | "react-native/no-raw-text": 2,
71 | "react-native/no-single-element-style-arrays": 2
72 | },
73 | "parserOptions": {
74 | "ecmaFeatures": {
75 | "jsx": true
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/packages/server/src/modules/user/mutations/UserRegisterWithEmailMutation.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { GraphQLNonNull, GraphQLString } from 'graphql';
3 | import { mutationWithClientMutationId } from 'graphql-relay';
4 | import bcrypt from 'bcrypt';
5 |
6 | import { errorField, successField } from '../../../graphql';
7 |
8 | import * as UserLoader from '../UserLoader';
9 | import User from '../UserModel';
10 | import UserType from '../UserType';
11 |
12 | import { generateToken } from '../../../auth';
13 |
14 | export default mutationWithClientMutationId({
15 | name: 'UserRegisterWithEmail',
16 | inputFields: {
17 | name: {
18 | type: new GraphQLNonNull(GraphQLString),
19 | },
20 | email: {
21 | type: new GraphQLNonNull(GraphQLString),
22 | },
23 | password: {
24 | type: new GraphQLNonNull(GraphQLString),
25 | },
26 | type: {
27 | type: new GraphQLNonNull(GraphQLString),
28 | },
29 | },
30 | mutateAndGetPayload: async ({ name, email, password, type }) => {
31 | const hasUser = await User.findOne({
32 | email: email.trim().toLowerCase(),
33 | });
34 |
35 | if (hasUser) {
36 | return {
37 | token: null,
38 | error: 'User already exists',
39 | };
40 | }
41 |
42 | const user = await new User({
43 | name,
44 | email,
45 | password: bcrypt.hashSync(password, 8),
46 | type
47 | }).save();
48 |
49 | return {
50 | token: generateToken({ user }),
51 | error: null,
52 | };
53 | },
54 | outputFields: {
55 | token: {
56 | type: GraphQLString,
57 | resolve: ({ token }) => token,
58 | },
59 | // error: {
60 | // type: GraphQLString,
61 | // resolve: ({ error }) => error,
62 | // },
63 | me: {
64 | type: UserType,
65 | resolve: async ({ id }, _, context) => {
66 | return await UserLoader.load(context, id);
67 | },
68 | },
69 | ...errorField,
70 | ...successField,
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { FlatList, Text, View } from 'react-native';
3 |
4 | import { useNavigation } from '@react-navigation/native';
5 | import { useLazyLoadQuery } from 'react-relay';
6 |
7 | import { NewProduct } from './NewProduct';
8 | import { useAuth } from '../../core/auth/useAuth';
9 | import { Store } from '../home/Store';
10 | import { HomeStoreListQuery } from './HomeStoreListQuery';
11 | import { Product } from '../ui/Product';
12 |
13 | import type { HomeStoreListQuery as HomeStoreListQueryType } from './__generated__/HomeStoreListQuery.graphql';
14 |
15 | export function Home() {
16 | const { type } = useAuth();
17 | const { navigate } = useNavigation();
18 |
19 | const data = useLazyLoadQuery(
20 | HomeStoreListQuery,
21 | {},
22 | { fetchPolicy: 'network-only' }
23 | );
24 |
25 | if (type === 'provider') {
26 | if (data.userStoreByUserId.edges.length < 1) {
27 | navigate('CreateStore');
28 | return <>>;
29 | }
30 |
31 | return (
32 | }
35 | keyExtractor={({ node }) => node._id.toString()}
36 | ListHeaderComponent={() => (
37 |
40 | )}
41 | />
42 | );
43 | }
44 |
45 | return (
46 |
47 | }
50 | keyExtractor={({ node }) => node._id.toString()}
51 | ItemSeparatorComponent={() => }
52 | ListHeaderComponent={() => }
53 | ListFooterComponent={() => }
54 | />
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/packages/server/src/modules/store/StoreType.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql';
3 | import { globalIdField } from 'graphql-relay';
4 | import { objectIdResolver } from '@entria/graphql-mongo-helpers';
5 |
6 | // import TeamType from "../team/TeamType";
7 | // import Team from "../team/TeamModel";
8 | import { nodeInterface, registerTypeLoader } from '../node/typeRegister';
9 |
10 | import { load } from './StoreLoader';
11 | import { connectionDefinitions } from '../../graphql/connectionDefinitions';
12 | import { withFilter } from '../../graphql/withFilter'
13 |
14 | import ProductType, { ProductConnection } from '../../modules/product/ProductType';
15 | import Product from '../product/ProductModel';
16 | import * as ProductLoader from '../product/ProductLoader';
17 |
18 | const StoreType = new GraphQLObjectType({
19 | name: 'Store',
20 | description: 'Store data',
21 | fields: () => ({
22 | id: globalIdField('Store'),
23 | ...objectIdResolver,
24 | name: {
25 | type: GraphQLString,
26 | resolve: (store) => store.name,
27 | },
28 | description: {
29 | type: GraphQLString,
30 | resolve: (store) => store.description,
31 | },
32 | pictureUrl: {
33 | type: GraphQLString,
34 | resolve: (store) => store.pictureUrl,
35 | },
36 | // products: {
37 | // type: GraphQLList(ProductType),
38 | // resolve: async (store) => {
39 | // return await Product.find({
40 | // storeId: store._id,
41 | // });
42 | // },
43 | // },
44 | products: {
45 | type: new GraphQLNonNull(ProductConnection.connectionType),
46 | resolve: async (store, args, context) =>
47 | ProductLoader.loadAll(context, withFilter(args, { storeId: store._id })),
48 | },
49 | }),
50 | interfaces: () => [nodeInterface],
51 | });
52 |
53 | registerTypeLoader(StoreType, load);
54 |
55 | export default StoreType;
56 |
57 | export const StoreConnection = connectionDefinitions({
58 | name: 'Store',
59 | nodeType: StoreType,
60 | });
61 |
--------------------------------------------------------------------------------
/packages/server/src/modules/userPoints/mutations/UserPointsCreateOrUpdate.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLString } from 'graphql';
2 | import { mutationWithClientMutationId } from 'graphql-relay';
3 |
4 | import { errorField, successField } from '../../../graphql';
5 |
6 | import * as UserPointsLoader from '../UserPointsLoader';
7 | import UserPoints from '../UserPointsModel';
8 | import UserPointsType from '../UserPointsType';
9 |
10 | export default mutationWithClientMutationId({
11 | name: 'UserPointsCreateOrUpdate',
12 | inputFields: {
13 | points: {
14 | type: new GraphQLNonNull(GraphQLInt),
15 | },
16 | storeId: {
17 | type: new GraphQLNonNull(GraphQLID),
18 | },
19 | userId: {
20 | type: new GraphQLNonNull(GraphQLID),
21 | },
22 | },
23 | mutateAndGetPayload: async ({ id, points, storeId, userId }) => {
24 |
25 | const hasUserPoints = await UserPoints.findOne({
26 | storeId: storeId,
27 | userId: userId,
28 | });
29 |
30 | if (hasUserPoints) {
31 | const pointsUpdated = points === 0 ? hasUserPoints.points + 1 : hasUserPoints.points - points
32 |
33 | if (pointsUpdated < 0) return {
34 | id: hasUserPoints.id,
35 | error: 'O usuário não possui pontos suficientes',
36 | };
37 |
38 | await hasUserPoints.updateOne({
39 | points: pointsUpdated, storeId, userId
40 | })
41 |
42 | return {
43 | id: hasUserPoints.id,
44 | success: 'Pontos do usuário atualizados com sucesso!',
45 | };
46 | }
47 |
48 | const userPoints = await new UserPoints({
49 | id,
50 | points,
51 | storeId,
52 | userId
53 | }).save();
54 |
55 | return {
56 | id: userPoints.id,
57 | success: 'Pontos do usuário atualizados com sucesso!',
58 | };
59 | },
60 | outputFields: {
61 | userPoints: {
62 | type: UserPointsType,
63 | resolve: async ({ id }, _, context) => {
64 | return await UserPointsLoader.load(context, id);
65 | },
66 | },
67 | ...errorField,
68 | ...successField,
69 | },
70 | });
71 |
--------------------------------------------------------------------------------
/packages/app/src/components/settings/Settings.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { Text, TouchableOpacity, View } from 'react-native';
3 |
4 | import { useNavigation } from '@react-navigation/native';
5 | import { useLazyLoadQuery, useQueryLoader } from 'react-relay';
6 |
7 | import { SettingsMeQuery } from './SettingsMeQuery';
8 | import { Button } from '../ui/Button';
9 | import { useAuth } from '../../core/auth/useAuth';
10 |
11 | import type { SettingsMeQuery as SettingsMeQueryType } from './__generated__/SettingsMeQuery.graphql';
12 |
13 | export function Settings() {
14 | const { navigate } = useNavigation();
15 | const { signOut } = useAuth();
16 |
17 | const [, loadQuery] = useQueryLoader(SettingsMeQuery);
18 |
19 | const refresh = useCallback(
20 | () => loadQuery({}, { fetchPolicy: 'network-only' }),
21 | []
22 | );
23 |
24 | const data = useLazyLoadQuery(
25 | SettingsMeQuery,
26 | {},
27 | undefined
28 | );
29 |
30 | return (
31 |
37 | {data.me?._id ? (
38 | <>
39 |
40 | Você está logado como {data.me.email}
41 |
42 |
43 | signOut(refresh)}>
44 |
45 | Sair
46 |
47 |
48 | >
49 | ) : (
50 | <>
51 | navigate('SignUp')}
53 | title="Criar uma conta"
54 | style={{ marginHorizontal: 16, padding: 8 }}
55 | />
56 | ou
57 | navigate('SignIn')}>
58 |
59 | Faça login com uma conta já existente
60 |
61 |
62 | >
63 | )}
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/packages/app/src/components/storeDetails/StoreDetails.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { FlatList, SafeAreaView, View } from 'react-native';
3 | import { useNavigation, useRoute } from '@react-navigation/native';
4 |
5 | import { useLazyLoadQuery, useQueryLoader } from 'react-relay';
6 |
7 | import { Header } from './Header';
8 | import { Product } from '../ui/Product';
9 | import { Button } from '../ui/Button';
10 | import { useAuth } from '../../core/auth/useAuth';
11 | import { StoreDetailsQuery } from './StoreDetailsQuery';
12 |
13 | import type { StoreDetailsQuery as StoreDetailsQueryType } from './__generated__/StoreDetailsQuery.graphql';
14 |
15 | export function StoreDetails() {
16 | const { navigate } = useNavigation();
17 | const { token } = useAuth();
18 | const { params } = useRoute();
19 | const { storeId } = params;
20 |
21 | const data = useLazyLoadQuery(
22 | StoreDetailsQuery,
23 | { storeId },
24 | { fetchPolicy: 'store-and-network', fetchKey: new Date().toString() }
25 | );
26 |
27 | const [, loadQueryStoreDetails] = useQueryLoader(StoreDetailsQuery);
28 | const refrehStoreDetails = useCallback(
29 | () => loadQueryStoreDetails({}, { fetchPolicy: 'network-only' }),
30 | []
31 | );
32 |
33 | const sortByPointsAsc = (a, b) => {
34 | if (a.node.points < b.node.points) {
35 | return -1;
36 | }
37 | if (a.node.points > b.node.points) {
38 | return 1;
39 | }
40 | return 0;
41 | };
42 |
43 | const userPoints =
44 | data?.userPoints?.edges.length > 0
45 | ? data?.userPoints?.edges[0].node
46 | : { points: 0 };
47 |
48 | useEffect(() => {
49 | setInterval(() => {
50 | refrehStoreDetails();
51 | }, 3000);
52 | }, []);
53 |
54 | return (
55 |
56 | (
59 |
60 | )}
61 | keyExtractor={({ node }) => node._id.toString()}
62 | ListHeaderComponent={() => (
63 |
64 | )}
65 | />
66 | {token && (
67 | navigate('QrCode')}
69 | title="Exibir seu QRCode"
70 | style={{ margin: 16, padding: 8 }}
71 | />
72 | )}
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/CreateStore.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { ActivityIndicator, ScrollView, View } from 'react-native';
3 | import { useMutation, useQueryLoader } from 'react-relay';
4 |
5 | import { useNavigation } from '@react-navigation/native';
6 |
7 | import { TextInput } from '../ui/TextInput';
8 | import { Button } from '../ui/Button';
9 | import Alert from '../ui/Alert';
10 | import { CreateStoreMutation } from './CreateStoreMutation';
11 | import { HomeStoreListQuery } from './HomeStoreListQuery';
12 |
13 | export function CreateStore() {
14 | const { navigate } = useNavigation();
15 |
16 | const [createStore, isLoading] = useMutation(CreateStoreMutation);
17 |
18 | const [name, setName] = useState('');
19 | const [description, setDescription] = useState('');
20 | const [pictureUrl] = useState(
21 | 'https://looklanches.com.br/wp-content/uploads/2020/09/salada-1.jpg'
22 | );
23 |
24 | const [, loadQueryHome] = useQueryLoader(HomeStoreListQuery);
25 | const refreshHome = useCallback(
26 | () => loadQueryHome({}, { fetchPolicy: 'network-only' }),
27 | []
28 | );
29 |
30 | function onCompleted(data) {
31 | if (data.StoreCreate.error) return Alert.alert(data.StoreCreate.error);
32 |
33 | return Alert.alert('Sucesso ao criar seu estabelecimento!', undefined, [
34 | {
35 | text: 'OK',
36 | onPress: async () => {
37 | await refreshHome();
38 | navigate('Home');
39 | },
40 | },
41 | ]);
42 | }
43 |
44 | function handleSubmit() {
45 | if (isLoading) return;
46 |
47 | if (!name) return Alert.alert('Campo Nome é obrigatório');
48 | if (!description) return Alert.alert('Campo Descrição é obrigatório');
49 |
50 | createStore({
51 | variables: {
52 | input: {
53 | name,
54 | description,
55 | pictureUrl,
56 | },
57 | },
58 | onCompleted,
59 | });
60 | }
61 |
62 | if (isLoading) {
63 | return ;
64 | }
65 |
66 | return (
67 |
68 | setName(text)}
71 | label="Nome"
72 | style={{ marginHorizontal: 16, marginBottom: 16 }}
73 | />
74 | setDescription(text)}
77 | label="Descrição"
78 | style={{ marginHorizontal: 16, marginBottom: 16 }}
79 | />
80 |
86 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/packages/app/src/components/Home/NewProduct.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { View } from 'react-native';
3 |
4 | import { useMutation, useQueryLoader } from 'react-relay';
5 |
6 | import { TextInput } from '../ui/TextInput';
7 | import { Button } from '../ui/Button';
8 | import Alert from '../ui/Alert';
9 | import { HomeStoreListQuery } from './HomeStoreListQuery';
10 | import { NewProductMutation } from './NewProductMutation';
11 |
12 | export function NewProduct({ storeId }) {
13 | console.log({ storeId });
14 | const [createProduct, isLoading] = useMutation(NewProductMutation);
15 |
16 | const [name, setName] = useState('');
17 | const [description, setDescription] = useState('');
18 | const [points, setPoints] = useState('');
19 | const [pictureUrl] = useState(
20 | 'https://looklanches.com.br/wp-content/uploads/2020/09/salada-1.jpg'
21 | );
22 |
23 | const [, loadQueryHome] = useQueryLoader(HomeStoreListQuery);
24 | const refreshHome = useCallback(
25 | () => loadQueryHome({}, { fetchPolicy: 'network-only' }),
26 | []
27 | );
28 |
29 | function onCompleted(data) {
30 | if (data.ProductCreate.error) return Alert.alert(data.ProductCreate.error);
31 |
32 | return Alert.alert('Sucesso ao criar seu produto!', undefined, [
33 | {
34 | text: 'OK',
35 | onPress: async () => {
36 | refreshHome();
37 | },
38 | },
39 | ]);
40 | }
41 |
42 | function handleSubmit() {
43 | if (isLoading) return;
44 |
45 | if (!name) return Alert.alert('Campo Nome é obrigatório');
46 | if (!points) return Alert.alert('Campo Quantidade de pontos é obrigatório');
47 |
48 | createProduct({
49 | variables: {
50 | input: {
51 | name,
52 | description,
53 | points: parseInt(points),
54 | pictureUrl,
55 | storeId,
56 | },
57 | },
58 | onCompleted,
59 | });
60 | }
61 |
62 | return (
63 |
64 | setName(text)}
67 | style={{ flex: 1, paddingBottom: 16 }}
68 | label="Nome do produto"
69 | />
70 | setDescription(text)}
73 | style={{ flex: 1, paddingBottom: 16 }}
74 | label="Descrição do produto"
75 | />
76 | setPoints(text)}
79 | style={{ flex: 1, paddingBottom: 16 }}
80 | label="Quantidade de pontos para resgatar"
81 | keyboardType="numeric"
82 | />
83 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/packages/app/src/components/auth/SignIn.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { View } from 'react-native';
3 | import { useMutation, useQueryLoader } from 'react-relay';
4 |
5 | import { useNavigation } from '@react-navigation/native';
6 |
7 | import { TextInput } from '../ui/TextInput';
8 | import { Button } from '../ui/Button';
9 | import Alert from '../ui/Alert';
10 | import { useAuth } from '../../core/auth/useAuth';
11 | import { HomeStoreListQuery } from '../home/HomeStoreListQuery';
12 | import { SettingsMeQuery } from '../settings/SettingsMeQuery';
13 | import { SignInUserLoginWithEmailMutation } from './SignInUserLoginWithEmailMutation';
14 |
15 | import { SignInUserLoginWithEmailMutation as SignInUserLoginWithEmailMutationType } from './__generated__/SignInUserLoginWithEmailMutation.graphql';
16 |
17 | export function SignIn() {
18 | const { navigate } = useNavigation();
19 | const { signIn } = useAuth();
20 |
21 | const [email, setEmail] = useState('');
22 | const [password, setPassword] = useState('');
23 |
24 | const [, loadQueryHome] = useQueryLoader(HomeStoreListQuery);
25 | const [, loadQuerySettings] = useQueryLoader(SettingsMeQuery);
26 |
27 | const refreshHome = useCallback(
28 | () => loadQueryHome({}, { fetchPolicy: 'network-only' }),
29 | []
30 | );
31 | const refreshSettings = useCallback(
32 | () => loadQuerySettings({}, { fetchPolicy: 'network-only' }),
33 | []
34 | );
35 |
36 | const [login, isLoading] = useMutation(
37 | SignInUserLoginWithEmailMutation
38 | );
39 |
40 | async function onLogin() {
41 | await refreshHome();
42 | await refreshSettings();
43 | return navigate('Home');
44 | }
45 |
46 | function onCompleted(data) {
47 | if (data.UserLoginWithEmail.error)
48 | return Alert.alert(data.UserLoginWithEmail.error);
49 |
50 | if (data.UserLoginWithEmail?.token) {
51 | signIn(
52 | data.UserLoginWithEmail?.token,
53 | data.UserLoginWithEmail?.me.type,
54 | () =>
55 | Alert.alert('Logado com sucesso!', undefined, [
56 | { text: 'OK', onPress: onLogin },
57 | ])
58 | );
59 | }
60 | }
61 |
62 | function handleSubmit() {
63 | if (isLoading) return;
64 |
65 | if (!email) return Alert.alert('Campo E-mail é obrigatório');
66 | if (!password) return Alert.alert('Campo Senha é obrigatório');
67 |
68 | login({
69 | variables: {
70 | input: {
71 | email: email.toLowerCase(),
72 | password,
73 | },
74 | },
75 | onCompleted,
76 | });
77 | }
78 |
79 | return (
80 |
86 | setEmail(text)}
89 | label="E-mail"
90 | style={{ marginHorizontal: 16, marginBottom: 16 }}
91 | />
92 | setPassword(text)}
95 | label="Senha"
96 | style={{ marginHorizontal: 16, marginBottom: 16 }}
97 | secureTextEntry={true}
98 | />
99 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/packages/server/src/graphql/createLoader.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // eslint-disable-next-line
3 | import { mongooseLoader } from "@entria/graphql-mongoose-loader";
4 | import DataLoader from 'dataloader';
5 | import { ConnectionArguments } from 'graphql-relay';
6 | import { Document, Model, Types } from 'mongoose';
7 |
8 | import {
9 | buildMongoConditionsFromFilters,
10 | GraphQLFilter,
11 | } from '@entria/graphql-mongo-helpers';
12 |
13 | // import { validateContextUser } from './validateContextUser';
14 | import { withConnectionCursor } from './withConnectionCursor';
15 |
16 | const defaultViewerCanSee = (
17 | context: BaseContext,
18 | data: Value
19 | ): Value => data;
20 |
21 | export type DataLoaderKey = string | Types.ObjectId;
22 |
23 | export interface BaseContext<
24 | LoaderName extends string,
25 | Value extends Document
26 | > {
27 | readonly dataloaders: Record>;
28 | }
29 |
30 | export type CreateLoaderArgs<
31 | Context extends BaseContext,
32 | LoaderName extends string,
33 | Value extends Document
34 | > = {
35 | readonly model: Model;
36 | readonly viewerCanSee?: (context: Context, data: Value) => Value | Promise;
37 | readonly loaderName: LoaderName;
38 | readonly filterMapping?: object;
39 | };
40 |
41 | export interface FilteredConnectionArguments extends ConnectionArguments {
42 | readonly filters: GraphQLFilter | null;
43 | }
44 |
45 | export const createLoader = <
46 | Context extends BaseContext,
47 | LoaderName extends string,
48 | Value extends Document
49 | >({
50 | model,
51 | viewerCanSee = defaultViewerCanSee,
52 | loaderName,
53 | filterMapping = {},
54 | }: CreateLoaderArgs) => {
55 | class Loader {
56 | readonly [key: string]: any;
57 | constructor(data: Value) {
58 | // TODO - improve this - get only model paths
59 | // eslint-disable-next-line
60 | Object.keys(data).map((key) => {
61 | this[key] = (data as any)[key];
62 | });
63 | this.id = data.id || data._id;
64 | }
65 | }
66 |
67 | const nameIt = (name: string, cls: typeof Loader): typeof Loader =>
68 | ({ [name]: class extends cls {} }[name]);
69 |
70 | const Wrapper = nameIt(model.collection.collectionName, Loader);
71 |
72 | const getLoader = () =>
73 | new DataLoader((ids) => mongooseLoader(model, ids));
74 |
75 | const load = async (context: Context, id: DataLoaderKey) => {
76 | if (!id) {
77 | return null;
78 | }
79 |
80 | try {
81 | const data = await context.dataloaders[loaderName].load(id.toString());
82 |
83 | if (!data) {
84 | return null;
85 | }
86 |
87 | const filteredData = await viewerCanSee(context, data);
88 |
89 | return filteredData ? (new Wrapper(filteredData) as Value) : null;
90 | } catch (err) {
91 | console.log('err', err);
92 | return null;
93 | }
94 | };
95 |
96 | const clearCache = ({ dataloaders }: Context, id: string) =>
97 | dataloaders[loaderName].clear(id.toString());
98 |
99 | // disable validate to simpify workshop
100 | // const loadAll = validateContextUser(
101 | const loadAll = withConnectionCursor(
102 | model,
103 | load,
104 | (context: Context, args: FilteredConnectionArguments) => {
105 | const builtMongoConditions = buildMongoConditionsFromFilters(
106 | context,
107 | args.filters,
108 | filterMapping as any
109 | );
110 |
111 | const conditions = {
112 | ...builtMongoConditions.conditions,
113 | };
114 |
115 | const sort = {
116 | createdAt: -1,
117 | };
118 |
119 | return {
120 | conditions,
121 | sort,
122 | };
123 | }
124 | );
125 |
126 | return {
127 | Wrapper: Wrapper as {
128 | new (value: Value): Value;
129 | },
130 | getLoader,
131 | clearCache,
132 | load,
133 | loadAll,
134 | };
135 | };
136 |
--------------------------------------------------------------------------------
/packages/app/src/components/auth/SignUp.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ActivityIndicator, ScrollView, View } from 'react-native';
3 | import { useMutation } from 'react-relay';
4 |
5 | import { useNavigation } from '@react-navigation/native';
6 |
7 | import { TextInput } from '../ui/TextInput';
8 | import { RadioButton } from '../ui/RadioButton';
9 | import { Button } from '../ui/Button';
10 | import Alert from '../ui/Alert';
11 | import { SignUpUserRegisterWithEmailMutation } from './SignUpUserRegisterWithEmailMutation';
12 |
13 | import { SignUpUserRegisterWithEmailMutation as SignUpUserRegisterWithEmailMutationType } from './__generated__/SignUpUserRegisterWithEmailMutation.graphql';
14 |
15 | export function SignUp() {
16 | const { navigate } = useNavigation();
17 |
18 | const [createUser, isLoading] =
19 | useMutation(
20 | SignUpUserRegisterWithEmailMutation
21 | );
22 |
23 | const [type, setType] = useState('customer'); // customer or provider
24 | const [name, setName] = useState('');
25 | const [email, setEmail] = useState('');
26 | const [password, setPassword] = useState('');
27 | const [confirmPassword, setConfirmPassword] = useState('');
28 |
29 | function onCompleted(data) {
30 | if (data.UserRegisterWithEmail.error)
31 | return Alert.alert(data.UserRegisterWithEmail.error);
32 |
33 | return Alert.alert('Sucesso ao criar sua conta!', undefined, [
34 | {
35 | text: 'OK',
36 | onPress: () => navigate('SignIn'),
37 | },
38 | ]);
39 | }
40 |
41 | function handleSubmit() {
42 | if (isLoading) return;
43 |
44 | if (!name) return Alert.alert('Campo Nome é obrigatório');
45 | if (!email) return Alert.alert('Campo E-mail é obrigatório');
46 | if (!password) return Alert.alert('Campo Senha é obrigatório');
47 | if (password !== confirmPassword)
48 | return Alert.alert('Os campos de senha devem ser iguais');
49 |
50 | createUser({
51 | variables: {
52 | input: {
53 | name,
54 | email: email.toLowerCase(),
55 | password,
56 | type,
57 | },
58 | },
59 | onCompleted,
60 | });
61 | }
62 |
63 | if (isLoading) {
64 | return ;
65 | }
66 |
67 | return (
68 |
69 |
77 | setType('customer')}
81 | />
82 | setType('provider')}
86 | />
87 |
88 | setName(text)}
91 | label="Nome"
92 | style={{ marginHorizontal: 16, marginBottom: 16 }}
93 | />
94 | setEmail(text)}
97 | label="E-mail"
98 | keyboardType="email-address"
99 | style={{ marginHorizontal: 16, marginBottom: 16 }}
100 | />
101 | setPassword(text)}
104 | label="Senha"
105 | style={{ marginHorizontal: 16, marginBottom: 16 }}
106 | secureTextEntry={true}
107 | />
108 | setConfirmPassword(text)}
111 | label="Confirmar Senha"
112 | style={{ marginHorizontal: 16, marginBottom: 16 }}
113 | secureTextEntry={true}
114 | />
115 |
121 |
122 |
123 | );
124 | }
125 |
--------------------------------------------------------------------------------
/packages/app/src/components/qrCode/ScanQrCode.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
3 |
4 | import { BarCodeScanner } from 'expo-barcode-scanner';
5 | import { useLazyLoadQuery, useMutation } from 'react-relay';
6 | import { Camera } from 'expo-camera';
7 |
8 | import { Button } from '../ui/Button';
9 | import Alert from '../ui/Alert';
10 |
11 | import { ScanQrCodeUserPointsCreateOrUpdateMutation } from './ScanQrCodeUserPointsCreateOrUpdateMutation';
12 | import type { ScanQrCodeUserPointsCreateOrUpdateMutation as ScanQrCodeUserPointsCreateOrUpdateMutationType } from './__generated__/ScanQrCodeUserPointsCreateOrUpdateMutation.graphql';
13 |
14 | import { ScanQrCodeStoreQuery } from './ScanQrCodeStoreQuery';
15 | import type { ScanQrCodeStoreQuery as ScanQrCodeStoreQueryType } from './__generated__/ScanQrCodeStoreQuery.graphql';
16 |
17 | export function ScanQrCode() {
18 | const [hasPermission, setHasPermission] = useState(null);
19 | const [scanned, setScanned] = useState(false);
20 |
21 | const [updateUserPoints, isLoading] =
22 | useMutation(
23 | ScanQrCodeUserPointsCreateOrUpdateMutation
24 | );
25 |
26 | const data = useLazyLoadQuery(
27 | ScanQrCodeStoreQuery,
28 | {},
29 | { fetchPolicy: 'network-only' }
30 | );
31 |
32 | // eslint-disable-next-line functional/no-return-void
33 | useEffect(() => {
34 | (async () => {
35 | const { status } = await BarCodeScanner.requestPermissionsAsync();
36 | setHasPermission(status === 'granted');
37 | })();
38 | }, []);
39 |
40 | const onCompleted = data => {
41 | if (data.UserPointsCreateOrUpdate.error) {
42 | return Alert.alert(data.UserPointsCreateOrUpdate.error);
43 | }
44 | return Alert.alert(data.UserPointsCreateOrUpdate.success);
45 | };
46 |
47 | const handleUpdateUserPoints = code => {
48 | return updateUserPoints({
49 | variables: {
50 | input: {
51 | points: code.points,
52 | storeId: data?.userStoreByUserId?.edges[0].node.storeId,
53 | userId: code.userId,
54 | },
55 | },
56 | onCompleted,
57 | });
58 | };
59 |
60 | const handleBarCodeScanned = ({ type, data }) => {
61 | setScanned(true);
62 |
63 | const code = JSON.parse(data);
64 |
65 | if (code.points > 0) {
66 | return Alert.alert(
67 | `Quer remover ${code.points} ponto${
68 | code.points > 1 ? 's' : ''
69 | } deste usuário?`,
70 | undefined,
71 | [
72 | { text: 'Sim', onPress: () => handleUpdateUserPoints(code) },
73 | { text: 'Não', onPress: () => setScanned(false), style: 'cancel' },
74 | ]
75 | );
76 | }
77 |
78 | return Alert.alert('Quer adicionar 1 ponto a este usuário?', undefined, [
79 | { text: 'Sim', onPress: () => handleUpdateUserPoints(code) },
80 | { text: 'Não', onPress: () => setScanned(false), style: 'cancel' },
81 | ]);
82 | };
83 |
84 | if (isLoading) {
85 | return (
86 |
87 |
88 |
89 | );
90 | }
91 |
92 | if (hasPermission === false) {
93 | return Sem acesso a camera;
94 | }
95 |
96 | return (
97 |
98 | {scanned ? (
99 | setScanned(false)}
103 | />
104 | ) : (
105 |
112 | //
116 | )}
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/packages/server/src/schema/QueryType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql';
2 | import mongoose, { Document, Model, Types } from 'mongoose';
3 | const { ObjectId } = mongoose.Schema.Types;
4 |
5 | import { withFilter } from '../graphql/withFilter'
6 |
7 | import UserType, { UserConnection } from '../modules/user/UserType';
8 | import * as UserLoader from '../modules/user/UserLoader';
9 |
10 | import StoreType, { StoreConnection } from '../modules/store/StoreType';
11 | import * as StoreLoader from '../modules/store/StoreLoader';
12 |
13 | import ProductType, { ProductConnection } from '../modules/product/ProductType';
14 | import * as ProductLoader from '../modules/product/ProductLoader';
15 |
16 | import UserPointsType, { UserPointsConnection } from '../modules/userPoints/UserPointsType';
17 | import * as UserPointsLoader from '../modules/userPoints/UserPointsLoader';
18 |
19 | import UserStoreType, { UserStoreConnection } from '../modules/userStore/UserStoreType';
20 | import * as UserStoreLoader from '../modules/userStore/UserStoreLoader';
21 |
22 | import { nodeField, nodesField } from '../modules/node/typeRegister';
23 | import { connectionArgs } from '../graphql/connectionDefinitions';
24 |
25 | // import { version } from "../../../package.json";
26 |
27 | export default new GraphQLObjectType({
28 | name: 'Query',
29 | description: 'The root of all queries',
30 | fields: () => ({
31 | node: nodeField,
32 | nodes: nodesField,
33 | version: {
34 | type: GraphQLString,
35 | resolve: () => '1.0.0',
36 | },
37 | me: {
38 | type: UserType,
39 | resolve: (root, args, context) =>
40 | // @ts-ignore
41 | UserLoader.load(context, context.user?._id)
42 | },
43 | users: {
44 | type: new GraphQLNonNull(UserConnection.connectionType),
45 | args: {
46 | ...connectionArgs,
47 | },
48 | resolve: async (_, args, context) =>
49 | await UserLoader.loadAll(context, args),
50 | },
51 | stores: {
52 | type: new GraphQLNonNull(StoreConnection.connectionType),
53 | args: {
54 | ...connectionArgs,
55 | },
56 | resolve: async (_, args, context) =>
57 | await StoreLoader.loadAll(context, args),
58 | },
59 | storeByStoreId: {
60 | type: StoreType,
61 | args: {
62 | id: {
63 | type: new GraphQLNonNull(GraphQLID)
64 | }
65 | },
66 | resolve: async (_, args, context) =>
67 | // @ts-ignore
68 | StoreLoader.load(context, args.id),
69 | },
70 | productsByStoreId: {
71 | type: new GraphQLNonNull(ProductConnection.connectionType),
72 | args: {
73 | storeId: {
74 | type: new GraphQLNonNull(GraphQLID)
75 | }
76 | },
77 | resolve: async (_, args, context) =>
78 | ProductLoader.loadAll(context, withFilter(args, { storeId: args.storeId })),
79 | },
80 | userPoints: {
81 | type: new GraphQLNonNull(UserPointsConnection.connectionType),
82 | args: {
83 | ...connectionArgs,
84 | },
85 | resolve: async (_, args, context) =>
86 | await UserPointsLoader.loadAll(context, args),
87 | },
88 | userPointsByStoreIdAndUserId: {
89 | type: new GraphQLNonNull(UserPointsConnection.connectionType),
90 | args: {
91 | storeId: {
92 | type: new GraphQLNonNull(GraphQLID)
93 | },
94 | userId: {
95 | type: GraphQLID,
96 | }
97 | },
98 | resolve: async (_, args, context) =>
99 | // @ts-ignore
100 | UserPointsLoader.loadAll(context, withFilter(args, { storeId: args.storeId, userId: args.userId || context.user?._id })),
101 | },
102 | userStore: {
103 | type: new GraphQLNonNull(UserStoreConnection.connectionType),
104 | args: {
105 | ...connectionArgs,
106 | },
107 | resolve: async (_, args, context) =>
108 | await UserStoreLoader.loadAll(context, args),
109 | },
110 | userStoreByUserId: {
111 | type: new GraphQLNonNull(UserStoreConnection.connectionType),
112 | args: {
113 | userId: {
114 | type: GraphQLID,
115 | }
116 | },
117 | resolve: async (_, args, context) =>
118 | {
119 | // @ts-ignore
120 | return UserStoreLoader.loadAll(context, withFilter(args, { userId: args.userId || context.user?._id }))
121 | }
122 | },
123 | }),
124 | });
--------------------------------------------------------------------------------
/packages/server/src/graphql/connectionDefinitions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLBoolean,
3 | GraphQLFieldConfigArgumentMap,
4 | GraphQLFieldConfigMap,
5 | GraphQLFieldResolver,
6 | GraphQLInt,
7 | GraphQLList,
8 | GraphQLNonNull,
9 | GraphQLObjectType,
10 | GraphQLString,
11 | } from 'graphql';
12 |
13 | export const forwardConnectionArgs: GraphQLFieldConfigArgumentMap = {
14 | after: {
15 | type: GraphQLString,
16 | },
17 | first: {
18 | type: GraphQLInt,
19 | },
20 | };
21 |
22 | export const backwardConnectionArgs: GraphQLFieldConfigArgumentMap = {
23 | before: {
24 | type: GraphQLString,
25 | },
26 | last: {
27 | type: GraphQLInt,
28 | },
29 | };
30 |
31 | export const connectionArgs: GraphQLFieldConfigArgumentMap = {
32 | ...forwardConnectionArgs,
33 | ...backwardConnectionArgs,
34 | };
35 |
36 | type ConnectionConfig = {
37 | readonly name?: string | null;
38 | readonly nodeType: GraphQLObjectType;
39 | readonly resolveNode?: GraphQLFieldResolver | null;
40 | readonly resolveCursor?: GraphQLFieldResolver | null;
41 | readonly edgeFields?: any | null;
42 | readonly connectionFields?: any | null;
43 | };
44 |
45 | export type GraphQLConnectionDefinitions = {
46 | readonly edgeType: GraphQLObjectType;
47 | readonly connectionType: GraphQLObjectType;
48 | };
49 |
50 | const pageInfoType = new GraphQLObjectType({
51 | name: 'PageInfoExtended',
52 | description: 'Information about pagination in a connection.',
53 | fields: () => ({
54 | hasNextPage: {
55 | type: new GraphQLNonNull(GraphQLBoolean),
56 | description: 'When paginating forwards, are there more items?',
57 | },
58 | hasPreviousPage: {
59 | type: new GraphQLNonNull(GraphQLBoolean),
60 | description: 'When paginating backwards, are there more items?',
61 | },
62 | startCursor: {
63 | type: GraphQLString,
64 | description: 'When paginating backwards, the cursor to continue.',
65 | },
66 | endCursor: {
67 | type: GraphQLString,
68 | description: 'When paginating forwards, the cursor to continue.',
69 | },
70 | }),
71 | });
72 |
73 | function resolveMaybeThunk(thingOrThunk: any): T {
74 | return typeof thingOrThunk === 'function'
75 | ? (thingOrThunk as () => T)()
76 | : thingOrThunk;
77 | }
78 |
79 | export function connectionDefinitions(
80 | config: ConnectionConfig
81 | ): GraphQLConnectionDefinitions {
82 | const { nodeType, resolveCursor, resolveNode } = config;
83 | const name = config.name || nodeType.name;
84 | const edgeFields = config.edgeFields || {};
85 | const connectionFields = config.connectionFields || {};
86 |
87 | const edgeType = new GraphQLObjectType({
88 | name: `${name}Edge`,
89 | description: 'An edge in a connection.',
90 | fields: () => ({
91 | node: {
92 | type: nodeType,
93 | resolve: resolveNode,
94 | description: 'The item at the end of the edge',
95 | },
96 | cursor: {
97 | type: new GraphQLNonNull(GraphQLString),
98 | resolve: resolveCursor,
99 | description: 'A cursor for use in pagination',
100 | },
101 | ...(resolveMaybeThunk(edgeFields) as any),
102 | }),
103 | });
104 |
105 | const connectionType = new GraphQLObjectType({
106 | name: `${name}Connection`,
107 | description: 'A connection to a list of items.',
108 | fields: () => ({
109 | count: {
110 | type: GraphQLInt,
111 | description: 'Number of items in this connection',
112 | },
113 | totalCount: {
114 | type: GraphQLInt,
115 | resolve: (connection) => connection.count,
116 | description: `A count of the total number of objects in this connection, ignoring pagination.
117 | This allows a client to fetch the first five objects by passing "5" as the
118 | argument to "first", then fetch the total count so it could display "5 of 83",
119 | for example.`,
120 | },
121 | startCursorOffset: {
122 | type: new GraphQLNonNull(GraphQLInt),
123 | description: 'Offset from start',
124 | },
125 | endCursorOffset: {
126 | type: new GraphQLNonNull(GraphQLInt),
127 | description: 'Offset till end',
128 | },
129 | pageInfo: {
130 | type: new GraphQLNonNull(pageInfoType),
131 | description: 'Information to aid in pagination.',
132 | },
133 | edges: {
134 | type: new GraphQLNonNull(new GraphQLList(edgeType)),
135 | description: 'A list of edges.',
136 | },
137 | ...(resolveMaybeThunk(connectionFields) as any),
138 | }),
139 | });
140 |
141 | return { edgeType, connectionType };
142 | }
143 |
--------------------------------------------------------------------------------
/packages/app/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { TouchableOpacity } from 'react-native';
3 |
4 | import { NavigationContainer } from '@react-navigation/native';
5 | import { createStackNavigator } from '@react-navigation/stack';
6 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
7 |
8 | import { AntDesign, Feather } from '@expo/vector-icons';
9 |
10 | import { Home } from './components/home/Home';
11 | import { CreateStore } from './components/home/CreateStore';
12 | import { Settings } from './components/settings/Settings';
13 | import { SignUp } from './components/auth/SignUp';
14 | import { SignIn } from './components/auth/SignIn';
15 | import { StoreDetails } from './components/storeDetails/StoreDetails';
16 | import { QrCode } from './components/qrCode/QrCode';
17 | import { ScanQrCode } from './components/qrCode/ScanQrCode';
18 | import { useAuth } from './core/auth/useAuth';
19 |
20 | const Stack = createStackNavigator();
21 | const Tab = createBottomTabNavigator();
22 |
23 | function BottomTab() {
24 | const { token, type } = useAuth();
25 |
26 | return (
27 |
28 | {
35 | return null;
36 | },
37 | tabBarIcon: ({ color }) => (
38 |
39 | ),
40 | }}
41 | />
42 | {token && type === 'customer' && (
43 | {
48 | return null;
49 | },
50 | tabBarIcon: ({ color }) => (
51 |
52 | ),
53 | }}
54 | listeners={({ navigation }) => ({
55 | tabPress: e => {
56 | e.preventDefault();
57 | return navigation.navigate('QrCode');
58 | },
59 | })}
60 | />
61 | )}
62 | {token && type === 'provider' && (
63 | {
68 | return null;
69 | },
70 | tabBarIcon: ({ color }) => (
71 |
72 | ),
73 | }}
74 | listeners={({ navigation }) => ({
75 | tabPress: e => {
76 | e.preventDefault();
77 | return navigation.navigate('ScanQrCode');
78 | },
79 | })}
80 | />
81 | )}
82 | {
88 | return null;
89 | },
90 | tabBarIcon: ({ color }) => (
91 |
92 | ),
93 | }}
94 | />
95 |
96 | );
97 | }
98 |
99 | export function Routes() {
100 | return (
101 |
102 |
103 |
110 |
111 | ({
115 | headerTitle: 'Criar sua conta',
116 | // headerBackTitleVisible: false,
117 | headerLeft: () => null,
118 | headerRight: () => (
119 | navigation.goBack()}
121 | style={{ padding: 8, marginRight: 8 }}
122 | >
123 |
124 |
125 | ),
126 | })}
127 | />
128 | ({
132 | headerTitle: 'Faça o login',
133 | // headerBackTitleVisible: false,
134 | headerLeft: () => null,
135 | headerRight: () => (
136 | navigation.goBack()}
138 | style={{ padding: 8, marginRight: 8 }}
139 | >
140 |
141 |
142 | ),
143 | })}
144 | />
145 |
146 |
147 | ({
151 | headerTitle: 'Seu QRCode',
152 | headerLeft: () => null,
153 | headerRight: () => (
154 | navigation.goBack()}
156 | style={{ padding: 8, marginRight: 8 }}
157 | >
158 |
159 |
160 | ),
161 | })}
162 | />
163 | ({
167 | headerTitle: 'Aponte para um QRCode',
168 | headerLeft: () => null,
169 | headerRight: () => (
170 | navigation.goBack()}
172 | style={{ padding: 8, marginRight: 8 }}
173 | >
174 |
175 |
176 | ),
177 | })}
178 | />
179 |
180 |
181 | ({
185 | headerTitle: 'Cadastre seu Estabelecimento',
186 | headerLeft: () => null,
187 | })}
188 | />
189 |
190 | ({
194 | title: route.params.title,
195 | headerBackTitleVisible: false,
196 | })}
197 | />
198 |
199 |
200 | );
201 | }
202 |
--------------------------------------------------------------------------------
/packages/server/schema/schema.graphql:
--------------------------------------------------------------------------------
1 | """The root of all queries"""
2 | type Query {
3 | """Fetches an object given its ID"""
4 | node(
5 | """The ID of an object"""
6 | id: ID!
7 | ): Node
8 |
9 | """Fetches objects given their IDs"""
10 | nodes(
11 | """The IDs of objects"""
12 | ids: [ID!]!
13 | ): [Node]!
14 | version: String
15 | me: User
16 | users(after: String, first: Int, before: String, last: Int): UserConnection!
17 | stores(after: String, first: Int, before: String, last: Int): StoreConnection!
18 | storeByStoreId(id: ID!): Store
19 | productsByStoreId(storeId: ID!): ProductConnection!
20 | userPoints(after: String, first: Int, before: String, last: Int): UserPointsConnection!
21 | userPointsByStoreIdAndUserId(storeId: ID!, userId: ID): UserPointsConnection!
22 | userStore(after: String, first: Int, before: String, last: Int): UserStoreConnection!
23 | userStoreByUserId(userId: ID): UserStoreConnection!
24 | }
25 |
26 | """An object with an ID"""
27 | interface Node {
28 | """The id of the object."""
29 | id: ID!
30 | }
31 |
32 | """User data"""
33 | type User implements Node {
34 | """The ID of an object"""
35 | id: ID!
36 |
37 | """mongoose _id"""
38 | _id: String!
39 | name: String
40 | username: String
41 | email: String
42 | type: String
43 | }
44 |
45 | """A connection to a list of items."""
46 | type UserConnection {
47 | """Number of items in this connection"""
48 | count: Int
49 |
50 | """
51 | A count of the total number of objects in this connection, ignoring pagination.
52 | This allows a client to fetch the first five objects by passing "5" as the
53 | argument to "first", then fetch the total count so it could display "5 of 83",
54 | for example.
55 | """
56 | totalCount: Int
57 |
58 | """Offset from start"""
59 | startCursorOffset: Int!
60 |
61 | """Offset till end"""
62 | endCursorOffset: Int!
63 |
64 | """Information to aid in pagination."""
65 | pageInfo: PageInfoExtended!
66 |
67 | """A list of edges."""
68 | edges: [UserEdge]!
69 | }
70 |
71 | """Information about pagination in a connection."""
72 | type PageInfoExtended {
73 | """When paginating forwards, are there more items?"""
74 | hasNextPage: Boolean!
75 |
76 | """When paginating backwards, are there more items?"""
77 | hasPreviousPage: Boolean!
78 |
79 | """When paginating backwards, the cursor to continue."""
80 | startCursor: String
81 |
82 | """When paginating forwards, the cursor to continue."""
83 | endCursor: String
84 | }
85 |
86 | """An edge in a connection."""
87 | type UserEdge {
88 | """The item at the end of the edge"""
89 | node: User
90 |
91 | """A cursor for use in pagination"""
92 | cursor: String!
93 | }
94 |
95 | """A connection to a list of items."""
96 | type StoreConnection {
97 | """Number of items in this connection"""
98 | count: Int
99 |
100 | """
101 | A count of the total number of objects in this connection, ignoring pagination.
102 | This allows a client to fetch the first five objects by passing "5" as the
103 | argument to "first", then fetch the total count so it could display "5 of 83",
104 | for example.
105 | """
106 | totalCount: Int
107 |
108 | """Offset from start"""
109 | startCursorOffset: Int!
110 |
111 | """Offset till end"""
112 | endCursorOffset: Int!
113 |
114 | """Information to aid in pagination."""
115 | pageInfo: PageInfoExtended!
116 |
117 | """A list of edges."""
118 | edges: [StoreEdge]!
119 | }
120 |
121 | """An edge in a connection."""
122 | type StoreEdge {
123 | """The item at the end of the edge"""
124 | node: Store
125 |
126 | """A cursor for use in pagination"""
127 | cursor: String!
128 | }
129 |
130 | """Store data"""
131 | type Store implements Node {
132 | """The ID of an object"""
133 | id: ID!
134 |
135 | """mongoose _id"""
136 | _id: String!
137 | name: String
138 | description: String
139 | pictureUrl: String
140 | products: ProductConnection!
141 | }
142 |
143 | """A connection to a list of items."""
144 | type ProductConnection {
145 | """Number of items in this connection"""
146 | count: Int
147 |
148 | """
149 | A count of the total number of objects in this connection, ignoring pagination.
150 | This allows a client to fetch the first five objects by passing "5" as the
151 | argument to "first", then fetch the total count so it could display "5 of 83",
152 | for example.
153 | """
154 | totalCount: Int
155 |
156 | """Offset from start"""
157 | startCursorOffset: Int!
158 |
159 | """Offset till end"""
160 | endCursorOffset: Int!
161 |
162 | """Information to aid in pagination."""
163 | pageInfo: PageInfoExtended!
164 |
165 | """A list of edges."""
166 | edges: [ProductEdge]!
167 | }
168 |
169 | """An edge in a connection."""
170 | type ProductEdge {
171 | """The item at the end of the edge"""
172 | node: Product
173 |
174 | """A cursor for use in pagination"""
175 | cursor: String!
176 | }
177 |
178 | """Product data"""
179 | type Product implements Node {
180 | """The ID of an object"""
181 | id: ID!
182 |
183 | """mongoose _id"""
184 | _id: String!
185 | name: String
186 | description: String
187 | pictureUrl: String
188 | points: Int
189 | storeId: ID
190 | }
191 |
192 | """A connection to a list of items."""
193 | type UserPointsConnection {
194 | """Number of items in this connection"""
195 | count: Int
196 |
197 | """
198 | A count of the total number of objects in this connection, ignoring pagination.
199 | This allows a client to fetch the first five objects by passing "5" as the
200 | argument to "first", then fetch the total count so it could display "5 of 83",
201 | for example.
202 | """
203 | totalCount: Int
204 |
205 | """Offset from start"""
206 | startCursorOffset: Int!
207 |
208 | """Offset till end"""
209 | endCursorOffset: Int!
210 |
211 | """Information to aid in pagination."""
212 | pageInfo: PageInfoExtended!
213 |
214 | """A list of edges."""
215 | edges: [UserPointsEdge]!
216 | }
217 |
218 | """An edge in a connection."""
219 | type UserPointsEdge {
220 | """The item at the end of the edge"""
221 | node: UserPoints
222 |
223 | """A cursor for use in pagination"""
224 | cursor: String!
225 | }
226 |
227 | """UserPoints data"""
228 | type UserPoints implements Node {
229 | """The ID of an object"""
230 | id: ID!
231 |
232 | """mongoose _id"""
233 | _id: String!
234 | points: Int
235 | storeId: ID
236 | userId: ID
237 | }
238 |
239 | """A connection to a list of items."""
240 | type UserStoreConnection {
241 | """Number of items in this connection"""
242 | count: Int
243 |
244 | """
245 | A count of the total number of objects in this connection, ignoring pagination.
246 | This allows a client to fetch the first five objects by passing "5" as the
247 | argument to "first", then fetch the total count so it could display "5 of 83",
248 | for example.
249 | """
250 | totalCount: Int
251 |
252 | """Offset from start"""
253 | startCursorOffset: Int!
254 |
255 | """Offset till end"""
256 | endCursorOffset: Int!
257 |
258 | """Information to aid in pagination."""
259 | pageInfo: PageInfoExtended!
260 |
261 | """A list of edges."""
262 | edges: [UserStoreEdge]!
263 | }
264 |
265 | """An edge in a connection."""
266 | type UserStoreEdge {
267 | """The item at the end of the edge"""
268 | node: UserStore
269 |
270 | """A cursor for use in pagination"""
271 | cursor: String!
272 | }
273 |
274 | """UserStore data"""
275 | type UserStore implements Node {
276 | """The ID of an object"""
277 | id: ID!
278 |
279 | """mongoose _id"""
280 | _id: String!
281 | storeId: ID
282 | userId: ID
283 | store: Store
284 | }
285 |
286 | """Root of ... mutations"""
287 | type Mutation {
288 | UserLoginWithEmail(input: UserLoginWithEmailInput!): UserLoginWithEmailPayload
289 | UserRegisterWithEmail(input: UserRegisterWithEmailInput!): UserRegisterWithEmailPayload
290 | StoreCreate(input: StoreCreateInput!): StoreCreatePayload
291 | ProductCreate(input: ProductCreateInput!): ProductCreatePayload
292 | UserPointsCreateOrUpdate(input: UserPointsCreateOrUpdateInput!): UserPointsCreateOrUpdatePayload
293 | UserStoreCreate(input: UserStoreCreateInput!): UserStoreCreatePayload
294 | }
295 |
296 | type UserLoginWithEmailPayload {
297 | token: String
298 | me: User
299 | error: String
300 | success: String
301 | clientMutationId: String
302 | }
303 |
304 | input UserLoginWithEmailInput {
305 | email: String!
306 | password: String!
307 | clientMutationId: String
308 | }
309 |
310 | type UserRegisterWithEmailPayload {
311 | token: String
312 | me: User
313 | error: String
314 | success: String
315 | clientMutationId: String
316 | }
317 |
318 | input UserRegisterWithEmailInput {
319 | name: String!
320 | email: String!
321 | password: String!
322 | type: String!
323 | clientMutationId: String
324 | }
325 |
326 | type StoreCreatePayload {
327 | store: Store
328 | error: String
329 | success: String
330 | clientMutationId: String
331 | }
332 |
333 | input StoreCreateInput {
334 | name: String!
335 | description: String!
336 | pictureUrl: String!
337 | clientMutationId: String
338 | }
339 |
340 | type ProductCreatePayload {
341 | product: Product
342 | error: String
343 | success: String
344 | clientMutationId: String
345 | }
346 |
347 | input ProductCreateInput {
348 | name: String!
349 | description: String!
350 | pictureUrl: String!
351 | points: Int!
352 | storeId: ID!
353 | clientMutationId: String
354 | }
355 |
356 | type UserPointsCreateOrUpdatePayload {
357 | userPoints: UserPoints
358 | error: String
359 | success: String
360 | clientMutationId: String
361 | }
362 |
363 | input UserPointsCreateOrUpdateInput {
364 | points: Int!
365 | storeId: ID!
366 | userId: ID!
367 | clientMutationId: String
368 | }
369 |
370 | type UserStoreCreatePayload {
371 | userStore: UserStore
372 | error: String
373 | success: String
374 | clientMutationId: String
375 | }
376 |
377 | input UserStoreCreateInput {
378 | storeId: ID!
379 | userId: ID!
380 | clientMutationId: String
381 | }
382 |
--------------------------------------------------------------------------------