├── .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 | ![React Native version](https://img.shields.io/github/package-json/dependency-version/sanjeevyadavit/magento_react_native_graphql/react-native) 4 | ![React Navigation version](https://img.shields.io/github/package-json/dependency-version/sanjeevyadavit/magento_react_native_graphql/@react-navigation/native?label=react-navigation) 5 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/sanjeevyadavit/magento_react_native_graphql/issues) 6 | ![Last commit](https://img.shields.io/github/last-commit/sanjeevyadavit/magento_react_native_graphql) 7 | ![License](https://img.shields.io/github/license/sanjeevyadavit/magento_react_native_graphql) 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 |
23 | 24 | 25 | 26 |
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 [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](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 | Buy Me A Coffee 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 |