├── .bundle
└── config
├── .env.template
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── dependabot.yml
├── .gitignore
├── .prettierrc.js
├── .watchmanconfig
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE
├── README.md
├── SECURITY.md
├── __tests__
└── App.test.tsx
├── android
├── app
│ ├── build.gradle
│ ├── debug.keystore
│ ├── proguard-rules.pro
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── assets
│ │ └── fonts
│ │ │ ├── Audiowide-Regular.ttf
│ │ │ ├── Lato-Bold.ttf
│ │ │ ├── Lato-Regular.ttf
│ │ │ └── Lato-Thin.ttf
│ │ ├── java
│ │ └── com
│ │ │ └── rnstartermedusa
│ │ │ ├── MainActivity.kt
│ │ │ └── MainApplication.kt
│ │ └── res
│ │ ├── drawable
│ │ └── rn_edit_text_material.xml
│ │ ├── 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
├── link-assets-manifest.json
└── settings.gradle
├── app.json
├── app
├── api
│ └── client.tsx
├── app.tsx
├── components
│ ├── cart
│ │ ├── animated-cart-button.tsx
│ │ ├── cart-content.tsx
│ │ ├── line-item-price.tsx
│ │ ├── line-item-quantity.tsx
│ │ └── promo-code-input.tsx
│ ├── checkout
│ │ ├── address-form.tsx
│ │ ├── checkout-steps.tsx
│ │ └── steps
│ │ │ ├── address-step.tsx
│ │ │ ├── payment-step.tsx
│ │ │ ├── review-step.tsx
│ │ │ └── shipping-step.tsx
│ ├── common
│ │ ├── accordion.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dropdown.tsx
│ │ ├── error-ui.tsx
│ │ ├── fab-button.tsx
│ │ ├── input.tsx
│ │ ├── loader.tsx
│ │ ├── navbar.tsx
│ │ ├── rounded-button.tsx
│ │ ├── tab-bar.tsx
│ │ └── text.tsx
│ ├── home
│ │ ├── featured-collection.tsx
│ │ ├── header.tsx
│ │ └── hero-carousel.tsx
│ └── product
│ │ ├── image-carousel.tsx
│ │ ├── option-select.tsx
│ │ ├── preview-price.tsx
│ │ ├── product-list.tsx
│ │ ├── product-price.tsx
│ │ └── wishlist-button.tsx
├── constants
│ └── fluent-templates
│ │ ├── en-US.ts
│ │ ├── id-ID.ts
│ │ ├── index.ts
│ │ └── type.ts
├── data
│ ├── cart-context.tsx
│ ├── customer-context.tsx
│ ├── hooks.ts
│ ├── locale-context.tsx
│ └── region-context.tsx
├── screens
│ ├── address
│ │ ├── address-form.tsx
│ │ └── address-list.tsx
│ ├── auth
│ │ ├── login.tsx
│ │ └── register.tsx
│ ├── cart.tsx
│ ├── category
│ │ ├── categories.tsx
│ │ └── category-detail.tsx
│ ├── checkout.tsx
│ ├── collection
│ │ ├── collection-detail.tsx
│ │ └── collections.tsx
│ ├── home.tsx
│ ├── order
│ │ ├── order-detail.tsx
│ │ └── orders.tsx
│ ├── product-detail.tsx
│ ├── profile
│ │ ├── profile-detail.tsx
│ │ └── profile.tsx
│ ├── region-select.tsx
│ ├── settings.tsx
│ └── splash.tsx
├── styles
│ ├── global.css
│ ├── hooks.tsx
│ ├── theme-provider.tsx
│ ├── themes.tsx
│ ├── types.tsx
│ └── utils.tsx
├── types
│ └── checkout.ts
└── utils
│ ├── common.tsx
│ ├── image-url.tsx
│ ├── order.ts
│ └── product-price.tsx
├── assets
└── fonts
│ ├── Audiowide-Regular.ttf
│ ├── Lato-Bold.ttf
│ ├── Lato-Regular.ttf
│ └── Lato-Thin.ttf
├── babel.config.js
├── index.js
├── ios
├── .xcode.env
├── Podfile
├── Podfile.lock
├── RnStarterMedusa.xcodeproj
│ ├── project.pbxproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── RnStarterMedusa.xcscheme
├── RnStarterMedusa.xcworkspace
│ └── contents.xcworkspacedata
├── RnStarterMedusa
│ ├── AppDelegate.swift
│ ├── Images.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── LaunchScreen.storyboard
│ ├── PrivacyInfo.xcprivacy
│ ├── info.plist
│ └── main.m
└── link-assets-manifest.json
├── jest.config.js
├── metro.config.js
├── nativewind-env.d.ts
├── package-lock.json
├── package.json
├── react-native.config.js
├── tailwind.config.js
├── tsconfig.json
└── types
├── components.d.ts
├── dot-env.d.ts
├── global.d.ts
├── nativewind-env.d.ts
└── navigation.d.ts
/.bundle/config:
--------------------------------------------------------------------------------
1 | BUNDLE_PATH: "vendor/bundle"
2 | BUNDLE_FORCE_RUBY_PLATFORM: 1
3 |
--------------------------------------------------------------------------------
/.env.template:
--------------------------------------------------------------------------------
1 | MEDUSA_BACKEND_URL=http://10.0.2.2:9000
2 | PUBLISHABLE_API_KEY=
3 | DEFAULT_LOCALE=en-US
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native',
4 | rules: {
5 | '@typescript-eslint/no-unused-vars': 'warn',
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
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 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/" # adjust if your package.json is not in the root
5 | schedule:
6 | interval: "weekly"
7 | allow:
8 | - dependency-type: "all"
9 | dependency-name: "@medusajs/js-sdk"
10 | - dependency-type: "all"
11 | dependency-name: "@medusajs/types"
--------------------------------------------------------------------------------
/.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 | **/.xcode.env.local
24 |
25 | # VS Code
26 | #
27 | .vscode/*
28 |
29 | # Android/IntelliJ
30 | #
31 | build/
32 | .idea
33 | .gradle
34 | local.properties
35 | *.iml
36 | *.hprof
37 | .cxx/
38 | *.keystore
39 | !debug.keystore
40 | .kotlin/
41 |
42 | # node.js
43 | #
44 | node_modules/
45 | npm-debug.log
46 | yarn-error.log
47 |
48 | # fastlane
49 | #
50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
51 | # screenshots whenever they are needed.
52 | # For more information about the recommended setup visit:
53 | # https://docs.fastlane.tools/best-practices/source-control/
54 |
55 | **/fastlane/report.xml
56 | **/fastlane/Preview.html
57 | **/fastlane/screenshots
58 | **/fastlane/test_output
59 |
60 | # Bundle artifact
61 | *.jsbundle
62 |
63 | # Ruby / CocoaPods
64 | **/Pods/
65 | /vendor/bundle/
66 |
67 | # Temporary files created by Metro to check the health of the file watcher
68 | .metro-health-check*
69 |
70 | # testing
71 | /coverage
72 |
73 | # Yarn
74 | .yarn/*
75 | !.yarn/patches
76 | !.yarn/plugins
77 | !.yarn/releases
78 | !.yarn/sdks
79 | !.yarn/versions
80 | .env
81 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSameLine: true,
4 | bracketSpacing: false,
5 | singleQuote: true,
6 | trailingComma: 'all',
7 | };
8 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | ranjithkumar8352@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to This Project
2 |
3 | Thank you for your interest in contributing to Medusa Mobile project! This document provides guidelines and instructions for contributing.
4 |
5 | ## Development Setup
6 |
7 | 1. Fork and clone the repository
8 | 2. Follow the pre-requisites and setup instructions from the [README](README.md)
9 |
10 | ## Making Contributions
11 |
12 | 1. Create a new branch for your feature/fix:
13 |
14 | ```bash
15 | git checkout -b feature/your-feature-name
16 | ```
17 |
18 | 2. Make your changes and test thoroughly
19 | 3. Commit your changes following conventional commits:
20 |
21 | ```bash
22 | git commit -m "feat: add new feature"
23 | git commit -m "fix: resolve issue with..."
24 | ```
25 |
26 | 4. Push to your fork and submit a Pull Request
27 |
28 | ## Pull Request Guidelines
29 |
30 | - Provide a clear description of the changes
31 | - Include any relevant issue numbers
32 | - Ensure your code follows the existing style conventions
33 | - Include tests if applicable
34 | - Update documentation as needed
35 |
36 | ## Code Style
37 |
38 | - Follow the existing code style and formatting
39 | - Use meaningful variable and function names
40 | - Add comments for complex logic
41 | - Keep functions focused and concise
42 |
43 | ## Need Help?
44 |
45 | If you need help or have questions, please:
46 |
47 | - Open an issue in the repository
48 | - Review existing issues and pull requests
49 |
50 | ## License
51 |
52 | By contributing, you agree that your contributions will be licensed under the same license as the project.
53 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
4 | ruby ">= 2.6.10"
5 |
6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures.
7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
9 | gem 'xcodeproj', '< 1.26.0'
10 | gem 'concurrent-ruby', '< 1.3.4'
11 |
12 | # Ruby 3.4.0 has removed some libraries from the standard library.
13 | gem 'bigdecimal'
14 | gem 'logger'
15 | gem 'benchmark'
16 | gem 'mutex_m'
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 bloomsynth
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 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | > 0.1.0 | :white_check_mark: |
8 |
9 | ## Reporting a Vulnerability
10 |
11 | Create an issue in the repository explaining the vulnerability. A security patch will be released as soon as possible.
12 |
--------------------------------------------------------------------------------
/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import React from 'react';
6 | import ReactTestRenderer from 'react-test-renderer';
7 | import App from '../app/app';
8 |
9 | test('renders correctly', async () => {
10 | await ReactTestRenderer.act(() => {
11 | ReactTestRenderer.create();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "org.jetbrains.kotlin.android"
3 | apply plugin: "com.facebook.react"
4 |
5 | /**
6 | * This is the configuration block to customize your React Native Android app.
7 | * By default you don't need to apply any configuration, just uncomment the lines you need.
8 | */
9 | react {
10 | /* Folders */
11 | // The root of your project, i.e. where "package.json" lives. Default is '../..'
12 | // root = file("../../")
13 | // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
14 | // reactNativeDir = file("../../node_modules/react-native")
15 | // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
16 | // codegenDir = file("../../node_modules/@react-native/codegen")
17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
18 | // cliFile = file("../../node_modules/react-native/cli.js")
19 |
20 | /* Variants */
21 | // The list of variants to that are debuggable. For those we're going to
22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
24 | // debuggableVariants = ["liteDebug", "prodDebug"]
25 |
26 | /* Bundling */
27 | // A list containing the node command and its flags. Default is just 'node'.
28 | // nodeExecutableAndArgs = ["node"]
29 | //
30 | // The command to run when bundling. By default is 'bundle'
31 | // bundleCommand = "ram-bundle"
32 | //
33 | // The path to the CLI configuration file. Default is empty.
34 | // bundleConfig = file(../rn-cli.config.js)
35 | //
36 | // The name of the generated asset file containing your JS bundle
37 | // bundleAssetName = "MyApplication.android.bundle"
38 | //
39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
40 | // entryFile = file("../js/MyApplication.android.js")
41 | //
42 | // A list of extra flags to pass to the 'bundle' commands.
43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
44 | // extraPackagerArgs = []
45 |
46 | /* Hermes Commands */
47 | // The hermes compiler command to run. By default it is 'hermesc'
48 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
49 | //
50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
51 | // hermesFlags = ["-O", "-output-source-map"]
52 |
53 | /* Autolinking */
54 | autolinkLibrariesWithApp()
55 | }
56 |
57 | /**
58 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
59 | */
60 | def enableProguardInReleaseBuilds = false
61 |
62 | /**
63 | * The preferred build flavor of JavaScriptCore (JSC)
64 | *
65 | * For example, to use the international variant, you can use:
66 | * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
67 | *
68 | * The international variant includes ICU i18n library and necessary data
69 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
70 | * give correct results when using with locales other than en-US. Note that
71 | * this variant is about 6MiB larger per architecture than default.
72 | */
73 | def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
74 |
75 | android {
76 | ndkVersion rootProject.ext.ndkVersion
77 | buildToolsVersion rootProject.ext.buildToolsVersion
78 | compileSdk rootProject.ext.compileSdkVersion
79 |
80 | namespace "com.rnstartermedusa"
81 | defaultConfig {
82 | applicationId "com.rnstartermedusa"
83 | minSdkVersion rootProject.ext.minSdkVersion
84 | targetSdkVersion rootProject.ext.targetSdkVersion
85 | versionCode 1
86 | versionName "1.0"
87 | }
88 | signingConfigs {
89 | debug {
90 | storeFile file('debug.keystore')
91 | storePassword 'android'
92 | keyAlias 'androiddebugkey'
93 | keyPassword 'android'
94 | }
95 | }
96 | buildTypes {
97 | debug {
98 | signingConfig signingConfigs.debug
99 | }
100 | release {
101 | // Caution! In production, you need to generate your own keystore file.
102 | // see https://reactnative.dev/docs/signed-apk-android.
103 | signingConfig signingConfigs.debug
104 | minifyEnabled enableProguardInReleaseBuilds
105 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
106 | }
107 | }
108 | }
109 |
110 | dependencies {
111 | // The version of react-native is set by the React Native Gradle Plugin
112 | implementation("com.facebook.react:react-android")
113 | implementation 'com.facebook.fresco:webpsupport:3.2.0'
114 |
115 | if (hermesEnabled.toBoolean()) {
116 | implementation("com.facebook.react:hermes-android")
117 | } else {
118 | implementation jscFlavor
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Audiowide-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/android/app/src/main/assets/fonts/Audiowide-Regular.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Lato-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/android/app/src/main/assets/fonts/Lato-Bold.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Lato-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/android/app/src/main/assets/fonts/Lato-Regular.ttf
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Lato-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/android/app/src/main/assets/fonts/Lato-Thin.ttf
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rnstartermedusa/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.rnstartermedusa
2 |
3 | import android.os.Bundle;
4 | import com.facebook.react.ReactActivity
5 | import com.facebook.react.ReactActivityDelegate
6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
7 | import com.facebook.react.defaults.DefaultReactActivityDelegate
8 |
9 | class MainActivity : ReactActivity() {
10 |
11 | /**
12 | * Returns the name of the main component registered from JavaScript. This is used to schedule
13 | * rendering of the component.
14 | */
15 | override fun getMainComponentName(): String = "RnStarterMedusa"
16 |
17 |
18 | /**
19 | * OnCreate for react-navigation
20 | *
21 | */
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(null)
24 | }
25 |
26 | /**
27 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
28 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
29 | */
30 | override fun createReactActivityDelegate(): ReactActivityDelegate =
31 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
32 | }
33 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/rnstartermedusa/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.rnstartermedusa
2 |
3 | import android.app.Application
4 | import com.facebook.react.PackageList
5 | import com.facebook.react.ReactApplication
6 | import com.facebook.react.ReactHost
7 | import com.facebook.react.ReactNativeHost
8 | import com.facebook.react.ReactPackage
9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
11 | import com.facebook.react.defaults.DefaultReactNativeHost
12 | import com.facebook.react.soloader.OpenSourceMergedSoMapping
13 | import com.facebook.soloader.SoLoader
14 |
15 | class MainApplication : Application(), ReactApplication {
16 |
17 | override val reactNativeHost: ReactNativeHost =
18 | object : DefaultReactNativeHost(this) {
19 | override fun getPackages(): List =
20 | PackageList(this).packages.apply {
21 | // Packages that cannot be autolinked yet can be added manually here, for example:
22 | // add(MyReactNativePackage())
23 | }
24 |
25 | override fun getJSMainModuleName(): String = "index"
26 |
27 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
28 |
29 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
30 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
31 | }
32 |
33 | override val reactHost: ReactHost
34 | get() = getDefaultReactHost(applicationContext, reactNativeHost)
35 |
36 | override fun onCreate() {
37 | super.onCreate()
38 | SoLoader.init(this, OpenSourceMergedSoMapping)
39 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
40 | // If you opted-in for the New Architecture, we load the native entry point for this app.
41 | load()
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RnStarterMedusa
3 |
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | buildToolsVersion = "35.0.0"
4 | minSdkVersion = 24
5 | compileSdkVersion = 35
6 | targetSdkVersion = 35
7 | ndkVersion = "27.1.12297006"
8 | kotlinVersion = "2.0.21"
9 | }
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle")
16 | classpath("com.facebook.react:react-native-gradle-plugin")
17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
18 | }
19 | }
20 |
21 | apply plugin: "com.facebook.react.rootproject"
22 |
--------------------------------------------------------------------------------
/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: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
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 |
25 | # Use this property to specify which architecture you want to build.
26 | # You can also override it from the CLI using
27 | # ./gradlew -PreactNativeArchitectures=x86_64
28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
29 |
30 | # Use this property to enable support to the new architecture.
31 | # This will allow you to use TurboModules and the Fabric render in
32 | # your application. You should enable this flag either if you want
33 | # to write custom TurboModules/Fabric components OR use libraries that
34 | # are providing them.
35 | newArchEnabled=true
36 |
37 | # Use this property to enable or disable the Hermes JS engine.
38 | # If set to false, you will be using JSC instead.
39 | hermesEnabled=true
40 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/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-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/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 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/android/link-assets-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "migIndex": 1,
3 | "data": [
4 | {
5 | "path": "assets/fonts/Audiowide-Regular.ttf",
6 | "sha1": "651503b7a0a89f49f78f14d04c62f811853573cd"
7 | },
8 | {
9 | "path": "assets/fonts/Lato-Bold.ttf",
10 | "sha1": "542498221d97bee5bdbccf86ee8890bf8e8005c9"
11 | },
12 | {
13 | "path": "assets/fonts/Lato-Regular.ttf",
14 | "sha1": "e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3"
15 | },
16 | {
17 | "path": "assets/fonts/Lato-Thin.ttf",
18 | "sha1": "07290446bee3f81ce501a3c3dbfde6097c70ca15"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
2 | plugins { id("com.facebook.react.settings") }
3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
4 | rootProject.name = 'RnStarterMedusa'
5 | include ':app'
6 | includeBuild('../node_modules/@react-native/gradle-plugin')
7 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RnStarterMedusa",
3 | "displayName": "RnStarterMedusa"
4 | }
5 |
--------------------------------------------------------------------------------
/app/api/client.tsx:
--------------------------------------------------------------------------------
1 | import Medusa from '@medusajs/js-sdk';
2 | import {MEDUSA_BACKEND_URL, PUBLISHABLE_API_KEY} from '@env';
3 | import AsyncStorage from '@react-native-async-storage/async-storage';
4 |
5 | export const apiUrl = MEDUSA_BACKEND_URL || 'http://localhost:9000';
6 |
7 | const publishableKey = PUBLISHABLE_API_KEY || '';
8 | export const AUTH_TOKEN_KEY = 'auth_token';
9 |
10 | const apiClient = new Medusa({
11 | baseUrl: apiUrl,
12 | publishableKey: publishableKey,
13 | auth: {
14 | type: 'jwt',
15 | jwtTokenStorageMethod: 'custom',
16 | jwtTokenStorageKey: AUTH_TOKEN_KEY,
17 | storage: AsyncStorage,
18 | },
19 | });
20 |
21 | export default apiClient;
22 |
--------------------------------------------------------------------------------
/app/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | createStaticNavigation,
4 | StaticParamList,
5 | } from '@react-navigation/native';
6 | import {createNativeStackNavigator} from '@react-navigation/native-stack';
7 | import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
8 | import ThemeProvider from '@styles/theme-provider';
9 | import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
10 | import {GestureHandlerRootView} from 'react-native-gesture-handler';
11 | import TabBar from '@components/common/tab-bar';
12 | import Splash from '@screens/splash';
13 | import Home from '@screens/home';
14 | import Categories from '@screens/category/categories';
15 | import CategoryDetail from '@screens/category/category-detail';
16 | import Collections from '@screens/collection/collections';
17 | import CollectionDetail from '@screens/collection/collection-detail';
18 | import ProductDetail from '@screens/product-detail';
19 | import Cart from '@screens/cart';
20 | import Checkout from '@screens/checkout';
21 | import Profile from '@screens/profile/profile';
22 | import SignIn from '@screens/auth/login';
23 | import Register from '@screens/auth/register';
24 | import Orders from '@screens/order/orders';
25 | import OrderDetail from '@screens/order/order-detail';
26 | import ProfileDetail from '@screens/profile/profile-detail';
27 | import {CartProvider} from '@data/cart-context';
28 | import {RegionProvider} from '@data/region-context';
29 | import {CustomerProvider} from '@data/customer-context';
30 | import {LocaleProvider} from '@data/locale-context';
31 | import AddressForm from '@screens/address/address-form';
32 | import AddressList from '@screens/address/address-list';
33 | import RegionSelect from '@screens/region-select';
34 | import Settings from '@screens/settings';
35 |
36 | import '@styles/global.css';
37 | import {SafeAreaProvider} from 'react-native-safe-area-context';
38 |
39 | export type RootStackParamList = StaticParamList;
40 |
41 | const queryClient = new QueryClient();
42 |
43 | export default function App() {
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | const HomeTabs = createBottomTabNavigator({
66 | tabBar: props => ,
67 | screens: {
68 | Home: {
69 | screen: Home,
70 | options: {
71 | title: 'home',
72 | },
73 | },
74 | Categories: {
75 | screen: Categories,
76 | options: {
77 | title: 'categories',
78 | },
79 | },
80 | Collections: {
81 | screen: Collections,
82 | options: {
83 | title: 'collections',
84 | },
85 | },
86 | Profile: {
87 | screen: Profile,
88 | options: {
89 | title: 'profile',
90 | },
91 | },
92 | },
93 | screenOptions: {
94 | headerShown: false,
95 | },
96 | });
97 |
98 | const RootStack = createNativeStackNavigator({
99 | initialRouteName: 'Splash',
100 | groups: {
101 | App: {
102 | screenOptions: {
103 | headerShown: false,
104 | },
105 | screens: {
106 | Main: HomeTabs,
107 | Splash,
108 | ProductDetail,
109 | CategoryDetail,
110 | CollectionDetail,
111 | Cart,
112 | Checkout,
113 | SignIn,
114 | Register,
115 | Orders,
116 | OrderDetail,
117 | ProfileDetail,
118 | AddressList,
119 | AddressForm,
120 | Settings,
121 | },
122 | },
123 | Modal: {
124 | screenOptions: {
125 | presentation: 'modal',
126 | headerShown: false,
127 | },
128 | screens: {
129 | RegionSelect: RegionSelect,
130 | },
131 | },
132 | },
133 | });
134 |
135 | const Navigation = createStaticNavigation(RootStack);
136 |
--------------------------------------------------------------------------------
/app/components/cart/animated-cart-button.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@components/common/button';
2 | import Text from '@components/common/text';
3 | import React, {useEffect, useRef} from 'react';
4 | import Icon from '@react-native-vector-icons/ant-design';
5 | import {View} from 'react-native';
6 | import Animated, {
7 | useAnimatedStyle,
8 | useSharedValue,
9 | withTiming,
10 | withSpring,
11 | withSequence,
12 | } from 'react-native-reanimated';
13 | import {useLocalization} from '@fluent/react';
14 | import {useColors} from '@styles/hooks';
15 | import {useCart} from '@data/cart-context';
16 | import {useProductQuantity} from '@data/hooks';
17 | import {useNavigation} from '@react-navigation/native';
18 | import Badge from '@components/common/badge';
19 |
20 | type AnimatedCartButtonProps = {
21 | productId: string;
22 | selectedVariantId?: string;
23 | disabled?: boolean;
24 | inStock?: boolean;
25 | hasSelectedAllOptions?: boolean;
26 | };
27 |
28 | const AnimatedCartButton = ({
29 | productId,
30 | selectedVariantId,
31 | disabled = false,
32 | inStock,
33 | hasSelectedAllOptions = false,
34 | }: AnimatedCartButtonProps) => {
35 | const {l10n} = useLocalization();
36 | const {addToCart} = useCart();
37 | const [adding, setAdding] = React.useState(false);
38 | const productQuantityInCart = useProductQuantity(productId);
39 | const showViewCart = useSharedValue(productQuantityInCart > 0);
40 | const viewCartWidth = 192;
41 |
42 | useEffect(() => {
43 | if (productQuantityInCart > 0) {
44 | showViewCart.value = true;
45 | } else {
46 | showViewCart.value = false;
47 | }
48 | }, [productQuantityInCart, showViewCart]);
49 |
50 | const rowStyles = useAnimatedStyle(() => {
51 | return {
52 | gap: withTiming(showViewCart.value ? 8 : 0),
53 | };
54 | });
55 |
56 | const viewCartStyles = useAnimatedStyle(() => {
57 | return {
58 | width: withTiming(showViewCart.value ? viewCartWidth : 0),
59 | opacity: withTiming(showViewCart.value ? 1 : 0),
60 | };
61 | });
62 |
63 | const addToCartHandler = async () => {
64 | if (!selectedVariantId || disabled || !inStock) {
65 | return;
66 | }
67 | setAdding(true);
68 | await addToCart(selectedVariantId, 1);
69 | setAdding(false);
70 | };
71 |
72 | return (
73 |
74 |
75 |
76 |
77 |
78 |
88 |
89 |
90 | );
91 | };
92 |
93 | const ViewCart = ({quantity}: {quantity: number}) => {
94 | const {l10n} = useLocalization();
95 | const colors = useColors();
96 | const navigation = useNavigation();
97 | const scale = useSharedValue(1);
98 | const prevQuantity = useRef(quantity);
99 |
100 | useEffect(() => {
101 | // Only animate when quantity increases
102 | if (prevQuantity.current && quantity > prevQuantity.current) {
103 | scale.value = withSequence(
104 | withSpring(1.3, {damping: 8}),
105 | withSpring(1, {damping: 8}),
106 | );
107 | }
108 | prevQuantity.current = quantity;
109 | }, [quantity, scale]);
110 |
111 | const badgeAnimatedStyle = useAnimatedStyle(() => {
112 | return {
113 | transform: [{scale: scale.value}],
114 | };
115 | });
116 |
117 | const navigateToCart = () => {
118 | navigation.navigate('Cart');
119 | };
120 |
121 | return (
122 |
144 | );
145 | };
146 |
147 | export default AnimatedCartButton;
148 |
--------------------------------------------------------------------------------
/app/components/cart/cart-content.tsx:
--------------------------------------------------------------------------------
1 | import {StoreCart} from '@medusajs/types';
2 | import React from 'react';
3 | import {Image, View} from 'react-native';
4 | import {useLocalization} from '@fluent/react';
5 | import Text from '@components/common/text';
6 | import {HttpTypes} from '@medusajs/types';
7 | import LineItemQuantity from '@components/cart/line-item-quantity';
8 | import LineItemUnitPrice from '@components/cart/line-item-price';
9 | import {convertToLocale} from '@utils/product-price';
10 | import {formatImageUrl} from '@utils/image-url';
11 | import PromoCodeInput from '@components/cart/promo-code-input';
12 |
13 | type CartContentProps = {
14 | cart: HttpTypes.StoreCart;
15 | mode: 'checkout' | 'cart';
16 | };
17 |
18 | const CartContent = ({cart, mode}: CartContentProps) => {
19 | return (
20 |
21 |
22 |
23 |
24 | {mode === 'cart' && }
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | const CartItems = ({
32 | cart,
33 | mode,
34 | }: {
35 | cart?: StoreCart;
36 | mode: 'checkout' | 'cart';
37 | }) => {
38 | if (!cart) {
39 | return null;
40 | }
41 | const sortedItems = cart?.items?.sort((a, b) => {
42 | return (a.created_at ?? '') < (b.created_at ?? '') ? -1 : 1;
43 | });
44 | return (
45 |
46 | {sortedItems?.map(item => (
47 |
53 | ))}
54 |
55 | );
56 | };
57 |
58 | type CartItemProps = {
59 | item: HttpTypes.StoreCartLineItem;
60 | currencyCode: string;
61 | mode: 'checkout' | 'cart';
62 | };
63 |
64 | const CartItem = ({item, currencyCode, mode}: CartItemProps) => {
65 | const {l10n} = useLocalization();
66 |
67 | return (
68 |
69 |
73 |
74 |
75 | {item.product_title}
76 |
77 | {!!item.variant_title && (
78 |
79 | {l10n.getString('variant')}: {item.variant_title}
80 |
81 | )}
82 |
83 |
88 |
89 |
90 |
91 |
92 | );
93 | };
94 |
95 | type SummaryItem = {
96 | name: string;
97 | key: keyof StoreCart;
98 | };
99 |
100 | const CartSummary = ({cart}: {cart?: StoreCart}) => {
101 | const {l10n} = useLocalization();
102 | if (!cart) {
103 | return null;
104 | }
105 | const summaryItems: SummaryItem[] = [
106 | {
107 | name: l10n.getString('subtotal'),
108 | key: 'item_subtotal',
109 | },
110 | {
111 | name: l10n.getString('shipping'),
112 | key: 'shipping_total',
113 | },
114 | {
115 | name: l10n.getString('taxes'),
116 | key: 'tax_total',
117 | },
118 | ];
119 |
120 | const discountTotal = cart.discount_total || 0;
121 |
122 | return (
123 |
124 | {l10n.getString('summary')}
125 |
126 | {summaryItems.map(item => (
127 |
130 | {item.name}
131 |
132 | {convertToLocale({
133 | amount: cart[item.key] as number,
134 | currency_code: cart.currency_code,
135 | })}
136 |
137 |
138 | ))}
139 | {discountTotal > 0 && (
140 |
141 | {l10n.getString('discount')}
142 |
143 | -
144 | {convertToLocale({
145 | amount: discountTotal,
146 | currency_code: cart.currency_code,
147 | })}
148 |
149 |
150 | )}
151 |
152 |
153 |
154 | {l10n.getString('total')}
155 |
156 | {convertToLocale({
157 | amount: cart.total,
158 | currency_code: cart.currency_code,
159 | })}
160 |
161 |
162 |
163 |
164 | );
165 | };
166 |
167 | export default CartContent;
168 |
--------------------------------------------------------------------------------
/app/components/cart/line-item-price.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, Text} from 'react-native';
3 | import {convertToLocale} from '@utils/product-price';
4 | import {HttpTypes} from '@medusajs/types';
5 |
6 | type LineItemUnitPriceProps = {
7 | item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem;
8 | style?: 'default' | 'tight';
9 | currencyCode: string;
10 | };
11 |
12 | const LineItemUnitPrice = ({
13 | item,
14 | style = 'default',
15 | currencyCode,
16 | }: LineItemUnitPriceProps) => {
17 | const {total, original_total, unit_price} = item;
18 |
19 | const hasReducedPrice = total < original_total;
20 |
21 | const percentage_diff = Math.round(
22 | ((original_total - total) / original_total) * 100,
23 | );
24 |
25 | return (
26 |
27 | {hasReducedPrice && (
28 | <>
29 |
30 | {style === 'default' && (
31 | Original:
32 | )}
33 |
36 | {convertToLocale({
37 | amount: original_total / item.quantity,
38 | currency_code: currencyCode,
39 | })}
40 |
41 |
42 | {style === 'default' && (
43 | -{percentage_diff}%
44 | )}
45 | >
46 | )}
47 |
51 | {convertToLocale({
52 | amount: unit_price * item.quantity,
53 | currency_code: currencyCode,
54 | })}
55 |
56 |
57 | );
58 | };
59 |
60 | export default LineItemUnitPrice;
61 |
--------------------------------------------------------------------------------
/app/components/cart/line-item-quantity.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {useColors} from '@styles/hooks';
3 | import {ActivityIndicator, TouchableOpacity, View} from 'react-native';
4 | import Icon from '@react-native-vector-icons/ant-design';
5 | import {useLocalization} from '@fluent/react';
6 | import Text from '@components/common/text';
7 | import {useCart} from '@data/cart-context';
8 |
9 | type LineItemQuantityProps = {
10 | quantity: number;
11 | lineItemId: string;
12 | mode: 'checkout' | 'cart';
13 | };
14 |
15 | const LineItemQuantity = ({
16 | quantity,
17 | lineItemId,
18 | mode,
19 | }: LineItemQuantityProps) => {
20 | const {l10n} = useLocalization();
21 | const colors = useColors();
22 | const {updateLineItem} = useCart();
23 | const [updating, setUpdating] = React.useState(false);
24 |
25 | const increment = async () => {
26 | const newQuantity = quantity + 1;
27 | return updateQuantity(newQuantity);
28 | };
29 |
30 | const decrement = async () => {
31 | const newQuantity = quantity - 1;
32 | return updateQuantity(newQuantity);
33 | };
34 |
35 | const deleteLineItem = async () => {
36 | setUpdating(true);
37 | await updateLineItem(lineItemId, 0);
38 | setUpdating(false);
39 | };
40 |
41 | const updateQuantity = async (newQuantity: number) => {
42 | setUpdating(true);
43 | await updateLineItem(lineItemId, newQuantity);
44 | setUpdating(false);
45 | };
46 |
47 | return (
48 |
49 | {mode === 'cart' ? (
50 |
51 |
54 |
55 |
56 | {quantity}
57 |
60 |
61 |
62 |
63 | ) : (
64 |
65 | {l10n.getString('qty')}: {quantity}
66 |
67 | )}
68 | {updating ? (
69 |
70 | ) : mode === 'cart' ? (
71 |
72 |
73 |
74 | ) : null}
75 |
76 | );
77 | };
78 |
79 | export default LineItemQuantity;
80 |
--------------------------------------------------------------------------------
/app/components/cart/promo-code-input.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {View, TouchableOpacity, Keyboard} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import {useCart} from '@data/cart-context';
5 | import Input from '@components/common/input';
6 | import RoundedButton from '@components/common/rounded-button';
7 | import Text from '@components/common/text';
8 | import Icon from '@react-native-vector-icons/ant-design';
9 | import {useColors} from '@styles/hooks';
10 | import {HttpTypes} from '@medusajs/types';
11 | import Badge from '@components/common/badge';
12 | import {convertToLocale} from '@utils/product-price';
13 | import Button from '@components/common/button';
14 | import Accordion from '@components/common/accordion';
15 | import {useSharedValue} from 'react-native-reanimated';
16 |
17 | type Promotion = HttpTypes.StorePromotion;
18 |
19 | const PromoCodeInput = () => {
20 | const {l10n} = useLocalization();
21 | const [code, setCode] = useState('');
22 | const [error, setError] = useState(null);
23 | const [isLoading, setIsLoading] = useState(false);
24 | const isExpanded = useSharedValue(false);
25 | const {applyPromoCode, removePromoCode, cart} = useCart();
26 | const colors = useColors();
27 |
28 | const handleCodeChange = (text: string) => {
29 | setCode(text);
30 | if (error) {
31 | setError(null);
32 | }
33 | };
34 |
35 | const handleApplyCode = async () => {
36 | if (!code || isLoading) {
37 | return;
38 | }
39 |
40 | Keyboard.dismiss();
41 |
42 | setError(null);
43 | setIsLoading(true);
44 | try {
45 | const applied = await applyPromoCode(code);
46 | if (applied) {
47 | setCode('');
48 | } else {
49 | setError(l10n.getString('invalid-promo-code'));
50 | }
51 | } catch (err: any) {
52 | setError(err?.message || l10n.getString('failed-to-apply-promotion'));
53 | } finally {
54 | setIsLoading(false);
55 | }
56 | };
57 |
58 | const handleRemoveCode = async (promoCode: string) => {
59 | if (isLoading) {
60 | return;
61 | }
62 | setIsLoading(true);
63 | try {
64 | await removePromoCode(promoCode);
65 | } catch (err: any) {
66 | setError(err?.message || l10n.getString('failed-to-remove-promotion'));
67 | } finally {
68 | setIsLoading(false);
69 | }
70 | };
71 |
72 | const promotions = cart?.promotions as Promotion[] | undefined;
73 |
74 | return (
75 | <>
76 |
77 | {
79 | isExpanded.value = !isExpanded.value;
80 | }}>
81 |
83 | {l10n.getString('add-a-promo-code')}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
97 |
98 |
99 |
105 |
106 |
107 |
108 |
109 |
110 | {promotions && promotions.length > 0 && (
111 |
112 |
113 | {l10n.getString('applied-promotions')}:
114 |
115 | {promotions.map((promotion, index) => (
116 |
119 |
120 |
121 |
125 |
126 | {promotion.code}
127 |
128 |
129 | {promotion.application_method?.value !== undefined && (
130 |
131 | {promotion.application_method.type === 'percentage'
132 | ? `${promotion.application_method.value}%`
133 | : convertToLocale({
134 | amount: promotion.application_method.value,
135 | currency_code:
136 | promotion.application_method.currency_code || '',
137 | })}
138 |
139 | )}
140 |
141 | {promotion.code && (
142 |
145 | promotion.code && handleRemoveCode(promotion.code)
146 | }>
147 |
148 |
149 | )}
150 |
151 | ))}
152 |
153 | )}
154 | >
155 | );
156 | };
157 |
158 | export default PromoCodeInput;
159 |
--------------------------------------------------------------------------------
/app/components/checkout/checkout-steps.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, TouchableOpacity} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from '@components/common/text';
5 | import AntDesign from '@react-native-vector-icons/ant-design';
6 | import {useColors} from '@styles/hooks';
7 | import {CheckoutStep, CHECKOUT_STEPS} from '../../types/checkout';
8 |
9 | type CheckoutStepsProps = {
10 | currentStep: CheckoutStep;
11 | onStepPress: (step: CheckoutStep) => void;
12 | };
13 |
14 | const CheckoutSteps = ({currentStep, onStepPress}: CheckoutStepsProps) => {
15 | const {l10n} = useLocalization();
16 | const colors = useColors();
17 |
18 | const getStepBackground = (isActive: boolean, isPast: boolean) => {
19 | if (isActive) {
20 | return 'bg-primary';
21 | } else if (isPast) {
22 | return 'bg-primary opacity-80';
23 | } else {
24 | return 'bg-gray-300';
25 | }
26 | };
27 |
28 | return (
29 |
30 | {CHECKOUT_STEPS.map((step, index: number) => {
31 | const isActive = step.id === currentStep;
32 | const isPast =
33 | CHECKOUT_STEPS.findIndex((s: typeof step) => s.id === currentStep) >
34 | index;
35 |
36 | return (
37 |
38 | {index > 0 && (
39 |
44 | )}
45 | isPast && onStepPress(step.id)}
47 | disabled={!isPast}
48 | className="items-center">
49 |
54 |
63 |
64 |
68 | {l10n.getString(step.title)}
69 |
70 |
71 |
72 | );
73 | })}
74 |
75 | );
76 | };
77 |
78 | export default CheckoutSteps;
79 |
--------------------------------------------------------------------------------
/app/components/checkout/steps/address-step.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, Switch} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from '@components/common/text';
5 | import Input from '@components/common/input';
6 | import {Controller, UseFormReturn} from 'react-hook-form';
7 | import {CheckoutFormData} from '../../../types/checkout';
8 | import AddressForm from '../address-form';
9 |
10 | type AddressStepProps = {
11 | form: UseFormReturn;
12 | isLoading: boolean;
13 | countries: {label: string; value: string}[];
14 | };
15 |
16 | const AddressStep = ({form, isLoading, countries}: AddressStepProps) => {
17 | const {l10n} = useLocalization();
18 | const {watch, clearErrors} = form;
19 | const useSameBilling = watch('use_same_billing');
20 |
21 | return (
22 |
23 | (
27 | {
31 | clearErrors('email');
32 | onChange(val);
33 | }}
34 | error={form.formState.errors.email?.message}
35 | editable={!isLoading}
36 | keyboardType="email-address"
37 | autoCapitalize="none"
38 | />
39 | )}
40 | />
41 |
42 |
49 |
50 |
51 | (
55 |
60 | )}
61 | />
62 | {l10n.getString('use-same-address-for-billing')}
63 |
64 |
65 | {!useSameBilling && (
66 |
67 |
74 |
75 | )}
76 |
77 | );
78 | };
79 |
80 | export default AddressStep;
81 |
--------------------------------------------------------------------------------
/app/components/checkout/steps/payment-step.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {View, TouchableOpacity} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from '@components/common/text';
5 | import {useQuery} from '@tanstack/react-query';
6 | import {HttpTypes} from '@medusajs/types';
7 | import apiClient from '@api/client';
8 | import {
9 | PaymentProvider,
10 | PAYMENT_PROVIDER_DETAILS_MAP,
11 | } from '../../../types/checkout';
12 |
13 | type PaymentStepProps = {
14 | cart: HttpTypes.StoreCart;
15 | selectedProviderId?: string;
16 | onSelectProvider: (providerId: string) => void;
17 | };
18 |
19 | const PaymentStep = ({
20 | cart,
21 | selectedProviderId,
22 | onSelectProvider,
23 | }: PaymentStepProps) => {
24 | const {l10n} = useLocalization();
25 | const [error, setError] = useState(null);
26 |
27 | // Fetch available payment providers
28 | const {data: paymentProviders, isLoading: isLoadingProviders} = useQuery({
29 | queryKey: ['payment-providers', cart?.region_id],
30 | queryFn: async () => {
31 | if (!cart?.region_id) {
32 | throw new Error(l10n.getString('no-region-id'));
33 | }
34 | const {payment_providers} =
35 | await apiClient.store.payment.listPaymentProviders({
36 | region_id: cart.region_id,
37 | });
38 | return payment_providers as PaymentProvider[];
39 | },
40 | enabled: !!cart?.region_id,
41 | });
42 |
43 | const handleProviderSelect = (provider: PaymentProvider) => {
44 | // Skip if this provider is already selected
45 | if (selectedProviderId === provider.id) {
46 | return;
47 | }
48 |
49 | setError(null);
50 | onSelectProvider(provider.id);
51 | };
52 |
53 | const getPaymentUI = () => {
54 | if (!selectedProviderId) {
55 | return null;
56 | }
57 |
58 | switch (selectedProviderId) {
59 | case 'pp_stripe_stripe':
60 | return (
61 |
62 | {l10n.getString('stripe-payment-coming-soon')}
63 |
64 | );
65 | case 'pp_system_default':
66 | return (
67 |
68 | {l10n.getString(
69 | 'no-additional-actions-required-for-manual-payment',
70 | )}
71 |
72 | );
73 | default:
74 | return (
75 |
76 | {l10n.getString('payment-provider-is-in-development', {
77 | provider:
78 | PAYMENT_PROVIDER_DETAILS_MAP[selectedProviderId]?.name ||
79 | selectedProviderId,
80 | })}
81 |
82 | );
83 | }
84 | };
85 |
86 | if (isLoadingProviders || !paymentProviders?.length) {
87 | return (
88 |
89 |
90 | {l10n.getString('loading-shipping-options')}...
91 |
92 |
93 | );
94 | }
95 |
96 | return (
97 |
98 |
99 | {l10n.getString('select-payment-method')}
100 |
101 | {error && (
102 |
103 | {error}
104 |
105 | )}
106 |
107 | {paymentProviders.map(provider => {
108 | const isSelected = selectedProviderId === provider.id;
109 | const displayName =
110 | PAYMENT_PROVIDER_DETAILS_MAP[provider.id]?.name || provider.id;
111 |
112 | return (
113 | handleProviderSelect(provider)}
116 | className={`p-4 border rounded-lg flex-row justify-between items-center ${
117 | isSelected ? 'border-primary' : 'border-gray-200'
118 | }`}>
119 |
120 |
124 | {isSelected ? (
125 |
126 | ) : null}
127 |
128 | {displayName}
129 |
130 |
131 | );
132 | })}
133 |
134 | {selectedProviderId && (
135 |
136 | {getPaymentUI()}
137 |
138 | )}
139 |
140 | );
141 | };
142 |
143 | export default PaymentStep;
144 |
--------------------------------------------------------------------------------
/app/components/checkout/steps/review-step.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View} from 'react-native';
3 | import Text from '@components/common/text';
4 | import {HttpTypes} from '@medusajs/types';
5 | import {useLocalization} from '@fluent/react';
6 | import CartContent from '@components/cart/cart-content';
7 | import {PAYMENT_PROVIDER_DETAILS_MAP} from '../../../types/checkout';
8 |
9 | type ReviewStepProps = {
10 | cart: HttpTypes.StoreCart;
11 | };
12 |
13 | const ReviewStep = ({cart}: ReviewStepProps) => {
14 | const {l10n} = useLocalization();
15 | // Find the selected shipping option
16 | const selectedShippingMethod = cart.shipping_methods?.at(-1);
17 |
18 | const selectedPaymentMethodId =
19 | cart.payment_collection?.payment_sessions?.find(
20 | (paymentSession: any) => paymentSession.status === 'pending',
21 | )?.provider_id || '';
22 |
23 | return (
24 |
25 |
26 |
27 | {/* Selected Shipping Method */}
28 |
29 |
30 | {l10n.getString('shipping-method')}
31 |
32 |
33 | {selectedShippingMethod?.name ||
34 | selectedShippingMethod?.id ||
35 | l10n.getString('no-shipping-method-selected')}
36 |
37 |
38 |
39 | {/* Selected Payment Method */}
40 |
41 |
42 | {l10n.getString('payment-method')}
43 |
44 |
45 | {selectedPaymentMethodId
46 | ? PAYMENT_PROVIDER_DETAILS_MAP[selectedPaymentMethodId]?.name ||
47 | selectedPaymentMethodId
48 | : l10n.getString('no-payment-method-selected')}
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default ReviewStep;
57 |
--------------------------------------------------------------------------------
/app/components/common/accordion.tsx:
--------------------------------------------------------------------------------
1 | import React, {PropsWithChildren} from 'react';
2 | import {View} from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | useDerivedValue,
6 | SharedValue,
7 | withTiming,
8 | useSharedValue,
9 | } from 'react-native-reanimated';
10 |
11 | type AccordionProps = PropsWithChildren<{
12 | isExpanded: SharedValue;
13 | duration?: number;
14 | className?: string;
15 | }>;
16 |
17 | const Accordion = ({
18 | isExpanded,
19 | children,
20 | duration = 300,
21 | className = '',
22 | }: AccordionProps) => {
23 | const height = useSharedValue(0);
24 |
25 | const derivedHeight = useDerivedValue(() =>
26 | withTiming(height.value * Number(isExpanded.value), {
27 | duration,
28 | }),
29 | );
30 |
31 | const bodyStyle = useAnimatedStyle(() => ({
32 | height: derivedHeight.value,
33 | }));
34 |
35 | return (
36 |
39 | {
41 | height.value = e.nativeEvent.layout.height;
42 | }}
43 | className="w-full absolute">
44 | {children}
45 |
46 |
47 | );
48 | };
49 |
50 | export default Accordion;
51 |
--------------------------------------------------------------------------------
/app/components/common/badge.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View} from 'react-native';
3 | import Text from '@components/common/text';
4 | import {tv, type VariantProps} from 'tailwind-variants';
5 |
6 | const badge = tv({
7 | base: 'w-5 h-5 rounded-full justify-center items-center',
8 | variants: {
9 | variant: {
10 | primary: 'bg-primary',
11 | secondary: 'bg-background border border-gray-300',
12 | },
13 | },
14 | defaultVariants: {
15 | variant: 'primary',
16 | },
17 | });
18 |
19 | const badgeText = tv({
20 | base: 'text-xs font-content-bold',
21 | variants: {
22 | variant: {
23 | primary: 'text-content-secondary',
24 | secondary: 'text-content',
25 | },
26 | },
27 | defaultVariants: {
28 | variant: 'primary',
29 | },
30 | });
31 |
32 | type BadgeProps = VariantProps & {
33 | quantity: number;
34 | className?: string;
35 | };
36 |
37 | const Badge = ({quantity, className, variant}: BadgeProps) => {
38 | return (
39 |
40 |
41 | {quantity}
42 |
43 |
44 | );
45 | };
46 |
47 | export default Badge;
48 |
--------------------------------------------------------------------------------
/app/components/common/button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {ActivityIndicator, TouchableOpacity} from 'react-native';
3 | import Text from './text';
4 | import {tv, type VariantProps} from 'tailwind-variants';
5 | import {useColors} from '@styles/hooks';
6 |
7 | const button = tv({
8 | base: 'justify-center items-center rounded-xl h-14',
9 | variants: {
10 | variant: {
11 | primary: 'bg-primary',
12 | secondary: 'bg-background border border-gray-300',
13 | },
14 | disabled: {
15 | true: 'bg-gray-300',
16 | false: '',
17 | },
18 | },
19 | defaultVariants: {
20 | variant: 'primary',
21 | disabled: false,
22 | },
23 | });
24 |
25 | const buttonText = tv({
26 | base: 'text-base font-content-bold block mx-4',
27 | variants: {
28 | disabled: {
29 | true: 'text-gray-400',
30 |
31 | false: '',
32 | },
33 | variant: {
34 | primary: 'text-content-secondary',
35 | secondary: 'text-content',
36 | },
37 | },
38 | defaultVariants: {
39 | variant: 'primary',
40 | disabled: false,
41 | },
42 | });
43 |
44 | type BaseProps = VariantProps & {
45 | className?: string;
46 | onPress?: () => void;
47 | disabled?: boolean;
48 | loading?: boolean;
49 | };
50 |
51 | type WithTitle = BaseProps & {
52 | title: string;
53 | children?: never;
54 | };
55 |
56 | type WithChildren = BaseProps & {
57 | title?: never;
58 | children: React.ReactNode;
59 | };
60 |
61 | type Props = WithTitle | WithChildren;
62 |
63 | const CommonButton = ({
64 | title,
65 | onPress,
66 | loading,
67 | disabled,
68 | variant,
69 | children,
70 | }: Props) => {
71 | const colors = useColors();
72 | const renderContent = () => {
73 | if (loading) {
74 | const color =
75 | variant === 'secondary' ? colors.content : colors.contentSecondary;
76 | return ;
77 | }
78 |
79 | if (children) {
80 | return children;
81 | }
82 | return (
83 |
84 | {title}
85 |
86 | );
87 | };
88 | return (
89 | {} : onPress}
91 | disabled={disabled || loading}
92 | className={button({disabled, variant})}>
93 | {renderContent()}
94 |
95 | );
96 | };
97 |
98 | export default CommonButton;
99 |
--------------------------------------------------------------------------------
/app/components/common/card.tsx:
--------------------------------------------------------------------------------
1 | import React, {PropsWithChildren} from 'react';
2 | import {View} from 'react-native';
3 |
4 | const Card = ({children}: PropsWithChildren) => {
5 | return (
6 |
7 | {children}
8 |
9 | );
10 | };
11 |
12 | export default Card;
13 |
--------------------------------------------------------------------------------
/app/components/common/dropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {View} from 'react-native';
3 | import {
4 | Dropdown as BaseDropdown,
5 | type IDropdownRef,
6 | } from 'react-native-element-dropdown';
7 | import Text from './text';
8 | import {DropdownProps as BaseDropdownProps} from 'react-native-element-dropdown/lib/typescript/components/Dropdown/model';
9 |
10 | type DropdownProps = BaseDropdownProps & {
11 | label?: string;
12 | error?: string;
13 | required?: boolean;
14 | className?: string;
15 | containerClassName?: string;
16 | };
17 |
18 | const Dropdown = React.forwardRef>(
19 | (
20 | {
21 | label,
22 | error,
23 | required,
24 | containerClassName = '',
25 | className = '',
26 | onChangeText,
27 | onFocus,
28 | onBlur,
29 | ...props
30 | },
31 | ref,
32 | ) => {
33 | const [isFocused, setIsFocused] = useState(false);
34 |
35 | return (
36 |
37 | {label && (
38 |
39 | {label}
40 | {required && '*'}
41 |
42 | )}
43 |
51 | {
54 | onChangeText?.(text);
55 | }}
56 | onFocus={() => {
57 | setIsFocused(true);
58 | onFocus?.();
59 | }}
60 | onBlur={() => {
61 | setIsFocused(false);
62 | onBlur?.();
63 | }}
64 | {...props}
65 | />
66 |
67 | {error ? {error} : null}
68 |
69 | );
70 | },
71 | );
72 |
73 | Dropdown.displayName = 'Input';
74 |
75 | export default Dropdown;
76 |
--------------------------------------------------------------------------------
/app/components/common/error-ui.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View} from 'react-native';
3 | import Text from '@components/common/text';
4 |
5 | const ErrorUI = () => {
6 | return (
7 |
8 |
9 |
10 | Something went wrong, Please try again later
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default ErrorUI;
18 |
--------------------------------------------------------------------------------
/app/components/common/fab-button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Pressable, TouchableOpacity} from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | withSpring,
6 | withDelay,
7 | withTiming,
8 | useSharedValue,
9 | SharedValue,
10 | } from 'react-native-reanimated';
11 | import Icon from '@react-native-vector-icons/ant-design';
12 | import {useColors} from '@styles/hooks';
13 |
14 | const OFFSET = 56; // Distance between buttons
15 | const SPRING_CONFIG = {
16 | duration: 1200,
17 | overshootClamping: true,
18 | dampingRatio: 0.8,
19 | };
20 |
21 | type IconName = 'tool' | 'switcher' | 'plus' | 'setting' | 'database' | 'bug';
22 |
23 | type ActionButton = {
24 | icon: IconName;
25 | onPress: () => void;
26 | label?: string;
27 | };
28 |
29 | type FabButtonProps = {
30 | actions: ActionButton[];
31 | mainIcon?: IconName;
32 | };
33 |
34 | const ActionButton = ({
35 | isExpanded,
36 | index,
37 | icon,
38 | onPress,
39 | }: {
40 | isExpanded: SharedValue;
41 | index: number;
42 | icon: IconName;
43 | onPress: () => void;
44 | }) => {
45 | const colors = useColors();
46 | const animatedStyles = useAnimatedStyle(() => {
47 | const moveValue = isExpanded.value ? OFFSET * index : 0;
48 | const translateValue = withSpring(-moveValue, SPRING_CONFIG);
49 | const delay = index * 100;
50 | const scaleValue = isExpanded.value ? 1 : 0;
51 |
52 | return {
53 | transform: [
54 | {translateY: translateValue},
55 | {
56 | scale: withDelay(delay, withTiming(scaleValue)),
57 | },
58 | ],
59 | opacity: withDelay(delay, withTiming(isExpanded.value ? 1 : 0)),
60 | };
61 | });
62 |
63 | return (
64 |
65 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | const FabButton = ({actions, mainIcon = 'plus'}: FabButtonProps) => {
75 | const colors = useColors();
76 | const isExpanded = useSharedValue(false);
77 |
78 | const toggleExpanded = () => {
79 | isExpanded.value = !isExpanded.value;
80 | };
81 |
82 | const mainButtonStyle = useAnimatedStyle(() => {
83 | return {
84 | transform: [
85 | {
86 | rotate: withSpring(
87 | isExpanded.value ? '45deg' : '0deg',
88 | SPRING_CONFIG,
89 | ),
90 | },
91 | ],
92 | };
93 | });
94 |
95 | return (
96 |
97 | {actions.map((action, index) => (
98 |
105 | ))}
106 |
107 |
110 |
111 |
112 |
113 |
114 | );
115 | };
116 |
117 | export default FabButton;
118 |
--------------------------------------------------------------------------------
/app/components/common/input.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {TextInput, TextInputProps, View} from 'react-native';
3 | import Text from './text';
4 |
5 | type InputProps = TextInputProps & {
6 | label?: string;
7 | error?: string;
8 | required?: boolean;
9 | containerClassName?: string;
10 | };
11 |
12 | const Input = React.forwardRef(
13 | (
14 | {
15 | label,
16 | error,
17 | required,
18 | containerClassName = '',
19 | className = '',
20 | onChangeText,
21 | onFocus,
22 | onBlur,
23 | ...props
24 | },
25 | ref,
26 | ) => {
27 | const [isFocused, setIsFocused] = useState(false);
28 |
29 | return (
30 |
31 | {label && (
32 |
33 | {label}
34 | {required && '*'}
35 |
36 | )}
37 | {
47 | onChangeText?.(text);
48 | }}
49 | onFocus={e => {
50 | setIsFocused(true);
51 | onFocus?.(e);
52 | }}
53 | onBlur={e => {
54 | setIsFocused(false);
55 | onBlur?.(e);
56 | }}
57 | {...props}
58 | />
59 | {error ? {error} : null}
60 |
61 | );
62 | },
63 | );
64 |
65 | Input.displayName = 'Input';
66 |
67 | export default Input;
68 |
--------------------------------------------------------------------------------
/app/components/common/loader.tsx:
--------------------------------------------------------------------------------
1 | import {useColors} from '@styles/hooks';
2 | import React from 'react';
3 | import {ActivityIndicator, View} from 'react-native';
4 |
5 | const Loader = () => {
6 | const colors = useColors();
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default Loader;
17 |
--------------------------------------------------------------------------------
/app/components/common/navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View} from 'react-native';
3 | import RoundedButton from '@components/common/rounded-button';
4 | import Icon from '@react-native-vector-icons/ant-design';
5 | import {useNavigation} from '@react-navigation/native';
6 | import Text from '@components/common/text';
7 |
8 | type NavbarProps = {
9 | title?: string;
10 | showBackButton?: boolean;
11 | };
12 |
13 | const Navbar = ({title, showBackButton = true}: NavbarProps) => {
14 | const navigation = useNavigation();
15 | const goBack = () => {
16 | navigation.goBack();
17 | };
18 | return (
19 |
20 |
21 | {showBackButton ? (
22 |
23 |
24 |
25 | ) : (
26 |
27 | )}
28 |
29 |
30 | {title}
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default Navbar;
38 |
--------------------------------------------------------------------------------
/app/components/common/rounded-button.tsx:
--------------------------------------------------------------------------------
1 | import React, {PropsWithChildren} from 'react';
2 | import {TouchableOpacity} from 'react-native';
3 | import {tv} from 'tailwind-variants';
4 |
5 | const button = tv({
6 | base: 'bg-content-secondary rounded-full items-center justify-center elevation-sm',
7 | variants: {
8 | size: {
9 | sm: 'h-6 w-6',
10 | md: 'h-12 w-12',
11 | lg: 'h-14 w-14',
12 | },
13 | },
14 | defaultVariants: {
15 | size: 'md',
16 | },
17 | });
18 |
19 | type RoundedButtonProps = {
20 | onPress?: () => void;
21 | size?: 'sm' | 'md' | 'lg';
22 | className?: string;
23 | };
24 |
25 | const RoundedButton = ({
26 | children,
27 | onPress,
28 | size,
29 | className,
30 | }: PropsWithChildren) => {
31 | return (
32 |
35 | {children}
36 |
37 | );
38 | };
39 |
40 | export default RoundedButton;
41 |
--------------------------------------------------------------------------------
/app/components/common/tab-bar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {TouchableNativeFeedback, View} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from './text';
5 | import {useColors} from '@styles/hooks';
6 | import {BottomTabBarProps} from '@react-navigation/bottom-tabs';
7 | import Icon from '@react-native-vector-icons/ant-design';
8 |
9 | type icon = 'home' | 'appstore' | 'profile' | 'bars';
10 |
11 | function TabBar({state, descriptors, navigation}: BottomTabBarProps) {
12 | const {l10n} = useLocalization();
13 | const colors = useColors();
14 | const iconMap: Record = {
15 | Home: 'home',
16 | Categories: 'appstore',
17 | Collections: 'bars',
18 | Profile: 'profile',
19 | };
20 |
21 | return (
22 |
23 | {state.routes.map((route, index) => {
24 | const {options} = descriptors[route.key];
25 | const label = options.title !== undefined ? options.title : route.name;
26 |
27 | const isFocused = state.index === index;
28 |
29 | const onPress = () => {
30 | const event = navigation.emit({
31 | type: 'tabPress',
32 | target: route.key,
33 | canPreventDefault: true,
34 | });
35 |
36 | if (!isFocused && !event.defaultPrevented) {
37 | navigation.navigate(route.name, route.params);
38 | }
39 | };
40 |
41 | const iconName = iconMap[route.name];
42 |
43 | return (
44 |
48 |
49 |
54 |
58 | {l10n.getString(label)}
59 |
60 |
61 |
62 | );
63 | })}
64 |
65 | );
66 | }
67 |
68 | export default TabBar;
69 |
--------------------------------------------------------------------------------
/app/components/common/text.tsx:
--------------------------------------------------------------------------------
1 | import React, {PropsWithChildren} from 'react';
2 | import {Text, TextProps} from 'react-native';
3 | import {tv, type VariantProps} from 'tailwind-variants';
4 |
5 | const text = tv({
6 | variants: {
7 | type: {
8 | display: 'text-display font-display',
9 | content: 'text-content font-content leading-4',
10 | },
11 | },
12 | defaultVariants: {
13 | type: 'content',
14 | },
15 | });
16 |
17 | type CommonTextProps = VariantProps & {
18 | className?: string;
19 | };
20 |
21 | const CommonText = ({
22 | type,
23 | className,
24 | children,
25 | ...rest
26 | }: PropsWithChildren) => {
27 | return (
28 |
34 | {children}
35 |
36 | );
37 | };
38 |
39 | export default CommonText;
40 |
--------------------------------------------------------------------------------
/app/components/home/featured-collection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | Image,
5 | TouchableOpacity,
6 | Dimensions,
7 | ScrollView,
8 | StyleSheet,
9 | } from 'react-native';
10 | import {useNavigation} from '@react-navigation/native';
11 | import {useQuery} from '@tanstack/react-query';
12 | import {useLocalization} from '@fluent/react';
13 | import Text from '@components/common/text';
14 | import {formatImageUrl} from '@utils/image-url';
15 | import {HttpTypes} from '@medusajs/types';
16 | import apiClient from '@api/client';
17 | import {useRegion} from '@data/region-context';
18 | import PreviewPrice from '@components/product/preview-price';
19 | import {getProductPrice} from '@utils/product-price';
20 | import {TabActions} from '@react-navigation/native';
21 |
22 | const {width} = Dimensions.get('window');
23 | const ITEM_WIDTH = width * 0.35;
24 | const ITEM_GAP = 16;
25 |
26 | type FeaturedCollectionProps = {
27 | limit?: number;
28 | name?: string;
29 | showCta?: boolean;
30 | queryKey?: string[];
31 | };
32 |
33 | const FeaturedCollection = ({
34 | limit = 10,
35 | name,
36 | showCta = true,
37 | queryKey,
38 | }: FeaturedCollectionProps) => {
39 | const {l10n} = useLocalization();
40 | const navigation = useNavigation();
41 | const {region} = useRegion();
42 |
43 | const {data} = useQuery({
44 | queryKey: queryKey ?? ['products', name, region?.id],
45 | queryFn: async () => {
46 | const response = await apiClient.store.product.list({
47 | limit: limit,
48 | region_id: region?.id,
49 | fields: '*variants.calculated_price',
50 | });
51 | return response;
52 | },
53 | });
54 |
55 | if (!data?.products || data.products.length === 0) {
56 | return null;
57 | }
58 |
59 | return (
60 |
61 |
62 |
63 | {name ?? l10n.getString('top-selling')}
64 |
65 | {showCta && (
66 |
68 | navigation.dispatch(TabActions.jumpTo('Collections'))
69 | }>
70 | {l10n.getString('see-all')}
71 |
72 | )}
73 |
74 |
75 |
82 | {data.products.map(product => (
83 |
84 | ))}
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | const styles = StyleSheet.create({
92 | contentContainer: {
93 | gap: ITEM_GAP,
94 | },
95 | });
96 |
97 | const ProductCard = ({product}: {product: HttpTypes.StoreProduct}) => {
98 | const navigation = useNavigation();
99 | const {cheapestPrice} = getProductPrice({product});
100 |
101 | return (
102 |
104 | navigation.navigate('ProductDetail', {productId: product.id})
105 | }
106 | style={{width: ITEM_WIDTH}}
107 | className="gap-2">
108 |
113 |
114 |
115 | {product.title}
116 |
117 | {cheapestPrice && }
118 |
119 |
120 | );
121 | };
122 |
123 | export default FeaturedCollection;
124 |
--------------------------------------------------------------------------------
/app/components/home/header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {TouchableOpacity, View} from 'react-native';
3 | import Text from '@components/common/text';
4 | import {useCartQuantity, useLoggedIn} from '@data/hooks';
5 | import Icon from '@react-native-vector-icons/ant-design';
6 | import MaterialIcon from '@react-native-vector-icons/material-design-icons';
7 | import {TabActions, useNavigation} from '@react-navigation/native';
8 | import {useColors} from '@styles/hooks';
9 | import Badge from '@components/common/badge';
10 | import {useQuery} from '@tanstack/react-query';
11 | import apiClient from '@api/client';
12 | import RoundedButton from '@components/common/rounded-button';
13 |
14 | const Header = () => {
15 | const {data} = useQuery({
16 | queryKey: ['regions'],
17 | queryFn: () => apiClient.store.region.list(),
18 | });
19 |
20 | const showRegionSelector = data?.regions && data.regions.length > 1;
21 |
22 | return (
23 |
24 | {showRegionSelector ? : }
25 |
26 | MEDUSA MOBILE
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | const RegionSelectorButton = () => {
34 | const colors = useColors();
35 | const navigation = useNavigation();
36 |
37 | return (
38 | {
40 | navigation.navigate('RegionSelect');
41 | }}>
42 |
43 |
44 | );
45 | };
46 |
47 | const ProfileButton = () => {
48 | const colors = useColors();
49 | const navigation = useNavigation();
50 | const loggedIn = useLoggedIn();
51 | return (
52 | {
54 | if (loggedIn) {
55 | navigation.dispatch(TabActions.jumpTo('Profile'));
56 | } else {
57 | navigation.navigate('SignIn');
58 | }
59 | }}>
60 |
61 |
62 | );
63 | };
64 |
65 | const CartButton = () => {
66 | const colors = useColors();
67 | const navigation = useNavigation();
68 | const itemCount = useCartQuantity();
69 | const navigateToCart = () => {
70 | navigation.navigate('Cart');
71 | };
72 | return (
73 |
76 |
77 |
78 | {itemCount > 0 && (
79 |
80 |
81 |
82 | )}
83 |
84 |
85 | );
86 | };
87 |
88 | export default Header;
89 |
--------------------------------------------------------------------------------
/app/components/home/hero-carousel.tsx:
--------------------------------------------------------------------------------
1 | import {formatImageUrl} from '@utils/image-url';
2 | import {cssInterop} from 'nativewind';
3 | import React from 'react';
4 | import {Dimensions, Image, View} from 'react-native';
5 | import {useSharedValue} from 'react-native-reanimated';
6 | import Carousel, {
7 | ICarouselInstance,
8 | Pagination,
9 | } from 'react-native-reanimated-carousel';
10 | import {useQuery} from '@tanstack/react-query';
11 | import {useNavigation} from '@react-navigation/native';
12 | import {TouchableWithoutFeedback} from 'react-native-gesture-handler';
13 |
14 | const width = Dimensions.get('window').width;
15 |
16 | type HeroCarouselItem = {
17 | type: 'product' | 'category' | 'collection';
18 | entityId: string;
19 | imageUrl: string;
20 | };
21 |
22 | const PagiationTw = cssInterop(Pagination.Basic, {
23 | containerClassName: 'containerStyle',
24 | dotClassName: 'dotStyle',
25 | activeDotClassName: 'activeDotStyle',
26 | });
27 |
28 | const HeroCarousel = () => {
29 | const ref = React.useRef(null);
30 | const progress = useSharedValue(0);
31 | const navigation = useNavigation();
32 | const enableNavigation = false;
33 |
34 | const {data} = useQuery({
35 | queryKey: ['hero-carousel'],
36 | queryFn: async () => {
37 | const response = await fetch(
38 | 'https://dummyjson.com/c/9fd9-bd11-4526-8405',
39 | );
40 | return response.json();
41 | },
42 | });
43 |
44 | if (!data || data.length === 0) {
45 | return null;
46 | }
47 |
48 | const onPressPagination = (index: number) => {
49 | ref.current?.scrollTo({
50 | count: index - progress.value,
51 | animated: true,
52 | });
53 | };
54 |
55 | const onPressItem = (index: number) => {
56 | if (!enableNavigation) {
57 | return;
58 | }
59 | const item = data[index];
60 | if (item.type === 'product') {
61 | navigation.navigate('ProductDetail', {productId: item.entityId});
62 | }
63 | if (item.type === 'category') {
64 | navigation.navigate('CategoryDetail', {categoryId: item.entityId});
65 | }
66 | if (item.type === 'collection') {
67 | navigation.navigate('CollectionDetail', {collectionId: item.entityId});
68 | }
69 | };
70 |
71 | return (
72 |
73 | {
88 | const uri = formatImageUrl(data[index].imageUrl);
89 | return (
90 | onPressItem(index)}>
91 |
96 |
97 | );
98 | }}
99 | />
100 | {data.length > 1 && (
101 |
110 | )}
111 |
112 | );
113 | };
114 |
115 | export default HeroCarousel;
116 |
--------------------------------------------------------------------------------
/app/components/product/image-carousel.tsx:
--------------------------------------------------------------------------------
1 | import {HttpTypes} from '@medusajs/types';
2 | import {formatImageUrl} from '@utils/image-url';
3 | import {cssInterop} from 'nativewind';
4 | import React from 'react';
5 | import {Dimensions, Image, View} from 'react-native';
6 | import {useSharedValue} from 'react-native-reanimated';
7 | import Carousel, {
8 | ICarouselInstance,
9 | Pagination,
10 | } from 'react-native-reanimated-carousel';
11 |
12 | const width = Dimensions.get('window').width;
13 | const height = Dimensions.get('window').height;
14 |
15 | type CarouselProps = {
16 | data: HttpTypes.StoreProductImage[];
17 | };
18 |
19 | const PagiationTw = cssInterop(Pagination.Basic, {
20 | containerClassName: 'containerStyle',
21 | dotClassName: 'dotStyle',
22 | activeDotClassName: 'activeDotStyle',
23 | });
24 |
25 | const ImageCarousel = ({data}: CarouselProps) => {
26 | const ref = React.useRef(null);
27 | const progress = useSharedValue(0);
28 | const onPressPagination = (index: number) => {
29 | ref.current?.scrollTo({
30 | /**
31 | * Calculate the difference between the current index and the target index
32 | * to ensure that the carousel scrolls to the nearest index
33 | */
34 | count: index - progress.value,
35 | animated: true,
36 | });
37 | };
38 |
39 | return (
40 |
41 | {
49 | const uri = formatImageUrl(data[index].url);
50 | return (
51 |
56 | );
57 | }}
58 | />
59 | {data.length > 1 && (
60 |
61 |
70 |
71 | )}
72 |
73 | );
74 | };
75 |
76 | export default ImageCarousel;
77 |
--------------------------------------------------------------------------------
/app/components/product/option-select.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, TouchableOpacity, ScrollView} from 'react-native';
3 | import Text from '@components/common/text';
4 | import {HttpTypes} from '@medusajs/types';
5 | import {tv} from 'tailwind-variants';
6 |
7 | const optionButton = tv({
8 | base: 'border border-gray-200 h-10 rounded-lg py-2 px-6 flex-1 justify-center items-center',
9 | variants: {
10 | selected: {
11 | true: 'border-primary bg-primary',
12 | false: 'active:shadow-lg bg-background-secondary',
13 | },
14 | },
15 | });
16 |
17 | const optionButtonText = tv({
18 | base: 'text-base text-center',
19 | variants: {
20 | selected: {
21 | true: 'text-content-secondary',
22 | false: 'text-content',
23 | },
24 | },
25 | });
26 |
27 | type OptionSelectProps = {
28 | option: HttpTypes.StoreProductOption;
29 | current: string | undefined;
30 | updateOption: (title: string, value: string) => void;
31 | title: string;
32 | disabled: boolean;
33 | };
34 |
35 | const OptionSelect: React.FC = ({
36 | option,
37 | current,
38 | updateOption,
39 | title,
40 | disabled,
41 | }) => {
42 | const filteredOptions = (option.values ?? []).map(v => v.value);
43 |
44 | return (
45 |
46 | Select {title}
47 |
48 |
49 | {filteredOptions.map(v => {
50 | return (
51 | updateOption(option.id, v)}
53 | key={v}
54 | disabled={disabled}
55 | testID="option-button"
56 | className={optionButton({
57 | selected: v === current,
58 | })}>
59 |
63 | {v}
64 |
65 |
66 | );
67 | })}
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default OptionSelect;
75 |
--------------------------------------------------------------------------------
/app/components/product/preview-price.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Text from '@components/common/text';
3 | import {VariantPrice} from 'types/global';
4 |
5 | export default function PreviewPrice({price}: {price: VariantPrice}) {
6 | if (!price) {
7 | return null;
8 | }
9 |
10 | return (
11 | <>
12 | {price.price_type === 'sale' && (
13 | {price.original_price}
14 | )}
15 | {price.calculated_price}
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/components/product/product-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | FlatList,
5 | TouchableOpacity,
6 | Image,
7 | ActivityIndicator,
8 | RefreshControl,
9 | } from 'react-native';
10 | import {useInfiniteQuery} from '@tanstack/react-query';
11 | import {useNavigation} from '@react-navigation/native';
12 | import {HttpTypes} from '@medusajs/types';
13 | import {useLocalization} from '@fluent/react';
14 | import {useColors} from '@styles/hooks';
15 | import Text from '@components/common/text';
16 | import Loader from '@components/common/loader';
17 | import ErrorUI from '@components/common/error-ui';
18 | import {getProductPrice} from '@utils/product-price';
19 | import {formatImageUrl} from '@utils/image-url';
20 | import PreviewPrice from '@components/product/preview-price';
21 | import apiClient from '@api/client';
22 | import {useRegion} from '@data/region-context';
23 | import WishlistButton from './wishlist-button';
24 |
25 | const LIMIT = 10;
26 |
27 | type ProductListRes = {
28 | products: HttpTypes.StoreProduct[];
29 | count: number;
30 | offset: number;
31 | limit: number;
32 | };
33 |
34 | type ProductsListProps = {
35 | queryKey?: string[];
36 | additionalParams?: Partial;
37 | headerComponent?: React.ReactElement;
38 | name?: string;
39 | hideTitle?: boolean;
40 | };
41 |
42 | const ProductsList = ({
43 | queryKey,
44 | additionalParams = {},
45 | headerComponent,
46 | name,
47 | hideTitle = false,
48 | }: ProductsListProps) => {
49 | const {l10n} = useLocalization();
50 | const colors = useColors();
51 | const {region} = useRegion();
52 | const {
53 | data,
54 | fetchNextPage,
55 | hasNextPage,
56 | isFetchingNextPage,
57 | isLoading,
58 | error,
59 | refetch,
60 | isRefetching,
61 | } = useInfiniteQuery({
62 | queryKey: queryKey ?? ['products', region?.id],
63 | initialPageParam: 0,
64 | queryFn: async ({pageParam}) => {
65 | const params: HttpTypes.StoreProductListParams = {
66 | limit: LIMIT,
67 | offset: pageParam,
68 | fields: '*variants.calculated_price',
69 | region_id: region?.id,
70 | order: '-created_at',
71 | ...additionalParams,
72 | };
73 | return apiClient.store.product.list(params);
74 | },
75 | getNextPageParam: (lastPage: ProductListRes, pages) => {
76 | if (lastPage.products.length < LIMIT) {
77 | return undefined;
78 | }
79 | return pages.length * LIMIT;
80 | },
81 | });
82 |
83 | if (isLoading) {
84 | return ;
85 | }
86 |
87 | if (error) {
88 | return ;
89 | }
90 |
91 | const products = data?.pages.flatMap(page => page.products) ?? [];
92 |
93 | const loadMore = () => {
94 | if (hasNextPage && !isFetchingNextPage) {
95 | fetchNextPage();
96 | }
97 | };
98 |
99 | const renderFooter = () => {
100 | if (!isFetchingNextPage) {
101 | return null;
102 | }
103 | return (
104 |
105 |
106 |
107 | );
108 | };
109 |
110 | const renderHeader = () => {
111 | return (
112 |
113 | {headerComponent}
114 | {!hideTitle && (
115 |
116 | {name ?? l10n.getString('latest-products')}
117 |
118 | )}
119 |
120 | );
121 | };
122 |
123 | return (
124 | }
131 | keyExtractor={item => item.id ?? ''}
132 | onEndReached={loadMore}
133 | onEndReachedThreshold={0.3}
134 | ListFooterComponent={renderFooter}
135 | refreshing={isRefetching}
136 | refreshControl={
137 |
142 | }
143 | onRefresh={refetch}
144 | />
145 | );
146 | };
147 |
148 | const ProductItem = ({product}: {product: HttpTypes.StoreProduct}) => {
149 | const {cheapestPrice} = getProductPrice({
150 | product,
151 | });
152 | const navigation = useNavigation();
153 | const navigateToProduct = () => {
154 | navigation.navigate('ProductDetail', {productId: product.id});
155 | };
156 | return (
157 |
160 |
161 |
162 |
167 |
168 |
169 |
170 |
171 |
172 | {product.title}
173 |
174 | {cheapestPrice && }
175 |
176 |
177 | );
178 | };
179 |
180 | export default ProductsList;
181 |
--------------------------------------------------------------------------------
/app/components/product/product-price.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View} from 'react-native';
3 | import {getProductPrice} from '@utils/product-price';
4 | import {HttpTypes} from '@medusajs/types';
5 | import Text from '@components/common/text';
6 |
7 | export default function ProductPrice({
8 | product,
9 | variant,
10 | }: {
11 | product: HttpTypes.StoreProduct;
12 | variant?: HttpTypes.StoreProductVariant;
13 | }) {
14 | const {cheapestPrice, variantPrice} = getProductPrice({
15 | product,
16 | variantId: variant?.id,
17 | });
18 |
19 | const selectedPrice = variant ? variantPrice : cheapestPrice;
20 |
21 | if (!selectedPrice) {
22 | return ;
23 | }
24 |
25 | return (
26 |
27 |
28 | {selectedPrice.calculated_price}
29 |
30 |
31 | {selectedPrice.price_type === 'sale' && (
32 | <>
33 |
34 | Original:
35 |
41 | {selectedPrice.original_price}
42 |
43 |
44 |
45 | -{selectedPrice.percentage_diff}%
46 |
47 | >
48 | )}
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/app/components/product/wishlist-button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MaterialIcon from '@react-native-vector-icons/material-design-icons';
3 | import {TouchableOpacity} from 'react-native';
4 | import {useColors} from '@styles/hooks';
5 | import {StoreProduct} from '@medusajs/types';
6 |
7 | type WishlistButtonProps = {
8 | product: StoreProduct;
9 | containerClassName?: string;
10 | iconColor?: string;
11 | bgColor?: string;
12 | };
13 |
14 | const WishlistButton = ({
15 | containerClassName,
16 | bgColor,
17 | iconColor,
18 | }: WishlistButtonProps) => {
19 | const colors = useColors();
20 | const isWishlisted = false;
21 | return (
22 |
25 |
30 |
31 | );
32 | };
33 |
34 | export default WishlistButton;
35 |
--------------------------------------------------------------------------------
/app/constants/fluent-templates/index.ts:
--------------------------------------------------------------------------------
1 | export {default as en_US} from './en-US';
2 | export {default as id_ID} from './id-ID';
3 |
--------------------------------------------------------------------------------
/app/constants/fluent-templates/type.ts:
--------------------------------------------------------------------------------
1 | export interface FluentTemplate {
2 | home: string;
3 | categories: string;
4 | collections: string;
5 | profile: string;
6 |
7 | 'top-selling': string;
8 | 'see-all': string;
9 | 'latest-products': string;
10 | 'trending-products': string;
11 | 'sign-in': string;
12 | 'sign-in-to-view-your-profile': string;
13 | 'signing-in': string;
14 | email: string;
15 | 'enter-your-email': string;
16 | password: string;
17 | 'enter-your-password': string;
18 | 'dont-have-an-account': string;
19 | 'invalid-credentials': string;
20 | 'no-collections-found': string;
21 | cart: string;
22 | checkout: string;
23 | 'go-home': string;
24 | 'you-dont-have-anything-in-your-cart': string;
25 | 'profile-information': string;
26 | 'order-with-id': string;
27 | orders: string;
28 | 'shipping-addresses': string;
29 | logout: string;
30 | 'profile-details': string;
31 | name: string;
32 | phone: string;
33 | 'edit-profile': string;
34 | save: string;
35 | cancel: string;
36 | 'first-name': string;
37 | 'last-name': string;
38 | 'no-orders-found': string;
39 | 'sign-in-to-view-orders': string;
40 | view: string;
41 | 'remaining-items': string;
42 | 'no-categories-found': string;
43 | variant: string;
44 | 'add-a-promo-code': string;
45 | 'enter-promo-code': string;
46 | 'applied-promotions': string;
47 | 'invalid-promo-code': string;
48 | 'failed-to-apply-promotion': string;
49 | 'failed-to-remove-promotion': string;
50 | apply: string;
51 | summary: string;
52 | discount: string;
53 | subtotal: string;
54 | shipping: string;
55 | taxes: string;
56 | total: string;
57 | 'use-same-address-for-billing': string;
58 | 'shipping-address': string;
59 | 'billing-address': string;
60 | address: string;
61 | company: string;
62 | optional: string;
63 | 'postal-code': string;
64 | city: string;
65 | 'province-or-state': string;
66 | country: string;
67 | 'select-a-country': string;
68 | delivery: string;
69 | payment: string;
70 | review: string;
71 | 'continue-to-delivery': string;
72 | 'continue-to-payment': string;
73 | 'review-order': string;
74 | 'pay-using-provider': string;
75 | 'place-order': string;
76 | continue: string;
77 | 'please-select-a-payment-method': string;
78 | 'no-cart-found': string;
79 | 'payment-provider-not-supported': string;
80 | 'failed-to-complete-order': string;
81 | error: string;
82 | 'an-error-occurred': string;
83 | 'no-cart-id': string;
84 | 'failed-to-update-shipping-method': string;
85 | 'loading-shipping-options': string;
86 | 'select-shipping-method': string;
87 | calculating: string;
88 | 'no-region-id': string;
89 | 'stripe-payment-coming-soon': string;
90 | 'no-additional-actions-required-for-manual-payment': string;
91 | 'payment-provider-is-in-development': string;
92 | 'loading-payment-options': string;
93 | 'select-payment-method': string;
94 | qty: string;
95 | 'shipping-method': string;
96 | 'payment-method': string;
97 | 'no-shipping-method-selected': string;
98 | 'no-payment-method-selected': string;
99 | canceled: string;
100 | 'not-fulfilled': string;
101 | 'partially-fulfilled': string;
102 | fulfilled: string;
103 | 'partially-shipped': string;
104 | shipped: string;
105 | 'partially-delivered': string;
106 | delivered: string;
107 | 'count-items': string;
108 | 'order-details': string;
109 | 'order-not-found': string;
110 | 'placed-on': string;
111 | status: string;
112 | 'order-items': string;
113 | contact: string;
114 | method: string;
115 | 'order-summary': string;
116 | 'continue-shopping': string;
117 | addresses: string;
118 | 'please-sign-in-to-view-your-address': string;
119 | 'no-addresses-found': string;
120 | 'add-new-address': string;
121 | 'add-address': string;
122 | 'edit-address': string;
123 | 'save-changes': string;
124 | 'country-code': string;
125 | 'product-information': string;
126 | material: string;
127 | 'country-of-origin': string;
128 | type: string;
129 | weight: string;
130 | dimensions: string;
131 | 'days-return': string;
132 | 'fast-delivery': string;
133 | 'add-to-cart': string;
134 | 'out-of-stock': string;
135 | 'view-cart': string;
136 | settings: string;
137 | language: string;
138 | 'select-language': string;
139 | 'language-is-required': string;
140 | 'first-name-is-required': string;
141 | 'last-name-is-required': string;
142 | 'address-is-required': string;
143 | 'postal-code-is-required': string;
144 | 'city-is-required': string;
145 | 'country-is-required': string;
146 | 'phone-is-required': string;
147 | 'please-enter-a-valid-email': string;
148 | 'invalid-email-address': string;
149 | 'password-must-be-at-least-n-characters': string;
150 | register: string;
151 | 'enter-your-first-name': string;
152 | 'enter-your-last-name': string;
153 | 'create-account': string;
154 | 'creating-account': string;
155 | 'already-have-an-account': string;
156 | 'registration-failed': string;
157 | }
158 |
--------------------------------------------------------------------------------
/app/data/customer-context.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useEffect, useState} from 'react';
2 | import {HttpTypes} from '@medusajs/types';
3 | import apiClient from '@api/client';
4 | import {useCart} from './cart-context';
5 |
6 | type CustomerContextType = {
7 | customer?: HttpTypes.StoreCustomer;
8 | login: (email: string, password: string) => Promise;
9 | logout: () => Promise;
10 | register: (
11 | email: string,
12 | password: string,
13 | firstName: string,
14 | lastName: string,
15 | ) => Promise;
16 | refreshCustomer: () => Promise;
17 | updateCustomer: (customer: HttpTypes.StoreUpdateCustomer) => Promise;
18 | };
19 |
20 | const CustomerContext = createContext(null);
21 |
22 | type CustomerProviderProps = {
23 | children: React.ReactNode;
24 | };
25 |
26 | export const CustomerProvider = ({children}: CustomerProviderProps) => {
27 | const [customer, setCustomer] = useState();
28 | const {linkCartToCustomer, resetCart} = useCart();
29 |
30 | useEffect(() => {
31 | // Check for existing session on mount
32 | refreshCustomer();
33 | }, []);
34 |
35 | const refreshCustomer = async () => {
36 | try {
37 | const {customer: existingCustomer} =
38 | await apiClient.store.customer.retrieve();
39 | setCustomer(existingCustomer);
40 | } catch (error) {
41 | // Customer isn't logged in
42 | setCustomer(undefined);
43 | }
44 | };
45 |
46 | const login = async (email: string, password: string) => {
47 | try {
48 | // Authenticate using the auth endpoint
49 | const response = await apiClient.auth.login('customer', 'emailpass', {
50 | email,
51 | password,
52 | });
53 |
54 | // Check if the response is a string
55 | if (typeof response === 'string') {
56 | await Promise.all([refreshCustomer(), linkCartToCustomer()]);
57 | } else {
58 | // Handle third party auth
59 | console.log('Third party auth', response);
60 | }
61 | } catch (error) {
62 | throw error;
63 | }
64 | };
65 |
66 | const logout = async () => {
67 | try {
68 | await apiClient.auth.logout();
69 | await resetCart();
70 | setCustomer(undefined);
71 | } catch (error) {
72 | throw error;
73 | }
74 | };
75 |
76 | const register = async (
77 | email: string,
78 | password: string,
79 | firstName: string,
80 | lastName: string,
81 | ) => {
82 | try {
83 | const token = await apiClient.auth.register('customer', 'emailpass', {
84 | email,
85 | password,
86 | });
87 |
88 | await apiClient.client.setToken(token);
89 |
90 | await apiClient.store.customer.create({
91 | first_name: firstName,
92 | last_name: lastName,
93 | email,
94 | });
95 | // There's some bug with the token received from the register endpoint
96 | // so we need to login again
97 | await login(email, password);
98 | } catch (error) {
99 | throw error;
100 | }
101 | };
102 |
103 | const updateCustomer = async (data: HttpTypes.StoreUpdateCustomer) => {
104 | try {
105 | const response = await apiClient.store.customer.update(data);
106 | setCustomer(response.customer);
107 | } catch (error) {
108 | throw error;
109 | }
110 | };
111 |
112 | return (
113 |
122 | {children}
123 |
124 | );
125 | };
126 |
127 | export const useCustomer = () => {
128 | const context = useContext(CustomerContext);
129 |
130 | if (!context) {
131 | throw new Error('useCustomer must be used within a CustomerProvider');
132 | }
133 |
134 | return context;
135 | };
136 |
--------------------------------------------------------------------------------
/app/data/hooks.ts:
--------------------------------------------------------------------------------
1 | import {useCart} from './cart-context';
2 | import {useRegion} from './region-context';
3 | import {CheckoutStep} from '../types/checkout';
4 | import {useCustomer} from './customer-context';
5 |
6 |
7 | export const useProductQuantity = (productId: string) => {
8 | const {cart} = useCart();
9 |
10 | const quantity = cart?.items?.reduce((acc, item) => {
11 | if (item.product_id === productId) {
12 | return acc + item.quantity;
13 | }
14 | return acc;
15 | }, 0);
16 |
17 | return quantity || 0;
18 | };
19 |
20 | export const useVariantQuantity = (variantId: string) => {
21 | const {cart} = useCart();
22 |
23 | const quantity = cart?.items?.reduce((acc, item) => {
24 | if (item.variant_id === variantId) {
25 | return acc + item.quantity;
26 | }
27 | return acc;
28 | }, 0);
29 |
30 | return quantity || 0;
31 | };
32 |
33 | export const useCartQuantity = () => {
34 | const {cart} = useCart();
35 |
36 | const quantity = cart?.items?.reduce((acc, item) => {
37 | return acc + item.quantity;
38 | }, 0);
39 |
40 | return quantity || 0;
41 | };
42 |
43 | type Country = {
44 | label: string;
45 | value: string;
46 | };
47 |
48 | export const useCountries = (): Country[] => {
49 | const {region} = useRegion();
50 |
51 | return (
52 | region?.countries
53 | ?.map(country => ({
54 | label: country.display_name || country.iso_2,
55 | value: country.iso_2,
56 | }))
57 | .filter((country): country is Country =>
58 | Boolean(country.label && country.value),
59 | ) || []
60 | );
61 | };
62 |
63 | export const useCurrentCheckoutStep = (): CheckoutStep => {
64 | const {cart} = useCart();
65 |
66 | if (!cart?.shipping_address?.address_1 || !cart?.email) {
67 | return 'address';
68 | }
69 | if (!cart?.shipping_methods?.[0]?.shipping_option_id) {
70 | return 'delivery';
71 | }
72 | if (cart?.total === 0) {
73 | return 'payment';
74 | }
75 | return 'review';
76 | };
77 |
78 | export const useActivePaymentSession = () => {
79 | const {cart} = useCart();
80 |
81 | return cart?.payment_collection?.payment_sessions?.find(
82 | session => session.status === 'pending',
83 | );
84 | };
85 |
86 | export const useLoggedIn = () => {
87 | const {customer} = useCustomer();
88 | return customer !== undefined;
89 | };
90 |
--------------------------------------------------------------------------------
/app/data/locale-context.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AsyncStorage from '@react-native-async-storage/async-storage';
3 | import {DEFAULT_LOCALE as ENV_DEFAULT_LOCALE} from '@env';
4 |
5 | import {FluentBundle, FluentResource} from '@fluent/bundle';
6 | import {negotiateLanguages} from '@fluent/langneg';
7 | import {LocalizationProvider, ReactLocalization} from '@fluent/react';
8 |
9 | import dayjs from 'dayjs';
10 |
11 | import {en_US, id_ID} from '@constants/fluent-templates';
12 |
13 | export type Locale = 'en-US' | 'id-ID';
14 |
15 | type LocaleContextType = {
16 | locale: Locale;
17 | setLocale: (locale: Locale) => void;
18 | };
19 |
20 | type LocaleProviderProps = {
21 | children: React.ReactNode;
22 | };
23 |
24 | export const DEFAULT_LOCALE: Locale = ENV_DEFAULT_LOCALE || 'en-US';
25 | const LOCALE_STORAGE_KEY = 'locale';
26 |
27 | function* lazilyParsedBundles(fetchedMessages: Array<[Locale, string]>) {
28 | for (const [locale, messages] of fetchedMessages) {
29 | const resource = new FluentResource(messages);
30 | const bundle = new FluentBundle(locale);
31 | bundle.addResource(resource);
32 | yield bundle;
33 | }
34 | }
35 |
36 | const LocaleContext = React.createContext(null);
37 |
38 | export const LocaleProvider = ({children}: LocaleProviderProps) => {
39 | const [locale, setLocale] = React.useState();
40 | const [l10n, setL10n] = React.useState(null);
41 |
42 | const changeLocales = React.useCallback(
43 | async (locales: Array) => {
44 | const currentLocales = negotiateLanguages(locales, ['en-US', 'id-ID'], {
45 | defaultLocale: DEFAULT_LOCALE,
46 | }) as Locale[];
47 |
48 | const localeAssets = currentLocales.map<[Locale, string]>(_locale => {
49 | if (_locale === 'en-US') {
50 | return [_locale, en_US];
51 | } else {
52 | return [_locale, id_ID];
53 | }
54 | });
55 |
56 | const bundles = lazilyParsedBundles(localeAssets);
57 | setL10n(new ReactLocalization(bundles, null));
58 | },
59 | [setL10n],
60 | );
61 |
62 | React.useEffect(() => {
63 | if (locale) {
64 | AsyncStorage.setItem(LOCALE_STORAGE_KEY, locale);
65 | changeLocales([locale]);
66 |
67 | if (locale === 'en-US') {
68 | dayjs.locale('en');
69 | } else if (locale === 'id-ID') {
70 | dayjs.locale('id');
71 | }
72 |
73 | return;
74 | }
75 |
76 | AsyncStorage.getItem(LOCALE_STORAGE_KEY).then(l => {
77 | const _locale = l as Locale | null;
78 | setLocale(_locale ?? DEFAULT_LOCALE);
79 | });
80 | }, [changeLocales, locale, setLocale]);
81 |
82 | if (!l10n) {
83 | return ;
84 | }
85 |
86 | return (
87 |
92 | {children}
93 |
94 | );
95 | };
96 |
97 | export const useLocale = () => {
98 | const context = React.useContext(LocaleContext);
99 |
100 | if (!context) {
101 | throw new Error('useLocale must be used within a LocaleProvider');
102 | }
103 |
104 | return context;
105 | };
106 |
--------------------------------------------------------------------------------
/app/data/region-context.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useEffect, useState} from 'react';
2 | import {HttpTypes} from '@medusajs/types';
3 | import AsyncStorage from '@react-native-async-storage/async-storage';
4 | import apiClient from '@api/client';
5 |
6 | const REGION_KEY = 'region_id';
7 |
8 | type RegionContextType = {
9 | region?: HttpTypes.StoreRegion;
10 | setRegion: React.Dispatch<
11 | React.SetStateAction
12 | >;
13 | };
14 |
15 | const RegionContext = createContext(null);
16 |
17 | type RegionProviderProps = {
18 | children: React.ReactNode;
19 | };
20 |
21 | export const RegionProvider = ({children}: RegionProviderProps) => {
22 | const [region, setRegion] = useState();
23 |
24 | useEffect(() => {
25 | if (region?.id) {
26 | // set its ID in the local storage in
27 | // case it changed
28 | AsyncStorage.setItem(REGION_KEY, region.id);
29 | return;
30 | }
31 |
32 | AsyncStorage.getItem(REGION_KEY).then(regionId => {
33 | if (!regionId) {
34 | // retrieve regions and select the first one
35 | apiClient.store.region.list().then(data => {
36 | const regions = data.regions;
37 | setRegion(regions[0]);
38 | });
39 | } else {
40 | // retrieve selected region
41 | apiClient.store.region
42 | .retrieve(regionId)
43 | .then(({region: dataRegion}) => {
44 | setRegion(dataRegion);
45 | });
46 | }
47 | });
48 | }, [region?.id]);
49 |
50 | return (
51 |
56 | {children}
57 |
58 | );
59 | };
60 |
61 | export const useRegion = () => {
62 | const context = useContext(RegionContext);
63 |
64 | if (!context) {
65 | throw new Error('useRegion must be used within a RegionProvider');
66 | }
67 |
68 | return context;
69 | };
70 |
71 | export const useCountries = () => {
72 | const {region} = useRegion();
73 | return region?.countries;
74 | };
75 |
--------------------------------------------------------------------------------
/app/screens/address/address-form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, ScrollView, KeyboardTypeOptions} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Navbar from '@components/common/navbar';
5 | import Button from '@components/common/button';
6 | import Input from '@components/common/input';
7 | import {useNavigation} from '@react-navigation/native';
8 | import {useMutation, useQueryClient} from '@tanstack/react-query';
9 | import apiClient from '@api/client';
10 | import {useForm, Controller} from 'react-hook-form';
11 | import {zodResolver} from '@hookform/resolvers/zod';
12 | import {z} from 'zod';
13 | import {HttpTypes} from '@medusajs/types';
14 | import {addressSchema, createEmptyAddress} from '../../types/checkout';
15 |
16 | type AddressFormData = z.infer;
17 |
18 | type Props = {
19 | route: {
20 | params?: {
21 | address?: HttpTypes.StoreCustomerAddress;
22 | };
23 | };
24 | };
25 |
26 | type FieldConfig = {
27 | name: keyof AddressFormData;
28 | label: string;
29 | required: boolean;
30 | keyboardType?: KeyboardTypeOptions;
31 | };
32 |
33 | const FIELDS: FieldConfig[] = [
34 | {name: 'first_name', label: 'first-name', required: true},
35 | {name: 'last_name', label: 'last-name', required: true},
36 | {name: 'address_1', label: 'address', required: true},
37 | {name: 'company', label: 'company', required: false},
38 | {name: 'city', label: 'city', required: true},
39 | {name: 'province', label: 'province-or-state', required: true},
40 | {name: 'postal_code', label: 'postal-code', required: true},
41 | {name: 'country_code', label: 'country-code', required: true},
42 | {name: 'phone', label: 'phone', required: false, keyboardType: 'phone-pad'},
43 | ];
44 |
45 | const AddressForm = ({route}: Props) => {
46 | const {l10n} = useLocalization();
47 | const address = route.params?.address;
48 | const isEditing = !!address;
49 | const navigation = useNavigation();
50 | const queryClient = useQueryClient();
51 |
52 | const defaultValues = isEditing
53 | ? {...createEmptyAddress(), ...address}
54 | : createEmptyAddress();
55 |
56 | const {
57 | control,
58 | handleSubmit,
59 | formState: {errors},
60 | } = useForm({
61 | resolver: zodResolver(addressSchema),
62 | defaultValues: {
63 | first_name: defaultValues.first_name || '',
64 | last_name: defaultValues.last_name || '',
65 | address_1: defaultValues.address_1 || '',
66 | postal_code: defaultValues.postal_code || '',
67 | city: defaultValues.city || '',
68 | country_code: defaultValues.country_code || '',
69 | phone: defaultValues.phone || '',
70 | company: defaultValues.company || undefined,
71 | province: defaultValues.province || undefined,
72 | },
73 | });
74 |
75 | const addAddressMutation = useMutation({
76 | mutationFn: async (data: AddressFormData) => {
77 | if (isEditing && address?.id) {
78 | await apiClient.store.customer.updateAddress(address.id, data);
79 | } else {
80 | await apiClient.store.customer.createAddress(data);
81 | }
82 | },
83 | onSuccess: () => {
84 | queryClient.invalidateQueries({queryKey: ['address-list']});
85 | navigation.goBack();
86 | },
87 | });
88 |
89 | const onSubmit = handleSubmit(data => {
90 | addAddressMutation.mutate(data);
91 | });
92 |
93 | return (
94 |
95 |
102 |
103 |
104 |
105 | {FIELDS.map(field => (
106 | (
111 |
124 | )}
125 | />
126 | ))}
127 |
128 |
129 |
130 |
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | export default AddressForm;
146 |
--------------------------------------------------------------------------------
/app/screens/address/address-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, ScrollView, TouchableOpacity, RefreshControl} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from '@components/common/text';
5 | import Navbar from '@components/common/navbar';
6 | import {useCustomer} from '@data/customer-context';
7 | import Button from '@components/common/button';
8 | import {useNavigation} from '@react-navigation/native';
9 | import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
10 | import apiClient from '@api/client';
11 | import {useColors} from '@styles/hooks';
12 | import Icon from '@react-native-vector-icons/ant-design';
13 | import {HttpTypes} from '@medusajs/types';
14 | import twColors from 'tailwindcss/colors';
15 | import {useCountries} from '@data/region-context';
16 | import utils from '@utils/common';
17 |
18 | const AddressList = () => {
19 | const {l10n} = useLocalization();
20 | const {customer} = useCustomer();
21 | const colors = useColors();
22 | const queryClient = useQueryClient();
23 | const navigation = useNavigation();
24 |
25 | const {data, refetch, isRefetching} = useQuery({
26 | queryKey: ['address-list'],
27 | queryFn: async () => {
28 | const {addresses} = await apiClient.store.customer.listAddress();
29 | return addresses;
30 | },
31 | enabled: !!customer,
32 | });
33 |
34 | const countries = useCountries();
35 |
36 | const deleteAddressMutation = useMutation({
37 | mutationFn: async (addressId: string) => {
38 | await apiClient.store.customer.deleteAddress(addressId);
39 | },
40 | onSuccess: () => {
41 | queryClient.invalidateQueries({queryKey: ['address-list']});
42 | },
43 | });
44 |
45 | const handleDelete = (addressId: string) => {
46 | deleteAddressMutation.mutate(addressId);
47 | };
48 |
49 | const renderAddressCard = (address: HttpTypes.StoreCustomerAddress) => (
50 |
53 |
54 |
55 |
56 | {address.first_name} {address.last_name}
57 |
58 | {address.address_1}
59 | {address.address_2 && (
60 | {address.address_2}
61 | )}
62 |
63 | {address.city}, {address.province} {address.postal_code}
64 |
65 |
66 | {utils.getCountryName(address.country_code || '', countries)}
67 |
68 | {address.phone && (
69 | {address.phone}
70 | )}
71 |
72 |
73 |
75 | navigation.navigate('AddressForm', {
76 | address,
77 | })
78 | }
79 | className="p-2">
80 |
81 |
82 | handleDelete(address.id)}
84 | className="p-2">
85 |
86 |
87 |
88 |
89 |
90 | );
91 |
92 | if (!customer) {
93 | return (
94 |
95 |
96 |
97 | {l10n.getString('please-sign-in-to-view-your-address')}
98 |
99 |
100 | );
101 | }
102 |
103 | return (
104 |
105 |
106 |
107 |
115 | }>
116 |
117 | {data?.map(renderAddressCard)}
118 | {data?.length === 0 && (
119 |
120 | {l10n.getString('no-addresses-found')}
121 |
122 | )}
123 |
124 |
125 |
126 |
132 |
133 |
134 | );
135 | };
136 |
137 | export default AddressList;
138 |
--------------------------------------------------------------------------------
/app/screens/auth/login.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {View, TouchableOpacity, Keyboard} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from '@components/common/text';
5 | import Input from '@components/common/input';
6 | import {useCustomer} from '@data/customer-context';
7 | import {CommonActions, useNavigation} from '@react-navigation/native';
8 | import Navbar from '@components/common/navbar';
9 | import Button from '@components/common/button';
10 | import {useForm, Controller} from 'react-hook-form';
11 | import {zodResolver} from '@hookform/resolvers/zod';
12 | import * as z from 'zod';
13 |
14 | const signInSchema = z.object({
15 | email: z.string().email('invalid-email-address'),
16 | password: z.string().min(3, 'password-must-be-at-least-n-characters'),
17 | });
18 |
19 | type SignInSchema = z.infer;
20 |
21 | const SignIn = () => {
22 | const {l10n} = useLocalization();
23 | const [loading, setLoading] = useState(false);
24 | const {login} = useCustomer();
25 | const navigation = useNavigation();
26 |
27 | const {
28 | control,
29 | handleSubmit,
30 | setError: setFormError,
31 | formState: {errors},
32 | } = useForm({
33 | resolver: zodResolver(signInSchema),
34 | defaultValues: {
35 | email: '',
36 | password: '',
37 | },
38 | });
39 |
40 | const onSubmit = async (data: SignInSchema) => {
41 | try {
42 | Keyboard.dismiss();
43 | setLoading(true);
44 | await login(data.email, data.password);
45 | navigation.dispatch(
46 | CommonActions.reset({
47 | index: 0,
48 | routes: [
49 | {
50 | name: 'Main',
51 | state: {
52 | routes: [{name: 'Profile'}],
53 | },
54 | },
55 | ],
56 | }),
57 | );
58 | } catch (err) {
59 | setFormError('root', {
60 | type: 'manual',
61 | message:
62 | err instanceof Error
63 | ? err.message
64 | : l10n.getString('invalid-credentials'),
65 | });
66 | } finally {
67 | setLoading(false);
68 | }
69 | };
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 | {errors.root && (
78 |
79 | {errors.root.message}
80 |
81 | )}
82 |
83 | (
87 |
101 | )}
102 | />
103 |
104 | (
108 |
123 | )}
124 | />
125 |
126 |
133 |
134 | navigation.navigate('Register')}>
135 |
136 | {l10n.getString('dont-have-an-account')}
137 |
138 |
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | export default SignIn;
146 |
--------------------------------------------------------------------------------
/app/screens/cart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Navbar from '@components/common/navbar';
3 | import Text from '@components/common/text';
4 | import {useCart} from '@data/cart-context';
5 | import {ScrollView, View} from 'react-native';
6 | import Button from '@components/common/button';
7 | import {useNavigation} from '@react-navigation/native';
8 | import {useLocalization} from '@fluent/react';
9 | import CartContent from '@components/cart/cart-content';
10 |
11 | const Cart = () => {
12 | const {l10n} = useLocalization();
13 | const {cart} = useCart();
14 | const isEmptyCart = !cart?.items || cart.items.length === 0;
15 |
16 | const navigation = useNavigation();
17 |
18 | const goToCheckout = () => {
19 | navigation.navigate('Checkout');
20 | };
21 |
22 | const goHome = () => {
23 | navigation.navigate('Main');
24 | };
25 | return (
26 |
27 |
28 |
29 |
30 | {!isEmptyCart ? (
31 |
32 | ) : (
33 |
34 | )}
35 |
36 |
37 |
38 | {!isEmptyCart ? (
39 |
40 | ) : (
41 |
42 | )}
43 |
44 |
45 | );
46 | };
47 |
48 | const EmptyCart = () => {
49 | const {l10n} = useLocalization();
50 | return (
51 |
52 |
53 | {l10n.getString('you-dont-have-anything-in-your-cart')}
54 |
55 |
56 | );
57 | };
58 |
59 | export default Cart;
60 |
--------------------------------------------------------------------------------
/app/screens/category/categories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, FlatList, TouchableOpacity} from 'react-native';
3 | import {useQuery} from '@tanstack/react-query';
4 | import {useNavigation} from '@react-navigation/native';
5 | import {useLocalization} from '@fluent/react';
6 | import Navbar from '@components/common/navbar';
7 | import Text from '@components/common/text';
8 | import {useColors} from '@styles/hooks';
9 | import apiClient from '@api/client';
10 | import Icon from '@react-native-vector-icons/ant-design';
11 | import {StoreProductCategory} from '@medusajs/types';
12 | import Loader from '@components/common/loader';
13 | import ErrorUI from '@components/common/error-ui';
14 |
15 | export default function Categories() {
16 | const {l10n} = useLocalization();
17 | const colors = useColors();
18 | const navigation = useNavigation();
19 | const {data, isLoading, error} = useQuery({
20 | queryKey: ['categories'],
21 | queryFn: () => apiClient.store.category.list(),
22 | });
23 |
24 | if (isLoading) {
25 | ;
26 | }
27 |
28 | if (error) {
29 | return ;
30 | }
31 |
32 | const renderItem = ({item}: {item: StoreProductCategory}) => (
33 | {
35 | navigation.navigate('CategoryDetail', {categoryId: item.id});
36 | }}
37 | className="flex-row items-center justify-between p-4 border-b border-gray-200">
38 |
39 | {item.name}
40 | {item.description && (
41 |
42 | {item.description}
43 |
44 | )}
45 |
46 |
47 |
48 | );
49 |
50 | return (
51 |
52 |
53 |
54 | item.id}
58 | contentContainerClassName="pb-4"
59 | ListEmptyComponent={
60 |
61 |
62 | {l10n.getString('no-categories-found')}
63 |
64 |
65 | }
66 | />
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/app/screens/category/category-detail.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View} from 'react-native';
3 | import Navbar from '@components/common/navbar';
4 | import ProductsList from '@components/product/product-list';
5 | import Loader from '@components/common/loader';
6 | import ErrorUI from '@components/common/error-ui';
7 | import apiClient from '@api/client';
8 | import {useQuery} from '@tanstack/react-query';
9 |
10 | type CategoryDetailRouteParams = {
11 | route: {
12 | params: {
13 | categoryId: string;
14 | };
15 | };
16 | };
17 |
18 | export default function CategoryDetail({route}: CategoryDetailRouteParams) {
19 | const {categoryId} = route.params;
20 |
21 | const {data, isPending, error} = useQuery({
22 | queryKey: ['category', categoryId],
23 | queryFn: () => apiClient.store.category.retrieve(categoryId),
24 | });
25 |
26 | if (isPending) {
27 | return ;
28 | }
29 |
30 | if (error || !data?.product_category) {
31 | return ;
32 | }
33 |
34 | const category = data.product_category;
35 |
36 | return (
37 |
38 |
39 |
40 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/screens/collection/collection-detail.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View} from 'react-native';
3 | import {useQuery} from '@tanstack/react-query';
4 | import Navbar from '@components/common/navbar';
5 | import ProductsList from '@components/product/product-list';
6 | import Loader from '@components/common/loader';
7 | import ErrorUI from '@components/common/error-ui';
8 | import apiClient from '@api/client';
9 |
10 | type CollectionDetailRouteParams = {
11 | route: {
12 | params: {
13 | collectionId: string;
14 | };
15 | };
16 | };
17 |
18 | export default function CollectionDetail({route}: CollectionDetailRouteParams) {
19 | const {collectionId} = route.params;
20 |
21 | const {data, isPending, error} = useQuery({
22 | queryKey: ['collection', collectionId],
23 | queryFn: () => apiClient.store.collection.retrieve(collectionId),
24 | });
25 |
26 | if (isPending) {
27 | return ;
28 | }
29 |
30 | if (error || !data?.collection) {
31 | return ;
32 | }
33 |
34 | const collection = data.collection;
35 |
36 | return (
37 |
38 |
39 |
40 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/app/screens/collection/collections.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, FlatList, TouchableOpacity} from 'react-native';
3 | import {useQuery} from '@tanstack/react-query';
4 | import {useNavigation} from '@react-navigation/native';
5 | import {useLocalization} from '@fluent/react';
6 | import Navbar from '@components/common/navbar';
7 | import Text from '@components/common/text';
8 | import {useColors} from '@styles/hooks';
9 | import apiClient from '@api/client';
10 | import Icon from '@react-native-vector-icons/ant-design';
11 | import {HttpTypes} from '@medusajs/types';
12 | import Loader from '@components/common/loader';
13 | import ErrorUI from '@components/common/error-ui';
14 | import FeaturedCollection from '@components/home/featured-collection';
15 |
16 | export default function Collections() {
17 | const {l10n} = useLocalization();
18 | const colors = useColors();
19 | const navigation = useNavigation();
20 | const {data, isLoading, error} = useQuery({
21 | queryKey: ['collections'],
22 | queryFn: () => apiClient.store.collection.list(),
23 | });
24 |
25 | if (isLoading) {
26 | return ;
27 | }
28 |
29 | if (error) {
30 | return ;
31 | }
32 |
33 | const renderItem = ({item}: {item: HttpTypes.StoreCollection}) => (
34 | {
36 | navigation.navigate('CollectionDetail', {collectionId: item.id});
37 | }}
38 | className="flex-row items-center justify-between p-4 border-b border-gray-200">
39 |
40 | {item.title}
41 |
42 |
43 |
44 | );
45 |
46 | return (
47 |
48 |
49 |
50 |
51 | item.id}
55 | contentContainerClassName="pb-4"
56 | ListEmptyComponent={
57 |
58 |
59 | {l10n.getString('no-collections-found')}
60 |
61 |
62 | }
63 | />
64 |
65 |
66 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/app/screens/home.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, StatusBar, TouchableOpacity} from 'react-native';
3 | import Icon from '@react-native-vector-icons/ant-design';
4 | import {useColors, useTheme} from '@styles/hooks';
5 | import Header from '@components/home/header';
6 | import ProductsList from '@components/product/product-list';
7 | import HeroCarousel from '@components/home/hero-carousel';
8 | import FeaturedCollection from '@components/home/featured-collection';
9 | import {themeNames} from '@styles/themes';
10 |
11 | const Home = () => {
12 | const {name, setThemeName} = useTheme();
13 | const switchTheme = () => {
14 | const nextTheme =
15 | themeNames[(themeNames.indexOf(name) + 1) % themeNames.length];
16 | setThemeName(nextTheme);
17 | };
18 | const colors = useColors();
19 | const {isDarkMode} = useTheme();
20 | return (
21 |
22 |
26 |
27 |
28 |
31 |
32 |
33 | >
34 | }
35 | />
36 |
37 |
38 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default Home;
49 |
--------------------------------------------------------------------------------
/app/screens/profile/profile.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, ScrollView, TouchableOpacity} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from '@components/common/text';
5 | import MaterialIcon from '@react-native-vector-icons/material-design-icons';
6 | import {useColors} from '@styles/hooks';
7 | import {useNavigation} from '@react-navigation/native';
8 | import Navbar from '@components/common/navbar';
9 | import {useCustomer} from '@data/customer-context';
10 | import twColors from 'tailwindcss/colors';
11 |
12 | type ProfileOptionType = {
13 | icon:
14 | | 'account-details'
15 | | 'text-box-multiple-outline'
16 | | 'map-marker-outline'
17 | | 'cog-outline'
18 | | 'logout';
19 | label: string;
20 | onPress?: () => void;
21 | };
22 |
23 | const Profile = () => {
24 | const {l10n} = useLocalization();
25 | const navigation = useNavigation();
26 | const {customer, logout} = useCustomer();
27 |
28 | if (!customer) {
29 | return (
30 |
31 |
32 |
33 |
34 | {l10n.getString('sign-in-to-view-your-profile')}
35 |
36 | navigation.navigate('SignIn')}
38 | className="bg-primary px-8 py-4 rounded-lg">
39 |
40 | {l10n.getString('sign-in')}
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | const handleLogout = async () => {
49 | try {
50 | await logout();
51 | navigation.navigate('Main');
52 | } catch (error) {
53 | console.log('Logout error:', error);
54 | }
55 | };
56 |
57 | const options: ProfileOptionType[] = [
58 | {
59 | icon: 'account-details',
60 | label: l10n.getString('profile-information'),
61 | onPress: () => navigation.navigate('ProfileDetail'),
62 | },
63 | {
64 | icon: 'text-box-multiple-outline',
65 | label: l10n.getString('orders'),
66 | onPress: () => navigation.navigate('Orders'),
67 | },
68 | {
69 | icon: 'map-marker-outline',
70 | label: l10n.getString('shipping-addresses'),
71 | onPress: () => navigation.navigate('AddressList'),
72 | },
73 | {
74 | icon: 'cog-outline',
75 | label: l10n.getString('settings'),
76 | onPress: () => navigation.navigate('Settings'),
77 | },
78 | {
79 | icon: 'logout',
80 | label: l10n.getString('logout'),
81 | onPress: handleLogout,
82 | },
83 | ];
84 |
85 | return (
86 |
87 | {/* Header */}
88 |
89 |
90 |
91 | {/* Profile Info Section */}
92 |
93 |
94 |
99 |
100 | {customer.first_name} {customer.last_name}
101 |
102 |
103 | {customer.email}
104 |
105 |
106 |
107 | {/* Profile Options */}
108 |
109 | {options.map((option, index) => (
110 |
116 | ))}
117 |
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | const ProfileOption = ({
125 | icon,
126 | label,
127 | onPress,
128 | }: ProfileOptionType & {onPress?: () => void}) => {
129 | const colors = useColors();
130 | return (
131 |
132 |
133 |
134 |
135 | {label}
136 |
137 |
138 |
139 |
140 | );
141 | };
142 |
143 | export default Profile;
144 |
--------------------------------------------------------------------------------
/app/screens/region-select.tsx:
--------------------------------------------------------------------------------
1 | import React, {useTransition} from 'react';
2 | import {TouchableOpacity, View, FlatList} from 'react-native';
3 | import Text from '@components/common/text';
4 | import {useColors} from '@styles/hooks';
5 | import Icon from '@react-native-vector-icons/ant-design';
6 | import {HttpTypes} from '@medusajs/types';
7 | import {useRegion} from '@data/region-context';
8 | import {useNavigation} from '@react-navigation/native';
9 | import Loader from '@components/common/loader';
10 | import apiClient from '@api/client';
11 | import {useQuery} from '@tanstack/react-query';
12 | import ErrorUI from '@components/common/error-ui';
13 | import Navbar from '@components/common/navbar';
14 |
15 | const RegionSelect = () => {
16 | const colors = useColors();
17 | const {region, setRegion} = useRegion();
18 | const [_, startTransition] = useTransition();
19 | const navigation = useNavigation();
20 |
21 | const onSelect = (selectedRegion: HttpTypes.StoreRegion) => {
22 | if (selectedRegion.id !== region?.id) {
23 | startTransition(() => {
24 | setRegion(selectedRegion);
25 | });
26 | }
27 | navigation.goBack();
28 | };
29 |
30 | const {data, isLoading, error} = useQuery({
31 | queryKey: ['regions'],
32 | queryFn: () => apiClient.store.region.list(),
33 | });
34 |
35 | if (isLoading) {
36 | return ;
37 | }
38 |
39 | if (error || !data?.regions) {
40 | return ;
41 | }
42 |
43 | const renderItem = ({item}: {item: HttpTypes.StoreRegion}) => (
44 | onSelect(item)}
46 | className="flex-row items-center justify-between p-4 border-b border-gray-200">
47 |
48 | {item.name}
49 |
50 | {item.currency_code.toUpperCase()}
51 |
52 |
53 | {region?.id === item.id && (
54 |
55 | )}
56 |
57 | );
58 |
59 | return (
60 |
61 |
62 | item.id}
66 | />
67 |
68 | );
69 | };
70 |
71 | export default RegionSelect;
72 |
--------------------------------------------------------------------------------
/app/screens/settings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, ScrollView} from 'react-native';
3 | import {useLocalization} from '@fluent/react';
4 | import Text from '@components/common/text';
5 | import Navbar from '@components/common/navbar';
6 | import {DEFAULT_LOCALE, useLocale} from '@data/locale-context';
7 | import Button from '@components/common/button';
8 | import Dropdown from '@components/common/dropdown';
9 | import {useForm, Controller} from 'react-hook-form';
10 | import {zodResolver} from '@hookform/resolvers/zod';
11 | import {z} from 'zod';
12 |
13 | const settingSchema = z.object({
14 | language: z.enum(['en-US', 'id-ID'], {message: 'language-is-required'}),
15 | });
16 |
17 | type ProfileFormData = z.infer;
18 |
19 | type DropdownData = {
20 | label: string;
21 | value: ProfileFormData['language'];
22 | };
23 |
24 | const dropdownData: DropdownData[] = [
25 | {
26 | label: 'English',
27 | value: 'en-US',
28 | },
29 | {
30 | label: 'Bahasa',
31 | value: 'id-ID',
32 | },
33 | ];
34 |
35 | const Settings = () => {
36 | const {l10n} = useLocalization();
37 | const {locale, setLocale} = useLocale();
38 |
39 | const {
40 | control,
41 | handleSubmit,
42 | formState: {errors},
43 | reset,
44 | } = useForm({
45 | resolver: zodResolver(settingSchema),
46 | defaultValues: {
47 | language: DEFAULT_LOCALE,
48 | },
49 | });
50 |
51 | const handleSave = React.useMemo(
52 | () =>
53 | handleSubmit(data => {
54 | setLocale(data.language);
55 | }),
56 | [handleSubmit, setLocale],
57 | );
58 |
59 | const renderContent = () => {
60 | return (
61 |
62 |
63 | {l10n.getString('language')}
64 | (
68 | onChange(data.value)}
73 | labelField="label"
74 | placeholder={l10n.getString('select-language')}
75 | valueField="value"
76 | error={
77 | errors.language?.message
78 | ? l10n.getString(errors.language.message)
79 | : undefined
80 | }
81 | />
82 | )}
83 | />
84 |
85 |
86 | );
87 | };
88 |
89 | React.useEffect(() => {
90 | reset({
91 | language: locale,
92 | });
93 | }, [locale, reset]);
94 |
95 | return (
96 |
97 |
98 |
99 |
100 | {renderContent()}
101 |
102 |
103 |
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default Settings;
115 |
--------------------------------------------------------------------------------
/app/screens/splash.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigation, StackActions} from '@react-navigation/native';
2 | import React, {useEffect} from 'react';
3 | import {StatusBar, View} from 'react-native';
4 | import Text from '@components/common/text';
5 | import {useTheme} from '@styles/hooks';
6 |
7 | const Splash = () => {
8 | const navigation = useNavigation();
9 | const {isDarkMode} = useTheme();
10 | useEffect(() => {
11 | setTimeout(() => {
12 | navigation.dispatch(StackActions.replace('Main'));
13 | }, 300);
14 | }, [navigation]);
15 | return (
16 | <>
17 |
22 |
23 |
26 | MEDUSA{'\n'}MOBILE
27 |
28 |
29 | >
30 | );
31 | };
32 |
33 | export default Splash;
34 |
--------------------------------------------------------------------------------
/app/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/app/styles/hooks.tsx:
--------------------------------------------------------------------------------
1 | import {useContext} from 'react';
2 | import {ThemeContext} from '@styles/theme-provider';
3 |
4 | export const useThemeName = () => {
5 | return useContext(ThemeContext).name;
6 | };
7 |
8 | export const useColorScheme = () => {
9 | return useContext(ThemeContext).colorScheme;
10 | };
11 |
12 | export const useColors = () => {
13 | return useContext(ThemeContext).colors;
14 | };
15 |
16 | export const useTheme = () => {
17 | return useContext(ThemeContext);
18 | };
19 |
--------------------------------------------------------------------------------
/app/styles/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import {useColorScheme} from 'nativewind';
2 | import React, {createContext, PropsWithChildren} from 'react';
3 | import {themes, themeColorSets, ThemeName} from '@styles/themes';
4 | import {View} from 'react-native';
5 | import {Colors} from '@styles/types';
6 |
7 | interface ThemeContext {
8 | name: ThemeName;
9 | colors: Colors;
10 | colorScheme: 'light' | 'dark';
11 | setThemeName: (name: ThemeName) => void;
12 | isDarkMode: boolean;
13 | }
14 |
15 | export const ThemeContext = createContext({
16 | name: 'default',
17 | colors: themeColorSets.default.light,
18 | colorScheme: 'light',
19 | setThemeName: () => {},
20 | isDarkMode: false,
21 | });
22 |
23 | type ThemeProviderProps = {
24 | name: ThemeName;
25 | };
26 |
27 | function ThemeProvider({
28 | name,
29 | children,
30 | }: PropsWithChildren) {
31 | const [themeName, setThemeName] = React.useState(name);
32 | const {colorScheme = 'light'} = useColorScheme();
33 | const themeStyles = themes[themeName][colorScheme];
34 | const colors = themeColorSets[themeName][colorScheme];
35 | return (
36 |
44 |
45 | {children}
46 |
47 |
48 | );
49 | }
50 |
51 | export default ThemeProvider;
52 |
--------------------------------------------------------------------------------
/app/styles/themes.tsx:
--------------------------------------------------------------------------------
1 | // import {vars} from 'nativewind';
2 | import {applyThemes} from '@styles/utils';
3 |
4 | export const themeColorSets = {
5 | default: {
6 | light: {
7 | primary: '#8e6cef',
8 | background: 'white',
9 | backgroundSecondary: '#F0F3F8',
10 | content: '#18181b',
11 | contentSecondary: 'white',
12 | },
13 | dark: {
14 | primary: '#8e6cef',
15 | background: '#1A1A1D',
16 | backgroundSecondary: '#3C3D37',
17 | content: 'white',
18 | contentSecondary: 'white',
19 | },
20 | },
21 | vintage: {
22 | light: {
23 | primary: '#C96868',
24 | background: '#FFF4EA',
25 | backgroundSecondary: '#FFFFFF',
26 | content: 'black',
27 | contentSecondary: 'white',
28 | },
29 | dark: {
30 | primary: '#C96868',
31 | background: '#131010',
32 | backgroundSecondary: '#543A14',
33 | content: 'white',
34 | contentSecondary: 'white',
35 | },
36 | },
37 | funky: {
38 | light: {
39 | primary: '#7EACB5',
40 | background: '#FADFA1',
41 | backgroundSecondary: '#FFF4EA',
42 | content: 'black',
43 | contentSecondary: 'white',
44 | },
45 | dark: {
46 | primary: '#7EACB5',
47 | background: '#131010',
48 | backgroundSecondary: '#543A14',
49 | content: 'white',
50 | contentSecondary: 'white',
51 | },
52 | },
53 | eco: {
54 | light: {
55 | primary: '#77B254',
56 | background: '#E1FFBB',
57 | backgroundSecondary: '#F5F5F5',
58 | content: 'black',
59 | contentSecondary: 'white',
60 | },
61 | dark: {
62 | primary: '#77B254',
63 | background: '#16423C',
64 | backgroundSecondary: '#659287',
65 | content: 'white',
66 | contentSecondary: 'white',
67 | },
68 | },
69 | };
70 |
71 | export type ThemeName = keyof typeof themeColorSets;
72 |
73 | export const themes = applyThemes(themeColorSets);
74 |
75 | export const themeNames = Object.keys(themeColorSets) as ThemeName[];
76 |
--------------------------------------------------------------------------------
/app/styles/types.tsx:
--------------------------------------------------------------------------------
1 | export type ThemeColorSet = {
2 | light: Colors;
3 | dark: Colors;
4 | };
5 |
6 | export type ThemeColorSets = {
7 | [key: string]: ThemeColorSet;
8 | };
9 |
10 | export type Theme = {
11 | light: Record;
12 | dark: Record;
13 | };
14 |
15 | export type Themes = {
16 | [key: string]: Theme;
17 | };
18 |
19 | export type Colors = {
20 | primary: string;
21 | background: string;
22 | content: string;
23 | contentSecondary: string;
24 | backgroundSecondary: string;
25 | };
26 |
--------------------------------------------------------------------------------
/app/styles/utils.tsx:
--------------------------------------------------------------------------------
1 | import {Themes, ThemeColorSets, Colors} from './types';
2 | import {vars} from 'nativewind';
3 |
4 | export const applyThemes = (themeColorSets: ThemeColorSets): Themes => {
5 | const themes: Themes = {};
6 | for (const themeName in themeColorSets) {
7 | const themeColorSet = themeColorSets[themeName];
8 | themes[themeName] = {
9 | light: getVars(themeColorSet.light),
10 | dark: getVars(themeColorSet.dark),
11 | };
12 | }
13 | return themes;
14 | };
15 |
16 | const getVars = (colors: Colors) => {
17 | return vars(
18 | Object.entries(colors).reduce(
19 | (acc: Record, [key, value]) => {
20 | // convert key from camelcase to kebab-case
21 | const kebabKey = key
22 | .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2')
23 | .toLowerCase();
24 | acc[`--color-${kebabKey}`] = value;
25 | return acc;
26 | },
27 | {},
28 | ),
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/app/types/checkout.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod';
2 |
3 | export type CheckoutStep = 'address' | 'delivery' | 'payment' | 'review';
4 |
5 | export const CHECKOUT_STEPS: {
6 | id: CheckoutStep;
7 | title: string;
8 | icon: 'environment' | 'inbox' | 'wallet' | 'profile';
9 | }[] = [
10 | {id: 'address', title: 'address', icon: 'environment'},
11 | {id: 'delivery', title: 'delivery', icon: 'inbox'},
12 | {id: 'payment', title: 'payment', icon: 'wallet'},
13 | {id: 'review', title: 'review', icon: 'profile'},
14 | ];
15 |
16 | export type AddressFields = {
17 | first_name: string;
18 | last_name: string;
19 | address_1: string;
20 | company?: string;
21 | postal_code: string;
22 | city: string;
23 | country_code: string;
24 | province?: string;
25 | phone: string;
26 | };
27 |
28 | export const addressSchema = z.object({
29 | first_name: z.string().min(1, 'first-name-is-required'),
30 | last_name: z.string().min(1, 'last-name-is-required'),
31 | address_1: z.string().min(1, 'address-is-required'),
32 | company: z.string().optional().or(z.literal('')),
33 | postal_code: z.string().min(1, 'postal-code-is-required'),
34 | city: z.string().min(1, 'city-is-required'),
35 | country_code: z.string().min(1, 'country-is-required'),
36 | province: z.string().optional().or(z.literal('')),
37 | phone: z.string().min(1, 'phone-is-required'),
38 | }) satisfies z.ZodType;
39 |
40 | export const createEmptyAddress = (): AddressFields =>
41 | Object.fromEntries(
42 | Object.keys(addressSchema.shape).map(key => [key, '']),
43 | ) as AddressFields;
44 |
45 | export const checkoutSchema = z.object({
46 | email: z.string().email('please-enter-a-valid-email'),
47 | shipping_address: addressSchema,
48 | billing_address: addressSchema,
49 | use_same_billing: z.boolean(),
50 | });
51 |
52 | export type CheckoutFormData = z.infer;
53 |
54 | export type PaymentProvider = {
55 | id: string;
56 | name: string;
57 | description?: string;
58 | is_installed: boolean;
59 | };
60 |
61 | export const PAYMENT_PROVIDER_DETAILS_MAP: Record<
62 | string,
63 | {name: string; hasExternalStep: boolean}
64 | > = {
65 | pp_system_default: {name: 'Manual', hasExternalStep: false},
66 | pp_stripe_stripe: {name: 'Stripe', hasExternalStep: true},
67 | };
68 |
--------------------------------------------------------------------------------
/app/utils/common.tsx:
--------------------------------------------------------------------------------
1 | import { BaseRegionCountry } from '@medusajs/types/dist/http/region/common';
2 |
3 | const utils = {
4 | areEqualObjects: function (
5 | o1?: Record,
6 | o2?: Record,
7 | ) {
8 | if (!o1 || !o2) {
9 | return false;
10 | }
11 | for (var p in o1) {
12 | if (o1.hasOwnProperty(p)) {
13 | if (o1[p] !== o2[p]) {
14 | return false;
15 | }
16 | }
17 | }
18 | for (var p in o2) {
19 | if (o2.hasOwnProperty(p)) {
20 | if (o1[p] !== o2[p]) {
21 | return false;
22 | }
23 | }
24 | }
25 | return true;
26 | },
27 | getCountryName: (
28 | countryCode?: string,
29 | countries?: BaseRegionCountry[],
30 | ) => {
31 | if (!countryCode || !countries) {
32 | return '';
33 | }
34 | const country = countries.find(c => c.iso_2 === countryCode);
35 | return country?.display_name || countryCode.toUpperCase();
36 | },
37 | };
38 |
39 | export default utils;
40 |
--------------------------------------------------------------------------------
/app/utils/image-url.tsx:
--------------------------------------------------------------------------------
1 | import {apiUrl} from '@api/client';
2 |
3 | export const formatImageUrl = (url?: string | undefined | null) => {
4 | if (!url) {
5 | return '';
6 | }
7 | // if url has localhost:9000, replace with the actual backend url
8 | if (url.includes('http://localhost:9000')) {
9 | return url.replace('http://localhost:9000', apiUrl);
10 | }
11 | return url;
12 | };
13 |
--------------------------------------------------------------------------------
/app/utils/order.ts:
--------------------------------------------------------------------------------
1 | export type FulfillmentStatus =
2 | | 'canceled'
3 | | 'not_fulfilled'
4 | | 'partially_fulfilled'
5 | | 'fulfilled'
6 | | 'partially_shipped'
7 | | 'shipped'
8 | | 'partially_delivered'
9 | | 'delivered';
10 |
11 | export const getFulfillmentStatus = (status: FulfillmentStatus) => {
12 | const statusMap: Record = {
13 | canceled: 'canceled',
14 | not_fulfilled: 'not-fulfilled',
15 | partially_fulfilled: 'partially-fulfilled',
16 | fulfilled: 'fulfilled',
17 | partially_shipped: 'partially-shipped',
18 | shipped: 'shipped',
19 | partially_delivered: 'partially-delivered',
20 | delivered: 'delivered',
21 | };
22 |
23 | return statusMap[status] || 'not-fulfilled';
24 | };
25 |
--------------------------------------------------------------------------------
/app/utils/product-price.tsx:
--------------------------------------------------------------------------------
1 | import {HttpTypes} from '@medusajs/types';
2 |
3 | export const getPricesForVariant = (variant: any) => {
4 | if (!variant?.calculated_price?.calculated_amount) {
5 | return null;
6 | }
7 |
8 | return {
9 | calculated_price_number: variant.calculated_price.calculated_amount,
10 | calculated_price: convertToLocale({
11 | amount: variant.calculated_price.calculated_amount,
12 | currency_code: variant.calculated_price.currency_code,
13 | }),
14 | original_price_number: variant.calculated_price.original_amount,
15 | original_price: convertToLocale({
16 | amount: variant.calculated_price.original_amount,
17 | currency_code: variant.calculated_price.currency_code,
18 | }),
19 | currency_code: variant.calculated_price.currency_code,
20 | price_type: variant.calculated_price.calculated_price.price_list_type,
21 | percentage_diff: getPercentageDiff(
22 | variant.calculated_price.original_amount,
23 | variant.calculated_price.calculated_amount,
24 | ),
25 | };
26 | };
27 |
28 | export function getProductPrice({
29 | product,
30 | variantId,
31 | }: {
32 | product: HttpTypes.StoreProduct;
33 | variantId?: string;
34 | }) {
35 | if (!product || !product.id) {
36 | throw new Error('No product provided');
37 | }
38 |
39 | const cheapestPrice = () => {
40 | if (!product || !product.variants?.length) {
41 | return null;
42 | }
43 |
44 | const cheapestVariant: any = product.variants
45 | .filter((v: any) => !!v.calculated_price)
46 | .sort((a: any, b: any) => {
47 | return (
48 | a.calculated_price.calculated_amount -
49 | b.calculated_price.calculated_amount
50 | );
51 | })[0];
52 |
53 | return getPricesForVariant(cheapestVariant);
54 | };
55 |
56 | const variantPrice = () => {
57 | if (!product || !variantId) {
58 | return null;
59 | }
60 |
61 | const variant: any = product.variants?.find(
62 | v => v.id === variantId || v.sku === variantId,
63 | );
64 |
65 | if (!variant) {
66 | return null;
67 | }
68 |
69 | return getPricesForVariant(variant);
70 | };
71 |
72 | return {
73 | product,
74 | cheapestPrice: cheapestPrice(),
75 | variantPrice: variantPrice(),
76 | };
77 | }
78 |
79 | export const getPercentageDiff = (original: number, calculated: number) => {
80 | const diff = original - calculated;
81 | const decrease = (diff / original) * 100;
82 |
83 | return decrease.toFixed();
84 | };
85 |
86 | type ConvertToLocaleParams = {
87 | amount: number;
88 | currency_code: string;
89 | minimumFractionDigits?: number;
90 | maximumFractionDigits?: number;
91 | locale?: string;
92 | };
93 |
94 | export const convertToLocale = ({
95 | amount,
96 | currency_code,
97 | minimumFractionDigits,
98 | maximumFractionDigits,
99 | locale = 'en-US',
100 | }: ConvertToLocaleParams) => {
101 | return currency_code && !isEmpty(currency_code)
102 | ? new Intl.NumberFormat(locale, {
103 | style: 'currency',
104 | currency: currency_code,
105 | minimumFractionDigits,
106 | maximumFractionDigits,
107 | }).format(amount)
108 | : amount.toString();
109 | };
110 |
111 | export const isObject = (input: any) => input instanceof Object;
112 | export const isArray = (input: any) => Array.isArray(input);
113 | export const isEmpty = (input: any) => {
114 | return (
115 | input === null ||
116 | input === undefined ||
117 | (isObject(input) && Object.keys(input).length === 0) ||
118 | (isArray(input) && (input as any[]).length === 0) ||
119 | (typeof input === 'string' && input.trim().length === 0)
120 | );
121 | };
122 |
--------------------------------------------------------------------------------
/assets/fonts/Audiowide-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/assets/fonts/Audiowide-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/Lato-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/assets/fonts/Lato-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Lato-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/assets/fonts/Lato-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/Lato-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/assets/fonts/Lato-Thin.ttf
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:@react-native/babel-preset', 'nativewind/babel'],
3 | plugins: [
4 | 'react-native-reanimated/plugin',
5 | 'module:react-native-dotenv',
6 | [
7 | 'module-resolver',
8 | {
9 | root: ['./app'],
10 | extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
11 | alias: {
12 | '@styles': './app/styles',
13 | '@images': './app/images',
14 | '@screens': './app/screens',
15 | '@components': './app/components',
16 | '@constants': './app/constants',
17 | '@api': './app/api',
18 | '@utils': './app/utils',
19 | '@data': './app/data',
20 | '@types': './app/types',
21 | },
22 | },
23 | ],
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import 'react-native-url-polyfill/auto';
6 |
7 | import {AppRegistry} from 'react-native';
8 | import App from './app/app';
9 | import {name as appName} from './app.json';
10 |
11 | AppRegistry.registerComponent(appName, () => App);
12 |
--------------------------------------------------------------------------------
/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | # This `.xcode.env` file is versioned and is used to source the environment
2 | # used when running script phases inside Xcode.
3 | # To customize your local environment, you can create an `.xcode.env.local`
4 | # file that is not versioned.
5 |
6 | # NODE_BINARY variable contains the PATH to the node executable.
7 | #
8 | # Customize the NODE_BINARY variable here.
9 | # For example, to use nvm with brew, add the following line
10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use
11 | export NODE_BINARY=$(command -v node)
12 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Resolve react_native_pods.rb with node to allow for hoisting
2 | require Pod::Executable.execute_command('node', ['-p',
3 | 'require.resolve(
4 | "react-native/scripts/react_native_pods.rb",
5 | {paths: [process.argv[1]]},
6 | )', __dir__]).strip
7 |
8 | platform :ios, min_ios_version_supported
9 | prepare_react_native_project!
10 |
11 | linkage = ENV['USE_FRAMEWORKS']
12 | if linkage != nil
13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
14 | use_frameworks! :linkage => linkage.to_sym
15 | end
16 |
17 | target 'RnStarterMedusa' do
18 | config = use_native_modules!
19 |
20 | use_react_native!(
21 | :path => config[:reactNativePath],
22 | # An absolute path to your application root.
23 | :app_path => "#{Pod::Config.instance.installation_root}/.."
24 | )
25 |
26 | post_install do |installer|
27 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
28 | react_native_post_install(
29 | installer,
30 | config[:reactNativePath],
31 | :mac_catalyst_enabled => false,
32 | # :ccache_enabled => true
33 | )
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa.xcodeproj/xcshareddata/xcschemes/RnStarterMedusa.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/RnStarterMedusa.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import React
3 | import React_RCTAppDelegate
4 | import ReactAppDependencyProvider
5 |
6 | @main
7 | class AppDelegate: UIResponder, UIApplicationDelegate {
8 | var window: UIWindow?
9 |
10 | var reactNativeDelegate: ReactNativeDelegate?
11 | var reactNativeFactory: RCTReactNativeFactory?
12 |
13 | func application(
14 | _ application: UIApplication,
15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
16 | ) -> Bool {
17 | let delegate = ReactNativeDelegate()
18 | let factory = RCTReactNativeFactory(delegate: delegate)
19 | delegate.dependencyProvider = RCTAppDependencyProvider()
20 |
21 | reactNativeDelegate = delegate
22 | reactNativeFactory = factory
23 |
24 | window = UIWindow(frame: UIScreen.main.bounds)
25 |
26 | factory.startReactNative(
27 | withModuleName: "RnStarterMedusa",
28 | in: window,
29 | launchOptions: launchOptions
30 | )
31 |
32 | return true
33 | }
34 | }
35 |
36 | class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
37 | override func sourceURL(for bridge: RCTBridge) -> URL? {
38 | self.bundleURL()
39 | }
40 |
41 | override func bundleURL() -> URL? {
42 | #if DEBUG
43 | RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
44 | #else
45 | Bundle.main.url(forResource: "main", withExtension: "jsbundle")
46 | #endif
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | CA92.1
21 |
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategorySystemBootTime
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 35F9.1
29 |
30 |
31 |
32 | NSPrivacyCollectedDataTypes
33 |
34 | NSPrivacyTracking
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | RnStarterMedusa
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 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 | NSAllowsLocalNetworking
32 |
33 |
34 | NSLocationWhenInUseUsageDescription
35 |
36 | UILaunchStoryboardName
37 | LaunchScreen
38 | UIRequiredDeviceCapabilities
39 |
40 | arm64
41 |
42 | UISupportedInterfaceOrientations
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 | UIViewControllerBasedStatusBarAppearance
49 |
50 | UIAppFonts
51 |
52 | Audiowide-Regular.ttf
53 | Lato-Bold.ttf
54 | Lato-Regular.ttf
55 | Lato-Thin.ttf
56 | AntDesign.ttf
57 | MaterialDesignIcons.ttf
58 |
59 |
60 |
--------------------------------------------------------------------------------
/ios/RnStarterMedusa/main.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char *argv[])
6 | {
7 | @autoreleasepool {
8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
9 | }
10 | }
--------------------------------------------------------------------------------
/ios/link-assets-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "migIndex": 1,
3 | "data": [
4 | {
5 | "path": "assets/fonts/Audiowide-Regular.ttf",
6 | "sha1": "651503b7a0a89f49f78f14d04c62f811853573cd"
7 | },
8 | {
9 | "path": "assets/fonts/Lato-Bold.ttf",
10 | "sha1": "542498221d97bee5bdbccf86ee8890bf8e8005c9"
11 | },
12 | {
13 | "path": "assets/fonts/Lato-Regular.ttf",
14 | "sha1": "e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3"
15 | },
16 | {
17 | "path": "assets/fonts/Lato-Thin.ttf",
18 | "sha1": "07290446bee3f81ce501a3c3dbfde6097c70ca15"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | };
4 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
2 | const {withNativeWind} = require('nativewind/metro');
3 | const {
4 | wrapWithReanimatedMetroConfig,
5 | } = require('react-native-reanimated/metro-config');
6 |
7 | /**
8 | * Metro configuration
9 | * https://reactnative.dev/docs/metro
10 | *
11 | * @type {import('@react-native/metro-config').MetroConfig}
12 | */
13 | const config = mergeConfig(getDefaultConfig(__dirname), {
14 | /* your config */
15 | });
16 |
17 | module.exports = wrapWithReanimatedMetroConfig(
18 | withNativeWind(config, {input: './app/styles/global.css'}),
19 | );
20 |
--------------------------------------------------------------------------------
/nativewind-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medusa-mobile-react-native",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "lint": "eslint .",
9 | "start": "react-native start",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "@fluent/bundle": "^0.19.1",
14 | "@fluent/langneg": "^0.7.0",
15 | "@fluent/react": "^0.15.2",
16 | "@hookform/resolvers": "^3.10.0",
17 | "@medusajs/js-sdk": "^2.8.3",
18 | "@medusajs/types": "^2.8.3",
19 | "@react-native-async-storage/async-storage": "^2.1.1",
20 | "@react-native-picker/picker": "^2.11.0",
21 | "@react-native-vector-icons/ant-design": "^4.4.2",
22 | "@react-native-vector-icons/common": "^11.0.0",
23 | "@react-native-vector-icons/material-design-icons": "^7.4.47",
24 | "@react-navigation/bottom-tabs": "^7.2.0",
25 | "@react-navigation/native": "^7.0.14",
26 | "@react-navigation/native-stack": "^7.2.0",
27 | "@tanstack/react-query": "^5.64.2",
28 | "dayjs": "^1.11.13",
29 | "nativewind": "^4.1.23",
30 | "react": "19.0.0",
31 | "react-hook-form": "^7.54.2",
32 | "react-native": "0.79.2",
33 | "react-native-element-dropdown": "^2.12.4",
34 | "react-native-gesture-handler": "^2.22.1",
35 | "react-native-reanimated": "^3.16.7",
36 | "react-native-reanimated-carousel": "^4.0.0-canary.22",
37 | "react-native-safe-area-context": "^5.2.0",
38 | "react-native-screens": "^4.6.0",
39 | "react-native-url-polyfill": "^2.0.0",
40 | "tailwind-variants": "^0.3.1",
41 | "tailwindcss": "^3.4.17",
42 | "zod": "^3.24.1"
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.25.2",
46 | "@babel/preset-env": "^7.25.3",
47 | "@babel/runtime": "^7.25.0",
48 | "@react-native-community/cli": "18.0.0",
49 | "@react-native-community/cli-platform-android": "18.0.0",
50 | "@react-native-community/cli-platform-ios": "18.0.0",
51 | "@react-native/babel-preset": "0.79.2",
52 | "@react-native/eslint-config": "0.79.2",
53 | "@react-native/metro-config": "0.79.2",
54 | "@react-native/typescript-config": "0.79.2",
55 | "@tanstack/eslint-plugin-query": "^5.64.2",
56 | "@types/jest": "^29.5.13",
57 | "@types/react": "^19.0.0",
58 | "@types/react-test-renderer": "^19.0.0",
59 | "babel-jest": "^29.6.3",
60 | "babel-plugin-module-resolver": "^5.0.2",
61 | "eslint": "^8.19.0",
62 | "jest": "^29.6.3",
63 | "prettier": "2.8.8",
64 | "react-native-dotenv": "^3.4.11",
65 | "react-test-renderer": "19.0.0",
66 | "typescript": "5.0.4"
67 | },
68 | "engines": {
69 | "node": ">=18"
70 | },
71 | "packageManager": "yarn@1.22.21+sha512.ca75da26c00327d26267ce33536e5790f18ebd53266796fbb664d2a4a5116308042dd8ee7003b276a20eace7d3c5561c3577bdd71bcb67071187af124779620a"
72 | }
73 |
--------------------------------------------------------------------------------
/react-native.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | project: {
3 | ios: {},
4 | android: {},
5 | },
6 | assets: ['./assets/fonts'],
7 | };
8 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | // NOTE: Update this to include the paths to all of your component files.
4 | content: ['./app/**/*.{js,jsx,ts,tsx}'],
5 | presets: [require('nativewind/preset')],
6 | theme: {
7 | extend: {
8 | colors: {
9 | primary: 'var(--color-primary)',
10 | background: 'var(--color-background)',
11 | 'background-secondary': 'var(--color-background-secondary)',
12 | content: 'var(--color-content)',
13 | 'content-secondary': 'var(--color-content-secondary)',
14 | },
15 | fontFamily: {
16 | display: 'Audiowide-Regular',
17 | content: 'Lato-Regular',
18 | 'content-thin': 'Lato-Thin',
19 | 'content-bold': 'Lato-Bold',
20 | },
21 | },
22 | },
23 | plugins: [],
24 | };
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@react-native/typescript-config/tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@styles/*": ["app/styles/*"],
7 | "@images/*": ["app/images/*"],
8 | "@screens/*": ["app/screens/*"],
9 | "@components/*": ["app/components/*"],
10 | "@constants/*": ["app/constants/*"],
11 | "@api/*": ["app/api/*"],
12 | "@utils/*": ["app/utils/*"],
13 | "@data/*": ["app/data/*"],
14 | "@types/*": ["app/types/*"]
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/types/components.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bloomsynth/medusa-mobile-react-native/242a0598660364a2fcf5506813dba56f07c3a280/types/components.d.ts
--------------------------------------------------------------------------------
/types/dot-env.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@env' {
2 | export const MEDUSA_BACKEND_URL: string;
3 | export const PUBLISHABLE_API_KEY: string;
4 | export const DEFAULT_LOCALE: 'en-US' | 'id-ID';
5 | }
6 |
--------------------------------------------------------------------------------
/types/global.d.ts:
--------------------------------------------------------------------------------
1 | import {StorePrice} from '@medusajs/types';
2 |
3 | export type FeaturedProduct = {
4 | id: string;
5 | title: string;
6 | handle: string;
7 | thumbnail?: string;
8 | };
9 |
10 | export type VariantPrice = {
11 | calculated_price_number: number;
12 | calculated_price: string;
13 | original_price_number: number;
14 | original_price: string;
15 | currency_code: string;
16 | price_type: string;
17 | percentage_diff: string;
18 | };
19 |
20 | export type StoreFreeShippingPrice = StorePrice & {
21 | target_reached: boolean;
22 | target_remaining: number;
23 | remaining_percentage: number;
24 | };
25 |
--------------------------------------------------------------------------------
/types/nativewind-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
4 |
--------------------------------------------------------------------------------
/types/navigation.d.ts:
--------------------------------------------------------------------------------
1 | import {RootStackParamList} from '../app/app';
2 |
3 | declare global {
4 | namespace ReactNavigation {
5 | interface RootParamList extends RootStackParamList {}
6 | }
7 | }
8 |
--------------------------------------------------------------------------------