├── .watchmanconfig ├── .node-version ├── .gitattributes ├── .eslintignore ├── app.json ├── dump.rdb ├── src ├── @types │ ├── image.d.ts │ └── Navigation │ │ └── index.d.ts ├── domain │ ├── models │ │ ├── index.ts │ │ ├── todo.ts │ │ ├── todos.ts │ │ ├── todo.test.ts │ │ └── todos.test.ts │ └── repositories │ │ └── todos.ts ├── components │ ├── organisms │ │ ├── index.ts │ │ ├── Carousel.tsx │ │ └── Todos │ │ │ ├── DeleteButton.tsx │ │ │ ├── DoneButton.tsx │ │ │ ├── index.tsx │ │ │ └── Todo.tsx │ ├── molecules │ │ ├── index.ts │ │ ├── ErrorPanel.tsx │ │ ├── NetworkPanel.tsx │ │ ├── Todo.tsx │ │ ├── CarouselItem.tsx │ │ └── ProgressPanel.tsx │ ├── atoms │ │ ├── Avatar.tsx │ │ ├── HeaderText.tsx │ │ ├── index.ts │ │ ├── Progress.tsx │ │ ├── Logo.tsx │ │ ├── Pagination.tsx │ │ ├── IconButton.tsx │ │ ├── LabelValueContainer.tsx │ │ ├── Button.tsx │ │ └── TextField.tsx │ └── pages │ │ ├── index.ts │ │ ├── Statistics │ │ └── index.tsx │ │ ├── ChooseLogin │ │ └── index.tsx │ │ ├── SignIn │ │ ├── SignInWithGoogle.tsx │ │ └── index.tsx │ │ ├── Home │ │ └── index.tsx │ │ ├── Initial │ │ └── index.tsx │ │ ├── UserInfo │ │ └── index.tsx │ │ ├── Loading │ │ └── index.tsx │ │ ├── Input │ │ └── index.tsx │ │ ├── Detail │ │ └── index.tsx │ │ └── SignUp │ │ └── index.tsx ├── lib │ ├── window.ts │ ├── format-date.ts │ ├── hooks │ │ ├── index.ts │ │ ├── use-controlled-component.ts │ │ └── use-networker.ts │ ├── round.ts │ ├── sleep.ts │ ├── local-store │ │ ├── index.ts │ │ ├── initial-launch.ts │ │ ├── initial-launch.test.ts │ │ ├── user-information.ts │ │ └── user-information.test.ts │ ├── window.test.ts │ ├── firebase │ │ ├── firestore.ts │ │ ├── sign-out.ts │ │ ├── register-user.ts │ │ ├── sign-in-with-password.ts │ │ └── sign-in-with-google.ts │ ├── format-date.test.ts │ ├── assert.ts │ ├── round.test.ts │ └── assert.test.ts ├── contexts │ ├── index.ts │ ├── user.ts │ ├── network.ts │ └── ui.ts ├── routes │ ├── Header │ │ ├── index.ts │ │ └── HeaderLeft.tsx │ ├── Main │ │ ├── UserInfo.tsx │ │ ├── Home.tsx │ │ ├── Statistics.tsx │ │ └── index.tsx │ └── index.tsx ├── constants │ ├── theme.ts │ ├── path.ts │ └── testIDs.ts ├── store.ts ├── containers │ ├── index.ts │ ├── Statistics.tsx │ ├── SignIn.tsx │ ├── SignUp.tsx │ ├── Loading.tsx │ ├── Input.tsx │ ├── Detail.tsx │ └── Home.tsx ├── modules │ ├── index.ts │ ├── todos.ts │ └── todos.test.ts ├── usecases │ ├── todos.ts │ └── todos.test.ts ├── selectors │ ├── todos.ts │ └── todos.test.ts └── App.tsx ├── assets ├── icon.png ├── person.png ├── splash.png ├── initial_bg.jpg ├── reactIcon.jpg └── signin-with-google@3x.png ├── babel.config.js ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── debug │ │ │ ├── res │ │ │ │ └── values │ │ │ │ │ └── strings.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── google-services.json │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── Entypo.ttf │ │ │ │ │ ├── Zocial.ttf │ │ │ │ │ ├── AntDesign.ttf │ │ │ │ │ ├── EvilIcons.ttf │ │ │ │ │ ├── Feather.ttf │ │ │ │ │ ├── Fontisto.ttf │ │ │ │ │ ├── Ionicons.ttf │ │ │ │ │ ├── Octicons.ttf │ │ │ │ │ ├── FontAwesome.ttf │ │ │ │ │ ├── Foundation.ttf │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ │ └── MaterialCommunityIcons.ttf │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── januswel │ │ │ │ │ └── praiser │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── januswel │ │ │ │ └── praiser │ │ │ │ └── DetoxTest.java │ │ └── release │ │ │ └── google-services.json │ ├── proguard-rules.pro │ ├── build_defs.bzl │ └── BUCK ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── gradle.properties ├── build.gradle └── gradlew.bat ├── ios ├── praiser │ ├── Images.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── icon@2x.png │ │ │ ├── icon@3x.png │ │ │ ├── settings@2x.png │ │ │ ├── settings@3x.png │ │ │ ├── store-icon.png │ │ │ ├── spotlight@2x.png │ │ │ ├── spotlight@3x.png │ │ │ ├── notification@2x.png │ │ │ ├── notification@3x.png │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.m │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.xib ├── praiser.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── praiserTests │ ├── Info.plist │ └── praiserTests.m ├── praiser-tvOSTests │ └── Info.plist ├── GoogleService-Info.plist.production ├── GoogleService-Info.plist.development ├── praiser-tvOS │ └── Info.plist ├── Podfile └── praiser.xcodeproj │ └── xcshareddata │ └── xcschemes │ ├── praiser-tvOS.xcscheme │ └── praiser.xcscheme ├── __mocks__ ├── @react-native-community │ └── async-storage.ts └── @react-native-firebase │ ├── analytics.ts │ └── firestore.ts ├── .buckconfig ├── e2e ├── config.json ├── lib │ └── utils.js └── init.js ├── index.js ├── .editorconfig ├── metro.config.js ├── tsconfig.json ├── README.md ├── .eslintrc.js ├── scripts └── test-e2e.js ├── .eslintrc.yml ├── .prettierrc.yml ├── .circleci └── config.yml ├── .gitignore ├── .flowconfig ├── privacy-policy.html └── package.json /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.16.1 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | *.js 3 | *.d.ts 4 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "praiser", 3 | "displayName": "praiser" 4 | } 5 | -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/dump.rdb -------------------------------------------------------------------------------- /src/@types/image.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpg'; 3 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/assets/person.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/initial_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/assets/initial_bg.jpg -------------------------------------------------------------------------------- /assets/reactIcon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/assets/reactIcon.jpg -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /assets/signin-with-google@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/assets/signin-with-google@3x.png -------------------------------------------------------------------------------- /android/app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 開praiser 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | praiser 3 | 4 | -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/models/index.ts: -------------------------------------------------------------------------------- 1 | import * as Todo from './todo'; 2 | import * as Todos from './todos'; 3 | 4 | export { Todo, Todos }; 5 | -------------------------------------------------------------------------------- /src/components/organisms/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Carousel } from './Carousel'; 2 | export { default as Todos } from './Todos'; 3 | -------------------------------------------------------------------------------- /__mocks__/@react-native-community/async-storage.ts: -------------------------------------------------------------------------------- 1 | export { default } from '@react-native-community/async-storage/jest/async-storage-mock'; 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /src/lib/window.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const { width, height } = Dimensions.get('window'); 4 | export { width, height }; 5 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/lib/format-date.ts: -------------------------------------------------------------------------------- 1 | export default function formatDate(date: Date) { 2 | return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; 3 | } 4 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useControlledComponent } from './use-controlled-component'; 2 | export { default as useNetworker } from './use-networker'; 3 | -------------------------------------------------------------------------------- /src/lib/round.ts: -------------------------------------------------------------------------------- 1 | export default function round(src: number, digit = 1) { 2 | const base = Math.pow(10, digit - 1); 3 | return Math.round(src * base) / base; 4 | } 5 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/icon@2x.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/icon@3x.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/settings@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/settings@2x.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/settings@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/settings@3x.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/store-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/store-icon.png -------------------------------------------------------------------------------- /src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export { Context as UiContext } from './ui'; 2 | export { Context as NetworkContext } from './network'; 3 | export { Context as UserContext } from './user'; 4 | -------------------------------------------------------------------------------- /src/lib/sleep.ts: -------------------------------------------------------------------------------- 1 | export default function sleep(ms: number) { 2 | return new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, ms); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/spotlight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/spotlight@2x.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/spotlight@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/spotlight@3x.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/notification@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/notification@2x.png -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/notification@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-jp/praiser/HEAD/ios/praiser/Images.xcassets/AppIcon.appiconset/notification@3x.png -------------------------------------------------------------------------------- /e2e/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "setupFilesAfterEnv": ["./init.js"], 3 | "testEnvironment": "node", 4 | "reporters": ["detox/runners/jest/streamlineReporter"], 5 | "verbose": true 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/local-store/index.ts: -------------------------------------------------------------------------------- 1 | import * as InitialLaunch from './initial-launch'; 2 | import * as UserInformation from './user-information'; 3 | 4 | export { InitialLaunch, UserInformation }; 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './src/App'; 3 | import { name as appName } from './app.json'; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /src/@types/Navigation/index.d.ts: -------------------------------------------------------------------------------- 1 | import { NavigationParams, NavigationScreenProp, NavigationState } from '@react-navigation/native'; 2 | 3 | declare type Navigation = NavigationScreenProp; 4 | -------------------------------------------------------------------------------- /src/lib/window.test.ts: -------------------------------------------------------------------------------- 1 | import { width, height } from './window'; 2 | 3 | describe('window', () => { 4 | it('returns', () => { 5 | expect(width).toBeGreaterThan(0); 6 | expect(height).toBeGreaterThan(0); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /__mocks__/@react-native-firebase/analytics.ts: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | logEvent(_eventName: string, _options: { id: string; name: string }) { 4 | return new Promise(resolve => resolve()); 5 | }, 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/firebase/firestore.ts: -------------------------------------------------------------------------------- 1 | import firestore from '@react-native-firebase/firestore'; 2 | 3 | export default function getFirestore(uid: string) { 4 | return firestore() 5 | .collection('users') 6 | .doc(uid) 7 | .collection('todos'); 8 | } 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/routes/Header/index.ts: -------------------------------------------------------------------------------- 1 | import { COLOR } from '../../constants/theme'; 2 | 3 | export { default as HeaderLeft } from './HeaderLeft'; 4 | export const headerStyle = { 5 | backgroundColor: COLOR.MAIN, 6 | }; 7 | export const headerTintColor = COLOR.PRIMARY; 8 | -------------------------------------------------------------------------------- /src/lib/firebase/sign-out.ts: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import auth from '@react-native-firebase/auth'; 3 | 4 | export default async function signOut() { 5 | await analytics().resetAnalyticsData(); 6 | await auth().signOut(); 7 | } 8 | -------------------------------------------------------------------------------- /src/constants/theme.ts: -------------------------------------------------------------------------------- 1 | export const COLOR = { 2 | MAIN: '#333', 3 | MAIN_LIGHT: '#555', 4 | MAIN_DARK: '#222', 5 | PRIMARY: '#00d8ff', 6 | SECONDARY: '#008080', 7 | WHITE: '#FFF', 8 | CAROUSEL_BACKGROUND: 'rgba(0, 0, 0, 0.5)', 9 | CAUTION: '#ff0000', 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/format-date.test.ts: -------------------------------------------------------------------------------- 1 | import formatDate from './format-date'; 2 | 3 | describe('formatDate', () => { 4 | it('returns formatted date', () => { 5 | const xmas2019 = new Date(2019, 11, 25); 6 | const actual = formatDate(xmas2019); 7 | expect(actual).toBe('2019/12/25'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /ios/praiser.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/molecules/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CarouselItem } from './CarouselItem'; 2 | export { default as Todo } from './Todo'; 3 | export { default as ProgressPanel } from './ProgressPanel'; 4 | export { default as NetworkPanel } from './NetworkPanel'; 5 | export { default as ErrorPanel } from './ErrorPanel'; 6 | -------------------------------------------------------------------------------- /ios/praiser.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore as create } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import appReducer, { createInitialState } from './modules'; 5 | 6 | export function createStore() { 7 | return create(appReducer, createInitialState(), applyMiddleware(thunk)); 8 | } 9 | 10 | export default createStore(); 11 | -------------------------------------------------------------------------------- /src/containers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Detail } from './Detail'; 2 | export { default as Home } from './Home'; 3 | export { default as Input } from './Input'; 4 | export { default as Loading } from './Loading'; 5 | export { default as SignIn } from './SignIn'; 6 | export { default as SignUp } from './SignUp'; 7 | export { default as Statistics } from './Statistics'; 8 | -------------------------------------------------------------------------------- /src/lib/hooks/use-controlled-component.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function useControlledComponent(initialValue: T) { 4 | const [value, setValue] = React.useState(initialValue); 5 | 6 | function onChangeText(newValue: T) { 7 | setValue(newValue); 8 | } 9 | 10 | return { 11 | value, 12 | onChangeText, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/local-store/initial-launch.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | 3 | const KEY = 'firstopen'; 4 | 5 | export async function isInitialLaunch() { 6 | const opened = await AsyncStorage.getItem(KEY); 7 | return !!opened; 8 | } 9 | 10 | export async function markAsTutorialIsDone() { 11 | await AsyncStorage.setItem(KEY, JSON.stringify(true)); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/assert.ts: -------------------------------------------------------------------------------- 1 | export function assert(condition: boolean, msg?: string): asserts condition { 2 | if (!condition) { 3 | throw new Error(msg); 4 | } 5 | } 6 | 7 | export function assertIsDefined(value: T): asserts value is NonNullable { 8 | if (value === undefined || value === null) { 9 | throw new Error(`Expected 'value' to be defined, but received ${value}`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import * as Todos from './todos'; 4 | 5 | export function createInitialState() { 6 | return { 7 | todos: Todos.createInitialState(), 8 | }; 9 | } 10 | 11 | export type AppState = Readonly>; 12 | 13 | export default combineReducers({ 14 | todos: Todos.default, 15 | }); 16 | -------------------------------------------------------------------------------- /src/constants/path.ts: -------------------------------------------------------------------------------- 1 | export const INITIAL = 'INITIAL'; 2 | export const HOME = 'HOME'; 3 | export const DETAIL = 'DETAIL'; 4 | export const SIGN_UP = 'SIGN_UP'; 5 | export const SIGN_IN = 'SIGN_IN'; 6 | export const USER_INFO = 'USER_INFO'; 7 | export const STATISTICS = 'STATISTICS'; 8 | export const LOADING = 'LOADING'; 9 | export const CHOOSE_LOGIN = 'CHOOSE_LOGIN'; 10 | export const INPUT = 'INPUT'; 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /e2e/lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | elementByLabel: label => { 3 | return element(by.text(label)); 4 | }, 5 | elementById: id => { 6 | return element(by.id(id)); 7 | }, 8 | pressBack: () => { 9 | if (device.getPlatform() === 'android') { 10 | return device.pressBack(); 11 | } else { 12 | // react-navigationの戻るボタン 13 | return element(by.id('header-back')).tap(); 14 | } 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /ios/praiser/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/januswel/praiser/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.januswel.praiser; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. This is used to schedule 9 | * rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "praiser"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ios/praiser/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/Statistics.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import { getStatistics, getHistories } from '../selectors/todos'; 5 | import { Statistics } from '../components/pages'; 6 | 7 | export default function ConnectedStatistics() { 8 | const statistics = useSelector(getStatistics); 9 | const histories = useSelector(getHistories); 10 | 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/lib/round.test.ts: -------------------------------------------------------------------------------- 1 | import round from './round'; 2 | 3 | describe('round', () => { 4 | it('returns rounded values to 1 decimal places', () => { 5 | expect(round(0.5)).toBe(1); 6 | expect(round(0.4)).toBe(0); 7 | }); 8 | 9 | it('returns rounded values to specified decimal places', () => { 10 | expect(round(0.55, 2)).toBe(0.6); 11 | expect(round(0.54, 2)).toBe(0.5); 12 | expect(round(0.555, 3)).toBe(0.56); 13 | expect(round(0.554, 3)).toBe(0.55); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/atoms/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Avatar as PaperAvatar } from 'react-native-paper'; 3 | import { ImageSourcePropType, ViewStyle } from 'react-native'; 4 | 5 | interface Props { 6 | size?: number; 7 | source: ImageSourcePropType; 8 | style?: ViewStyle | ViewStyle[]; 9 | } 10 | 11 | export default function Avatar(props: Props) { 12 | const { size = 220, source, style } = props; 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/atoms/HeaderText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, StyleSheet } from 'react-native'; 3 | import { COLOR } from '../../constants/theme'; 4 | 5 | const styles = StyleSheet.create({ 6 | headerText: { 7 | color: COLOR.WHITE, 8 | fontSize: 24, 9 | }, 10 | }); 11 | 12 | interface Props { 13 | text: string; 14 | } 15 | 16 | export default function HeaderText(props: Props) { 17 | const { text } = props; 18 | return {text}; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/atoms/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button'; 2 | export { default as Logo } from './Logo'; 3 | export { default as TextField, dismiss } from './TextField'; 4 | export { default as Pagination } from './Pagination'; 5 | export { default as IconButton } from './IconButton'; 6 | export { default as Avatar } from './Avatar'; 7 | export { default as LabelValueContainer } from './LabelValueContainer'; 8 | export { default as Progress } from './Progress'; 9 | export { default as HeaderText } from './HeaderText'; 10 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'praiser' 2 | include ':react-native-svg' 3 | project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android') 4 | include ':react-native-vector-icons' 5 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') 6 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 7 | include ':app' 8 | -------------------------------------------------------------------------------- /src/components/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SignIn } from './SignIn'; 2 | export { default as Home } from './Home'; 3 | export { default as Detail } from './Detail'; 4 | export { default as Statistics } from './Statistics'; 5 | export { default as UserInfo } from './UserInfo'; 6 | export { default as Loading } from './Loading'; 7 | export { default as Initial } from './Initial'; 8 | export { default as ChooseLogin } from './ChooseLogin'; 9 | export { default as SignUp } from './SignUp'; 10 | export { default as Input } from './Input'; 11 | -------------------------------------------------------------------------------- /src/contexts/user.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface User { 4 | id: string; 5 | name: string | null; 6 | mailAddress: string | null; 7 | photoUrl: string | null; 8 | createdAt: number | null; 9 | lastLoginAt: number | null; 10 | } 11 | 12 | export type UserInformation = User | null; 13 | 14 | export function createInitialState(): UserInformation { 15 | return null; 16 | } 17 | 18 | export const Context = React.createContext({ 19 | userState: createInitialState(), 20 | setUserState: (_: UserInformation) => {}, 21 | }); 22 | -------------------------------------------------------------------------------- /src/containers/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Todos } from '../domain/models'; 4 | import { set } from '../modules/todos'; 5 | import { SignIn } from '../components/pages'; 6 | 7 | export default function ConnectedSignIn() { 8 | const dispatch = useDispatch(); 9 | const actions = React.useMemo( 10 | () => ({ 11 | setTodos(newValues: Todos.Model) { 12 | dispatch(set(newValues)); 13 | }, 14 | }), 15 | [dispatch], 16 | ); 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /src/containers/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Todos } from '../domain/models'; 4 | import { set } from '../modules/todos'; 5 | import { SignUp } from '../components/pages'; 6 | 7 | export default function ConnectedSignUp() { 8 | const dispatch = useDispatch(); 9 | const actions = React.useMemo( 10 | () => ({ 11 | setTodos(newValues: Todos.Model) { 12 | dispatch(set(newValues)); 13 | }, 14 | }), 15 | [dispatch], 16 | ); 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /src/containers/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { Todos } from '../domain/models'; 5 | import { set } from '../modules/todos'; 6 | import { Loading } from '../components/pages'; 7 | 8 | export default function ConnectedLoading() { 9 | const dispatch = useDispatch(); 10 | const actions = React.useMemo( 11 | () => ({ 12 | setTodos(newValues: Todos.Model) { 13 | dispatch(set(newValues)); 14 | }, 15 | }), 16 | [dispatch], 17 | ); 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/atoms/Progress.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import { ProgressCircle } from 'react-native-svg-charts'; 4 | import { COLOR } from '../../constants/theme'; 5 | 6 | const styles = StyleSheet.create({ 7 | progress: { 8 | height: 200, 9 | width: 200, 10 | }, 11 | }); 12 | 13 | interface Props { 14 | value: number; 15 | } 16 | 17 | export default function Progress(props: Props) { 18 | const { value } = props; 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/assert.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertIsDefined } from './assert'; 2 | 3 | describe('assert', () => { 4 | it('throws when be passed falsy values', () => { 5 | expect(() => { 6 | assert(false); 7 | }).toThrow(); 8 | }); 9 | }); 10 | 11 | describe('assertIsDefined', () => { 12 | it('throws when be passed null', () => { 13 | expect(() => { 14 | assertIsDefined(null); 15 | }).toThrow(); 16 | }); 17 | 18 | it('throws when be passed undefined', () => { 19 | expect(() => { 20 | assertIsDefined(undefined); 21 | }).toThrow(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/lib/local-store/initial-launch.test.ts: -------------------------------------------------------------------------------- 1 | import * as InitialLaunch from './initial-launch'; 2 | 3 | describe('InitialLaunch', () => { 4 | describe('isInitialLaunch', () => { 5 | it('returns false', async () => { 6 | expect(await InitialLaunch.isInitialLaunch()).toBe(false); 7 | }); 8 | }); 9 | 10 | describe('markAsTutorialIsDone', () => { 11 | it('marks tutorial is done', async () => { 12 | expect(await InitialLaunch.isInitialLaunch()).toBe(false); 13 | await InitialLaunch.markAsTutorialIsDone(); 14 | expect(await InitialLaunch.isInitialLaunch()).toBe(true); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/lib/hooks/use-networker.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { UiContext, NetworkContext } from '../../contexts'; 4 | 5 | type Task = () => Promise; 6 | export default function useNetworker() { 7 | const { dispatchNetworkActions } = React.useContext(NetworkContext); 8 | const { setError } = React.useContext(UiContext); 9 | 10 | return async (task: Task) => { 11 | try { 12 | dispatchNetworkActions({ type: 'begin' }); 13 | await task(); 14 | setError(null); 15 | } catch (e) { 16 | setError(e); 17 | } finally { 18 | dispatchNetworkActions({ type: 'end' }); 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/local-store/user-information.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | 3 | import { UserInformation } from '../../contexts/user'; 4 | 5 | const KEY = 'userinformation'; 6 | 7 | export async function save(userInformation: UserInformation) { 8 | await AsyncStorage.setItem(KEY, JSON.stringify(userInformation)); 9 | } 10 | 11 | export async function retrieve() { 12 | const serialized = await AsyncStorage.getItem(KEY); 13 | if (!serialized) { 14 | return null; 15 | } 16 | return JSON.parse(serialized); 17 | } 18 | 19 | export async function clear() { 20 | await AsyncStorage.removeItem(KEY); 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "charset": "utf8", 4 | "newLine": "LF", 5 | "target": "ESNext", 6 | "module": "CommonJS", 7 | "lib": ["ESNext"], 8 | "esModuleInterop": true, 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "jsx": "react-native", 12 | "strict": true, 13 | "noErrorTruncation": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "skipLibCheck": true 20 | }, 21 | "exclude": ["node_modules", "dist", "**/*.js"] 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # praiser [![CircleCI](https://circleci.com/gh/react-native-jp/praiser.svg?style=svg)](https://circleci.com/gh/react-native-jp/praiser) 2 | 3 | このリポジトリーには[技術評論社「React Native ~ JavaScript による iOS/Android アプリ開発の実践」](https://gihyo.jp/book/2020/978-4-297-11391-9)( [電子版](https://gihyo.jp/dp/ebook/2020/978-4-297-11392-6) )で開発しているアプリのコードが収められています。 4 | 5 | ## Getting Started - 動かし方 6 | 7 | ```console 8 | git clone https://github.com/react-native-jp/praiser.git 9 | cd praiser 10 | yarn 11 | 12 | # for iOS 13 | yarn ios 14 | 15 | # for Android 16 | yarn android 17 | ``` 18 | 19 | ## Appendix - 付録 20 | 21 | - [Bitrise の設定](https://github.com/react-native-jp/praiser/wiki/bitrise.yml) 22 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /android/app/src/androidTest/java/com/januswel/praiser/DetoxTest.java: -------------------------------------------------------------------------------- 1 | package com.januswel.praiser; 2 | 3 | import com.wix.detox.Detox; 4 | 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import androidx.test.ext.junit.runners.AndroidJUnit4; 10 | import androidx.test.filters.LargeTest; 11 | import androidx.test.rule.ActivityTestRule; 12 | 13 | @RunWith(AndroidJUnit4.class) 14 | @LargeTest 15 | public class DetoxTest { 16 | 17 | @Rule 18 | public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); 19 | 20 | @Test 21 | public void runDetoxTests() { 22 | Detox.runTests(mActivityRule); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/contexts/network.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function createInitialState() { 4 | return 0; 5 | } 6 | 7 | type State = ReturnType; 8 | 9 | interface Action { 10 | type: 'begin' | 'end'; 11 | } 12 | 13 | interface Context { 14 | networkState: number; 15 | dispatchNetworkActions: React.Dispatch; 16 | } 17 | 18 | export function reducer(state: State, action: Action) { 19 | switch (action.type) { 20 | case 'begin': 21 | return state + 1; 22 | case 'end': 23 | return state - 1; 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export const Context = React.createContext({ 30 | networkState: createInitialState(), 31 | dispatchNetworkActions: () => {}, 32 | }); 33 | -------------------------------------------------------------------------------- /src/routes/Header/HeaderLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DrawerActions } from '@react-navigation/routers'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | import Icon from 'react-native-vector-icons/FontAwesome'; 5 | import { COLOR } from '../../constants/theme'; 6 | import testIDs from '../../constants/testIDs'; 7 | 8 | export default function HeaderLeft() { 9 | const { dispatch } = useNavigation(); 10 | const onPress = React.useCallback(() => { 11 | dispatch(DrawerActions.openDrawer()); 12 | }, [dispatch]); 13 | return ( 14 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /e2e/init.js: -------------------------------------------------------------------------------- 1 | const detox = require('detox'); 2 | const config = require('../package.json').detox; 3 | const adapter = require('detox/runners/jest/adapter'); 4 | const specReporter = require('detox/runners/jest/specReporter'); 5 | 6 | // Set the default timeout 7 | jest.setTimeout(120000); 8 | jasmine.getEnv().addReporter(adapter); 9 | 10 | // This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level. 11 | // This is strictly optional. 12 | jasmine.getEnv().addReporter(specReporter); 13 | 14 | beforeAll(async () => { 15 | await detox.init(config, { launch: false }); 16 | }); 17 | 18 | beforeEach(async () => { 19 | await adapter.beforeEach(); 20 | }); 21 | 22 | afterAll(async () => { 23 | await adapter.afterAll(); 24 | await detox.cleanup(); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/atoms/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, ImageSourcePropType, StyleSheet, ImageStyle } from 'react-native'; 3 | import { width } from '../../lib/window'; 4 | import reactImage from '../../../assets/reactIcon.jpg'; 5 | 6 | const edgeNumber = 2; 7 | const ratio = 3; 8 | const imageRatio = edgeNumber / ratio; 9 | 10 | const styles = StyleSheet.create({ 11 | image: { 12 | width: width * imageRatio, 13 | flex: 1, 14 | resizeMode: 'contain', 15 | }, 16 | }); 17 | 18 | interface Props { 19 | image?: ImageSourcePropType; 20 | style?: ImageStyle | ImageStyle[]; 21 | } 22 | 23 | export default function Logo(props: Props) { 24 | const { image = reactImage, style } = props; 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/firebase/register-user.ts: -------------------------------------------------------------------------------- 1 | import auth from '@react-native-firebase/auth'; 2 | 3 | export default async function registerUser(mailAddress: string, password: string) { 4 | const response = await auth().createUserWithEmailAndPassword(mailAddress, password); 5 | 6 | if (!response.user) { 7 | throw new Error('user information is null'); 8 | } 9 | 10 | const { 11 | uid: id, 12 | displayName: name, 13 | photoURL: photoUrl, 14 | metadata: { creationTime, lastSignInTime }, 15 | } = response.user; 16 | const createdAt = creationTime ? new Date(creationTime).getTime() : null; 17 | const lastLoginAt = lastSignInTime ? new Date(lastSignInTime).getTime() : null; 18 | return { 19 | id, 20 | name, 21 | mailAddress, 22 | photoUrl, 23 | createdAt, 24 | lastLoginAt, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const yaml = require('js-yaml') 4 | 5 | function searchProjectRoot(currentPath) { 6 | if (currentPath === '/') { 7 | throw new Error('project root is not found') 8 | } 9 | if (fs.existsSync(path.join(currentPath, 'package.json'))) { 10 | return currentPath 11 | } 12 | return searchProjectRoot(path.normalize(path.join(currentPath, '..'))) 13 | } 14 | 15 | function main() { 16 | const projectRoot = searchProjectRoot(__dirname) 17 | const config = yaml.safeLoad(fs.readFileSync(path.join(projectRoot, '.eslintrc.yml'), 'utf8')) 18 | 19 | if (config.parserOptions == null) { 20 | config.parserOptions = {} 21 | } 22 | 23 | config.parserOptions.tsconfigRootDir = projectRoot 24 | 25 | return config 26 | } 27 | 28 | module.exports = main() 29 | -------------------------------------------------------------------------------- /src/lib/firebase/sign-in-with-password.ts: -------------------------------------------------------------------------------- 1 | import auth from '@react-native-firebase/auth'; 2 | 3 | export default async function signInWithPassword(mailAddress: string, password: string) { 4 | const response = await auth().signInWithEmailAndPassword(mailAddress, password); 5 | 6 | if (!response.user) { 7 | throw new Error('user information is null'); 8 | } 9 | 10 | const { 11 | uid: id, 12 | displayName: name, 13 | photoURL: photoUrl, 14 | metadata: { creationTime, lastSignInTime }, 15 | } = response.user; 16 | const createdAt = creationTime ? new Date(creationTime).getTime() : null; 17 | const lastLoginAt = lastSignInTime ? new Date(lastSignInTime).getTime() : null; 18 | return { 19 | id, 20 | name, 21 | mailAddress, 22 | photoUrl, 23 | createdAt, 24 | lastLoginAt, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/local-store/user-information.test.ts: -------------------------------------------------------------------------------- 1 | import * as UserInformation from './user-information'; 2 | 3 | describe('UserInformation', () => { 4 | it('saves UserInformation', async () => { 5 | expect(await UserInformation.retrieve()).toBeNull(); 6 | const now = new Date().getTime(); 7 | const userInformation = { 8 | id: '0', 9 | name: 'YutamaKotaro', 10 | mailAddress: 'yutama.kotaro@example.com', 11 | photoUrl: 'https://example.com/images/photo.png', 12 | createdAt: now, 13 | lastLoginAt: now, 14 | }; 15 | await UserInformation.save(userInformation); 16 | 17 | const actual = await UserInformation.retrieve(); 18 | expect(actual).toEqual(userInformation); 19 | 20 | await UserInformation.clear(); 21 | expect(await UserInformation.retrieve()).toBeNull(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ios/praiserTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/containers/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { Todo } from '../domain/models'; 5 | import * as Todos from '../usecases/todos'; 6 | import { Input } from '../components/pages'; 7 | import { UserContext } from '../contexts'; 8 | 9 | export default function ConnectedInput() { 10 | const { userState } = React.useContext(UserContext); 11 | 12 | const dispatch = useDispatch(); 13 | const actions = React.useMemo( 14 | () => 15 | userState 16 | ? { 17 | addTodo(newValues: Todo.Values) { 18 | dispatch(Todos.addAndSync(userState.id, newValues)); 19 | }, 20 | } 21 | : null, 22 | [userState, dispatch], 23 | ); 24 | 25 | if (!actions) { 26 | return null; 27 | } 28 | 29 | return ; 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/Main/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import { USER_INFO } from '../../constants/path'; 4 | import { UserInfo } from '../../components/pages'; 5 | import { HeaderLeft, headerStyle, headerTintColor } from '../Header'; 6 | import { COLOR } from '../../constants/theme'; 7 | 8 | const cardStyle = { 9 | backgroundColor: COLOR.MAIN, 10 | }; 11 | 12 | const Stack = createStackNavigator(); 13 | function UserInfoNavigator() { 14 | return ( 15 | , 21 | }} 22 | > 23 | 24 | 25 | ); 26 | } 27 | 28 | export default UserInfoNavigator; 29 | -------------------------------------------------------------------------------- /__mocks__/@react-native-firebase/firestore.ts: -------------------------------------------------------------------------------- 1 | const Collection = { 2 | doc(_: string) { 3 | return Document; 4 | }, 5 | get(_: string) { 6 | return new Promise(resolve => resolve()); 7 | }, 8 | }; 9 | 10 | const Document = { 11 | collection(_: string) { 12 | return Collection; 13 | }, 14 | get(_: string) { 15 | return new Promise(resolve => resolve({} as T)); 16 | }, 17 | set(_: T) { 18 | return new Promise(resolve => resolve()); 19 | }, 20 | delete(_: string) { 21 | return new Promise(resolve => resolve()); 22 | }, 23 | update(_: T) { 24 | return new Promise(resolve => resolve()); 25 | }, 26 | }; 27 | 28 | export default function createInstance() { 29 | return { 30 | collection(_: string) { 31 | return Collection; 32 | }, 33 | doc(_: string) { 34 | return Document; 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/containers/Detail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Todo } from '../domain/models'; 4 | import * as Todos from '../usecases/todos'; 5 | import { Detail } from '../components/pages'; 6 | import { UserContext } from '../contexts'; 7 | 8 | export default function ConnectedDetail() { 9 | const { userState } = React.useContext(UserContext); 10 | const dispatch = useDispatch(); 11 | 12 | const actions = React.useMemo( 13 | () => 14 | userState 15 | ? { 16 | changeTodo(id: string, newValues: Todo.Values) { 17 | dispatch(Todos.editAndSync(userState.id, id, newValues)); 18 | }, 19 | } 20 | : null, 21 | [userState, dispatch], 22 | ); 23 | 24 | if (!actions) { 25 | return null; 26 | } 27 | 28 | return ; 29 | } 30 | -------------------------------------------------------------------------------- /ios/praiser-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/organisms/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SnapCarousel from 'react-native-snap-carousel'; 3 | import CarouselItem from '../molecules/CarouselItem'; 4 | import { width } from '../../lib/window'; 5 | 6 | interface Props { 7 | onEnd: () => void; 8 | onNext: () => void; 9 | carouselRef: any; 10 | onSnapToItem: (slide: number) => void; 11 | data: { text: string; testID: string }[]; 12 | } 13 | 14 | export default function Carousel(props: Props) { 15 | const { onEnd, onNext, onSnapToItem, carouselRef, data } = props; 16 | return ( 17 | ( 21 | 22 | )} 23 | sliderWidth={width} 24 | itemWidth={width} 25 | onSnapToItem={onSnapToItem} 26 | /> 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /scripts/test-e2e.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('shell-utils').exec; 2 | 3 | const android = process.argv.includes('--android'); 4 | const release = process.argv.includes('--release'); 5 | const skipBuild = process.argv.includes('--skipBuild'); 6 | const headless = process.argv.includes('--headless'); 7 | const multi = process.argv.includes('--multi'); 8 | 9 | console.log({ 10 | release, 11 | skipBuild, 12 | }); 13 | 14 | function run() { 15 | const prefix = android ? `android.emu` : `ios.sim`; 16 | const suffix = release ? `release` : `debug`; 17 | const configuration = `${prefix}.${suffix}`; 18 | const headless$ = android ? (headless ? `--headless` : ``) : ``; 19 | const workers = multi ? 3 : 1; 20 | 21 | if (!skipBuild) { 22 | execSync(`detox build --configuration ${configuration}`); 23 | } 24 | execSync(`detox test --configuration ${configuration} ${headless$} ${!android ? `-w ${workers}` : ``}`); 25 | } 26 | 27 | run(); 28 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: 4 | - plugin:@typescript-eslint/recommended 5 | - prettier 6 | - prettier/@typescript-eslint 7 | - plugin:react/recommended 8 | - '@react-native-community' 9 | - plugin:react-native/all 10 | - plugin:jest/recommended 11 | parser: '@typescript-eslint/parser' 12 | parserOptions: 13 | project: ./tsconfig.json 14 | ecmaVersion: 2018 15 | sourceType: module 16 | ecmaFeatures: 17 | jsx: true 18 | plugins: 19 | - '@typescript-eslint' 20 | - react 21 | - react-native 22 | - jest 23 | settings: 24 | react: 25 | version: detect 26 | rules: 27 | no-console: warn 28 | semi: off 29 | yoda: off 30 | '@typescript-eslint/camelcase': 31 | - error 32 | - allow: 33 | - item_id 34 | - item_name 35 | - item_category 36 | '@typescript-eslint/explicit-function-return-type': off 37 | '@typescript-eslint/no-empty-function': off 38 | react-native/no-raw-text: off 39 | react-native/sort-styles: off 40 | -------------------------------------------------------------------------------- /src/contexts/ui.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type ErrorState = Error | null; 4 | export function createErrorInitialState(): ErrorState { 5 | return null; 6 | } 7 | 8 | export function createSnackbarInitialState() { 9 | return { 10 | visible: false, 11 | message: '', 12 | label: 'Done', 13 | }; 14 | } 15 | 16 | type SnackbarState = ReturnType; 17 | 18 | export enum Status { 19 | LOADING = 'loading', 20 | FIRST_OPEN = 'firstOpen', 21 | UN_AUTHORIZED = 'unAuthorized', 22 | AUTHORIZED = 'authorized', 23 | } 24 | 25 | export function createApplicationInitialState(): Status { 26 | return Status.LOADING; 27 | } 28 | 29 | export const Context = React.createContext({ 30 | error: createErrorInitialState(), 31 | setError: (_: ErrorState) => {}, 32 | snackbar: createSnackbarInitialState(), 33 | setSnackbar: (_: SnackbarState) => {}, 34 | applicationState: createApplicationInitialState(), 35 | setApplicationState: (_: Status) => {}, 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/atoms/Pagination.tsx: -------------------------------------------------------------------------------- 1 | // src/components/atoms/Pagination.tsx 2 | import React from 'react'; 3 | import { StyleSheet } from 'react-native'; 4 | import { Pagination as SCPagination } from 'react-native-snap-carousel'; 5 | import { COLOR } from '../../constants/theme'; 6 | 7 | const styles = StyleSheet.create({ 8 | pagination: { 9 | backgroundColor: COLOR.CAROUSEL_BACKGROUND, 10 | }, 11 | dot: { 12 | width: 10, 13 | height: 10, 14 | borderRadius: 5, 15 | marginHorizontal: 8, 16 | backgroundColor: COLOR.WHITE, 17 | }, 18 | }); 19 | 20 | interface Props { 21 | length: number; 22 | index: number; 23 | } 24 | 25 | export default function Pagination(props: Props) { 26 | const { length, index } = props; 27 | return ( 28 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/containers/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | 4 | import { getTodos } from '../selectors/todos'; 5 | import * as Todos from '../usecases/todos'; 6 | import { Home } from '../components/pages'; 7 | import { UserContext } from '../contexts'; 8 | 9 | export default function ConnectedHome() { 10 | const todos = useSelector(getTodos); 11 | const { userState } = React.useContext(UserContext); 12 | 13 | const dispatch = useDispatch(); 14 | const actions = React.useMemo( 15 | () => 16 | userState 17 | ? { 18 | removeTodo(id: string) { 19 | dispatch(Todos.removeAndSync(userState.id, id)); 20 | }, 21 | toggleTodo(id: string) { 22 | dispatch(Todos.toggleAndSync(userState.id, id)); 23 | }, 24 | } 25 | : null, 26 | [userState, dispatch], 27 | ); 28 | 29 | if (!actions) { 30 | return null; 31 | } 32 | 33 | return ; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/atoms/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton as PaperIconButton } from 'react-native-paper'; 3 | import { StyleSheet, ViewStyle } from 'react-native'; 4 | import { COLOR } from '../../constants/theme'; 5 | 6 | const styles = StyleSheet.create({ 7 | button: { 8 | height: 120, 9 | justifyContent: 'center', 10 | alignItems: 'center', 11 | width: 80, 12 | borderRadius: 0, 13 | margin: 0, 14 | }, 15 | }); 16 | 17 | interface Props { 18 | icon: string; 19 | onPress: () => void; 20 | style?: ViewStyle | ViewStyle[]; 21 | testID?: string; 22 | iconColor?: string; 23 | size?: number; 24 | } 25 | 26 | export default function IconButton(props: Props) { 27 | const { icon, onPress, style, testID, iconColor = COLOR.WHITE, size = 18 } = props; 28 | 29 | return ( 30 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://prettier.io/docs/en/options.html 3 | 4 | # specify correct parser 5 | parser: babel 6 | filepath: '' 7 | 8 | # main settings 9 | printWidth: 120 10 | tabWidth: 2 11 | useTabs: false 12 | semi: true 13 | singleQuote: true 14 | quoteProps: as-needed 15 | trailingComma: all 16 | bracketSpacing: true 17 | jsxBracketSameLine: false 18 | jsxSingleQuote: false 19 | arrowParens: avoid 20 | endOfLine: lf 21 | 22 | # process always 23 | requirePragma: false 24 | insertPragma: false 25 | 26 | # for Markdown 27 | # proseWrap: preserve 28 | # for HTML 29 | # htmlWhitespaceSensitivity: css 30 | 31 | overrides: 32 | - files: '*.json' 33 | options: 34 | parser: json 35 | - files: '*.html' 36 | options: 37 | parser: html 38 | - files: 39 | - '*.md' 40 | - '*.markdown' 41 | options: 42 | parser: markdown 43 | - files: 44 | - '*.ts' 45 | - '*.tsx' 46 | options: 47 | parser: typescript 48 | - files: 49 | - '*.yml' 50 | - '*.yaml' 51 | options: 52 | parser: yaml 53 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # refer: https://circleci.com/docs/2.1/language-javascript/ 3 | version: 2.1 4 | 5 | executors: 6 | node-lts: 7 | docker: 8 | - image: circleci/node:12.13 9 | working_directory: ~/repo 10 | 11 | commands: 12 | setup: 13 | steps: 14 | - checkout 15 | - restore_cache: 16 | keys: 17 | - v1-dependencies-{{ checksum "package.json" }} 18 | - v1-dependencies- 19 | 20 | jobs: 21 | build: 22 | executor: node-lts 23 | steps: 24 | - setup 25 | - run: yarn 26 | - save_cache: 27 | paths: 28 | - node_modules 29 | key: v1-dependencies-{{ checksum "package.json" }} 30 | test: 31 | executor: node-lts 32 | steps: 33 | - setup 34 | - run: 35 | command: | 36 | yarn type-check 37 | yarn lint 38 | yarn coverage 39 | - store_artifacts: 40 | path: ~/repo/coverage 41 | 42 | workflows: 43 | version: 2 44 | test: 45 | jobs: 46 | - build 47 | - test: 48 | requires: 49 | - build 50 | -------------------------------------------------------------------------------- /src/routes/Main/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import { DETAIL, HOME } from '../../constants/path'; 4 | import { Home, Detail } from '../../containers'; 5 | import { HeaderLeft, headerStyle, headerTintColor } from '../Header'; 6 | import { COLOR } from '../../constants/theme'; 7 | 8 | const cardStyle = { 9 | backgroundColor: COLOR.MAIN, 10 | }; 11 | 12 | const Stack = createStackNavigator(); 13 | function HomeNavigator() { 14 | return ( 15 | 22 | , 27 | title: 'Home', 28 | }} 29 | /> 30 | 37 | 38 | ); 39 | } 40 | 41 | export default HomeNavigator; 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | 61 | coverage 62 | GoogleService-Info.plist 63 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/routes/Main/Statistics.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStackNavigator } from '@react-navigation/stack'; 3 | import { DETAIL, STATISTICS } from '../../constants/path'; 4 | import { Detail, Statistics } from '../../containers'; 5 | import { HeaderLeft, headerStyle, headerTintColor } from '../Header'; 6 | import { COLOR } from '../../constants/theme'; 7 | 8 | const cardStyle = { 9 | backgroundColor: COLOR.MAIN, 10 | }; 11 | 12 | const Stack = createStackNavigator(); 13 | function StatisticsNavigator() { 14 | return ( 15 | 22 | , 27 | title: 'Statistcs', 28 | }} 29 | /> 30 | 37 | 38 | ); 39 | } 40 | 41 | export default StatisticsNavigator; 42 | -------------------------------------------------------------------------------- /src/components/molecules/ErrorPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { UiContext } from '../../contexts'; 4 | import { width } from '../../lib/window'; 5 | import { COLOR } from '../../constants/theme'; 6 | import SafeAreaView from 'react-native-safe-area-view'; 7 | 8 | const styles = StyleSheet.create({ 9 | container: { 10 | position: 'absolute', 11 | width, 12 | }, 13 | panel: { 14 | backgroundColor: COLOR.CAUTION, 15 | padding: 8, 16 | }, 17 | label: { 18 | color: COLOR.WHITE, 19 | }, 20 | }); 21 | 22 | export default function ErrorPanel() { 23 | const { error, setError } = React.useContext(UiContext); 24 | React.useEffect(() => { 25 | if (error) { 26 | setTimeout(() => { 27 | setError(null); 28 | }, 2000); 29 | } 30 | }, [error, setError]); 31 | return ( 32 | <> 33 | {error && ( 34 | 35 | 36 | {error.toString()} 37 | 38 | 39 | )} 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/organisms/Todos/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { UiContext } from '../../../contexts'; 5 | import { COLOR } from '../../../constants/theme'; 6 | import testIDs from '../../../constants/testIDs'; 7 | import IconButton from '../../atoms/IconButton'; 8 | 9 | const styles = StyleSheet.create({ 10 | button: { 11 | backgroundColor: COLOR.CAUTION, 12 | }, 13 | }); 14 | 15 | export interface RemoveTodo { 16 | (id: string): void; 17 | } 18 | interface Props { 19 | state: { 20 | id: string; 21 | }; 22 | actions: { 23 | removeTodo: RemoveTodo; 24 | }; 25 | } 26 | 27 | export function Component(props: Props) { 28 | const { setError } = React.useContext(UiContext); 29 | 30 | const { 31 | state: { id }, 32 | actions: { removeTodo }, 33 | } = props; 34 | 35 | const onPress = React.useCallback(() => { 36 | try { 37 | removeTodo(id); 38 | } catch (e) { 39 | setError(e); 40 | } 41 | }, [id, removeTodo, setError]); 42 | 43 | return ; 44 | } 45 | -------------------------------------------------------------------------------- /src/components/molecules/NetworkPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, StyleSheet } from 'react-native'; 3 | import SafeAreaView from 'react-native-safe-area-view'; 4 | import { NetworkContext } from '../../contexts'; 5 | import { height, width } from '../../lib/window'; 6 | import { COLOR } from '../../constants/theme'; 7 | 8 | const styles = StyleSheet.create({ 9 | dropdown: { 10 | position: 'absolute', 11 | width, 12 | height, 13 | backgroundColor: COLOR.WHITE, 14 | opacity: 0.5, 15 | }, 16 | container: { 17 | position: 'absolute', 18 | width, 19 | height, 20 | justifyContent: 'center', 21 | alignItems: 'center', 22 | }, 23 | }); 24 | 25 | export default function NetworkPanel() { 26 | const { networkState } = React.useContext(NetworkContext); 27 | const isCommunicating = React.useMemo(() => 0 < networkState, [networkState]); 28 | if (!isCommunicating) { 29 | return null; 30 | } 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/firebase/sign-in-with-google.ts: -------------------------------------------------------------------------------- 1 | import auth from '@react-native-firebase/auth'; 2 | import { GoogleSignin } from '@react-native-community/google-signin'; 3 | 4 | GoogleSignin.configure({ 5 | scopes: ['profile', 'email'], 6 | }); 7 | 8 | export default async function signInWithGoogle() { 9 | await GoogleSignin.hasPlayServices(); 10 | const user = await GoogleSignin.signIn(); 11 | const { idToken } = user; 12 | const { accessToken } = await GoogleSignin.getTokens(); 13 | const credential = auth.GoogleAuthProvider.credential(idToken, accessToken); 14 | 15 | const response = await auth().signInWithCredential(credential); 16 | 17 | if (!response.user) { 18 | throw new Error('user information is null'); 19 | } 20 | 21 | const { 22 | uid: id, 23 | displayName: name, 24 | email: mailAddress, 25 | photoURL: photoUrl, 26 | metadata: { creationTime, lastSignInTime }, 27 | } = response.user; 28 | const createdAt = creationTime ? new Date(creationTime).getTime() : null; 29 | const lastLoginAt = lastSignInTime ? new Date(lastSignInTime).getTime() : null; 30 | return { 31 | id, 32 | name, 33 | mailAddress, 34 | photoUrl, 35 | createdAt, 36 | lastLoginAt, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/atoms/LabelValueContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View, StyleSheet } from 'react-native'; 3 | import { COLOR } from '../../constants/theme'; 4 | 5 | const styles = StyleSheet.create({ 6 | row: { 7 | alignSelf: 'stretch', 8 | alignItems: 'center', 9 | paddingHorizontal: 50, 10 | flexDirection: 'row', 11 | marginBottom: 10, 12 | }, 13 | labelContainer: { 14 | minWidth: 100, 15 | }, 16 | labelText: { 17 | color: COLOR.WHITE, 18 | fontSize: 18, 19 | }, 20 | valueContainer: { 21 | flexShrink: 1, 22 | paddingLeft: 10, 23 | }, 24 | valueText: { 25 | color: COLOR.WHITE, 26 | fontSize: 16, 27 | }, 28 | }); 29 | 30 | interface Props { 31 | label: string; 32 | value: string | number | null; 33 | } 34 | 35 | export default function LabelViewContainer(props: Props) { 36 | const { label, value = '' } = props; 37 | 38 | return ( 39 | 40 | 41 | {label} 42 | 43 | 44 | {value} 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useAndroidX=true 21 | android.enableJetifier=true 22 | 23 | org.gradle.warning.mode=all 24 | 25 | STORE_FILE=praiser.keystore 26 | KEY_ALIAS=praiser 27 | STORE_PASSWORD=praiser 28 | KEY_PASSWORD=praiser 29 | 30 | org.gradle.daemon=true 31 | org.gradle.configureondemand=true 32 | org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 33 | -------------------------------------------------------------------------------- /src/domain/repositories/todos.ts: -------------------------------------------------------------------------------- 1 | import firestore from '../../lib/firebase/firestore'; 2 | import { Todo, Todos } from '../models'; 3 | 4 | export function getAll(userId: string) { 5 | return firestore(userId) 6 | .get() 7 | .then(querySnapshot => { 8 | const todos = querySnapshot.docs.reduce((result: Todos.Model, doc) => { 9 | result[doc.id] = doc.data() as Todo.Model; 10 | return result; 11 | }, {}); 12 | return todos; 13 | }); 14 | } 15 | 16 | export function add(userId: string, newTodo: Todo.Model) { 17 | firestore(userId) 18 | .doc(newTodo.id) 19 | .set(newTodo) 20 | .catch(e => { 21 | throw e; 22 | }); 23 | } 24 | 25 | export function remove(userId: string, id: string) { 26 | firestore(userId) 27 | .doc(id) 28 | .delete() 29 | .catch(e => { 30 | throw e; 31 | }); 32 | } 33 | 34 | export function toggle(userId: string, id: string, newValue: string | null) { 35 | firestore(userId) 36 | .doc(id) 37 | .update({ 38 | completedAt: newValue, 39 | }) 40 | .catch(e => { 41 | throw e; 42 | }); 43 | } 44 | 45 | export function change(userId: string, id: string, newValue: object) { 46 | firestore(userId) 47 | .doc(id) 48 | .update(newValue) 49 | .catch(e => { 50 | throw e; 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/atoms/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, TextStyle, ViewStyle } from 'react-native'; 3 | import { Button as PaperButton } from 'react-native-paper'; 4 | import { COLOR } from '../../constants/theme'; 5 | 6 | const styles = StyleSheet.create({ 7 | text: { 8 | fontSize: 18, 9 | fontWeight: '900', 10 | color: COLOR.WHITE, 11 | }, 12 | }); 13 | 14 | interface Props { 15 | onPress: () => void; 16 | style?: ViewStyle | ViewStyle[]; 17 | textStyle?: TextStyle; 18 | label?: string; 19 | color?: string; 20 | disabled?: boolean; 21 | disabledColor?: string; 22 | testID?: string; 23 | icon?: string; 24 | } 25 | 26 | export default function Button(props: Props) { 27 | const { 28 | onPress, 29 | style, 30 | textStyle, 31 | label, 32 | color = COLOR.SECONDARY, 33 | disabled, 34 | disabledColor = COLOR.MAIN_LIGHT, 35 | testID, 36 | icon, 37 | } = props; 38 | return ( 39 | 50 | {label && {label}} 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/domain/models/todo.ts: -------------------------------------------------------------------------------- 1 | import 'react-native-get-random-values'; 2 | import { v4 as generateUuid } from 'uuid'; 3 | 4 | import { assertIsDefined } from '../../lib/assert'; 5 | 6 | export interface Model { 7 | readonly id: string; 8 | readonly title: string; 9 | readonly detail?: string; 10 | readonly createdAt: string; 11 | readonly updatedAt: string; 12 | readonly completedAt: string | null; 13 | } 14 | 15 | export interface Values { 16 | readonly title: string; 17 | readonly detail?: string; 18 | } 19 | 20 | export function factory(todo: Values): Model { 21 | assertIsDefined(todo.title); 22 | 23 | const now = new Date().toISOString(); 24 | return { 25 | id: generateUuid(), 26 | title: todo.title, 27 | detail: todo.detail, 28 | createdAt: now, 29 | updatedAt: now, 30 | completedAt: null, 31 | }; 32 | } 33 | 34 | export function isDone(todo: Model): boolean { 35 | return todo.completedAt !== null; 36 | } 37 | 38 | export function change(todo: Model, newValues: Values): Model { 39 | assertIsDefined(newValues.title); 40 | 41 | const now = new Date().toISOString(); 42 | return { 43 | ...todo, 44 | ...newValues, 45 | updatedAt: now, 46 | }; 47 | } 48 | 49 | export function toggle(todo: Model): Model { 50 | const now = new Date().toISOString(); 51 | return { 52 | ...todo, 53 | updatedAt: now, 54 | completedAt: todo.completedAt === null ? now : null, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/components/pages/Statistics/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | 5 | import Todos, { Todo, State as TodosState } from '../../organisms/Todos'; 6 | import ProgressPanel, { Statistic } from '../../molecules/ProgressPanel'; 7 | import { DETAIL } from '../../../constants/path'; 8 | import HeaderText from '../../atoms/HeaderText'; 9 | 10 | const styles = StyleSheet.create({ 11 | headerTextContainer: { 12 | paddingLeft: 20, 13 | marginTop: 20, 14 | marginBottom: 8, 15 | }, 16 | }); 17 | 18 | interface Props { 19 | statistics: Statistic; 20 | histories: TodosState; 21 | } 22 | 23 | function Header(props: Props) { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default function Statistics(props: Props) { 35 | const { navigate } = useNavigation(); 36 | const gotoDetail = React.useCallback( 37 | (state: Todo.State, isEditable: boolean) => { 38 | navigate(DETAIL, { ...state, isEditable }); 39 | }, 40 | [navigate], 41 | ); 42 | const actions = React.useMemo(() => ({ gotoDetail }), [gotoDetail]); 43 | 44 | return } />; 45 | } 46 | -------------------------------------------------------------------------------- /ios/GoogleService-Info.plist.production: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 166018290578-80r4bl4320bubg73usfnu3rnntr9u6qd.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.166018290578-80r4bl4320bubg73usfnu3rnntr9u6qd 9 | ANDROID_CLIENT_ID 10 | 166018290578-qrkmbtn8ce4pont913u0lnompm8b3b93.apps.googleusercontent.com 11 | API_KEY 12 | AIzaSyBHuq1yLXgSGeRCU2O8FQPhCVF6JukS-jE 13 | GCM_SENDER_ID 14 | 166018290578 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | com.januswel.praiser 19 | PROJECT_ID 20 | praiser-4653c 21 | STORAGE_BUCKET 22 | praiser-4653c.appspot.com 23 | IS_ADS_ENABLED 24 | 25 | IS_ANALYTICS_ENABLED 26 | 27 | IS_APPINVITE_ENABLED 28 | 29 | IS_GCM_ENABLED 30 | 31 | IS_SIGNIN_ENABLED 32 | 33 | GOOGLE_APP_ID 34 | 1:166018290578:ios:d944b2588723c64853a74b 35 | DATABASE_URL 36 | https://praiser-4653c.firebaseio.com 37 | 38 | -------------------------------------------------------------------------------- /ios/GoogleService-Info.plist.development: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 997443237631-la6am5c0d84d2qk77s0t7is8kc7t3ve9.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.997443237631-la6am5c0d84d2qk77s0t7is8kc7t3ve9 9 | ANDROID_CLIENT_ID 10 | 997443237631-8t6srs5e2bc8spamdfd3e5ravgau02bl.apps.googleusercontent.com 11 | API_KEY 12 | AIzaSyAJNeXeIsujOZ5U3U1ZUyeQy9wFUQ1ifuE 13 | GCM_SENDER_ID 14 | 997443237631 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | com.januswel.praiser 19 | PROJECT_ID 20 | reactnativetodo-2f2df 21 | STORAGE_BUCKET 22 | reactnativetodo-2f2df.appspot.com 23 | IS_ADS_ENABLED 24 | 25 | IS_ANALYTICS_ENABLED 26 | 27 | IS_APPINVITE_ENABLED 28 | 29 | IS_GCM_ENABLED 30 | 31 | IS_SIGNIN_ENABLED 32 | 33 | GOOGLE_APP_ID 34 | 1:997443237631:ios:4cdace491a305b5926beca 35 | DATABASE_URL 36 | https://reactnativetodo-2f2df.firebaseio.com 37 | 38 | -------------------------------------------------------------------------------- /ios/praiser/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "notification@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "notification@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "settings@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "settings@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "spotlight@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "spotlight@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "1024x1024", 53 | "idiom" : "ios-marketing", 54 | "filename" : "store-icon.png", 55 | "scale" : "1x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /src/components/organisms/Todos/DoneButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { UiContext } from '../../../contexts'; 5 | import { COLOR } from '../../../constants/theme'; 6 | import testIDs from '../../../constants/testIDs'; 7 | import IconButton from '../../atoms/IconButton'; 8 | 9 | const styles = StyleSheet.create({ 10 | button: { 11 | backgroundColor: COLOR.PRIMARY, 12 | }, 13 | done: { 14 | backgroundColor: COLOR.MAIN_DARK, 15 | }, 16 | }); 17 | 18 | export interface ToggleTodo { 19 | (id: string): void; 20 | } 21 | interface Props { 22 | state: { 23 | id: string; 24 | isDone?: boolean; 25 | }; 26 | actions: { 27 | toggleTodo: ToggleTodo; 28 | closeRow: () => void; 29 | }; 30 | } 31 | 32 | export function Component(props: Props) { 33 | const { setError } = React.useContext(UiContext); 34 | 35 | const { 36 | state: { id, isDone }, 37 | actions: { toggleTodo, closeRow }, 38 | } = props; 39 | 40 | const onPress = React.useCallback(async () => { 41 | try { 42 | toggleTodo(id); 43 | closeRow(); 44 | } catch (error) { 45 | setError(error); 46 | } 47 | }, [id, closeRow, toggleTodo, setError]); 48 | 49 | return ( 50 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/domain/models/todos.ts: -------------------------------------------------------------------------------- 1 | import { filter } from '@januswel/object-utilities'; 2 | 3 | import { assert } from '../../lib/assert'; 4 | import * as Todo from './todo'; 5 | 6 | export interface Model { 7 | [id: string]: Todo.Model; 8 | } 9 | 10 | export function factory(newValues?: Todo.Values[]): Model { 11 | if (!newValues) { 12 | return {}; 13 | } 14 | 15 | return newValues.reduce((result, newValue) => { 16 | const newTodo = Todo.factory(newValue); 17 | result[newTodo.id] = newTodo; 18 | return result; 19 | }, {}); 20 | } 21 | 22 | export function add(todos: Model, newTodo: Todo.Model): Model { 23 | return { 24 | ...todos, 25 | [newTodo.id]: newTodo, 26 | }; 27 | } 28 | 29 | export function remove(todos: Model, targetId: string): Model { 30 | return filter(todos, id => id !== targetId); 31 | } 32 | 33 | export function update(todos: Model, id: string, values: Todo.Values): Model { 34 | assert(id in todos); 35 | 36 | return { 37 | ...todos, 38 | [id]: Todo.change(todos[id], values), 39 | }; 40 | } 41 | 42 | export function toggle(todos: Model, id: string): Model { 43 | assert(id in todos); 44 | 45 | return { 46 | ...todos, 47 | [id]: Todo.toggle(todos[id]), 48 | }; 49 | } 50 | 51 | export function getNumof(todos: Model): number { 52 | return Object.keys(todos).length; 53 | } 54 | 55 | export function findByTitle(todos: Model, title: string): Todo.Model[] { 56 | return Object.values(todos).filter(todo => todo.title === title); 57 | } 58 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.praiser", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.praiser", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 18 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | kotlinVersion = '1.3.41' 10 | supportLibVersion = "27.1.1" 11 | googlePlayServicesAuthVersion = "16.0.1" 12 | } 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | dependencies { 18 | classpath("com.android.tools.build:gradle:3.4.2") 19 | 20 | // NOTE: Do not place your application dependencies here; they belong 21 | // in the individual module build.gradle files 22 | 23 | classpath("com.google.gms:google-services:4.2.0") 24 | 25 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 26 | } 27 | } 28 | 29 | allprojects { 30 | repositories { 31 | mavenLocal() 32 | maven { 33 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 34 | url("$rootDir/../node_modules/react-native/android") 35 | } 36 | maven { 37 | url "$rootDir/../node_modules/detox/Detox-android" 38 | } 39 | maven { 40 | // Android JSC is installed from npm 41 | url("$rootDir/../node_modules/jsc-android/dist") 42 | } 43 | 44 | google() 45 | jcenter() 46 | maven { url 'https://jitpack.io' } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/usecases/todos.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import analytics from '@react-native-firebase/analytics'; 3 | 4 | import { Todo } from '../domain/models'; 5 | import * as TodosRepository from '../domain/repositories/todos'; 6 | import { AppState } from '../modules'; 7 | import { add, remove, toggle, update } from '../modules/todos'; 8 | 9 | export function addAndSync(userId: string, newValues: Todo.Values) { 10 | return async function(dispatch: Dispatch) { 11 | const newTodo = Todo.factory(newValues); 12 | dispatch(add(newTodo)); 13 | TodosRepository.add(userId, newTodo); 14 | }; 15 | } 16 | 17 | export function removeAndSync(userId: string, id: string) { 18 | return async function(dispatch: Dispatch) { 19 | dispatch(remove(id)); 20 | TodosRepository.remove(userId, id); 21 | }; 22 | } 23 | 24 | export function toggleAndSync(userId: string, id: string) { 25 | return async function(dispatch: Dispatch, getState: () => AppState) { 26 | dispatch(toggle(id)); 27 | const { completedAt: newValue, title } = getState().todos[id]; 28 | const eventName = newValue ? 'complete_todo' : 'uncomplete_todo'; 29 | await analytics().logEvent(eventName, { 30 | id: id, 31 | name: title, 32 | }); 33 | TodosRepository.toggle(userId, id, newValue); 34 | }; 35 | } 36 | 37 | export function editAndSync(userId: string, id: string, newValues: Todo.Values) { 38 | return async function(dispatch: Dispatch) { 39 | dispatch(update(id, newValues)); 40 | TodosRepository.change(userId, id, newValues); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/selectors/todos.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | import * as Domain from '../domain/models'; 4 | import { AppState } from '../modules'; 5 | import round from '../lib/round'; 6 | 7 | function selectTodos(state: AppState) { 8 | return state.todos; 9 | } 10 | 11 | export const getArray = createSelector([selectTodos], todos => 12 | Object.values(todos).map(todo => ({ 13 | id: todo.id, 14 | title: todo.title, 15 | detail: todo.detail, 16 | isDone: Domain.Todo.isDone(todo), 17 | createdAt: new Date(todo.createdAt).getTime(), 18 | updatedAt: new Date(todo.updatedAt).getTime(), 19 | })), 20 | ); 21 | 22 | export const getTodos = createSelector([getArray], todos => todos.sort((a, b) => b.createdAt - a.createdAt)); 23 | 24 | export const getCompletedAll = createSelector([getArray], todos => todos.filter(todo => todo.isDone)); 25 | 26 | export const getNumofCompleted = createSelector([getCompletedAll], todos => todos.length); 27 | 28 | export const getStatistics = createSelector([getArray, getNumofCompleted], (todos, numofCompleted) => { 29 | const numofAll = todos.length; 30 | const numofUncompleted = numofAll - numofCompleted; 31 | const completedRatio = round(numofCompleted / numofAll, 3); 32 | const uncompletedRatio = round(1 - completedRatio, 3); 33 | 34 | return { 35 | numofAll, 36 | numofCompleted, 37 | numofUncompleted, 38 | completedRatio, 39 | uncompletedRatio, 40 | }; 41 | }); 42 | 43 | export const getHistories = createSelector([getCompletedAll], todos => todos.sort((a, b) => b.updatedAt - a.updatedAt)); 44 | -------------------------------------------------------------------------------- /src/components/atoms/TextField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ViewStyle, Keyboard } from 'react-native'; 3 | import { TextInput } from 'react-native-paper'; 4 | import { COLOR } from '../../constants/theme'; 5 | 6 | interface Props { 7 | label: string; 8 | value: string; 9 | onChangeText?: (str: string) => void; 10 | style?: ViewStyle; 11 | autoCompleteType?: 12 | | 'off' 13 | | 'username' 14 | | 'password' 15 | | 'email' 16 | | 'name' 17 | | 'tel' 18 | | 'street-address' 19 | | 'postal-code' 20 | | 'cc-number' 21 | | 'cc-csc' 22 | | 'cc-exp' 23 | | 'cc-exp-month' 24 | | 'cc-exp-year'; 25 | secureTextEntry?: boolean; 26 | disabled?: boolean; 27 | testID?: string; 28 | } 29 | const theme = { 30 | dark: true, 31 | colors: { 32 | primary: COLOR.PRIMARY, 33 | background: COLOR.MAIN, 34 | text: COLOR.WHITE, 35 | placeholder: COLOR.PRIMARY, 36 | }, 37 | }; 38 | 39 | export default function TextField(props: Props) { 40 | const { label, value, onChangeText = () => {}, style, autoCompleteType, secureTextEntry, disabled, testID } = props; 41 | return ( 42 | 55 | ); 56 | } 57 | 58 | export function dismiss() { 59 | Keyboard.dismiss(); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/molecules/Todo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, TouchableHighlight, View, StyleSheet } from 'react-native'; 3 | import Icon from 'react-native-vector-icons/FontAwesome'; 4 | 5 | import { COLOR } from '../../constants/theme'; 6 | 7 | const styles = StyleSheet.create({ 8 | contentContainer: { 9 | backgroundColor: COLOR.MAIN, 10 | height: 120, 11 | flex: 1, 12 | flexDirection: 'row', 13 | justifyContent: 'space-between', 14 | alignItems: 'center', 15 | paddingHorizontal: 20, 16 | }, 17 | title: { 18 | fontWeight: 'bold', 19 | fontSize: 32, 20 | color: COLOR.WHITE, 21 | }, 22 | doneText: { 23 | textDecorationLine: 'line-through', 24 | }, 25 | detail: { 26 | fontSize: 16, 27 | color: COLOR.WHITE, 28 | }, 29 | }); 30 | 31 | interface Props { 32 | onPress: () => void; 33 | title: string; 34 | detail: string | undefined; 35 | isDone: boolean | undefined; 36 | } 37 | 38 | export default function TodoDisplay(props: Props) { 39 | const { onPress, title, detail, isDone } = props; 40 | 41 | const labelStyle = React.useMemo(() => (isDone ? [styles.title, styles.doneText] : styles.title), [isDone]); 42 | 43 | return ( 44 | 45 | 46 | 47 | {title} 48 | {!!detail && {detail}} 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /ios/praiser/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | #import 13 | #import 14 | #import 15 | 16 | @implementation AppDelegate 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 19 | { 20 | [FIRApp configure]; 21 | 22 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 23 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 24 | moduleName:@"praiser" 25 | initialProperties:nil]; 26 | 27 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 28 | 29 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 30 | UIViewController *rootViewController = [UIViewController new]; 31 | rootViewController.view = rootView; 32 | self.window.rootViewController = rootViewController; 33 | [self.window makeKeyAndVisible]; 34 | return YES; 35 | } 36 | 37 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 38 | { 39 | #if DEBUG 40 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 41 | #else 42 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 43 | #endif 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /src/constants/testIDs.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | INITIAL: 'INITIAL', 3 | INITIAL_NEXT_BUTTON1: 'INITIAL_NEXT_BUTTON1', 4 | INITIAL_NEXT_BUTTON2: 'INITIAL_NEXT_BUTTON2', 5 | INITIAL_NEXT_BUTTON3: 'INITIAL_NEXT_BUTTON3', 6 | CHOOSE_LOGIN: 'CHOOSE_LOGIN', 7 | SIGN_UP: 'SIGN_UP', 8 | SIGN_UP_BUTTON: 'SIGN_UP_BUTTON', 9 | SIGN_UP_EMAIL: 'SIGN_UP_EMAIL', 10 | SIGN_UP_PASSWORD: 'SIGN_UP_PASSWORD', 11 | SIGN_UP_REGISTER_BUTTON: 'SIGN_UP_REGISTER_BUTTON', 12 | SIGN_IN: 'SIGN_IN', 13 | SIGN_IN_BUTTON: 'SIGN_IN_BUTTON', 14 | SIGN_IN_EMAIL: 'SIGN_IN_EMAIL', 15 | SIGN_IN_PASSWORD: 'SIGN_IN_PASSWORD', 16 | SIGN_IN_WITH_GOOGLE_BUTTON: 'SIGN_IN_WITH_GOOGLE_BUTTON', 17 | SIGN_IN_EMAIL_BUTTON: 'SIGN_IN_EMAIL_BUTTON', 18 | HOME: 'HOME', 19 | MENU_HEADER_LEFT_BUTTON: 'MENU_HEADER_LEFT_BUTTON', 20 | MENU_DRAWER_ITEMS: 'MENU_DRAWER_ITEMS', 21 | USER_INFO_SCREEN: 'USER_INFO_SCREEN', 22 | USER_INFO_SIGN_OUT_BUTTON: 'USER_INFO_SIGN_OUT_BUTTON', 23 | TODO_OPEN_INPUT_BUTTON: 'TODO_OPEN_INPUT_BUTTON', 24 | TODO_INPUT_SCREEN: 'TODO_INPUT_SCREEN', 25 | TODO_INPUT_TITLE: 'TODO_INPUT_TITLE', 26 | TODO_INPUT_DETAIL: 'TODO_INPUT_DETAIL', 27 | TODO_INPUT_CLOSE: 'TODO_INPUT_CLOSE', 28 | TODO_INPUT_ADD_BUTTON: 'TODO_INPUT_ADD_BUTTON', 29 | TODO_INPUT_DISMISS: 'TODO_INPUT_DISMISS', 30 | TODO_ROW_DONE: 'TODO_ROW_DONE', 31 | TODO_ROW_NOT_DONE: 'TODO_ROW_NOT_DONE', 32 | TODO_ROW_DELETE: 'TODO_ROW_DELETE', 33 | TODO_DETAIL_SCREEN: 'TODO_DETAIL_SCREEN', 34 | TODO_DETAIL_INPUT_TITLE: 'TODO_DETAIL_INPUT_TITLE', 35 | TODO_DETAIL_INPUT_DETAIL: 'TODO_DETAIL_INPUT_DETAIL', 36 | TODO_DETAIL_SUBMIT_BUTTON: 'TODO_DETAIL_SUBMIT_BUTTON', 37 | } as Readonly<{ [key: string]: string }>; 38 | -------------------------------------------------------------------------------- /src/components/organisms/Todos/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, StyleSheet, View } from 'react-native'; 3 | 4 | import * as Todo from './Todo'; 5 | import { COLOR } from '../../../constants/theme'; 6 | 7 | export { Todo }; 8 | const styles = StyleSheet.create({ 9 | container: { 10 | alignSelf: 'stretch', 11 | }, 12 | separator: { 13 | height: 1, 14 | backgroundColor: COLOR.SECONDARY, 15 | }, 16 | }); 17 | 18 | export type State = Array; 19 | interface EditableProps { 20 | isEditable: true; 21 | todos: State; 22 | actions: Todo.EditableActions; 23 | } 24 | interface ReadonlyPrpos { 25 | isEditable: false; 26 | todos: State; 27 | header: React.ReactElement; 28 | actions: Todo.ReadonlyActions; 29 | } 30 | 31 | type Props = EditableProps | ReadonlyPrpos; 32 | 33 | export default function Todos(props: Props) { 34 | if (props.isEditable) { 35 | return ( 36 | } 40 | ItemSeparatorComponent={() => } 41 | keyExtractor={item => item.id} 42 | /> 43 | ); 44 | } 45 | 46 | return ( 47 | } 51 | ItemSeparatorComponent={() => } 52 | keyExtractor={item => item.id} 53 | ListHeaderComponent={props.header} 54 | /> 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /ios/praiser-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/pages/ChooseLogin/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { useNavigation } from '@react-navigation/native'; 4 | 5 | import Button from '../../atoms/Button'; 6 | import { SIGN_IN, SIGN_UP } from '../../../constants/path'; 7 | import { COLOR } from '../../../constants/theme'; 8 | import Logo from '../../atoms/Logo'; 9 | import testIDs from '../../../constants/testIDs'; 10 | 11 | const padding = 20; 12 | const styles = StyleSheet.create({ 13 | container: { 14 | flex: 1, 15 | backgroundColor: COLOR.MAIN, 16 | }, 17 | imageContainer: { 18 | flex: 1, 19 | justifyContent: 'center', 20 | alignItems: 'center', 21 | }, 22 | contentContainer: { 23 | flex: 1, 24 | justifyContent: 'center', 25 | alignItems: 'center', 26 | paddingBottom: 40, 27 | paddingVertical: padding, 28 | }, 29 | button: { 30 | marginBottom: 40, 31 | width: 300, 32 | }, 33 | }); 34 | 35 | export default function ChooseLogin() { 36 | const { navigate } = useNavigation(); 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 |