├── .buckconfig
├── .editorconfig
├── .eslintrc.js
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── custom.md
│ └── feature_request.md
└── screenshots
│ ├── dark
│ ├── 1.jpg
│ ├── 2.jpg
│ ├── 3.jpg
│ ├── 4.jpg
│ └── 5.jpg
│ └── light
│ ├── 1.jpg
│ ├── 2.jpg
│ ├── 3.jpg
│ ├── 4.jpg
│ ├── 5.jpg
│ ├── 6.jpg
│ ├── 7.jpg
│ └── 8.jpg
├── .gitignore
├── .huskyrc
├── .lintstagedrc
├── .prettierrc.js
├── .watchmanconfig
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── __mocks__
└── react-native-localize.js
├── __tests__
└── App-test.tsx
├── android
├── app
│ ├── _BUCK
│ ├── build.gradle
│ ├── build_defs.bzl
│ ├── debug.keystore
│ ├── proguard-rules.pro
│ └── src
│ │ ├── debug
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── rnmagentographql
│ │ │ └── ReactNativeFlipper.java
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── rnmagentographql
│ │ │ ├── MainActivity.java
│ │ │ └── MainApplication.java
│ │ └── res
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── strings.xml
│ │ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
├── app.json
├── babel.config.js
├── index.js
├── ios
├── Podfile
├── RNMagentoGraphQL-tvOS
│ └── Info.plist
├── RNMagentoGraphQL-tvOSTests
│ └── Info.plist
├── RNMagentoGraphQL.xcodeproj
│ ├── project.pbxproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── RNMagentoGraphQL-tvOS.xcscheme
│ │ └── RNMagentoGraphQL.xcscheme
├── RNMagentoGraphQL
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Images.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Info.plist
│ ├── LaunchScreen.storyboard
│ └── main.m
└── RNMagentoGraphQLTests
│ ├── Info.plist
│ └── RNMagentoGraphQLTests.m
├── jest-setup.js
├── magento.config.ts
├── metro.config.js
├── package.json
├── scripts
└── generatePossibleTypes.js
├── src
├── App.tsx
├── apollo
│ ├── client.ts
│ ├── data
│ │ └── possibleTypes.json
│ ├── mutations
│ │ ├── addProductsToCart.ts
│ │ ├── createCustomer.ts
│ │ └── createCustomerToken.ts
│ └── queries
│ │ ├── basicCartFragment.ts
│ │ ├── configurableProductFragment.ts
│ │ ├── getCart.ts
│ │ ├── getCategories.ts
│ │ ├── getCategoryProducts.ts
│ │ ├── getCustomer.ts
│ │ ├── getProductDetails.ts
│ │ ├── getSearchProducts.ts
│ │ ├── isLoggedIn.ts
│ │ ├── mediaGalleryFragment.ts
│ │ ├── productPriceFragment.ts
│ │ └── productsFragment.ts
├── components
│ ├── CategoryListItem
│ │ └── CategoryListItem.tsx
│ ├── CustomHeaderButtons
│ │ └── CustomHeaderButtons.tsx
│ ├── GenericTemplate
│ │ └── GenericTemplate.tsx
│ ├── MediaGallery
│ │ └── MediaGallery.tsx
│ ├── ProductListItem
│ │ └── ProductListItem.tsx
│ ├── Spinner
│ │ └── Spinner.tsx
│ └── index.ts
├── constants
│ ├── dimens.ts
│ ├── index.ts
│ ├── limits.ts
│ └── spacing.ts
├── i18n
│ ├── __tests__
│ │ └── i18n.test.ts
│ ├── i18n.ts
│ ├── index.ts
│ ├── locales
│ │ ├── en.json
│ │ └── es.json
│ └── translate.ts
├── logic
│ ├── app
│ │ ├── __tests__
│ │ │ └── useForm.test.ts
│ │ └── useForm.ts
│ ├── auth
│ │ ├── __tests__
│ │ │ ├── useLogin.test.tsx
│ │ │ ├── useLogout.test.ts
│ │ │ └── useSignup.test.tsx
│ │ ├── useLogin.ts
│ │ ├── useLogout.ts
│ │ └── useSignup.ts
│ ├── cart
│ │ └── useCart.ts
│ ├── categories
│ │ ├── __tests__
│ │ │ └── useCategories.test.tsx
│ │ └── useCategories.ts
│ ├── index.ts
│ ├── products
│ │ ├── __tests__
│ │ │ └── useProductDetails.test.tsx
│ │ ├── useCategoryProducts.ts
│ │ ├── useProductDetails.ts
│ │ ├── useSearch.ts
│ │ └── useSort.ts
│ ├── profile
│ │ ├── __tests__
│ │ │ └── useCustomer.test.tsx
│ │ └── useCustomer.ts
│ └── utils
│ │ ├── __tests__
│ │ ├── cartHelpers.test.ts
│ │ ├── price.test.ts
│ │ └── storage.test.ts
│ │ ├── cartHelpers.ts
│ │ ├── loginPrompt.ts
│ │ ├── price.ts
│ │ └── storage.ts
├── navigation
│ ├── AuthenticationNavigator.tsx
│ ├── BottomTabNavigator.tsx
│ ├── RootNavigator.tsx
│ ├── StackNavigator.tsx
│ ├── index.ts
│ └── routeNames.ts
├── screens
│ ├── CartScreen
│ │ ├── CartFooter.tsx
│ │ ├── CartListItem.tsx
│ │ └── CartScreen.tsx
│ ├── CategoriesScreen
│ │ └── CategoriesScreen.tsx
│ ├── DrawerScreen
│ │ └── DrawerScreen.tsx
│ ├── HomeScreen
│ │ ├── FeaturedProductList.tsx
│ │ └── HomeScreen.tsx
│ ├── LoginScreen
│ │ └── LoginScreen.tsx
│ ├── ProductDetailsScreen
│ │ ├── ConfigurableOptionValues.tsx
│ │ ├── ConfigurableProductOptions.tsx
│ │ └── ProductDetailsScreen.tsx
│ ├── ProductListScreen
│ │ └── ProductListScreen.tsx
│ ├── ProfileScreen
│ │ └── ProfileScreen.tsx
│ ├── SearchScreen
│ │ └── SearchScreen.tsx
│ ├── SignupScreen
│ │ └── SignupScreen.tsx
│ └── index.ts
└── theme
│ ├── colors.ts
│ ├── darkTheme.ts
│ ├── index.ts
│ ├── lightTheme.ts
│ └── typography.ts
└── tsconfig.json
/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | jest: true,
6 | },
7 | root: true,
8 | extends: [
9 | '@react-native-community',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'eslint-config-prettier',
12 | ],
13 | parser: '@typescript-eslint/parser',
14 | plugins: ['@typescript-eslint'],
15 | rules: {
16 | 'react-hooks/exhaustive-deps': 'warn',
17 | '@typescript-eslint/explicit-function-return-type': 'off',
18 | '@typescript-eslint/no-use-before-define': 'off',
19 | 'no-unused-vars': 'error',
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: sanjeevyadavit
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Expected behavior**
14 | A clear and concise description of what you expected to happen.
15 |
16 | **Current Behavior**
17 | What is the current behavior?
18 |
19 | **Steps to Reproduce**
20 | Please provide detailed steps for reproducing the issue.
21 | 1. Navigate to...
22 | 2. Press on...
23 | 3. Scroll to...
24 | 4. See error...
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Environment**
30 | - React native Version: [e.g. 0.59.8]
31 | - React Version: [e.g. 16.8.3]
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Version [e.g. 22]
35 |
36 | **Additional context**
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom Issue Template
3 | about: Tell us something related to the project or general discussion
4 | title: ''
5 | labels: question
6 | assignees: sanjeevyadavit
7 |
8 | ---
9 |
10 | **Are there certain things to report that are not a bug or feature?**
11 | Please tell us as exactly as possible about your request, thanks.
12 | We will reply as soon as possible.
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this starter code
4 | title: ''
5 | labels: enhancement
6 | assignees: sanjeevyadavit
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/screenshots/dark/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/dark/1.jpg
--------------------------------------------------------------------------------
/.github/screenshots/dark/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/dark/2.jpg
--------------------------------------------------------------------------------
/.github/screenshots/dark/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/dark/3.jpg
--------------------------------------------------------------------------------
/.github/screenshots/dark/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/dark/4.jpg
--------------------------------------------------------------------------------
/.github/screenshots/dark/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/dark/5.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/1.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/2.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/3.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/4.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/5.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/6.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/7.jpg
--------------------------------------------------------------------------------
/.github/screenshots/light/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/.github/screenshots/light/8.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 |
24 | # Android/IntelliJ
25 | #
26 | build/
27 | .idea
28 | .gradle
29 | local.properties
30 | *.iml
31 |
32 | # Visual Studio Code
33 | #
34 | .vscode/
35 |
36 | # node.js
37 | #
38 | node_modules/
39 | *.log
40 | .nvm
41 | package-lock.json
42 | yarn.lock
43 | npm-debug.log
44 | yarn-error.log
45 |
46 | # BUCK
47 | buck-out/
48 | \.buckd/
49 | *.keystore
50 | !debug.keystore
51 |
52 | # fastlane
53 | #
54 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
55 | # screenshots whenever they are needed.
56 | # For more information about the recommended setup visit:
57 | # https://docs.fastlane.tools/best-practices/source-control/
58 |
59 | */fastlane/report.xml
60 | */fastlane/Preview.html
61 | */fastlane/screenshots
62 |
63 | # Bundle artifact
64 | *.jsbundle
65 |
66 | # CocoaPods
67 | /ios/Pods/
68 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "lint-staged && npm run validate"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "**/*.+(js|json|jsx|ts|tsx)": [
3 | "prettier --write",
4 | "git add"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: true,
3 | jsxBracketSameLine: false,
4 | singleQuote: true,
5 | trailingComma: 'all',
6 | arrowParens: 'avoid',
7 | };
8 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2020] [Sanjeev yadav]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native eCommerce App for Magento GraphQL api
2 |
3 | 
4 | 
5 | [](https://github.com/sanjeevyadavit/magento_react_native_graphql/issues)
6 | 
7 | 
8 |
9 |
10 | E-Commerce App written in React Native which consumes [Magento 2 GraphQL api](https://devdocs.magento.com/guides/v2.4/graphql/) to display catalog, products, add products to cart
11 |
12 | ## :camera: Screenshots
13 |
14 |
15 |

16 |

17 |

18 |

19 |

20 |
21 |
22 |
27 |
28 | ### (Dark mode)
29 |
30 |
31 |

32 |

33 |

34 |

35 |

36 |
37 |
38 | ## 🚀 Getting Started:
39 |
40 | 1. Clone the repository, by tying this command in terminal
41 |
42 | ```sh
43 | git clone https://github.com/sanjeevyadavit/magento_react_native_graphql.git && cd magento_react_native_graphql
44 |
45 | 2. Install the dependencies
46 |
47 | ```bash
48 | yarn install
49 | ```
50 |
51 | ### For Android
52 |
53 | Run the following command while the emulator is open or a device is connected via adb.
54 |
55 | ```
56 | yarn android
57 | ```
58 |
59 | ### For iOS
60 |
61 | Run the following commands to install pods and run the app on iPhone simulator
62 |
63 | ```
64 | cd ios && pod install && cd ..
65 | yarn ios
66 | ```
67 |
68 | ## 🙋 Contribute [](http://makeapullrequest.com)
69 |
70 | If you find a bug, or if you have an idea for this app, please file an issue [here](https://github.com/sanjeevyadavit/magento_react_native_graphql/issues). We really appreciate feedback and inputs!
71 |
72 | More information on contributing, head over to our [contribution guidelines](CONTRIBUTING.md).
73 |
74 | ## 🗃️ Similar project
75 |
76 | * [magento_react_native](https://github.com/sanjeevyadavit/magento_react_native) - (REST api)Complete eCommerce app written in React Native for magento 2 using best practices
77 |
78 | * [magento-react-native-community](https://github.com/troublediehard/magento-react-native-community) - Original project on which this project is based
79 |
80 | ## ♥️ Donate
81 |
82 | If this project help you, or to help acclerate development, you can give me a cup of coffee :smile: :
83 |
84 |
85 |
86 | ## 🛡 License
87 |
88 | This project is licensed under the GNU v3 Public License License - see the [LICENSE.md](LICENSE.md) file for details.
89 |
--------------------------------------------------------------------------------
/__mocks__/react-native-localize.js:
--------------------------------------------------------------------------------
1 | const getLocales = jest.fn(() => [
2 | // you can choose / add the locales you want
3 | { countryCode: 'US', languageTag: 'en-US', languageCode: 'en', isRTL: false },
4 | { countryCode: 'FR', languageTag: 'fr-FR', languageCode: 'fr', isRTL: false },
5 | ]);
6 |
7 | // use a provided translation, or return undefined to test your fallback
8 | const findBestAvailableLanguage = jest.fn(() => ({
9 | languageTag: 'en',
10 | isRTL: false,
11 | }));
12 |
13 | const getNumberFormatSettings = () => ({
14 | decimalSeparator: '.',
15 | groupingSeparator: ',',
16 | });
17 |
18 | const getCalendar = () => 'gregorian'; // or "japanese", "buddhist"
19 | const getCountry = () => 'US'; // the country code you want
20 | const getCurrencies = () => ['USD', 'EUR']; // can be empty array
21 | const getTemperatureUnit = () => 'celsius'; // or "fahrenheit"
22 | const getTimeZone = () => 'Europe/Paris'; // the timezone you want
23 | const uses24HourClock = () => true;
24 | const usesMetricSystem = () => true;
25 |
26 | const addEventListener = jest.fn();
27 | const removeEventListener = jest.fn();
28 |
29 | export {
30 | findBestAvailableLanguage,
31 | getLocales,
32 | getNumberFormatSettings,
33 | getCalendar,
34 | getCountry,
35 | getCurrencies,
36 | getTemperatureUnit,
37 | getTimeZone,
38 | uses24HourClock,
39 | usesMetricSystem,
40 | addEventListener,
41 | removeEventListener,
42 | };
43 |
--------------------------------------------------------------------------------
/__tests__/App-test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import 'react-native';
6 | import React from 'react';
7 | import { MockedProvider } from '@apollo/client/testing';
8 | import { ThemeProvider } from 'react-native-elements';
9 | import { OverflowMenuProvider } from 'react-navigation-header-buttons';
10 | import Navigator from '../src/navigation';
11 | import { GET_CATEGORIES } from '../src/apollo/queries/getCategories';
12 |
13 | // Note: test renderer must be required after react-native.
14 | import renderer from 'react-test-renderer';
15 |
16 | jest.useFakeTimers();
17 |
18 | const mocks = [
19 | {
20 | request: {
21 | query: GET_CATEGORIES,
22 | variables: {
23 | id: 2,
24 | },
25 | },
26 | result: {
27 | data: {
28 | categoryList: {
29 | id: '2',
30 | children: [],
31 | },
32 | },
33 | },
34 | },
35 | ];
36 |
37 | it('renders correctly', () => {
38 | renderer.create(
39 |
40 |
41 |
42 |
43 |
44 |
45 | ,
46 | );
47 | });
48 |
--------------------------------------------------------------------------------
/android/app/_BUCK:
--------------------------------------------------------------------------------
1 | # To learn about Buck see [Docs](https://buckbuild.com/).
2 | # To run your application with Buck:
3 | # - install Buck
4 | # - `npm start` - to start the packager
5 | # - `cd android`
6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
8 | # - `buck install -r android/app` - compile, install and run application
9 | #
10 |
11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
12 |
13 | lib_deps = []
14 |
15 | create_aar_targets(glob(["libs/*.aar"]))
16 |
17 | create_jar_targets(glob(["libs/*.jar"]))
18 |
19 | android_library(
20 | name = "all-libs",
21 | exported_deps = lib_deps,
22 | )
23 |
24 | android_library(
25 | name = "app-code",
26 | srcs = glob([
27 | "src/main/java/**/*.java",
28 | ]),
29 | deps = [
30 | ":all-libs",
31 | ":build_config",
32 | ":res",
33 | ],
34 | )
35 |
36 | android_build_config(
37 | name = "build_config",
38 | package = "com.rnmagentographql",
39 | )
40 |
41 | android_resource(
42 | name = "res",
43 | package = "com.rnmagentographql",
44 | res = "src/main/res",
45 | )
46 |
47 | android_binary(
48 | name = "app",
49 | keystore = "//android/keystores:debug",
50 | manifest = "src/main/AndroidManifest.xml",
51 | package_type = "debug",
52 | deps = [
53 | ":app-code",
54 | ],
55 | )
56 |
--------------------------------------------------------------------------------
/android/app/build_defs.bzl:
--------------------------------------------------------------------------------
1 | """Helper definitions to glob .aar and .jar targets"""
2 |
3 | def create_aar_targets(aarfiles):
4 | for aarfile in aarfiles:
5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
6 | lib_deps.append(":" + name)
7 | android_prebuilt_aar(
8 | name = name,
9 | aar = aarfile,
10 | )
11 |
12 | def create_jar_targets(jarfiles):
13 | for jarfile in jarfiles:
14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
15 | lib_deps.append(":" + name)
16 | prebuilt_jar(
17 | name = name,
18 | binary_jar = jarfile,
19 | )
20 |
--------------------------------------------------------------------------------
/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/debug.keystore
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/debug/java/com/rnmagentographql/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.rnmagentographql;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | public class ReactNativeFlipper {
28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
29 | if (FlipperUtils.shouldEnableFlipper(context)) {
30 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
31 |
32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
33 | client.addPlugin(new ReactFlipperPlugin());
34 | client.addPlugin(new DatabasesFlipperPlugin(context));
35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
36 | client.addPlugin(CrashReporterPlugin.getInstance());
37 |
38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
39 | NetworkingModule.setCustomClientBuilder(
40 | new NetworkingModule.CustomClientBuilder() {
41 | @Override
42 | public void apply(OkHttpClient.Builder builder) {
43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
44 | }
45 | });
46 | client.addPlugin(networkFlipperPlugin);
47 | client.start();
48 |
49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
50 | // Hence we run if after all native modules have been initialized
51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
52 | if (reactContext == null) {
53 | reactInstanceManager.addReactInstanceEventListener(
54 | new ReactInstanceManager.ReactInstanceEventListener() {
55 | @Override
56 | public void onReactContextInitialized(ReactContext reactContext) {
57 | reactInstanceManager.removeReactInstanceEventListener(this);
58 | reactContext.runOnNativeModulesQueueThread(
59 | new Runnable() {
60 | @Override
61 | public void run() {
62 | client.addPlugin(new FrescoFlipperPlugin());
63 | }
64 | });
65 | }
66 | });
67 | } else {
68 | client.addPlugin(new FrescoFlipperPlugin());
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rnmagentographql/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.rnmagentographql;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import android.content.res.Configuration;
5 |
6 | public class MainActivity extends ReactActivity {
7 |
8 | /**
9 | * Returns the name of the main component registered from JavaScript. This is used to schedule
10 | * rendering of the component.
11 | */
12 | @Override
13 | protected String getMainComponentName() {
14 | return "RNMagentoGraphQL";
15 | }
16 |
17 | @Override
18 | public void onConfigurationChanged(Configuration newConfig) {
19 | super.onConfigurationChanged(newConfig);
20 | getReactInstanceManager().onConfigurationChanged(this, newConfig);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rnmagentographql/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.rnmagentographql;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import com.facebook.react.PackageList;
6 | import com.facebook.react.ReactApplication;
7 | import com.facebook.react.ReactInstanceManager;
8 | import com.facebook.react.ReactNativeHost;
9 | import com.facebook.react.ReactPackage;
10 | import com.facebook.soloader.SoLoader;
11 | import java.lang.reflect.InvocationTargetException;
12 | import java.util.List;
13 |
14 | public class MainApplication extends Application implements ReactApplication {
15 |
16 | private final ReactNativeHost mReactNativeHost =
17 | new ReactNativeHost(this) {
18 | @Override
19 | public boolean getUseDeveloperSupport() {
20 | return BuildConfig.DEBUG;
21 | }
22 |
23 | @Override
24 | protected List getPackages() {
25 | @SuppressWarnings("UnnecessaryLocalVariable")
26 | List packages = new PackageList(this).getPackages();
27 | // Packages that cannot be autolinked yet can be added manually here, for example:
28 | // packages.add(new MyReactNativePackage());
29 | return packages;
30 | }
31 |
32 | @Override
33 | protected String getJSMainModuleName() {
34 | return "index";
35 | }
36 | };
37 |
38 | @Override
39 | public ReactNativeHost getReactNativeHost() {
40 | return mReactNativeHost;
41 | }
42 |
43 | @Override
44 | public void onCreate() {
45 | super.onCreate();
46 | SoLoader.init(this, /* native exopackage */ false);
47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
48 | }
49 |
50 | /**
51 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like
52 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
53 | *
54 | * @param context
55 | * @param reactInstanceManager
56 | */
57 | private static void initializeFlipper(
58 | Context context, ReactInstanceManager reactInstanceManager) {
59 | if (BuildConfig.DEBUG) {
60 | try {
61 | /*
62 | We use reflection here to pick up the class that initializes Flipper,
63 | since Flipper library is not available in release mode
64 | */
65 | Class> aClass = Class.forName("com.rnmagentographql.ReactNativeFlipper");
66 | aClass
67 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
68 | .invoke(null, context, reactInstanceManager);
69 | } catch (ClassNotFoundException e) {
70 | e.printStackTrace();
71 | } catch (NoSuchMethodException e) {
72 | e.printStackTrace();
73 | } catch (IllegalAccessException e) {
74 | e.printStackTrace();
75 | } catch (InvocationTargetException e) {
76 | e.printStackTrace();
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RNMagentoGraphQL
3 |
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "29.0.2"
6 | minSdkVersion = 16
7 | compileSdkVersion = 29
8 | targetSdkVersion = 29
9 | }
10 | repositories {
11 | google()
12 | jcenter()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle:3.4.2")
16 | // NOTE: Do not place your application dependencies here; they belong
17 | // in the individual module build.gradle files
18 | }
19 | }
20 |
21 | allprojects {
22 | repositories {
23 | mavenLocal()
24 | maven {
25 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
26 | url("$rootDir/../node_modules/react-native/android")
27 | }
28 | maven {
29 | // Android JSC is installed from npm
30 | url("$rootDir/../node_modules/jsc-android/dist")
31 | }
32 |
33 | google()
34 | jcenter()
35 | maven { url 'https://www.jitpack.io' }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.54.0
29 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanjeevyadavIT/magento_react_native_graphql/72b41d230dae7da9b8623cb53c8e2997d925bf00/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto init
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto init
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :init
68 | @rem Get command-line arguments, handling Windows variants
69 |
70 | if not "%OS%" == "Windows_NT" goto win9xME_args
71 |
72 | :win9xME_args
73 | @rem Slurp the command line arguments.
74 | set CMD_LINE_ARGS=
75 | set _SKIP=2
76 |
77 | :win9xME_args_slurp
78 | if "x%~1" == "x" goto execute
79 |
80 | set CMD_LINE_ARGS=%*
81 |
82 | :execute
83 | @rem Setup the command line
84 |
85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
86 |
87 | @rem Execute Gradle
88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
89 |
90 | :end
91 | @rem End local scope for the variables with windows NT shell
92 | if "%ERRORLEVEL%"=="0" goto mainEnd
93 |
94 | :fail
95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
96 | rem the _cmd.exe /c_ return code!
97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
98 | exit /b 1
99 |
100 | :mainEnd
101 | if "%OS%"=="Windows_NT" endlocal
102 |
103 | :omega
104 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'RNMagentoGraphQL'
2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
3 | include ':app'
4 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNMagentoGraphQL",
3 | "displayName": "RNMagentoGraphQL"
4 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import { AppRegistry } from 'react-native';
6 | import 'react-native-gesture-handler';
7 | import App from './src/App';
8 | import { name as appName } from './app.json';
9 |
10 | AppRegistry.registerComponent(appName, () => App);
11 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | require_relative '../node_modules/react-native/scripts/react_native_pods'
2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
3 |
4 | platform :ios, '10.0'
5 |
6 | target 'RNMagentoGraphQL' do
7 | config = use_native_modules!
8 |
9 | use_react_native!(:path => config["reactNativePath"])
10 |
11 | target 'RNMagentoGraphQLTests' do
12 | inherit! :complete
13 | # Pods for testing
14 | end
15 |
16 | # Enables Flipper.
17 | #
18 | # Note that if you have use_frameworks! enabled, Flipper will not work and
19 | # you should disable these next few lines.
20 | use_flipper!
21 | post_install do |installer|
22 | flipper_post_install(installer)
23 | end
24 | end
25 |
26 | target 'RNMagentoGraphQL-tvOS' do
27 | # Pods for RNMagentoGraphQL-tvOS
28 |
29 | target 'RNMagentoGraphQL-tvOSTests' do
30 | inherit! :search_paths
31 | # Pods for testing
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL-tvOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | NSAppTransportSecurity
26 |
27 | NSExceptionDomains
28 |
29 | localhost
30 |
31 | NSExceptionAllowsInsecureHTTPLoads
32 |
33 |
34 |
35 |
36 | NSLocationWhenInUseUsageDescription
37 |
38 | UILaunchStoryboardName
39 | LaunchScreen
40 | UIRequiredDeviceCapabilities
41 |
42 | armv7
43 |
44 | UISupportedInterfaceOrientations
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationLandscapeLeft
48 | UIInterfaceOrientationLandscapeRight
49 |
50 | UIViewControllerBasedStatusBarAppearance
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL-tvOSTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL.xcodeproj/xcshareddata/xcschemes/RNMagentoGraphQL-tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL.xcodeproj/xcshareddata/xcschemes/RNMagentoGraphQL.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : UIResponder
5 |
6 | @property (nonatomic, strong) UIWindow *window;
7 |
8 | @end
9 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL/AppDelegate.m:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 | #import
5 | #import
6 |
7 | #ifdef FB_SONARKIT_ENABLED
8 | #import
9 | #import
10 | #import
11 | #import
12 | #import
13 | #import
14 |
15 | static void InitializeFlipper(UIApplication *application) {
16 | FlipperClient *client = [FlipperClient sharedClient];
17 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
18 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
19 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
20 | [client addPlugin:[FlipperKitReactPlugin new]];
21 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
22 | [client start];
23 | }
24 | #endif
25 |
26 | @implementation AppDelegate
27 |
28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
29 | {
30 | #ifdef FB_SONARKIT_ENABLED
31 | InitializeFlipper(application);
32 | #endif
33 |
34 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
35 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
36 | moduleName:@"RNMagentoGraphQL"
37 | initialProperties:nil];
38 |
39 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
40 |
41 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
42 | UIViewController *rootViewController = [UIViewController new];
43 | rootViewController.view = rootView;
44 | self.window.rootViewController = rootViewController;
45 | [self.window makeKeyAndVisible];
46 | return YES;
47 | }
48 |
49 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
50 | {
51 | #if DEBUG
52 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
53 | #else
54 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
55 | #endif
56 | }
57 |
58 | @end
59 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | RNMagentoGraphQL
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 | NSExceptionDomains
32 |
33 | localhost
34 |
35 | NSExceptionAllowsInsecureHTTPLoads
36 |
37 |
38 |
39 |
40 | NSLocationWhenInUseUsageDescription
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | UIViewControllerBasedStatusBarAppearance
55 |
56 | UIAppFonts
57 |
58 | MaterialIcons.ttf
59 | MaterialCommunityIcons.ttf
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQL/main.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char * argv[]) {
6 | @autoreleasepool {
7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQLTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/ios/RNMagentoGraphQLTests/RNMagentoGraphQLTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface RNMagentoGraphQLTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation RNMagentoGraphQLTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
38 | if (level >= RCTLogLevelError) {
39 | redboxError = message;
40 | }
41 | });
42 | #endif
43 |
44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 |
48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
50 | return YES;
51 | }
52 | return NO;
53 | }];
54 | }
55 |
56 | #ifdef DEBUG
57 | RCTSetLogFunction(RCTDefaultLogFunction);
58 | #endif
59 |
60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
62 | }
63 |
64 |
65 | @end
66 |
--------------------------------------------------------------------------------
/jest-setup.js:
--------------------------------------------------------------------------------
1 | import 'react-native-gesture-handler/jestSetup';
2 | import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
3 |
4 | jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
5 |
6 | jest.mock('react-native-reanimated', () => {
7 | // eslint-disable-next-line @typescript-eslint/no-var-requires
8 | const Reanimated = require('react-native-reanimated/mock');
9 |
10 | // The mock for `call` immediately calls the callback which is incorrect
11 | // So we override it with a no-op
12 | Reanimated.default.call = () => {
13 | console.log('Mock');
14 | };
15 |
16 | return Reanimated;
17 | });
18 |
19 | // Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
20 | jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
21 |
--------------------------------------------------------------------------------
/magento.config.ts:
--------------------------------------------------------------------------------
1 | export const magentoConfig = {
2 | // Base url
3 | url: 'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/',
4 | // Id of the category that host first level categories
5 | baseCategoryId: '2',
6 | // Home Screen carousel images (Temp solution)
7 | homeCarousel: [
8 | {
9 | disabled: false,
10 | label: '',
11 | position: 0,
12 | url:
13 | 'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/media/shallow-focus-photography-of-man-wearing-eyeglasses-837306_sm.jpg?auto=webp&format=pjpg&quality=85',
14 | },
15 | {
16 | disabled: false,
17 | label: '',
18 | position: 1,
19 | url:
20 | 'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/media/venia-hero1.jpg?auto=webp&format=pjpg&quality=85',
21 | },
22 | {
23 | disabled: false,
24 | label: '',
25 | position: 2,
26 | url:
27 | 'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/media/woman-wearing-orange-dress-3503488_sm.jpg?auto=webp&format=pjpg&quality=85',
28 | },
29 | ],
30 | // featured Catgeory products to be shown on HomeScreen, add as many as you want
31 | homeFeaturedCategories: [
32 | {
33 | id: '12',
34 | name: 'Skirts',
35 | },
36 | { id: '11', name: 'Pants & Shorts' },
37 | ],
38 | };
39 |
40 | interface CurrencySymbols {
41 | [key: string]: string;
42 | }
43 |
44 | /**
45 | * Magento 2 REST API doesn't return currency symbol,
46 | * so manually specify all currency symbol(that your store support)
47 | * along side their currency code.
48 | */
49 | export const currencySymbols: CurrencySymbols = Object.freeze({
50 | USD: '$',
51 | EUR: '€',
52 | AUD: 'A$',
53 | GBP: '£',
54 | CAD: 'CA$',
55 | CNY: 'CN¥',
56 | JPY: '¥',
57 | SEK: 'SEK',
58 | CHF: 'CHF',
59 | INR: '₹',
60 | KWD: 'د.ك',
61 | RON: 'RON',
62 | });
63 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Metro configuration for React Native
3 | * https://github.com/facebook/react-native
4 | *
5 | * @format
6 | */
7 |
8 | module.exports = {
9 | transformer: {
10 | getTransformOptions: async () => ({
11 | transform: {
12 | experimentalImportSupport: false,
13 | inlineRequires: false,
14 | },
15 | }),
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNMagentoGraphQL",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "start": "react-native start",
9 | "test": "jest",
10 | "test:debug": "node --inspect node_modules/.bin/jest --runInBand",
11 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
12 | "check-types": "tsc",
13 | "prettier": "prettier --ignore-path .gitignore \"src/**/*.+(js|json|ts|tsx)\"",
14 | "format": "npm run prettier -- --write",
15 | "check-format": "npm run prettier -- --list-different",
16 | "validate": "npm-run-all --parallel check-format lint test",
17 | "generatePossibleTypes": "node ./scripts/generatePossibleTypes.js"
18 | },
19 | "dependencies": {
20 | "@apollo/client": "^3.3.6",
21 | "@react-native-async-storage/async-storage": "^1.13.2",
22 | "@react-native-community/masked-view": "^0.1.10",
23 | "@react-navigation/bottom-tabs": "^5.11.2",
24 | "@react-navigation/drawer": "^5.11.4",
25 | "@react-navigation/native": "^5.8.10",
26 | "@react-navigation/stack": "^5.12.8",
27 | "@types/i18n-js": "^3.0.3",
28 | "graphql": "^15.4.0",
29 | "i18n-js": "^3.8.0",
30 | "react": "16.13.1",
31 | "react-native": "0.63.3",
32 | "react-native-elements": "^3.0.1",
33 | "react-native-flash-message": "^0.1.22",
34 | "react-native-gesture-handler": "^1.9.0",
35 | "react-native-localize": "^2.0.1",
36 | "react-native-reanimated": "^1.13.2",
37 | "react-native-render-html": "^5.0.1",
38 | "react-native-safe-area-context": "^3.1.9",
39 | "react-native-screens": "^2.15.0",
40 | "react-native-size-matters": "^0.4.0",
41 | "react-native-vector-icons": "^7.1.0",
42 | "react-navigation-header-buttons": "^6.0.1"
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.8.4",
46 | "@babel/runtime": "^7.8.4",
47 | "@react-native-community/eslint-config": "^1.1.0",
48 | "@testing-library/react-hooks": "^5.0.3",
49 | "@types/jest": "^25.2.3",
50 | "@types/react-native": "^0.63.2",
51 | "@types/react-test-renderer": "^16.9.2",
52 | "@typescript-eslint/eslint-plugin": "^2.27.0",
53 | "@typescript-eslint/parser": "^2.27.0",
54 | "babel-jest": "^25.1.0",
55 | "eslint": "^6.5.1",
56 | "eslint-config-prettier": "^8.3.0",
57 | "eslint-plugin-prettier": "^3.4.0",
58 | "husky": "^4.3.5",
59 | "jest": "^25.1.0",
60 | "jest-transform-stub": "^2.0.0",
61 | "lint-staged": "^10.5.3",
62 | "metro-react-native-babel-preset": "^0.59.0",
63 | "npm-run-all": "^4.1.5",
64 | "prettier": "2.2.1",
65 | "react-test-renderer": "16.13.1",
66 | "typescript": "^3.8.3"
67 | },
68 | "jest": {
69 | "preset": "react-native",
70 | "testEnvironment": "jsdom",
71 | "transformIgnorePatterns": [
72 | "node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)"
73 | ],
74 | "setupFiles": [
75 | "./node_modules/react-native-gesture-handler/jestSetup.js",
76 | "./jest-setup.js"
77 | ],
78 | "moduleFileExtensions": [
79 | "ts",
80 | "tsx",
81 | "js",
82 | "jsx",
83 | "json",
84 | "node"
85 | ],
86 | "moduleNameMapper": {
87 | ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
88 | "\\.(css|less)$": "identity-obj-proxy"
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/scripts/generatePossibleTypes.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const fetch = require('cross-fetch');
3 | const fs = require('fs');
4 |
5 | fetch(`https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/graphql`, {
6 | method: 'POST',
7 | headers: { 'Content-Type': 'application/json' },
8 | body: JSON.stringify({
9 | variables: {},
10 | query: `
11 | {
12 | __schema {
13 | types {
14 | kind
15 | name
16 | possibleTypes {
17 | name
18 | }
19 | }
20 | }
21 | }
22 | `,
23 | }),
24 | })
25 | .then(result => result.json())
26 | .then(result => {
27 | const possibleTypes = {};
28 |
29 | result.data.__schema.types.forEach(supertype => {
30 | if (supertype.possibleTypes) {
31 | possibleTypes[supertype.name] = supertype.possibleTypes.map(
32 | subtype => subtype.name,
33 | );
34 | }
35 | });
36 |
37 | fs.writeFile(
38 | './src/apollo/data/possibleTypes.json',
39 | JSON.stringify(possibleTypes),
40 | err => {
41 | if (err) {
42 | console.error('Error writing possibleTypes.json', err);
43 | } else {
44 | console.log('Fragment types successfully extracted!');
45 | }
46 | },
47 | );
48 | });
49 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Appearance, ColorSchemeName, useColorScheme } from 'react-native';
3 | import { ThemeProvider } from 'react-native-elements';
4 | import FlashMessage from 'react-native-flash-message';
5 | import { SafeAreaProvider } from 'react-native-safe-area-context';
6 | import { OverflowMenuProvider } from 'react-navigation-header-buttons';
7 | import Navigator from './navigation';
8 | import { ApolloClient, ApolloProvider } from '@apollo/client';
9 | import { getApolloClient } from './apollo/client';
10 | import { lightTheme, darkTheme } from './theme';
11 | import { Spinner } from './components';
12 |
13 | const App = (): React.ReactElement => {
14 | const [client, setClient] = useState>();
15 | const colorScheme: ColorSchemeName = useColorScheme();
16 |
17 | useEffect(() => {
18 | getApolloClient()
19 | .then(setClient)
20 | .catch(e => console.log(e));
21 |
22 | const listener = ({
23 | colorScheme: newColorScheme,
24 | }: {
25 | colorScheme: ColorSchemeName;
26 | }) => {
27 | // do something when color scheme changes
28 | const theme = newColorScheme === 'dark' ? darkTheme : lightTheme;
29 | FlashMessage.setColorTheme({
30 | success: theme.colors.success,
31 | info: theme.colors.info,
32 | warning: theme.colors.warning,
33 | danger: theme.colors.error,
34 | });
35 | };
36 |
37 | Appearance.addChangeListener(listener);
38 |
39 | return () => Appearance.removeChangeListener(listener);
40 | }, []);
41 |
42 | if (client) {
43 | return (
44 |
45 |
46 |
50 | <>
51 |
52 |
53 |
54 |
55 | >
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | // TODO: SplashScreen logic
63 | return ;
64 | };
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/src/apollo/client.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
2 | import { setContext } from '@apollo/client/link/context';
3 | import { magentoConfig } from '../../magento.config';
4 | import { loadCustomerToken } from '../logic';
5 | import { IS_LOGGED_IN } from './queries/isLoggedIn';
6 | import possibleTypes from './data/possibleTypes.json';
7 |
8 | let _client: ApolloClient;
9 |
10 | export async function getApolloClient(): Promise> {
11 | if (_client) {
12 | return _client;
13 | }
14 |
15 | const cache = new InMemoryCache({
16 | possibleTypes,
17 | typePolicies: {
18 | Query: {
19 | fields: {
20 | products: {
21 | // Cache separate results based on
22 | // any of this field's arguments.
23 | keyArgs: ['search', 'filter'],
24 | // Concatenate the incoming list items with
25 | // the existing list items.
26 | merge(existing, incoming, { args: { currentPage } }) {
27 | if (currentPage === 1) {
28 | return incoming;
29 | }
30 | const _existing = existing ?? { items: [] };
31 | const _incoming = incoming ?? { items: [] };
32 | return {
33 | ..._existing,
34 | ..._incoming,
35 | items: [..._existing.items, ..._incoming.items],
36 | };
37 | },
38 | },
39 | },
40 | },
41 | },
42 | });
43 |
44 | const customerToken = await loadCustomerToken();
45 |
46 | if (customerToken !== null) {
47 | cache.writeQuery({
48 | query: IS_LOGGED_IN,
49 | data: {
50 | isLoggedIn: true,
51 | },
52 | });
53 | }
54 |
55 | const httpLink = createHttpLink({
56 | uri: `${magentoConfig.url}/graphql`,
57 | });
58 |
59 | const authLink = setContext(async (_, { headers }) => {
60 | // get the authentication token from local storage if it exists
61 | const token = await loadCustomerToken();
62 | // return the headers to the context so httpLink can read them
63 | return {
64 | headers: {
65 | ...headers,
66 | authorization: token !== null ? `Bearer ${token}` : '',
67 | },
68 | };
69 | });
70 |
71 | const client = new ApolloClient({
72 | link: authLink.concat(httpLink),
73 | cache,
74 | });
75 |
76 | _client = client;
77 |
78 | return client;
79 | }
80 |
--------------------------------------------------------------------------------
/src/apollo/data/possibleTypes.json:
--------------------------------------------------------------------------------
1 | {
2 | "CartAddressInterface": ["BillingCartAddress", "ShippingCartAddress"],
3 | "CartItemInterface": [
4 | "SimpleCartItem",
5 | "VirtualCartItem",
6 | "DownloadableCartItem",
7 | "ConfigurableCartItem",
8 | "BundleCartItem",
9 | "GiftCardCartItem"
10 | ],
11 | "ProductInterface": [
12 | "VirtualProduct",
13 | "SimpleProduct",
14 | "DownloadableProduct",
15 | "BundleProduct",
16 | "GroupedProduct",
17 | "ConfigurableProduct",
18 | "GiftCardProduct"
19 | ],
20 | "CategoryInterface": ["CategoryTree"],
21 | "MediaGalleryInterface": ["ProductImage", "ProductVideo"],
22 | "ProductLinksInterface": ["ProductLinks"],
23 | "CreditMemoItemInterface": [
24 | "DownloadableCreditMemoItem",
25 | "BundleCreditMemoItem",
26 | "CreditMemoItem",
27 | "GiftCardCreditMemoItem"
28 | ],
29 | "OrderItemInterface": [
30 | "DownloadableOrderItem",
31 | "BundleOrderItem",
32 | "OrderItem",
33 | "GiftCardOrderItem"
34 | ],
35 | "InvoiceItemInterface": [
36 | "DownloadableInvoiceItem",
37 | "BundleInvoiceItem",
38 | "InvoiceItem",
39 | "GiftCardInvoiceItem"
40 | ],
41 | "ShipmentItemInterface": [
42 | "BundleShipmentItem",
43 | "ShipmentItem",
44 | "GiftCardShipmentItem"
45 | ],
46 | "WishlistItemInterface": [
47 | "SimpleWishlistItem",
48 | "VirtualWishlistItem",
49 | "DownloadableWishlistItem",
50 | "BundleWishlistItem",
51 | "GroupedProductWishlistItem",
52 | "ConfigurableWishlistItem",
53 | "GiftCardWishlistItem"
54 | ],
55 | "AggregationOptionInterface": ["AggregationOption"],
56 | "LayerFilterItemInterface": ["LayerFilterItem", "SwatchLayerFilterItem"],
57 | "PhysicalProductInterface": [
58 | "SimpleProduct",
59 | "BundleProduct",
60 | "GroupedProduct",
61 | "ConfigurableProduct",
62 | "GiftCardProduct"
63 | ],
64 | "CustomizableOptionInterface": [
65 | "CustomizableAreaOption",
66 | "CustomizableDateOption",
67 | "CustomizableDropDownOption",
68 | "CustomizableMultipleOption",
69 | "CustomizableFieldOption",
70 | "CustomizableFileOption",
71 | "CustomizableRadioOption",
72 | "CustomizableCheckboxOption"
73 | ],
74 | "CustomizableProductInterface": [
75 | "VirtualProduct",
76 | "SimpleProduct",
77 | "DownloadableProduct",
78 | "BundleProduct",
79 | "ConfigurableProduct",
80 | "GiftCardProduct"
81 | ],
82 | "SwatchDataInterface": [
83 | "ImageSwatchData",
84 | "TextSwatchData",
85 | "ColorSwatchData"
86 | ],
87 | "SwatchLayerFilterItemInterface": ["SwatchLayerFilterItem"]
88 | }
89 |
--------------------------------------------------------------------------------
/src/apollo/mutations/addProductsToCart.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import {
3 | BasicCartDetailsType,
4 | BASIC_CART_DETAILS_FRAGMENT,
5 | } from '../queries/basicCartFragment';
6 |
7 | export interface AddProductsToCartDataType {
8 | addProductsToCart: {
9 | cart: BasicCartDetailsType;
10 | };
11 | }
12 |
13 | export interface AddProductsToCartVars {
14 | cartId: string;
15 | cartItems: Array;
16 | }
17 |
18 | export interface CartItemInputType {
19 | quantity: number;
20 | sku: string;
21 | }
22 |
23 | export const ADD_PRODUCTS_TO_CART = gql`
24 | mutation AddProductsToCart($cartId: String!, $cartItems: [CartItemInput!]!) {
25 | addProductsToCart(cartId: $cartId, cartItems: $cartItems) {
26 | cart {
27 | ...BasicCartDetailsFragment
28 | }
29 | }
30 | }
31 | ${BASIC_CART_DETAILS_FRAGMENT}
32 | `;
33 |
--------------------------------------------------------------------------------
/src/apollo/mutations/createCustomer.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface CreateCustomerDataType {
4 | createCustomerV2: {
5 | customer: {
6 | email: string;
7 | };
8 | };
9 | }
10 |
11 | export interface CreateCustomerVars {
12 | firstName: string;
13 | lastName: string;
14 | email: string;
15 | password: string;
16 | }
17 |
18 | export const CREATE_CUSTOMER = gql`
19 | mutation CreateCustomer(
20 | $firstName: String!
21 | $lastName: String!
22 | $email: String!
23 | $password: String!
24 | ) {
25 | createCustomerV2(
26 | input: {
27 | firstname: $firstName
28 | lastname: $lastName
29 | email: $email
30 | password: $password
31 | }
32 | ) {
33 | customer {
34 | email
35 | }
36 | }
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/src/apollo/mutations/createCustomerToken.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface CreateCustomerTokenDataType {
4 | generateCustomerToken: {
5 | token: string;
6 | };
7 | }
8 |
9 | export interface CreateCustomerTokenVars {
10 | email: string;
11 | password: string;
12 | }
13 |
14 | export const CREATE_CUSTOMER_TOKEN = gql`
15 | mutation CreateCustomerToken($email: String!, $password: String!) {
16 | generateCustomerToken(email: $email, password: $password) {
17 | token
18 | }
19 | }
20 | `;
21 |
--------------------------------------------------------------------------------
/src/apollo/queries/basicCartFragment.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface BasicCartDetailsType {
4 | id: string;
5 | items: Array;
6 | prices: {
7 | grandTotal: {
8 | value: number;
9 | currency: string;
10 | };
11 | };
12 | totalQuantity: number;
13 | }
14 |
15 | export interface CartItemType {
16 | id: number;
17 | product: {
18 | sku: string;
19 | name: string;
20 | small_image: {
21 | url: string;
22 | };
23 | };
24 | prices: {
25 | rowTotal: {
26 | currency: string;
27 | value: number;
28 | };
29 | };
30 | quantity: number;
31 | }
32 |
33 | export const BASIC_CART_DETAILS_FRAGMENT = gql`
34 | fragment BasicCartDetailsFragment on Cart {
35 | id
36 | items {
37 | id
38 | prices {
39 | rowTotal: row_total {
40 | currency
41 | value
42 | }
43 | }
44 | product {
45 | name
46 | sku
47 | small_image {
48 | url
49 | }
50 | }
51 | quantity
52 | }
53 | prices {
54 | grandTotal: grand_total {
55 | value
56 | currency
57 | }
58 | }
59 | totalQuantity: total_quantity
60 | }
61 | `;
62 |
--------------------------------------------------------------------------------
/src/apollo/queries/configurableProductFragment.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import {
3 | MEDIA_GALLERY_FRAGMENT,
4 | MediaGalleryItemType,
5 | } from './mediaGalleryFragment';
6 | import { PriceRangeType, PRODUCT_PRICE_FRAGMENT } from './productPriceFragment';
7 |
8 | export interface ConfigurableOptionType {
9 | id: number;
10 | label: string;
11 | position: number;
12 | attributeCode: string;
13 | values: Array;
14 | }
15 |
16 | export interface ConfigurableProductOptionValueType {
17 | label: string;
18 | valueIndex: number;
19 | swatchData: {
20 | value: string;
21 | __typename: 'ImageSwatchData' | 'TextSwatchData' | 'ColorSwatchData';
22 | };
23 | }
24 |
25 | export interface ConfigurableProductVariant {
26 | attributes: Array;
27 | product: ConfigurableProductVariantProduct;
28 | }
29 |
30 | export interface ConfigurableProductVariantAttribute {
31 | code: string;
32 | valueIndex: number;
33 | }
34 |
35 | export interface ConfigurableProductVariantProduct {
36 | sku: string;
37 | mediaGallery: Array;
38 | priceRange: PriceRangeType;
39 | }
40 |
41 | export const CONFIGURABLE_PRODUCT_FRAGMENT = gql`
42 | fragment ConfigurableProduct on ConfigurableProduct {
43 | configurableOptions: configurable_options {
44 | id
45 | label
46 | position
47 | attributeCode: attribute_code
48 | values {
49 | label
50 | valueIndex: value_index
51 | swatchData: swatch_data {
52 | value
53 | }
54 | }
55 | }
56 | variants {
57 | attributes {
58 | code
59 | valueIndex: value_index
60 | }
61 | product {
62 | sku
63 | ...MediaGallery
64 | ...ProductPrice
65 | }
66 | }
67 | }
68 | ${MEDIA_GALLERY_FRAGMENT}
69 | ${PRODUCT_PRICE_FRAGMENT}
70 | `;
71 |
--------------------------------------------------------------------------------
/src/apollo/queries/getCart.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import {
3 | BasicCartDetailsType,
4 | BASIC_CART_DETAILS_FRAGMENT,
5 | } from './basicCartFragment';
6 |
7 | export interface GetCartDataType {
8 | customerCart: CartType;
9 | }
10 |
11 | export type CartType = BasicCartDetailsType;
12 |
13 | export const GET_CART = gql`
14 | query GetCart {
15 | customerCart {
16 | ...BasicCartDetailsFragment
17 | }
18 | }
19 | ${BASIC_CART_DETAILS_FRAGMENT}
20 | `;
21 |
--------------------------------------------------------------------------------
/src/apollo/queries/getCategories.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface GetCategoriesVars {
4 | id: string;
5 | }
6 |
7 | export interface CategoriesDataType {
8 | categoryList: CategoryListType[];
9 | }
10 |
11 | interface CategoryListType {
12 | id: number;
13 | children: CategoryType[];
14 | }
15 |
16 | export interface CategoryType {
17 | id: number;
18 | name: string;
19 | productCount: number;
20 | childrenCount: string;
21 | image: string;
22 | /**
23 | * In case category doesn't contain image,
24 | * use one of the product's image inside the category
25 | */
26 | productPreviewImage: CategoryProductPreviewImageType;
27 | }
28 |
29 | interface CategoryProductPreviewImageType {
30 | items: Array<{
31 | smallImage: {
32 | url: string;
33 | };
34 | }>;
35 | }
36 |
37 | export const GET_CATEGORIES = gql`
38 | query GetCategories($id: String) {
39 | categoryList(filters: { ids: { eq: $id } }) {
40 | id
41 | children {
42 | id
43 | name
44 | productCount: product_count
45 | childrenCount: children_count
46 | image
47 | productPreviewImage: products(pageSize: 1) {
48 | items {
49 | smallImage: small_image {
50 | url
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 | `;
58 |
--------------------------------------------------------------------------------
/src/apollo/queries/getCategoryProducts.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { ProductInListType, PRODUCTS_FRAGMENT } from './productsFragment';
3 |
4 | export interface GetCategoryProductsVars {
5 | id: string;
6 | pageSize: number;
7 | currentPage: number;
8 | price?: SortEnum;
9 | name?: SortEnum;
10 | }
11 |
12 | export interface CategoryProductsDataType {
13 | products: {
14 | totalCount: number;
15 | items: Array;
16 | };
17 | }
18 |
19 | export enum SortEnum {
20 | ASC = 'ASC',
21 | DESC = 'DESC',
22 | }
23 |
24 | export const GET_CATGEORY_PRODUCTS = gql`
25 | query GetCategoryProducts(
26 | $id: String
27 | $pageSize: Int!
28 | $currentPage: Int!
29 | $price: SortEnum
30 | $name: SortEnum
31 | ) {
32 | products(
33 | filter: { category_id: { eq: $id } }
34 | pageSize: $pageSize
35 | sort: { price: $price, name: $name }
36 | currentPage: $currentPage
37 | ) {
38 | totalCount: total_count
39 | ...ProductListFragment
40 | }
41 | }
42 | ${PRODUCTS_FRAGMENT}
43 | `;
44 |
--------------------------------------------------------------------------------
/src/apollo/queries/getCustomer.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface GetCustomerDataType {
4 | customer: CustomerType;
5 | }
6 |
7 | export interface CustomerType {
8 | email: string;
9 | firstName: string;
10 | lastName: string;
11 | }
12 |
13 | export const GET_CUSTOMER = gql`
14 | query GetCustomer {
15 | customer {
16 | email
17 | firstName: firstname
18 | lastName: lastname
19 | }
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/src/apollo/queries/getProductDetails.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import {
3 | ConfigurableOptionType,
4 | ConfigurableProductVariant,
5 | CONFIGURABLE_PRODUCT_FRAGMENT,
6 | } from './configurableProductFragment';
7 | import {
8 | MEDIA_GALLERY_FRAGMENT,
9 | MediaGalleryItemType,
10 | } from './mediaGalleryFragment';
11 | import { PriceRangeType, PRODUCT_PRICE_FRAGMENT } from './productPriceFragment';
12 |
13 | export interface GetProductDetailsVars {
14 | sku: string;
15 | }
16 |
17 | export interface ProductDetailsDataType {
18 | products: {
19 | items: Array;
20 | };
21 | }
22 |
23 | export type ProductDetailsType =
24 | | SimpleProductDetailsType
25 | | ConfigurableProductDetailsType
26 | | GroupedProductDetailsType;
27 |
28 | export interface ProductInterfaceDetailsType {
29 | id: number;
30 | sku: string;
31 | name: string;
32 | description: {
33 | html: string;
34 | };
35 | priceRange: PriceRangeType;
36 | mediaGallery: Array;
37 | }
38 |
39 | export interface SimpleProductDetailsType extends ProductInterfaceDetailsType {
40 | type: ProductTypeEnum.SIMPLE;
41 | }
42 |
43 | export interface ConfigurableProductDetailsType
44 | extends ProductInterfaceDetailsType {
45 | type: ProductTypeEnum.CONFIGURED;
46 | configurableOptions: Array;
47 | variants: Array;
48 | }
49 |
50 | export interface GroupedProductDetailsType extends ProductInterfaceDetailsType {
51 | type: ProductTypeEnum.GROUPED;
52 | }
53 |
54 | export enum ProductTypeEnum {
55 | SIMPLE = 'SimpleProduct',
56 | GROUPED = 'GroupedProduct',
57 | CONFIGURED = 'ConfigurableProduct',
58 | }
59 |
60 | export const GET_PRODUCT_DETAILS = gql`
61 | query GetProductDetails($sku: String) {
62 | products(filter: { sku: { eq: $sku } }) {
63 | items {
64 | id
65 | sku
66 | name
67 | description {
68 | html
69 | }
70 | type: __typename
71 | ...ProductPrice
72 | ...MediaGallery
73 | ...ConfigurableProduct
74 | }
75 | }
76 | }
77 | ${PRODUCT_PRICE_FRAGMENT}
78 | ${MEDIA_GALLERY_FRAGMENT}
79 | ${CONFIGURABLE_PRODUCT_FRAGMENT}
80 | `;
81 |
--------------------------------------------------------------------------------
/src/apollo/queries/getSearchProducts.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { CategoryProductsDataType } from './getCategoryProducts';
3 | import { PRODUCTS_FRAGMENT } from './productsFragment';
4 |
5 | export interface GetSearchProductsVars {
6 | searchText: string;
7 | pageSize: number;
8 | currentPage: number;
9 | }
10 |
11 | export type SearchProductsDataType = CategoryProductsDataType;
12 |
13 | export const GET_SEARCH_PRODUCTS = gql`
14 | query GetSearchProducts(
15 | $searchText: String!
16 | $pageSize: Int!
17 | $currentPage: Int!
18 | ) {
19 | products(
20 | search: $searchText
21 | pageSize: $pageSize
22 | currentPage: $currentPage
23 | ) {
24 | totalCount: total_count
25 | ...ProductListFragment
26 | }
27 | }
28 | ${PRODUCTS_FRAGMENT}
29 | `;
30 |
--------------------------------------------------------------------------------
/src/apollo/queries/isLoggedIn.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface IsLoggedInDataType {
4 | isLoggedIn: boolean | undefined;
5 | }
6 |
7 | export const IS_LOGGED_IN = gql`
8 | query IsUserLoggedIn {
9 | isLoggedIn @client
10 | }
11 | `;
12 |
--------------------------------------------------------------------------------
/src/apollo/queries/mediaGalleryFragment.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface MediaGalleryItemType {
4 | disabled: boolean;
5 | label: string;
6 | position: number;
7 | url: string;
8 | }
9 |
10 | export const MEDIA_GALLERY_FRAGMENT = gql`
11 | fragment MediaGallery on ProductInterface {
12 | mediaGallery: media_gallery {
13 | disabled
14 | label
15 | position
16 | url
17 | }
18 | }
19 | `;
20 |
--------------------------------------------------------------------------------
/src/apollo/queries/productPriceFragment.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | export interface PriceRangeType {
4 | maximumPrice: {
5 | finalPrice: {
6 | currency: string;
7 | value: number;
8 | };
9 | };
10 | }
11 |
12 | export const PRODUCT_PRICE_FRAGMENT = gql`
13 | fragment ProductPrice on ProductInterface {
14 | priceRange: price_range {
15 | maximumPrice: maximum_price {
16 | finalPrice: final_price {
17 | value
18 | currency
19 | }
20 | }
21 | }
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/src/apollo/queries/productsFragment.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { PRODUCT_PRICE_FRAGMENT } from './productPriceFragment';
3 | import type { PriceRangeType } from './productPriceFragment';
4 |
5 | export interface ProductInListType {
6 | id: number;
7 | sku: string;
8 | name: string;
9 | smallImage: {
10 | url: string;
11 | };
12 | priceRange: PriceRangeType;
13 | }
14 |
15 | export const PRODUCTS_FRAGMENT = gql`
16 | fragment ProductListFragment on Products {
17 | items {
18 | id
19 | sku
20 | name
21 | smallImage: small_image {
22 | url
23 | }
24 | ...ProductPrice
25 | }
26 | }
27 | ${PRODUCT_PRICE_FRAGMENT}
28 | `;
29 |
--------------------------------------------------------------------------------
/src/components/CategoryListItem/CategoryListItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { ListItem, Image } from 'react-native-elements';
4 | import { DIMENS } from '../../constants';
5 | import { CategoryType } from '../../apollo/queries/getCategories';
6 | import { Routes } from '../../navigation';
7 |
8 | interface Props {
9 | item: CategoryType;
10 | navigation: {
11 | navigate: (arg0: string, arg1: object) => {};
12 | };
13 | }
14 |
15 | const CategoryListItem = ({ item, navigation }: Props): React.ReactElement => {
16 | const [disabled] = useState(
17 | +item.childrenCount < 1 && item.productCount < 1,
18 | );
19 | const onCategoryPress = () => {
20 | if (+item.childrenCount > 0) {
21 | navigation.navigate(Routes.NAVIGATION_TO_CATEGORIES_SCREEN, {
22 | categoryId: item.id,
23 | name: item.name,
24 | });
25 | return;
26 | }
27 | navigation.navigate(Routes.NAVIGATION_TO_PRODUCT_LIST_SCREEN, {
28 | categoryId: item.id,
29 | name: item.name,
30 | });
31 | };
32 |
33 | const renderImage = () => {
34 | const rawUri =
35 | item.image ?? item.productPreviewImage?.items?.[0]?.smallImage?.url;
36 | if (!rawUri) {
37 | return null;
38 | }
39 | const uri = `${rawUri ?? ''}?width=100`;
40 |
41 | return ;
42 | };
43 |
44 | const renderContent = () => {
45 | return (
46 | <>
47 |
48 | {item.name}
49 |
50 | >
51 | );
52 | };
53 |
54 | return (
55 |
61 | {renderImage()}
62 | {renderContent()}
63 |
64 | );
65 | };
66 |
67 | const styles = StyleSheet.create({
68 | conatiner: {
69 | padding: 0,
70 | },
71 | image: {
72 | width: DIMENS.categoryListItem.imageWidth,
73 | height: DIMENS.categoryListItem.imageHeight,
74 | },
75 | });
76 |
77 | export default CategoryListItem;
78 |
--------------------------------------------------------------------------------
/src/components/CustomHeaderButtons/CustomHeaderButtons.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
3 | import {
4 | HeaderButtons,
5 | HeaderButton,
6 | HeaderButtonProps,
7 | HeaderButtonsProps,
8 | } from 'react-navigation-header-buttons';
9 | import { useTheme } from '@react-navigation/native';
10 |
11 | // define IconComponent, color, sizes and OverflowIcon in one place
12 | const CustomHeaderButton = (props: HeaderButtonProps) => {
13 | const { colors } = useTheme();
14 | return (
15 |
21 | );
22 | };
23 |
24 | export const CustomHeaderButtons = (props: HeaderButtonsProps) => {
25 | return (
26 |
27 | );
28 | };
29 | export { Item as CustomHeaderItem } from 'react-navigation-header-buttons';
30 |
--------------------------------------------------------------------------------
/src/components/GenericTemplate/GenericTemplate.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { View, StyleSheet, ScrollView, ViewStyle } from 'react-native';
3 | import { Text } from 'react-native-elements';
4 | import { SPACING } from '../../constants';
5 | import Spinner from '../Spinner/Spinner';
6 |
7 | interface Props {
8 | /**
9 | * Element to be render when no loading and no error
10 | */
11 | children: ReactNode;
12 | /**
13 | * Render spinner at center
14 | */
15 | loading?: boolean;
16 | /**
17 | * Add children in ScrollView component
18 | */
19 | scrollable?: boolean;
20 | /**
21 | * Add sticky Footer at bottom
22 | */
23 | footer?: ReactNode;
24 | /**
25 | * in case of status === Status.ERROR, the error message to be shown
26 | */
27 | errorMessage?: string;
28 | /**
29 | * Container style that wrap children except footer component
30 | */
31 | style?: ViewStyle;
32 | }
33 |
34 | const GenericTemplate = ({
35 | children,
36 | footer,
37 | scrollable = false,
38 | loading = false,
39 | errorMessage,
40 | style = {},
41 | }: Props): React.ReactElement => {
42 | const ViewGroup = scrollable ? ScrollView : View;
43 |
44 | const renderLoader = () =>
45 | loading && (
46 |
47 |
48 |
49 | );
50 |
51 | const renderError = () =>
52 | !loading &&
53 | !!errorMessage && (
54 |
55 | {errorMessage}
56 |
57 | );
58 |
59 | const renderContent = () => !loading && !errorMessage && children;
60 |
61 | return (
62 | <>
63 |
64 | {renderLoader()}
65 | {renderError()}
66 | {renderContent()}
67 |
68 | {!loading && footer}
69 | >
70 | );
71 | };
72 |
73 | const styles = StyleSheet.create({
74 | container: {
75 | flex: 1,
76 | },
77 | center: {
78 | flex: 1,
79 | alignItems: 'center',
80 | justifyContent: 'center',
81 | padding: SPACING.large,
82 | },
83 | });
84 |
85 | export default GenericTemplate;
86 |
--------------------------------------------------------------------------------
/src/components/MediaGallery/MediaGallery.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState } from 'react';
2 | import {
3 | View,
4 | FlatList,
5 | Image,
6 | StyleSheet,
7 | ImageResizeMode,
8 | ViewStyle,
9 | } from 'react-native';
10 | import { Text } from 'react-native-elements';
11 | import { MediaGalleryItemType } from '../../apollo/queries/mediaGalleryFragment';
12 | import { SPACING, DIMENS } from '../../constants';
13 |
14 | interface Props {
15 | items: Array;
16 | resizeMode?: ImageResizeMode;
17 | containerStyle?: ViewStyle;
18 | width?: number;
19 | height?: number;
20 | }
21 |
22 | const MediaGallery = ({
23 | items,
24 | resizeMode = 'contain',
25 | containerStyle = {},
26 | width = DIMENS.common.WINDOW_WIDTH,
27 | height = DIMENS.common.WINDOW_WIDTH,
28 | }: Props): React.ReactElement => {
29 | const [currentPage, setCurrentPage] = useState(1);
30 | const imageDimension = useMemo(() => ({ width, height }), [width, height]);
31 |
32 | const onMomentumScrollEnd = event => {
33 | const contentOffset = event.nativeEvent.contentOffset.x;
34 | const width = event.nativeEvent.layoutMeasurement.width;
35 |
36 | const currentNumber = Math.floor(contentOffset / width) + 1;
37 | setCurrentPage(currentNumber);
38 | };
39 |
40 | const renderItem = ({ item }: { item: MediaGalleryItemType }) => {
41 | return (
42 |
43 |
48 |
49 | );
50 | };
51 |
52 | return (
53 |
54 | `mediaGalleryItem#${index}`}
59 | renderItem={renderItem}
60 | onMomentumScrollEnd={onMomentumScrollEnd}
61 | showsHorizontalScrollIndicator={false}
62 | />
63 | {`${currentPage}/${items.length}`}
64 |
65 | );
66 | };
67 |
68 | const styles = StyleSheet.create({
69 | container: {
70 | backgroundColor: '#fff',
71 | },
72 | pagination: {
73 | position: 'absolute',
74 | top: 0,
75 | right: 0,
76 | margin: SPACING.large,
77 | paddingHorizontal: SPACING.small,
78 | paddingVertical: SPACING.tiny,
79 | backgroundColor: 'rgba(0,0,0,.6)',
80 | color: 'white',
81 | borderRadius: SPACING.large,
82 | },
83 | });
84 |
85 | export default MediaGallery;
86 |
--------------------------------------------------------------------------------
/src/components/ProductListItem/ProductListItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { View, StyleSheet, TouchableOpacity } from 'react-native';
3 | import { Text, Image, ThemeContext } from 'react-native-elements';
4 | import { ProductInListType } from '../../apollo/queries/productsFragment';
5 | import { formatPrice } from '../../logic';
6 | import { DIMENS } from '../../constants';
7 |
8 | interface Props {
9 | item: ProductInListType;
10 | index: number;
11 | horizontalMode?: boolean;
12 | onPress(arg0: number): void;
13 | navigation?: {
14 | navigate: (arg0: string, arg1: object) => {};
15 | };
16 | }
17 |
18 | const COLUMN_WIDTH = DIMENS.common.WINDOW_WIDTH / 2;
19 |
20 | const ProductListItem = ({
21 | item,
22 | index,
23 | horizontalMode = false,
24 | onPress,
25 | }: Props): React.ReactElement => {
26 | const { theme } = useContext(ThemeContext);
27 | const renderImage = () => {
28 | const uri = `${item.smallImage.url}?width=${COLUMN_WIDTH}`;
29 | return ;
30 | };
31 |
32 | return (
33 | onPress(index)}>
34 |
45 | {renderImage()}
46 | {item.name}
47 |
48 | {formatPrice(item.priceRange.maximumPrice.finalPrice)}
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | const styles = StyleSheet.create({
56 | container: {
57 | flex: 1,
58 | width: COLUMN_WIDTH,
59 | borderBottomWidth: DIMENS.common.borderWidth,
60 | overflow: 'hidden',
61 | },
62 | leftBorder: {
63 | borderLeftWidth: DIMENS.common.borderWidth,
64 | },
65 | topBorder: {
66 | borderTopWidth: DIMENS.common.borderWidth,
67 | },
68 | image: {
69 | width: COLUMN_WIDTH,
70 | height: (COLUMN_WIDTH / 3) * 4,
71 | },
72 | name: {
73 | textAlign: 'center',
74 | },
75 | price: {
76 | textAlign: 'center',
77 | fontWeight: 'bold',
78 | },
79 | });
80 |
81 | export default ProductListItem;
82 |
--------------------------------------------------------------------------------
/src/components/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { ActivityIndicator, ViewStyle } from 'react-native';
3 | import { ThemeContext } from 'react-native-elements';
4 |
5 | interface Props {
6 | /**
7 | * size of the spinner, can be
8 | * 1. 'large'
9 | * 2. 'small'
10 | */
11 | size?: 'large' | 'small';
12 | /**
13 | * custom color for the spinner
14 | */
15 | color?: string;
16 | /**
17 | * style containing padding & margin
18 | */
19 | style?: ViewStyle;
20 | }
21 |
22 | const Spinner = ({
23 | size = 'large',
24 | color,
25 | style = {},
26 | }: Props): React.ReactElement => {
27 | const { theme } = useContext(ThemeContext);
28 | return (
29 |
34 | );
35 | };
36 |
37 | export default Spinner;
38 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | import CategoryListItem from './CategoryListItem/CategoryListItem';
2 | import {
3 | CustomHeaderButtons,
4 | CustomHeaderItem,
5 | } from './CustomHeaderButtons/CustomHeaderButtons';
6 | import GenericTemplate from './GenericTemplate/GenericTemplate';
7 | import MediaGallery from './MediaGallery/MediaGallery';
8 | import ProductListItem from './ProductListItem/ProductListItem';
9 | import Spinner from './Spinner/Spinner';
10 |
11 | export {
12 | CategoryListItem,
13 | CustomHeaderButtons,
14 | CustomHeaderItem,
15 | GenericTemplate,
16 | MediaGallery,
17 | ProductListItem,
18 | Spinner,
19 | };
20 |
--------------------------------------------------------------------------------
/src/constants/dimens.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions, StyleSheet } from 'react-native';
2 | import { moderateScale } from 'react-native-size-matters';
3 |
4 | const screenWidth = Dimensions.get('window').width;
5 | const screenHeight = Dimensions.get('window').height;
6 |
7 | /**
8 | * All the dimension related to sizes, should be stored here
9 | */
10 | const DIMENS = Object.freeze({
11 | /**
12 | * App level constants common among all components
13 | */
14 | common: {
15 | WINDOW_WIDTH: screenWidth,
16 | WINDOW_HEIGHT: screenHeight,
17 | cartItemCountFontSize: moderateScale(10),
18 | borderWidth: moderateScale(StyleSheet.hairlineWidth),
19 | appbarIconSize: moderateScale(23),
20 | },
21 | cartScreen: {
22 | imageSize: moderateScale(100),
23 | },
24 | categoryListItem: {
25 | imageWidth: moderateScale(70),
26 | imageHeight: moderateScale(70),
27 | },
28 | homeScreen: {
29 | carouselHeight: moderateScale(200),
30 | },
31 | productDetailScreen: {
32 | configurableOptionValueBoxSize: moderateScale(46),
33 | },
34 | });
35 |
36 | export default DIMENS;
37 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | import SPACING from './spacing';
2 | import DIMENS from './dimens';
3 | import LIMITS from './limits';
4 |
5 | export { DIMENS, SPACING, LIMITS };
6 |
--------------------------------------------------------------------------------
/src/constants/limits.ts:
--------------------------------------------------------------------------------
1 | const LIMITS = Object.freeze({
2 | categoryProductsPageSize: 10,
3 | searchTextMinLength: 3,
4 | searchScreenPageSize: 10,
5 | autoSearchApiTimeDelay: 1000,
6 | });
7 |
8 | export default LIMITS;
9 |
--------------------------------------------------------------------------------
/src/constants/spacing.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * NOTE:
3 | *
4 | * Spacing should be consistent and whitespace thought of as a first class technique up
5 | * there with color and typefaces.
6 | *
7 | * To scale or shrink overall spacing, change @param baseSpacing value.
8 | *
9 | * Feel free to delete this block.
10 | */
11 | import { moderateScale } from 'react-native-size-matters';
12 |
13 | const baseSpacing = 10;
14 |
15 | const SPACING = Object.freeze({
16 | tiny: moderateScale(baseSpacing * 0.4),
17 | small: moderateScale(baseSpacing * 0.8),
18 | medium: moderateScale(baseSpacing * 1.2),
19 | large: moderateScale(baseSpacing * 1.6),
20 | extraLarge: moderateScale(baseSpacing * 2.4),
21 | });
22 |
23 | export default SPACING;
24 |
--------------------------------------------------------------------------------
/src/i18n/__tests__/i18n.test.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18n-js';
2 | import * as RNLocalize from 'react-native-localize';
3 | import { initLocale } from '../i18n';
4 | import { translate } from '../translate';
5 |
6 | jest.mock('i18n-js');
7 |
8 | describe('i18n', () => {
9 | beforeEach(() => {
10 | jest.clearAllMocks();
11 | });
12 |
13 | test('should set locale to system locale if available', () => {
14 | // Setup
15 | RNLocalize.findBestAvailableLanguage.mockReturnValueOnce({
16 | languageTag: 'es',
17 | isRTL: false,
18 | });
19 |
20 | // Exercise
21 | initLocale();
22 |
23 | // Verify
24 | expect(i18n.locale).toBe('es');
25 | expect(RNLocalize.findBestAvailableLanguage).toBeCalledTimes(1);
26 | });
27 |
28 | test('should set locale to english, if system locale is not supported', () => {
29 | // Setup
30 | RNLocalize.findBestAvailableLanguage.mockReturnValueOnce(undefined);
31 |
32 | // Exercise
33 | initLocale();
34 |
35 | // Verify
36 | expect(i18n.locale).toBe('en');
37 | expect(RNLocalize.findBestAvailableLanguage).toBeCalledTimes(1);
38 | });
39 |
40 | test('should translate in specific language', () => {
41 | // Setup
42 | const key = 'common.login';
43 | const option = { locale: 'es' };
44 | const translatedString = 'Acceso';
45 |
46 | const tSpy = jest.spyOn(i18n, 't');
47 | tSpy.mockReturnValueOnce(translatedString);
48 |
49 | // Exercise
50 | const result = translate(key, option);
51 |
52 | // Verify
53 | expect(tSpy).toHaveBeenCalledWith(key, option);
54 | expect(result).toBe(translatedString);
55 |
56 | tSpy.mockRestore();
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/i18n/i18n.ts:
--------------------------------------------------------------------------------
1 | import * as RNLocalize from 'react-native-localize';
2 | import i18n from 'i18n-js';
3 | import en from './locales/en.json';
4 | import es from './locales/es.json';
5 |
6 | // Should the app fallback to English if user locale doesn't exists
7 | i18n.fallbacks = true;
8 |
9 | // Define the supported translation
10 | i18n.translations = {
11 | en,
12 | es,
13 | };
14 |
15 | export const initLocale = () => {
16 | const fallback = { languageTag: 'en', isRTL: false };
17 | const { languageTag } =
18 | RNLocalize.findBestAvailableLanguage(Object.keys(i18n.translations)) ||
19 | fallback;
20 |
21 | i18n.locale = languageTag;
22 | };
23 |
24 | initLocale();
25 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import './i18n';
2 |
3 | export * from './translate';
4 |
--------------------------------------------------------------------------------
/src/i18n/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "brand": "E-Kart",
4 | "ok": "OK",
5 | "cancel": "Cancel",
6 | "yes": "Yes",
7 | "no": "No",
8 | "menu": "Menu",
9 | "search": "Search",
10 | "sort": "Sort",
11 | "aToZ": "Name: A to Z",
12 | "zToA": "Name: Z to A",
13 | "lowToHigh": "Price: Low to High",
14 | "highToLow": "Price: High to Low",
15 | "user": "User",
16 | "dearUser": "Dear User",
17 | "login": "Login",
18 | "signup": "Signup",
19 | "email": "Email",
20 | "password": "Password",
21 | "firstName": "First Name",
22 | "lastName": "Last Name",
23 | "register": "Register",
24 | "logout": "Logout",
25 | "cart": "Cart",
26 | "quantity": "Qty",
27 | "price": "price",
28 | "total": "Total",
29 | "success": "Success",
30 | "error": "Error",
31 | "attention": "Attention",
32 | "pluralizationExample": {
33 | "one": "You have 1 item",
34 | "other": "You have %{count} items",
35 | "zero": "You cart is empty"
36 | }
37 | },
38 | "errors": {
39 | "genericError": "Something went wrong! Please try again."
40 | },
41 | "cartScreen": {
42 | "appbarTitle": "Cart",
43 | "guestUserPromptMessage": "In order to view cart, please login or create new account",
44 | "cartEmptyTitle": "Hey, it feels so light!",
45 | "cartEmptyMessage": "There is nothing in your bag. Let's add some items.",
46 | "placeOrderButton": "Place Order"
47 | },
48 | "categoriesScreen": {
49 | "appbarTitle": "Category"
50 | },
51 | "homeScreen": {
52 | "appbarTitle": "Home"
53 | },
54 | "loginScreen": {
55 | "appbarTitle": "Login",
56 | "noAccount": "Don't have account? SignUp",
57 | "successMessage": "Login Successful"
58 | },
59 | "productDetailsScreen": {
60 | "appbarTitle": "Product Details",
61 | "addToCart": "Add To Cart",
62 | "guestUserPromptMessage": "In order to add product to cart, please login or create new account",
63 | "productTypeNotSupported": "Currently only Simple & grouped product types are supported",
64 | "addToCartSuccessful": "Product added to cart",
65 | "addToCartError": "Unable to add product to cart, something went wrong.",
66 | "select": "Select %{label}"
67 | },
68 | "productListScreen": {
69 | "appbarTitle": "Products"
70 | },
71 | "profileScreen": {
72 | "appbarTitle": "Profile",
73 | "guestUserPromptMessage": "In order to view profile, please login or create new account",
74 | "greeting": "Welcome %{name}"
75 | },
76 | "searchScreen": {
77 | "searchBarHint": "Search product by name...",
78 | "noProductsFound": "We couldn't found any products matching %{searchText}"
79 | },
80 | "signupScreen": {
81 | "appbarTitle": "Signup",
82 | "haveAccount": "Aldready have an account? Login",
83 | "successMessage": "Account created successfully! Please Login to continue."
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/i18n/locales/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "brand": "E-Kart",
4 | "ok": "OK",
5 | "cancel": "Cancelar",
6 | "yes": "Sí",
7 | "no": "No",
8 | "menu": "Menú",
9 | "search": "Buscar",
10 | "sort": "Clasificar",
11 | "aToZ": "Nombre: de la A a la Z",
12 | "zToA": "Nombre: de la Z a la A",
13 | "lowToHigh": "Precio: Bajo a Alto",
14 | "highToLow": "Precio: Alto a Bajo",
15 | "user": "Usuario",
16 | "dearUser": "Estimado usuario",
17 | "login": "Acceso",
18 | "signup": "Inscribirse",
19 | "email": "Correo electrónico",
20 | "password": "Contraseña",
21 | "firstName": "Nombre de pila",
22 | "lastName": "Apellido",
23 | "register": "Registrarse",
24 | "logout": "Cerrar sesión",
25 | "cart": "Carro",
26 | "quantity": "Qty",
27 | "price": "precio",
28 | "total": "Total",
29 | "success": "Éxito",
30 | "error": "Error",
31 | "attention": "Atención",
32 | "pluralizationExample": {
33 | "one": "Tienes 1 artículo",
34 | "other": "Tienes %{count} artículos",
35 | "zero": "Tu carrito está vacío"
36 | }
37 | },
38 | "errors": {
39 | "genericError": "¡Algo salió mal! Inténtalo de nuevo."
40 | },
41 | "cartScreen": {
42 | "appbarTitle": "Cart",
43 | "guestUserPromptMessage": "In order to view cart, please login or create new account",
44 | "cartEmptyTitle": "Hey, it feels so light!",
45 | "cartEmptyMessage": "There is nothing in your bag. Let's add some items.",
46 | "placeOrderButton": "Place Order"
47 | },
48 | "categoriesScreen": {
49 | "appbarTitle": "Categoría"
50 | },
51 | "homeScreen": {
52 | "appbarTitle": "Hogar"
53 | },
54 | "loginScreen": {
55 | "appbarTitle": "Acceso",
56 | "noAccount": "Don't have account? SignUp",
57 | "successMessage": "Login Successful"
58 | },
59 | "productDetailsScreen": {
60 | "appbarTitle": "Product Details",
61 | "addToCart": "Add To Cart",
62 | "guestUserPromptMessage": "In order to add product to cart, please login or create new account",
63 | "productTypeNotSupported": "Currently only Simple & grouped product types are supported",
64 | "addToCartSuccessful": "Product added to cart",
65 | "addToCartError": "Unable to add product to cart, something went wrong.",
66 | "select": "Select %{label}"
67 | },
68 | "productListScreen": {
69 | "appbarTitle": "Products"
70 | },
71 | "profileScreen": {
72 | "appbarTitle": "Profile",
73 | "guestUserPromptMessage": "In order to view profile, please login or create new account",
74 | "greeting": "Welcome %{name}"
75 | },
76 | "searchScreen": {
77 | "searchBarHint": "Search product by name...",
78 | "noProductsFound": "We couldn't found any products matching %{searchText}"
79 | },
80 | "signupScreen": {
81 | "appbarTitle": "Signup",
82 | "haveAccount": "Aldready have an account? Login",
83 | "successMessage": "Account created successfully! Please Login to continue."
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/i18n/translate.ts:
--------------------------------------------------------------------------------
1 | import i18n, { TranslateOptions } from 'i18n-js';
2 |
3 | /**
4 | * Translates text.
5 | *
6 | * @param key The i18n key.
7 | */
8 | export function translate(key: string, options: TranslateOptions = {}): string {
9 | return i18n.t(key, options);
10 | }
11 |
--------------------------------------------------------------------------------
/src/logic/app/__tests__/useForm.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react-hooks';
2 | import { useForm } from '../useForm';
3 |
4 | describe('useForm', () => {
5 | // Global constants
6 | const email = 'alex@gmail.com';
7 | const password = '123456789';
8 |
9 | test('should return initial values', () => {
10 | // Setup
11 | const inititalProps = {
12 | initialValues: {
13 | email: '',
14 | password: '',
15 | },
16 | onSubmit: jest.fn(),
17 | };
18 | const expectedValues = {
19 | email: '',
20 | password: '',
21 | };
22 |
23 | // Exercise
24 | const { result } = renderHook(() =>
25 | useForm<{ email: string; password: string }>(inititalProps),
26 | );
27 |
28 | // Verify
29 | expect(result.current.values).toEqual(expectedValues);
30 | expect(inititalProps.onSubmit).not.toHaveBeenCalled();
31 | });
32 |
33 | test('should handle change in values', () => {
34 | // Setup
35 | const inititalProps = {
36 | initialValues: {
37 | email: '',
38 | password: '',
39 | },
40 | onSubmit: jest.fn(),
41 | };
42 | const expectedValues = {
43 | email,
44 | password,
45 | };
46 |
47 | // Exercise
48 | const { result } = renderHook(() =>
49 | useForm<{ email: string; password: string }>(inititalProps),
50 | );
51 | act(() => {
52 | result.current.handleChange('email')(email);
53 | result.current.handleChange('password')(password);
54 | });
55 |
56 | // Verify
57 | expect(result.current.values).toEqual(expectedValues);
58 | });
59 |
60 | test('should toggle boolean values on change', () => {
61 | // Setup
62 | const inititalProps = {
63 | initialValues: {
64 | isChecked: false,
65 | },
66 | };
67 | const expectedValues = {
68 | isChecked: true,
69 | };
70 |
71 | // Exercise
72 | const { result } = renderHook(() =>
73 | useForm<{ isChecked: boolean }>(inititalProps),
74 | );
75 | act(() => {
76 | // Purposefully set it to false, should be discarded, and simply toggle current value
77 | result.current.handleChange('isChecked')(false);
78 | });
79 |
80 | // Verify
81 | expect(result.current.values).toEqual(expectedValues);
82 | });
83 |
84 | test('should handle submit event', async () => {
85 | // Setup
86 | const inititalProps = {
87 | initialValues: {
88 | email,
89 | password,
90 | },
91 | onSubmit: jest.fn(),
92 | };
93 | const expectedValues = {
94 | email,
95 | password,
96 | };
97 |
98 | // Exercise
99 | const { result } = renderHook(() =>
100 | useForm<{ email: string; password: string }>(inititalProps),
101 | );
102 | await act(async () => {
103 | await result.current.handleSubmit();
104 | });
105 |
106 | // Verify
107 | expect(inititalProps.onSubmit).toHaveBeenCalledTimes(1);
108 | expect(inititalProps.onSubmit).toHaveBeenCalledWith(expectedValues);
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/src/logic/app/useForm.ts:
--------------------------------------------------------------------------------
1 | // Copied from https://github.com/formium/formik/blob/master/packages/formik/src/Formik.tsx
2 | import { useReducer } from 'react';
3 |
4 | interface FormConfig {
5 | /**
6 | * Initial values of the form
7 | */
8 | initialValues: Values;
9 | /**
10 | * Submission handler
11 | */
12 | onSubmit?: (values: Values) => void | Promise;
13 | }
14 |
15 | export interface FormResult {
16 | values: Values;
17 | handleChange(field1: string): (field2: any) => void;
18 | handleSubmit(): Promise;
19 | }
20 |
21 | /**
22 | * Values of fields in the form
23 | */
24 | interface FormValues {
25 | [field: string]: any;
26 | }
27 |
28 | interface FormState {
29 | /** Form values */
30 | values: Values;
31 | }
32 |
33 | type FormAction = {
34 | type: 'SET_FIELD_VALUE';
35 | payload: { field: string; value?: any };
36 | };
37 |
38 | function formReducer(state: FormState, action: FormAction) {
39 | switch (action.type) {
40 | case 'SET_FIELD_VALUE':
41 | const field = state.values[action.payload.field];
42 | let newValue = action.payload.value;
43 | if (typeof field === 'boolean') {
44 | // If field is boolean, simply toggle previous value and ignore new value
45 | newValue = !field;
46 | }
47 | return {
48 | ...state,
49 | values: {
50 | ...state.values,
51 | [action.payload.field]: newValue,
52 | },
53 | };
54 | default:
55 | return state;
56 | }
57 | }
58 |
59 | export function useForm(
60 | props: FormConfig,
61 | ): FormResult {
62 | const [state, dispatch] = useReducer<
63 | React.Reducer, FormAction>
64 | >(formReducer, {
65 | values: props.initialValues,
66 | });
67 |
68 | const handleChange = (field: string) => (value: any): void => {
69 | dispatch({
70 | type: 'SET_FIELD_VALUE',
71 | payload: {
72 | field,
73 | value,
74 | },
75 | });
76 | };
77 |
78 | const handleSubmit = async () => {
79 | await props.onSubmit?.(state.values);
80 | };
81 |
82 | return {
83 | values: state.values,
84 | handleChange,
85 | handleSubmit,
86 | };
87 | }
88 |
--------------------------------------------------------------------------------
/src/logic/auth/__tests__/useLogin.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderHook, act } from '@testing-library/react-hooks';
3 | import { InMemoryCache } from '@apollo/client';
4 | import { MockedProvider } from '@apollo/client/testing';
5 | import { LoginForm, useLogin } from '../useLogin';
6 | import { CREATE_CUSTOMER_TOKEN } from '../../../apollo/mutations/createCustomerToken';
7 | import { IS_LOGGED_IN } from '../../../apollo/queries/isLoggedIn';
8 |
9 | const initialLoginForm: LoginForm = {
10 | email: '',
11 | password: '',
12 | secureTextEntry: true,
13 | };
14 | const email = 'alexwarner@gmail.com';
15 | const password = 'password123';
16 | const successResponse = {
17 | generateCustomerToken: {
18 | token: '#$Gfh12DF%22kauw',
19 | },
20 | };
21 | const loginMutationMock = {
22 | request: {
23 | query: CREATE_CUSTOMER_TOKEN,
24 | variables: {
25 | email,
26 | password,
27 | },
28 | },
29 | result: {
30 | data: successResponse,
31 | },
32 | };
33 | const loginMutationErrorMock = {
34 | request: {
35 | query: CREATE_CUSTOMER_TOKEN,
36 | variables: {
37 | email,
38 | password,
39 | },
40 | },
41 | error: new Error('Something went wrong'),
42 | };
43 |
44 | describe('useLogin', () => {
45 | function getHookWrapper(mocks: any = []) {
46 | const cache = new InMemoryCache({
47 | addTypename: false,
48 | });
49 | const wrapper = ({
50 | children,
51 | }: {
52 | children: React.ReactElement;
53 | }): React.ReactElement => (
54 |
55 | {children}
56 |
57 | );
58 | const { result, waitForNextUpdate } = renderHook(() => useLogin(), {
59 | wrapper,
60 | });
61 | const getLoggedInStatusFromCache = () =>
62 | cache.readQuery({ query: IS_LOGGED_IN });
63 | // Test the initial state of the request
64 | expect(result.current.loading).toBeFalsy();
65 | expect(result.current.error).toBeUndefined();
66 | expect(result.current.data).toBeUndefined();
67 | expect(result.current.values).toEqual(initialLoginForm);
68 | expect(getLoggedInStatusFromCache()).toBeNull();
69 | expect(typeof result.current.handleChange).toBe('function');
70 | expect(typeof result.current.handleSubmit).toBe('function');
71 |
72 | return { result, waitForNextUpdate, getLoggedInStatusFromCache };
73 | }
74 |
75 | test('should handle change in values', () => {
76 | // Setup
77 | const expectedValues: LoginForm = {
78 | email,
79 | password,
80 | secureTextEntry: false,
81 | };
82 | const { result } = getHookWrapper();
83 |
84 | // Exercise
85 | act(() => {
86 | result.current.handleChange('email')(email);
87 | result.current.handleChange('password')(password);
88 | result.current.handleChange('secureTextEntry')(true); // the value true will be ignored, it will get toggled of previous value
89 | });
90 |
91 | // Verify
92 | expect(result.current.values).toEqual(expectedValues);
93 | });
94 |
95 | test('should handle success', async () => {
96 | // Setup
97 | const {
98 | result,
99 | getLoggedInStatusFromCache,
100 | waitForNextUpdate,
101 | } = getHookWrapper([loginMutationMock]);
102 |
103 | // Enter correct credentials for login
104 | await act(async () => {
105 | result.current.handleChange('email')(email);
106 | result.current.handleChange('password')(password);
107 | await waitForNextUpdate();
108 | await result.current.handleSubmit();
109 | });
110 |
111 | // Verify
112 | expect(result.current.loading).toBeFalsy();
113 | expect(result.current.error).toBeUndefined();
114 | expect(result.current.data).toEqual(successResponse);
115 | expect(getLoggedInStatusFromCache()).toEqual({ isLoggedIn: true });
116 | });
117 |
118 | test('should handle error', async () => {
119 | // Setup
120 | const {
121 | result,
122 | getLoggedInStatusFromCache,
123 | waitForNextUpdate,
124 | } = getHookWrapper([loginMutationErrorMock]);
125 |
126 | // Enter credentials for login
127 | await act(async () => {
128 | result.current.handleChange('email')(email);
129 | result.current.handleChange('password')(password);
130 | await waitForNextUpdate();
131 | await result.current.handleSubmit();
132 | });
133 |
134 | // Verify
135 | expect(result.current.loading).toBeFalsy();
136 | expect(result.current.data).toBeUndefined();
137 | expect(getLoggedInStatusFromCache()).toBeNull();
138 | expect(result.current.error).toEqual(new Error('Something went wrong'));
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/src/logic/auth/__tests__/useLogout.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloClient,
3 | InMemoryCache as Cache,
4 | ApolloLink,
5 | } from '@apollo/client';
6 | import { renderHook, act } from '@testing-library/react-hooks';
7 | import { useLogout } from '../useLogout';
8 | import * as apolloClient from '../../../apollo/client';
9 | import * as storage from '../../utils/storage';
10 | import { IS_LOGGED_IN } from '../../../apollo/queries/isLoggedIn';
11 |
12 | describe('useLogout', () => {
13 | const customerToken = 'dgFt1#$2i1j';
14 |
15 | beforeEach(async () => {
16 | const client = new ApolloClient({
17 | cache: new Cache({
18 | addTypename: false,
19 | }),
20 | link: new ApolloLink(),
21 | });
22 | // Pre-fill apollo-cache to mimic logged in user state
23 | client.writeQuery({
24 | query: IS_LOGGED_IN,
25 | data: {
26 | isLoggedIn: true,
27 | },
28 | });
29 | expect(client.readQuery({ query: IS_LOGGED_IN })).toEqual({
30 | isLoggedIn: true,
31 | });
32 | jest.spyOn(apolloClient, 'getApolloClient').mockResolvedValue(client);
33 | jest.spyOn(storage, 'saveCustomerToken');
34 | jest.spyOn(storage, 'loadCustomerToken');
35 | // Pre-fill the AsyncStorage with customer token to mimic logged in user state
36 | await storage.saveCustomerToken(customerToken);
37 | const customerTokenFromCache = await storage.loadCustomerToken();
38 | expect(customerTokenFromCache).toBe(customerToken);
39 | });
40 |
41 | afterEach(() => {
42 | jest.restoreAllMocks();
43 | });
44 |
45 | test('should clear cache onLogout()', async () => {
46 | // Setup
47 | const client = await apolloClient.getApolloClient();
48 | expect(client).toBeDefined();
49 |
50 | const { result } = renderHook(() => useLogout());
51 | expect(typeof result.current.logout).toBe('function');
52 |
53 | // Exercise
54 | await act(async () => {
55 | await result.current.logout();
56 | });
57 |
58 | // Verify
59 | expect(storage.saveCustomerToken).toHaveBeenCalledWith(null);
60 | const customerTokenFromCache = await storage.loadCustomerToken();
61 | expect(customerTokenFromCache).toBeNull();
62 | expect(client.readQuery({ query: IS_LOGGED_IN })).toBeNull();
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/logic/auth/useLogin.ts:
--------------------------------------------------------------------------------
1 | import { ApolloError, useMutation } from '@apollo/client';
2 | import {
3 | CREATE_CUSTOMER_TOKEN,
4 | CreateCustomerTokenVars,
5 | CreateCustomerTokenDataType,
6 | } from '../../apollo/mutations/createCustomerToken';
7 | import { IS_LOGGED_IN } from '../../apollo/queries/isLoggedIn';
8 | import { useForm, FormResult } from '../app/useForm';
9 | import { saveCustomerToken } from '../utils/storage';
10 |
11 | export interface LoginForm {
12 | email: string;
13 | password: string;
14 | secureTextEntry: boolean;
15 | }
16 |
17 | interface Result extends FormResult {
18 | loading: boolean;
19 | data?: CreateCustomerTokenDataType | null;
20 | error?: ApolloError;
21 | }
22 |
23 | export const useLogin = (): Result => {
24 | const [createCustomerToken, { loading, data, error }] = useMutation<
25 | CreateCustomerTokenDataType,
26 | CreateCustomerTokenVars
27 | >(CREATE_CUSTOMER_TOKEN, {
28 | async update(cache, { data: _data }) {
29 | if (_data?.generateCustomerToken?.token) {
30 | await saveCustomerToken(_data.generateCustomerToken.token);
31 | cache.writeQuery({
32 | query: IS_LOGGED_IN,
33 | data: {
34 | isLoggedIn: true,
35 | },
36 | });
37 | }
38 | },
39 | });
40 | const { values, handleChange, handleSubmit } = useForm({
41 | initialValues: {
42 | email: '',
43 | password: '',
44 | secureTextEntry: true,
45 | },
46 | onSubmit: async _values => {
47 | try {
48 | await createCustomerToken({
49 | variables: {
50 | email: _values.email,
51 | password: _values.password,
52 | },
53 | });
54 | } catch {}
55 | },
56 | });
57 |
58 | return {
59 | values,
60 | data,
61 | error,
62 | loading,
63 | handleChange,
64 | handleSubmit,
65 | };
66 | };
67 |
--------------------------------------------------------------------------------
/src/logic/auth/useLogout.ts:
--------------------------------------------------------------------------------
1 | import { getApolloClient } from '../../apollo/client';
2 | import { saveCustomerToken } from '../utils/storage';
3 |
4 | interface Result {
5 | logout(): Promise;
6 | }
7 |
8 | export const useLogout = (): Result => {
9 | const logout = async () => {
10 | try {
11 | // clear apollo cache
12 | const client = await getApolloClient();
13 | client.resetStore();
14 | await saveCustomerToken(null);
15 | } catch {}
16 | };
17 | return {
18 | logout,
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/logic/auth/useSignup.ts:
--------------------------------------------------------------------------------
1 | import { ApolloError, useMutation } from '@apollo/client';
2 | import {
3 | CREATE_CUSTOMER,
4 | CreateCustomerVars,
5 | CreateCustomerDataType,
6 | } from '../../apollo/mutations/createCustomer';
7 | import { useForm, FormResult } from '../app/useForm';
8 |
9 | export interface SignupForm {
10 | firstName: string;
11 | lastName: string;
12 | email: string;
13 | password: string;
14 | secureTextEntry: boolean;
15 | }
16 |
17 | interface Result extends FormResult {
18 | loading: boolean;
19 | data: CreateCustomerDataType | null | undefined;
20 | error: ApolloError | null | undefined;
21 | }
22 |
23 | export const useSignup = (): Result => {
24 | const [createCustomer, { loading, data, error }] = useMutation<
25 | CreateCustomerDataType,
26 | CreateCustomerVars
27 | >(CREATE_CUSTOMER);
28 | const { values, handleChange, handleSubmit } = useForm({
29 | initialValues: {
30 | firstName: '',
31 | lastName: '',
32 | email: '',
33 | password: '',
34 | secureTextEntry: true,
35 | },
36 | onSubmit: async _values => {
37 | try {
38 | await createCustomer({
39 | variables: {
40 | firstName: _values.firstName,
41 | lastName: _values.lastName,
42 | email: _values.email,
43 | password: _values.password,
44 | },
45 | });
46 | } catch {}
47 | },
48 | });
49 |
50 | return {
51 | values,
52 | data,
53 | error,
54 | loading,
55 | handleChange,
56 | handleSubmit,
57 | };
58 | };
59 |
--------------------------------------------------------------------------------
/src/logic/cart/useCart.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import {
3 | ApolloError,
4 | useLazyQuery,
5 | useMutation,
6 | useQuery,
7 | } from '@apollo/client';
8 | import { showMessage } from 'react-native-flash-message';
9 | import {
10 | IsLoggedInDataType,
11 | IS_LOGGED_IN,
12 | } from '../../apollo/queries/isLoggedIn';
13 | import { GetCartDataType, GET_CART } from '../../apollo/queries/getCart';
14 | import {
15 | AddProductsToCartDataType,
16 | AddProductsToCartVars,
17 | ADD_PRODUCTS_TO_CART,
18 | CartItemInputType,
19 | } from '../../apollo/mutations/addProductsToCart';
20 | import { translate } from '../../i18n';
21 | import { getCartCount } from '../utils/cartHelpers';
22 |
23 | interface Result {
24 | cartCount: string;
25 | cartData: GetCartDataType | undefined;
26 | cartLoading: boolean;
27 | cartError: ApolloError | undefined;
28 | addToCartLoading: boolean;
29 | isLoggedIn: boolean;
30 | addProductsToCart(arg0: CartItemInputType): void;
31 | }
32 |
33 | export const useCart = (): Result => {
34 | const { data: { isLoggedIn = false } = {} } = useQuery(
35 | IS_LOGGED_IN,
36 | );
37 | const [
38 | fetchCart,
39 | { data: cartData, loading: cartLoading, error: cartError },
40 | ] = useLazyQuery(GET_CART);
41 | const [_addProductsToCart, { loading: addToCartLoading }] = useMutation<
42 | AddProductsToCartDataType,
43 | AddProductsToCartVars
44 | >(ADD_PRODUCTS_TO_CART, {
45 | onCompleted() {
46 | showMessage({
47 | message: translate('common.success'),
48 | description: translate('productDetailsScreen.addToCartSuccessful'),
49 | type: 'success',
50 | });
51 | },
52 | onError(_error) {
53 | showMessage({
54 | message: translate('common.error'),
55 | description:
56 | _error.message || translate('productDetailsScreen.addToCartError'),
57 | type: 'danger',
58 | });
59 | },
60 | });
61 | const cartCount: string = getCartCount(cartData?.customerCart?.items?.length);
62 |
63 | useEffect(() => {
64 | if (isLoggedIn) {
65 | fetchCart();
66 | }
67 | }, [isLoggedIn]);
68 |
69 | const addProductsToCart = (productToAdd: CartItemInputType) => {
70 | if (isLoggedIn && cartData?.customerCart.id) {
71 | _addProductsToCart({
72 | variables: {
73 | cartId: cartData.customerCart.id,
74 | cartItems: [productToAdd],
75 | },
76 | });
77 | }
78 | };
79 |
80 | return {
81 | addProductsToCart,
82 | isLoggedIn,
83 | cartCount,
84 | cartData,
85 | cartLoading,
86 | cartError,
87 | addToCartLoading,
88 | };
89 | };
90 |
--------------------------------------------------------------------------------
/src/logic/categories/__tests__/useCategories.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import { renderHook } from '@testing-library/react-hooks';
3 | import { MockedProvider } from '@apollo/client/testing';
4 | import { GET_CATEGORIES } from '../../../apollo/queries/getCategories';
5 | import { useCategories } from '../useCategories';
6 |
7 | const categoryList = [
8 | {
9 | id: '12',
10 | name: 'Pants',
11 | productCount: 0,
12 | childrenCount: 0,
13 | image: '',
14 | productPreviewImage: { items: [] },
15 | },
16 | ];
17 | const categoriesQueryMock = {
18 | request: {
19 | query: GET_CATEGORIES,
20 | variables: {
21 | id: '2',
22 | },
23 | },
24 | result: {
25 | data: {
26 | categoryList: [
27 | {
28 | id: '2',
29 | children: categoryList,
30 | },
31 | ],
32 | },
33 | },
34 | };
35 |
36 | const categoriesQueryErrorMock = {
37 | request: {
38 | query: GET_CATEGORIES,
39 | variables: {
40 | id: '2',
41 | },
42 | },
43 | error: new Error('Something went wrong'),
44 | };
45 |
46 | describe('useCategories', () => {
47 | function getHookWrapper(mocks: any = [], variables: { categoryId: string }) {
48 | const wrapper = ({ children }: { children: ReactElement }) => (
49 |
50 | {children}
51 |
52 | );
53 | const { result, waitForNextUpdate } = renderHook(
54 | () => useCategories(variables),
55 | {
56 | wrapper,
57 | },
58 | );
59 | // Test the initial state of the request
60 | expect(result.current.loading).toBeTruthy();
61 | expect(result.current.error).toBeUndefined();
62 | expect(result.current.categories).toBeUndefined();
63 | return { result, waitForNextUpdate };
64 | }
65 |
66 | test('should return an array of category on success', async () => {
67 | // Setup
68 | const { result, waitForNextUpdate } = getHookWrapper(
69 | [categoriesQueryMock],
70 | { categoryId: '2' },
71 | );
72 |
73 | // Exercise
74 | await waitForNextUpdate();
75 |
76 | // Verify
77 | expect(result.current.loading).toBeFalsy();
78 | expect(result.current.error).toBeUndefined();
79 | expect(result.current.categories).toEqual(categoryList);
80 | });
81 |
82 | test('should return error when request fails', async () => {
83 | // Setup
84 | const { result, waitForNextUpdate } = getHookWrapper(
85 | [categoriesQueryErrorMock],
86 | { categoryId: '2' },
87 | );
88 |
89 | // Exercise
90 | await waitForNextUpdate();
91 |
92 | // Verify
93 | expect(result.current.loading).toBeFalsy();
94 | expect(result.current.error).toBeTruthy();
95 | expect(result.current.categories).toBeUndefined();
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/logic/categories/useCategories.ts:
--------------------------------------------------------------------------------
1 | import { useQuery, ApolloError } from '@apollo/client';
2 | import {
3 | GET_CATEGORIES,
4 | CategoryType,
5 | GetCategoriesVars,
6 | CategoriesDataType,
7 | } from '../../apollo/queries/getCategories';
8 |
9 | interface Props {
10 | categoryId: string;
11 | }
12 |
13 | interface Result {
14 | categories?: Array;
15 | loading: boolean;
16 | error: ApolloError | undefined;
17 | }
18 |
19 | export const useCategories = ({ categoryId: id }: Props): Result => {
20 | const { loading, data, error } = useQuery<
21 | CategoriesDataType,
22 | GetCategoriesVars
23 | >(GET_CATEGORIES, {
24 | variables: {
25 | id,
26 | },
27 | });
28 |
29 | return {
30 | categories: data?.categoryList?.[0]?.children,
31 | loading,
32 | error,
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/src/logic/index.ts:
--------------------------------------------------------------------------------
1 | export * from './app/useForm';
2 | export * from './categories/useCategories';
3 | export * from './products/useCategoryProducts';
4 | export * from './products/useSearch';
5 | export * from './products/useSort';
6 | export * from './products/useProductDetails';
7 | export * from './auth/useLogin';
8 | export * from './auth/useLogout';
9 | export * from './auth/useSignup';
10 | export * from './profile/useCustomer';
11 | export * from './utils/price';
12 | export * from './utils/loginPrompt';
13 | export * from './utils/storage';
14 |
--------------------------------------------------------------------------------
/src/logic/products/__tests__/useProductDetails.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderHook } from '@testing-library/react-hooks';
3 | import { MockedProvider } from '@apollo/client/testing';
4 | import { useProductDetails } from '../useProductDetails';
5 | import {
6 | GET_PRODUCT_DETAILS,
7 | ProductTypeEnum,
8 | } from '../../../apollo/queries/getProductDetails';
9 |
10 | const sku = 'MH01';
11 | const product = {
12 | id: 1,
13 | sku,
14 | name: 'Green Jacket',
15 | type: ProductTypeEnum.CONFIGURED,
16 | description: {
17 | html: 'Product Description',
18 | },
19 | priceRange: {
20 | maximumPrice: {
21 | finalPrice: {
22 | currency: 'USD',
23 | value: 29.99,
24 | },
25 | },
26 | },
27 | mediaGallery: [],
28 | configurableOptions: [
29 | {
30 | id: 23,
31 | label: 'Color',
32 | position: 0,
33 | values: [],
34 | },
35 | ],
36 | };
37 | const productDetailsQueryMock = {
38 | request: {
39 | query: GET_PRODUCT_DETAILS,
40 | variables: { sku },
41 | },
42 | result: {
43 | data: {
44 | products: {
45 | items: [product],
46 | },
47 | },
48 | },
49 | };
50 |
51 | const productDetailsErrorMock = {
52 | request: {
53 | query: GET_PRODUCT_DETAILS,
54 | variables: { sku },
55 | },
56 | error: new Error('Something went wrong'),
57 | };
58 |
59 | describe('useProductDetails', () => {
60 | function getHookWrapper(mocks: any = [], variables: { sku: string }) {
61 | const wrapper = ({
62 | children,
63 | }: {
64 | children: React.ReactElement;
65 | }): React.ReactElement => (
66 |
74 | {children}
75 |
76 | );
77 | const { result, waitForNextUpdate } = renderHook(
78 | () => useProductDetails(variables),
79 | {
80 | wrapper,
81 | },
82 | );
83 | // Test the initial state of the request
84 | expect(result.current.loading).toBeTruthy();
85 | expect(result.current.error).toBeUndefined();
86 | expect(result.current.productDetails).toBeUndefined();
87 |
88 | return { result, waitForNextUpdate };
89 | }
90 |
91 | test('should return product details on success', async () => {
92 | // Setup
93 | const { result, waitForNextUpdate } = getHookWrapper(
94 | [productDetailsQueryMock],
95 | { sku },
96 | );
97 |
98 | // Exercise
99 | await waitForNextUpdate();
100 |
101 | // Verify
102 | expect(result.current.loading).toBeFalsy();
103 | expect(result.current.error).toBeUndefined();
104 | expect(result.current.productDetails).toEqual(product);
105 | });
106 |
107 | test('should return error when request fails', async () => {
108 | // Setup
109 | const { result, waitForNextUpdate } = getHookWrapper(
110 | [productDetailsErrorMock],
111 | { sku },
112 | );
113 |
114 | // Exercise
115 | await waitForNextUpdate();
116 |
117 | // Verify
118 | expect(result.current.loading).toBeFalsy();
119 | expect(result.current.error).toBeTruthy();
120 | expect(result.current.productDetails).toBeUndefined();
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/src/logic/products/useCategoryProducts.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useQuery, ApolloError, NetworkStatus } from '@apollo/client';
3 | import {
4 | GET_CATGEORY_PRODUCTS,
5 | GetCategoryProductsVars,
6 | CategoryProductsDataType,
7 | SortEnum,
8 | } from '../../apollo/queries/getCategoryProducts';
9 | import { LIMITS } from '../../constants';
10 |
11 | interface Props {
12 | categoryId: string;
13 | }
14 |
15 | interface Result {
16 | data: CategoryProductsDataType | undefined;
17 | networkStatus: NetworkStatus;
18 | error: ApolloError | undefined;
19 | refresh: (arg0?: { name?: SortEnum; price?: SortEnum }) => void;
20 | loadMore(): void;
21 | }
22 |
23 | export const useCategoryProducts = ({ categoryId: id }: Props): Result => {
24 | const [currentPage, setCurrentPage] = useState(1);
25 |
26 | const { refetch, loading, data, error, fetchMore, networkStatus } = useQuery<
27 | CategoryProductsDataType,
28 | GetCategoryProductsVars
29 | >(GET_CATGEORY_PRODUCTS, {
30 | variables: {
31 | id,
32 | pageSize: LIMITS.categoryProductsPageSize,
33 | currentPage: 1,
34 | },
35 | notifyOnNetworkStatusChange: true,
36 | });
37 |
38 | useEffect(() => {
39 | if (!loading && currentPage !== 1) {
40 | fetchMore({
41 | variables: {
42 | currentPage,
43 | },
44 | });
45 | }
46 | }, [currentPage]);
47 |
48 | const loadMore = () => {
49 | if (loading) {
50 | return;
51 | }
52 |
53 | if (
54 | currentPage * LIMITS.categoryProductsPageSize ===
55 | data?.products?.items?.length &&
56 | data?.products?.items.length < data?.products?.totalCount
57 | ) {
58 | setCurrentPage(prevState => prevState + 1);
59 | }
60 | };
61 |
62 | const refresh = ({
63 | price,
64 | name,
65 | }: { price?: SortEnum; name?: SortEnum } = {}) => {
66 | refetch({
67 | price,
68 | name,
69 | });
70 | setCurrentPage(1);
71 | };
72 |
73 | return {
74 | data,
75 | networkStatus,
76 | error,
77 | refresh,
78 | loadMore,
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/src/logic/products/useProductDetails.ts:
--------------------------------------------------------------------------------
1 | import { useQuery, ApolloError } from '@apollo/client';
2 | import { useState, useEffect, useReducer } from 'react';
3 | import {
4 | GET_PRODUCT_DETAILS,
5 | GetProductDetailsVars,
6 | ProductDetailsDataType,
7 | ProductDetailsType,
8 | ProductTypeEnum,
9 | } from '../../apollo/queries/getProductDetails';
10 | import type { ConfigurableProductVariant } from '../../apollo/queries/configurableProductFragment';
11 | import { PriceRangeType } from '../../apollo/queries/productPriceFragment';
12 | import type { MediaGalleryItemType } from '../../apollo/queries/mediaGalleryFragment';
13 |
14 | interface Props {
15 | sku: string;
16 | }
17 |
18 | export type SelectedConfigurableProductOptions = { [key: string]: number };
19 |
20 | export type HandleSelectedConfigurableOptions = (
21 | optionCode: string,
22 | valueIndex: number,
23 | ) => void;
24 |
25 | interface Result extends ProductState {
26 | productDetails?: ProductDetailsType | null;
27 | loading: boolean;
28 | error: ApolloError | undefined;
29 | selectedConfigurableProductOptions: SelectedConfigurableProductOptions;
30 | handleSelectedConfigurableOptions: HandleSelectedConfigurableOptions;
31 | }
32 |
33 | interface ProductState {
34 | priceRange: PriceRangeType | null;
35 | mediaGallery: Array;
36 | }
37 |
38 | const findSelectedProductVariant = (
39 | selectedConfigurableProductOptions: SelectedConfigurableProductOptions,
40 | productData: ProductDetailsType,
41 | ): ConfigurableProductVariant | null => {
42 | if (productData.type !== ProductTypeEnum.CONFIGURED) {
43 | return null;
44 | }
45 | let variants = productData.variants;
46 | Object.keys(selectedConfigurableProductOptions).forEach(code => {
47 | variants = variants.filter(variant => {
48 | const attribute = variant.attributes.find(attr => attr.code === code);
49 | return attribute?.valueIndex === selectedConfigurableProductOptions[code];
50 | });
51 | });
52 | return variants?.[0];
53 | };
54 |
55 | export const useProductDetails = ({ sku }: Props): Result => {
56 | const [
57 | selectedConfigurableProductOptions,
58 | setSelectedConfigurableProductOptions,
59 | ] = useState({});
60 | const [
61 | selectedVariant,
62 | setSelectedVariant,
63 | ] = useState(null);
64 | const [{ priceRange, mediaGallery }, setState] = useReducer<
65 | React.Reducer
66 | >((prevState, newState) => ({ ...prevState, ...newState }), {
67 | priceRange: null,
68 | mediaGallery: [],
69 | });
70 | const { data, loading, error } = useQuery<
71 | ProductDetailsDataType,
72 | GetProductDetailsVars
73 | >(GET_PRODUCT_DETAILS, {
74 | variables: {
75 | sku,
76 | },
77 | });
78 |
79 | useEffect(() => {
80 | // User has selected configurable options, find the matching simple product
81 | if (
82 | data?.products?.items?.[0] &&
83 | Object.keys(selectedConfigurableProductOptions).length > 0
84 | ) {
85 | const variant = findSelectedProductVariant(
86 | selectedConfigurableProductOptions,
87 | data?.products?.items?.[0],
88 | );
89 | setSelectedVariant(variant);
90 | }
91 | }, [data, selectedConfigurableProductOptions]);
92 |
93 | useEffect(() => {
94 | if (data?.products?.items?.[0]) {
95 | if (selectedVariant) {
96 | setState({
97 | priceRange: selectedVariant.product.priceRange,
98 | mediaGallery: [
99 | ...selectedVariant.product.mediaGallery,
100 | ...data?.products?.items?.[0].mediaGallery,
101 | ],
102 | });
103 | } else {
104 | setState({
105 | priceRange: data?.products?.items?.[0].priceRange,
106 | mediaGallery: data?.products?.items?.[0].mediaGallery,
107 | });
108 | }
109 | }
110 | }, [data, selectedVariant]);
111 |
112 | const handleSelectedConfigurableOptions: HandleSelectedConfigurableOptions = (
113 | optionCode,
114 | valueIndex,
115 | ) => {
116 | setSelectedConfigurableProductOptions(prevState => ({
117 | ...prevState,
118 | [optionCode]: valueIndex,
119 | }));
120 | };
121 |
122 | return {
123 | productDetails: data?.products?.items?.[0],
124 | priceRange,
125 | mediaGallery,
126 | loading,
127 | error,
128 | selectedConfigurableProductOptions,
129 | handleSelectedConfigurableOptions,
130 | };
131 | };
132 |
--------------------------------------------------------------------------------
/src/logic/products/useSearch.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { ApolloError, NetworkStatus, useLazyQuery } from '@apollo/client';
3 | import {
4 | GetSearchProductsVars,
5 | GET_SEARCH_PRODUCTS,
6 | SearchProductsDataType,
7 | } from '../../apollo/queries/getSearchProducts';
8 | import { LIMITS } from '../../constants';
9 |
10 | interface Result {
11 | data: SearchProductsDataType | undefined;
12 | networkStatus: NetworkStatus;
13 | called: boolean;
14 | error?: ApolloError;
15 | searchText: string;
16 | handleChange(arg1: string): void;
17 | getSearchProducts(): void;
18 | loadMore(): void;
19 | }
20 |
21 | export const useSearch = (): Result => {
22 | const [searchText, handleChange] = useState('');
23 | const [currentPage, setCurrentPage] = useState(1);
24 | const [
25 | getSearchProducts,
26 | { called, loading, error, networkStatus, fetchMore, data },
27 | ] = useLazyQuery(
28 | GET_SEARCH_PRODUCTS,
29 | {
30 | notifyOnNetworkStatusChange: true,
31 | },
32 | );
33 |
34 | useEffect(() => {
35 | if (searchText.trim().length < LIMITS.searchTextMinLength) {
36 | // Don't do anything
37 | return;
38 | }
39 | const task = setTimeout(() => {
40 | getSearchProducts({
41 | variables: {
42 | searchText,
43 | pageSize: LIMITS.searchScreenPageSize,
44 | currentPage: 1,
45 | },
46 | });
47 | setCurrentPage(1);
48 | }, LIMITS.autoSearchApiTimeDelay);
49 |
50 | // eslint-disable-next-line consistent-return
51 | return () => {
52 | clearTimeout(task);
53 | };
54 | }, [searchText]);
55 |
56 | useEffect(() => {
57 | if (currentPage === 1) return;
58 | fetchMore?.({
59 | variables: {
60 | currentPage,
61 | },
62 | });
63 | }, [currentPage]);
64 |
65 | const loadMore = () => {
66 | if (loading) {
67 | return;
68 | }
69 |
70 | if (
71 | currentPage * LIMITS.searchScreenPageSize ===
72 | data?.products?.items?.length &&
73 | data?.products?.items.length < data?.products?.totalCount
74 | ) {
75 | setCurrentPage(prevPage => prevPage + 1);
76 | }
77 | };
78 |
79 | return {
80 | data,
81 | networkStatus,
82 | called,
83 | error,
84 | searchText,
85 | loadMore,
86 | handleChange,
87 | getSearchProducts,
88 | };
89 | };
90 |
--------------------------------------------------------------------------------
/src/logic/products/useSort.ts:
--------------------------------------------------------------------------------
1 | import { useMemo, useContext, useReducer } from 'react';
2 | import { TextStyle, ViewStyle } from 'react-native';
3 | import { ThemeContext } from 'react-native-elements';
4 | import { SortEnum } from '../../apollo/queries/getCategoryProducts';
5 | import { translate } from '../../i18n';
6 |
7 | interface Props {
8 | onPress(arg0: any): void;
9 | }
10 |
11 | interface Result {
12 | isVisible: boolean;
13 | selectedIndex: number;
14 | setVisible(arg1: boolean): void;
15 | sortOptions: Array<{
16 | title: string;
17 | titleStyle?: TextStyle;
18 | containerStyle?: ViewStyle;
19 | onPress?(): void;
20 | }>;
21 | }
22 |
23 | interface SortState {
24 | isVisible: boolean;
25 | selectedIndex: number;
26 | }
27 |
28 | export const useSort = ({ onPress }: Props): Result => {
29 | const [state, setState] = useReducer<
30 | React.Reducer
31 | >((prevState, newState) => ({ ...prevState, ...newState }), {
32 | isVisible: false,
33 | selectedIndex: -1,
34 | });
35 | const { theme } = useContext(ThemeContext);
36 | const sortOptions = useMemo(
37 | () => [
38 | {
39 | title: translate('common.aToZ'),
40 | onPress: () => {
41 | onPress({ name: SortEnum.ASC });
42 | setState({ isVisible: false, selectedIndex: 0 });
43 | },
44 | },
45 | {
46 | title: translate('common.zToA'),
47 | onPress: () => {
48 | onPress({ name: SortEnum.DESC });
49 | setState({ isVisible: false, selectedIndex: 1 });
50 | },
51 | },
52 | {
53 | title: translate('common.lowToHigh'),
54 | onPress: () => {
55 | onPress({ price: SortEnum.ASC });
56 | setState({ isVisible: false, selectedIndex: 2 });
57 | },
58 | },
59 | {
60 | title: translate('common.highToLow'),
61 | onPress: () => {
62 | onPress({ price: SortEnum.DESC });
63 | setState({ isVisible: false, selectedIndex: 3 });
64 | },
65 | },
66 | {
67 | title: translate('common.cancel'),
68 | containerStyle: { backgroundColor: theme.colors?.error },
69 | titleStyle: { color: 'white' },
70 | onPress: () => setState({ isVisible: false }),
71 | },
72 | ],
73 | [theme, onPress],
74 | );
75 |
76 | const setVisible = (visible: boolean) => setState({ isVisible: visible });
77 |
78 | return {
79 | isVisible: state.isVisible,
80 | selectedIndex: state.selectedIndex,
81 | setVisible,
82 | sortOptions,
83 | };
84 | };
85 |
--------------------------------------------------------------------------------
/src/logic/profile/__tests__/useCustomer.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import { renderHook, act } from '@testing-library/react-hooks';
3 | import { MockedProvider } from '@apollo/client/testing';
4 | import { GET_CUSTOMER } from '../../../apollo/queries/getCustomer';
5 | import { useCustomer } from '../useCustomer';
6 |
7 | const customer = {
8 | firstName: 'Alex',
9 | lastName: 'Warner',
10 | email: 'alexwarner@gmail.com',
11 | };
12 | const customerQueryMock = {
13 | request: {
14 | query: GET_CUSTOMER,
15 | },
16 | result: {
17 | data: {
18 | customer,
19 | },
20 | },
21 | };
22 |
23 | const customerQueryErrorMock = {
24 | request: {
25 | query: GET_CUSTOMER,
26 | },
27 | error: new Error('Something went wrong'),
28 | };
29 |
30 | describe('useCustomer', () => {
31 | function getHookWrapper(mocks: any = []) {
32 | const wrapper = ({ children }: { children: ReactElement }) => (
33 |
34 | {children}
35 |
36 | );
37 | const { result, waitForNextUpdate } = renderHook(() => useCustomer(), {
38 | wrapper,
39 | });
40 | // Test the initial state of the request
41 | expect(result.current.loading).toBeFalsy();
42 | expect(result.current.error).toBeUndefined();
43 | expect(result.current.data).toBeUndefined();
44 | expect(typeof result.current.getCustomer).toBe('function');
45 |
46 | return { result, waitForNextUpdate };
47 | }
48 |
49 | test('should return customer on success', async () => {
50 | // Setup
51 | const { result, waitForNextUpdate } = getHookWrapper([customerQueryMock]);
52 |
53 | // Exercise
54 | await act(async () => {
55 | await result.current.getCustomer();
56 | await waitForNextUpdate();
57 | });
58 |
59 | // Verify
60 | expect(result.current.loading).toBeFalsy();
61 | expect(result.current.error).toBeUndefined();
62 | expect(result.current.data).toEqual({ customer });
63 | });
64 |
65 | test('should return error when request fails', async () => {
66 | // Setup
67 | const { result, waitForNextUpdate } = getHookWrapper([
68 | customerQueryErrorMock,
69 | ]);
70 |
71 | // Exercise
72 | await act(async () => {
73 | await result.current.getCustomer();
74 | await waitForNextUpdate();
75 | });
76 |
77 | // Verify
78 | expect(result.current.loading).toBeFalsy();
79 | expect(result.current.error).toBeTruthy();
80 | expect(result.current.data).toBeUndefined();
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/src/logic/profile/useCustomer.ts:
--------------------------------------------------------------------------------
1 | import { useLazyQuery, ApolloError } from '@apollo/client';
2 | import {
3 | GetCustomerDataType,
4 | GET_CUSTOMER,
5 | } from '../../apollo/queries/getCustomer';
6 |
7 | interface Result {
8 | getCustomer(): void;
9 | data: GetCustomerDataType | undefined;
10 | loading: boolean;
11 | error: ApolloError | undefined;
12 | }
13 |
14 | export const useCustomer = (): Result => {
15 | const [
16 | getCustomer,
17 | { data, loading, error },
18 | ] = useLazyQuery(GET_CUSTOMER);
19 |
20 | return {
21 | getCustomer,
22 | data,
23 | loading,
24 | error,
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/src/logic/utils/__tests__/cartHelpers.test.ts:
--------------------------------------------------------------------------------
1 | import { getCartCount } from '../cartHelpers';
2 |
3 | describe('cartHelpers.js', () => {
4 | describe('getCartCount()', () => {
5 | test('should return empty string on false values', () => {
6 | // Setup
7 | const falseValues = [undefined, 0];
8 |
9 | // Exercise
10 | const resultArray = falseValues.map(value => getCartCount(value));
11 |
12 | // verify
13 | resultArray.forEach(result => expect(result).toBe(''));
14 | });
15 |
16 | test('should return string for truthy values', () => {
17 | // Setup
18 | const cartCount1 = 5;
19 | const cartCount2 = 100;
20 | const cartCount3 = 101;
21 | const expectedResult1 = '5';
22 | const expectedResult2 = '100';
23 | const expectedResult3 = '100+';
24 |
25 | // Exercise
26 | const result1 = getCartCount(cartCount1);
27 | const result2 = getCartCount(cartCount2);
28 | const result3 = getCartCount(cartCount3);
29 |
30 | // verify
31 | expect(result1).toBe(expectedResult1);
32 | expect(result2).toBe(expectedResult2);
33 | expect(result3).toBe(expectedResult3);
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/logic/utils/__tests__/price.test.ts:
--------------------------------------------------------------------------------
1 | import { getCurrencySymbolFromCode, formatPrice } from '../price';
2 | import { currencySymbols } from '../../../../magento.config';
3 |
4 | describe('price.js', () => {
5 | describe('getCurrencySymbolFromCode()', () => {
6 | test('should return correct currency symbol', () => {
7 | // Setup
8 | const currencyCode = 'USD';
9 | const expectedCurrencySymbol = currencySymbols[currencyCode];
10 |
11 | // Exercise
12 | const result = getCurrencySymbolFromCode(currencyCode);
13 |
14 | // verify
15 | expect(result).toBe(expectedCurrencySymbol);
16 | });
17 |
18 | test('should return currency code, if currency symbol not available', () => {
19 | // Setup
20 | const currencyCode = 'WWW';
21 | const expectedCurrencySymbol = 'WWW';
22 |
23 | // Exercise
24 | const result = getCurrencySymbolFromCode(currencyCode);
25 |
26 | // verify
27 | expect(result).toBe(expectedCurrencySymbol);
28 | });
29 | });
30 |
31 | describe('formatPrice()', () => {
32 | test('should return correct string', () => {
33 | // Setup
34 | const currencyCode = 'USD';
35 | const value = 29.99;
36 | const price = {
37 | currency: currencyCode,
38 | value,
39 | };
40 | const expectedResult = `${currencySymbols[currencyCode]} ${value}`;
41 |
42 | // Exercise
43 | const result = formatPrice(price);
44 |
45 | // Verify
46 | expect(result).toBe(expectedResult);
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/src/logic/utils/__tests__/storage.test.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage';
2 | import {
3 | AsyncStorageKeys,
4 | saveCustomerToken,
5 | loadCustomerToken,
6 | } from '../storage';
7 |
8 | describe('storage.ts', () => {
9 | beforeEach(() => {
10 | AsyncStorage.clear();
11 | });
12 |
13 | test('should save customer token', async () => {
14 | // Setup
15 | const customerToken = '123abc';
16 |
17 | // Exercise
18 | const status = await saveCustomerToken(customerToken);
19 |
20 | // Verify
21 | expect(status).toBe(true);
22 | expect(AsyncStorage.setItem).toHaveBeenCalledWith(
23 | AsyncStorageKeys.CustomerToken,
24 | customerToken,
25 | );
26 | });
27 |
28 | test('should load customer token', async () => {
29 | // setup
30 | const customerToken = 'abc123';
31 | await AsyncStorage.setItem(AsyncStorageKeys.CustomerToken, customerToken);
32 |
33 | // Exercise
34 | const result = await loadCustomerToken();
35 |
36 | // Verify
37 | expect(result).toBe(customerToken);
38 | expect(AsyncStorage.getItem).toHaveBeenCalledWith(
39 | AsyncStorageKeys.CustomerToken,
40 | );
41 | });
42 |
43 | test('should remove customer token', async () => {
44 | // setup
45 | const customerToken = 'abc123';
46 | await AsyncStorage.setItem(AsyncStorageKeys.CustomerToken, customerToken);
47 |
48 | // Exercise
49 | const result = await saveCustomerToken(null);
50 |
51 | // Verify
52 | expect(result).toBe(true);
53 | expect(AsyncStorage.removeItem).toHaveBeenCalledWith(
54 | AsyncStorageKeys.CustomerToken,
55 | );
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/logic/utils/cartHelpers.ts:
--------------------------------------------------------------------------------
1 | export const getCartCount = (count: number | undefined): string => {
2 | let displayText = '';
3 | if (count) {
4 | if (count <= 100) {
5 | displayText = `${count}`;
6 | } else {
7 | displayText = '100+';
8 | }
9 | }
10 |
11 | return displayText;
12 | };
13 |
--------------------------------------------------------------------------------
/src/logic/utils/loginPrompt.ts:
--------------------------------------------------------------------------------
1 | import { Alert } from 'react-native';
2 | import { StackNavigationProp } from '@react-navigation/stack';
3 | import { translate } from '../../i18n';
4 | import { AppStackParamList, Routes } from '../../navigation';
5 |
6 | export const showLoginPrompt = (
7 | message: string,
8 | navigation: StackNavigationProp,
9 | ): void => {
10 | Alert.alert(
11 | translate('common.dearUser'),
12 | message,
13 | [
14 | {
15 | text: translate('common.login'),
16 | onPress: () =>
17 | navigation.navigate(
18 | Routes.NAVIGATION_TO_AUTHENTICATION_SPLASH_SCREEN,
19 | { screen: Routes.NAVIGATION_TO_LOGIN_SCREEN },
20 | ),
21 | },
22 | {
23 | text: translate('common.signup'),
24 | onPress: () =>
25 | navigation.navigate(
26 | Routes.NAVIGATION_TO_AUTHENTICATION_SPLASH_SCREEN,
27 | { screen: Routes.NAVIGATION_TO_SIGNUP_SCREEN },
28 | ),
29 | },
30 | ],
31 | { cancelable: true },
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/logic/utils/price.ts:
--------------------------------------------------------------------------------
1 | import { currencySymbols } from '../../../magento.config';
2 |
3 | export const getCurrencySymbolFromCode = (currencyCode: string) => {
4 | return currencySymbols[currencyCode] ?? currencyCode;
5 | };
6 |
7 | export const formatPrice = ({
8 | currency,
9 | value,
10 | }: {
11 | currency: string;
12 | value: number;
13 | }): string => `${getCurrencySymbolFromCode(currency)} ${value}`;
14 |
--------------------------------------------------------------------------------
/src/logic/utils/storage.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage';
2 |
3 | export enum AsyncStorageKeys {
4 | CustomerToken = 'customer_token',
5 | }
6 |
7 | // If a valid string is passed then only it is stored, else key is removed
8 | const saveValue = async (
9 | key: string,
10 | value?: string | null,
11 | ): Promise => {
12 | try {
13 | if (typeof value === 'string' && value !== '') {
14 | await AsyncStorage.setItem(key, value);
15 | } else {
16 | await AsyncStorage.removeItem(key);
17 | }
18 | return true;
19 | } catch (e) {
20 | // saving error
21 | return false;
22 | }
23 | };
24 |
25 | const loadValue = async (key: string): Promise => {
26 | try {
27 | const value = await AsyncStorage.getItem(key);
28 | return value;
29 | } catch (e) {
30 | // error reading value
31 | return null;
32 | }
33 | };
34 |
35 | export const saveCustomerToken = async (token?: string | null) =>
36 | saveValue(AsyncStorageKeys.CustomerToken, token);
37 |
38 | export const loadCustomerToken = async () =>
39 | loadValue(AsyncStorageKeys.CustomerToken);
40 |
--------------------------------------------------------------------------------
/src/navigation/AuthenticationNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStackNavigator } from '@react-navigation/stack';
3 | import { Routes } from './routeNames';
4 | import { translate } from '../i18n';
5 | import { LoginScreen, SignupScreen } from '../screens';
6 |
7 | export type AutheticationStackParamList = {
8 | [Routes.NAVIGATION_TO_LOGIN_SCREEN]: undefined;
9 | [Routes.NAVIGATION_TO_SIGNUP_SCREEN]: undefined;
10 | };
11 |
12 | const AuthStack = createStackNavigator();
13 |
14 | const AuthenticationNavigator = () => (
15 |
16 |
23 |
30 |
31 | );
32 |
33 | export default AuthenticationNavigator;
34 |
--------------------------------------------------------------------------------
/src/navigation/BottomTabNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { useQuery } from '@apollo/client';
3 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
4 | import { StackNavigationProp } from '@react-navigation/stack';
5 | import { Icon, ThemeContext } from 'react-native-elements';
6 | import { translate } from '../i18n';
7 | import { HomeScreen, CartScreen, ProfileScreen } from '../screens';
8 | import { Routes } from './routeNames';
9 | import type { AppStackParamList } from './StackNavigator';
10 | import { IS_LOGGED_IN, IsLoggedInDataType } from '../apollo/queries/isLoggedIn';
11 | import { showLoginPrompt } from '../logic';
12 | import { useCart } from '../logic/cart/useCart';
13 |
14 | export type BottomTabNavigatorParamList = {
15 | [Routes.NAVIGATION_TO_HOME_SCREEN]: undefined;
16 | [Routes.NAVIGATION_TO_PROFILE_SCREEN]: undefined;
17 | [Routes.NAVIGATION_TO_CART_SCREEN]: undefined;
18 | };
19 |
20 | const Tab = createBottomTabNavigator();
21 |
22 | type Props = {
23 | navigation: StackNavigationProp<
24 | AppStackParamList,
25 | Routes.NAVIGATION_TO_HOME_SCREEN
26 | >;
27 | };
28 |
29 | const BottomTabNavigator = ({ navigation }: Props) => {
30 | const { data } = useQuery(IS_LOGGED_IN);
31 | const { cartCount } = useCart();
32 | const { theme } = useContext(ThemeContext);
33 |
34 | return (
35 |
36 | (
42 |
43 | ),
44 | }}
45 | />
46 | (
52 |
53 | ),
54 | }}
55 | listeners={{
56 | tabPress: e => {
57 | if (!data?.isLoggedIn) {
58 | // Prevent default action
59 | e.preventDefault();
60 | showLoginPrompt(
61 | translate('profileScreen.guestUserPromptMessage'),
62 | navigation,
63 | );
64 | }
65 | },
66 | }}
67 | />
68 | (
74 |
75 | ),
76 | tabBarBadgeStyle: {
77 | backgroundColor: theme.colors?.error,
78 | },
79 | ...(cartCount !== '' && { tabBarBadge: cartCount }),
80 | }}
81 | listeners={{
82 | tabPress: e => {
83 | if (!data?.isLoggedIn) {
84 | // Prevent default action
85 | e.preventDefault();
86 | showLoginPrompt(
87 | translate('cartScreen.guestUserPromptMessage'),
88 | navigation,
89 | );
90 | }
91 | },
92 | }}
93 | />
94 |
95 | );
96 | };
97 |
98 | export default BottomTabNavigator;
99 |
--------------------------------------------------------------------------------
/src/navigation/RootNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useColorScheme } from 'react-native';
3 | import { NavigationContainer } from '@react-navigation/native';
4 | import { createDrawerNavigator } from '@react-navigation/drawer';
5 | import StackNavigator from './StackNavigator';
6 | import { DrawerScreen } from '../screens';
7 | import { navigationLightTheme, navigationDarkTheme } from '../theme';
8 |
9 | const Drawer = createDrawerNavigator();
10 |
11 | const RootNavigator = () => {
12 | const scheme = useColorScheme();
13 | return (
14 |
17 | }>
18 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default RootNavigator;
31 |
--------------------------------------------------------------------------------
/src/navigation/StackNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStackNavigator } from '@react-navigation/stack';
3 | import BottomTabNavigator from './BottomTabNavigator';
4 | import { NavigatorScreenParams } from '@react-navigation/native';
5 | import AuthenticationNavigator from './AuthenticationNavigator';
6 | import { CustomHeaderButtons, CustomHeaderItem } from '../components';
7 | import { Routes } from './routeNames';
8 | import { translate } from '../i18n';
9 | import {
10 | SearchScreen,
11 | CategoriesScreen,
12 | ProductListScreen,
13 | ProductDetailsScreen,
14 | } from '../screens';
15 |
16 | export type AppStackParamList = {
17 | [Routes.NAVIGATION_TO_HOME_SCREEN]: undefined;
18 | [Routes.NAVIGATION_TO_AUTHENTICATION_SPLASH_SCREEN]: NavigatorScreenParams;
19 | [Routes.NAVIGATION_TO_CATEGORIES_SCREEN]: {
20 | categoryId: string;
21 | name: string;
22 | };
23 | [Routes.NAVIGATION_TO_PRODUCT_LIST_SCREEN]: {
24 | categoryId: string;
25 | name: string;
26 | };
27 | [Routes.NAVIGATION_TO_PRODUCT_DETAILS_SCREEN]: {
28 | name: string;
29 | sku: string;
30 | };
31 | [Routes.NAVIGATION_TO_SEARCH_SCREEN]: undefined;
32 | };
33 |
34 | const Stack = createStackNavigator();
35 |
36 | const StackNavigator = () => (
37 |
38 | ({
42 | title: translate('common.brand'),
43 | headerLeft: () => (
44 |
45 |
50 |
51 | ),
52 | headerRight: () => (
53 |
54 |
58 | navigation.navigate(Routes.NAVIGATION_TO_SEARCH_SCREEN)
59 | }
60 | />
61 |
62 | ),
63 | })}
64 | />
65 |
72 | {
76 | const {
77 | params: { name },
78 | } = route;
79 | return {
80 | title: name ?? translate('categoriesScreen.appbarTitle'),
81 | };
82 | }}
83 | />
84 | {
88 | const {
89 | params: { name },
90 | } = route;
91 | return {
92 | title: name ?? translate('productListScreen.appbarTitle'),
93 | };
94 | }}
95 | />
96 | {
100 | const {
101 | params: { name },
102 | } = route;
103 | return {
104 | title: name ?? translate('productDetailsScreen.appbarTitle'),
105 | };
106 | }}
107 | />
108 |
115 |
116 | );
117 |
118 | export default StackNavigator;
119 |
--------------------------------------------------------------------------------
/src/navigation/index.ts:
--------------------------------------------------------------------------------
1 | import RootNavigator from './RootNavigator';
2 | import type { BottomTabNavigatorParamList } from './BottomTabNavigator';
3 | import type { AutheticationStackParamList } from './AuthenticationNavigator';
4 | import type { AppStackParamList } from './StackNavigator';
5 |
6 | /**
7 | * Only one default export will be there,
8 | * which will be a top level navigator.
9 | */
10 | export default RootNavigator;
11 | export * from './routeNames';
12 | export type {
13 | AppStackParamList,
14 | BottomTabNavigatorParamList,
15 | AutheticationStackParamList,
16 | };
17 |
--------------------------------------------------------------------------------
/src/navigation/routeNames.ts:
--------------------------------------------------------------------------------
1 | export enum Routes {
2 | NAVIGATION_TO_HOME_SCREEN = 'HomeScreen',
3 | NAVIGATION_TO_CATEGORIES_SCREEN = 'CategoriesScreen',
4 | NAVIGATION_TO_CART_SCREEN = 'CartScreen',
5 | NAVIGATION_TO_PROFILE_SCREEN = 'ProfileScreen',
6 | NAVIGATION_TO_PRODUCT_LIST_SCREEN = 'ProductListScreen',
7 | NAVIGATION_TO_PRODUCT_DETAILS_SCREEN = 'ProductDetailsScreen',
8 | NAVIGATION_TO_AUTHENTICATION_SPLASH_SCREEN = 'AuthenticationSplashScreen',
9 | NAVIGATION_TO_LOGIN_SCREEN = 'LoginScreen',
10 | NAVIGATION_TO_SIGNUP_SCREEN = 'SignupScreen',
11 | NAVIGATION_TO_SEARCH_SCREEN = 'SearchScreen',
12 | }
13 |
--------------------------------------------------------------------------------
/src/screens/CartScreen/CartFooter.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useMemo } from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 | import { Button, Text, ThemeContext } from 'react-native-elements';
4 | import { DIMENS, SPACING } from '../../constants';
5 | import { translate } from '../../i18n';
6 | import { formatPrice } from '../../logic';
7 |
8 | type Props = {
9 | grandTotal?: {
10 | currency: string;
11 | value: number;
12 | };
13 | handlePlaceOrder(): void;
14 | };
15 |
16 | export const CartFooter = ({
17 | grandTotal,
18 | handlePlaceOrder,
19 | }: Props): React.ReactElement => {
20 | const { theme } = useContext(ThemeContext);
21 | const containerStyle = useMemo(
22 | () => ({
23 | backgroundColor: theme.colors?.white,
24 | borderColor: theme.colors?.divider,
25 | }),
26 | [theme],
27 | );
28 | return (
29 |
30 | {grandTotal && (
31 | {`${translate('common.total')} : ${formatPrice(
32 | grandTotal,
33 | )}`}
34 | )}
35 |
40 |
41 | );
42 | };
43 |
44 | const styles = StyleSheet.create({
45 | container: {
46 | flexDirection: 'row',
47 | alignItems: 'center',
48 | padding: SPACING.small,
49 | borderTopWidth: DIMENS.common.borderWidth,
50 | },
51 | placeOrder: {
52 | flex: 1,
53 | marginStart: SPACING.large,
54 | },
55 | });
56 |
--------------------------------------------------------------------------------
/src/screens/CartScreen/CartListItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { Image, ListItem } from 'react-native-elements';
4 | import { translate } from '../../i18n';
5 | import { DIMENS } from '../../constants';
6 | import { CartItemType } from '../../apollo/queries/basicCartFragment';
7 | import { formatPrice } from '../../logic';
8 |
9 | type Props = {
10 | item: CartItemType;
11 | index: number;
12 | onPress(arg0: number): void;
13 | onRemovePress(arg0: number): void;
14 | };
15 |
16 | const CartListItem = ({
17 | item,
18 | index,
19 | onPress,
20 | onRemovePress,
21 | }: Props): React.ReactElement => {
22 | const renderImage = () => {
23 | const uri = `${item.product.small_image.url}?width=${DIMENS.cartScreen.imageSize}`;
24 | return ;
25 | };
26 |
27 | return (
28 | onPress(index)} bottomDivider>
29 | {renderImage()}
30 |
31 | {item.product.name}
32 | {`${translate('common.quantity')} : ${
33 | item.quantity
34 | }`}
35 | {`${translate('common.price')} : ${formatPrice(
36 | item.prices.rowTotal,
37 | )}`}
38 |
39 | onRemovePress(index)} />
40 |
41 | );
42 | };
43 |
44 | const styles = StyleSheet.create({
45 | image: {
46 | height: DIMENS.cartScreen.imageSize,
47 | width: DIMENS.cartScreen.imageSize,
48 | },
49 | });
50 |
51 | export default CartListItem;
52 |
--------------------------------------------------------------------------------
/src/screens/CartScreen/CartScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, FlatList, StyleSheet } from 'react-native';
3 | import { Text } from 'react-native-elements';
4 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
5 | import { BottomTabNavigatorParamList, Routes } from '../../navigation';
6 | import { useCart } from '../../logic/cart/useCart';
7 | import { GenericTemplate } from '../../components';
8 | import { translate } from '../../i18n';
9 | import CartListItem from './CartListItem';
10 | import { SPACING } from '../../constants';
11 | import { CartFooter } from './CartFooter';
12 |
13 | type Props = {
14 | navigation: BottomTabNavigationProp<
15 | BottomTabNavigatorParamList,
16 | Routes.NAVIGATION_TO_PROFILE_SCREEN
17 | >;
18 | };
19 |
20 | const CartScreen = ({ navigation }: Props): React.ReactElement => {
21 | const { cartData, cartLoading, cartError } = useCart();
22 | const handleOnPress = (index: number) => console.log(index);
23 | const handleRemoveItem = (index: number) => console.log(index);
24 | const handlePlaceOrder = () => console.log('handle place order');
25 |
26 | const renderEmptyList = () =>
27 | (!cartLoading && (
28 |
29 |
30 | {translate('cartScreen.cartEmptyTitle')}
31 |
32 |
33 | {translate('cartScreen.cartEmptyMessage')}
34 |
35 |
36 | )) || <>>;
37 |
38 | return (
39 | 0 && (
44 |
48 | )
49 | }
50 | >
51 | (
54 |
60 | )}
61 | contentContainerStyle={
62 | cartData?.customerCart?.items.length === 0 && styles.fullScreen
63 | }
64 | keyExtractor={item => String(item.id)}
65 | ListEmptyComponent={renderEmptyList}
66 | />
67 |
68 | );
69 | };
70 |
71 | const styles = StyleSheet.create({
72 | fullScreen: {
73 | flex: 1,
74 | },
75 | emptyContainer: {
76 | flex: 1,
77 | justifyContent: 'center',
78 | alignItems: 'center',
79 | marginHorizontal: SPACING.small,
80 | },
81 | centerText: {
82 | textAlign: 'center',
83 | marginBottom: SPACING.small,
84 | },
85 | });
86 |
87 | export default CartScreen;
88 |
--------------------------------------------------------------------------------
/src/screens/CategoriesScreen/CategoriesScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlatList } from 'react-native';
3 | import { RouteProp } from '@react-navigation/native';
4 | import { StackNavigationProp } from '@react-navigation/stack';
5 | import { CategoryListItem, GenericTemplate } from '../../components';
6 | import { useCategories } from '../../logic';
7 | import { AppStackParamList, Routes } from '../../navigation';
8 |
9 | type Props = {
10 | navigation: StackNavigationProp<
11 | AppStackParamList,
12 | Routes.NAVIGATION_TO_CATEGORIES_SCREEN
13 | >;
14 | route: RouteProp;
15 | };
16 |
17 | const CategoriesScreen = ({
18 | navigation,
19 | route: {
20 | params: { categoryId },
21 | },
22 | }: Props): React.ReactElement => {
23 | const { categories = [], loading, error } = useCategories({
24 | categoryId,
25 | });
26 |
27 | return (
28 |
29 | `categoryItem${item.id.toString()}`}
32 | renderItem={({ item }) => (
33 |
34 | )}
35 | />
36 |
37 | );
38 | };
39 |
40 | export default CategoriesScreen;
41 |
--------------------------------------------------------------------------------
/src/screens/DrawerScreen/DrawerScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlatList } from 'react-native';
3 | import { DrawerContentComponentProps } from '@react-navigation/drawer';
4 | import { CategoryListItem, GenericTemplate } from '../../components';
5 | import { useCategories } from '../../logic';
6 | import { magentoConfig } from '../../../magento.config';
7 |
8 | type Props = {
9 | navigation: DrawerContentComponentProps;
10 | };
11 |
12 | const DrawerScreen = ({ navigation }: Props): React.ReactElement => {
13 | const { categories, loading, error } = useCategories({
14 | categoryId: magentoConfig.baseCategoryId,
15 | });
16 |
17 | return (
18 |
19 | `categoryItem${item.id.toString()}`}
22 | renderItem={({ item }) => (
23 |
24 | )}
25 | />
26 |
27 | );
28 | };
29 |
30 | export default DrawerScreen;
31 |
--------------------------------------------------------------------------------
/src/screens/HomeScreen/FeaturedProductList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { useNavigation } from '@react-navigation/native';
3 | import { View, FlatList, StyleSheet } from 'react-native';
4 | import { Text, ThemeContext } from 'react-native-elements';
5 | import { ProductInListType } from '../../apollo/queries/productsFragment';
6 | import { ProductListItem, Spinner } from '../../components';
7 | import { DIMENS, SPACING } from '../../constants';
8 | import { useCategoryProducts } from '../../logic';
9 | import { Routes } from '../../navigation';
10 | import { NetworkStatus } from '@apollo/client';
11 |
12 | type Props = {
13 | name?: string;
14 | categoryId: string;
15 | };
16 |
17 | const FeaturedProductList = ({
18 | name,
19 | categoryId,
20 | }: Props): React.ReactElement => {
21 | const { data, networkStatus, error } = useCategoryProducts({ categoryId });
22 | const { theme } = useContext(ThemeContext);
23 | const navigation = useNavigation();
24 |
25 | const onProductItemClicked = (index: number) => {
26 | if (data?.products?.items) {
27 | navigation.navigate(Routes.NAVIGATION_TO_PRODUCT_DETAILS_SCREEN, {
28 | name: data.products.items[index].name,
29 | sku: data.products.items[index].sku,
30 | });
31 | }
32 | };
33 |
34 | const renderItem = ({
35 | item,
36 | index,
37 | }: {
38 | item: ProductInListType;
39 | index: number;
40 | }) => {
41 | return (
42 |
48 | );
49 | };
50 |
51 | if (error?.message) {
52 | return (
53 |
54 | {error.message}
55 |
56 | );
57 | }
58 |
59 | if (networkStatus === NetworkStatus.loading) {
60 | return (
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | return (
68 |
69 | {name && (
70 |
71 | {name}
72 |
73 | )}
74 | `productListItem${item.sku}`}
80 | />
81 |
82 | );
83 | };
84 |
85 | const styles = StyleSheet.create({
86 | container: theme => ({
87 | marginBottom: SPACING.large,
88 | backgroundColor: theme.colors?.white,
89 | }),
90 | title: {
91 | marginStart: SPACING.large,
92 | paddingVertical: SPACING.small,
93 | },
94 | loadingBox: theme => ({
95 | alignContent: 'center',
96 | justifyContent: 'center',
97 | width: '100%',
98 | backgroundColor: theme.colors?.white,
99 | marginBottom: SPACING.large,
100 | height: (DIMENS.common.WINDOW_WIDTH / 3) * 2, // This is linked to ProductListItem height
101 | }),
102 | });
103 |
104 | export default FeaturedProductList;
105 |
--------------------------------------------------------------------------------
/src/screens/HomeScreen/HomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
3 | import { BottomTabNavigatorParamList, Routes } from '../../navigation';
4 | import { GenericTemplate, MediaGallery } from '../../components';
5 | import { magentoConfig } from '../../../magento.config';
6 | import { DIMENS, SPACING } from '../../constants';
7 | import FeaturedProductList from './FeaturedProductList';
8 | import { StyleSheet } from 'react-native';
9 |
10 | type Props = {
11 | navigation: BottomTabNavigationProp<
12 | BottomTabNavigatorParamList,
13 | Routes.NAVIGATION_TO_HOME_SCREEN
14 | >;
15 | };
16 |
17 | const HomeScreen = ({}: Props): React.ReactElement => {
18 | return (
19 |
20 |
26 | {magentoConfig.homeFeaturedCategories.map(featuredCategory => (
27 |
32 | ))}
33 |
34 | );
35 | };
36 |
37 | const styles = StyleSheet.create({
38 | mediaContainer: {
39 | marginBottom: SPACING.large,
40 | },
41 | });
42 |
43 | export default HomeScreen;
44 |
--------------------------------------------------------------------------------
/src/screens/LoginScreen/LoginScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { Button, Input } from 'react-native-elements';
4 | import { CompositeNavigationProp } from '@react-navigation/native';
5 | import { StackNavigationProp } from '@react-navigation/stack';
6 | import { showMessage } from 'react-native-flash-message';
7 | import { useLogin } from '../../logic';
8 | import { translate } from '../../i18n';
9 | import { SPACING } from '../../constants';
10 | import { GenericTemplate } from '../../components';
11 | import {
12 | AutheticationStackParamList,
13 | AppStackParamList,
14 | Routes,
15 | } from '../../navigation';
16 |
17 | type LoginScreenNavigationProp = CompositeNavigationProp<
18 | StackNavigationProp<
19 | AutheticationStackParamList,
20 | Routes.NAVIGATION_TO_LOGIN_SCREEN
21 | >,
22 | StackNavigationProp
23 | >;
24 |
25 | type Props = {
26 | navigation: LoginScreenNavigationProp;
27 | };
28 |
29 | const LoginScreen = ({ navigation }: Props): React.ReactElement => {
30 | const {
31 | values,
32 | loading,
33 | data,
34 | error,
35 | handleChange,
36 | handleSubmit,
37 | } = useLogin();
38 |
39 | useEffect(() => {
40 | if (data?.generateCustomerToken?.token) {
41 | showMessage({
42 | message: translate('common.success'),
43 | description: translate('loginScreen.successMessage'),
44 | type: 'success',
45 | });
46 | navigation.navigate(Routes.NAVIGATION_TO_HOME_SCREEN);
47 | }
48 | }, [data]);
49 |
50 | useEffect(() => {
51 | if (error) {
52 | showMessage({
53 | message: translate('common.error'),
54 | description: error.message ?? translate('errors.genericError'),
55 | type: 'danger',
56 | });
57 | }
58 | }, [error]);
59 |
60 | return (
61 |
62 |
70 |
87 |
93 |
100 | );
101 | };
102 |
103 | const styles = StyleSheet.create({
104 | container: {
105 | paddingHorizontal: SPACING.large,
106 | paddingTop: SPACING.large,
107 | },
108 | submitButton: {
109 | marginBottom: SPACING.large,
110 | },
111 | });
112 |
113 | export default LoginScreen;
114 |
--------------------------------------------------------------------------------
/src/screens/ProductDetailsScreen/ConfigurableOptionValues.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 | import { Text, Icon, ThemeContext } from 'react-native-elements';
4 | import { TouchableOpacity } from 'react-native-gesture-handler';
5 | import { DIMENS, SPACING } from '../../constants';
6 | import { ConfigurableProductOptionValueType } from '../../apollo/queries/configurableProductFragment';
7 | import { HandleSelectedConfigurableOptions } from '../../logic';
8 |
9 | interface Props {
10 | values: Array;
11 | optionCode: string;
12 | selectedIndex: number;
13 | handleSelectedConfigurableOptions: HandleSelectedConfigurableOptions;
14 | }
15 |
16 | const ConfigurableOptionValues = ({
17 | values,
18 | optionCode,
19 | selectedIndex,
20 | handleSelectedConfigurableOptions,
21 | }: Props): React.ReactElement => {
22 | const { theme } = useContext(ThemeContext);
23 |
24 | const renderValue = (value: ConfigurableProductOptionValueType) => {
25 | const selected = selectedIndex === value.valueIndex;
26 | switch (value.swatchData.__typename) {
27 | case 'ColorSwatchData': {
28 | return (
29 | <>
30 |
39 | {selected ? (
40 |
41 |
42 |
43 | ) : null}
44 | >
45 | );
46 | }
47 | case 'TextSwatchData': {
48 | return (
49 |
59 |
64 | {value.swatchData.value}
65 |
66 |
67 | );
68 | }
69 | default: {
70 | return null;
71 | }
72 | }
73 | };
74 |
75 | return (
76 |
77 | {values.map(value => (
78 |
81 | handleSelectedConfigurableOptions(optionCode, value.valueIndex)
82 | }
83 | >
84 | {renderValue(value)}
85 |
86 | ))}
87 |
88 | );
89 | };
90 |
91 | const styles = StyleSheet.create({
92 | container: {
93 | flexDirection: 'row',
94 | flexWrap: 'wrap',
95 | marginBottom: SPACING.large,
96 | },
97 | valueContainer: {
98 | borderWidth: DIMENS.common.borderWidth,
99 | marginEnd: SPACING.small,
100 | alignItems: 'center',
101 | justifyContent: 'center',
102 | borderRadius: 2,
103 | overflow: 'hidden',
104 | height: DIMENS.productDetailScreen.configurableOptionValueBoxSize,
105 | minWidth: DIMENS.productDetailScreen.configurableOptionValueBoxSize,
106 | },
107 | selectedColor: {
108 | ...StyleSheet.absoluteFillObject,
109 | backgroundColor: `rgba(0,0,0,.1)`,
110 | justifyContent: 'center',
111 | alignItems: 'center',
112 | },
113 | selectedText: {
114 | ...StyleSheet.absoluteFillObject,
115 | justifyContent: 'center',
116 | alignItems: 'center',
117 | },
118 | });
119 |
120 | export default ConfigurableOptionValues;
121 |
--------------------------------------------------------------------------------
/src/screens/ProductDetailsScreen/ConfigurableProductOptions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Divider, Text } from 'react-native-elements';
4 | import { ConfigurableOptionType } from '../../apollo/queries/configurableProductFragment';
5 | import { SPACING } from '../../constants';
6 | import ConfigurableOptionValues from './ConfigurableOptionValues';
7 | import {
8 | SelectedConfigurableProductOptions,
9 | HandleSelectedConfigurableOptions,
10 | } from '../../logic';
11 |
12 | export interface Props {
13 | options: Array;
14 | selectedConfigurableProductOptions: SelectedConfigurableProductOptions;
15 | handleSelectedConfigurableOptions: HandleSelectedConfigurableOptions;
16 | }
17 |
18 | const ConfigurableProductOptions: React.FC = ({
19 | options,
20 | selectedConfigurableProductOptions,
21 | handleSelectedConfigurableOptions,
22 | }) => {
23 | const renderOption = (item: ConfigurableOptionType) => (
24 |
25 |
26 | {item.label}
27 |
28 |
34 |
35 | );
36 |
37 | return (
38 | <>
39 |
40 | {options?.map(renderOption)}
41 |
42 | >
43 | );
44 | };
45 |
46 | const styles = StyleSheet.create({
47 | container: {
48 | marginHorizontal: SPACING.large,
49 | },
50 | divider: {
51 | marginVertical: SPACING.tiny,
52 | },
53 | label: {
54 | marginBottom: SPACING.small,
55 | },
56 | });
57 |
58 | export default ConfigurableProductOptions;
59 |
--------------------------------------------------------------------------------
/src/screens/ProductListScreen/ProductListScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useLayoutEffect } from 'react';
2 | import { View, RefreshControl, StyleSheet, FlatList } from 'react-native';
3 | import { RouteProp } from '@react-navigation/native';
4 | import { StackNavigationProp } from '@react-navigation/stack';
5 | import { NetworkStatus } from '@apollo/client';
6 | import { BottomSheet, ListItem, ThemeContext } from 'react-native-elements';
7 | import { useCategoryProducts, useSort } from '../../logic';
8 | import { Routes, AppStackParamList } from '../../navigation';
9 | import { ProductInListType } from '../../apollo/queries/productsFragment';
10 | import { SPACING } from '../../constants';
11 | import {
12 | GenericTemplate,
13 | ProductListItem,
14 | Spinner,
15 | CustomHeaderButtons,
16 | CustomHeaderItem,
17 | } from '../../components';
18 | import { translate } from '../../i18n';
19 |
20 | interface Props {
21 | navigation: StackNavigationProp<
22 | AppStackParamList,
23 | Routes.NAVIGATION_TO_PRODUCT_LIST_SCREEN
24 | >;
25 | route: RouteProp;
26 | }
27 |
28 | const ProductListScreen = ({
29 | navigation,
30 | route: {
31 | params: { categoryId },
32 | },
33 | }: Props): React.ReactElement => {
34 | const { data, networkStatus, error, refresh, loadMore } = useCategoryProducts(
35 | {
36 | categoryId,
37 | },
38 | );
39 | const { isVisible, selectedIndex, setVisible, sortOptions } = useSort({
40 | onPress: refresh,
41 | });
42 | const { theme } = useContext(ThemeContext);
43 |
44 | useLayoutEffect(() => {
45 | navigation.setOptions({
46 | headerRight: () => (
47 |
48 | setVisible(true)}
52 | />
53 |
54 | ),
55 | });
56 | }, [navigation]);
57 |
58 | const onProductItemClicked = (index: number) => {
59 | if (data?.products?.items) {
60 | navigation.navigate(Routes.NAVIGATION_TO_PRODUCT_DETAILS_SCREEN, {
61 | name: data.products.items[index].name,
62 | sku: data.products.items[index].sku,
63 | });
64 | }
65 | };
66 |
67 | const renderItem = ({
68 | item,
69 | index,
70 | }: {
71 | item: ProductInListType;
72 | index: number;
73 | }) => {
74 | return (
75 |
80 | );
81 | };
82 |
83 | const renderFooterComponent = () =>
84 | (networkStatus === NetworkStatus.fetchMore && (
85 |
86 |
87 |
88 | )) || <>>;
89 |
90 | return (
91 |
92 | `productListItem${item.sku}`}
97 | refreshControl={
98 |
105 | }
106 | onEndReached={loadMore}
107 | ListFooterComponent={renderFooterComponent}
108 | />
109 |
110 | {sortOptions.map((option, index) => (
111 |
121 |
122 |
123 | {option.title}
124 |
125 |
126 |
127 | ))}
128 |
129 |
130 | );
131 | };
132 |
133 | const styles = StyleSheet.create({
134 | footerContainer: {
135 | alignItems: 'center',
136 | marginVertical: SPACING.small,
137 | },
138 | sortContainer: {
139 | backgroundColor: 'rgba(0.5, 0.25, 0, 0.2)',
140 | },
141 | });
142 |
143 | export default ProductListScreen;
144 |
--------------------------------------------------------------------------------
/src/screens/ProfileScreen/ProfileScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { View } from 'react-native';
3 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
4 | import { Button, Text } from 'react-native-elements';
5 | import { GenericTemplate } from '../../components';
6 | import { translate } from '../../i18n';
7 | import { useCustomer, useLogout } from '../../logic';
8 | import { BottomTabNavigatorParamList, Routes } from '../../navigation';
9 |
10 | type Props = {
11 | navigation: BottomTabNavigationProp<
12 | BottomTabNavigatorParamList,
13 | Routes.NAVIGATION_TO_PROFILE_SCREEN
14 | >;
15 | };
16 |
17 | const ProfileScreen = ({ navigation }: Props): React.ReactElement => {
18 | const { getCustomer, data, loading, error } = useCustomer();
19 | const { logout } = useLogout();
20 |
21 | useEffect(() => {
22 | getCustomer();
23 | }, []);
24 |
25 | const handleLogout = () => {
26 | logout();
27 | navigation.jumpTo(Routes.NAVIGATION_TO_HOME_SCREEN);
28 | };
29 |
30 | if (error?.message) {
31 | return (
32 |
33 | {error.message}
34 |
35 |
36 | );
37 | }
38 |
39 | return (
40 |
41 |
42 | {translate('profileScreen.greeting', {
43 | name: data?.customer?.firstName ?? translate('common.user'),
44 | })}
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default ProfileScreen;
52 |
--------------------------------------------------------------------------------
/src/screens/SearchScreen/SearchScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useMemo } from 'react';
2 | import { StyleSheet, FlatList, View } from 'react-native';
3 | import { Text, SearchBar, ThemeContext } from 'react-native-elements';
4 | import { StackNavigationProp } from '@react-navigation/stack';
5 | import { GenericTemplate, ProductListItem, Spinner } from '../../components';
6 | import { useSearch } from '../../logic';
7 | import { AppStackParamList, Routes } from '../../navigation';
8 | import { ProductInListType } from '../../apollo/queries/productsFragment';
9 | import { translate } from '../../i18n';
10 | import { LIMITS, SPACING } from '../../constants';
11 | import { NetworkStatus } from '@apollo/client';
12 |
13 | type Props = {
14 | navigation: StackNavigationProp<
15 | AppStackParamList,
16 | Routes.NAVIGATION_TO_SEARCH_SCREEN
17 | >;
18 | };
19 |
20 | const SearchScreen = ({ navigation }: Props): React.ReactElement => {
21 | const {
22 | searchText,
23 | handleChange,
24 | networkStatus,
25 | called,
26 | loadMore,
27 | data: { products: { items: products = [] } = {} } = {},
28 | } = useSearch();
29 | const { theme } = useContext(ThemeContext);
30 | const loadingProps = useMemo(() => ({ color: theme.colors?.primary }), [
31 | theme,
32 | ]);
33 |
34 | const handleBackPress = () => navigation.pop();
35 |
36 | const onProductItemClicked = (index: number) => {
37 | navigation.navigate(Routes.NAVIGATION_TO_PRODUCT_DETAILS_SCREEN, {
38 | name: products[index].name,
39 | sku: products[index].sku,
40 | });
41 | };
42 |
43 | const renderItem = ({
44 | item,
45 | index,
46 | }: {
47 | item: ProductInListType;
48 | index: number;
49 | }) => {
50 | return (
51 |
56 | );
57 | };
58 |
59 | // FIXME: Don't show when previous search result was empty, and user is typing
60 | // create a separate state
61 | const renderEmptyComponent = () =>
62 | (searchText.length >= LIMITS.searchTextMinLength &&
63 | products.length === 0 &&
64 | called &&
65 | networkStatus !== NetworkStatus.loading && (
66 |
67 |
68 | {translate('searchScreen.noProductsFound', { searchText })}
69 |
70 |
71 | )) || <>>;
72 |
73 | const renderFooterComponent = () =>
74 | (networkStatus === NetworkStatus.fetchMore && (
75 |
76 |
77 |
78 | )) || <>>;
79 |
80 | return (
81 |
82 |
94 | `productListItem${item.sku}`}
99 | ListEmptyComponent={renderEmptyComponent}
100 | ListFooterComponent={renderFooterComponent}
101 | onEndReached={loadMore}
102 | />
103 |
104 | );
105 | };
106 |
107 | const styles = StyleSheet.create({
108 | center: {
109 | flex: 1,
110 | alignItems: 'center',
111 | justifyContent: 'center',
112 | padding: SPACING.large,
113 | },
114 | searchBarContainer: {
115 | borderTopWidth: 0,
116 | borderBottomWidth: StyleSheet.hairlineWidth,
117 | borderBottomColor: 'rgba(0,0,0,.1)',
118 | borderLeftWidth: 0,
119 | borderRightWidth: 0,
120 | shadowColor: 'black',
121 | shadowOffset: {
122 | width: 0,
123 | height: 2,
124 | },
125 | shadowOpacity: 0.1,
126 | shadowRadius: 2,
127 | elevation: 1,
128 | },
129 | footerContainer: {
130 | alignItems: 'center',
131 | marginVertical: SPACING.small,
132 | },
133 | });
134 |
135 | export default SearchScreen;
136 |
--------------------------------------------------------------------------------
/src/screens/SignupScreen/SignupScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { Button, Input } from 'react-native-elements';
4 | import { StackNavigationProp } from '@react-navigation/stack';
5 | import { showMessage } from 'react-native-flash-message';
6 | import { translate } from '../../i18n';
7 | import { AutheticationStackParamList, Routes } from '../../navigation';
8 | import { SPACING } from '../../constants';
9 | import { GenericTemplate } from '../../components';
10 | import { useSignup } from '../../logic';
11 |
12 | type Props = {
13 | navigation: StackNavigationProp<
14 | AutheticationStackParamList,
15 | Routes.NAVIGATION_TO_LOGIN_SCREEN
16 | >;
17 | };
18 |
19 | const SignupScreen = ({ navigation }: Props): React.ReactElement => {
20 | const {
21 | values,
22 | loading,
23 | data,
24 | error,
25 | handleChange,
26 | handleSubmit,
27 | } = useSignup();
28 |
29 | useEffect(() => {
30 | if (data?.createCustomerV2?.customer?.email) {
31 | showMessage({
32 | message: translate('common.success'),
33 | description: translate('signupScreen.successMessage'),
34 | type: 'success',
35 | });
36 | navigation.replace(Routes.NAVIGATION_TO_LOGIN_SCREEN);
37 | }
38 | }, [data]);
39 |
40 | useEffect(() => {
41 | if (error) {
42 | showMessage({
43 | message: translate('common.error'),
44 | description: error.message ?? translate('errors.genericError'),
45 | type: 'danger',
46 | });
47 | }
48 | }, [error]);
49 |
50 | return (
51 |
52 |
59 |
66 |
74 |
91 |
97 |
104 | );
105 | };
106 |
107 | const styles = StyleSheet.create({
108 | container: {
109 | paddingHorizontal: SPACING.large,
110 | paddingTop: SPACING.large,
111 | },
112 | submitButton: {
113 | marginBottom: SPACING.large,
114 | },
115 | });
116 |
117 | export default SignupScreen;
118 |
--------------------------------------------------------------------------------
/src/screens/index.ts:
--------------------------------------------------------------------------------
1 | import CartScreen from './CartScreen/CartScreen';
2 | import CategoriesScreen from './CategoriesScreen/CategoriesScreen';
3 | import DrawerScreen from './DrawerScreen/DrawerScreen';
4 | import HomeScreen from './HomeScreen/HomeScreen';
5 | import LoginScreen from './LoginScreen/LoginScreen';
6 | import ProductDetailsScreen from './ProductDetailsScreen/ProductDetailsScreen';
7 | import ProductListScreen from './ProductListScreen/ProductListScreen';
8 | import ProfileScreen from './ProfileScreen/ProfileScreen';
9 | import SearchScreen from './SearchScreen/SearchScreen';
10 | import SignupScreen from './SignupScreen/SignupScreen';
11 |
12 | export {
13 | CartScreen,
14 | CategoriesScreen,
15 | DrawerScreen,
16 | HomeScreen,
17 | LoginScreen,
18 | ProductDetailsScreen,
19 | ProductListScreen,
20 | ProfileScreen,
21 | SearchScreen,
22 | SignupScreen,
23 | };
24 |
--------------------------------------------------------------------------------
/src/theme/colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Note: Don't use the variables directly in code
3 | * use colors from lightTheme and darkTheme
4 | */
5 |
6 | interface Colors {
7 | primary: string;
8 | secondary: string;
9 | background: string;
10 | sucess: string;
11 | error: string;
12 | warning: string;
13 | info: string;
14 | }
15 |
16 | const BLACK = '#000';
17 | const PRIMARY_COLOR = '#333333';
18 | const SECONDARY_COLOR = '#FF7900';
19 | const BACKGROUND_COLOR = '#fbfbfb';
20 | const SUCCESS_COLOR = '#00C851';
21 | const SUCCESS_DARK_COLOR = '#007E33';
22 | const ERROR_COLOR = '#ff4444';
23 | const ERROR_DARK_COLOR = '#CC0000';
24 | const WARNING_COLOR = '#ffbb33';
25 | const WARNING_DARK_COLOR = '#FF8800';
26 | const INFO_COLOR = '#33b5e5';
27 | const INFO_DARK_COLOR = '#0099CC';
28 |
29 | export const lightColors: Colors = {
30 | primary: PRIMARY_COLOR,
31 | secondary: SECONDARY_COLOR,
32 | background: BACKGROUND_COLOR,
33 | sucess: SUCCESS_COLOR,
34 | error: ERROR_COLOR,
35 | warning: WARNING_COLOR,
36 | info: INFO_COLOR,
37 | };
38 |
39 | export const darkColors: Colors = {
40 | primary: PRIMARY_COLOR,
41 | secondary: SECONDARY_COLOR,
42 | background: BLACK,
43 | sucess: SUCCESS_DARK_COLOR,
44 | error: ERROR_DARK_COLOR,
45 | warning: WARNING_DARK_COLOR,
46 | info: INFO_DARK_COLOR,
47 | };
48 |
--------------------------------------------------------------------------------
/src/theme/darkTheme.ts:
--------------------------------------------------------------------------------
1 | import { DarkTheme } from '@react-navigation/native';
2 | import { darkColors } from './colors';
3 | import { typography } from './typography';
4 |
5 | export const darkTheme = {
6 | ...typography,
7 | colors: {
8 | primary: darkColors.secondary,
9 | // secondary
10 | // white: 'white',
11 | // black: 'black',
12 | // grey0: '#f9f9f9',
13 | // grey1: '#e0e0e0',
14 | // grey2: '#ced2d9',
15 | // grey3: '#979da0',
16 | // grey4: '#6d787e',
17 | // grey5: '#354052',
18 | // searchBg
19 | // greyOutline: ,
20 | success: darkColors.sucess,
21 | error: darkColors.error,
22 | warning: darkColors.warning,
23 | info: darkColors.info,
24 | // divider: '#ced2d9',
25 | },
26 | };
27 |
28 | export const navigationDarkTheme = {
29 | dark: true,
30 | colors: {
31 | ...DarkTheme.colors,
32 | /**
33 | * Used as tint color in bottombar
34 | */
35 | primary: '#fff',
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/src/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lightTheme';
2 | export * from './darkTheme';
3 |
--------------------------------------------------------------------------------
/src/theme/lightTheme.ts:
--------------------------------------------------------------------------------
1 | import { DefaultTheme } from '@react-navigation/native';
2 | import { lightColors } from './colors';
3 | import { typography } from './typography';
4 |
5 | export const lightTheme = {
6 | ...typography,
7 | colors: {
8 | primary: lightColors.primary,
9 | // secondary
10 | // white: 'white',
11 | // black: 'black',
12 | // grey0: '#f9f9f9',
13 | // grey1: '#e0e0e0',
14 | // grey2: '#ced2d9',
15 | // grey3: '#979da0',
16 | // grey4: '#6d787e',
17 | // grey5: '#354052',
18 | searchBg: '#ebebeb',
19 | // greyOutline: ,
20 | success: lightColors.sucess,
21 | error: lightColors.error,
22 | warning: lightColors.warning,
23 | info: lightColors.info,
24 | // divider: '#ced2d9',
25 | },
26 | };
27 |
28 | export const navigationLightTheme = {
29 | dark: false,
30 | colors: {
31 | ...DefaultTheme.colors,
32 | /**
33 | * Used as tint color in bottombar
34 | */
35 | primary: lightColors.secondary,
36 | background: lightColors.background,
37 | text: lightColors.primary,
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/theme/typography.ts:
--------------------------------------------------------------------------------
1 | import { moderateScale } from 'react-native-size-matters';
2 |
3 | export const typography = {
4 | Text: {
5 | h1Style: {
6 | fontSize: moderateScale(20),
7 | },
8 | h2Style: {
9 | fontSize: moderateScale(18),
10 | },
11 | h3Style: {
12 | fontSize: moderateScale(16),
13 | },
14 | h4Style: {
15 | fontSize: moderateScale(14),
16 | },
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
5 | "lib": [
6 | "es6"
7 | ] /* Specify library files to be included in the compilation. */,
8 | "allowJs": true /* Allow javascript files to be compiled. */,
9 | "jsx": "react-native",
10 | "noEmit": true,
11 | "isolatedModules": true,
12 | "strict": true,
13 | "noImplicitAny": true,
14 | "strictNullChecks": true,
15 | "strictFunctionTypes": true,
16 | "strictPropertyInitialization": true,
17 | "noImplicitThis": true,
18 | "alwaysStrict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noImplicitReturns": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "forceConsistentCasingInFileNames": true,
24 | "moduleResolution": "node",
25 | "allowSyntheticDefaultImports": true,
26 | "esModuleInterop": true,
27 | "resolveJsonModule": true,
28 | "skipLibCheck": true
29 | },
30 | "exclude": [
31 | "node_modules",
32 | "babel.config.js",
33 | "metro.config.js",
34 | "jest.config.js"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------