├── src
├── types
│ └── index.ts
├── api
│ ├── index.tsx
│ ├── common
│ │ ├── index.tsx
│ │ ├── client.tsx
│ │ ├── api-provider.tsx
│ │ └── utils.tsx
│ ├── posts
│ │ ├── types.ts
│ │ ├── index.ts
│ │ ├── use-post.ts
│ │ ├── use-add-post.ts
│ │ └── use-posts.ts
│ └── types.ts
├── lib
│ ├── hooks
│ │ ├── index.tsx
│ │ ├── use-is-first-time.tsx
│ │ └── use-selected-theme.tsx
│ ├── index.tsx
│ ├── i18n
│ │ ├── react-i18next.d.ts
│ │ ├── resources.ts
│ │ ├── types.ts
│ │ ├── index.tsx
│ │ └── utils.tsx
│ ├── auth
│ │ ├── utils.tsx
│ │ └── index.tsx
│ ├── storage.tsx
│ ├── env.js
│ ├── utils.ts
│ ├── use-theme-config.tsx
│ └── test-utils.tsx
├── components
│ ├── ui
│ │ ├── icons
│ │ │ ├── index.tsx
│ │ │ ├── caret-down.tsx
│ │ │ ├── home.tsx
│ │ │ ├── feed.tsx
│ │ │ ├── arrow-right.tsx
│ │ │ ├── website.tsx
│ │ │ ├── share.tsx
│ │ │ ├── style.tsx
│ │ │ ├── support.tsx
│ │ │ ├── rate.tsx
│ │ │ ├── github.tsx
│ │ │ ├── settings.tsx
│ │ │ └── language.tsx
│ │ ├── focus-aware-status-bar.tsx
│ │ ├── image.tsx
│ │ ├── index.tsx
│ │ ├── text.tsx
│ │ ├── progress-bar.tsx
│ │ ├── utils.tsx
│ │ ├── colors.js
│ │ └── modal-keyboard-aware-scroll-view.tsx
│ ├── title.tsx
│ ├── settings
│ │ ├── items-container.tsx
│ │ ├── item.tsx
│ │ ├── language-item.tsx
│ │ └── theme-item.tsx
│ ├── typography.tsx
│ ├── colors.tsx
│ ├── card.tsx
│ ├── buttons.tsx
│ ├── inputs.tsx
│ ├── login-form.tsx
│ └── login-form.test.tsx
├── app
│ ├── [...messing].tsx
│ ├── (app)
│ │ ├── style.tsx
│ │ ├── index.tsx
│ │ ├── _layout.tsx
│ │ └── settings.tsx
│ ├── login.tsx
│ ├── feed
│ │ ├── [id].tsx
│ │ └── add-post.tsx
│ ├── onboarding.tsx
│ ├── +html.tsx
│ └── _layout.tsx
└── translations
│ ├── ar.json
│ └── en.json
├── .husky
├── .gitignore
├── commit-msg
├── common.sh
├── post-merge
└── pre-commit
├── .npmrc
├── android
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── values-night
│ │ │ │ │ └── colors.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ │ └── ic_launcher_foreground.webp
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ │ └── ic_launcher_foreground.webp
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ │ └── ic_launcher_foreground.webp
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ │ └── ic_launcher_foreground.webp
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ ├── ic_launcher_round.webp
│ │ │ │ │ └── ic_launcher_foreground.webp
│ │ │ │ ├── drawable-hdpi
│ │ │ │ │ └── splashscreen_logo.png
│ │ │ │ ├── drawable-mdpi
│ │ │ │ │ └── splashscreen_logo.png
│ │ │ │ ├── drawable-xhdpi
│ │ │ │ │ └── splashscreen_logo.png
│ │ │ │ ├── drawable-xxhdpi
│ │ │ │ │ └── splashscreen_logo.png
│ │ │ │ ├── drawable-xxxhdpi
│ │ │ │ │ └── splashscreen_logo.png
│ │ │ │ ├── values
│ │ │ │ │ ├── colors.xml
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── drawable
│ │ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ │ └── rn_edit_text_material.xml
│ │ │ │ └── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── assets
│ │ │ │ └── fonts
│ │ │ │ │ └── Inter.ttf
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── obytes
│ │ │ │ └── development
│ │ │ │ ├── MainApplication.kt
│ │ │ │ └── MainActivity.kt
│ │ └── debug
│ │ │ └── AndroidManifest.xml
│ ├── debug.keystore
│ └── proguard-rules.pro
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── .gitignore
├── build.gradle
├── settings.gradle
└── gradle.properties
├── nativewind-env.d.ts
├── docs
├── tsconfig.json
├── public
│ ├── _redirects
│ ├── og.jpg
│ └── reviews
│ │ ├── aman.jpg
│ │ ├── brandon.png
│ │ ├── kawtar.jpg
│ │ ├── simon.jpg
│ │ └── yuri.jpeg
├── src
│ ├── env.d.ts
│ ├── assets
│ │ └── logo.webp
│ ├── content
│ │ ├── config.ts
│ │ └── docs
│ │ │ ├── reviews.md
│ │ │ ├── changelog.md
│ │ │ ├── guides
│ │ │ ├── storage.mdx
│ │ │ └── navigation.mdx
│ │ │ ├── how-to-contribute.md
│ │ │ ├── getting-started
│ │ │ └── customize-app.mdx
│ │ │ ├── stay-updated.md
│ │ │ ├── ui-and-theme
│ │ │ └── fonts.mdx
│ │ │ ├── ci-cd
│ │ │ └── overview.mdx
│ │ │ ├── libraries-recommendation.md
│ │ │ └── testing
│ │ │ └── overview.mdx
│ ├── components
│ │ ├── code.astro
│ │ ├── LastUpdated.astro
│ │ └── Comments.astro
│ └── styles
│ │ └── custom.css
├── .gitignore
├── ec.config.mjs
├── package.json
└── README.md
├── global.css
├── __mocks__
├── @gorhom
│ └── bottom-sheet.ts
├── react-native-gesture-handler.ts
├── react-native-keyboard-controller.ts
├── expo-localization.ts
└── moti.ts
├── commitlint.config.js
├── assets
├── icon.png
├── favicon.png
├── fonts
│ └── Inter.ttf
├── splash-icon.png
└── adaptive-icon.png
├── .maestro
├── utils
│ ├── hide-keyboard-android.yaml
│ ├── hide-keyboard-ios.yaml
│ ├── onboarding.yaml
│ ├── onboarding-and-login.yaml
│ ├── hide-keyboard.yaml
│ └── login.yaml
├── auth
│ ├── onboarding.yaml
│ └── login-with-validation.yaml
├── config.yaml
└── app
│ ├── tabs.yaml
│ └── create-post.yaml
├── ios
├── ObytesApp
│ ├── Images.xcassets
│ │ ├── Contents.json
│ │ ├── SplashScreenLogo.imageset
│ │ │ ├── image.png
│ │ │ ├── image@2x.png
│ │ │ ├── image@3x.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── App-Icon-1024x1024@1x.png
│ │ │ └── Contents.json
│ │ └── SplashScreenBackground.colorset
│ │ │ └── Contents.json
│ ├── ObytesApp-Bridging-Header.h
│ ├── ObytesApp.entitlements
│ ├── Supporting
│ │ └── Expo.plist
│ ├── PrivacyInfo.xcprivacy
│ └── AppDelegate.swift
├── Podfile.properties.json
├── ObytesApp.xcworkspace
│ └── contents.xcworkspacedata
├── .gitignore
├── .xcode.env
└── Podfile
├── .prettierrc.js
├── jest-setup.ts
├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── workflows
│ ├── stale.yml
│ ├── new-github-release.yml
│ ├── compress-images.yml
│ ├── lint-ts.yml
│ ├── eas-build-prod.yml
│ ├── test.yml
│ ├── eas-build-qa.yml
│ ├── expo-doctor.yml
│ ├── type-check.yml
│ ├── e2e-android-maestro.yml
│ └── new-app-version.yml
├── scripts
│ └── expo-doctor.sh
└── actions
│ ├── setup-node-pnpm-install
│ └── action.yml
│ └── setup-jdk-generate-apk
│ └── action.yml
├── .env.development
├── .env.production
├── .env.staging
├── metro.config.js
├── scripts
├── genrate-apk-and-install
└── i18next-syntax-validation.js
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── tailwind.config.js
├── lint-staged.config.js
├── tsconfig.json
├── prompts
├── expo-doctor.md
└── svg-icon.md
├── babel.config.js
├── cli
├── package.json
├── index.js
├── clone-repo.js
├── utils.js
├── pnpm-lock.yaml
└── setup-project.js
├── LICENSE
├── jest.config.js
├── eas.json
├── README-project.md
└── app.config.ts
/src/types/index.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | pnpm commitlint --edit $1
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | node-linker=hoisted
2 | auto-install-peers=true
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nativewind-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict"
3 | }
4 |
--------------------------------------------------------------------------------
/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/__mocks__/@gorhom/bottom-sheet.ts:
--------------------------------------------------------------------------------
1 | module.exports = require('@gorhom/bottom-sheet/mock');
2 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/.maestro/utils/hide-keyboard-android.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | tags:
3 | - util
4 | ---
5 | - hideKeyboard
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/assets/favicon.png
--------------------------------------------------------------------------------
/docs/public/_redirects:
--------------------------------------------------------------------------------
1 | # redirect all /docs requests to the root domain
2 |
3 | /docs/\* /:splat 301
4 |
--------------------------------------------------------------------------------
/docs/public/og.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/docs/public/og.jpg
--------------------------------------------------------------------------------
/src/api/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './common';
2 | export * from './posts';
3 | export * from './types';
4 |
--------------------------------------------------------------------------------
/src/lib/hooks/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './use-is-first-time';
2 | export * from './use-selected-theme';
3 |
--------------------------------------------------------------------------------
/docs/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/__mocks__/react-native-gesture-handler.ts:
--------------------------------------------------------------------------------
1 | module.exports = require('react-native-gesture-handler/src/mocks.ts');
2 |
--------------------------------------------------------------------------------
/__mocks__/react-native-keyboard-controller.ts:
--------------------------------------------------------------------------------
1 | module.exports = require('react-native-keyboard-controller/jest');
2 |
--------------------------------------------------------------------------------
/assets/fonts/Inter.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/assets/fonts/Inter.ttf
--------------------------------------------------------------------------------
/assets/splash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/assets/splash-icon.png
--------------------------------------------------------------------------------
/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/debug.keystore
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/docs/src/assets/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/docs/src/assets/logo.webp
--------------------------------------------------------------------------------
/src/api/common/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './api-provider';
2 | export * from './client';
3 | export * from './utils';
4 |
--------------------------------------------------------------------------------
/docs/public/reviews/aman.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/docs/public/reviews/aman.jpg
--------------------------------------------------------------------------------
/ios/ObytesApp/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "version": 1,
4 | "author": "expo"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/docs/public/reviews/brandon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/docs/public/reviews/brandon.png
--------------------------------------------------------------------------------
/docs/public/reviews/kawtar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/docs/public/reviews/kawtar.jpg
--------------------------------------------------------------------------------
/docs/public/reviews/simon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/docs/public/reviews/simon.jpg
--------------------------------------------------------------------------------
/docs/public/reviews/yuri.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/docs/public/reviews/yuri.jpeg
--------------------------------------------------------------------------------
/src/lib/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './auth';
2 | export * from './hooks';
3 | export * from './i18n';
4 | export * from './utils';
5 |
--------------------------------------------------------------------------------
/.maestro/utils/hide-keyboard-ios.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | tags:
3 | - util
4 | ---
5 | - tapOn:
6 | id: "Return" # Keyboard Return
--------------------------------------------------------------------------------
/src/api/posts/types.ts:
--------------------------------------------------------------------------------
1 | export type Post = {
2 | userId: number;
3 | id: number;
4 | title: string;
5 | body: string;
6 | };
7 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/ios/Podfile.properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo.jsEngine": "hermes",
3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
4 | "newArchEnabled": "true"
5 | }
6 |
--------------------------------------------------------------------------------
/src/api/posts/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './use-add-post';
3 | export * from './use-post';
4 | export * from './use-posts';
5 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/fonts/Inter.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/assets/fonts/Inter.ttf
--------------------------------------------------------------------------------
/ios/ObytesApp/ObytesApp-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
--------------------------------------------------------------------------------
/src/api/types.ts:
--------------------------------------------------------------------------------
1 | export type PaginateQuery = {
2 | results: T[];
3 | count: number;
4 | next: string | null;
5 | previous: string | null;
6 | };
7 |
--------------------------------------------------------------------------------
/src/api/common/client.tsx:
--------------------------------------------------------------------------------
1 | import { Env } from '@env';
2 | import axios from 'axios';
3 | export const client = axios.create({
4 | baseURL: Env.API_URL,
5 | });
6 |
--------------------------------------------------------------------------------
/__mocks__/expo-localization.ts:
--------------------------------------------------------------------------------
1 | export const locale = 'en-US';
2 | export const locales = ['en-US'];
3 | export const timezone = 'UTC';
4 | export const isRTL = false;
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | const config = {
3 | singleQuote: true,
4 | endOfLine: 'auto',
5 | trailingComma: 'es5',
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/ios/ObytesApp/Images.xcassets/SplashScreenLogo.imageset/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/ios/ObytesApp/Images.xcassets/SplashScreenLogo.imageset/image.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/ios/ObytesApp/Images.xcassets/SplashScreenLogo.imageset/image@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/ios/ObytesApp/Images.xcassets/SplashScreenLogo.imageset/image@2x.png
--------------------------------------------------------------------------------
/ios/ObytesApp/Images.xcassets/SplashScreenLogo.imageset/image@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obytes/react-native-template-obytes/HEAD/ios/ObytesApp/Images.xcassets/SplashScreenLogo.imageset/image@3x.png
--------------------------------------------------------------------------------
/jest-setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/react-native/extend-expect';
2 |
3 | // react-hook form setup for testing
4 | // @ts-ignore
5 | global.window = {};
6 | // @ts-ignore
7 | global.window = global;
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Summary:
2 |
3 | ## Steps to reproduce:
4 |
5 | ## Expected behavior:
6 |
7 | ## Additional notes:
8 |
9 | #### Tasks
10 |
11 | - [ ] Task 1
12 | - [ ] Task 2
13 | - [ ] Task 3
14 |
--------------------------------------------------------------------------------
/.husky/common.sh:
--------------------------------------------------------------------------------
1 | command_exists() {
2 | command -v "$1" >/dev/null 2>&1
3 | }
4 |
5 | # Workaround for Windows 10, Git Bash and Yarn
6 | if command_exists winpty && test -t 1; then
7 | exec
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.maestro/config.yaml:
--------------------------------------------------------------------------------
1 | flows:
2 | - auth/*
3 | - app/*
4 |
5 | excludeTags:
6 | - util
7 |
8 | executionOrder:
9 | continueOnFailure: false # default is true
10 | flowsOrder:
11 | - onboarding
12 | - login-with-validation
--------------------------------------------------------------------------------
/.maestro/utils/onboarding-and-login.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | tags:
3 | - util
4 | ---
5 | - runFlow:
6 | when:
7 | visible: "Obytes Starter"
8 | file: onboarding.yaml
9 | - runFlow:
10 | when:
11 | visible: Sign In
12 | file: login.yaml
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 | #2E3C4B
3 | #2E3C4B
4 | #023c69
5 | #2E3C4B
6 |
--------------------------------------------------------------------------------
/src/lib/i18n/react-i18next.d.ts:
--------------------------------------------------------------------------------
1 | import type { resources } from './resources';
2 |
3 | // react-i18next versions higher than 11.11.0
4 |
5 | declare module 'react-i18next' {
6 | interface CustomTypeOptions {
7 | resources: (typeof resources)['en'];
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.maestro/utils/hide-keyboard.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | tags:
3 | - util
4 | ---
5 | - runFlow:
6 | when:
7 | platform: iOS
8 | file: ./hide-keyboard-ios.yaml
9 | - runFlow:
10 | when:
11 | platform: Android
12 | file: ./hide-keyboard-android.yaml
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | API_URL=https://dummyjson.com/
2 |
3 | ## TODO: add the variable to your CI and remove it from here, not recommended setting sensitive values on your git repo
4 | SECRET_KEY=my-secret-key
5 | VAR_NUMBER=10 # this is a number variable
6 | VAR_BOOL=true # this is a boolean variable
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | API_URL=https://dummyjson.com/
2 |
3 | ## TODO: add the variable to your CI and remove it from here, not recommended setting sensitive values on your git repo
4 | SECRET_KEY=my-secret-key
5 | VAR_NUMBER=10 # this is a number variable
6 | VAR_BOOL=true # this is a boolean variable
--------------------------------------------------------------------------------
/.env.staging:
--------------------------------------------------------------------------------
1 | API_URL=https://dummyjson.com/
2 |
3 | ## TODO: add the variable to your CI and remove it from here, not recommended setting sensitive values on your git repo
4 | SECRET_KEY=my-secret-key
5 | VAR_NUMBER=10 # this is a number variable
6 | VAR_BOOL=true # this is a boolean variable
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const { getDefaultConfig } = require('expo/metro-config');
4 | const { withNativeWind } = require('nativewind/metro');
5 |
6 | const config = getDefaultConfig(__dirname);
7 |
8 | module.exports = withNativeWind(config, { input: './global.css' });
9 |
--------------------------------------------------------------------------------
/scripts/genrate-apk-and-install:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # this simple script will get the latest build url for the android platform
3 | ./android/gradlew assembleRelease -p ./android # build debug apk
4 | find ./android -type f -name "app-release.apk" # find apk file
5 | adb install ""
--------------------------------------------------------------------------------
/docs/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection } from 'astro:content';
2 | import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
3 |
4 | export const collections = {
5 | docs: defineCollection({ schema: docsSchema() }),
6 | i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/i18n/resources.ts:
--------------------------------------------------------------------------------
1 | import ar from '@/translations/ar.json';
2 | import en from '@/translations/en.json';
3 |
4 | export const resources = {
5 | en: {
6 | translation: en,
7 | },
8 | ar: {
9 | translation: ar,
10 | },
11 | };
12 |
13 | export type Language = keyof typeof resources;
14 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ios/ObytesApp.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/ios/ObytesApp/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "filename": "App-Icon-1024x1024@1x.png",
5 | "idiom": "universal",
6 | "platform": "ios",
7 | "size": "1024x1024"
8 | }
9 | ],
10 | "info": {
11 | "version": 1,
12 | "author": "expo"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/docs/src/content/docs/reviews.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: What people say about the starter
3 | description: What people say about the starter
4 | head:
5 | - tag: title
6 | content: Reviews | React Native / Expo Starter
7 | ---
8 |
9 | This is a list of reviews from people who have used the starter kit.
10 |
11 | Please feel free to add your in the comments section 👇
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 | yarn-error.log
13 | /coverage
14 | # macOS
15 | .DS_Store
16 |
17 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
18 | # The following patterns were generated by expo-cli
19 |
20 | expo-env.d.ts
21 | # @end expo-cli
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ObytesApp
3 | automatic
4 | contain
5 | false
6 |
--------------------------------------------------------------------------------
/src/components/ui/icons/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './arrow-right';
2 | export * from './caret-down';
3 | export * from './feed';
4 | export * from './github';
5 | export * from './home';
6 | export * from './language';
7 | export * from './rate';
8 | export * from './settings';
9 | export * from './share';
10 | export * from './style';
11 | export * from './support';
12 | export * from './website';
13 |
--------------------------------------------------------------------------------
/docs/src/content/docs/changelog.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: CHANGELOG
3 | description: New features, improvements, and bug fixes for the React Native / Expo Starter.
4 | head:
5 | - tag: title
6 | content: Obytes Starter ChangeLog | React Native / Expo Starter
7 | ---
8 |
9 | For complete changelog, please check the [GitHub releases](https://github.com/obytes/react-native-template-obytes/releases) page.
10 |
--------------------------------------------------------------------------------
/src/lib/auth/utils.tsx:
--------------------------------------------------------------------------------
1 | import { getItem, removeItem, setItem } from '@/lib/storage';
2 |
3 | const TOKEN = 'token';
4 |
5 | export type TokenType = {
6 | access: string;
7 | refresh: string;
8 | };
9 |
10 | export const getToken = () => getItem(TOKEN);
11 | export const removeToken = () => removeItem(TOKEN);
12 | export const setToken = (value: TokenType) => setItem(TOKEN, value);
13 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/ObytesApp/Supporting/Expo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | EXUpdatesCheckOnLaunch
6 | ALWAYS
7 | EXUpdatesEnabled
8 |
9 | EXUpdatesLaunchWaitMs
10 | 0
11 |
12 |
--------------------------------------------------------------------------------
/src/components/title.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Text, View } from '@/components/ui';
4 |
5 | type Props = {
6 | text: string;
7 | };
8 | export const Title = ({ text }: Props) => {
9 | return (
10 |
11 | {text}
12 |
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/.maestro/app/tabs.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | env:
3 | Name: 'User'
4 | EMAIL: 'user@test.com'
5 | PASSWORD: 'password'
6 | ---
7 | - launchApp
8 | - runFlow: ../utils/onboarding-and-login.yaml
9 | - assertVisible: 'Feed'
10 | - assertVisible:
11 | id: 'style-tab'
12 | - tapOn:
13 | id: 'style-tab'
14 | - assertVisible: 'Typography'
15 | - tapOn:
16 | id: 'settings-tab'
17 | - assertVisible: 'Settings'
18 | - scroll
19 | - assertVisible: 'Logout'
20 |
--------------------------------------------------------------------------------
/ios/.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 | project.xcworkspace
24 | .xcode.env.local
25 |
26 | # Bundle artifacts
27 | *.jsbundle
28 |
29 | # CocoaPods
30 | /Pods/
31 |
--------------------------------------------------------------------------------
/src/lib/hooks/use-is-first-time.tsx:
--------------------------------------------------------------------------------
1 | import { useMMKVBoolean } from 'react-native-mmkv';
2 |
3 | import { storage } from '../storage';
4 |
5 | const IS_FIRST_TIME = 'IS_FIRST_TIME';
6 |
7 | export const useIsFirstTime = () => {
8 | const [isFirstTime, setIsFirstTime] = useMMKVBoolean(IS_FIRST_TIME, storage);
9 | if (isFirstTime === undefined) {
10 | return [true, setIsFirstTime] as const;
11 | }
12 | return [isFirstTime, setIsFirstTime] as const;
13 | };
14 |
--------------------------------------------------------------------------------
/src/lib/storage.tsx:
--------------------------------------------------------------------------------
1 | import { MMKV } from 'react-native-mmkv';
2 |
3 | export const storage = new MMKV();
4 |
5 | export function getItem(key: string): T | null {
6 | const value = storage.getString(key);
7 | return value ? JSON.parse(value) || null : null;
8 | }
9 |
10 | export async function setItem(key: string, value: T) {
11 | storage.set(key, JSON.stringify(value));
12 | }
13 |
14 | export async function removeItem(key: string) {
15 | storage.delete(key);
16 | }
17 |
--------------------------------------------------------------------------------
/.husky/post-merge:
--------------------------------------------------------------------------------
1 |
2 |
3 | function changed {
4 | git diff --name-only HEAD@{1} HEAD | grep "^$1" >/dev/null 2>&1
5 | }
6 |
7 | echo 'Checking for changes in pnpm-lock.yml...'
8 |
9 | if changed 'pnpm-lock.yml'; then
10 | echo "📦 pnpm-lock.yml changed. Run pnpm install to bring your dependencies up to date."
11 | pnpm install
12 | fi
13 |
14 | echo 'You are up to date :)'
15 |
16 | echo 'If necessary, you can run pnpm prebuild to generate native code.'
17 |
18 | exit 0
19 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "esbenp.prettier-vscode",
5 | "yoavbls.pretty-ts-errors",
6 | "mikestead.dotenv",
7 | "eamodio.gitlens",
8 | "streetsidesoftware.code-spell-checker",
9 | "formulahendry.auto-close-tag",
10 | "formulahendry.auto-rename-tag",
11 | "bradlc.vscode-tailwindcss",
12 | "lokalise.i18n-ally",
13 | "wesbos.theme-cobalt2",
14 | "ChakrounAnas.turbo-console-log"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/ios/ObytesApp/Images.xcassets/SplashScreenBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors": [
3 | {
4 | "color": {
5 | "components": {
6 | "alpha": "1.000",
7 | "blue": "0.294117647058824",
8 | "green": "0.235294117647059",
9 | "red": "0.180392156862745"
10 | },
11 | "color-space": "srgb"
12 | },
13 | "idiom": "universal"
14 | }
15 | ],
16 | "info": {
17 | "version": 1,
18 | "author": "expo"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.maestro/utils/login.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | env:
3 | Name: "User"
4 | EMAIL: "user@test.com"
5 | PASSWORD: "password"
6 | tags:
7 | - util
8 | ---
9 | - tapOn:
10 | id: "name"
11 | - inputText: ${Name}
12 | - tapOn:
13 | id: "email-input"
14 | - inputText: ${EMAIL}
15 | - runFlow: ../utils/hide-keyboard.yaml
16 | - tapOn:
17 | id: "password-input"
18 | - inputText: ${PASSWORD}
19 | - runFlow: ../utils/hide-keyboard.yaml
20 | - tapOn:
21 | id: "login-button"
22 | - assertVisible: "Typography"
23 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('./src/components/ui/colors');
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | // NOTE: Update this to include the paths to all of your component files.
6 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
7 | presets: [require('nativewind/preset')],
8 | darkMode: 'class',
9 | theme: {
10 | extend: {
11 | fontFamily: {
12 | inter: ['Inter'],
13 | },
14 | colors,
15 | },
16 | },
17 | plugins: [],
18 | };
19 |
--------------------------------------------------------------------------------
/docs/ec.config.mjs:
--------------------------------------------------------------------------------
1 | import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections';
2 | import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers';
3 |
4 | /** @type {import('@astrojs/starlight/expressive-code').StarlightExpressiveCodeOptions} */
5 | export default {
6 | // Example: Using a custom plugin (which makes this `ec.config.mjs` file necessary)
7 | // plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
8 | // ... any other options you want to configure
9 | };
10 |
--------------------------------------------------------------------------------
/ios/ObytesApp/Images.xcassets/SplashScreenLogo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "universal",
5 | "filename": "image.png",
6 | "scale": "1x"
7 | },
8 | {
9 | "idiom": "universal",
10 | "filename": "image@2x.png",
11 | "scale": "2x"
12 | },
13 | {
14 | "idiom": "universal",
15 | "filename": "image@3x.png",
16 | "scale": "3x"
17 | }
18 | ],
19 | "info": {
20 | "version": 1,
21 | "author": "expo"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '**/*.{js,jsx,ts,tsx}': (filenames) => [
3 | `npx eslint --fix ${filenames
4 | .map((filename) => `"${filename}"`)
5 | .join(' ')}`,
6 | ],
7 | '**/*.(md|json)': (filenames) =>
8 | `npx prettier --write ${filenames
9 | .map((filename) => `"${filename}"`)
10 | .join(' ')}`,
11 | 'src/translations/*.(json)': (filenames) => [
12 | `npx eslint --fix ${filenames
13 | .map((filename) => `"${filename}"`)
14 | .join(' ')}`,
15 | ],
16 | };
17 |
--------------------------------------------------------------------------------
/src/api/posts/use-post.ts:
--------------------------------------------------------------------------------
1 | import type { AxiosError } from 'axios';
2 | import { createQuery } from 'react-query-kit';
3 |
4 | import { client } from '../common';
5 | import type { Post } from './types';
6 |
7 | type Variables = { id: string };
8 | type Response = Post;
9 |
10 | export const usePost = createQuery({
11 | queryKey: ['posts'],
12 | fetcher: (variables) => {
13 | return client
14 | .get(`posts/${variables.id}`)
15 | .then((response) => response.data);
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/api/common/api-provider.tsx:
--------------------------------------------------------------------------------
1 | import { useReactQueryDevTools } from '@dev-plugins/react-query';
2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3 | import * as React from 'react';
4 |
5 | export const queryClient = new QueryClient();
6 |
7 | export function APIProvider({ children }: { children: React.ReactNode }) {
8 | useReactQueryDevTools(queryClient);
9 | return (
10 | // Provide the client to your App
11 | {children}
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/ui/icons/caret-down.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { Path } from 'react-native-svg';
4 |
5 | export const CaretDown = ({ ...props }: SvgProps) => (
6 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/api/posts/use-add-post.ts:
--------------------------------------------------------------------------------
1 | import type { AxiosError } from 'axios';
2 | import { createMutation } from 'react-query-kit';
3 |
4 | import { client } from '../common';
5 | import type { Post } from './types';
6 |
7 | type Variables = { title: string; body: string; userId: number };
8 | type Response = Post;
9 |
10 | export const useAddPost = createMutation({
11 | mutationFn: async (variables) =>
12 | client({
13 | url: 'posts/add',
14 | method: 'POST',
15 | data: variables,
16 | }).then((response) => response.data),
17 | });
18 |
--------------------------------------------------------------------------------
/src/api/posts/use-posts.ts:
--------------------------------------------------------------------------------
1 | import type { AxiosError } from 'axios';
2 | import { createQuery } from 'react-query-kit';
3 |
4 | import { client } from '../common';
5 | import type { Post } from './types';
6 |
7 | type Response = Post[];
8 | type Variables = void; // as react-query-kit is strongly typed, we need to specify the type of the variables as void in case we don't need them
9 |
10 | export const usePosts = createQuery({
11 | queryKey: ['posts'],
12 | fetcher: () => {
13 | return client.get(`posts`).then((response) => response.data.posts);
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/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 | # react-native-reanimated
11 | -keep class com.swmansion.reanimated.** { *; }
12 | -keep class com.facebook.react.turbomodule.** { *; }
13 |
14 | # Add any project specific keep options here:
15 |
--------------------------------------------------------------------------------
/src/app/[...messing].tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from 'expo-router';
2 |
3 | import { Text, View } from '@/components/ui';
4 |
5 | export default function NotFoundScreen() {
6 | return (
7 | <>
8 |
9 |
10 |
11 | This screen doesn't exist.
12 |
13 |
14 |
15 | Go to home screen!
16 |
17 |
18 | >
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/settings/items-container.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text, View } from '@/components/ui';
4 | import type { TxKeyPath } from '@/lib';
5 |
6 | type Props = {
7 | children: React.ReactNode;
8 | title?: TxKeyPath;
9 | };
10 |
11 | export const ItemsContainer = ({ children, title }: Props) => {
12 | return (
13 | <>
14 | {title && }
15 | {
16 |
17 | {children}
18 |
19 | }
20 | >
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
11 |
--------------------------------------------------------------------------------
/src/lib/env.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file should not be modified; use `env.js` in the project root to add your client environment variables.
3 | * If you import `Env` from `@env`, this is the file that will be loaded.
4 | * You can only access the client environment variables here.
5 | * NOTE: We use js file so we can load the client env types
6 | */
7 |
8 | import Constants from 'expo-constants';
9 | /**
10 | * @type {typeof import('../../env.js').ClientEnv}
11 | */
12 | //@ts-ignore // Don't worry about TypeScript here; we know we're passing the correct environment variables to `extra` in `app.config.ts`.
13 | export const Env = Constants.expoConfig?.extra ?? {};
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["./src/*"],
8 | "@env": ["./src/lib/env.js"]
9 | },
10 | "esModuleInterop": true,
11 | "checkJs": true
12 | },
13 | "exclude": [
14 | "node_modules",
15 | "babel.config.js",
16 | "metro.config.js",
17 | "docs",
18 | "cli",
19 | "android",
20 | "ios",
21 | "lint-staged.config.js"
22 | ],
23 | "include": [
24 | "**/*.ts",
25 | "**/*.tsx",
26 | ".expo/types/**/*.ts",
27 | "expo-env.d.ts",
28 | "nativewind-env.d.ts"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## What does this do?
2 |
3 |
6 |
7 | ## Why did you do this?
8 |
9 |
12 |
13 | ## Who/what does this impact?
14 |
15 |
18 |
19 | ## How did you test this?
20 |
21 |
24 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 |
2 | . "$(dirname "$0")/common.sh"
3 |
4 |
5 | echo "===\n>> Checking branch name..."
6 |
7 | # Check if branch protection is enabled
8 | if [[ -z $SKIP_BRANCH_PROTECTION ]]; then
9 | BRANCH=$(git rev-parse --abbrev-ref HEAD)
10 | PROTECTED_BRANCHES="^(main|master)"
11 |
12 | if [[ $BRANCH =~ $PROTECTED_BRANCHES ]]; then
13 | echo ">> Direct commits to the $BRANCH branch are not allowed. Please choose a new branch name."
14 | exit 1
15 | fi
16 | else
17 | echo ">> Skipping branch protection."
18 | fi
19 |
20 | echo ">> Finish checking branch name"
21 | echo ">> Linting your files and fixing them if needed..."
22 |
23 | pnpm type-check
24 | pnpm lint-staged
--------------------------------------------------------------------------------
/docs/src/components/code.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Code as SCode } from '@astrojs/starlight/components'
3 |
4 | import fs from 'node:fs/promises';
5 |
6 | interface Props {
7 | file: string;
8 | language?: string;
9 | meta?: string;
10 | }
11 |
12 | const { file, language, meta } = Astro.props;
13 | const fileNamePath = '../' + file;
14 | const fileEtension = file.split('.').pop() ?? 'js';
15 | const code = await fs.readFile(fileNamePath, 'utf-8');
16 | const lang = language ?? fileEtension;
17 | const metaa = `title="${file}"` + (meta ? ` ${meta}` : '')
18 |
19 |
20 | ---
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/prompts/expo-doctor.md:
--------------------------------------------------------------------------------
1 | You are an expert in TypeScript, Expo, and React Native.
2 |
3 | You are given a React Native project and you are tasked with fixing the project dependencies.
4 |
5 | You should follow the following steps:
6 |
7 | 1. Run expo doctor command using `pnpm run doctor`
8 | 2. Analyze the check results and provide an explanation of what we need to do to fix the issues
9 | 3. Run commands to fix the issues in case there are any
10 | 4. Run expo doctor command again to check if the issues are fixed
11 | 5. If the issues is fixed, make sure to commit changes for package.json and pnpm-lock.yaml with the message `git add package.json pnpm-lock.yaml && git commit -m "fix(deps): expo doctor issues"`
12 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ossified-orbit",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/starlight": "^0.31.1",
14 | "@expressive-code/plugin-collapsible-sections": "^0.33.4",
15 | "@expressive-code/plugin-line-numbers": "^0.33.4",
16 | "@fontsource/ibm-plex-mono": "^5.0.8",
17 | "@fontsource/ibm-plex-serif": "^5.0.8",
18 | "astro": "^5.1.10",
19 | "hast-util-to-html": "^9.0.0",
20 | "sharp": "^0.32.3",
21 | "starlight-llms-txt": "^0.4.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/docs/src/components/LastUpdated.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Props } from '@astrojs/starlight/props';
3 | import Default from '@astrojs/starlight/components/LastUpdated.astro';
4 | import Comments from './Comments.astro';
5 |
6 | const { lastUpdated } = Astro.props;
7 |
8 | ---
9 |
10 | {
11 | lastUpdated && (
12 |
18 | )
19 | }
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/ui/icons/home.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { Path } from 'react-native-svg';
4 |
5 | export function Home({ color = '#000', ...props }: SvgProps) {
6 | return (
7 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ui/focus-aware-status-bar.tsx:
--------------------------------------------------------------------------------
1 | import { useIsFocused } from '@react-navigation/native';
2 | import { useColorScheme } from 'nativewind';
3 | import * as React from 'react';
4 | import { Platform } from 'react-native';
5 | import { SystemBars } from 'react-native-edge-to-edge';
6 |
7 | type Props = { hidden?: boolean };
8 | export const FocusAwareStatusBar = ({ hidden = false }: Props) => {
9 | const isFocused = useIsFocused();
10 | const { colorScheme } = useColorScheme();
11 |
12 | if (Platform.OS === 'web') return null;
13 |
14 | return isFocused ? (
15 |
19 | ) : null;
20 | };
21 |
--------------------------------------------------------------------------------
/src/app/(app)/style.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Buttons } from '@/components/buttons';
4 | import { Colors } from '@/components/colors';
5 | import { Inputs } from '@/components/inputs';
6 | import { Typography } from '@/components/typography';
7 | import { FocusAwareStatusBar, SafeAreaView, ScrollView } from '@/components/ui';
8 |
9 | export default function Style() {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | >
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { Linking } from 'react-native';
2 | import type { StoreApi, UseBoundStore } from 'zustand';
3 |
4 | export function openLinkInBrowser(url: string) {
5 | Linking.canOpenURL(url).then((canOpen) => canOpen && Linking.openURL(url));
6 | }
7 |
8 | type WithSelectors = S extends { getState: () => infer T }
9 | ? S & { use: { [K in keyof T]: () => T[K] } }
10 | : never;
11 |
12 | export const createSelectors = >>(
13 | _store: S
14 | ) => {
15 | let store = _store as WithSelectors;
16 | store.use = {};
17 | for (let k of Object.keys(store.getState())) {
18 | (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
19 | }
20 |
21 | return store;
22 | };
23 |
--------------------------------------------------------------------------------
/src/app/login.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'expo-router';
2 | import React from 'react';
3 |
4 | import type { LoginFormProps } from '@/components/login-form';
5 | import { LoginForm } from '@/components/login-form';
6 | import { FocusAwareStatusBar } from '@/components/ui';
7 | import { useAuth } from '@/lib';
8 |
9 | export default function Login() {
10 | const router = useRouter();
11 | const signIn = useAuth.use.signIn();
12 |
13 | const onSubmit: LoginFormProps['onSubmit'] = (data) => {
14 | console.log(data);
15 | signIn({ access: 'access-token', refresh: 'refresh-token' });
16 | router.push('/');
17 | };
18 | return (
19 | <>
20 |
21 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/ui/image.tsx:
--------------------------------------------------------------------------------
1 | import type { ImageProps } from 'expo-image';
2 | import { Image as NImage } from 'expo-image';
3 | import { cssInterop } from 'nativewind';
4 | import * as React from 'react';
5 |
6 | export type ImgProps = ImageProps & {
7 | className?: string;
8 | };
9 |
10 | cssInterop(NImage, { className: 'style' });
11 |
12 | export const Image = ({
13 | style,
14 | className,
15 | placeholder = 'L6PZfSi_.AyE_3t7t7R**0o#DgR4',
16 | ...props
17 | }: ImgProps) => {
18 | return (
19 |
25 | );
26 | };
27 |
28 | export const preloadImages = (sources: string[]) => {
29 | NImage.prefetch(sources);
30 | };
31 |
--------------------------------------------------------------------------------
/src/lib/i18n/types.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/infinitered/ignite/blob/master/boilerplate/app/i18n/i18n.ts
2 | export type RecursiveKeyOf = {
3 | [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<
4 | TObj[TKey],
5 | `${TKey}`
6 | >;
7 | }[keyof TObj & (string | number)];
8 |
9 | type RecursiveKeyOfInner = {
10 | [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<
11 | TObj[TKey],
12 | `['${TKey}']` | `.${TKey}`
13 | >;
14 | }[keyof TObj & (string | number)];
15 |
16 | type RecursiveKeyOfHandleValue<
17 | TValue,
18 | Text extends string,
19 | > = TValue extends any[]
20 | ? Text
21 | : TValue extends object
22 | ? Text | `${Text}${RecursiveKeyOfInner}`
23 | : Text;
24 |
--------------------------------------------------------------------------------
/scripts/i18next-syntax-validation.js:
--------------------------------------------------------------------------------
1 | const validate = (message = '') => {
2 | if (!(message || '').trim()) {
3 | throw new SyntaxError('Message is Empty.');
4 | }
5 | if (typeof message !== 'string') {
6 | throw new TypeError('Message must be a String.');
7 | }
8 | if (
9 | (message.includes('{') || message.includes('}')) &&
10 | !/{{ ?(?:- |\w+?)(, ?)?\w+? ?}}/g.test(message)
11 | ) {
12 | throw new SyntaxError(
13 | 'Interpolation error. See: https://www.i18next.com/misc/json-format'
14 | );
15 | }
16 | if (message.includes('$t(') && !/\$t\([\w]+:\w+(?:\.\w+)*\)/g.test(message)) {
17 | throw new SyntaxError(
18 | 'Nesting error. See: https://www.i18next.com/misc/json-format'
19 | );
20 | }
21 | };
22 |
23 | module.exports = validate;
24 |
--------------------------------------------------------------------------------
/src/components/ui/icons/feed.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { Path } from 'react-native-svg';
4 |
5 | export const Feed = ({ color = '#000', ...props }: SvgProps) => (
6 |
12 | );
13 |
--------------------------------------------------------------------------------
/docs/src/components/Comments.astro:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 |
5 |
6 |
12 |
13 |
14 |
29 |
30 |
--------------------------------------------------------------------------------
/docs/src/content/docs/guides/storage.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Storage
3 | description: Storage guide with mmkv library
4 | head:
5 | - tag: title
6 | content: Storage| React Native / Expo Starter
7 | ---
8 |
9 | import CodeBlock from '../../../components/code.astro';
10 |
11 | # Storage
12 |
13 | The starter comes with a simple storage module that uses [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) to store data in a key-value format. We also added a simple storage utility to assist you in using the storage module.
14 |
15 |
16 |
17 | The `react-native-mmkv` library provides various features such as using hooks and adding encryption to stored data. Feel free to check the [official docs](https://github.com/mrousavy/react-native-mmkv) for more information.
18 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: [
5 | ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
6 | 'nativewind/babel',
7 | ],
8 | plugins: [
9 | [
10 | 'module-resolver',
11 | {
12 | root: ['./'],
13 | alias: {
14 | '@': './src',
15 | '@env': './src/lib/env.js',
16 | },
17 | extensions: [
18 | '.ios.ts',
19 | '.android.ts',
20 | '.ts',
21 | '.ios.tsx',
22 | '.android.tsx',
23 | '.tsx',
24 | '.jsx',
25 | '.js',
26 | '.json',
27 | ],
28 | },
29 | ],
30 | 'react-native-reanimated/plugin',
31 | ],
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/ui/icons/arrow-right.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import type { SvgProps } from 'react-native-svg';
4 | import Svg, { Path } from 'react-native-svg';
5 |
6 | import { isRTL } from '@/lib';
7 |
8 | export const ArrowRight = ({ color = '#CCC', style, ...props }: SvgProps) => (
9 |
25 | );
26 |
--------------------------------------------------------------------------------
/src/translations/ar.json:
--------------------------------------------------------------------------------
1 | {
2 | "onboarding": {
3 | "message": "مرحبا بكم في موقع تطبيق obytes"
4 | },
5 | "settings": {
6 | "about": "حول التطبيق ",
7 | "app_name": "اسم التطبيق",
8 | "arabic": "عربي",
9 | "english": "إنجليزي",
10 | "generale": "عام",
11 | "github": "جيثب",
12 | "language": "لغة",
13 | "links": "الروابط",
14 | "logout": "تسجيل خروج",
15 | "more": "أكثر",
16 | "privacy": "سياسة الخصوصية",
17 | "rate": "تقييم",
18 | "share": "شارك",
19 | "support": "الدعم",
20 | "support_us": "ادعمنا",
21 | "terms": "شروط الخدمة",
22 | "theme": {
23 | "dark": "مظلم",
24 | "light": "خفيفة",
25 | "system": "System",
26 | "title": "سمة"
27 | },
28 | "title": "إعدادات",
29 | "version": "إصدار",
30 | "website": "موقع الكتروني"
31 | },
32 | "welcome": "test arabic"
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Mark stale issues and pull requests
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *'
6 |
7 | permissions:
8 | contents: read
9 | pull-requests: write
10 |
11 | jobs:
12 | stale:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/stale@v1
17 | with:
18 | repo-token: ${{ secrets.GITHUB_TOKEN }}
19 | stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 14 days'
20 | stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 14 days'
21 | stale-issue-label: 'no-issue-activity'
22 | stale-pr-label: 'no-pr-activity'
23 | days-before-stale: 60
24 | days-before-close: 14
25 |
--------------------------------------------------------------------------------
/src/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "onboarding": {
3 | "message": "Welcome to obytes app site"
4 | },
5 | "settings": {
6 | "about": "About",
7 | "app_name": "App Name",
8 | "arabic": "Arabic",
9 | "english": "English",
10 | "generale": "General",
11 | "github": "Github",
12 | "language": "Language",
13 | "links": "Links",
14 | "logout": "Logout",
15 | "more": "More",
16 | "privacy": "Privacy Policy",
17 | "rate": "Rate",
18 | "share": "Share",
19 | "support": "Support",
20 | "support_us": "Support Us",
21 | "terms": "Terms of Service",
22 | "theme": {
23 | "dark": "Dark",
24 | "light": "Light",
25 | "system": "System",
26 | "title": "Theme"
27 | },
28 | "title": "Settings",
29 | "version": "Version",
30 | "website": "Website"
31 | },
32 | "welcome": "Welcome to obytes app site"
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ui/index.tsx:
--------------------------------------------------------------------------------
1 | import { cssInterop } from 'nativewind';
2 | import Svg from 'react-native-svg';
3 |
4 | export * from './button';
5 | export * from './checkbox';
6 | export { default as colors } from './colors';
7 | export * from './focus-aware-status-bar';
8 | export * from './image';
9 | export * from './input';
10 | export * from './list';
11 | export * from './modal';
12 | export * from './progress-bar';
13 | export * from './select';
14 | export * from './text';
15 | export * from './utils';
16 |
17 | // export base components from react-native
18 | export {
19 | ActivityIndicator,
20 | Pressable,
21 | ScrollView,
22 | TouchableOpacity,
23 | View,
24 | } from 'react-native';
25 | export { SafeAreaView } from 'react-native-safe-area-context';
26 |
27 | //Apply cssInterop to Svg to resolve className string into style
28 | cssInterop(Svg, {
29 | className: {
30 | target: 'style',
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/.github/scripts/expo-doctor.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Run expo-doctor and capture output and exit code
4 | output=$(npx expo-doctor@latest 2>&1)
5 | exit_code=$?
6 |
7 | # Output file location
8 | output_file=".expo/expo-doctor.md"
9 | {
10 | # Add summary based on exit code
11 | if [ $exit_code -eq 0 ]; then
12 | echo "✅ **Good news!** We ran Expo Doctor for this PR and everything looks good, Great job!" > "$output_file"
13 | else
14 | echo "❌ **Action Required:** We ran Expo Doctor for this PR and found some issues that need to be addressed. Please review the complete report below 👇" > "$output_file"
15 | echo >> "$output_file" # Add blank line
16 | echo "\`\`\`shell" >> "$output_file"
17 | echo "$output" >> "$output_file"
18 | echo "\`\`\`" >> "$output_file"
19 | fi
20 | }
21 |
22 | # Show original output in terminal
23 | echo "$output"
24 |
25 | # Return the original exit code
26 | exit $exit_code
27 |
--------------------------------------------------------------------------------
/.maestro/auth/login-with-validation.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | env:
3 | Name: 'User'
4 | EMAIL: 'user@test.com'
5 | PASSWORD: 'password'
6 | ---
7 | - launchApp
8 | - runFlow:
9 | when:
10 | visible: 'Obytes Starter'
11 | file: ../utils/onboarding.yaml
12 | - assertVisible: 'Sign In'
13 | - assertVisible:
14 | id: 'login-button'
15 | - tapOn:
16 | id: 'login-button'
17 | - assertVisible: 'Email is required'
18 | - assertVisible: 'Password is required'
19 | - tapOn:
20 | id: 'name'
21 | - inputText: ${Name}
22 | - runFlow: ../utils/hide-keyboard.yaml
23 | - tapOn:
24 | id: 'email-input'
25 | - inputText: 'email'
26 | - assertVisible: 'Invalid email format'
27 | - inputText: ${EMAIL}
28 | - runFlow: ../utils/hide-keyboard.yaml
29 | - tapOn:
30 | id: 'password-input'
31 | - inputText: ${PASSWORD}
32 | - runFlow: ../utils/hide-keyboard.yaml
33 | - tapOn:
34 | id: 'login-button'
35 | - assertVisible: 'Feed'
36 |
--------------------------------------------------------------------------------
/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-obytes-app",
3 | "version": "1.7.1",
4 | "description": "Obytes expo starter cli",
5 | "homepage": "https://github.com/obytes/react-native-template-obytes",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/obytes/react-native-template-obytes.git"
9 | },
10 | "main": "index.js",
11 | "scripts": {
12 | "start": "node ."
13 | },
14 | "bin": {
15 | "create-obytes-app": "index.js"
16 | },
17 | "files": [
18 | "index.js",
19 | "utils.js",
20 | "clone-repo.js",
21 | "setup-project.js"
22 | ],
23 | "dependencies": {
24 | "consola": "^3.2.3",
25 | "fs-extra": "^10.1.0"
26 | },
27 | "keywords": [
28 | "react-native",
29 | "expo",
30 | "template",
31 | "react-native-starter",
32 | "expo-starter",
33 | "react-native-boilerplate",
34 | "expo-boilerplate",
35 | "cli",
36 | "obytes"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/ui/icons/website.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg';
4 |
5 | import colors from '../colors';
6 |
7 | export const Website = ({
8 | color = colors.neutral[500],
9 | ...props
10 | }: SvgProps) => (
11 |
28 | );
29 |
--------------------------------------------------------------------------------
/.maestro/app/create-post.yaml:
--------------------------------------------------------------------------------
1 | appId: ${APP_ID}
2 | env:
3 | Title: 'Post title'
4 | CONTENT:
5 | "It is a long established fact that a reader will be distracted by the\
6 | \ readable content of a page when looking at its layout. The point of using Lorem\
7 | \ Ipsum is that it has a more-or-less normal distribution of letters, as opposed\
8 | \ to using"
9 | ---
10 | - launchApp
11 | - runFlow: ../utils/onboarding-and-login.yaml
12 | - assertVisible: 'Feed'
13 | - assertVisible: 'Create'
14 | - tapOn: 'Create'
15 | - assertVisible: 'Add Post'
16 | - tapOn:
17 | id: 'title'
18 | - inputText: ${Title}
19 | - tapOn:
20 | id: 'body-input'
21 | - inputText: 'short content'
22 | - tapOn:
23 | id: 'add-post-button'
24 | - assertVisible: 'String must contain at least 120 character(s)'
25 | - inputText: ${CONTENT}
26 | - runFlow: ../utils/hide-keyboard.yaml
27 | - tapOn:
28 | id: 'add-post-button'
29 | - assertVisible: 'Post added successfully'
30 |
--------------------------------------------------------------------------------
/src/components/ui/icons/share.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { Path } from 'react-native-svg';
4 |
5 | import colors from '../colors';
6 |
7 | export const Share = ({ color = colors.neutral[500], ...props }: SvgProps) => (
8 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/components/ui/icons/style.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg';
4 |
5 | export const Style = ({ color, ...props }: SvgProps) => (
6 |
22 | );
23 |
--------------------------------------------------------------------------------
/src/lib/use-theme-config.tsx:
--------------------------------------------------------------------------------
1 | import type { Theme } from '@react-navigation/native';
2 | import {
3 | DarkTheme as _DarkTheme,
4 | DefaultTheme,
5 | } from '@react-navigation/native';
6 | import { useColorScheme } from 'nativewind';
7 |
8 | import colors from '@/components/ui/colors';
9 |
10 | const DarkTheme: Theme = {
11 | ..._DarkTheme,
12 | colors: {
13 | ..._DarkTheme.colors,
14 | primary: colors.primary[200],
15 | background: colors.charcoal[950],
16 | text: colors.charcoal[100],
17 | border: colors.charcoal[500],
18 | card: colors.charcoal[850],
19 | },
20 | };
21 |
22 | const LightTheme: Theme = {
23 | ...DefaultTheme,
24 | colors: {
25 | ...DefaultTheme.colors,
26 | primary: colors.primary[400],
27 | background: colors.white,
28 | },
29 | };
30 |
31 | export function useThemeConfig() {
32 | const { colorScheme } = useColorScheme();
33 |
34 | if (colorScheme === 'dark') return DarkTheme;
35 |
36 | return LightTheme;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ui/icons/support.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg';
4 |
5 | import colors from '../colors';
6 |
7 | export const Support = ({
8 | color = colors.neutral[500],
9 | ...props
10 | }: SvgProps) => (
11 |
27 | );
28 |
--------------------------------------------------------------------------------
/src/components/typography.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text, View } from '@/components/ui';
4 |
5 | import { Title } from './title';
6 |
7 | export const Typography = () => {
8 | return (
9 | <>
10 |
11 |
12 |
13 | H1: Lorem ipsum dolor sit
14 |
15 | H2: Lorem ipsum dolor sit
16 | H3: Lorem ipsum dolor sit
17 | H4: Lorem ipsum dolor sit
18 |
19 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Cumque quasi
20 | aut, expedita tempore ratione quidem in, corporis quia minus et
21 | dolorem sunt temporibus iusto consequatur culpa. Omnis sequi debitis
22 | recusandae?
23 |
24 |
25 | >
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/lib/i18n/index.tsx:
--------------------------------------------------------------------------------
1 | import { locale } from 'expo-localization';
2 | import i18n from 'i18next';
3 | import { initReactI18next } from 'react-i18next';
4 | import { I18nManager } from 'react-native';
5 |
6 | import { resources } from './resources';
7 | import { getLanguage } from './utils';
8 | export * from './utils';
9 |
10 | i18n.use(initReactI18next).init({
11 | resources,
12 | lng: getLanguage() || locale, // TODO: if you are not supporting multiple languages or languages with multiple directions you can set the default value to `en`
13 | fallbackLng: 'en',
14 | compatibilityJSON: 'v3', // By default React Native projects does not support Intl
15 |
16 | // allows integrating dynamic values into translations.
17 | interpolation: {
18 | escapeValue: false, // escape passed in values to avoid XSS injections
19 | },
20 | });
21 |
22 | // Is it a RTL language?
23 | export const isRTL: boolean = i18n.dir() === 'rtl';
24 |
25 | I18nManager.allowRTL(isRTL);
26 | I18nManager.forceRTL(isRTL);
27 |
28 | export default i18n;
29 |
--------------------------------------------------------------------------------
/src/app/(app)/index.tsx:
--------------------------------------------------------------------------------
1 | import { FlashList } from '@shopify/flash-list';
2 | import React from 'react';
3 |
4 | import type { Post } from '@/api';
5 | import { usePosts } from '@/api';
6 | import { Card } from '@/components/card';
7 | import { EmptyList, FocusAwareStatusBar, Text, View } from '@/components/ui';
8 |
9 | export default function Feed() {
10 | const { data, isPending, isError } = usePosts();
11 | const renderItem = React.useCallback(
12 | ({ item }: { item: Post }) => ,
13 | []
14 | );
15 |
16 | if (isError) {
17 | return (
18 |
19 | Error Loading data
20 |
21 | );
22 | }
23 | return (
24 |
25 |
26 | `item-${index}`}
30 | ListEmptyComponent={}
31 | estimatedItemSize={300}
32 | />
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 | dependencies {
9 | classpath('com.android.tools.build:gradle')
10 | classpath('com.facebook.react:react-native-gradle-plugin')
11 | classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
12 | }
13 | }
14 |
15 | def reactNativeAndroidDir = new File(
16 | providers.exec {
17 | workingDir(rootDir)
18 | commandLine("node", "--print", "require.resolve('react-native/package.json')")
19 | }.standardOutput.asText.get().trim(),
20 | "../android"
21 | )
22 |
23 | allprojects {
24 | repositories {
25 | maven {
26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
27 | url(reactNativeAndroidDir)
28 | }
29 |
30 | google()
31 | mavenCentral()
32 | maven { url 'https://www.jitpack.io' }
33 | }
34 | }
35 |
36 | apply plugin: "expo-root-project"
37 | apply plugin: "com.facebook.react.rootproject"
38 |
--------------------------------------------------------------------------------
/.github/workflows/new-github-release.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/new-github-release.yml
3 | # Starter releasing process: https://starter.obytes.com/ci-cd/app-releasing-process/
4 |
5 | # ✍️ Description:
6 | # This workflow will be triggered automatically after the new app version workflow has been executed successfully.
7 | # It will create a new GitHub release with the new app version and the release notes.
8 |
9 | # 🚨 GITHUB SECRETS REQUIRED: None
10 |
11 | name: New GitHub Release
12 |
13 | on:
14 | push:
15 | tags:
16 | - '*'
17 |
18 | jobs:
19 | release:
20 | name: New GitHub Release
21 | runs-on: ubuntu-latest
22 | permissions:
23 | contents: write
24 | steps:
25 | - name: 📦 Checkout project repo
26 | uses: actions/checkout@v4
27 | with:
28 | fetch-depth: 0
29 |
30 | - name: 🏃♂️Create A Draft Github Release
31 | uses: ncipollo/release-action@v1
32 | with:
33 | generateReleaseNotes: true
34 | draft: false
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Obytes
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 |
--------------------------------------------------------------------------------
/src/components/ui/icons/rate.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg';
4 |
5 | import colors from '../colors';
6 |
7 | export const Rate = ({ color = colors.neutral[500], ...props }: SvgProps) => (
8 |
24 | );
25 |
--------------------------------------------------------------------------------
/src/components/settings/item.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Pressable, Text, View } from '@/components/ui';
4 | import { ArrowRight } from '@/components/ui/icons';
5 | import type { TxKeyPath } from '@/lib';
6 |
7 | type ItemProps = {
8 | text: TxKeyPath;
9 | value?: string;
10 | onPress?: () => void;
11 | icon?: React.ReactNode;
12 | };
13 |
14 | export const Item = ({ text, value, icon, onPress }: ItemProps) => {
15 | const isPressable = onPress !== undefined;
16 | return (
17 |
22 |
23 | {icon && {icon}}
24 |
25 |
26 |
27 | {value}
28 | {isPressable && (
29 |
30 |
31 |
32 | )}
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/ui/text.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { TextProps, TextStyle } from 'react-native';
3 | import { I18nManager, StyleSheet, Text as NNText } from 'react-native';
4 | import { twMerge } from 'tailwind-merge';
5 |
6 | import type { TxKeyPath } from '@/lib/i18n';
7 | import { translate } from '@/lib/i18n';
8 |
9 | interface Props extends TextProps {
10 | className?: string;
11 | tx?: TxKeyPath;
12 | }
13 |
14 | export const Text = ({
15 | className = '',
16 | style,
17 | tx,
18 | children,
19 | ...props
20 | }: Props) => {
21 | const textStyle = React.useMemo(
22 | () =>
23 | twMerge(
24 | 'text-base text-black dark:text-white font-inter font-normal',
25 | className
26 | ),
27 | [className]
28 | );
29 |
30 | const nStyle = React.useMemo(
31 | () =>
32 | StyleSheet.flatten([
33 | {
34 | writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
35 | },
36 | style,
37 | ]) as TextStyle,
38 | [style]
39 | );
40 | return (
41 |
42 | {tx ? translate(tx) : children}
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/.github/actions/setup-node-pnpm-install/action.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/actions/setup-node-pnpm-install/action.yml
3 | # Composite actions docs: https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
4 |
5 | # ✍️ Description:
6 | # This is a composite action, which means it can be used in other actions.
7 | # It is used in almost all workflows to set up the environment and install dependencies.
8 | # Updating the package manager or Node version here will be reflected in all workflows.
9 |
10 | # 👀 Example usage:
11 | # - name : 📦 Setup Node + PNPM + install deps
12 | # uses: ./.github/actions/setup-node-pnpm-install
13 |
14 | name: 'Setup Node + PNPM + Install Dependencies'
15 | description: 'Setup Node + PNPM + Install Dependencies'
16 | runs:
17 | using: 'composite'
18 | steps:
19 | - uses: pnpm/action-setup@v4
20 | with:
21 | run_install: false
22 | - uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 | cache: 'pnpm'
26 |
27 | - name: 📦 Install Project Dependencies
28 | run: pnpm install --frozen-lockfile
29 | shell: bash
30 |
--------------------------------------------------------------------------------
/src/components/ui/icons/github.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg';
4 |
5 | import colors from '../colors';
6 |
7 | export const Github = ({ color = colors.neutral[500], ...props }: SvgProps) => (
8 |
25 | );
26 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "editor.detectIndentation": false,
4 | "search.exclude": {
5 | "yarn.lock": true
6 | },
7 | "editor.defaultFormatter": "esbenp.prettier-vscode",
8 | "editor.formatOnSave": true,
9 | "typescript.tsdk": "node_modules/typescript/lib",
10 | "eslint.format.enable": true,
11 | "[javascript][typescript][typescriptreact]": {
12 | "editor.formatOnSave": true,
13 | "editor.defaultFormatter": "dbaeumer.vscode-eslint",
14 | "editor.codeActionsOnSave": [
15 | "source.addMissingImports",
16 | "source.fixAll.eslint"
17 | ]
18 | },
19 | "[json][jsonc]": {
20 | "editor.formatOnSave": true,
21 | "editor.defaultFormatter": "esbenp.prettier-vscode"
22 | },
23 | "[astro]": {
24 | "editor.formatOnSave": true,
25 | "editor.defaultFormatter": "astro-build.astro-vscode"
26 | },
27 | "cSpell.words": ["Flashlist", "Lato"],
28 | "i18n-ally.localesPaths": ["src/translations/"],
29 | "i18n-ally.keystyle": "nested",
30 | "i18n-ally.disabled": false, // make sure to disable i18n-ally in your global setting and only enable it for such projects
31 | "tailwindCSS.experimental.classRegex": [
32 | ["tv\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/cli/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { consola } = require('consola');
4 | const { showMoreDetails } = require('./utils.js');
5 | const { cloneLastTemplateRelease } = require('./clone-repo.js');
6 | const { setupProject, installDeps } = require('./setup-project.js');
7 |
8 | const createObytesApp = async () => {
9 | consola.box('Obytes Starter\nPerfect React Native App Kickstart 🚀!');
10 | // get project name from command line
11 | const projectName = process.argv[2];
12 | // check if project name is provided
13 | if (!projectName) {
14 | consola.error(
15 | 'Please provide a name for your project: `npx create-obytes-app@latest `'
16 | );
17 | process.exit(1);
18 | }
19 | // clone the last release of the template from github
20 | await cloneLastTemplateRelease(projectName);
21 |
22 | // setup the project: remove unnecessary files, update package.json infos, name and set version to 0.0.1 + add initial version to osMetadata
23 | await setupProject(projectName);
24 |
25 | // install project dependencies using pnpm
26 | await installDeps(projectName);
27 |
28 | // show instructions to run the project + link to the documentation
29 | showMoreDetails(projectName);
30 | };
31 |
32 | createObytesApp();
33 |
--------------------------------------------------------------------------------
/src/components/ui/icons/settings.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { Path } from 'react-native-svg';
4 |
5 | export const Settings = ({ color = '#000', ...props }: SvgProps) => {
6 | return (
7 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/cli/clone-repo.js:
--------------------------------------------------------------------------------
1 | const { runCommand } = require('./utils.js');
2 | const { consola } = require('consola');
3 |
4 | const getLatestRelease = async () => {
5 | try {
6 | const repoData = await fetch(
7 | 'https://api.github.com/repos/obytes/react-native-template-obytes/releases/latest'
8 | );
9 | const releaseData = await repoData.json();
10 | return releaseData.tag_name || 'master';
11 | } catch (error) {
12 | console.warn(
13 | 'Failed to retrieve the latest release; will use the master branch instead'
14 | );
15 | return 'master';
16 | }
17 | };
18 |
19 | const cloneLastTemplateRelease = async (projectName) => {
20 | consola.start('Extracting last release number 👀');
21 | const latest_release = await getLatestRelease();
22 | consola.info(`Using Obytes starter ${latest_release}`);
23 |
24 | // create a new project based on obytes template
25 | const cloneStarter = `git clone -b ${latest_release} --depth=1 https://github.com/obytes/react-native-template-obytes.git ${projectName}`;
26 | await runCommand(cloneStarter, {
27 | loading: 'Extracting the starter template...',
28 | success: 'Starter extracted successfully',
29 | error: 'Failed to download and extract template',
30 | });
31 | };
32 |
33 | module.exports = {
34 | cloneLastTemplateRelease,
35 | };
36 |
--------------------------------------------------------------------------------
/cli/utils.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const { exec } = require('child_process');
3 | const { consola } = require('consola');
4 |
5 | const execShellCommand = (cmd) => {
6 | return new Promise((resolve, reject) => {
7 | exec(cmd, (error, stdout, stderr) => {
8 | if (error) {
9 | console.warn(error);
10 | reject(error);
11 | }
12 | resolve(stdout ? stdout : stderr);
13 | });
14 | });
15 | };
16 |
17 | const runCommand = async (
18 | command,
19 | { loading = 'loading ....', success = 'success', error = 'error' }
20 | ) => {
21 | consola.start(loading);
22 | try {
23 | await execShellCommand(command);
24 | consola.success(success);
25 | } catch (err) {
26 | consola.error(`Failed to execute ${command}`, err);
27 | process.exit(1);
28 | }
29 | };
30 | // show more details message using chalk
31 | const showMoreDetails = (projectName) => {
32 | consola.box(
33 | 'Your project is ready to go! \n\n\n',
34 | '🚀 To get started, run the following commands: \n\n',
35 | ` \`cd ${projectName}\` \n`,
36 | ' IOS : `pnpm ios` \n',
37 | ' Android : `pnpm android` \n\n',
38 | '📚 Starter Documentation: https://starter.obytes.com'
39 | );
40 | };
41 |
42 | module.exports = {
43 | runCommand,
44 | showMoreDetails,
45 | execShellCommand,
46 | };
47 |
--------------------------------------------------------------------------------
/src/lib/test-utils.tsx:
--------------------------------------------------------------------------------
1 | import '@shopify/flash-list/jestSetup';
2 |
3 | import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
4 | import { NavigationContainer } from '@react-navigation/native';
5 | import type { RenderOptions } from '@testing-library/react-native';
6 | import { render, userEvent } from '@testing-library/react-native';
7 | import type { ReactElement } from 'react';
8 | import React from 'react';
9 |
10 | const createAppWrapper = () => {
11 | return ({ children }: { children: React.ReactNode }) => (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | const customRender = (
19 | ui: ReactElement,
20 | options?: Omit
21 | ) => {
22 | const Wrapper = createAppWrapper(); // make sure we have a new wrapper for each render
23 | return render(ui, { wrapper: Wrapper, ...options });
24 | };
25 |
26 | // use this if you want to test user events
27 | export const setup = (
28 | ui: ReactElement,
29 | options?: Omit
30 | ) => {
31 | const Wrapper = createAppWrapper();
32 | return {
33 | user: userEvent.setup(),
34 | ...render(ui, { wrapper: Wrapper, ...options }),
35 | };
36 | };
37 |
38 | export * from '@testing-library/react-native';
39 | export { customRender as render };
40 |
--------------------------------------------------------------------------------
/src/components/ui/progress-bar.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useImperativeHandle } from 'react';
2 | import { View } from 'react-native';
3 | import Animated, {
4 | Easing,
5 | useAnimatedStyle,
6 | useSharedValue,
7 | withTiming,
8 | } from 'react-native-reanimated';
9 | import { twMerge } from 'tailwind-merge';
10 |
11 | type Props = {
12 | initialProgress?: number;
13 | className?: string;
14 | };
15 |
16 | export type ProgressBarRef = {
17 | setProgress: (value: number) => void;
18 | };
19 |
20 | export const ProgressBar = forwardRef(
21 | ({ initialProgress = 0, className = '' }, ref) => {
22 | const progress = useSharedValue(initialProgress ?? 0);
23 | useImperativeHandle(ref, () => {
24 | return {
25 | setProgress: (value: number) => {
26 | progress.value = withTiming(value, {
27 | duration: 250,
28 | easing: Easing.inOut(Easing.quad),
29 | });
30 | },
31 | };
32 | }, [progress]);
33 |
34 | const style = useAnimatedStyle(() => {
35 | return {
36 | width: `${progress.value}%`,
37 | backgroundColor: '#000',
38 | height: 2,
39 | };
40 | });
41 | return (
42 |
43 |
44 |
45 | );
46 | }
47 | );
48 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'jest-expo',
3 | setupFilesAfterEnv: ['/jest-setup.ts'],
4 | testMatch: ['**/?(*.)+(spec|test).ts?(x)'],
5 | collectCoverageFrom: [
6 | 'src/**/*.{ts,tsx}',
7 | '!**/coverage/**',
8 | '!**/node_modules/**',
9 | '!**/babel.config.js',
10 | '!**/jest.setup.js',
11 | '!**/docs/**',
12 | '!**/cli/**',
13 | ],
14 | moduleFileExtensions: ['js', 'ts', 'tsx'],
15 | transformIgnorePatterns: [
16 | `node_modules/(?!(?:.pnpm/)?((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|@sentry/.*|native-base|react-native-svg))`,
17 | ],
18 | coverageReporters: ['json-summary', ['text', { file: 'coverage.txt' }]],
19 | reporters: [
20 | 'default',
21 | ['github-actions', { silent: false }],
22 | 'summary',
23 | [
24 | 'jest-junit',
25 | {
26 | outputDirectory: 'coverage',
27 | outputName: 'jest-junit.xml',
28 | ancestorSeparator: ' › ',
29 | uniqueOutputName: 'false',
30 | suiteNameTemplate: '{filepath}',
31 | classNameTemplate: '{classname}',
32 | titleTemplate: '{title}',
33 | },
34 | ],
35 | ],
36 | coverageDirectory: '/coverage/',
37 | moduleNameMapper: {
38 | '^@/(.*)$': '/src/$1',
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/app/feed/[id].tsx:
--------------------------------------------------------------------------------
1 | import { Stack, useLocalSearchParams } from 'expo-router';
2 | import * as React from 'react';
3 |
4 | import { usePost } from '@/api';
5 | import {
6 | ActivityIndicator,
7 | FocusAwareStatusBar,
8 | Text,
9 | View,
10 | } from '@/components/ui';
11 |
12 | export default function Post() {
13 | const local = useLocalSearchParams<{ id: string }>();
14 |
15 | const { data, isPending, isError } = usePost({
16 | //@ts-ignore
17 | variables: { id: local.id },
18 | });
19 |
20 | if (isPending) {
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 | if (isError) {
30 | return (
31 |
32 |
33 |
34 | Error loading post
35 |
36 | );
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 | {data.title}
44 | {data.body}
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/prompts/svg-icon.md:
--------------------------------------------------------------------------------
1 | You are an expert in TypeScript, Expo, nativeWind and React Native
2 |
3 | You are given an svg icon as string file or url and you are tasked with creating a react native component for it.
4 |
5 | You should follow the following steps:
6 |
7 | 1. Analyze the svg icon and create a react native component for it
8 | 2. The component should be named after the svg file or the user will provide the name
9 | 3. The component should be placed in the src/components/ui/icons folder
10 | 4. The component should be exported in the src/components/ui/icons/index.ts file
11 |
12 | Here is an example of how to create a react native component for an svg icon:
13 |
14 | ```tsx
15 | import * as React from 'react';
16 | import Svg, { Path, type SvgProps } from 'react-native-svg';
17 |
18 | export function ArrowLeft({
19 | color = 'white',
20 | size = 24,
21 | ...props
22 | }: SvgProps & { size?: number }) {
23 | return (
24 |
40 | );
41 | }
42 | ```
43 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def reactNativeGradlePlugin = new File(
3 | providers.exec {
4 | workingDir(rootDir)
5 | commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
6 | }.standardOutput.asText.get().trim()
7 | ).getParentFile().absolutePath
8 | includeBuild(reactNativeGradlePlugin)
9 |
10 | def expoPluginsPath = new File(
11 | providers.exec {
12 | workingDir(rootDir)
13 | commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
14 | }.standardOutput.asText.get().trim(),
15 | "../android/expo-gradle-plugin"
16 | ).absolutePath
17 | includeBuild(expoPluginsPath)
18 | }
19 |
20 | plugins {
21 | id("com.facebook.react.settings")
22 | id("expo-autolinking-settings")
23 | }
24 |
25 | extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
26 | if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
27 | ex.autolinkLibrariesFromCommand()
28 | } else {
29 | ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
30 | }
31 | }
32 | expoAutolinking.useExpoModules()
33 |
34 | rootProject.name = 'ObytesApp'
35 |
36 | expoAutolinking.useExpoVersionCatalog()
37 |
38 | include ':app'
39 | includeBuild(expoAutolinking.reactNativeGradlePlugin)
40 |
--------------------------------------------------------------------------------
/src/lib/auth/index.tsx:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import { createSelectors } from '../utils';
4 | import type { TokenType } from './utils';
5 | import { getToken, removeToken, setToken } from './utils';
6 |
7 | interface AuthState {
8 | token: TokenType | null;
9 | status: 'idle' | 'signOut' | 'signIn';
10 | signIn: (data: TokenType) => void;
11 | signOut: () => void;
12 | hydrate: () => void;
13 | }
14 |
15 | const _useAuth = create((set, get) => ({
16 | status: 'idle',
17 | token: null,
18 | signIn: (token) => {
19 | setToken(token);
20 | set({ status: 'signIn', token });
21 | },
22 | signOut: () => {
23 | removeToken();
24 | set({ status: 'signOut', token: null });
25 | },
26 | hydrate: () => {
27 | try {
28 | const userToken = getToken();
29 | if (userToken !== null) {
30 | get().signIn(userToken);
31 | } else {
32 | get().signOut();
33 | }
34 | } catch (e) {
35 | // only to remove eslint error, handle the error properly
36 | console.error(e);
37 | // catch error here
38 | // Maybe sign_out user!
39 | }
40 | },
41 | }));
42 |
43 | export const useAuth = createSelectors(_useAuth);
44 |
45 | export const signOut = () => _useAuth.getState().signOut();
46 | export const signIn = (token: TokenType) => _useAuth.getState().signIn(token);
47 | export const hydrateAuth = () => _useAuth.getState().hydrate();
48 |
--------------------------------------------------------------------------------
/src/components/settings/language-item.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import type { OptionType } from '@/components/ui';
4 | import { Options, useModal } from '@/components/ui';
5 | import { useSelectedLanguage } from '@/lib';
6 | import { translate } from '@/lib';
7 | import type { Language } from '@/lib/i18n/resources';
8 |
9 | import { Item } from './item';
10 |
11 | export const LanguageItem = () => {
12 | const { language, setLanguage } = useSelectedLanguage();
13 | const modal = useModal();
14 | const onSelect = React.useCallback(
15 | (option: OptionType) => {
16 | setLanguage(option.value as Language);
17 | modal.dismiss();
18 | },
19 | [setLanguage, modal]
20 | );
21 |
22 | const langs = React.useMemo(
23 | () => [
24 | { label: translate('settings.english'), value: 'en' },
25 | { label: translate('settings.arabic'), value: 'ar' },
26 | ],
27 | []
28 | );
29 |
30 | const selectedLanguage = React.useMemo(
31 | () => langs.find((lang) => lang.value === language),
32 | [language, langs]
33 | );
34 |
35 | return (
36 | <>
37 |
42 |
48 | >
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/colors.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Text, View } from '@/components/ui';
4 | import colors from '@/components/ui/colors';
5 |
6 | import { Title } from './title';
7 | type ColorName = keyof typeof colors;
8 |
9 | export const Colors = () => {
10 | return (
11 | <>
12 |
13 | {(Object.keys(colors) as ColorName[]).map((name) => (
14 |
15 | ))}
16 | >
17 | );
18 | };
19 |
20 | const Color = ({ name }: { name: ColorName }) => {
21 | if (typeof colors[name] === 'string') return null;
22 | return (
23 |
24 | {name.toUpperCase()}
25 |
26 | {Object.entries(colors[name]).map(([key, value]) => {
27 | return (
28 |
33 | );
34 | })}
35 |
36 |
37 | );
38 | };
39 |
40 | const ColorCard = ({ color, value }: { value: string; color: string }) => {
41 | return (
42 |
43 |
47 | {value}
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/ios/ObytesApp/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryUserDefaults
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | CA92.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryFileTimestamp
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | 0A2A.1
21 | 3B52.1
22 | C617.1
23 |
24 |
25 |
26 | NSPrivacyAccessedAPIType
27 | NSPrivacyAccessedAPICategoryDiskSpace
28 | NSPrivacyAccessedAPITypeReasons
29 |
30 | E174.1
31 | 85F4.1
32 |
33 |
34 |
35 | NSPrivacyAccessedAPIType
36 | NSPrivacyAccessedAPICategorySystemBootTime
37 | NSPrivacyAccessedAPITypeReasons
38 |
39 | 35F9.1
40 |
41 |
42 |
43 | NSPrivacyCollectedDataTypes
44 |
45 | NSPrivacyTracking
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/components/settings/theme-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import type { OptionType } from '@/components/ui';
4 | import { Options, useModal } from '@/components/ui';
5 | import type { ColorSchemeType } from '@/lib';
6 | import { translate, useSelectedTheme } from '@/lib';
7 |
8 | import { Item } from './item';
9 |
10 | export const ThemeItem = () => {
11 | const { selectedTheme, setSelectedTheme } = useSelectedTheme();
12 | const modal = useModal();
13 |
14 | const onSelect = React.useCallback(
15 | (option: OptionType) => {
16 | setSelectedTheme(option.value as ColorSchemeType);
17 | modal.dismiss();
18 | },
19 | [setSelectedTheme, modal]
20 | );
21 |
22 | const themes = React.useMemo(
23 | () => [
24 | { label: `${translate('settings.theme.dark')} 🌙`, value: 'dark' },
25 | { label: `${translate('settings.theme.light')} 🌞`, value: 'light' },
26 | { label: `${translate('settings.theme.system')} ⚙️`, value: 'system' },
27 | ],
28 | []
29 | );
30 |
31 | const theme = React.useMemo(
32 | () => themes.find((t) => t.value === selectedTheme),
33 | [selectedTheme, themes]
34 | );
35 |
36 | return (
37 | <>
38 |
43 |
49 | >
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/cli/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '6.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | dependencies:
8 | consola:
9 | specifier: ^3.2.3
10 | version: 3.2.3
11 | fs-extra:
12 | specifier: ^10.1.0
13 | version: 10.1.0
14 |
15 | packages:
16 |
17 | /consola@3.2.3:
18 | resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
19 | engines: {node: ^14.18.0 || >=16.10.0}
20 | dev: false
21 |
22 | /fs-extra@10.1.0:
23 | resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
24 | engines: {node: '>=12'}
25 | dependencies:
26 | graceful-fs: 4.2.10
27 | jsonfile: 6.1.0
28 | universalify: 2.0.0
29 | dev: false
30 |
31 | /graceful-fs@4.2.10:
32 | resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
33 | dev: false
34 |
35 | /jsonfile@6.1.0:
36 | resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
37 | dependencies:
38 | universalify: 2.0.0
39 | optionalDependencies:
40 | graceful-fs: 4.2.10
41 | dev: false
42 |
43 | /universalify@2.0.0:
44 | resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
45 | engines: {node: '>= 10.0.0'}
46 | dev: false
47 |
--------------------------------------------------------------------------------
/src/api/common/utils.tsx:
--------------------------------------------------------------------------------
1 | import type {
2 | GetNextPageParamFunction,
3 | GetPreviousPageParamFunction,
4 | } from '@tanstack/react-query';
5 |
6 | import type { PaginateQuery } from '../types';
7 |
8 | type KeyParams = {
9 | [key: string]: any;
10 | };
11 | export const DEFAULT_LIMIT = 10;
12 |
13 | export function getQueryKey(key: string, params?: T) {
14 | return [key, ...(params ? [params] : [])];
15 | }
16 |
17 | // for infinite query pages to flatList data
18 | export function normalizePages(pages?: PaginateQuery[]): T[] {
19 | return pages
20 | ? pages.reduce((prev: T[], current) => [...prev, ...current.results], [])
21 | : [];
22 | }
23 |
24 | // a function that accept a url and return params as an object
25 | export function getUrlParameters(
26 | url: string | null
27 | ): { [k: string]: string } | null {
28 | if (url === null) {
29 | return null;
30 | }
31 | let regex = /[?&]([^=#]+)=([^]*)/g,
32 | params = {},
33 | match;
34 | while ((match = regex.exec(url))) {
35 | if (match[1] !== null) {
36 | //@ts-ignore
37 | params[match[1]] = match[2];
38 | }
39 | }
40 | return params;
41 | }
42 |
43 | export const getPreviousPageParam: GetNextPageParamFunction<
44 | unknown,
45 | PaginateQuery
46 | > = (page) => getUrlParameters(page.previous)?.offset ?? null;
47 |
48 | export const getNextPageParam: GetPreviousPageParamFunction<
49 | unknown,
50 | PaginateQuery
51 | > = (page) => getUrlParameters(page.next)?.offset ?? null;
52 |
--------------------------------------------------------------------------------
/src/lib/hooks/use-selected-theme.tsx:
--------------------------------------------------------------------------------
1 | import { colorScheme, useColorScheme } from 'nativewind';
2 | import React from 'react';
3 | import { useMMKVString } from 'react-native-mmkv';
4 |
5 | import { storage } from '../storage';
6 |
7 | const SELECTED_THEME = 'SELECTED_THEME';
8 | export type ColorSchemeType = 'light' | 'dark' | 'system';
9 | /**
10 | * this hooks should only be used while selecting the theme
11 | * This hooks will return the selected theme which is stored in MMKV
12 | * selectedTheme should be one of the following values 'light', 'dark' or 'system'
13 | * don't use this hooks if you want to use it to style your component based on the theme use useColorScheme from nativewind instead
14 | *
15 | */
16 | export const useSelectedTheme = () => {
17 | const { colorScheme: _color, setColorScheme } = useColorScheme();
18 | const [theme, _setTheme] = useMMKVString(SELECTED_THEME, storage);
19 |
20 | const setSelectedTheme = React.useCallback(
21 | (t: ColorSchemeType) => {
22 | setColorScheme(t);
23 | _setTheme(t);
24 | },
25 | [setColorScheme, _setTheme]
26 | );
27 |
28 | const selectedTheme = (theme ?? 'system') as ColorSchemeType;
29 | return { selectedTheme, setSelectedTheme } as const;
30 | };
31 | // to be used in the root file to load the selected theme from MMKV
32 | export const loadSelectedTheme = () => {
33 | const theme = storage.getString(SELECTED_THEME);
34 | if (theme !== undefined) {
35 | console.log('theme', theme);
36 | colorScheme.set(theme as ColorSchemeType);
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/ui/icons/language.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { SvgProps } from 'react-native-svg';
3 | import Svg, { G, Path, Text, TSpan } from 'react-native-svg';
4 |
5 | export const Language = ({ ...props }: SvgProps) => (
6 |
33 | );
34 |
--------------------------------------------------------------------------------
/.github/workflows/compress-images.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/compress-images.yml
3 |
4 | # ✍️ Description:
5 | # This workflow is used to compress images in the repo.
6 | # This workflow will trigger on a push to the "master" or "main" branch and only run when a new image is added or updated.
7 | # If it's the case, it will compress those images and create a pull request with the compressed images.
8 |
9 | # 🚨 GITHUB SECRETS REQUIRED: None
10 |
11 | name: Compress images
12 | on:
13 | push:
14 | branches:
15 | - master
16 | - main
17 | paths:
18 | - '**.jpg'
19 | - '**.jpeg'
20 | - '**.png'
21 | - '**.webp'
22 | workflow_dispatch:
23 |
24 | jobs:
25 | build:
26 | name: calibreapp/image-actions
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Checkout Branch
30 | uses: actions/checkout@v4
31 | with:
32 | fetch-depth: 0
33 | - name: Compress Images
34 | id: calibre
35 | uses: calibreapp/image-actions@main
36 | with:
37 | githubToken: ${{ secrets.GITHUB_TOKEN }}
38 | compressOnly: true
39 | ignorePaths: 'node_modules/**,ios/**,android/**'
40 |
41 | - name: Create Pull Request
42 | if: steps.calibre.outputs.markdown != ''
43 | uses: peter-evans/create-pull-request@v3
44 | with:
45 | title: Auto Compress Images
46 | branch-suffix: timestamp
47 | commit-message: Compress Images
48 | body: ${{ steps.calibre.outputs.markdown }}
49 |
--------------------------------------------------------------------------------
/src/components/card.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'expo-router';
2 | import React from 'react';
3 |
4 | import type { Post } from '@/api';
5 | import { Image, Pressable, Text, View } from '@/components/ui';
6 |
7 | type Props = Post;
8 |
9 | const images = [
10 | 'https://images.unsplash.com/photo-1489749798305-4fea3ae63d43?auto=format&fit=crop&w=800&q=80',
11 | 'https://images.unsplash.com/photo-1564507004663-b6dfb3c824d5?auto=format&fit=crop&w=800&q=80',
12 | 'https://images.unsplash.com/photo-1515386474292-47555758ef2e?auto=format&fit=crop&w=800&q=80',
13 | 'https://plus.unsplash.com/premium_photo-1666815503002-5f07a44ac8fb?auto=format&fit=crop&w=800&q=80',
14 | 'https://images.unsplash.com/photo-1587974928442-77dc3e0dba72?auto=format&fit=crop&w=800&q=80',
15 | ];
16 |
17 | export const Card = ({ title, body, id }: Props) => {
18 | return (
19 |
20 |
21 |
22 |
29 |
30 |
31 | {title}
32 |
33 | {body}
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/docs/src/content/docs/how-to-contribute.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: How to Contribute
3 | description: How to contribute to the project, including ways to show your support, report bugs, and more.
4 | head:
5 | - tag: title
6 | content: How to Contribute | React Native / Expo Starter
7 | ---
8 |
9 | Thank you for your interest in contributing to our project. Your involvement is greatly appreciated and we welcome your contributions. Here are some ways you can help us improve this project:
10 |
11 | 1. Show your support for the project by giving it a 🌟 on [Github](https://github.com/obytes/react-native-template-obytes). This helps us increase visibility and attract more contributors.
12 | 2. Share your thoughts and ideas with us by [opening an issue](https://github.com/obytes/react-native-template-obytes/issues). If you have any suggestions or feedback about any aspect of the project, we are always eager to hear from you and have a [discussion](https://github.com/obytes/react-native-template-obytes/discussions).
13 | 3. If you have any questions about the project, please don't hesitate to ask. Simply open a new [QA discussion](https://github.com/obytes/react-native-template-obytes/discussions/categories/q-a) and our team will do our best to provide a helpful and informative response.
14 | 4. If you encounter a bug or typo while using the starter kit or reading the documentation, we would be grateful if you could bring it to our attention. You can open an issue to report the issue, or even better, submit a pull request with a fix.
15 |
16 | We value the input and contributions of our community and look forward to working with you to improve this project.
17 |
--------------------------------------------------------------------------------
/.github/workflows/lint-ts.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/lint-ts.yml
3 |
4 | # ✍️ Description:
5 | # This action is used to run eslint checks
6 | # Runs on pull requests and pushes to the main/master branches
7 | # Based on the event type:
8 | # - If it's a pull request, it will run eslint, then add the check to the PR as well as annotate the code with the errors and warnings.
9 | # - If it's a push to main/master, it will run the type checking and fail if there are any errors.
10 |
11 | # 🚨 GITHUB SECRETS REQUIRED: NONE
12 |
13 | name: Lint TS (eslint, prettier)
14 |
15 | on:
16 | push:
17 | branches: [main, master]
18 | pull_request:
19 | branches: [main, master]
20 |
21 | permissions:
22 | contents: read
23 | pull-requests: write
24 |
25 | jobs:
26 | lint:
27 | name: Lint TS (eslint, prettier)
28 | runs-on: ubuntu-latest
29 |
30 | steps:
31 | - name: 📦 Checkout project repo
32 | uses: actions/checkout@v4
33 | with:
34 | fetch-depth: 0
35 |
36 | - name: 📦 Setup Node + PNPM + install deps
37 | uses: ./.github/actions/setup-node-pnpm-install
38 |
39 | - name: 🏃♂️ Run ESLint PR
40 | if: github.event_name == 'pull_request'
41 | uses: reviewdog/action-eslint@v1
42 | with:
43 | github_token: ${{ secrets.GITHUB_TOKEN }}
44 | reporter: github-pr-review
45 | eslint_flags: '. --ext .js,.jsx,.ts,.tsx'
46 |
47 | - name: 🏃♂️ Run ESLint PR
48 | if: github.event_name != 'pull_request'
49 | run: pnpm run lint
50 |
--------------------------------------------------------------------------------
/src/components/ui/utils.tsx:
--------------------------------------------------------------------------------
1 | import type { AxiosError } from 'axios';
2 | import { Dimensions, Platform } from 'react-native';
3 | import { showMessage } from 'react-native-flash-message';
4 |
5 | export const IS_IOS = Platform.OS === 'ios';
6 | const { width, height } = Dimensions.get('screen');
7 |
8 | export const WIDTH = width;
9 | export const HEIGHT = height;
10 |
11 | // for onError react queries and mutations
12 | export const showError = (error: AxiosError) => {
13 | console.log(JSON.stringify(error?.response?.data));
14 | const description = extractError(error?.response?.data).trimEnd();
15 |
16 | showMessage({
17 | message: 'Error',
18 | description,
19 | type: 'danger',
20 | duration: 4000,
21 | icon: 'danger',
22 | });
23 | };
24 |
25 | export const showErrorMessage = (message: string = 'Something went wrong ') => {
26 | showMessage({
27 | message,
28 | type: 'danger',
29 | duration: 4000,
30 | });
31 | };
32 |
33 | export const extractError = (data: unknown): string => {
34 | if (typeof data === 'string') {
35 | return data;
36 | }
37 | if (Array.isArray(data)) {
38 | const messages = data.map((item) => {
39 | return ` ${extractError(item)}`;
40 | });
41 |
42 | return `${messages.join('')}`;
43 | }
44 |
45 | if (typeof data === 'object' && data !== null) {
46 | const messages = Object.entries(data).map((item) => {
47 | const [key, value] = item;
48 | const separator = Array.isArray(value) ? ':\n ' : ': ';
49 |
50 | return `- ${key}${separator}${extractError(value)} \n `;
51 | });
52 | return `${messages.join('')} `;
53 | }
54 | return 'Something went wrong ';
55 | };
56 |
--------------------------------------------------------------------------------
/.github/workflows/eas-build-prod.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/eas-build-prod.yml
3 | # Starter releasing process: https://starter.obytes.com/ci-cd/app-releasing-process/
4 |
5 | # ✍️ Description:
6 | # This workflow is used to trigger a build on EAS for Prod environment.
7 | # Can be triggered manually from the actions tab.
8 | # This workflow will use ./actions/eas-build action to trigger the build on EAS with production env.
9 |
10 | # 🚨 GITHUB SECRETS REQUIRED:
11 | # - EXPO_TOKEN: Expo token to authenticate with EAS
12 | # - You can get it from https://expo.dev/settings/access-tokens
13 |
14 | name: EAS Production Build (Android & IOS) (EAS)
15 |
16 | on:
17 | workflow_dispatch:
18 |
19 | jobs:
20 | Build:
21 | name: EAS Production Build (Android & IOS) (EAS)
22 | runs-on: ubuntu-latest
23 | steps:
24 | - name: Check for EXPO_TOKEN
25 | run: |
26 | if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
27 | echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
28 | exit 1
29 | fi
30 |
31 | - name: 📦 Checkout project repo
32 | uses: actions/checkout@v4
33 | with:
34 | fetch-depth: 0
35 |
36 | - name: 📦 Setup Node + PNPM + install deps
37 | uses: ./.github/actions/setup-node-pnpm-install
38 |
39 | - name: ⏱️ EAS Build
40 | uses: ./.github/actions/eas-build
41 | with:
42 | APP_ENV: production
43 | EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
44 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 3.8.1"
4 | },
5 | "build": {
6 | "production": {
7 | "channel": "production",
8 | "distribution": "store",
9 | "pnpm": "9.12.3",
10 | "ios": {
11 | "image": "latest"
12 | },
13 | "android": {
14 | "buildType": "app-bundle",
15 | "image": "latest"
16 | },
17 | "env": {
18 | "EXPO_NO_DOTENV": "1",
19 | "APP_ENV": "production",
20 | "FLIPPER_DISABLE": "1"
21 | }
22 | },
23 | "staging": {
24 | "channel": "staging",
25 | "distribution": "internal",
26 | "pnpm": "9.12.3",
27 | "ios": {
28 | "image": "latest"
29 | },
30 | "android": {
31 | "buildType": "apk",
32 | "image": "latest"
33 | },
34 | "env": {
35 | "APP_ENV": "staging",
36 | "EXPO_NO_DOTENV": "1",
37 | "FLIPPER_DISABLE": "1"
38 | }
39 | },
40 | "development": {
41 | "developmentClient": true,
42 | "distribution": "internal",
43 | "pnpm": "9.12.3",
44 | "ios": {
45 | "image": "latest"
46 | },
47 | "android": {
48 | "image": "latest"
49 | },
50 | "env": {
51 | "APP_ENV": "development",
52 | "EXPO_NO_DOTENV": "1"
53 | }
54 | },
55 | "simulator": {
56 | "pnpm": "9.12.3",
57 | "ios": {
58 | "simulator": true,
59 | "image": "latest"
60 | },
61 | "android": {
62 | "image": "latest"
63 | },
64 | "env": {
65 | "APP_ENV": "development",
66 | "EXPO_NO_DOTENV": "1"
67 | }
68 | }
69 | },
70 | "submit": {
71 | "production": {}
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/docs/src/content/docs/guides/navigation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Expo Router
3 | description: How to use Expo Router in your app.
4 | head:
5 | - tag: title
6 | content: Expo Router | React Native / Expo Starter
7 | ---
8 |
9 | import CodeBlock from '../../../components/code.astro';
10 |
11 | [expo-router](https://docs.expo.dev/router/introduction/) is a navigation library provided by Expo that simplifies the implementation of navigation in React Native applications. It is built on top of React Navigation, a widely used navigation library, and abstracts away much of the complexity involved in managing navigation state and transitions between screens.
12 |
13 | Navigation in Expo Router is expressed declaratively, utilizing components to define the flow of the application. This approach makes it intuitive for developers to structure their navigation hierarchy.
14 |
15 | Conventional React Native projects typically adopt a structure where a sole root component is commonly specified in either ./App.js or ./index.js. Within the context of Expo Router, an alternative approach is offered through the utilization of the Root Layout, located in `app/_layout.tsx` in our Demo. Thereby, the `_layout` section of our app handles the overall structure and navigation setup.
16 |
17 |
18 |
19 | The Demo app comes with a simple stack and tabs layout. Feel free to remove what is not working for you and add your own using the same approach as the existing ones.
20 |
21 | Here is a simple example of the tabs layout.
22 |
23 |
24 |
25 | Make sure to check the official docs for more information and examples about [expo-router](https://docs.expo.dev/router/introduction/).
26 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/customize-app.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Customize your App
3 | description: Customize your app by updating Expo configuration and app icon and splash screen.
4 | head:
5 | - tag: title
6 | content: Customize your App | React Native / Expo Starter
7 | ---
8 |
9 | import Code from '../../../components/code.astro';
10 |
11 | The starter is a simple expo app. You just need to edit `app.config.ts` file to update expo attributes and configuration.
12 |
13 | Here is the complete config file :
14 |
15 |
16 |
17 | You can read more about expo configuration [here](https://docs.expo.io/workflow/configuration/).
18 | If you have any configurations that depend on environment variables, such as API URLs or keys, you can create it in `config` file following [the environment variables guide](/getting-started/environment-vars-config) and import your config to `app.config.ts` file.
19 |
20 | :::note
21 | We included TODO comments in the project to guide you to the areas requiring updates.
22 | :::
23 |
24 | ## Splash screen and app icon
25 |
26 | As we are using expo to create the starter, updating the app icon and splash screen is straightforward. You only need to update the app icon and splash screen images inside the `assets` folder and run `expo prebuild` to update the app icon and splash screen.
27 |
28 | As we are supporting multiple variants for development, staging and production environments you need 3 different icons but the right solution is to use the same icon with badges for each environment.
29 |
30 | For more details about the app icon and splash screen, please refer to the expo documentation.
31 |
32 | 👉 [Create a splash screen](https://docs.expo.dev/guides/splash-screens/)
33 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/test.yml
3 |
4 | # ✍️ Description:
5 | # This action is used to run unit tests
6 | # Runs on pull requests and pushes to the main/master branches
7 | # Based on the event type:
8 | # - If it's a pull request, it will run the tests and post a comment with coverage details.
9 | # - If it's a push to main/master, it will run the tests and add the check to the commit.
10 |
11 | # 🚨 GITHUB SECRETS REQUIRED: NONE
12 |
13 | name: Tests (jest)
14 |
15 | on:
16 | push:
17 | branches: [main, master]
18 | pull_request:
19 | branches: [main, master]
20 |
21 | jobs:
22 | test:
23 | name: Tests (jest)
24 | runs-on: ubuntu-latest
25 |
26 | steps:
27 | - name: 📦 Checkout project repo
28 | uses: actions/checkout@v4
29 | with:
30 | fetch-depth: 0
31 |
32 | - name: 📦 Setup Node + PNPM + install deps
33 | uses: ./.github/actions/setup-node-pnpm-install
34 |
35 | - name: 🏃♂️ Run Tests
36 | run: pnpm run test:ci
37 |
38 | - name: Jest Coverage Comment
39 | uses: MishaKav/jest-coverage-comment@main
40 | if: (success() || failure()) && github.event_name == 'pull_request'
41 | with:
42 | coverage-summary-path: ./coverage/coverage-summary.json
43 | summary-title: '💯 Test Coverage'
44 | badge-title: Coverage
45 | create-new-comment: false
46 | junitxml-title: 😎 Tests Results
47 | junitxml-path: ./coverage/jest-junit.xml
48 | coverage-title: 👀 Tests Details
49 | coverage-path: ./coverage/coverage.txt
50 | report-only-changed-files: true
51 |
--------------------------------------------------------------------------------
/src/components/ui/colors.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | white: '#ffffff',
3 | black: '#000000',
4 | charcoal: {
5 | 50: '#F2F2F2',
6 | 100: '#E5E5E5',
7 | 200: '#C9C9C9',
8 | 300: '#B0B0B0',
9 | 400: '#969696',
10 | 500: '#7D7D7D',
11 | 600: '#616161',
12 | 700: '#474747',
13 | 800: '#383838',
14 | 850: '#2E2E2E',
15 | 900: '#1E1E1E',
16 | 950: '#121212',
17 | },
18 | neutral: {
19 | 50: '#FAFAFA',
20 | 100: '#F5F5F5',
21 | 200: '#F0EFEE',
22 | 300: '#D4D4D4',
23 | 400: '#A3A3A3',
24 | 500: '#737373',
25 | 600: '#525252',
26 | 700: '#404040',
27 | 800: '#262626',
28 | 900: '#171717',
29 | },
30 | primary: {
31 | 50: '#FFE2CC',
32 | 100: '#FFC499',
33 | 200: '#FFA766',
34 | 300: '#FF984C',
35 | 400: '#FF8933',
36 | 500: '#FF7B1A',
37 | 600: '#FF6C00',
38 | 700: '#E56100',
39 | 800: '#CC5600',
40 | 900: '#B24C00',
41 | },
42 | success: {
43 | 50: '#F0FDF4',
44 | 100: '#DCFCE7',
45 | 200: '#BBF7D0',
46 | 300: '#86EFAC',
47 | 400: '#4ADE80',
48 | 500: '#22C55E',
49 | 600: '#16A34A',
50 | 700: '#15803D',
51 | 800: '#166534',
52 | 900: '#14532D',
53 | },
54 | warning: {
55 | 50: '#FFFBEB',
56 | 100: '#FEF3C7',
57 | 200: '#FDE68A',
58 | 300: '#FCD34D',
59 | 400: '#FBBF24',
60 | 500: '#F59E0B',
61 | 600: '#D97706',
62 | 700: '#B45309',
63 | 800: '#92400E',
64 | 900: '#78350F',
65 | },
66 | danger: {
67 | 50: '#FEF2F2',
68 | 100: '#FEE2E2',
69 | 200: '#FECACA',
70 | 300: '#FCA5A5',
71 | 400: '#F87171',
72 | 500: '#EF4444',
73 | 600: '#DC2626',
74 | 700: '#B91C1C',
75 | 800: '#991B1B',
76 | 900: '#7F1D1D',
77 | },
78 | };
79 |
--------------------------------------------------------------------------------
/src/app/onboarding.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'expo-router';
2 | import React from 'react';
3 |
4 | import { Cover } from '@/components/cover';
5 | import {
6 | Button,
7 | FocusAwareStatusBar,
8 | SafeAreaView,
9 | Text,
10 | View,
11 | } from '@/components/ui';
12 | import { useIsFirstTime } from '@/lib/hooks';
13 | export default function Onboarding() {
14 | const [_, setIsFirstTime] = useIsFirstTime();
15 | const router = useRouter();
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Obytes Starter
25 |
26 |
27 | The right way to build your mobile app
28 |
29 |
30 |
31 | 🚀 Production-ready{' '}
32 |
33 |
34 | 🥷 Developer experience + Productivity
35 |
36 |
37 | 🧩 Minimal code and dependencies
38 |
39 |
40 | 💪 well maintained third-party libraries
41 |
42 |
43 |
44 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/README-project.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 | Mobile App
4 |
5 | > This Project is based on [Obytes starter](https://starter.obytes.com)
6 |
7 | ## Requirements
8 |
9 | - [React Native dev environment ](https://reactnative.dev/docs/environment-setup)
10 | - [Node.js LTS release](https://nodejs.org/en/)
11 | - [Git](https://git-scm.com/)
12 | - [Watchman](https://facebook.github.io/watchman/docs/install#buildinstall), required only for macOS or Linux users
13 | - [Pnpm](https://pnpm.io/installation)
14 | - [Cursor](https://www.cursor.com/) or [VS Code Editor](https://code.visualstudio.com/download) ⚠️ Make sure to install all recommended extension from `.vscode/extensions.json`
15 |
16 | ## 👋 Quick start
17 |
18 | Clone the repo to your machine and install deps :
19 |
20 | ```sh
21 | git clone https://github.com/user/repo-name
22 |
23 | cd ./repo-name
24 |
25 | pnpm install
26 | ```
27 |
28 | To run the app on ios
29 |
30 | ```sh
31 | pnpm ios
32 | ```
33 |
34 | To run the app on Android
35 |
36 | ```sh
37 | pnpm android
38 | ```
39 |
40 | ## ✍️ Documentation
41 |
42 | - [Rules and Conventions](https://starter.obytes.com/getting-started/rules-and-conventions/)
43 | - [Project structure](https://starter.obytes.com/getting-started/project-structure)
44 | - [Environment vars and config](https://starter.obytes.com/getting-started/environment-vars-config)
45 | - [UI and Theming](https://starter.obytes.com/ui-and-theme/ui-theming)
46 | - [Components](https://starter.obytes.com/ui-and-theme/components)
47 | - [Forms](https://starter.obytes.com/ui-and-theme/Forms)
48 | - [Data fetching](https://starter.obytes.com/guides/data-fetching)
49 | - [Contribute to starter](https://starter.obytes.com/how-to-contribute/)
50 |
--------------------------------------------------------------------------------
/src/components/buttons.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, View } from '@/components/ui';
4 |
5 | import { Title } from './title';
6 |
7 | export const Buttons = () => {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
49 |
50 | >
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/docs/src/content/docs/stay-updated.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Stay Up-to-Date with React Native
3 | description: Stay up-to-date with React Native news.
4 | head:
5 | - tag: title
6 | content: Stay Updated | React Native / Expo Starter
7 | ---
8 |
9 | Make sure to check out the links below to stay up-to-date with React Native news.
10 |
11 | :::note
12 | We are in favor of reducing the number of news sources you use to avoid feeling overwhelmed. Therefore, we have only included resources that we think are essential and deserve your attention.
13 |
14 | :::
15 |
16 | ## Websites
17 |
18 | - [React Native Documentation](https://reactnative.dev/)
19 | - [React Native Directory](https://reactnative.directory/)
20 | - [React Native Reddit Channel](https://www.reddit.com/r/reactnative/)
21 |
22 | ## Blogs
23 |
24 | - [Expo blog](https://blog.expo.dev/)
25 | - [React Native Dev Blog](https://reactnative.dev/blog)
26 | - [TkDodo's blog](https://tkdodo.eu/blog/)
27 | - [Developer way](https://www.developerway.com/)
28 | - [Bam React Native blog](https://www.bam.tech/blog/react-native)
29 |
30 | ## Newsletters
31 |
32 | - [This week in react](https://thisweekinreact.com/)
33 | - [The React Native Newsletter](https://reactnativenewsletter.com/)
34 |
35 | ## Twitter accounts
36 |
37 | - [Evan Bacon](https://twitter.com/Baconbrix)
38 | - [React Native](https://twitter.com/reactnative)
39 | - [Expo](https://twitter.com/expo)
40 | - [Sebastien Lorber](https://twitter.com/sebastienlorber)
41 |
42 | ## Open source projects
43 |
44 | A list of open-source projects and production-ready apps that you can use to learn and get inspired from.
45 |
46 | - [showtime-frontend](https://github.com/showtime-xyz/showtime-frontend)
47 | - [xLog-mobile](https://github.com/Crossbell-Box/xLog-mobile)
48 | - [CommE2E](https://github.com/CommE2E/comm)
49 | - [Expensify](https://github.com/Expensify/App)
50 |
--------------------------------------------------------------------------------
/.github/workflows/eas-build-qa.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/eas-build-qa.yml
3 | # Starter releasing process: https://starter.obytes.com/ci-cd/app-releasing-process/
4 |
5 | # ✍️ Description:
6 | # This workflow is used to trigger a build on EAS for the QA environment.
7 | # It will run on every GitHub release published on the repo or can be triggered manually from the actions tab.
8 | # This workflow will use ./actions/eas-build action to trigger the build on EAS with staging env.
9 |
10 | # 🚨 GITHUB SECRETS REQUIRED:
11 | # - EXPO_TOKEN: Expo token to authenticate with EAS
12 | # - You can get it from https://expo.dev/settings/access-tokens
13 |
14 | name: EAS QA Build (Android & IOS) (EAS)
15 |
16 | on:
17 | workflow_dispatch:
18 | release:
19 | types: [published]
20 |
21 | jobs:
22 | Build:
23 | name: EAS QA Build (Android & IOS) (EAS)
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: Check for EXPO_TOKEN
27 | run: |
28 | if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
29 | echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
30 | exit 1
31 | fi
32 | - name: 📦 Checkout project repo
33 | uses: actions/checkout@v4
34 | with:
35 | fetch-depth: 0
36 |
37 | - name: 📦 Setup Node + PNPM + install deps
38 | uses: ./.github/actions/setup-node-pnpm-install
39 |
40 | - name: ⏱️ EAS Build
41 | uses: ./.github/actions/eas-build
42 | with:
43 | APP_ENV: staging
44 | EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
45 | VERSION: ${{ github.event.release.tag_name }}
46 | IOS: false # TODO: set as true when IOS account is ready
47 |
48 |
--------------------------------------------------------------------------------
/src/lib/i18n/utils.tsx:
--------------------------------------------------------------------------------
1 | import type TranslateOptions from 'i18next';
2 | import i18n from 'i18next';
3 | import memoize from 'lodash.memoize';
4 | import { useCallback } from 'react';
5 | import { I18nManager, NativeModules, Platform } from 'react-native';
6 | import { useMMKVString } from 'react-native-mmkv';
7 | import RNRestart from 'react-native-restart';
8 |
9 | import { storage } from '../storage';
10 | import type { Language, resources } from './resources';
11 | import type { RecursiveKeyOf } from './types';
12 |
13 | type DefaultLocale = typeof resources.en.translation;
14 | export type TxKeyPath = RecursiveKeyOf;
15 |
16 | export const LOCAL = 'local';
17 |
18 | export const getLanguage = () => storage.getString(LOCAL); // 'Marc' getItem(LOCAL);
19 |
20 | export const translate = memoize(
21 | (key: TxKeyPath, options = undefined) =>
22 | i18n.t(key, options) as unknown as string,
23 | (key: TxKeyPath, options: typeof TranslateOptions) =>
24 | options ? key + JSON.stringify(options) : key
25 | );
26 |
27 | export const changeLanguage = (lang: Language) => {
28 | i18n.changeLanguage(lang);
29 | if (lang === 'ar') {
30 | I18nManager.forceRTL(true);
31 | } else {
32 | I18nManager.forceRTL(false);
33 | }
34 | if (Platform.OS === 'ios' || Platform.OS === 'android') {
35 | if (__DEV__) NativeModules.DevSettings.reload();
36 | else RNRestart.restart();
37 | } else if (Platform.OS === 'web') {
38 | window.location.reload();
39 | }
40 | };
41 |
42 | export const useSelectedLanguage = () => {
43 | const [language, setLang] = useMMKVString(LOCAL);
44 |
45 | const setLanguage = useCallback(
46 | (lang: Language) => {
47 | setLang(lang);
48 | if (lang !== undefined) changeLanguage(lang as Language);
49 | },
50 | [setLang]
51 | );
52 |
53 | return { language: language as Language, setLanguage };
54 | };
55 |
--------------------------------------------------------------------------------
/.github/workflows/expo-doctor.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/expo-doctor.yml
3 |
4 | # ✍️ Description:
5 | # This workflow runs the expo doctor command to check if your project dependencies are aligned with the expo sdk version you are using.
6 | # Can be triggered manually from the Actions tab in your project.
7 | # Runs Also on pull requests and pushes to the main/master branch, but only if the `package.json` or `pnpm-lock.yaml` files have been changed.
8 |
9 | # 🚨 GITHUB SECRETS REQUIRED: NONE
10 |
11 | name: Expo Doctor (expo)
12 |
13 | on:
14 | push:
15 | branches:
16 | - main
17 | - master
18 | paths:
19 | - 'package.json'
20 | - 'pnpm-lock.yaml'
21 | pull_request:
22 | paths:
23 | - 'package.json'
24 | - 'pnpm-lock.yaml'
25 |
26 | permissions:
27 | contents: read
28 | pull-requests: write
29 |
30 | jobs:
31 | doctor:
32 | name: Expo Doctor (expo)
33 | runs-on: ubuntu-latest
34 | permissions:
35 | contents: read
36 | pull-requests: write
37 |
38 | steps:
39 | - name: 📦 Checkout project repo
40 | uses: actions/checkout@v4
41 | with:
42 | fetch-depth: 0
43 |
44 | - name: 📦 Setup Node + PNPM + install deps
45 | uses: ./.github/actions/setup-node-pnpm-install
46 |
47 | - name: Run prebuild
48 | run: pnpm run prebuild
49 |
50 | - name: 🚑 Run Doctor Checks
51 | run: |
52 | chmod +x .github/scripts/expo-doctor.sh
53 | rm -rf ios android
54 | .github/scripts/expo-doctor.sh
55 |
56 | - name: Add doctor report as comment on PR
57 | if: github.event_name == 'pull_request' && always()
58 | uses: marocchino/sticky-pull-request-comment@v2
59 | with:
60 | header: expo-doctor
61 | path: .expo/expo-doctor.md
62 |
--------------------------------------------------------------------------------
/src/components/ui/modal-keyboard-aware-scroll-view.tsx:
--------------------------------------------------------------------------------
1 | // source https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view
2 | /**
3 | * This component is used to handle the keyboard in a modal.
4 | * It is a wrapper around the `KeyboardAwareScrollView` component from `react-native-keyboard-controller`.
5 | * It is used to handle the keyboard in a modal.
6 | * example usage:
7 | export function Example() {
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | }
15 | */
16 | import {
17 | type BottomSheetScrollViewMethods,
18 | createBottomSheetScrollableComponent,
19 | SCROLLABLE_TYPE,
20 | } from '@gorhom/bottom-sheet';
21 | import { type BottomSheetScrollViewProps } from '@gorhom/bottom-sheet/src/components/bottomSheetScrollable/types';
22 | import { memo } from 'react';
23 | import { type KeyboardAwareScrollViewProps } from 'react-native-keyboard-controller';
24 | import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
25 | import Reanimated from 'react-native-reanimated';
26 |
27 | const AnimatedScrollView =
28 | Reanimated.createAnimatedComponent(
29 | KeyboardAwareScrollView
30 | );
31 | const BottomSheetScrollViewComponent = createBottomSheetScrollableComponent<
32 | BottomSheetScrollViewMethods,
33 | BottomSheetScrollViewProps
34 | >(SCROLLABLE_TYPE.SCROLLVIEW, AnimatedScrollView);
35 | const BottomSheetKeyboardAwareScrollView = memo(BottomSheetScrollViewComponent);
36 |
37 | BottomSheetKeyboardAwareScrollView.displayName =
38 | 'BottomSheetKeyboardAwareScrollView';
39 |
40 | export default BottomSheetKeyboardAwareScrollView as (
41 | props: BottomSheetScrollViewProps & KeyboardAwareScrollViewProps
42 | ) => ReturnType;
43 |
--------------------------------------------------------------------------------
/src/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from 'expo-router/html';
2 |
3 | // This file is web-only and used to configure the root HTML for every
4 | // web page during static rendering.
5 | // The contents of this function only run in Node.js environments and
6 | // do not have access to the DOM or browser APIs.
7 | export default function Root({ children }: { children: React.ReactNode }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | {/*
15 | This viewport disables scaling which makes the mobile website act more like a native app.
16 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
17 |
18 | */}
19 |
23 | {/*
24 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
25 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
26 | */}
27 |
28 |
29 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
30 |
31 | {/* Add any additional elements that you want globally available on web... */}
32 |
33 | {children}
34 |
35 | );
36 | }
37 |
38 | const responsiveBackground = `
39 | body {
40 | background-color: #fff;
41 | }
42 | @media (prefers-color-scheme: dark) {
43 | body {
44 | background-color: #000;
45 | }
46 | }`;
47 |
--------------------------------------------------------------------------------
/.github/actions/setup-jdk-generate-apk/action.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/actions/setup-jdk-generate-apk/action.yml
3 | # Composite actions docs: https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
4 |
5 | # ✍️ Description:
6 | # This is a composite action, which means it can be used in other actions.
7 | # This action is used to set up the JDK environment and generate an Android APK for testing.
8 | # This action accepts one input: `APP_ENV`, which is used to generate an APK for a specific environment (development, staging, production). We use staging by default.
9 | # Before generating the APK, we run a pre-build script to generate the necessary native folders based on the APP_ENV.
10 | # On success, the APK is generated at `./android/app/build/outputs/apk/release/app-release.apk`.
11 |
12 | # 👀 Example usage:
13 | # - name : 📦 Set Up JDK + Generate Test APK
14 | # uses: ./.github/actions/setup-jdk-generate-apk
15 | # with:
16 | # APP_ENV: 'staging'
17 |
18 | name: 'Setup JDK + GRADLE + Generate APK'
19 | description: 'Setup JDK + GRADLE + Generate APK'
20 | inputs:
21 | APP_ENV:
22 | description: 'APP_ENV (one of): development, staging, production'
23 | required: true
24 | default: 'staging'
25 |
26 | runs:
27 | using: 'composite'
28 | steps:
29 | - name: Set Up JDK
30 | uses: actions/setup-java@v3
31 | with:
32 | distribution: 'zulu' # See 'Supported distributions' for available options
33 | java-version: '17'
34 | - name: Setup Gradle
35 | uses: gradle/gradle-build-action@v2
36 |
37 | - name: Generate Test APK
38 | run: |
39 | pnpm prebuild:${{ inputs.APP_ENV }}
40 | cd android
41 | chmod +x ./gradlew
42 | ./gradlew assembleRelease --no-daemon
43 | cd ..
44 | shell: bash
45 | env:
46 | EXPO_NO_DOTENV: '1'
47 | APP_ENV: ${{ inputs.APP_ENV }}
48 | CI: 'true'
49 |
--------------------------------------------------------------------------------
/docs/src/content/docs/ui-and-theme/fonts.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Fonts
3 | description: How to add custom fonts to your app.
4 | head:
5 | - tag: title
6 | content: Fonts | React Native / Expo Starter
7 | ---
8 |
9 | With Expo, you can load fonts dynamically using `useFont` hook from `expo-font` library. With this approach, you need to wait for the font to load before showing or hiding your splash screen.
10 |
11 | With the last version of `expo-font` introduced with Expo 50, you can use `expo-font` as a plugin in your `app.config.js` to load fonts natively.
12 |
13 | To add a custom font you only need to put the font file in the `assets/fonts` and update the expo config by adding the exact file path to the config like the following:
14 |
15 | ```js title="app.config.js"
16 | import type { ConfigContext, ExpoConfig } from '@expo/config';
17 |
18 | export default ({ config }: ConfigContext): ExpoConfig => ({
19 | ...config,
20 | plugins: [
21 | [
22 | 'expo-font',
23 | {
24 | fonts: ['./assets/fonts/Inter.ttf'],
25 | },
26 | ],
27 | ],
28 | });
29 | ```
30 |
31 | Next, Make sure to add your new font to Tailwind CSS config to use it with `className`
32 |
33 | ```js title="tailwind.config.js"
34 | const colors = require('./src/components/ui/theme/colors');
35 |
36 | /** @type {import('tailwindcss').Config} */
37 | module.exports = {
38 | // NOTE: Update this to include the paths to all of your component files.
39 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
40 | presets: [require('nativewind/preset')],
41 | theme: {
42 | extend: {
43 | fontFamily: {
44 | inter: ['Inter'],
45 | },
46 | colors,
47 | },
48 | },
49 | plugins: [],
50 | };
51 | ```
52 |
53 | :::info
54 | As we are linking font natively you need to run `expo prebuild` and then `expo ios` or `expo android` to use the new font.
55 | :::
56 |
57 | More details about adding fonts with Tailwind CSS can be found in the [Nativewind documentation](https://www.nativewind.dev/v4/tailwind/typography/font-family).
58 |
--------------------------------------------------------------------------------
/docs/src/content/docs/ci-cd/overview.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Overview
3 | description: All you need to know about the CI/CD of the starter.
4 | head:
5 | - tag: title
6 | content: CI/CD | React Native / Expo Starter
7 | ---
8 |
9 | CI/CD stands for Continuous Integration and Continuous Delivery/Deployment. It is a set of practices that help developers automate the process of building, testing, and distributing applications to end-users (such as app stores for mobile apps).
10 |
11 | Having a good CI/CD process in place is crucial for any project, as it helps you save time and effort, as well as ensuring that you are delivering a high-quality product to your users. It helps avoid common mistakes and bugs, reduces your time to market, and allows for faster updates to your users.
12 |
13 | As most of our projects are hosted on GitHub, we use [GitHub Actions](https://github.com/features/actions) and [Expo EAS](https://expo.dev/eas) as our CI/CD solution. The starter kit comes with over 10 GitHub Actions workflows:
14 |
15 | - `.github/workflows/lint-ts.yml`: On PR and new commits on the master branch, run linting and formatting checks.
16 | - `.github/workflows/test.yml`: On PR and new commits on the master branch, run unit tests.
17 | - `.github/workflows/type-check.yml`: On PR and new commits on the master branch, run type checking.
18 | - `.github/workflows/compress-images.yml`: On new commits to the master branch, when adding images, open a pull request (PR) with the compressed images.
19 | - `.github/workflows/expo-doctor.yml`: On PR and new commits to the master branch, check whether dependencies are aligned with the Expo SDK version.
20 | - `.github/workflows/new-app-version.yml`: A workflow to manually trigger and update the app version, and publish a tag.
21 |
22 | You can check the full list of workflows [here](/ci-cd/workflows-references/)
23 |
24 | Pushing the new release to the stores is a weekly process for our team. To make it easier, we have created a simple process using GitHub Actions and Expo EAS. You can check the full guide [here](/ci-cd/app-releasing-process/).
25 |
--------------------------------------------------------------------------------
/.github/workflows/type-check.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/type-check.yml
3 |
4 | # ✍️ Description:
5 | # This action is used to run the type-check on the project.
6 | # Runs on pull requests and pushes to the main/master branches
7 | # Based on the event type:
8 | # - If it's a pull request, it will run type checking, then add the check to the PR as well as annotate the code with the errors using reviewdog.
9 | # - If it's a push to main/master, it will run the type checking and fail if there are any errors.
10 |
11 | # 🚨 GITHUB SECRETS REQUIRED: NONE
12 |
13 | name: Type Check (tsc)
14 |
15 | on:
16 | push:
17 | branches: [main, master]
18 | pull_request:
19 | branches: [main, master]
20 |
21 | permissions:
22 | contents: read
23 | pull-requests: write
24 |
25 | jobs:
26 | type-check:
27 | name: Type Check (tsc)
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: 📦 Checkout project repo
31 | uses: actions/checkout@v4
32 | with:
33 | fetch-depth: 0
34 |
35 | - name: 📦 Setup Node + PNPM + install deps
36 | uses: ./.github/actions/setup-node-pnpm-install
37 |
38 | - name: 📦 Install Reviewdog
39 | if: github.event_name == 'pull_request'
40 | uses: reviewdog/action-setup@v1
41 |
42 | - name: 🏃♂️ Run TypeScript PR # Reviewdog tsc errorformat: %f:%l:%c - error TS%n: %m
43 | # We only need to add the reviewdog step if it's a pull request
44 | if: github.event_name == 'pull_request'
45 | run: |
46 | pnpm type-check | reviewdog -name="tsc" -efm="%f(%l,%c): error TS%n: %m" -reporter="github-pr-review" -filter-mode="nofilter" -fail-on-error -tee
47 | env:
48 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 |
50 | - name:
51 | 🏃♂️ Run TypeScript Commit
52 | # If it's not a Pull Request then we just need to run the type-check
53 | if: github.event_name != 'pull_request'
54 | run: pnpm type-check
55 |
--------------------------------------------------------------------------------
/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/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/src/content/docs/libraries-recommendation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Libraries Recommendation
3 | description: React Native / Expo Libraries recommendation for the project based on use cases.
4 | head:
5 | - tag: title
6 | content: Libraries Recommendation | React Native / Expo Starter
7 | ---
8 |
9 | The starter kit comes with a set of pre-installed and configured libraries. We recommend using these libraries for your project.
10 |
11 | Below, we'll list other libraries we often use in our projects. These aren't included in the starter because:
12 |
13 | 1. They're for specific use cases.
14 | 2. They need a lot of setup.
15 |
16 | This way, you can add them to your project only if you need them, keeping things simple to start with.
17 |
18 | ### State Management:
19 |
20 | The starter kit comes with Zustand out of the box but if your application implements a lot of workflows, you might want to use [XState](https://xstate.js.org/) as it's more powerful on managing complex workflows and state machines.
21 |
22 | For example, if you have a workflow to create a new card for user and this workflow has a lot of steps and conditions, Zustand might not be the best choice as it's more designed for simple state management and XState is your best choice in this case.
23 |
24 | ### Error Reporting:
25 |
26 | - [Sentry](https://sentry.io/welcome/): very popular solution for error reporting in the javascript ecosystem and has a great integration with Expo.
27 |
28 | ### Notifications:
29 |
30 | There is no solution fit all for notifications, but based on your use case we would recommend one of the following:
31 |
32 | - [Expo Push Notifications](https://docs.expo.dev/push-notifications/overview/)
33 | - [OneSignal](https://onesignal.com/)
34 |
35 | ### Analytics:
36 |
37 | - [PostHog](https://posthog.com/docs/libraries/react-native) : Easy to setup and use and has a great free tier.
38 |
39 | - [Google Analytics](https://rnfirebase.io/analytics/usage)
40 |
41 | ### Charts:
42 |
43 | - [Victory Native](https://github.com/FormidableLabs/victory-native-xl)
44 |
45 | ---
46 |
47 | For sure we are missing some great libraries here, so we count on your contribution to add them in the comments sections below.
48 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Starlight Starter Kit: Basics
2 |
3 | ```
4 | npm create astro@latest -- --template starlight
5 | ```
6 |
7 | [](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
8 | [](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
9 |
10 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
11 |
12 | ## 🚀 Project Structure
13 |
14 | Inside of your Astro + Starlight project, you'll see the following folders and files:
15 |
16 | ```
17 | .
18 | ├── public/
19 | ├── src/
20 | │ ├── assets/
21 | │ ├── content/
22 | │ │ ├── docs/
23 | │ │ └── config.ts
24 | │ └── env.d.ts
25 | ├── astro.config.mjs
26 | ├── package.json
27 | └── tsconfig.json
28 | ```
29 |
30 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
31 |
32 | Images can be added to `src/assets/` and embedded in Markdown with a relative link.
33 |
34 | Static assets, like favicons, can be placed in the `public/` directory.
35 |
36 | ## 🧞 Commands
37 |
38 | All commands are run from the root of the project, from a terminal:
39 |
40 | | Command | Action |
41 | | :------------------------ | :----------------------------------------------- |
42 | | `npm install` | Installs dependencies |
43 | | `npm run dev` | Starts local dev server at `localhost:3000` |
44 | | `npm run build` | Build your production site to `./dist/` |
45 | | `npm run preview` | Preview your build locally, before deploying |
46 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
47 | | `npm run astro -- --help` | Get help using the Astro CLI |
48 |
49 | ## 👀 Want to learn more?
50 |
51 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
52 |
--------------------------------------------------------------------------------
/app.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-lines-per-function */
2 | import type { ConfigContext, ExpoConfig } from '@expo/config';
3 | import type { AppIconBadgeConfig } from 'app-icon-badge/types';
4 |
5 | import { ClientEnv, Env } from './env';
6 |
7 | const appIconBadgeConfig: AppIconBadgeConfig = {
8 | enabled: Env.APP_ENV !== 'production',
9 | badges: [
10 | {
11 | text: Env.APP_ENV,
12 | type: 'banner',
13 | color: 'white',
14 | },
15 | {
16 | text: Env.VERSION.toString(),
17 | type: 'ribbon',
18 | color: 'white',
19 | },
20 | ],
21 | };
22 |
23 | export default ({ config }: ConfigContext): ExpoConfig => ({
24 | ...config,
25 | name: Env.NAME,
26 | description: `${Env.NAME} Mobile App`,
27 | owner: Env.EXPO_ACCOUNT_OWNER,
28 | scheme: Env.SCHEME,
29 | slug: 'obytesapp',
30 | version: Env.VERSION.toString(),
31 | orientation: 'portrait',
32 | icon: './assets/icon.png',
33 | userInterfaceStyle: 'automatic',
34 | newArchEnabled: true,
35 | updates: {
36 | fallbackToCacheTimeout: 0,
37 | },
38 | assetBundlePatterns: ['**/*'],
39 | ios: {
40 | supportsTablet: true,
41 | bundleIdentifier: Env.BUNDLE_ID,
42 | infoPlist: {
43 | ITSAppUsesNonExemptEncryption: false,
44 | },
45 | },
46 | experiments: {
47 | typedRoutes: true,
48 | },
49 | android: {
50 | adaptiveIcon: {
51 | foregroundImage: './assets/adaptive-icon.png',
52 | backgroundColor: '#2E3C4B',
53 | },
54 | package: Env.PACKAGE,
55 | },
56 | web: {
57 | favicon: './assets/favicon.png',
58 | bundler: 'metro',
59 | },
60 | plugins: [
61 | [
62 | 'expo-splash-screen',
63 | {
64 | backgroundColor: '#2E3C4B',
65 | image: './assets/splash-icon.png',
66 | imageWidth: 150,
67 | },
68 | ],
69 | [
70 | 'expo-font',
71 | {
72 | fonts: ['./assets/fonts/Inter.ttf'],
73 | },
74 | ],
75 | 'expo-localization',
76 | 'expo-router',
77 | ['app-icon-badge', appIconBadgeConfig],
78 | ['react-native-edge-to-edge'],
79 | ],
80 | extra: {
81 | ...ClientEnv,
82 | eas: {
83 | projectId: Env.EAS_PROJECT_ID,
84 | },
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/src/app/feed/add-post.tsx:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { Stack } from 'expo-router';
3 | import * as React from 'react';
4 | import { useForm } from 'react-hook-form';
5 | import { showMessage } from 'react-native-flash-message';
6 | import { z } from 'zod';
7 |
8 | import { useAddPost } from '@/api';
9 | import {
10 | Button,
11 | ControlledInput,
12 | showErrorMessage,
13 | View,
14 | } from '@/components/ui';
15 |
16 | const schema = z.object({
17 | title: z.string().min(10),
18 | body: z.string().min(120),
19 | });
20 |
21 | type FormType = z.infer;
22 |
23 | export default function AddPost() {
24 | const { control, handleSubmit } = useForm({
25 | resolver: zodResolver(schema),
26 | });
27 | const { mutate: addPost, isPending } = useAddPost();
28 |
29 | const onSubmit = (data: FormType) => {
30 | console.log(data);
31 | addPost(
32 | { ...data, userId: 1 },
33 | {
34 | onSuccess: () => {
35 | showMessage({
36 | message: 'Post added successfully',
37 | type: 'success',
38 | });
39 | // here you can navigate to the post list and refresh the list data
40 | //queryClient.invalidateQueries(usePosts.getKey());
41 | },
42 | onError: () => {
43 | showErrorMessage('Error adding post');
44 | },
45 | }
46 | );
47 | };
48 | return (
49 | <>
50 |
56 |
57 |
63 |
70 |
76 |
77 | >
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/docs/src/content/docs/testing/overview.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Testing Overview
3 | description: What to test and how to test it.
4 | head:
5 | - tag: title
6 | content: Testing Overview| React Native / Expo Starter
7 | ---
8 |
9 | Testing is an essential part of any software development process. It helps ensure that your code is working as expected and that you don't introduce any bugs into your codebase. As we believe in the importance of testing, we have made it easy to write your tests with the starter template. We include the [Jest](https://jestjs.io/) testing framework and the [React Native Testing Library](https://callstack.github.io/react-native-testing-library/) for unit tests, along with mocks for most libraries. We also use [Maestro](https://maestro.mobile.dev/) for end-to-end tests.
10 |
11 | ## Testing approaches
12 |
13 | It's clear that testing is important, but what should you test? Apparently, and especially for front-end apps (including mobile apps), there is no clear answer. Should I aim for 100% code coverage? Should I test every component?
14 |
15 | The answer is it depends. It depends on your app, your team, and your goals. We believe that testing is a trade-off between the time you spend writing tests and the confidence you get from them. You should aim for a good balance between the two.
16 |
17 | Aiming for 100% test coverage is not always a good idea and doesn't always make sense. Same with testing views and components that only render a UI. Instead, you should focus on the following:
18 |
19 | - **Business logic**: Test component and function utilities that contain business logic. Form validation, data manipulation and calculations, etc.
20 |
21 | - **Complex components**: Test components that contain complex logic. For example, components that contain a lot of conditional rendering, or components that contain a lot of state management logic.
22 |
23 | Focus on the above and write unit tests for them. This will help you get the most out of your tests and will assist you in finding bugs early on.
24 |
25 | To ensure that your app is functioning as expected, we recommend writing end-to-end tests, as they provide the highest level of confidence and help you test the app as a whole similar to how your users will use it.
26 |
--------------------------------------------------------------------------------
/src/app/(app)/_layout.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-unstable-nested-components */
2 | import { Link, Redirect, SplashScreen, Tabs } from 'expo-router';
3 | import React, { useCallback, useEffect } from 'react';
4 |
5 | import { Pressable, Text } from '@/components/ui';
6 | import {
7 | Feed as FeedIcon,
8 | Settings as SettingsIcon,
9 | Style as StyleIcon,
10 | } from '@/components/ui/icons';
11 | import { useAuth, useIsFirstTime } from '@/lib';
12 |
13 | export default function TabLayout() {
14 | const status = useAuth.use.status();
15 | const [isFirstTime] = useIsFirstTime();
16 | const hideSplash = useCallback(async () => {
17 | await SplashScreen.hideAsync();
18 | }, []);
19 | useEffect(() => {
20 | if (status !== 'idle') {
21 | setTimeout(() => {
22 | hideSplash();
23 | }, 1000);
24 | }
25 | }, [hideSplash, status]);
26 |
27 | if (isFirstTime) {
28 | return ;
29 | }
30 | if (status === 'signOut') {
31 | return ;
32 | }
33 | return (
34 |
35 | ,
40 | headerRight: () => ,
41 | tabBarButtonTestID: 'feed-tab',
42 | }}
43 | />
44 |
45 | ,
51 | tabBarButtonTestID: 'style-tab',
52 | }}
53 | />
54 | ,
60 | tabBarButtonTestID: 'settings-tab',
61 | }}
62 | />
63 |
64 | );
65 | }
66 |
67 | const CreateNewPostLink = () => {
68 | return (
69 |
70 |
71 | Create
72 |
73 |
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/.github/workflows/e2e-android-maestro.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/e2e-android.yml
3 | # End-to-end testing: https://starter.obytes.com/testing/end-to-end-testing/
4 |
5 | # ✍️ Description:
6 | # This workflow is used to run end-to-end tests on Android using Maestro Cloud.
7 | # As a first step, it will generate a test APK using the Gradle build and then trigger Maestro Cloud to run the tests on the generated APK.
8 | # This workflow will be triggered on pull requests (PRs) with the label "android-test-maestro-cloud" or can be manually triggered from the Actions tab.
9 |
10 | # 🚨 GITHUB SECRETS REQUIRED:
11 | # MAESTRO_CLOUD_API_KEY: API key for Maestro Cloud. You can get it from https://cloud.mobile.dev/ci-integration/github-actions#add-your-api-key-secret
12 |
13 | name: E2E Tests Android (Maestro Cloud)
14 |
15 | on:
16 | workflow_dispatch:
17 | pull_request:
18 | branches: [main, master]
19 |
20 | jobs:
21 | generate-and-test-apk:
22 | if: github.event_name != 'pull_request' || ( github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'android-test-maestro-cloud'))
23 | name: Generate and Test Test APK (Maestro Cloud)
24 | runs-on: ubuntu-latest
25 |
26 | steps:
27 | - name: 📦 Checkout project repo
28 | uses: actions/checkout@v4
29 | with:
30 | fetch-depth: 0
31 |
32 | - name: 📦 Setup Node + PNPM + install deps
33 | uses: ./.github/actions/setup-node-pnpm-install
34 |
35 | - name: 📦 Set Up JDK + Generate Test APK
36 | uses: ./.github/actions/setup-jdk-generate-apk
37 | with:
38 | APP_ENV: staging
39 |
40 | - name: Upload Test APK
41 | uses: actions/upload-artifact@v3
42 | with:
43 | name: test-apk
44 | path: ./android/app/build/outputs/apk/release/app-release.apk
45 |
46 | - name: 📱 Run E2E Tests with Maestro Cloud
47 | uses: mobile-dev-inc/action-maestro-cloud@v1.4.1
48 | with:
49 | api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
50 | app-file: ./android/app/build/outputs/apk/release/app-release.apk
51 | env: |
52 | APP_ID=com.obytes.staging
53 |
--------------------------------------------------------------------------------
/src/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | // Import global CSS file
2 | import '../../global.css';
3 |
4 | import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
5 | import { ThemeProvider } from '@react-navigation/native';
6 | import { Stack } from 'expo-router';
7 | import * as SplashScreen from 'expo-splash-screen';
8 | import React from 'react';
9 | import { StyleSheet } from 'react-native';
10 | import FlashMessage from 'react-native-flash-message';
11 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
12 | import { KeyboardProvider } from 'react-native-keyboard-controller';
13 |
14 | import { APIProvider } from '@/api';
15 | import { hydrateAuth, loadSelectedTheme } from '@/lib';
16 | import { useThemeConfig } from '@/lib/use-theme-config';
17 |
18 | export { ErrorBoundary } from 'expo-router';
19 |
20 | export const unstable_settings = {
21 | initialRouteName: '(app)',
22 | };
23 |
24 | hydrateAuth();
25 | loadSelectedTheme();
26 | // Prevent the splash screen from auto-hiding before asset loading is complete.
27 | SplashScreen.preventAutoHideAsync();
28 | // Set the animation options. This is optional.
29 | SplashScreen.setOptions({
30 | duration: 500,
31 | fade: true,
32 | });
33 |
34 | export default function RootLayout() {
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | function Providers({ children }: { children: React.ReactNode }) {
47 | const theme = useThemeConfig();
48 | return (
49 |
53 |
54 |
55 |
56 |
57 | {children}
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | const styles = StyleSheet.create({
68 | container: {
69 | flex: 1,
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/src/components/inputs.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import type { OptionType } from '@/components/ui';
4 | import { Input, Select, View } from '@/components/ui';
5 | import { Checkbox, Radio, Switch } from '@/components/ui';
6 |
7 | import { Title } from './title';
8 |
9 | const options: OptionType[] = [
10 | { value: 'chocolate', label: 'Chocolate' },
11 | { value: 'strawberry', label: 'Strawberry' },
12 | { value: 'vanilla', label: 'Vanilla' },
13 | ];
14 |
15 | export const Inputs = () => {
16 | const [value, setValue] = React.useState();
17 | return (
18 | <>
19 |
20 |
21 |
22 |
23 |
24 |
34 | >
35 | );
36 | };
37 |
38 | const CheckboxExample = () => {
39 | const [checked, setChecked] = React.useState(false);
40 | return (
41 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | const RadioExample = () => {
54 | const [selected, setSelected] = React.useState(false);
55 | return (
56 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | const SwitchExample = () => {
69 | const [active, setActive] = React.useState(false);
70 | return (
71 |
77 |
78 |
79 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/ios/ObytesApp/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Expo
2 | import React
3 | import ReactAppDependencyProvider
4 |
5 | @UIApplicationMain
6 | public class AppDelegate: ExpoAppDelegate {
7 | var window: UIWindow?
8 |
9 | var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
10 | var reactNativeFactory: RCTReactNativeFactory?
11 |
12 | public override func application(
13 | _ application: UIApplication,
14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
15 | ) -> Bool {
16 | let delegate = ReactNativeDelegate()
17 | let factory = ExpoReactNativeFactory(delegate: delegate)
18 | delegate.dependencyProvider = RCTAppDependencyProvider()
19 |
20 | reactNativeDelegate = delegate
21 | reactNativeFactory = factory
22 | bindReactNativeFactory(factory)
23 |
24 | #if os(iOS) || os(tvOS)
25 | window = UIWindow(frame: UIScreen.main.bounds)
26 | factory.startReactNative(
27 | withModuleName: "main",
28 | in: window,
29 | launchOptions: launchOptions)
30 | #endif
31 |
32 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
33 | }
34 |
35 | // Linking API
36 | public override func application(
37 | _ app: UIApplication,
38 | open url: URL,
39 | options: [UIApplication.OpenURLOptionsKey: Any] = [:]
40 | ) -> Bool {
41 | return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
42 | }
43 |
44 | // Universal Links
45 | public override func application(
46 | _ application: UIApplication,
47 | continue userActivity: NSUserActivity,
48 | restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
49 | ) -> Bool {
50 | let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
51 | return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
52 | }
53 | }
54 |
55 | class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
56 | // Extension point for config-plugins
57 |
58 | override func sourceURL(for bridge: RCTBridge) -> URL? {
59 | // needed to return the correct URL for expo-dev-client.
60 | bridge.bundleURL ?? bundleURL()
61 | }
62 |
63 | override func bundleURL() -> URL? {
64 | #if DEBUG
65 | return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
66 | #else
67 | return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
68 | #endif
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/login-form.tsx:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import React from 'react';
3 | import type { SubmitHandler } from 'react-hook-form';
4 | import { useForm } from 'react-hook-form';
5 | import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
6 | import * as z from 'zod';
7 |
8 | import { Button, ControlledInput, Text, View } from '@/components/ui';
9 |
10 | const schema = z.object({
11 | name: z.string().optional(),
12 | email: z
13 | .string({
14 | required_error: 'Email is required',
15 | })
16 | .email('Invalid email format'),
17 | password: z
18 | .string({
19 | required_error: 'Password is required',
20 | })
21 | .min(6, 'Password must be at least 6 characters'),
22 | });
23 |
24 | export type FormType = z.infer;
25 |
26 | export type LoginFormProps = {
27 | onSubmit?: SubmitHandler;
28 | };
29 |
30 | export const LoginForm = ({ onSubmit = () => {} }: LoginFormProps) => {
31 | const { handleSubmit, control } = useForm({
32 | resolver: zodResolver(schema),
33 | });
34 | return (
35 |
40 |
41 |
42 |
46 | Sign In
47 |
48 |
49 |
50 | Welcome! 👋 This is a demo login screen! Feel free to use any email
51 | and password to sign in and try it out.
52 |
53 |
54 |
55 |
61 |
62 |
68 |
76 |
81 |
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/src/components/login-form.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { cleanup, screen, setup, waitFor } from '@/lib/test-utils';
4 |
5 | import type { LoginFormProps } from './login-form';
6 | import { LoginForm } from './login-form';
7 |
8 | afterEach(cleanup);
9 |
10 | const onSubmitMock: jest.Mock = jest.fn();
11 |
12 | describe('LoginForm Form ', () => {
13 | it('renders correctly', async () => {
14 | setup();
15 | expect(await screen.findByTestId('form-title')).toBeOnTheScreen();
16 | });
17 |
18 | it('should display required error when values are empty', async () => {
19 | const { user } = setup();
20 |
21 | const button = screen.getByTestId('login-button');
22 | expect(screen.queryByText(/Email is required/i)).not.toBeOnTheScreen();
23 | await user.press(button);
24 | expect(await screen.findByText(/Email is required/i)).toBeOnTheScreen();
25 | expect(screen.getByText(/Password is required/i)).toBeOnTheScreen();
26 | });
27 |
28 | it('should display matching error when email is invalid', async () => {
29 | const { user } = setup();
30 |
31 | const button = screen.getByTestId('login-button');
32 | const emailInput = screen.getByTestId('email-input');
33 | const passwordInput = screen.getByTestId('password-input');
34 |
35 | await user.type(emailInput, 'yyyyy');
36 | await user.type(passwordInput, 'test');
37 | await user.press(button);
38 |
39 | expect(await screen.findByText(/Invalid Email Format/i)).toBeOnTheScreen();
40 | expect(screen.queryByText(/Email is required/i)).not.toBeOnTheScreen();
41 | });
42 |
43 | it('Should call LoginForm with correct values when values are valid', async () => {
44 | const { user } = setup();
45 |
46 | const button = screen.getByTestId('login-button');
47 | const emailInput = screen.getByTestId('email-input');
48 | const passwordInput = screen.getByTestId('password-input');
49 |
50 | await user.type(emailInput, 'youssef@gmail.com');
51 | await user.type(passwordInput, 'password');
52 | await user.press(button);
53 | await waitFor(() => {
54 | expect(onSubmitMock).toHaveBeenCalledTimes(1);
55 | });
56 | // expect.objectContaining({}) because we don't want to test the target event we are receiving from the onSubmit function
57 | expect(onSubmitMock).toHaveBeenCalledWith(
58 | {
59 | email: 'youssef@gmail.com',
60 | password: 'password',
61 | },
62 | expect.objectContaining({})
63 | );
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/obytes/development/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.obytes.development
2 | import com.facebook.react.common.assets.ReactFontManager
3 |
4 | import android.app.Application
5 | import android.content.res.Configuration
6 |
7 | import com.facebook.react.PackageList
8 | import com.facebook.react.ReactApplication
9 | import com.facebook.react.ReactNativeHost
10 | import com.facebook.react.ReactPackage
11 | import com.facebook.react.ReactHost
12 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
13 | import com.facebook.react.defaults.DefaultReactNativeHost
14 | import com.facebook.react.soloader.OpenSourceMergedSoMapping
15 | import com.facebook.soloader.SoLoader
16 |
17 | import expo.modules.ApplicationLifecycleDispatcher
18 | import expo.modules.ReactNativeHostWrapper
19 |
20 | class MainApplication : Application(), ReactApplication {
21 |
22 | override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
23 | this,
24 | object : DefaultReactNativeHost(this) {
25 | override fun getPackages(): List {
26 | val packages = PackageList(this).packages
27 | // Packages that cannot be autolinked yet can be added manually here, for example:
28 | // packages.add(MyReactNativePackage())
29 | return packages
30 | }
31 |
32 | override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
33 |
34 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
35 |
36 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
37 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
38 | }
39 | )
40 |
41 | override val reactHost: ReactHost
42 | get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
43 |
44 | override fun onCreate() {
45 | super.onCreate()
46 | // @generated begin xml-fonts-init - expo prebuild (DO NOT MODIFY) sync-da39a3ee5e6b4b0d3255bfef95601890afd80709
47 |
48 | // @generated end xml-fonts-init
49 | SoLoader.init(this, OpenSourceMergedSoMapping)
50 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
51 | // If you opted-in for the New Architecture, we load the native entry point for this app.
52 | load()
53 | }
54 | ApplicationLifecycleDispatcher.onApplicationCreate(this)
55 | }
56 |
57 | override fun onConfigurationChanged(newConfig: Configuration) {
58 | super.onConfigurationChanged(newConfig)
59 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/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 | # Enable AAPT2 PNG crunching
26 | android.enablePngCrunchInReleaseBuilds=true
27 |
28 | # Use this property to specify which architecture you want to build.
29 | # You can also override it from the CLI using
30 | # ./gradlew -PreactNativeArchitectures=x86_64
31 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
32 |
33 | # Use this property to enable support to the new architecture.
34 | # This will allow you to use TurboModules and the Fabric render in
35 | # your application. You should enable this flag either if you want
36 | # to write custom TurboModules/Fabric components OR use libraries that
37 | # are providing them.
38 | newArchEnabled=true
39 |
40 | # Use this property to enable or disable the Hermes JS engine.
41 | # If set to false, you will be using JSC instead.
42 | hermesEnabled=true
43 |
44 | # Enable GIF support in React Native images (~200 B increase)
45 | expo.gif.enabled=true
46 | # Enable webp support in React Native images (~85 KB increase)
47 | expo.webp.enabled=true
48 | # Enable animated webp support (~3.4 MB increase)
49 | # Disabled by default because iOS doesn't support animated webp
50 | expo.webp.animated=false
51 |
52 | # Enable network inspector
53 | EX_DEV_CLIENT_NETWORK_INSPECTOR=true
54 |
55 | # Use legacy packaging to compress native libraries in the resulting APK.
56 | expo.useLegacyPackaging=false
57 |
58 | # Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
59 | expo.edgeToEdgeEnabled=true
--------------------------------------------------------------------------------
/docs/src/styles/custom.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --sl-font: 'IBM Plex Mono', sans-serif;
3 | }
4 |
5 | /* Dark mode colors. */
6 | :root {
7 | --sl-color-accent-low: #2c230a;
8 | --sl-color-accent: #846500;
9 | --sl-color-accent-high: #d4c8ab;
10 | --sl-color-white: #ffffff;
11 | --sl-color-gray-1: #eceef2;
12 | --sl-color-gray-2: #c0c2c7;
13 | --sl-color-gray-3: #888b96;
14 | --sl-color-gray-4: #545861;
15 | --sl-color-gray-5: #353841;
16 | --sl-color-gray-6: #24272f;
17 | --sl-color-black: #17181c;
18 | }
19 | /* Light mode colors. */
20 | :root[data-theme='light'] {
21 | --sl-color-accent-low: #dfd6c0;
22 | --sl-color-accent: #866700;
23 | --sl-color-accent-high: #3f3003;
24 | --sl-color-white: #17181c;
25 | --sl-color-gray-1: #24272f;
26 | --sl-color-gray-2: #353841;
27 | --sl-color-gray-3: #545861;
28 | --sl-color-gray-4: #888b96;
29 | --sl-color-gray-5: #c0c2c7;
30 | --sl-color-gray-6: #eceef2;
31 | --sl-color-gray-7: #f5f6f8;
32 | --sl-color-black: #ffffff;
33 | }
34 |
35 | :root {
36 | --purple-hsl: 205, 60%, 60%;
37 | --overlay-blurple: hsla(var(--purple-hsl), 0.4);
38 | }
39 |
40 | :root[data-theme='light'] {
41 | --purple-hsl: 255, 85%, 65%;
42 | }
43 |
44 | [data-has-hero] .page {
45 | background: linear-gradient(215deg, var(--overlay-blurple), transparent 40%),
46 | radial-gradient(var(--overlay-blurple), transparent 40%) no-repeat -60vw -40vh /
47 | 105vw 200vh,
48 | radial-gradient(var(--overlay-blurple), transparent 65%) no-repeat 50%
49 | calc(100% + 20rem) / 60rem 30rem;
50 | }
51 |
52 | [data-has-hero] header {
53 | border-bottom: 1px solid transparent;
54 | background-color: transparent;
55 | -webkit-backdrop-filter: blur(16px);
56 | backdrop-filter: blur(16px);
57 | }
58 |
59 | [data-has-hero] .hero > img {
60 | filter: drop-shadow(0 0 3rem var(--overlay-blurple));
61 | }
62 |
63 | [data-page-title] {
64 | font-size: 3rem;
65 | }
66 |
67 | /* date page title onl 2.5rem on mobile devices */
68 | @media (max-width: 768px) {
69 | [data-page-title] {
70 | font-size: 2.5rem;
71 | }
72 | }
73 |
74 | .card-grid > .card {
75 | border-radius: 10px;
76 | }
77 |
78 | .card > .title {
79 | font-size: 1.3rem;
80 | font-weight: 600;
81 | line-height: 1.2;
82 | }
83 |
84 | .embed-container {
85 | position: relative;
86 | padding-bottom: 56.25%;
87 | height: 0;
88 | overflow: hidden;
89 | max-width: 100%;
90 | }
91 | .embed-container iframe,
92 | .embed-container object,
93 | .embed-container embed {
94 | position: absolute;
95 | top: 0;
96 | left: 0;
97 | width: 100%;
98 | height: 100%;
99 | }
100 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
3 |
4 | require 'json'
5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
6 |
7 | ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
9 |
10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
11 | install! 'cocoapods',
12 | :deterministic_uuids => false
13 |
14 | prepare_react_native_project!
15 |
16 | target 'ObytesApp' do
17 | use_expo_modules!
18 |
19 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
20 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
21 | else
22 | config_command = [
23 | 'npx',
24 | 'expo-modules-autolinking',
25 | 'react-native-config',
26 | '--json',
27 | '--platform',
28 | 'ios'
29 | ]
30 | end
31 |
32 | config = use_native_modules!(config_command)
33 |
34 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
35 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
36 |
37 | use_react_native!(
38 | :path => config[:reactNativePath],
39 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
40 | # An absolute path to your application root.
41 | :app_path => "#{Pod::Config.instance.installation_root}/..",
42 | :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
43 | )
44 |
45 | post_install do |installer|
46 | react_native_post_install(
47 | installer,
48 | config[:reactNativePath],
49 | :mac_catalyst_enabled => false,
50 | :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
51 | )
52 |
53 | # This is necessary for Xcode 14, because it signs resource bundles by default
54 | # when building for devices.
55 | installer.target_installation_results.pod_target_installation_results
56 | .each do |pod_name, target_installation_result|
57 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
58 | resource_bundle_target.build_configurations.each do |config|
59 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
60 | end
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/.github/workflows/new-app-version.yml:
--------------------------------------------------------------------------------
1 | # 🔗 Links:
2 | # Source file: https://github.com/obytes/react-native-template-obytes/blob/master/.github/workflows/lint-ts.yml
3 | # Starter releasing process: https://starter.obytes.com/ci-cd/app-releasing-process/
4 |
5 | # ✍️ Description:
6 | # This workflow is used to create a new version of the app and push a new tag to the repo.
7 | # As this workflow will push code to the repo, we set up GitHub Bot as a Git user.
8 | # This Workflow need to be triggered manually from the Actions tab in the repo.
9 | # 1. Choose your release type (patch, minor, major)
10 | # 2. The workflow will run the np-release script which runs the following steps:
11 | # - Bump the version in package.json based on the release type using np
12 | # - Run the prebuild of the app to align the package.json version with the native code
13 | # - Create a new tag with the new version
14 | # - Push the new tag to the repo
15 | #
16 |
17 | # 🚨 GITHUB SECRETS REQUIRED:
18 | # - GH_TOKEN: A GitHub token with write repo access.
19 | # You can generate one from here: https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
20 | # make sure to add it to the repo secrets with the name GH_TOKEN
21 |
22 | name: New App Version
23 |
24 | on:
25 | workflow_dispatch:
26 | inputs:
27 | release-type:
28 | type: choice
29 | description: 'Release type (one of): patch, minor, major'
30 | required: true
31 | default: 'patch'
32 | options:
33 | - patch
34 | - minor
35 | - major
36 |
37 | jobs:
38 | release:
39 | name: Create New Version and push new tag
40 | runs-on: ubuntu-latest
41 | permissions:
42 | contents: write
43 | steps:
44 | - name: 🔍 GH_TOKEN
45 | if: env.GH_TOKEN == ''
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | run: echo "GH_TOKEN=${GITHUB_TOKEN}" >> $GITHUB_ENV
49 | - name: 📦 Checkout project repo
50 | uses: actions/checkout@v4
51 | with:
52 | fetch-depth: 0
53 | token: ${{ secrets.GH_TOKEN }}
54 |
55 | - name: 📝 Git User Setup
56 | run: |
57 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
58 | git config --global user.name "github-actions[bot]"
59 |
60 | - name: 📦 Setup Node + PNPM + install deps
61 | uses: ./.github/actions/setup-node-pnpm-install
62 |
63 | - name: 🏃♂️ Run App release
64 | run: |
65 | pnpm app-release ${{ github.event.inputs.release-type }}
66 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/obytes/development/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.obytes.development
2 | import expo.modules.splashscreen.SplashScreenManager
3 |
4 | import android.os.Build
5 | import android.os.Bundle
6 |
7 | import com.facebook.react.ReactActivity
8 | import com.facebook.react.ReactActivityDelegate
9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
10 | import com.facebook.react.defaults.DefaultReactActivityDelegate
11 |
12 | import expo.modules.ReactActivityDelegateWrapper
13 |
14 | class MainActivity : ReactActivity() {
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | // Set the theme to AppTheme BEFORE onCreate to support
17 | // coloring the background, status bar, and navigation bar.
18 | // This is required for expo-splash-screen.
19 | // setTheme(R.style.AppTheme);
20 | // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
21 | SplashScreenManager.registerOnActivity(this)
22 | // @generated end expo-splashscreen
23 | super.onCreate(null)
24 | }
25 |
26 | /**
27 | * Returns the name of the main component registered from JavaScript. This is used to schedule
28 | * rendering of the component.
29 | */
30 | override fun getMainComponentName(): String = "main"
31 |
32 | /**
33 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
34 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
35 | */
36 | override fun createReactActivityDelegate(): ReactActivityDelegate {
37 | return ReactActivityDelegateWrapper(
38 | this,
39 | BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
40 | object : DefaultReactActivityDelegate(
41 | this,
42 | mainComponentName,
43 | fabricEnabled
44 | ){})
45 | }
46 |
47 | /**
48 | * Align the back button behavior with Android S
49 | * where moving root activities to background instead of finishing activities.
50 | * @see onBackPressed
51 | */
52 | override fun invokeDefaultOnBackPressed() {
53 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
54 | if (!moveTaskToBack(false)) {
55 | // For non-root activities, use the default implementation to finish them.
56 | super.invokeDefaultOnBackPressed()
57 | }
58 | return
59 | }
60 |
61 | // Use the default back button implementation on Android S
62 | // because it's doing more than [Activity.moveTaskToBack] in fact.
63 | super.invokeDefaultOnBackPressed()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/(app)/settings.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/react-in-jsx-scope */
2 | import { Env } from '@env';
3 | import { useColorScheme } from 'nativewind';
4 |
5 | import { Item } from '@/components/settings/item';
6 | import { ItemsContainer } from '@/components/settings/items-container';
7 | import { LanguageItem } from '@/components/settings/language-item';
8 | import { ThemeItem } from '@/components/settings/theme-item';
9 | import {
10 | colors,
11 | FocusAwareStatusBar,
12 | ScrollView,
13 | Text,
14 | View,
15 | } from '@/components/ui';
16 | import { Github, Rate, Share, Support, Website } from '@/components/ui/icons';
17 | import { translate, useAuth } from '@/lib';
18 |
19 | export default function Settings() {
20 | const signOut = useAuth.use.signOut();
21 | const { colorScheme } = useColorScheme();
22 | const iconColor =
23 | colorScheme === 'dark' ? colors.neutral[400] : colors.neutral[500];
24 | return (
25 | <>
26 |
27 |
28 |
29 |
30 |
31 | {translate('settings.title')}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | }
47 | onPress={() => {}}
48 | />
49 | }
52 | onPress={() => {}}
53 | />
54 | }
57 | onPress={() => {}}
58 | />
59 |
60 |
61 |
62 | - {}} />
63 |
- {}} />
64 |
}
67 | onPress={() => {}}
68 | />
69 | }
72 | onPress={() => {}}
73 | />
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | >
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/cli/setup-project.js:
--------------------------------------------------------------------------------
1 | const { execShellCommand, runCommand } = require('./utils.js');
2 | const { consola } = require('consola');
3 | const fs = require('fs-extra');
4 | const path = require('path');
5 |
6 | const initGit = async (projectName) => {
7 | await execShellCommand(`cd ${projectName} && git init && cd ..`);
8 | };
9 |
10 | const installDeps = async (projectName) => {
11 | await runCommand(`cd ${projectName} && pnpm install`, {
12 | loading: 'Installing project dependencies',
13 | success: 'Dependencies installed',
14 | error: 'Failed to install dependencies, Make sure you have pnpm installed',
15 | });
16 | };
17 |
18 | // remove unnecessary files, such us .git, ios, android, docs, cli, LICENSE
19 | const removeFiles = async (projectName) => {
20 | const FILES_TO_REMOVE = [
21 | '.git',
22 | 'README.md',
23 | 'ios',
24 | 'android',
25 | 'docs',
26 | 'cli',
27 | 'LICENSE',
28 | ];
29 |
30 | FILES_TO_REMOVE.forEach((file) => {
31 | fs.removeSync(path.join(process.cwd(), `${projectName}/${file}`));
32 | });
33 | };
34 |
35 | // Update package.json infos, name and set version to 0.0.1 + add initial version to osMetadata
36 | const updatePackageInfos = async (projectName) => {
37 | const packageJsonPath = path.join(
38 | process.cwd(),
39 | `${projectName}/package.json`
40 | );
41 | const packageJson = fs.readJsonSync(packageJsonPath);
42 | packageJson.osMetadata = { initVersion: packageJson.version };
43 | packageJson.version = '0.0.1';
44 | packageJson.name = projectName?.toLowerCase();
45 | packageJson.repository = {
46 | type: 'git',
47 | url: 'git+https://github.com/user/repo-name.git',
48 | };
49 | fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
50 | };
51 |
52 | const updateProjectConfig = async (projectName) => {
53 | const configPath = path.join(process.cwd(), `${projectName}/env.js`);
54 | const contents = fs.readFileSync(configPath, {
55 | encoding: 'utf-8',
56 | });
57 | const replaced = contents
58 | .replace(/ObytesApp/gi, projectName)
59 | .replace(/com.obytes/gi, `com.${projectName.toLowerCase()}`)
60 | .replace(/obytes/gi, 'expo-owner');
61 |
62 | fs.writeFileSync(configPath, replaced, { spaces: 2 });
63 | const readmeFilePath = path.join(
64 | process.cwd(),
65 | `${projectName}/README-project.md`
66 | );
67 | fs.renameSync(
68 | readmeFilePath,
69 | path.join(process.cwd(), `${projectName}/README.md`)
70 | );
71 | };
72 |
73 | const setupProject = async (projectName) => {
74 | consola.start(`Clean up and setup your project 🧹`);
75 | try {
76 | removeFiles(projectName);
77 | await initGit(projectName);
78 | updatePackageInfos(projectName);
79 | updateProjectConfig(projectName);
80 | consola.success(`Clean up and setup your project 🧹`);
81 | } catch (error) {
82 | consola.error(`Failed to clean up project folder`, error);
83 | process.exit(1);
84 | }
85 | };
86 |
87 | module.exports = {
88 | setupProject,
89 | installDeps,
90 | };
91 |
--------------------------------------------------------------------------------