diff --git a/.dooboo/expo b/.dooboo/expo new file mode 100644 index 0000000..24ce80c --- /dev/null +++ b/.dooboo/expo @@ -0,0 +1,5 @@ +/** + * dooboo-cli by dooboolab + * Do not delete this folder and files if you are using dooboo-cli + * https://www.npmjs.com/package/dooboo + */ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..bb3d978 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +**/*.js +# **/*/__tests__/**/* +node_modules/ +assets/langs/ios/ + +# types +*.d.ts diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..4910950 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,32 @@ +const path = require('path'); + +module.exports = { + root: true, + extends: [ + '@dooboo/eslint-config-react-native', + 'plugin:i18n-json/recommended', + ], + rules: { + 'eslint-comments/no-unlimited-disable': 0, + 'eslint-comments/no-unused-disable': 0, + 'i18n-json/identical-keys': [ + 2, + { + filePath: path.resolve('./assets/langs/ko.json'), + }, + ], + 'i18n-json/sorted-keys': [ + 2, + { + order: 'asc', + indentSpaces: 2, + }, + ], + 'i18n-json/valid-message-syntax': [ + 2, + { + syntax: path.resolve('./custom-syntax-validator.ts'), + }, + ], + }, +}; diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..a4378cc --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ba572e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18.x + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install modules + run: bun install --immutable + + - name: Check linting + run: bun lint:all + + - name: Build typescript + run: bun tsc + + - name: Remove dist + run: rm -rf ./dist + + - name: Test + run: bun run test --coverage --silent + + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v1 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # directory: ./coverage/ + # flags: unittests + # name: codecov-umbrella + # fail_ci_if_error: false + # path_to_write_report: ./coverage/codecov_report.gz diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..11f16ca --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,28 @@ +name: Deploy Web on Release +on: + workflow_dispatch: + release: + types: [created] + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + - uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Install modules + run: bun install --immutable + + - name: Build web + run: bun build:web + env: + ROOT_URL: ${{ secrets.ROOT_URL }} + expoProjectId: ${{ secrets.expoProjectId }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77e4e2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Expo +.expo/* +web-build/ + +# Yarn +.yarn/* +!.yarn/releases + +.env* +.jest + +# Android +android + +# iOS +ios + + + +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..4974c35 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..e957ca6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +bun lint:ci +bun lint:i18n \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..dab4906 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,8 @@ +// prettier.config.js or .prettierrc.js +module.exports = { + trailingComma: 'all', + arrowParens: 'always', + singleQuote: true, + jsxSingleQuote: false, + bracketSpacing: false, +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..6fc23dc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "formulahendry.auto-rename-tag", + "aaron-bond.better-comments", + "vscode-icons-team.vscode-icons", + "streetsidesoftware.code-spell-checker", + "wix.vscode-import-cost", + "styled-components.vscode-styled-components", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9a11947 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to packager", + "cwd": "${workspaceFolder}", + "type": "reactnative", + "request": "attach" + } + ] +} + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..07d2a0d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,45 @@ +{ + "eslint.enable": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "javascript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.importModuleSpecifier": "relative", + "prettier.configPath": ".prettierrc.js", + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.classpath": true, + "**/.project": true, + "**/.settings": true, + "**/.factorypath": true, + }, + "cSpell.words": [ + "crossplatformkorea", + "dooboo", + "Pressable", + "Pretendard" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..565158f --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Expo Starter with Router + +[![CI](https://github.com/dooboolab-community/expo-router-starter/actions/workflows/ci.yml/badge.svg)](https://github.com/dooboolab-community/expo-router-starter/actions/workflows/ci.yml) + +The `expo` template generated with `dooboo-cli`. + +We believe that the fastest way to build the app is using [Expo](https://expo.io). +You can create app even more easily with the cli tool [dooboo-cli](https://github.com/dooboolab-community/dooboo-cli). + +## Stacks used + +- [react-native](https://github.com/facebook/react-native) +- [expo-router](https://expo.github.io/router) +- [emotion](https://emotion.sh) +- [dooboo-ui](https://github.com/dooboolab/dooboo-ui) +- [jest](https://github.com/facebook/jest) +- [react-native-testing-library](https://github.com/callstack/react-native-testing-library) +- [typescript](https://github.com/Microsoft/TypeScript) +- [ts-jest](https://github.com/kulshekhar/ts-jest) +- [prettier](https://prettier.io) +- [react-native-web](https://github.com/necolas/react-native-web) +- [expo-localization](https://docs.expo.dev/versions/latest/sdk/localization) + +## Quick News + +- In default, [dooboo-ui](https://github.com/dooboolab/dooboo-ui), a ui framework for [Expo](https://expo.io) is preinstalled in the project. Hope you like it ๐Ÿงก. +- Default package manager is set to [bun](https://bun.sh) which is fastest in 2023. \ No newline at end of file diff --git a/__mocks__/expo-asset.ts b/__mocks__/expo-asset.ts new file mode 100644 index 0000000..8ab6cc1 --- /dev/null +++ b/__mocks__/expo-asset.ts @@ -0,0 +1,3 @@ +export default { + loadSingleFontAsync: jest.fn(), +}; diff --git a/__mocks__/sentry-expo.ts b/__mocks__/sentry-expo.ts new file mode 100644 index 0000000..2a03407 --- /dev/null +++ b/__mocks__/sentry-expo.ts @@ -0,0 +1,3 @@ +export const Native = { + captureException: jest.fn(), +}; diff --git a/app.config.ts b/app.config.ts new file mode 100644 index 0000000..caa2344 --- /dev/null +++ b/app.config.ts @@ -0,0 +1,127 @@ +import 'dotenv/config'; + +import type {ConfigContext, ExpoConfig} from '@expo/config'; +import withAndroidLocalizedName from '@mmomtchev/expo-android-localized-app-name'; +import dotenv from 'dotenv'; +import {expand} from 'dotenv-expand'; +import path from 'path'; + +import {version} from './package.json'; + +// https://github.com/expo/expo/issues/23727#issuecomment-1651609858 +if (process.env.STAGE) { + expand( + dotenv.config({ + path: path.join( + __dirname, + ['./.env', process.env.STAGE].filter(Boolean).join('.'), + ), + override: true, + }), + ); +} + +const DEEP_LINK_URL = '[firebaseAppId].web.app'; + +const buildNumber = 1; + +export default ({config}: ConfigContext): ExpoConfig => ({ + ...config, + name: 'Cross-Platform Korea', + scheme: 'CPK', + slug: 'CPK-Slug', + privacy: 'public', + platforms: ['ios', 'android', 'web'], + version, + orientation: 'default', + icon: './assets/icon.png', + plugins: [ + // @ts-ignore + withAndroidLocalizedName, + 'expo-router', + 'expo-tracking-transparency', + 'expo-localization', + [ + 'expo-font', + { + fonts: [ + 'node_modules/dooboo-ui/uis/Icon/doobooui.ttf', + 'node_modules/dooboo-ui/uis/Icon/Pretendard-Bold.otf', + 'node_modules/dooboo-ui/uis/Icon/Pretendard-Regular.otf', + 'node_modules/dooboo-ui/uis/Icon/Pretendard-Thin.otf', + ], + }, + ], + ], + experiments: { + typedRoutes: true, + }, + splash: { + image: './assets/splash.png', + resizeMode: 'contain', + backgroundColor: '#1B1B1B', + }, + extra: { + ROOT_URL: process.env.ROOT_URL, + googleClientIdIOS: process.env.googleClientIdIOS, + googleClientIdAndroid: process.env.googleClientIdAndroid, + googleClientIdWeb: process.env.googleClientIdWeb, + facebookAppId: process.env.facebookAppId, + expoProjectId: process.env.expoProjectId, + firebaseWebApiKey: process.env.firebaseWebApiKey, + // eas: {projectId: ''}, + }, + updates: { + fallbackToCacheTimeout: 0, + // requestHeaders: {'expo-channel-name': 'production'}, + // url: '', + }, + assetBundlePatterns: ['**/*'], + userInterfaceStyle: 'automatic', + locales: { + ko: './assets/langs/ios/ko.json', + }, + ios: { + buildNumber: buildNumber.toString(), + bundleIdentifier: 'com.crossplatformkorea', + associatedDomains: [`applinks:${DEEP_LINK_URL}`], + supportsTablet: true, + entitlements: { + 'com.apple.developer.applesignin': ['Default'], + }, + infoPlist: { + CFBundleAllowMixedLocalizations: true, + }, + }, + android: { + userInterfaceStyle: 'automatic', + permissions: [ + 'RECEIVE_BOOT_COMPLETED', + 'CAMERA', + 'CAMERA_ROLL', + 'READ_EXTERNAL_STORAGE', + 'WRITE_EXTERNAL_STORAGE', + 'NOTIFICATIONS', + 'USER_FACING_NOTIFICATIONS', + ], + adaptiveIcon: { + foregroundImage: './assets/adaptive_icon.png', + backgroundColor: '#2F2F2F', + }, + package: 'com.crossplatformkorea', + intentFilters: [ + { + action: 'VIEW', + autoVerify: true, + data: { + scheme: 'https', + host: DEEP_LINK_URL, + pathPrefix: '/', + }, + category: ['BROWSABLE', 'DEFAULT'], + }, + ], + }, + description: 'Starter project from dooboo-cli.', + web: {bundler: 'metro', favicon: './assets/favicon.png'}, +}); diff --git a/app/_layout.tsx b/app/_layout.tsx new file mode 100644 index 0000000..b634c7b --- /dev/null +++ b/app/_layout.tsx @@ -0,0 +1,144 @@ +import {useEffect, useState} from 'react'; +import type {ColorSchemeName} from 'react-native'; +import {Platform, useColorScheme} from 'react-native'; +import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import {dark, light} from '@dooboo-ui/theme'; +import styled, {css} from '@emotion/native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import {Icon, useDooboo} from 'dooboo-ui'; +import CustomPressable from 'dooboo-ui/uis/CustomPressable'; +import StatusBarBrightness from 'dooboo-ui/uis/StatusbarBrightness'; +import {Stack, useRouter} from 'expo-router'; +import * as SplashScreen from 'expo-splash-screen'; +import * as SystemUI from 'expo-system-ui'; + +import RootProvider from '../src/providers'; +import { + AsyncStorageKey, + COMPONENT_WIDTH, + delayPressIn, + WEB_URL, +} from '../src/utils/constants'; + +SplashScreen.preventAutoHideAsync(); + +const Container = styled.View` + flex: 1; + align-self: stretch; + background-color: ${({theme}) => theme.bg.paper}; +`; + +const Content = styled.View` + align-self: center; + width: 100%; + flex: 1; + max-width: ${COMPONENT_WIDTH + 'px'}; + background-color: ${({theme}) => theme.bg.basic}; +`; + +function Layout(): JSX.Element | null { + const {assetLoaded, theme} = useDooboo(); + const {back, replace} = useRouter(); + + useEffect(() => { + if (assetLoaded) { + SplashScreen.hideAsync(); + } + }, [assetLoaded]); + + if (!assetLoaded) { + return null; + } + + return ( + + + + canGoBack && ( + + canGoBack + ? back() + : Platform.OS === 'web' + ? (window.location.href = WEB_URL) + : replace('/') + } + style={ + Platform.OS === 'web' + ? css` + padding: 8px; + border-radius: 48px; + ` + : css` + padding: 8px; + border-radius: 48px; + margin-left: -8px; + ` + } + > + + + ), + }} + > + {/* Note: Only modals are written here. */} + + + + ); +} + +export default function RootLayout(): JSX.Element | null { + const colorScheme = useColorScheme(); + const [localThemeType, setLocalThemeType] = useState( + undefined, + ); + + // ํ…Œ๋งˆ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + useEffect(() => { + const initializeThemeType = async (): Promise => { + const darkMode = await AsyncStorage.getItem(AsyncStorageKey.DarkMode); + + const isDarkMode = !darkMode + ? colorScheme === 'dark' + : darkMode === 'true'; + + SystemUI.setBackgroundColorAsync( + isDarkMode ? dark.bg.basic : light.bg.basic, + ); + + setLocalThemeType(isDarkMode ? 'dark' : 'light'); + }; + + initializeThemeType(); + }, [colorScheme]); + + if (!localThemeType) { + return null; + } + + return ( + + + <> + + + + + + ); +} diff --git a/app/details.tsx b/app/details.tsx new file mode 100644 index 0000000..9ba786c --- /dev/null +++ b/app/details.tsx @@ -0,0 +1,30 @@ +import styled from '@emotion/native'; +import {Typography} from 'dooboo-ui'; +import {Stack} from 'expo-router'; + +import {t} from '../src/STRINGS'; + +const Container = styled.View` + flex: 1; + align-self: stretch; + background-color: ${({theme}) => theme.bg.basic}; +`; + +const Content = styled.View` + padding: 16px; +`; + +export default function Details(): JSX.Element { + return ( + + + + {t('DETAILS')} + + + ); +} diff --git a/app/index.tsx b/app/index.tsx new file mode 100644 index 0000000..8bcad3f --- /dev/null +++ b/app/index.tsx @@ -0,0 +1,64 @@ +import styled, {css} from '@emotion/native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import {Button, SwitchToggle, useDooboo} from 'dooboo-ui'; +import {Stack, useRouter} from 'expo-router'; + +import {t} from '../src/STRINGS'; +import {AsyncStorageKey} from '../src/utils/constants'; + +const Container = styled.View` + background-color: ${({theme}) => theme.bg.basic}; + + flex: 1; + align-self: stretch; + justify-content: center; + align-items: center; +`; + +const Content = styled.View` + padding: 16px; + + justify-content: center; + align-items: center; +`; + +export default function Index(): JSX.Element { + const {themeType, changeThemeType} = useDooboo(); + const {push} = useRouter(); + + return ( + + + + { + const nextTheme = themeType === 'dark' ? 'light' : 'dark'; + AsyncStorage.setItem( + AsyncStorageKey.DarkMode, + themeType === 'dark' ? 'false' : 'true', + ); + changeThemeType(nextTheme); + }} + /> +