├── .buckconfig ├── .env ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── App.tsx ├── README.md ├── __mocks__ ├── core │ ├── api │ │ ├── MockBearerAuthorizationRxAxiosProvider.ts │ │ └── index.ts │ └── index.ts ├── data │ ├── data-source │ │ ├── MockLocalAuthenticationDataSource.ts │ │ ├── MockRemoteAuthenticationDataSource.ts │ │ └── index.ts │ └── index.ts ├── di │ └── AppModule.test.ts ├── index.ts ├── react-native-config.js ├── react-native-keychain.js └── time-travel.js ├── __tests__ ├── core │ ├── api │ │ └── RxRemoteProvider.test.ts │ └── error │ │ └── Exception.ts ├── data │ ├── data-source │ │ ├── ApiAuthenticationDataSource.test.ts │ │ └── LocalAuthenticationDataSource.ts │ └── repository │ │ └── AuthenticationRepository.test.ts └── presentation │ └── component │ ├── boundary │ ├── ErrorBoundary.test.tsx │ └── __snapshots__ │ │ └── ErrorBoundary.test.tsx.snap │ ├── button │ ├── FlatButton.test.tsx │ ├── RoundedButton.test.tsx │ └── __snapshots__ │ │ ├── FlatButton.test.tsx.snap │ │ └── RoundedButton.test.tsx.snap │ ├── indicator │ ├── FullScreenLoadingIndicator.test.tsx │ └── __snapshots__ │ │ └── FullScreenLoadingIndicator.test.tsx.snap │ ├── input │ ├── TextField.test.tsx │ └── __snapshots__ │ │ └── TextField.test.tsx.snap │ ├── label │ ├── IconLabel.test.tsx │ ├── TextView.test.tsx │ └── __snapshots__ │ │ ├── IconLabel.test.tsx.snap │ │ └── TextView.test.tsx.snap │ └── listing │ ├── EmptyListView.test.tsx │ ├── ListView.test.tsx │ ├── SkeletonLoadingItem.test.tsx │ └── __snapshots__ │ ├── EmptyListView.test.tsx.snap │ ├── ListView.test.tsx.snap │ └── SkeletonLoadingItem.test.tsx.snap ├── android ├── Gemfile ├── Gemfile.lock ├── app │ ├── _BUCK │ ├── build.gradle │ ├── build_defs.bzl │ ├── debug.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── ReactNativeFlipper.java │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── fastlane │ ├── Appfile │ ├── Fastfile │ ├── Pluginfile │ └── README.md ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── app.json ├── assets ├── background.png ├── background@2x.png ├── background@3x.png ├── icon-email.png ├── icon-email@2x.png ├── icon-email@3x.png ├── icon-eye.png ├── icon-eye@2x.png ├── icon-eye@3x.png ├── icon-facebook.png ├── icon-facebook@2x.png ├── icon-facebook@3x.png ├── icon-google.png ├── icon-google@2x.png ├── icon-google@3x.png ├── icon-lock.png ├── icon-lock@2x.png ├── icon-lock@3x.png ├── icon-twitter.png ├── icon-twitter@2x.png ├── icon-twitter@3x.png └── index.ts ├── babel.config.js ├── blueprint-templates ├── component │ └── __name__.tsx ├── hot-redux-module │ └── __lowerCase_name__ │ │ ├── __name__.epic.ts │ │ ├── __name__.hooks.ts │ │ ├── __name__.slice.ts │ │ ├── __name__.style.ts │ │ ├── __name__.view.tsx │ │ ├── package.json │ │ └── types.ts └── sweet-state-module │ ├── __name__.action.ts │ ├── __name__.container.tsx │ ├── __name__.store.ts │ ├── __name__.style.ts │ ├── __name__.type.ts │ ├── __name__.view.tsx │ ├── constants.ts │ └── package.json ├── demo.gif ├── image.script.js ├── index.js ├── ios ├── Example-tvOS │ └── Info.plist ├── Example-tvOSTests │ └── Info.plist ├── Example.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── Example-tvOS.xcscheme │ │ └── Example.xcscheme ├── Example.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── LaunchScreen.storyboard │ └── main.m ├── ExampleTests │ ├── ExampleTests.m │ └── Info.plist ├── Podfile └── Podfile.lock ├── jest ├── enzyme.setup.js ├── jest.setup.js ├── module.setup.js └── ui.setup.js ├── metro.config.js ├── package.json ├── script └── asset-icon-generator.script.js ├── src ├── core │ ├── api │ │ ├── RxRemoteProvider.ts │ │ ├── RxUnsplashRemoteProvider.ts │ │ └── index.ts │ ├── config │ │ ├── BuildConfig.ts │ │ └── index.ts │ ├── error │ │ ├── Exception.ts │ │ └── index.ts │ ├── index.ts │ ├── style │ │ ├── ColorScheme.ts │ │ ├── TextTheme.ts │ │ ├── Theme.ts │ │ └── index.ts │ └── use-case │ │ ├── UseCase.ts │ │ └── index.ts ├── data │ ├── data-source │ │ ├── authentication │ │ │ ├── AuthenticationDataSource.ts │ │ │ ├── LocalAuthenticationDataSource.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── unsplash │ │ │ ├── UnsplashLocalDataSource.ts │ │ │ ├── UnsplashRemoteDataSource.ts │ │ │ └── index.ts │ │ └── user │ │ │ ├── LocalReqresDataSource.ts │ │ │ ├── RemoteReqresDataSource.ts │ │ │ └── index.ts │ ├── index.ts │ ├── model │ │ ├── ApiResult.ts │ │ ├── AuthenticationModels.ts │ │ ├── UserModel.ts │ │ ├── index.ts │ │ ├── reqres.model.ts │ │ └── unsplash.model.ts │ └── repository │ │ ├── AuthenticationRepository.ts │ │ ├── ReqresUserRepository.ts │ │ ├── UnsplashRepository.ts │ │ └── index.ts ├── di │ ├── AppModule.ts │ ├── DataModule.ts │ ├── RepositoryModule.ts │ ├── index.ts │ └── type.ts ├── domain │ ├── entity │ │ ├── User.ts │ │ ├── authentication.ts │ │ └── index.ts │ ├── index.ts │ ├── repository │ │ ├── AuthenticationRepository.ts │ │ └── index.ts │ └── use-case │ │ ├── authentication │ │ ├── SignIn.use-case.ts │ │ └── index.ts │ │ └── index.ts └── presentation │ ├── component │ ├── boundary │ │ ├── ErrorBoundary.tsx │ │ └── index.ts │ ├── button │ │ ├── FlatButton.tsx │ │ ├── RoundedButton.tsx │ │ └── index.ts │ ├── index.ts │ ├── indicator │ │ ├── FullScreenLoadingIndicator.tsx │ │ └── index.ts │ ├── input │ │ ├── TextField.tsx │ │ └── index.ts │ ├── label │ │ ├── IconLabel.tsx │ │ ├── TextView.tsx │ │ └── index.ts │ ├── listing │ │ ├── EmptyListView.tsx │ │ ├── ListView.tsx │ │ ├── SectionListView.tsx │ │ ├── SkeletonLoadingItem.tsx │ │ └── index.ts │ └── primaryBg │ │ ├── PrimaryBackground.tsx │ │ └── index.ts │ ├── container │ ├── authentication │ │ ├── index.ts │ │ └── sign-in │ │ │ ├── SignIn.hooks.ts │ │ │ ├── SignIn.redux-selector.ts │ │ │ ├── SignIn.view.tsx │ │ │ ├── package.json │ │ │ └── types.ts │ ├── authorized │ │ ├── home │ │ │ ├── Home.hooks.ts │ │ │ ├── Home.item.tsx │ │ │ ├── Home.style.ts │ │ │ ├── Home.view.tsx │ │ │ ├── home.epic.ts │ │ │ ├── home.slice.ts │ │ │ ├── package.json │ │ │ └── types.ts │ │ ├── index.ts │ │ └── profile │ │ │ ├── Profile.epic.ts │ │ │ ├── Profile.hooks.ts │ │ │ ├── Profile.slice.ts │ │ │ ├── Profile.style.ts │ │ │ ├── Profile.view.tsx │ │ │ ├── package.json │ │ │ └── types.ts │ └── index.ts │ ├── hoc │ ├── index.ts │ ├── withHotEnhanceRedux.hoc.tsx │ └── withHotRedux.hoc.tsx │ ├── hook │ ├── index.ts │ └── share-state │ │ ├── index.ts │ │ └── useTheme.ts │ ├── index.ts │ ├── navigation │ ├── AuthenticationStack.tsx │ ├── AuthorizedStack.tsx │ ├── RootNavigator.tsx │ └── index.ts │ ├── resource │ ├── index.ts │ └── values │ │ ├── colors.ts │ │ ├── dimensions.ts │ │ ├── font.ts │ │ ├── index.ts │ │ └── themes.ts │ ├── shared-state │ ├── index.ts │ └── redux │ │ ├── actions │ │ ├── authentication.action.ts │ │ ├── configuration.action.ts │ │ └── index.ts │ │ ├── epic.ts │ │ ├── epics │ │ ├── authentication.epic.ts │ │ ├── configuration.epic.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── reducers │ │ ├── authentication.reducer.ts │ │ ├── configuration.reducer.ts │ │ └── index.ts │ │ ├── store.ts │ │ └── types.ts │ └── storyboard │ ├── Authentication.storyboard.ts │ ├── Authorized.storyboard.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | API_URL=https://reqres.in/api/ 2 | UNSPLASH_KEY=64d4b4eafb6e794548a9b03518ece8bd7193ccf26bbaa188e00399ac91d80759 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | }; 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * 5 | * Generated with the TypeScript template 6 | * https://github.com/react-native-community/react-native-template-typescript 7 | * 8 | * @format 9 | */ 10 | 11 | import React from 'react'; 12 | 13 | import {Provider} from 'react-redux'; 14 | 15 | import {registerDependencies, registerFlyValue, container} from '@di'; 16 | import {RootNavigator} from '@presentation'; 17 | import {StoreContainer} from '@shared-state'; 18 | 19 | registerDependencies(); 20 | registerFlyValue(); 21 | const App = () => { 22 | return ( 23 | ('StoreContainer').store}> 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | An Example for rn-clean-architecture-template 3 | 4 | Special thanks to Jerry Tran 5 | https://github.com/jerrytran-wrk/rn-clean-architecture-template 6 | 7 | ![demo](demo.gif) 8 | 9 | ## :star: Features 10 | 11 | - Authentication flow with react-navigation 12 | - Combine local and remote data for UnsplashRepository 13 | - Infinity scroll with Observable epic and SectionList 14 | 15 | ## :star: Todos 16 | 17 | - CI intergrate 18 | -------------------------------------------------------------------------------- /__mocks__/core/api/MockBearerAuthorizationRxAxiosProvider.ts: -------------------------------------------------------------------------------- 1 | import {BearerAuthorizationRxAxiosProvider} from '@core'; 2 | import {AxiosRequestConfig, AxiosResponse} from 'axios'; 3 | import {Observable} from 'rxjs'; 4 | 5 | export class MockBearerAuthorizationRxAxiosProvider extends BearerAuthorizationRxAxiosProvider { 6 | mockResult: Observable> = new Observable(); 7 | overrideFunctions: ('request' | '')[] = []; 8 | logs: string[] = []; 9 | request(requestConfig: AxiosRequestConfig): Observable> { 10 | this.logs.push('request'); 11 | if (this.overrideFunctions.includes('request')) { 12 | return this.mockResult; 13 | } 14 | return super.request(requestConfig); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /__mocks__/core/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MockBearerAuthorizationRxAxiosProvider'; 2 | -------------------------------------------------------------------------------- /__mocks__/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | -------------------------------------------------------------------------------- /__mocks__/data/data-source/MockLocalAuthenticationDataSource.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs'; 2 | 3 | import {LocalAuthenticationDataSource} from '@data'; 4 | 5 | export class MockLocalAuthenticationDataSource 6 | implements LocalAuthenticationDataSource { 7 | mockSaveTokenResult: Observable = new Observable(); 8 | mockGetTokenResult: Observable = new Observable(); 9 | saveToken(): Observable { 10 | return this.mockSaveTokenResult; 11 | } 12 | getToken(): Observable { 13 | return this.mockGetTokenResult; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__mocks__/data/data-source/MockRemoteAuthenticationDataSource.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs'; 2 | 3 | import { 4 | RemoteAuthenticationDataSource, 5 | SignInResponseData, 6 | ApiResult, 7 | } from '@data'; 8 | 9 | export class MockRemoteAuthenticationDataSource 10 | implements RemoteAuthenticationDataSource { 11 | mockSignInResult: Observable< 12 | ApiResult 13 | > = new Observable(); 14 | signIn(): Observable> { 15 | return this.mockSignInResult; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /__mocks__/data/data-source/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MockRemoteAuthenticationDataSource'; 2 | export * from './MockLocalAuthenticationDataSource'; 3 | -------------------------------------------------------------------------------- /__mocks__/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data-source'; 2 | -------------------------------------------------------------------------------- /__mocks__/di/AppModule.test.ts: -------------------------------------------------------------------------------- 1 | import {registerDependencies, container} from '@di'; 2 | import {RxRemoteProvider} from '@core'; 3 | import {LocalAuthenticationDataSource} from '@data'; 4 | 5 | describe('Register app dependencies', () => { 6 | it('register', () => { 7 | registerDependencies(); 8 | expect(container.resolve('ApiProvider')).toBeDefined(); 9 | expect( 10 | container.resolve( 11 | 'LocalAuthenticationDataSource', 12 | ), 13 | ).toBeDefined(); 14 | expect( 15 | container.resolve( 16 | 'LocalAuthenticationDataSource', 17 | ), 18 | ).toBeDefined(); 19 | expect( 20 | container.resolve( 21 | 'LocalAuthenticationDataSource', 22 | ), 23 | ).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__mocks__/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './time-travel'; 3 | export * from './react-native-keychain'; 4 | export * from './data'; 5 | -------------------------------------------------------------------------------- /__mocks__/react-native-config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | API_URL: 'API_URL', 3 | }; 4 | -------------------------------------------------------------------------------- /__mocks__/react-native-keychain.js: -------------------------------------------------------------------------------- 1 | export const keychainMock = { 2 | SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY', 3 | SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE', 4 | SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE', 5 | setGenericPassword: jest.fn().mockResolvedValue(), 6 | getGenericPassword: jest.fn().mockResolvedValue(), 7 | resetGenericPassword: jest.fn().mockResolvedValue(), 8 | }; 9 | -------------------------------------------------------------------------------- /__mocks__/time-travel.js: -------------------------------------------------------------------------------- 1 | // timeTravel.js 2 | import MockDate from 'mockdate'; 3 | 4 | const FRAME_TIME = 10; 5 | 6 | const advanceOneFrame = () => { 7 | const now = Date.now(); 8 | MockDate.set(new Date(now + FRAME_TIME)); 9 | jest.advanceTimersByTime(FRAME_TIME); 10 | }; 11 | 12 | const timeTravel = (msToAdvance = FRAME_TIME) => { 13 | const numberOfFramesToRun = msToAdvance / FRAME_TIME; 14 | let framesElapsed = 0; 15 | 16 | // Step through each of the frames until we've ran them all 17 | while (framesElapsed < numberOfFramesToRun) { 18 | advanceOneFrame(); 19 | framesElapsed++; 20 | } 21 | }; 22 | 23 | export default timeTravel; 24 | export const setupTimeTravel = () => { 25 | MockDate.set(0); 26 | jest.useFakeTimers(); 27 | }; 28 | -------------------------------------------------------------------------------- /__tests__/core/api/RxRemoteProvider.test.ts: -------------------------------------------------------------------------------- 1 | import {AxiosRequestConfig} from 'axios'; 2 | import { 3 | BearerAuthorizationRxAxiosProvider, 4 | RxAxiosProviderException, 5 | } from '@core'; 6 | import {MockBearerAuthorizationRxAxiosProvider} from '@mocks'; 7 | 8 | describe('RxRemoteProvider', () => { 9 | let provider: BearerAuthorizationRxAxiosProvider; 10 | let requestConfigs: AxiosRequestConfig[] = []; 11 | let responsePromises: {[key in string]: Promise} = {}; 12 | 13 | beforeEach(() => { 14 | responsePromises = {}; 15 | requestConfigs = []; 16 | provider = new BearerAuthorizationRxAxiosProvider({ 17 | adapter: (config) => { 18 | requestConfigs.push(config); 19 | return responsePromises[config?.url ?? '']; 20 | }, 21 | }); 22 | }); 23 | 24 | it('request successfully', (done) => { 25 | const config: AxiosRequestConfig = {url: 'Test', method: 'post'}; 26 | const response = new Promise((res) => res({data: {test: 'test'}})); 27 | responsePromises = {[config.url ?? '']: response}; 28 | // success case 29 | provider.request(config).subscribe({ 30 | next: (val) => { 31 | expect(val.data.test).toBe('test'); 32 | expect(requestConfigs.length).toBeGreaterThan(0); 33 | }, 34 | complete: done, 35 | }); 36 | }); 37 | 38 | it('request failed', (done) => { 39 | const config: AxiosRequestConfig = {url: 'Test', method: 'post'}; 40 | const response = new Promise((_, rej) => rej({data: {}})); 41 | responsePromises = {[config.url ?? '']: response}; 42 | 43 | provider.post(config.url!, config.data).subscribe({ 44 | error: (error) => { 45 | expect(error).toBeInstanceOf(RxAxiosProviderException); 46 | done(); 47 | }, 48 | }); 49 | }); 50 | 51 | it('post', () => { 52 | const mockProvider = new MockBearerAuthorizationRxAxiosProvider({}); 53 | mockProvider.post('test', {}); 54 | expect(mockProvider.logs).toEqual(['request']); 55 | }); 56 | 57 | it('get', () => { 58 | const mockProvider = new MockBearerAuthorizationRxAxiosProvider({}); 59 | mockProvider.get('test'); 60 | expect(mockProvider.logs).toEqual(['request']); 61 | }); 62 | it('delete', () => { 63 | const mockProvider = new MockBearerAuthorizationRxAxiosProvider({}); 64 | mockProvider.delete('test'); 65 | expect(mockProvider.logs).toEqual(['request']); 66 | }); 67 | it('put', () => { 68 | const mockProvider = new MockBearerAuthorizationRxAxiosProvider({}); 69 | mockProvider.put('test', {}); 70 | expect(mockProvider.logs).toEqual(['request']); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /__tests__/core/error/Exception.ts: -------------------------------------------------------------------------------- 1 | import {RemoteException, LocalException} from '@core'; 2 | 3 | describe('Exception', () => { 4 | describe('Remote Exception', () => { 5 | it('get raw error', () => { 6 | const raw = {dummy: ''}; 7 | const exception = new RemoteException(raw); 8 | expect(exception.rootCause).toBe(raw); 9 | }); 10 | }); 11 | 12 | describe('Local Exception', () => { 13 | it('get raw error', () => { 14 | const raw = {dummy: ''}; 15 | const exception = new LocalException(raw); 16 | expect(exception.rootCause).toBe(raw); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/data/data-source/ApiAuthenticationDataSource.test.ts: -------------------------------------------------------------------------------- 1 | import {ApiAuthenticationDataSource} from '@data'; 2 | import {MockBearerAuthorizationRxAxiosProvider} from '@mocks'; 3 | import {of, throwError} from 'rxjs'; 4 | import {RxAxiosProviderException} from '@core'; 5 | 6 | describe('ApiAuthenticationDataSource', () => { 7 | let dataSource: ApiAuthenticationDataSource; 8 | let provider: MockBearerAuthorizationRxAxiosProvider; 9 | beforeEach(() => { 10 | provider = new MockBearerAuthorizationRxAxiosProvider({}); 11 | dataSource = new ApiAuthenticationDataSource(provider); 12 | }); 13 | it('sign in successfully', async (done) => { 14 | provider.overrideFunctions = ['request']; 15 | provider.mockResult = of({ 16 | data: {data: {token: 'test'}}, 17 | config: {}, 18 | status: 200, 19 | headers: {}, 20 | statusText: '', 21 | }); 22 | dataSource.signIn({}).subscribe({ 23 | next: (response) => expect(response.data.token).toBe('test'), 24 | complete: done, 25 | }); 26 | }); 27 | it('sign in failed', async (done) => { 28 | provider.overrideFunctions = ['request']; 29 | provider.mockResult = throwError( 30 | new RxAxiosProviderException(JSON.parse('{}')), 31 | ); 32 | dataSource.signIn({}).subscribe({ 33 | error: (error: RxAxiosProviderException) => { 34 | expect(error).toBeInstanceOf(RxAxiosProviderException); 35 | expect(error.rootCause).toStrictEqual({}); 36 | done(); 37 | }, 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/data/data-source/LocalAuthenticationDataSource.ts: -------------------------------------------------------------------------------- 1 | import * as Keychain from 'react-native-keychain'; 2 | import {KeyChainAuthenticationDataSource} from '@data'; 3 | import {LocalException} from '@core'; 4 | 5 | describe('ApiAuthenticationDataSource', () => { 6 | let dataSource: KeyChainAuthenticationDataSource; 7 | beforeEach(() => { 8 | dataSource = new KeyChainAuthenticationDataSource(); 9 | }); 10 | describe('save token', () => { 11 | it('save token success', (done) => { 12 | dataSource.saveToken('test', 'test').subscribe({ 13 | next: (val) => expect(val).toBeTruthy(), 14 | complete: done, 15 | }); 16 | }); 17 | it('save token failed', (done) => { 18 | const spy = jest 19 | .spyOn(Keychain, 'setGenericPassword') 20 | .mockImplementation(jest.fn().mockRejectedValue(1)); 21 | 22 | dataSource.saveToken('test', 'test').subscribe({ 23 | error: (error: LocalException) => { 24 | expect(error).toBeInstanceOf(LocalException); 25 | done(); 26 | }, 27 | }); 28 | spy.mockRestore(); 29 | }); 30 | }); 31 | 32 | describe('get token', () => { 33 | it('successfully', (done) => { 34 | const spy = jest 35 | .spyOn(Keychain, 'getGenericPassword') 36 | .mockImplementation(jest.fn().mockResolvedValue({password: 'test'})); 37 | 38 | dataSource.getToken().subscribe({ 39 | next: (val) => expect(val).toBe('test'), 40 | complete: done, 41 | }); 42 | spy.mockRestore(); 43 | }); 44 | it('empty', (done) => { 45 | const spy = jest 46 | .spyOn(Keychain, 'getGenericPassword') 47 | .mockImplementation(jest.fn().mockResolvedValue(null)); 48 | 49 | dataSource.getToken().subscribe({ 50 | error: (error: LocalException) => { 51 | expect(error).toBeInstanceOf(LocalException); 52 | done(); 53 | }, 54 | }); 55 | spy.mockRestore(); 56 | }); 57 | it('failed', (done) => { 58 | const spy = jest 59 | .spyOn(Keychain, 'getGenericPassword') 60 | .mockImplementation(jest.fn().mockRejectedValue(1)); 61 | 62 | dataSource.getToken().subscribe({ 63 | error: (error: LocalException) => { 64 | expect(error).toBeInstanceOf(LocalException); 65 | done(); 66 | }, 67 | }); 68 | spy.mockRestore(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /__tests__/data/repository/AuthenticationRepository.test.ts: -------------------------------------------------------------------------------- 1 | import {of, throwError} from 'rxjs'; 2 | 3 | import {CombineAuthenticationRepository} from '@data'; 4 | import { 5 | MockRemoteAuthenticationDataSource, 6 | MockLocalAuthenticationDataSource, 7 | } from '@mocks'; 8 | import {RemoteException, LocalException} from '@core'; 9 | 10 | describe('AuthenticationRepository', () => { 11 | let repository: CombineAuthenticationRepository; 12 | let localDataSource: MockLocalAuthenticationDataSource; 13 | let remoteDataSource: MockRemoteAuthenticationDataSource; 14 | beforeEach(() => { 15 | localDataSource = new MockLocalAuthenticationDataSource(); 16 | remoteDataSource = new MockRemoteAuthenticationDataSource(); 17 | repository = new CombineAuthenticationRepository( 18 | localDataSource, 19 | remoteDataSource, 20 | ); 21 | }); 22 | 23 | describe('sign in', () => { 24 | it('successfully', (done) => { 25 | remoteDataSource.mockSignInResult = of({data: {token: 'test', user: {}}}); 26 | repository.signIn({}).subscribe({ 27 | next: (result) => 28 | expect(result).toStrictEqual({fromLocal: false, token: 'test'}), 29 | complete: done, 30 | }); 31 | }); 32 | 33 | it('failed', (done) => { 34 | remoteDataSource.mockSignInResult = throwError(new RemoteException({})); 35 | repository.signIn({}).subscribe({ 36 | error: (error) => { 37 | expect(error).toBeInstanceOf(RemoteException); 38 | done(); 39 | }, 40 | }); 41 | }); 42 | }); 43 | 44 | describe('get token', () => { 45 | it('successfully', (done) => { 46 | localDataSource.mockGetTokenResult = of('token'); 47 | repository.getToken().subscribe({ 48 | next: (result) => expect(result).toBe('token'), 49 | complete: done, 50 | }); 51 | }); 52 | 53 | it('failed', (done) => { 54 | localDataSource.mockGetTokenResult = throwError(new LocalException({})); 55 | repository.getToken().subscribe({ 56 | error: (error) => { 57 | expect(error).toBeInstanceOf(LocalException); 58 | done(); 59 | }, 60 | }); 61 | }); 62 | }); 63 | 64 | describe('save token', () => { 65 | it('successfully', (done) => { 66 | localDataSource.mockSaveTokenResult = of(true); 67 | repository.saveToken('test', 'key').subscribe({ 68 | next: (result) => expect(result).toBeTruthy(), 69 | complete: done, 70 | }); 71 | }); 72 | 73 | it('failed', (done) => { 74 | localDataSource.mockSaveTokenResult = throwError(new LocalException({})); 75 | repository.saveToken('test', 'test').subscribe({ 76 | error: (error) => { 77 | expect(error).toBeInstanceOf(LocalException); 78 | done(); 79 | }, 80 | }); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /__tests__/presentation/component/boundary/ErrorBoundary.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {View} from 'react-native'; 6 | import {ErrorBoundary} from '@components'; 7 | 8 | // Note: test renderer must be required after react-native. 9 | import renderer from 'react-test-renderer'; 10 | import {shallow} from 'enzyme'; 11 | it('renders correctly', () => { 12 | const instance = renderer.create( 13 | 14 | 15 | , 16 | ); 17 | // instance.root 18 | expect(instance.toJSON()).toMatchSnapshot(); 19 | }); 20 | 21 | it('renders errors', () => { 22 | const instance = shallow( 23 | 24 | 25 | , 26 | ); 27 | instance.setState({hasError: true}); 28 | expect(instance.childAt(0).text()).toBe('Error Fallback'); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/presentation/component/boundary/__snapshots__/ErrorBoundary.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ``; 4 | -------------------------------------------------------------------------------- /__tests__/presentation/component/button/FlatButton.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {FlatButton} from '@components'; 6 | 7 | // Note: test renderer must be required after react-native. 8 | import renderer from 'react-test-renderer'; 9 | 10 | it('renders correctly', () => { 11 | const instance = renderer.create(); 12 | expect(instance.toJSON()).toMatchSnapshot(); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/presentation/component/button/RoundedButton.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {RoundedButton} from '@components'; 6 | 7 | // Note: test renderer must be required after react-native. 8 | import renderer from 'react-test-renderer'; 9 | 10 | it('renders correctly', () => { 11 | const instance = renderer.create(); 12 | expect(instance.toJSON()).toMatchSnapshot(); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/presentation/component/button/__snapshots__/FlatButton.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 26 | 36 | 54 | 55 | `; 56 | -------------------------------------------------------------------------------- /__tests__/presentation/component/button/__snapshots__/RoundedButton.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 22 | 52 | 60 | 61 | 79 | 80 | `; 81 | -------------------------------------------------------------------------------- /__tests__/presentation/component/indicator/FullScreenLoadingIndicator.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {Modal} from 'react-native'; 6 | 7 | import { 8 | FullScreenLoadingIndicator, 9 | FullScreenLoadingIndicatorProps, 10 | } from '@components'; 11 | 12 | // Note: test renderer must be required after react-native. 13 | import renderer from 'react-test-renderer'; 14 | 15 | it('test visible', () => { 16 | const props: FullScreenLoadingIndicatorProps = { 17 | visible: true, 18 | }; 19 | const instance = renderer.create(); 20 | expect(instance.root.findByType(Modal).props?.visible).toBeTruthy(); 21 | }); 22 | 23 | it('render correctly', () => { 24 | const props: FullScreenLoadingIndicatorProps = { 25 | visible: true, 26 | }; 27 | const instance = renderer.create(); 28 | expect(instance.toJSON()).toMatchSnapshot(); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/presentation/component/indicator/__snapshots__/FullScreenLoadingIndicator.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`render correctly 1`] = ` 4 | 10 | 20 | 32 | 38 | 39 | 40 | 41 | `; 42 | -------------------------------------------------------------------------------- /__tests__/presentation/component/input/TextField.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {Text, Image, TextInputProps} from 'react-native'; 6 | 7 | import {TextField} from '@components'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | describe('Test TextView', () => { 13 | it('render correctly', () => { 14 | const instance = renderer.create(); 15 | expect(instance.toJSON()).toMatchSnapshot(); 16 | }); 17 | it('Test render prefix', () => { 18 | const instance = renderer.create(); 19 | expect(instance.root.findAllByType(Image)).toHaveLength(0); 20 | 21 | // test render Prefix 22 | instance.update(} />); 23 | expect(instance.root.findByProps({children: 'test'})).toBeDefined(); 24 | expect(instance.root.findAllByType(Image)).toHaveLength(0); 25 | 26 | // test render Prefix 27 | const prefixIcon = 1; 28 | instance.update(); 29 | expect(instance.root.findByType(Image)?.props?.source).toEqual(prefixIcon); 30 | }); 31 | 32 | it('Test render suffix', () => { 33 | const instance = renderer.create(); 34 | expect(instance.root.findAllByType(Image)).toHaveLength(0); 35 | 36 | // test render Prefix 37 | instance.update(} />); 38 | expect(instance.root.findByProps({children: 'test'})).toBeDefined(); 39 | expect(instance.root.findAllByType(Image)).toHaveLength(0); 40 | 41 | // test render Prefix 42 | const prefixIcon = 1; 43 | instance.update(); 44 | expect(instance.root.findByType(Image)?.props?.source).toEqual(prefixIcon); 45 | }); 46 | 47 | it('Test render TextInput with props', () => { 48 | const inputProps: TextInputProps = {value: 'test'}; 49 | const instance = renderer.create(); 50 | expect(instance.root.findByProps(inputProps)).toBeDefined(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/presentation/component/input/__snapshots__/TextField.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test TextView render correctly 1`] = ` 4 | 12 | 20 | 27 | 43 | 44 | 52 | 62 | 63 | `; 64 | -------------------------------------------------------------------------------- /__tests__/presentation/component/label/IconLabel.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {Text, Image} from 'react-native'; 6 | 7 | import {IconLabel} from '@components'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('render correctly', () => { 13 | const instance = renderer.create(); 14 | expect(instance.toJSON()).toMatchSnapshot(); 15 | }); 16 | it('render prefix', () => { 17 | const instance = renderer.create(); 18 | const textView = instance.root.findByType(Text); 19 | expect(textView.props.children).toBe('test'); 20 | expect(instance.root.findAllByType(Image)).toHaveLength(0); 21 | 22 | // test render Prefix 23 | instance.update(} />); 24 | expect(instance.root.findByProps({children: 'test'})).toBeDefined(); 25 | expect(instance.root.findAllByType(Image)).toHaveLength(0); 26 | 27 | // test render Prefix 28 | const prefixIcon = 1; 29 | instance.update(); 30 | expect(instance.root.findByType(Image)?.props?.source).toEqual(prefixIcon); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/presentation/component/label/TextView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {Text} from 'react-native'; 6 | import {TextView} from '@components'; 7 | 8 | // Note: test renderer must be required after react-native. 9 | import renderer from 'react-test-renderer'; 10 | 11 | it('renders correctly', () => { 12 | const instance = renderer.create(); 13 | expect(instance.toJSON()).toMatchSnapshot(); 14 | }); 15 | 16 | it('renders with text', () => { 17 | const instance = renderer.create(); 18 | const textView = instance.root.findByType(Text); 19 | expect(textView.props.children).toBe('test'); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/presentation/component/label/__snapshots__/IconLabel.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`render correctly 1`] = ` 4 | 14 | 24 | 25 | `; 26 | -------------------------------------------------------------------------------- /__tests__/presentation/component/label/__snapshots__/TextView.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 11 | `; 12 | -------------------------------------------------------------------------------- /__tests__/presentation/component/listing/EmptyListView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {EmptyListView} from '@components'; 6 | 7 | // Note: test renderer must be required after react-native. 8 | import renderer from 'react-test-renderer'; 9 | 10 | it('renders correctly', () => { 11 | const instance = renderer.create(); 12 | expect(instance.toJSON()).toMatchSnapshot(); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/presentation/component/listing/ListView.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {ListView} from '@components'; 6 | 7 | // Note: test renderer must be required after react-native. 8 | import renderer from 'react-test-renderer'; 9 | 10 | it('renders correctly', () => { 11 | const instance = renderer.create(); 12 | expect(instance.toJSON()).toMatchSnapshot(); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/presentation/component/listing/SkeletonLoadingItem.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | import React from 'react'; 5 | import {SkeletonLoadingItem} from '@components'; 6 | 7 | // Note: test renderer must be required after react-native. 8 | import renderer from 'react-test-renderer'; 9 | import {setupTimeTravel} from '@mocks'; 10 | 11 | beforeEach(setupTimeTravel); 12 | 13 | it('renders correctly', () => { 14 | const instance = renderer.create(); 15 | expect(instance.toJSON()).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/presentation/component/listing/__snapshots__/EmptyListView.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 15 | 23 | Oops! 24 | 25 | 34 | Không có dữ liệu! 35 | 36 | 37 | `; 38 | -------------------------------------------------------------------------------- /__tests__/presentation/component/listing/__snapshots__/ListView.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 13 | } 15 | collapsable={false} 16 | disableVirtualization={false} 17 | getItem={[Function]} 18 | getItemCount={[Function]} 19 | horizontal={false} 20 | initialNumToRender={10} 21 | keyExtractor={[Function]} 22 | maxToRenderPerBatch={10} 23 | onContentSizeChange={[Function]} 24 | onEndReached={[Function]} 25 | onEndReachedThreshold={2} 26 | onGestureHandlerEvent={[Function]} 27 | onGestureHandlerStateChange={[Function]} 28 | onLayout={[Function]} 29 | onMomentumScrollEnd={[Function]} 30 | onScroll={[Function]} 31 | onScrollBeginDrag={[Function]} 32 | onScrollEndDrag={[Function]} 33 | refreshControl={ 34 | 37 | } 38 | removeClippedSubviews={false} 39 | renderItem={[Function]} 40 | renderScrollComponent={[Function]} 41 | scrollEventThrottle={50} 42 | stickyHeaderIndices={Array []} 43 | style={ 44 | Object { 45 | "flex": 1, 46 | } 47 | } 48 | updateCellsBatchingPeriod={50} 49 | viewabilityConfigCallbackPairs={Array []} 50 | windowSize={21} 51 | > 52 | 53 | 54 | 65 | 73 | Oops! 74 | 75 | 84 | Không có dữ liệu! 85 | 86 | 87 | 88 | 89 | 90 | `; 91 | -------------------------------------------------------------------------------- /android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /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.example", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.example", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/debug.keystore -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | # hermes 12 | -keep class com.facebook.hermes.unicode.** { *; } 13 | -keep class com.facebook.jni.** { *; } 14 | 15 | # BuildConfig 16 | -keep class com.example.BuildConfig { *; } 17 | 18 | # fastimage 19 | -keep public class com.dylanvann.fastimage.* {*;} 20 | -keep public class com.dylanvann.fastimage.** {*;} 21 | -keep public class * implements com.bumptech.glide.module.GlideModule 22 | -keep public class * extends com.bumptech.glide.module.AppGlideModule 23 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { 24 | **[] $VALUES; 25 | public *; 26 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/example/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.example; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import com.facebook.react.ReactActivityDelegate; 5 | import com.facebook.react.ReactRootView; 6 | import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "Example"; 17 | } 18 | 19 | @Override 20 | protected ReactActivityDelegate createReactActivityDelegate() { 21 | return new ReactActivityDelegate(this, getMainComponentName()) { 22 | @Override 23 | protected ReactRootView createRootView() { 24 | return new RNGestureHandlerEnabledRootView(MainActivity.this); 25 | } 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = 17 | new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | // Packages that cannot be autolinked yet can be added manually here, for example: 28 | // packages.add(new MyReactNativePackage()); 29 | return packages; 30 | } 31 | 32 | @Override 33 | protected String getJSMainModuleName() { 34 | return "index"; 35 | } 36 | }; 37 | 38 | @Override 39 | public ReactNativeHost getReactNativeHost() { 40 | return mReactNativeHost; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | super.onCreate(); 46 | SoLoader.init(this, /* native exopackage */ false); 47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 48 | } 49 | 50 | /** 51 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 52 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 53 | * 54 | * @param context 55 | * @param reactInstanceManager 56 | */ 57 | private static void initializeFlipper( 58 | Context context, ReactInstanceManager reactInstanceManager) { 59 | if (BuildConfig.DEBUG) { 60 | try { 61 | /* 62 | We use reflection here to pick up the class that initializes Flipper, 63 | since Flipper library is not available in release mode 64 | */ 65 | Class aClass = Class.forName("com.example.ReactNativeFlipper"); 66 | aClass 67 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 68 | .invoke(null, context, reactInstanceManager); 69 | } catch (ClassNotFoundException e) { 70 | e.printStackTrace(); 71 | } catch (NoSuchMethodException e) { 72 | e.printStackTrace(); 73 | } catch (IllegalAccessException e) { 74 | e.printStackTrace(); 75 | } catch (InvocationTargetException e) { 76 | e.printStackTrace(); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Example 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "29.0.2" 6 | minSdkVersion = 21 7 | compileSdkVersion = 29 8 | targetSdkVersion = 29 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.5.3") 16 | // NOTE: Do not place your application dependencies here; they belong 17 | // in the individual module build.gradle files 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | mavenLocal() 24 | maven { 25 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 26 | url("$rootDir/../node_modules/react-native/android") 27 | } 28 | maven { 29 | // Android JSC is installed from npm 30 | url("$rootDir/../node_modules/jsc-android/dist") 31 | } 32 | 33 | google() 34 | jcenter() 35 | maven { url 'https://www.jitpack.io' } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name("com.example") # e.g. com.krausefx.app 3 | -------------------------------------------------------------------------------- /android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:android) 17 | 18 | platform :android do 19 | 20 | desc "Distribute app via diawi" 21 | lane :beta do 22 | sh("yarn") 23 | gradle(task: "clean assembleRelease") 24 | diawi( 25 | token: "2SZhLzXlmDDLV5eKishHC6lXcJj7FuUxSMUY2SQrvn" 26 | ) 27 | # sh "your_script.sh" 28 | # You can also use other beta testing services here 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /android/fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-diawi' 6 | -------------------------------------------------------------------------------- /android/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew install fastlane` 16 | 17 | # Available Actions 18 | ## Android 19 | ### android beta 20 | ``` 21 | fastlane android beta 22 | ``` 23 | Submit a new Beta Build to Crashlytics Beta 24 | 25 | ---- 26 | 27 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 28 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 29 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.37.0 29 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Example' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example", 3 | "displayName": "Example" 4 | } -------------------------------------------------------------------------------- /assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/background.png -------------------------------------------------------------------------------- /assets/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/background@2x.png -------------------------------------------------------------------------------- /assets/background@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/background@3x.png -------------------------------------------------------------------------------- /assets/icon-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-email.png -------------------------------------------------------------------------------- /assets/icon-email@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-email@2x.png -------------------------------------------------------------------------------- /assets/icon-email@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-email@3x.png -------------------------------------------------------------------------------- /assets/icon-eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-eye.png -------------------------------------------------------------------------------- /assets/icon-eye@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-eye@2x.png -------------------------------------------------------------------------------- /assets/icon-eye@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-eye@3x.png -------------------------------------------------------------------------------- /assets/icon-facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-facebook.png -------------------------------------------------------------------------------- /assets/icon-facebook@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-facebook@2x.png -------------------------------------------------------------------------------- /assets/icon-facebook@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-facebook@3x.png -------------------------------------------------------------------------------- /assets/icon-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-google.png -------------------------------------------------------------------------------- /assets/icon-google@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-google@2x.png -------------------------------------------------------------------------------- /assets/icon-google@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-google@3x.png -------------------------------------------------------------------------------- /assets/icon-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-lock.png -------------------------------------------------------------------------------- /assets/icon-lock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-lock@2x.png -------------------------------------------------------------------------------- /assets/icon-lock@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-lock@3x.png -------------------------------------------------------------------------------- /assets/icon-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-twitter.png -------------------------------------------------------------------------------- /assets/icon-twitter@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-twitter@2x.png -------------------------------------------------------------------------------- /assets/icon-twitter@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/assets/icon-twitter@3x.png -------------------------------------------------------------------------------- /assets/index.ts: -------------------------------------------------------------------------------- 1 | export const BACKGROUND = require('./background.png'); 2 | export const ICON_EMAIL = require('./icon-email.png'); 3 | export const ICON_EYE = require('./icon-eye.png'); 4 | export const ICON_FACEBOOK = require('./icon-facebook.png'); 5 | export const ICON_GOOGLE = require('./icon-google.png'); 6 | export const ICON_LOCK = require('./icon-lock.png'); 7 | export const ICON_TWITTER = require('./icon-twitter.png'); 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: [ 4 | [ 5 | 'module-resolver', 6 | { 7 | root: ['./src'], 8 | extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'], 9 | alias: { 10 | // Top Level alias 11 | '@assets': './assets', 12 | '@presentation': './src/presentation', 13 | '@core': './src/core', 14 | '@domain': './src/domain', 15 | '@data': './src/data', 16 | '@di': './src/di', 17 | // Presentation level alias 18 | '@hocs': './src/presentation/hoc', 19 | '@hooks': './src/presentation/hook', 20 | '@components': './src/presentation/component', 21 | '@containers': './src/presentation/container', 22 | '@shared-state': './src/presentation/shared-state', 23 | '@resources': './src/presentation/resource', 24 | '@storyboards': './src/presentation/storyboard', 25 | '@navigation': './src/presentation/navigation', 26 | // development 27 | '@mocks': './__mocks__', 28 | }, 29 | }, 30 | ], 31 | 'babel-plugin-transform-typescript-metadata', 32 | ['@babel/plugin-proposal-decorators', {legacy: true}], 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /blueprint-templates/component/__name__.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, StyleSheet} from 'react-native'; 3 | 4 | export interface {{$name}}Props {}; 5 | 6 | export const {{$name}}: React.FC<{{$name}}Props> = (props) => { 7 | return ; 8 | }; 9 | 10 | const styles = StyleSheet.create({ 11 | container: {}, 12 | }); 13 | -------------------------------------------------------------------------------- /blueprint-templates/hot-redux-module/__lowerCase_name__/__name__.epic.ts: -------------------------------------------------------------------------------- 1 | import {combineEpics} from 'redux-observable'; 2 | import { 3 | 4 | } from 'rxjs/operators'; 5 | import {} from 'rxjs'; 6 | 7 | import { {{camelCase name}}Slice} from './{{name}}.slice'; 8 | import { {{name}}State} from './types'; 9 | 10 | 11 | export const {{camelCase name}}Epic = combineEpics(); 12 | -------------------------------------------------------------------------------- /blueprint-templates/hot-redux-module/__lowerCase_name__/__name__.hooks.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {} from 'react-native'; 3 | 4 | import {useDispatch, useSelector, Selector} from 'react-redux'; 5 | import { {{camelCase name}}Slice, INITIAL_STATE} from './{{name}}.slice'; 6 | import { {{name}}ReduxSelectionState, StoreStateWith{{name}} } from './types'; 7 | 8 | export const {{camelCase name}}Selector: Selector< 9 | StoreStateWith{{name}}, 10 | {{name}}ReduxSelectionState 11 | > = ({ {{name}} = INITIAL_STATE}) => {{name}}; 12 | 13 | const { 14 | actions: {}, 15 | } = {{camelCase name}}Slice; 16 | 17 | export function use{{name}}Model() { 18 | const {} = useSelector< 19 | StoreStateWith{{name}}, 20 | {{name}}ReduxSelectionState 21 | >( {{camelCase name}}Selector); 22 | const dispatch = useDispatch(); 23 | 24 | 25 | 26 | return {}; 27 | } 28 | -------------------------------------------------------------------------------- /blueprint-templates/hot-redux-module/__lowerCase_name__/__name__.slice.ts: -------------------------------------------------------------------------------- 1 | import {createSlice} from '@reduxjs/toolkit'; 2 | import {} from '@reduxjs/toolkit'; 3 | import { {{name}}State} from './types'; 4 | 5 | export const INITIAL_STATE: {{name}}State = { 6 | 7 | }; 8 | export const {{camelCase name}}Slice = createSlice({ 9 | name: '{{name}}', 10 | initialState: INITIAL_STATE, 11 | reducers: { 12 | 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /blueprint-templates/hot-redux-module/__lowerCase_name__/__name__.style.ts: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | }, 7 | listView: { 8 | paddingHorizontal: 8, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /blueprint-templates/hot-redux-module/__lowerCase_name__/__name__.view.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {} from 'react-native'; 3 | // import from library 4 | import {SafeAreaView} from 'react-native-safe-area-context'; 5 | // import from alias 6 | import {TextView} from '@components'; 7 | import {withHotRedux} from '@hocs'; 8 | // localImport 9 | import {use{{name}}Model} from './{{name}}.hooks'; 10 | import { {{camelCase name}}Slice} from './{{name}}.slice'; 11 | import { {{camelCase name}}Epic} from './{{name}}.epic'; 12 | import { {{name}}Props} from './types'; 13 | import {styles} from './{{name}}.style'; 14 | 15 | const _{{name}}: React.FC< {{name}}Props> = (props) => { 16 | const {} = props; 17 | const {} = use{{name}}Model(); 18 | 19 | 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export const {{name}} = withHotRedux( 28 | {{camelCase name}}Slice.name, 29 | {{camelCase name}}Slice.reducer, 30 | {{camelCase name}}Epic, 31 | )(_{{name}}); 32 | -------------------------------------------------------------------------------- /blueprint-templates/hot-redux-module/__lowerCase_name__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "{{name}}.view.tsx" 3 | } -------------------------------------------------------------------------------- /blueprint-templates/hot-redux-module/__lowerCase_name__/types.ts: -------------------------------------------------------------------------------- 1 | import {StackNavigationProp} from '@react-navigation/stack'; 2 | import {RouteProp} from '@react-navigation/native'; 3 | 4 | import {ParamsType} from '@storyboards'; 5 | import {RootStoreState} from '@shared-state'; 6 | 7 | export type {{name}}NavigationProps = StackNavigationProp< 8 | ParamsType, 9 | '{{name}}' 10 | >; 11 | 12 | export type {{name}}RouteProp = RouteProp; 13 | 14 | export type {{name}}Props = { 15 | navigation: {{name}}NavigationProps; 16 | route: {{name}}RouteProp; 17 | }; 18 | 19 | 20 | 21 | export type {{name}}State = { 22 | 23 | }; 24 | 25 | export type StoreStateWith{{name}} = RootStoreState & { 26 | {{name}}?: {{name}}State; 27 | }; 28 | 29 | export type {{name}}ReduxSelectionState = {{name}}State & {}; 30 | -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/__name__.action.ts: -------------------------------------------------------------------------------- 1 | import { {{$name}}StoreApi} from './{{$name}}.type'; 2 | 3 | export const {{$name}}Actions = { 4 | init: () => async ({setState}: {{$name}}StoreApi) => { 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/__name__.container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {createContainer} from 'react-sweet-state'; 4 | 5 | import { {{$name}}Actions} from './{{$name}}.action'; 6 | import { 7 | {{$name}}StoreState, 8 | {{$name}}ContainerInitialState, 9 | {{$name}}Props, 10 | } from './{{$name}}.type'; 11 | 12 | import { {{$name}}Store} from './{{$name}}.store'; 13 | import { {{$name}}View} from './{{$name}}.view'; 14 | 15 | export const {{$name}}StoreContainer = createContainer< 16 | {{$name}}StoreState, 17 | typeof {{$name}}Actions, 18 | {{$name}}ContainerInitialState 19 | >({{$name}}Store, { 20 | onInit: () => ({dispatch}, {}) => { 21 | dispatch({{$name}}Actions.init()); 22 | }, 23 | }); 24 | 25 | export const {{$name}}: React.FC<{{$name}}Props> = (props) => { 26 | return ( 27 | <{{$name}}StoreContainer> 28 | <{{$name}}View {...props} /> 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/__name__.store.ts: -------------------------------------------------------------------------------- 1 | import {createStore, createHook} from 'react-sweet-state'; 2 | import { {{$name}}Actions} from './{{$name}}.action'; 3 | import { {{$name}}State, {{$name}}StoreState} from './{{$name}}.type'; 4 | import {INITIAL_STATE} from './constants'; 5 | 6 | export const {{$name}}Store = createStore<{{$name}}StoreState, typeof {{$name}}Actions>({ 7 | initialState: Object.assign({}, INITIAL_STATE), 8 | actions: {{$name}}Actions, 9 | name: '{{$name}}Store', 10 | }); 11 | 12 | export const use{{$name}} = createHook({{$name}}Store, { 13 | selector: (state): {{$name}}State => { 14 | return {}; 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/__name__.style.ts: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | export const {{$name}}Styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | }, 7 | }); -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/__name__.type.ts: -------------------------------------------------------------------------------- 1 | import {StoreActionApi} from 'react-sweet-state'; 2 | import {StackNavigationProp} from '@react-navigation/stack'; 3 | import {RouteProp} from '@react-navigation/native'; 4 | 5 | import {ParamsType} from '@storyboards'; 6 | 7 | export type {{$name}}ContainerInitialState = {}; 8 | export type {{$name}}StoreState = {}; 9 | export type {{$name}}State = {}; 10 | export type {{$name}}StoreApi = StoreActionApi<{{$name}}State>; 11 | 12 | export type {{$name}}NavigationProps = StackNavigationProp; 13 | 14 | export type {{$name}}RouteProp = RouteProp; 15 | 16 | export type {{$name}}Props = { 17 | navigation: {{$name}}NavigationProps; 18 | route: {{$name}}RouteProp; 19 | }; 20 | -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/__name__.view.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {} from 'react-native'; 3 | // import from library section 4 | 5 | // importing from alias section 6 | import {ErrorBoundary, TextView} from '@components'; 7 | // importing from local file 8 | import {use{{$name}}, {{$name}}StoreContainer} from './{{$name}}.store'; 9 | import { {{$name}}Props } from './{{$name}}.type'; 10 | import { {{$name}}Styles } from './{{$name}}.style'; 11 | 12 | export const {{$name}}View: React.FC<{{$name}}Props> = (props) => { 13 | const [state, action] = use{{$name}}(); 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/constants.ts: -------------------------------------------------------------------------------- 1 | import { {{$name}}StoreState} from './{{$name}}.type'; 2 | export const INITIAL_STATE: {{$name}}StoreState = { 3 | 4 | }; 5 | -------------------------------------------------------------------------------- /blueprint-templates/sweet-state-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "{{$name}}.container.tsx" 3 | } -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiennm16/Example/0f183bb1659bca00110256bc2af4027045904a41/demo.gif -------------------------------------------------------------------------------- /image.script.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const [folder] = process.argv.slice(2); 3 | const dir = `${__dirname}/${folder}`; 4 | 5 | function replaceAll(raw, regex, replace) { 6 | while (raw.includes(regex)) { 7 | raw = raw.replace(regex, replace); 8 | } 9 | return raw; 10 | } 11 | fs.readdir(dir, (err, files) => { 12 | if (err) { 13 | return; 14 | } 15 | const requireItems = files 16 | .filter((x) => !x.includes('@') && x.includes('.png')) 17 | .map((file) => { 18 | const name = replaceAll(file, '-', '_').replace('.png', '').toUpperCase(); 19 | return `export const ${name} = require('./${file}')`; 20 | }); 21 | const outputName = `${dir}/index.ts`; 22 | fs.writeFile(outputName, requireItems.join(';\n').concat(';\n'), (wError) => { 23 | console.log(!!wError ? wError : "Success"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'reflect-metadata'; 6 | import 'react-native-gesture-handler'; 7 | import {AppRegistry} from 'react-native'; 8 | import App from './App'; 9 | import {name as appName} from './app.json'; 10 | 11 | AppRegistry.registerComponent(appName, () => App); 12 | -------------------------------------------------------------------------------- /ios/Example-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Example-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/Example.xcodeproj/xcshareddata/xcschemes/Example-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Example/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /ios/Example/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #ifdef FB_SONARKIT_ENABLED 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | static void InitializeFlipper(UIApplication *application) { 16 | FlipperClient *client = [FlipperClient sharedClient]; 17 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 18 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 19 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 20 | [client addPlugin:[FlipperKitReactPlugin new]]; 21 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 22 | [client start]; 23 | } 24 | #endif 25 | 26 | @implementation AppDelegate 27 | 28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 29 | { 30 | #ifdef FB_SONARKIT_ENABLED 31 | InitializeFlipper(application); 32 | #endif 33 | 34 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 35 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 36 | moduleName:@"Example" 37 | initialProperties:nil]; 38 | 39 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 40 | 41 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 42 | UIViewController *rootViewController = [UIViewController new]; 43 | rootViewController.view = rootView; 44 | self.window.rootViewController = rootViewController; 45 | [self.window makeKeyAndVisible]; 46 | return YES; 47 | } 48 | 49 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 50 | { 51 | #if DEBUG 52 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 53 | #else 54 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 55 | #endif 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /ios/Example/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /ios/Example/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /ios/Example/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ios/ExampleTests/ExampleTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface ExampleTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation ExampleTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /ios/ExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '10.0' 5 | 6 | target 'Example' do 7 | config = use_native_modules! 8 | 9 | use_react_native!(:path => config["reactNativePath"]) 10 | 11 | target 'ExampleTests' do 12 | inherit! :complete 13 | # Pods for testing 14 | end 15 | 16 | # Enables Flipper. 17 | # 18 | # Note that if you have use_frameworks! enabled, Flipper will not work and 19 | # you should disable these next few lines. 20 | use_flipper! 21 | post_install do |installer| 22 | flipper_post_install(installer) 23 | end 24 | end 25 | 26 | target 'Example-tvOS' do 27 | # Pods for Example-tvOS 28 | 29 | target 'Example-tvOSTests' do 30 | inherit! :search_paths 31 | # Pods for testing 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /jest/enzyme.setup.js: -------------------------------------------------------------------------------- 1 | import {configure} from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({adapter: new Adapter()}); 5 | -------------------------------------------------------------------------------- /jest/jest.setup.js: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import './enzyme.setup'; 3 | import './ui.setup'; 4 | import './module.setup'; 5 | -------------------------------------------------------------------------------- /jest/module.setup.js: -------------------------------------------------------------------------------- 1 | jest.mock('react-native-keychain', () => ({ 2 | SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY', 3 | SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE', 4 | SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE', 5 | setGenericPassword: jest.fn().mockResolvedValue(), 6 | getGenericPassword: jest.fn().mockResolvedValue(), 7 | resetGenericPassword: jest.fn().mockResolvedValue(), 8 | })); 9 | -------------------------------------------------------------------------------- /jest/ui.setup.js: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler/jestSetup'; 2 | 3 | jest.mock('react-native-reanimated', () => { 4 | const Reanimated = require('react-native-reanimated/mock'); 5 | 6 | // The mock for `call` immediately calls the callback which is incorrect 7 | // So we override it with a no-op 8 | Reanimated.default.call = () => {}; 9 | 10 | return Reanimated; 11 | }); 12 | 13 | // Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing 14 | jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); 15 | 16 | // Mock async storage 17 | 18 | const FRAME_TIME = 10; 19 | 20 | global.requestAnimationFrame = (cb) => { 21 | setTimeout(cb, FRAME_TIME); 22 | }; 23 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example", 3 | "version": "0.0.1", 4 | "private": false, 5 | "license": "MIT", 6 | "scripts": { 7 | "android": "react-native run-android", 8 | "ios": "react-native run-ios", 9 | "start": "react-native start", 10 | "test": "jest", 11 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 12 | "cu-test": "yarn test -- --coverage --updateSnapshot" 13 | }, 14 | "dependencies": { 15 | "@react-native-community/async-storage": "^1.12.0", 16 | "@react-native-community/masked-view": "^0.1.10", 17 | "@react-navigation/native": "^5.7.3", 18 | "@react-navigation/stack": "^5.9.0", 19 | "@reduxjs/toolkit": "^1.4.0", 20 | "@types/yup": "^0.29.7", 21 | "axios": "^0.20.0", 22 | "buffer": "^5.6.0", 23 | "date-fns": "^2.16.1", 24 | "formik": "^2.1.5", 25 | "react": "16.13.1", 26 | "react-native": "0.63.2", 27 | "react-native-config": "^1.3.3", 28 | "react-native-elements": "^2.3.2", 29 | "react-native-fast-image": "^8.3.2", 30 | "react-native-gesture-handler": "^1.7.0", 31 | "react-native-keyboard-aware-scroll-view": "^0.9.2", 32 | "react-native-keychain": "^6.1.1", 33 | "react-native-linear-gradient": "^2.5.6", 34 | "react-native-material-ripple": "^0.9.1", 35 | "react-native-reanimated": "^1.13.0", 36 | "react-native-safe-area-context": "^3.1.7", 37 | "react-native-screens": "^2.10.1", 38 | "react-native-shared-element": "^0.7.0", 39 | "react-native-skeleton-placeholder": "^2.0.7", 40 | "react-native-uuid": "^1.4.9", 41 | "react-native-vector-icons": "^7.1.0", 42 | "react-navigation": "^4.4.0", 43 | "react-navigation-shared-element": "^3.0.0", 44 | "react-navigation-stack": "^2.8.2", 45 | "react-redux": "^7.2.1", 46 | "redux": "^4.0.5", 47 | "redux-observable": "^1.2.0", 48 | "reflect-metadata": "^0.1.13", 49 | "rxjs": "^6.6.2", 50 | "tsyringe": "^4.3.0", 51 | "yup": "^0.29.3" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.8.4", 55 | "@babel/plugin-proposal-decorators": "^7.10.5", 56 | "@babel/runtime": "^7.8.4", 57 | "@react-native-community/eslint-config": "^1.1.0", 58 | "@types/enzyme": "^3.10.5", 59 | "@types/jest": "^25.2.3", 60 | "@types/react-native": "^0.63.2", 61 | "@types/react-native-material-ripple": "^0.9.1", 62 | "@types/react-native-uuid": "^1.4.0", 63 | "@types/react-redux": "^7.1.9", 64 | "@types/react-test-renderer": "^16.9.2", 65 | "@typescript-eslint/eslint-plugin": "^2.27.0", 66 | "@typescript-eslint/parser": "^2.27.0", 67 | "babel-jest": "^25.1.0", 68 | "babel-plugin-module-resolver": "^4.0.0", 69 | "babel-plugin-transform-typescript-metadata": "^0.3.0", 70 | "enzyme": "^3.11.0", 71 | "enzyme-adapter-react-16": "^1.15.4", 72 | "eslint": "^6.5.1", 73 | "jest": "^25.1.0", 74 | "jest-transform-stub": "^2.0.0", 75 | "metro-react-native-babel-preset": "^0.59.0", 76 | "mockdate": "^3.0.2", 77 | "prettier": "^2.0.4", 78 | "react-dom": "^16.13.1", 79 | "react-test-renderer": "16.13.1", 80 | "typescript": "^3.8.3" 81 | }, 82 | "jest": { 83 | "preset": "react-native", 84 | "setupFiles": [ 85 | "/jest/jest.setup.js" 86 | ], 87 | "moduleNameMapper": { 88 | ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "babel-jest" 89 | }, 90 | "moduleFileExtensions": [ 91 | "ts", 92 | "tsx", 93 | "js", 94 | "jsx", 95 | "json", 96 | "node" 97 | ] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /script/asset-icon-generator.script.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const dirname = path.dirname(__dirname); 4 | const folders = process.argv.slice(2); 5 | 6 | function replaceAll(raw, regex, replace) { 7 | while (raw.includes(regex)) { 8 | raw = raw.replace(regex, replace); 9 | } 10 | return raw; 11 | } 12 | 13 | function gen(folder) { 14 | const dir = `${dirname}/${folder}`; 15 | 16 | fs.readdir(dir, (err, files) => { 17 | if (err) { 18 | return; 19 | } 20 | const requireItems = files 21 | .filter( 22 | (x) => !x.includes('@') && (x.includes('.png') || x.includes('.jpg')), 23 | ) 24 | .map((file) => { 25 | const name = replaceAll(file, '-', '_') 26 | .replace('.png', '') 27 | .replace('.jpg', '') 28 | .toUpperCase(); 29 | return `export const ${name} = require('./${file}')`; 30 | }); 31 | const outputName = `${dir}/index.ts`; 32 | fs.writeFile( 33 | outputName, 34 | requireItems.join(';\n').concat(';\n'), 35 | (wError) => { 36 | console.log(wError); 37 | }, 38 | ); 39 | }); 40 | } 41 | folders.forEach(gen); 42 | -------------------------------------------------------------------------------- /src/core/api/RxRemoteProvider.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosInstance, 3 | AxiosRequestConfig, 4 | AxiosResponse, 5 | AxiosError, 6 | } from 'axios'; 7 | import {Observable, Observer} from 'rxjs'; 8 | import {RemoteException} from '../error'; 9 | 10 | export interface RxRemoteProvider { 11 | /** 12 | * @summary perform @POST request with config 13 | * @param url 14 | * @param data 15 | * 16 | * @returns Either Axios response with generic data: T or @RemoteException if failed 17 | */ 18 | post(url: string, data: any): Observable>; 19 | 20 | /** 21 | * @summary perform @GET request with config 22 | * @param url 23 | * 24 | * @returns Either Axios response with generic data: T or @RemoteException if failed 25 | */ 26 | get(url: string): Observable>; 27 | 28 | /** 29 | * @summary perform @PUT request with config 30 | * @param url 31 | * @param data 32 | * 33 | * @returns Either Axios response with generic data: T or @RemoteException if failed 34 | */ 35 | put(url: string, data: any): Observable>; 36 | 37 | /** 38 | * @summary perform @DELETE request with config 39 | * @param url 40 | * 41 | * @returns Either Axios response with generic data: T or @RemoteException if failed 42 | */ 43 | delete(url: string): Observable>; 44 | } 45 | 46 | export class RxAxiosProviderException extends RemoteException {} 47 | 48 | export class BearerAuthorizationRxAxiosProvider 49 | implements RxRemoteProvider { 50 | private readonly axiosInstance: AxiosInstance; 51 | 52 | private token?: string; 53 | 54 | constructor(config: AxiosRequestConfig) { 55 | this.axiosInstance = axios.create(config); 56 | } 57 | 58 | request(requestConfig: AxiosRequestConfig): Observable> { 59 | return Observable.create(async (observer: Observer>) => { 60 | try { 61 | const result = await this.axiosInstance.request(requestConfig); 62 | observer.next(result); 63 | observer.complete(); 64 | } catch (error) { 65 | observer.error(new RxAxiosProviderException(error)); 66 | } 67 | }); 68 | } 69 | 70 | post(url: string, data: any): Observable> { 71 | return this.request({ 72 | method: 'POST', 73 | data, 74 | url, 75 | }); 76 | } 77 | get(url: string): Observable> { 78 | return this.request({ 79 | method: 'GET', 80 | url, 81 | }); 82 | } 83 | put(url: string, data: any): Observable> { 84 | return this.request({ 85 | method: 'PUT', 86 | data, 87 | url, 88 | }); 89 | } 90 | delete(url: string): Observable> { 91 | return this.request({ 92 | method: 'DELETE', 93 | url, 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/core/api/RxUnsplashRemoteProvider.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosInstance, 3 | AxiosRequestConfig, 4 | AxiosResponse, 5 | AxiosError, 6 | } from 'axios'; 7 | import {Observable, Observer} from 'rxjs'; 8 | import {RemoteException} from '../error'; 9 | import {RxRemoteProvider} from './RxRemoteProvider'; 10 | 11 | export class RxUnsplashProviderException extends RemoteException {} 12 | 13 | export class RxUnsplashProvider implements RxRemoteProvider { 14 | private readonly axiosInstance: AxiosInstance; 15 | 16 | constructor(private readonly token: string) { 17 | this.axiosInstance = axios.create({ 18 | baseURL: 'https://api.unsplash.com/', 19 | headers: { 20 | Authorization: `Client-ID ${token}`, 21 | }, 22 | }); 23 | } 24 | 25 | request(requestConfig: AxiosRequestConfig): Observable> { 26 | return Observable.create(async (observer: Observer>) => { 27 | try { 28 | const result = await this.axiosInstance.request(requestConfig); 29 | setTimeout(() => { 30 | observer.next(result); 31 | observer.complete(); 32 | }, 500); 33 | } catch (error) { 34 | observer.error(new RxUnsplashProviderException(error)); 35 | } 36 | }); 37 | } 38 | 39 | post(url: string, data: any): Observable> { 40 | return this.request({ 41 | method: 'POST', 42 | data, 43 | url, 44 | }); 45 | } 46 | get(url: string): Observable> { 47 | return this.request({ 48 | method: 'GET', 49 | url, 50 | }); 51 | } 52 | put(url: string, data: any): Observable> { 53 | return this.request({ 54 | method: 'PUT', 55 | data, 56 | url, 57 | }); 58 | } 59 | delete(url: string): Observable> { 60 | return this.request({ 61 | method: 'DELETE', 62 | url, 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/core/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RxRemoteProvider'; 2 | export * from './RxUnsplashRemoteProvider'; 3 | -------------------------------------------------------------------------------- /src/core/config/BuildConfig.ts: -------------------------------------------------------------------------------- 1 | import Config from 'react-native-config'; 2 | export const BuildConfig = { 3 | ApiUrl: Config.API_URL, 4 | UNSPLASH_KEY: Config.UNSPLASH_KEY, 5 | }; 6 | -------------------------------------------------------------------------------- /src/core/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BuildConfig'; 2 | -------------------------------------------------------------------------------- /src/core/error/Exception.ts: -------------------------------------------------------------------------------- 1 | export class Exception {} 2 | 3 | export class UnKnowException extends Exception {} 4 | export class RemoteException extends Exception { 5 | get rootCause(): Raw { 6 | return this.raw; 7 | } 8 | 9 | constructor(private readonly raw: Raw) { 10 | super(); 11 | } 12 | } 13 | 14 | export class ServerException extends RemoteException {} 15 | 16 | export class UnAuthorizedException extends RemoteException {} 17 | 18 | export class LocalException extends Exception { 19 | get rootCause(): Raw { 20 | return this.raw; 21 | } 22 | 23 | constructor(private readonly raw: Raw) { 24 | super(); 25 | } 26 | } 27 | 28 | export class PermissionDenied extends LocalException {} 29 | -------------------------------------------------------------------------------- /src/core/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Exception' -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './error'; 3 | export * from './use-case'; 4 | export * from './style'; 5 | export * from './config'; 6 | -------------------------------------------------------------------------------- /src/core/style/ColorScheme.ts: -------------------------------------------------------------------------------- 1 | export interface ColorScheme { 2 | primary: string; 3 | onPrimary: string; 4 | secondary: string; 5 | onSecondary: string; 6 | background: string; 7 | onBackground: string; 8 | surface: string; 9 | onSurface: string; 10 | error: string; 11 | onError: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/core/style/TextTheme.ts: -------------------------------------------------------------------------------- 1 | export interface TextTheme {} 2 | -------------------------------------------------------------------------------- /src/core/style/Theme.ts: -------------------------------------------------------------------------------- 1 | import {TextTheme} from './TextTheme'; 2 | import {ColorScheme} from './ColorScheme'; 3 | 4 | export enum ThemeConfig { 5 | Dark, 6 | Light, 7 | System, 8 | } 9 | 10 | export interface Theme { 11 | textTheme?: TextTheme; 12 | colorScheme: ColorScheme; 13 | } 14 | -------------------------------------------------------------------------------- /src/core/style/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextTheme'; 2 | export * from './Theme'; 3 | -------------------------------------------------------------------------------- /src/core/use-case/UseCase.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs'; 2 | 3 | export interface UseCase { 4 | call(param?: Params): Observable; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/use-case/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UseCase'; 2 | -------------------------------------------------------------------------------- /src/data/data-source/authentication/AuthenticationDataSource.ts: -------------------------------------------------------------------------------- 1 | import {injectable, inject} from 'tsyringe'; 2 | import {Observable} from 'rxjs'; 3 | import {map} from 'rxjs/operators'; 4 | 5 | import {RxRemoteProvider} from '@core'; 6 | import {SignInResponseData, SignInRequestData, ApiResult} from '../../model'; 7 | 8 | export interface RemoteAuthenticationDataSource { 9 | /** 10 | * @method signIn 11 | * 12 | * @description Sign in user with phone 13 | */ 14 | signIn(body: SignInRequestData): Observable>; 15 | } 16 | 17 | @injectable() 18 | export class ApiAuthenticationDataSource 19 | implements RemoteAuthenticationDataSource { 20 | constructor( 21 | @inject('ApiProvider') private readonly provider: RxRemoteProvider, 22 | ) {} 23 | signIn(body: SignInRequestData): Observable> { 24 | return this.provider 25 | .post>('/login', body) 26 | .pipe(map((response) => response.data)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/data/data-source/authentication/LocalAuthenticationDataSource.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'tsyringe'; 2 | import { Observable, Observer } from 'rxjs'; 3 | 4 | import * as Keychain from 'react-native-keychain'; 5 | import { LocalException } from '@core'; 6 | 7 | export interface LocalAuthenticationDataSource { 8 | saveToken(username: string, token: string): Observable; 9 | 10 | getToken(): Observable; 11 | } 12 | 13 | @injectable() 14 | export class KeyChainAuthenticationDataSource 15 | implements LocalAuthenticationDataSource { 16 | saveToken(username: string, token: string): Observable { 17 | return Observable.create(async (observer: Observer) => { 18 | try { 19 | let localToken = await Keychain.setGenericPassword(username, token); 20 | console.log("-----localToken", localToken); 21 | observer.next(true); 22 | observer.complete(); 23 | } catch (error) { 24 | observer.error(new LocalException(error)); 25 | } 26 | }); 27 | } 28 | getToken(): Observable { 29 | return Observable.create(async (observer: Observer) => { 30 | try { 31 | const result = await Keychain.getGenericPassword(); 32 | if (result) { 33 | observer.next(result.password); 34 | observer.complete(); 35 | return; 36 | } 37 | observer.error(new LocalException({})); 38 | } catch (error) { 39 | observer.error(new LocalException(error)); 40 | } 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/data/data-source/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthenticationDataSource'; 2 | export * from './LocalAuthenticationDataSource'; 3 | -------------------------------------------------------------------------------- /src/data/data-source/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication'; 2 | export * from './unsplash'; 3 | export * from './user'; 4 | -------------------------------------------------------------------------------- /src/data/data-source/unsplash/UnsplashLocalDataSource.ts: -------------------------------------------------------------------------------- 1 | import {injectable} from 'tsyringe'; 2 | import {Observable, Observer} from 'rxjs'; 3 | import {add} from 'date-fns'; 4 | 5 | import AsyncStorage from '@react-native-community/async-storage'; 6 | 7 | import {UnsplashPhoto} from '../../model'; 8 | 9 | export interface LocalUnsplashDataSource { 10 | /** 11 | * @method getPhotos 12 | * 13 | * @description Sign in user with phone 14 | */ 15 | getPhotos(page: number): Observable; 16 | 17 | savePhotos(photo: UnsplashPhoto[], page: number): void; 18 | } 19 | 20 | @injectable() 21 | export class AsyncStorageUnsplashDataSource implements LocalUnsplashDataSource { 22 | static KEY = 'LocalUnsplashDataSource'; 23 | savePhotos(photos: UnsplashPhoto[], page: number = 1): void { 24 | AsyncStorage.setItem( 25 | `${AsyncStorageUnsplashDataSource.KEY}/${page}`, 26 | JSON.stringify({ 27 | expired: add(new Date(), {hours: 2}).toISOString(), 28 | photos, 29 | }), 30 | ); 31 | } 32 | 33 | getPhotos(page: number = 1): Observable { 34 | return Observable.create(async (observer: Observer) => { 35 | const data = await AsyncStorage.getItem( 36 | `${AsyncStorageUnsplashDataSource.KEY}/${page}`, 37 | ); 38 | const cache = data ? JSON.parse(data) : {}; 39 | if (new Date(cache.expired) > new Date()) { 40 | observer.next(cache.photos); 41 | } else { 42 | observer.next([]); 43 | } 44 | observer.complete(); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/data/data-source/unsplash/UnsplashRemoteDataSource.ts: -------------------------------------------------------------------------------- 1 | import {injectable, inject} from 'tsyringe'; 2 | import {Observable} from 'rxjs'; 3 | import {map, delay} from 'rxjs/operators'; 4 | 5 | import {RxUnsplashProvider} from '@core'; 6 | import {UnsplashPhoto} from '../../model'; 7 | 8 | export interface RemoteUnsplashDataSource { 9 | /** 10 | * @method signIn 11 | * 12 | * @description Sign in user with phone 13 | */ 14 | getPhotos(page: number): Observable; 15 | } 16 | 17 | @injectable() 18 | export class ApiUnsplashDataSource implements RemoteUnsplashDataSource { 19 | constructor( 20 | @inject('UnsplashApiProvider') 21 | private readonly provider: RxUnsplashProvider, 22 | ) {} 23 | getPhotos(page: number = 1): Observable { 24 | return this.provider.get(`/photos?page=${page}`).pipe( 25 | delay(1000), 26 | map((x) => x.data), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/data/data-source/unsplash/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UnsplashRemoteDataSource'; 2 | export * from './UnsplashLocalDataSource'; 3 | -------------------------------------------------------------------------------- /src/data/data-source/user/LocalReqresDataSource.ts: -------------------------------------------------------------------------------- 1 | export interface LocalReqresDataSource {} 2 | -------------------------------------------------------------------------------- /src/data/data-source/user/RemoteReqresDataSource.ts: -------------------------------------------------------------------------------- 1 | import {inject, injectable} from 'tsyringe'; 2 | import {RxUnsplashProvider} from '@core'; 3 | import {Observable} from 'rxjs'; 4 | import {map} from 'rxjs/operators'; 5 | import {UnsplashPhoto, UnsplashUser} from '../../model'; 6 | 7 | export interface RemoteReqresDataSource { 8 | list(page?: number): Observable; 9 | get(username: string): Observable; 10 | } 11 | 12 | @injectable() 13 | export class ApiReqresDataSource implements RemoteReqresDataSource { 14 | constructor( 15 | @inject('UnsplashApiProvider') 16 | private readonly provider: RxUnsplashProvider, 17 | ) {} 18 | list(page: number = 1): Observable { 19 | return this.provider.get(`/photos?page=${page}`).pipe( 20 | map((x) => 21 | x.data.map( 22 | (p): UnsplashUser => ({ 23 | ...p.user, 24 | profile_image: {...p.user.profile_image, large: p.urls.regular}, 25 | }), 26 | ), 27 | ), 28 | ); 29 | } 30 | get(username: string): Observable { 31 | return this.provider 32 | .get(`/users/${username}`) 33 | .pipe(map((x) => x.data)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/data/data-source/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RemoteReqresDataSource'; 2 | export * from './LocalReqresDataSource'; 3 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data-source'; 2 | export * from './model'; 3 | export * from './repository'; 4 | -------------------------------------------------------------------------------- /src/data/model/ApiResult.ts: -------------------------------------------------------------------------------- 1 | import {AxiosResponse} from 'axios'; 2 | 3 | export interface ApiResult { 4 | data: DataT; 5 | message?: string; 6 | } 7 | 8 | export type ApiProviderResult = AxiosResponse>; 9 | -------------------------------------------------------------------------------- /src/data/model/AuthenticationModels.ts: -------------------------------------------------------------------------------- 1 | import {UserModel} from './UserModel'; 2 | 3 | export interface SignInRequestData {} 4 | 5 | export interface SignInResponseData { 6 | token: string; 7 | user: UserModel; 8 | } 9 | export interface SignUpResponseData {} 10 | -------------------------------------------------------------------------------- /src/data/model/UserModel.ts: -------------------------------------------------------------------------------- 1 | export interface UserModel {} 2 | -------------------------------------------------------------------------------- /src/data/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthenticationModels'; 2 | export * from './UserModel'; 3 | export * from './ApiResult'; 4 | export * from './unsplash.model'; 5 | export * from './reqres.model'; 6 | -------------------------------------------------------------------------------- /src/data/model/reqres.model.ts: -------------------------------------------------------------------------------- 1 | export interface ReqresUser { 2 | id: string; 3 | email: string; 4 | avatar: string; 5 | } 6 | 7 | export type ReqresGetUserResult = { 8 | data: ReqresUser; 9 | }; 10 | 11 | export type ReqresUserPaginationResult = { 12 | page: number; 13 | data: ReqresUser[]; 14 | }; 15 | -------------------------------------------------------------------------------- /src/data/model/unsplash.model.ts: -------------------------------------------------------------------------------- 1 | export interface UnsplashPhoto { 2 | id: string; 3 | description: string; 4 | urls: { 5 | raw: string; 6 | full: string; 7 | regular: string; 8 | small: string; 9 | thumb: string; 10 | }; 11 | user: UnsplashUser; 12 | } 13 | 14 | export interface UnsplashUser { 15 | id: string; 16 | name: string; 17 | username: string; 18 | profile_image: { 19 | small: string; 20 | medium: string; 21 | large: string; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/data/repository/AuthenticationRepository.ts: -------------------------------------------------------------------------------- 1 | import {inject, injectable} from 'tsyringe'; 2 | import {Observable} from 'rxjs'; 3 | import {map} from 'rxjs/operators'; 4 | 5 | import {AuthenticationRepository, SignInResult} from '@domain'; 6 | 7 | import { 8 | LocalAuthenticationDataSource, 9 | RemoteAuthenticationDataSource, 10 | } from '../data-source'; 11 | 12 | @injectable() 13 | export class CombineAuthenticationRepository 14 | implements AuthenticationRepository { 15 | constructor( 16 | @inject('LocalAuthenticationDataSource') 17 | private readonly localDataSource: LocalAuthenticationDataSource, 18 | @inject('RemoteAuthenticationDataSource') 19 | private readonly remoteDataSource: RemoteAuthenticationDataSource, 20 | ) {} 21 | 22 | signIn(credential?: any): Observable { 23 | return this.remoteDataSource.signIn(credential).pipe( 24 | map( 25 | (result: any): SignInResult => { 26 | return {fromLocal: false, token: result.token}; // change data for free API 27 | }, 28 | ), 29 | ); 30 | } 31 | getToken(): Observable { 32 | return this.localDataSource.getToken(); 33 | } 34 | saveToken(key: string, token: string): Observable { 35 | return this.localDataSource.saveToken(key, token); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/data/repository/ReqresUserRepository.ts: -------------------------------------------------------------------------------- 1 | import {inject, injectable} from 'tsyringe'; 2 | import {Observable} from 'rxjs'; 3 | import {RemoteReqresDataSource} from '../data-source'; 4 | import {UnsplashUser} from '../model'; 5 | 6 | @injectable() 7 | export class ReqresRepository { 8 | constructor( 9 | @inject('RemoteReqresDataSource') 10 | private readonly dataSource: RemoteReqresDataSource, 11 | ) {} 12 | 13 | getUser(username: string): Observable { 14 | return this.dataSource.get(username); 15 | } 16 | 17 | listUsers(page: number = 1): Observable { 18 | return this.dataSource.list(page); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/data/repository/UnsplashRepository.ts: -------------------------------------------------------------------------------- 1 | import {inject, injectable} from 'tsyringe'; 2 | import {Observable, merge} from 'rxjs'; 3 | import {UnsplashPhoto} from '../model'; 4 | import { 5 | RemoteUnsplashDataSource, 6 | LocalUnsplashDataSource, 7 | } from '../data-source'; 8 | import {tap} from 'rxjs/operators'; 9 | 10 | @injectable() 11 | export class UnsplashRepository { 12 | maximumCachedRequest: number = 10; 13 | constructor( 14 | @inject('RemoteUnsplashDataSource') 15 | private readonly dataSource: RemoteUnsplashDataSource, 16 | @inject('LocalUnsplashDataSource') 17 | private readonly localDataSource: LocalUnsplashDataSource, 18 | ) {} 19 | 20 | getPhotos(page: number = 1): Observable { 21 | if (page >= this.maximumCachedRequest) { 22 | return this.dataSource.getPhotos(page); 23 | } 24 | return merge( 25 | this.localDataSource.getPhotos(page), 26 | this.dataSource.getPhotos(page).pipe( 27 | tap((x) => { 28 | page < this.maximumCachedRequest && 29 | this.localDataSource.savePhotos(x, page); 30 | }), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/data/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthenticationRepository'; 2 | export * from './UnsplashRepository'; 3 | export * from './ReqresUserRepository'; 4 | -------------------------------------------------------------------------------- /src/di/AppModule.ts: -------------------------------------------------------------------------------- 1 | import {registerDatDependencies} from './DataModule'; 2 | import {container} from 'tsyringe'; 3 | import {StoreContainer, configureStore} from '@shared-state'; 4 | import {registerRepositoryDependencies} from './RepositoryModule'; 5 | 6 | function registerDependencies() { 7 | registerDatDependencies(); 8 | registerRepositoryDependencies(); 9 | } 10 | 11 | function registerFlyValue() { 12 | container.register('StoreContainer', { 13 | useValue: configureStore(), 14 | }); 15 | } 16 | 17 | export {registerDependencies, registerFlyValue, container}; 18 | -------------------------------------------------------------------------------- /src/di/DataModule.ts: -------------------------------------------------------------------------------- 1 | import {container} from 'tsyringe'; 2 | import { 3 | KeyChainAuthenticationDataSource, 4 | ApiAuthenticationDataSource, 5 | ApiUnsplashDataSource, 6 | AsyncStorageUnsplashDataSource, 7 | ApiReqresDataSource, 8 | } from '@data'; 9 | import {SignInUseCase} from '@domain'; 10 | import { 11 | BearerAuthorizationRxAxiosProvider, 12 | BuildConfig, 13 | RxUnsplashProvider, 14 | } from '@core'; 15 | 16 | export function registerDatDependencies() { 17 | container.register('ApiProvider', { 18 | useValue: new BearerAuthorizationRxAxiosProvider({ 19 | baseURL: BuildConfig.ApiUrl, 20 | }), 21 | }); 22 | container.register('UnsplashApiProvider', { 23 | useValue: new RxUnsplashProvider(BuildConfig.UNSPLASH_KEY), 24 | }); 25 | container.register('LocalAuthenticationDataSource', { 26 | useClass: KeyChainAuthenticationDataSource, 27 | }); 28 | 29 | container.register('RemoteAuthenticationDataSource', { 30 | useClass: ApiAuthenticationDataSource, 31 | }); 32 | 33 | container.register('RemoteUnsplashDataSource', { 34 | useClass: ApiUnsplashDataSource, 35 | }); 36 | 37 | container.register('LocalUnsplashDataSource', { 38 | useClass: AsyncStorageUnsplashDataSource, 39 | }); 40 | 41 | container.register('RemoteReqresDataSource', { 42 | useClass: ApiReqresDataSource, 43 | }); 44 | 45 | container.register('SignInUseCase', { 46 | useClass: SignInUseCase, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/di/RepositoryModule.ts: -------------------------------------------------------------------------------- 1 | import {container} from 'tsyringe'; 2 | import { 3 | CombineAuthenticationRepository, 4 | ReqresRepository, 5 | UnsplashRepository, 6 | } from '@data'; 7 | 8 | export function registerRepositoryDependencies() { 9 | container.register('AuthenticationRepository', { 10 | useClass: CombineAuthenticationRepository, 11 | }); 12 | 13 | container.register('UnsplashRepository', { 14 | useClass: UnsplashRepository, 15 | }); 16 | 17 | container.register('ReqresRepository', { 18 | useClass: ReqresRepository, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/di/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppModule'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/di/type.ts: -------------------------------------------------------------------------------- 1 | export enum AppDependencies { 2 | StoreContainer = 'StoreContainer', 3 | ApiProvider = 'ApiProvider', 4 | LocalAuthenticationDataSource = 'LocalAuthenticationDataSource', 5 | RemoteAuthenticationDataSource = 'RemoteAuthenticationDataSource', 6 | AuthenticationRepository = 'AuthenticationRepository', 7 | SignInUseCase = 'SignInUseCase', 8 | } 9 | -------------------------------------------------------------------------------- /src/domain/entity/User.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/domain/entity/authentication.ts: -------------------------------------------------------------------------------- 1 | export interface SignInResult { 2 | token: string; 3 | fromLocal: boolean; 4 | } 5 | 6 | export interface Credential { 7 | email: string; 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication'; 2 | export * from './User'; 3 | -------------------------------------------------------------------------------- /src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entity'; 2 | export * from './repository'; 3 | export * from './use-case'; 4 | -------------------------------------------------------------------------------- /src/domain/repository/AuthenticationRepository.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rxjs'; 2 | import {SignInResult} from '../entity'; 3 | 4 | export interface AuthenticationRepository { 5 | /** 6 | * @summary sign the @credential with remote api 7 | * @param credential 8 | * @return signed token of credential 9 | */ 10 | signIn(credential: any): Observable; 11 | 12 | /** 13 | * @summary get token by key 14 | * @param key 15 | * @returns token's saved before 16 | */ 17 | getToken(): Observable; 18 | 19 | /** 20 | * @summary save the token with key 21 | * @param key identify with other token 22 | * @param token 23 | * @returns boolean variable to indicate that success or failure 24 | */ 25 | saveToken(key: string, token: string): Observable; 26 | } 27 | -------------------------------------------------------------------------------- /src/domain/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthenticationRepository'; 2 | -------------------------------------------------------------------------------- /src/domain/use-case/authentication/SignIn.use-case.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | import { Observable } from 'rxjs'; 3 | import { map, mergeMap, mapTo, catchError } from 'rxjs/operators'; 4 | 5 | import { UseCase } from '@core'; 6 | import { AuthenticationRepository } from '../../repository'; 7 | import { SignInResult } from '../../entity'; 8 | 9 | @injectable() 10 | export class SignInUseCase implements UseCase { 11 | constructor( 12 | @inject('AuthenticationRepository') 13 | private readonly authenticationRepository: AuthenticationRepository, 14 | ) { } 15 | 16 | call(param?: any): Observable { 17 | if (typeof param === 'undefined') { 18 | return this.localSignIn(); 19 | } 20 | return this.remoteSignIn(param); 21 | } 22 | 23 | private localSignIn(): Observable { 24 | console.log("------localSignIn"); 25 | return this.authenticationRepository.getToken().pipe( 26 | map( 27 | (token): SignInResult => { 28 | return { fromLocal: true, token }; 29 | }, 30 | ), 31 | ); 32 | } 33 | 34 | private remoteSignIn(param?: any): Observable { 35 | console.log("------remoteSignIn", param); 36 | return this.authenticationRepository 37 | .signIn(param) 38 | .pipe(mergeMap((result: any) => { 39 | console.log("-----ressult", result); 40 | 41 | return this.onRemoteSignInSuccess(result) 42 | }), catchError((err: any) => { 43 | console.log("-----er", err); 44 | return Observable.throw(err); 45 | })); 46 | } 47 | 48 | onRemoteSignInSuccess(result: SignInResult): Observable { 49 | return this.authenticationRepository 50 | .saveToken('ExampleToken', result.token) 51 | .pipe(mapTo(result)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/domain/use-case/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SignIn.use-case'; 2 | -------------------------------------------------------------------------------- /src/domain/use-case/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication'; 2 | -------------------------------------------------------------------------------- /src/presentation/component/boundary/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text} from 'react-native'; 3 | 4 | export interface ErrorBoundaryProps {} 5 | 6 | export interface ErrorBoundaryState { 7 | hasError: boolean; 8 | } 9 | 10 | export class ErrorBoundary extends React.PureComponent< 11 | ErrorBoundaryProps, 12 | ErrorBoundaryState 13 | > { 14 | constructor(props: ErrorBoundaryProps) { 15 | super(props); 16 | this.state = {hasError: false}; 17 | } 18 | componentDidCatch() { 19 | this.setState({hasError: true}); 20 | } 21 | render() { 22 | const {hasError} = this.state; 23 | if (hasError) { 24 | return Error Fallback; 25 | } 26 | return this.props.children; 27 | } 28 | } 29 | 30 | const styles = StyleSheet.create({ 31 | container: {}, 32 | }); 33 | -------------------------------------------------------------------------------- /src/presentation/component/boundary/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ErrorBoundary'; 2 | -------------------------------------------------------------------------------- /src/presentation/component/button/FlatButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, TextProps, StyleProp, ViewStyle, TextStyle } from 'react-native'; 3 | 4 | import Ripple from 'react-native-material-ripple'; 5 | 6 | export interface FlatButtonProps extends TextProps { 7 | containerStyle?: StyleProp; 8 | titleStyle?: StyleProp; 9 | title?: string; 10 | onPress?: () => void; 11 | } 12 | 13 | const _FlatButton: React.FC = (props) => { 14 | const { title, onPress, titleStyle } = props; 15 | return ( 16 | 19 | {title} 20 | 21 | ); 22 | }; 23 | 24 | const _styles = StyleSheet.create({ 25 | container: { 26 | flexDirection: 'row', 27 | height: 44, 28 | borderRadius: 22, 29 | justifyContent: 'center', 30 | alignItems: 'center', 31 | }, 32 | title: { 33 | fontWeight: '600', 34 | }, 35 | }); 36 | 37 | export const FlatButton = React.memo(_FlatButton); 38 | -------------------------------------------------------------------------------- /src/presentation/component/button/RoundedButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, TextProps, StyleProp, ViewStyle} from 'react-native'; 3 | 4 | import LinearGradient from 'react-native-linear-gradient'; 5 | import Ripple from 'react-native-material-ripple'; 6 | 7 | export interface RoundedButtonProps extends TextProps { 8 | containerStyle?: StyleProp; 9 | title?: string; 10 | onPress?: () => void; 11 | } 12 | 13 | const _RoundedButton: React.FC = (props) => { 14 | const {title, onPress} = props; 15 | return ( 16 | 19 | 20 | {title} 21 | 22 | 23 | ); 24 | }; 25 | 26 | const _styles = StyleSheet.create({ 27 | container: { 28 | height: 44, 29 | }, 30 | linear: { 31 | flex: 1, 32 | flexDirection: 'row', 33 | justifyContent: 'center', 34 | alignItems: 'center', 35 | borderRadius: 8, 36 | }, 37 | title: { 38 | color: 'white', 39 | fontWeight: '600', 40 | }, 41 | }); 42 | 43 | export const RoundedButton = React.memo(_RoundedButton); 44 | -------------------------------------------------------------------------------- /src/presentation/component/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RoundedButton'; 2 | export * from './FlatButton'; 3 | -------------------------------------------------------------------------------- /src/presentation/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input'; 2 | export * from './label'; 3 | export * from './button'; 4 | export * from './indicator'; 5 | export * from './boundary'; 6 | export * from './listing'; 7 | export * from './primaryBg'; 8 | -------------------------------------------------------------------------------- /src/presentation/component/indicator/FullScreenLoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Modal, ActivityIndicator, View, StyleSheet} from 'react-native'; 3 | 4 | export interface FullScreenLoadingIndicatorProps { 5 | visible: boolean; 6 | } 7 | 8 | const _FullScreenLoadingIndicator: React.FC = ( 9 | props, 10 | ) => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | const _styles = StyleSheet.create({ 23 | container: { 24 | flex: 1, 25 | backgroundColor: 'rgba(51,51,51,0.3)', 26 | justifyContent: 'center', 27 | alignItems: 'center', 28 | }, 29 | box: { 30 | width: 80, 31 | height: 80, 32 | borderRadius: 8, 33 | backgroundColor: 'rgb(39,43,50)', 34 | justifyContent: 'center', 35 | alignItems: 'center', 36 | }, 37 | }); 38 | 39 | export const FullScreenLoadingIndicator = React.memo( 40 | _FullScreenLoadingIndicator, 41 | ); 42 | -------------------------------------------------------------------------------- /src/presentation/component/indicator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FullScreenLoadingIndicator'; 2 | -------------------------------------------------------------------------------- /src/presentation/component/input/TextField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | Image, 5 | ImageSourcePropType, 6 | TextInputProps, 7 | StyleSheet, 8 | StyleProp, 9 | ViewStyle, 10 | } from 'react-native'; 11 | import { TextInput } from 'react-native-gesture-handler'; 12 | import { TextView } from '../label'; 13 | 14 | export interface TextFieldProps { 15 | containerStyle?: StyleProp; 16 | errorTextStyle?: StyleProp; 17 | 18 | prefix?: React.ReactNode; 19 | prefixIcon?: ImageSourcePropType; 20 | 21 | suffix?: React.ReactNode; 22 | suffixIcon?: ImageSourcePropType; 23 | 24 | errorLabel?: string; 25 | 26 | inputProps?: TextInputProps; 27 | } 28 | 29 | export const TextField: React.FC = (props) => { 30 | const { 31 | containerStyle, 32 | errorTextStyle, 33 | prefix, 34 | prefixIcon, 35 | suffix, 36 | suffixIcon, 37 | errorLabel, 38 | inputProps = {}, 39 | } = props; 40 | 41 | const renderPrefix = () => { 42 | if (prefix) { 43 | return prefix; 44 | } 45 | if (prefixIcon) { 46 | return ; 47 | } 48 | return null; 49 | }; 50 | 51 | const renderSuffix = () => { 52 | if (suffix) { 53 | return suffix; 54 | } 55 | if (suffixIcon) { 56 | return ; 57 | } 58 | return null; 59 | }; 60 | 61 | return ( 62 | 63 | 64 | {renderPrefix()} 65 | 66 | 67 | {renderSuffix()} 68 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | const _styles = StyleSheet.create({ 76 | container: {}, 77 | divider: { 78 | height: 1, 79 | // marginTop: 8, 80 | width: '100%', 81 | backgroundColor: '#F1F3F8', 82 | }, 83 | content: { 84 | alignItems: 'center', 85 | flexDirection: 'row', 86 | }, 87 | input: { 88 | flex: 1, 89 | }, 90 | error: { 91 | fontSize: 10, 92 | marginTop: 4, 93 | marginBottom: 4, 94 | color: "#F1F3F8" 95 | }, 96 | padding: { width: 16 }, 97 | }); 98 | -------------------------------------------------------------------------------- /src/presentation/component/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextField'; 2 | -------------------------------------------------------------------------------- /src/presentation/component/label/IconLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StyleSheet, 4 | Text, 5 | TextProps, 6 | View, 7 | ImageSourcePropType, 8 | Image, 9 | StyleProp, 10 | ViewStyle, 11 | TextStyle, 12 | } from 'react-native'; 13 | 14 | export interface IconLabelProps extends TextProps { 15 | prefix?: React.ReactNode; 16 | prefixIcon?: ImageSourcePropType; 17 | text?: string; 18 | 19 | containerStyle?: StyleProp; 20 | labelStyle?: StyleProp; 21 | } 22 | 23 | const _IconLabel: React.FC = (props) => { 24 | const {prefixIcon, text, containerStyle, labelStyle, prefix} = props; 25 | const renderPrefix = () => { 26 | if (prefix) { 27 | return prefix; 28 | } 29 | if (prefixIcon) { 30 | return ; 31 | } 32 | return null; 33 | }; 34 | return ( 35 | 36 | {renderPrefix()} 37 | {text} 38 | 39 | ); 40 | }; 41 | 42 | const _styles = StyleSheet.create({ 43 | container: { 44 | flexDirection: 'row', 45 | }, 46 | label: { 47 | marginHorizontal: 8, 48 | }, 49 | }); 50 | 51 | export const IconLabel = React.memo(_IconLabel); 52 | -------------------------------------------------------------------------------- /src/presentation/component/label/TextView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, TextProps} from 'react-native'; 3 | 4 | export interface TextViewProps extends TextProps { 5 | text?: string; 6 | } 7 | 8 | const _TextView: React.FC = (props) => { 9 | return ( 10 | 13 | {props.text} 14 | 15 | ); 16 | }; 17 | 18 | export const TextView = React.memo(_TextView); 19 | -------------------------------------------------------------------------------- /src/presentation/component/label/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextView'; 2 | export * from './IconLabel'; 3 | -------------------------------------------------------------------------------- /src/presentation/component/listing/EmptyListView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, StyleSheet} from 'react-native'; 3 | import {TextView} from '../label'; 4 | import {LightTheme} from '@resources'; 5 | 6 | export type EmptyListViewProps = { 7 | title?: string; 8 | content?: string; 9 | }; 10 | 11 | export const EmptyListView: React.FC = (props) => { 12 | return ( 13 | 14 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | const styles = StyleSheet.create({ 24 | container: { 25 | flex: 1, 26 | justifyContent: 'center', 27 | alignItems: 'center', 28 | }, 29 | title: { 30 | color: LightTheme.colorScheme.primary, 31 | }, 32 | content: { 33 | fontSize: 8, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/presentation/component/listing/ListView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, StyleSheet, FlatListProps, RefreshControl} from 'react-native'; 3 | import {FlatList} from 'react-native-gesture-handler'; 4 | import {EmptyListView, EmptyListViewProps} from './EmptyListView'; 5 | import {SkeletonLoadingItem} from './SkeletonLoadingItem'; 6 | 7 | export interface ListViewProps extends FlatListProps { 8 | refreshing?: boolean; 9 | onRefresh?: () => void; 10 | emptyListViewProps?: EmptyListViewProps; 11 | isLoadingMore?: boolean; 12 | onLoadMore?: () => void; 13 | LoadingComponent?: React.ComponentType | React.ReactElement | null; 14 | } 15 | 16 | export type ListViewFC = React.FC>; 17 | 18 | export const ListView: ListViewFC = (props) => { 19 | const { 20 | refreshing, 21 | ListFooterComponent, 22 | data, 23 | isLoadingMore, 24 | onLoadMore, 25 | LoadingComponent, 26 | } = props; 27 | 28 | const refreshIndicatorVisible = 29 | refreshing === true && (data?.length ?? 0) > 0; 30 | 31 | const skeletonDisplayable = 32 | (refreshing && data?.length === 0) || isLoadingMore; 33 | 34 | const onEndReached = React.useCallback(() => { 35 | if (!onLoadMore || isLoadingMore) { 36 | return; 37 | } 38 | onLoadMore(); 39 | }, [isLoadingMore, onLoadMore]); 40 | 41 | const emptyItem = () => { 42 | if (refreshing) { 43 | return null; 44 | } 45 | return ; 46 | }; 47 | 48 | const footer = () => { 49 | if (skeletonDisplayable) { 50 | if (LoadingComponent) { 51 | return LoadingComponent; 52 | } 53 | return ( 54 | <> 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | return ListFooterComponent; 62 | }; 63 | 64 | return ( 65 | 66 | 75 | } 76 | style={styles.list} 77 | onEndReached={onEndReached} 78 | /> 79 | 80 | ); 81 | }; 82 | 83 | const styles = StyleSheet.create({ 84 | container: { 85 | flex: 1, 86 | }, 87 | list: { 88 | flex: 1, 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /src/presentation/component/listing/SectionListView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | SectionListProps, 6 | RefreshControl, 7 | SectionList, 8 | } from 'react-native'; 9 | import {EmptyListView, EmptyListViewProps} from './EmptyListView'; 10 | import {SkeletonLoadingItem} from './SkeletonLoadingItem'; 11 | 12 | export interface SectionListViewProps extends SectionListProps { 13 | refreshing?: boolean; 14 | onRefresh?: () => void; 15 | emptyListViewProps?: EmptyListViewProps; 16 | isLoadingMore?: boolean; 17 | onLoadMore?: () => void; 18 | LoadingComponent?: React.ComponentType | React.ReactElement | null; 19 | } 20 | 21 | export const SectionListView = ( 22 | props: SectionListViewProps, 23 | ) => { 24 | const { 25 | refreshing, 26 | ListFooterComponent, 27 | sections, 28 | isLoadingMore, 29 | onLoadMore, 30 | LoadingComponent, 31 | } = props; 32 | 33 | const refreshIndicatorVisible = 34 | refreshing === true && (sections?.length ?? 0) > 0; 35 | 36 | const skeletonDisplayable = 37 | (refreshing && sections?.length === 0) || isLoadingMore; 38 | const onEndReached = React.useCallback(() => { 39 | if (!onLoadMore || isLoadingMore) { 40 | return; 41 | } 42 | onLoadMore(); 43 | }, [isLoadingMore, onLoadMore]); 44 | 45 | const emptyItem = () => { 46 | if (refreshing) { 47 | return null; 48 | } 49 | return ; 50 | }; 51 | 52 | const footer = () => { 53 | if (skeletonDisplayable) { 54 | if (LoadingComponent) { 55 | return LoadingComponent; 56 | } 57 | return ( 58 | <> 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | return ListFooterComponent; 66 | }; 67 | 68 | return ( 69 | 70 | 79 | } 80 | style={styles.list} 81 | onEndReached={onEndReached} 82 | /> 83 | 84 | ); 85 | }; 86 | 87 | const styles = StyleSheet.create({ 88 | container: { 89 | flex: 1, 90 | }, 91 | list: { 92 | flex: 1, 93 | }, 94 | }); 95 | -------------------------------------------------------------------------------- /src/presentation/component/listing/SkeletonLoadingItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; 4 | export interface SkeletonLoadingItemProps {} 5 | export const SkeletonLoadingItem: React.FC = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/presentation/component/listing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ListView'; 2 | export * from './EmptyListView'; 3 | export * from './SkeletonLoadingItem'; 4 | export * from './SectionListView'; 5 | -------------------------------------------------------------------------------- /src/presentation/component/primaryBg/PrimaryBackground.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, StatusBar, ImageBackground} from 'react-native'; 3 | import {BACKGROUND} from '@assets'; 4 | 5 | export interface PrimaryBackgroundProps {} 6 | 7 | const _PrimaryBackground: React.FC = (props) => { 8 | const {children} = props; 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | flex: 1, 20 | }, 21 | }); 22 | 23 | export const PrimaryBackground = React.memo(_PrimaryBackground); 24 | -------------------------------------------------------------------------------- /src/presentation/component/primaryBg/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PrimaryBackground'; 2 | -------------------------------------------------------------------------------- /src/presentation/container/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign-in'; 2 | // export * from './sign-up'; 3 | -------------------------------------------------------------------------------- /src/presentation/container/authentication/sign-in/SignIn.hooks.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { container } from 'tsyringe'; 4 | import { filter, catchError } from 'rxjs/operators'; 5 | import { useSelector, useDispatch } from 'react-redux'; 6 | 7 | import { signIn, StoreContainer, signInFailed, signInSuccess } from '@shared-state'; 8 | 9 | import { signInSelector } from './SignIn.redux-selector'; 10 | import { SignInHandle } from './types'; 11 | import { Alert } from 'react-native'; 12 | import { Observable } from 'rxjs'; 13 | 14 | export function useSignIn(handle: SignInHandle) { 15 | const { onSignInFailed } = handle; 16 | const { isAuthenticating } = useSelector(signInSelector); 17 | const dispatch = useDispatch(); 18 | const submit = (props: { email: string, password: string }) => dispatch(signIn({ 19 | "email": props.email, 20 | "password": props.password 21 | })); 22 | const { action$ } = container.resolve('StoreContainer'); 23 | 24 | React.useEffect(() => { 25 | const subscription = action$ 26 | .pipe(filter(signInFailed.match)) 27 | .subscribe(() => { 28 | console.log("------- false"); 29 | onSignInFailed(); 30 | }); 31 | return () => { 32 | if (subscription.closed) { 33 | return; 34 | } 35 | subscription.unsubscribe(); 36 | }; 37 | }, [action$, onSignInFailed]); 38 | return { isAuthenticating, submit }; 39 | } 40 | 41 | export function socialAction() { 42 | const loginSocial = () => { 43 | Alert.alert('This feature is on development!'); 44 | }; 45 | return { loginSocial }; 46 | } 47 | -------------------------------------------------------------------------------- /src/presentation/container/authentication/sign-in/SignIn.redux-selector.ts: -------------------------------------------------------------------------------- 1 | import {Selector} from 'react-redux'; 2 | import {RootStoreState} from '@shared-state'; 3 | import {SignInReduxSelectionState} from './types'; 4 | 5 | export const signInSelector: Selector< 6 | RootStoreState, 7 | SignInReduxSelectionState 8 | > = (state) => { 9 | return { 10 | isAuthenticating: state.authentication.isAuthenticating, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/presentation/container/authentication/sign-in/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "SignIn.view.tsx" 3 | } -------------------------------------------------------------------------------- /src/presentation/container/authentication/sign-in/types.ts: -------------------------------------------------------------------------------- 1 | import {StackNavigationProp} from '@react-navigation/stack'; 2 | import {RouteProp} from '@react-navigation/native'; 3 | 4 | import {AuthenticationStoryboardParamList} from '@storyboards'; 5 | 6 | export type SignInNavigationProps = StackNavigationProp< 7 | AuthenticationStoryboardParamList, 8 | 'SignIn' 9 | >; 10 | 11 | export type SignInRouteProp = RouteProp< 12 | AuthenticationStoryboardParamList, 13 | 'SignIn' 14 | >; 15 | 16 | export type SignInProps = { 17 | navigation: SignInNavigationProps; 18 | route: SignInRouteProp; 19 | }; 20 | 21 | export type SignInHandle = { 22 | onSignInFailed: () => void; 23 | }; 24 | 25 | export type SignInReduxSelectionState = { 26 | isAuthenticating: boolean; 27 | }; 28 | 29 | export type SingInState = { 30 | isAuthenticating: boolean; 31 | signIn: () => void; 32 | }; 33 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/Home.hooks.ts: -------------------------------------------------------------------------------- 1 | import {signOut} from '@shared-state'; 2 | import React from 'react'; 3 | import {} from 'react-native'; 4 | 5 | import {useDispatch, useSelector, Selector} from 'react-redux'; 6 | import {homeSlice, INITIAL_STATE} from './home.slice'; 7 | import {HomeReduxSelectionState, StoreStateWithHome} from './types'; 8 | 9 | export const homeSelector: Selector< 10 | StoreStateWithHome, 11 | HomeReduxSelectionState 12 | > = ({home = INITIAL_STATE}) => home; 13 | 14 | const { 15 | actions: {refresh, loadMore}, 16 | } = homeSlice; 17 | 18 | export function useHomeModel() { 19 | const {data, refreshing, loadingMore} = useSelector< 20 | StoreStateWithHome, 21 | HomeReduxSelectionState 22 | >(homeSelector); 23 | const dispatch = useDispatch(); 24 | 25 | const doSignOut = React.useCallback(() => { 26 | dispatch(signOut()); 27 | }, [dispatch]); 28 | 29 | const doRefresh = React.useCallback(() => { 30 | dispatch(refresh()); 31 | }, [dispatch]); 32 | 33 | const doLoadMore = React.useCallback(() => { 34 | dispatch(loadMore()); 35 | }, [dispatch]); 36 | 37 | React.useEffect(() => { 38 | doRefresh(); 39 | }, [doRefresh]); 40 | 41 | return {data, refreshing, loadingMore, doLoadMore, doRefresh, doSignOut}; 42 | } 43 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/Home.item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View, Dimensions, Pressable} from 'react-native'; 3 | 4 | import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; 5 | import {SharedElement} from 'react-navigation-shared-element'; 6 | import Image from 'react-native-fast-image'; 7 | 8 | import {UnsplashPhoto} from '@data'; 9 | 10 | export type UnSplashItemProps = { 11 | item: UnsplashPhoto; 12 | onPress?: (item: UnsplashPhoto) => void; 13 | }; 14 | const _UnSplashItem: React.FC = (props) => { 15 | const {item, onPress} = props; 16 | 17 | const onItemPress = React.useCallback(() => { 18 | onPress && onPress(item); 19 | }, [item, onPress]); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | {/* */} 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export const UnsplashLoadingItem: React.FC = () => { 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | 53 | 60 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | const styles = StyleSheet.create({ 67 | container: { 68 | height: Dimensions.get('window').width * 1.2, 69 | marginBottom: 10, 70 | }, 71 | loadingContainer: { 72 | height: Dimensions.get('window').width * 0.5, 73 | marginBottom: 10, 74 | borderWidth: 2, 75 | borderColor: '#fff', 76 | borderRadius: 12, 77 | justifyContent: 'flex-end', 78 | padding: 16, 79 | }, 80 | image: { 81 | ...StyleSheet.absoluteFillObject, 82 | borderRadius: 12, 83 | }, 84 | overlay: { 85 | flex: 1, 86 | justifyContent: 'flex-end', 87 | padding: 16, 88 | }, 89 | avatar: { 90 | width: 60, 91 | height: 60, 92 | borderRadius: 30, 93 | borderWidth: 3, 94 | borderColor: '#fff', 95 | }, 96 | row: { 97 | flexDirection: 'row', 98 | alignItems: 'center', 99 | }, 100 | }); 101 | 102 | export const UnSplashItem = React.memo(_UnSplashItem); 103 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/Home.style.ts: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | }, 7 | listView: { 8 | marginTop: 16, 9 | paddingHorizontal: 8, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/Home.view.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {SectionListRenderItemInfo} from 'react-native'; 3 | 4 | import {Header} from 'react-native-elements'; 5 | import {SafeAreaView} from 'react-native-safe-area-context'; 6 | 7 | import {FlatButton, SectionListView} from '@components'; 8 | import {withHotRedux} from '@hocs'; 9 | import {UnsplashPhoto} from '@data'; 10 | 11 | import {useHomeModel} from './Home.hooks'; 12 | import {UnSplashItem, UnsplashLoadingItem} from './Home.item'; 13 | import {homeSlice} from './home.slice'; 14 | import {homeEpic} from './home.epic'; 15 | import {HomeProps} from './types'; 16 | import {styles} from './Home.style'; 17 | import {useTheme} from '@hooks'; 18 | 19 | const _Home: React.FC = (props) => { 20 | const {navigation} = props; 21 | const {colorScheme} = useTheme(); 22 | const { 23 | data, 24 | refreshing, 25 | loadingMore, 26 | doLoadMore, 27 | doRefresh, 28 | doSignOut, 29 | } = useHomeModel(); 30 | const navigateToProfile = React.useCallback( 31 | (item: UnsplashPhoto) => { 32 | navigation.navigate('Profile', {id: item.user.username}); 33 | }, 34 | [navigation], 35 | ); 36 | 37 | const renderItem = React.useCallback( 38 | ({item}: SectionListRenderItemInfo) => { 39 | return ; 40 | }, 41 | [navigateToProfile], 42 | ); 43 | 44 | const keyExtractor = React.useCallback((item: UnsplashPhoto) => item.id, []); 45 | return ( 46 | 49 |

} 52 | /> 53 | 54 | contentContainerStyle={styles.listView} 55 | sections={data} 56 | renderItem={renderItem} 57 | refreshing={refreshing} 58 | onRefresh={doRefresh} 59 | isLoadingMore={loadingMore} 60 | onLoadMore={doLoadMore} 61 | keyExtractor={keyExtractor} 62 | LoadingComponent={ 63 | <> 64 | 65 | 66 | 67 | 68 | 69 | 70 | } 71 | windowSize={11} 72 | /> 73 | 74 | ); 75 | }; 76 | 77 | export const Home = withHotRedux( 78 | homeSlice.name, 79 | homeSlice.reducer, 80 | homeEpic, 81 | )(_Home); 82 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/home.epic.ts: -------------------------------------------------------------------------------- 1 | import {Epic, combineEpics} from 'redux-observable'; 2 | import { 3 | filter, 4 | switchMap, 5 | map, 6 | catchError, 7 | skipWhile, 8 | mergeMap, 9 | throttle, 10 | } from 'rxjs/operators'; 11 | import {of, concat, timer} from 'rxjs'; 12 | 13 | import {homeSlice} from './home.slice'; 14 | import {container} from 'tsyringe'; 15 | import {UnsplashPhoto, UnsplashRepository} from '@data'; 16 | import {Action} from 'redux'; 17 | import {HomeState} from './types'; 18 | 19 | const { 20 | actions: { 21 | refresh, 22 | refreshSuccess, 23 | refreshFailed, 24 | loadMore, 25 | loadMoreStart, 26 | loadMoreFailed, 27 | loadMoreSuccess, 28 | }, 29 | } = homeSlice; 30 | 31 | const refreshEpic$: Epic = (action$) => 32 | action$.pipe( 33 | filter(refresh.match), 34 | switchMap(() => { 35 | const repo = container.resolve('UnsplashRepository'); 36 | return repo.getPhotos().pipe( 37 | filter((data) => data.length > 0), 38 | map(refreshSuccess), 39 | catchError((err) => { 40 | console.warn(err); 41 | return of(refreshFailed()); 42 | }), 43 | ); 44 | }), 45 | ); 46 | 47 | const loadMoreEpic$: Epic = ( 48 | action$, 49 | state$, 50 | ) => 51 | action$.pipe( 52 | throttle(() => timer(300)), 53 | skipWhile(() => state$.value.home?.loadingMore), 54 | filter(loadMore.match), 55 | mergeMap(() => { 56 | const page = state$.value.home.data.length + 1; 57 | const repo = container.resolve('UnsplashRepository'); 58 | return concat( 59 | of(loadMoreStart()), 60 | repo.getPhotos(page).pipe( 61 | filter( 62 | (data) => 63 | !compareData(data, state$.value.home?.data[page - 1]?.data), 64 | ), 65 | map((data) => loadMoreSuccess({data, page})), 66 | catchError((x) => { 67 | console.warn(x); 68 | return of(loadMoreFailed()); 69 | }), 70 | ), 71 | ); 72 | }), 73 | ); 74 | 75 | function compareData(next: UnsplashPhoto[], old?: UnsplashPhoto[]): boolean { 76 | if (!old) { 77 | return false; 78 | } 79 | const nextIds = next.map((x) => x.id).join(','); 80 | const oldIds = old.map((x) => x.id).join(','); 81 | const equal = nextIds === oldIds; 82 | return equal; 83 | } 84 | 85 | export const homeEpic = combineEpics(refreshEpic$, loadMoreEpic$); 86 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/home.slice.ts: -------------------------------------------------------------------------------- 1 | import {UnsplashPhoto} from '@data'; 2 | import {createSlice} from '@reduxjs/toolkit'; 3 | import {PayloadAction} from '@reduxjs/toolkit'; 4 | import {HomeState, Section} from './types'; 5 | 6 | export const INITIAL_STATE: HomeState = { 7 | data: [], 8 | refreshing: true, 9 | loadingMore: false, 10 | }; 11 | export const homeSlice = createSlice({ 12 | name: 'home', 13 | initialState: INITIAL_STATE, 14 | reducers: { 15 | refresh: (state) => Object.assign(state, {refreshing: true}), 16 | refreshSuccess: (state, {payload}: PayloadAction) => 17 | Object.assign(state, { 18 | refreshing: false, 19 | data: [{page: 1, data: payload}], 20 | }), 21 | refreshFailed: (state) => Object.assign(state, {refreshing: false}), 22 | loadMore: (state) => state, 23 | loadMoreStart: (state) => Object.assign(state, {loadingMore: true}), 24 | loadMoreSuccess: ( 25 | state, 26 | { 27 | payload: {data, page}, 28 | }: PayloadAction<{data: UnsplashPhoto[]; page: number}>, 29 | ) => { 30 | if (page <= state.data.length) { 31 | const sections: Section[] = [...state.data]; 32 | sections[page - 1] = {page, data}; 33 | return Object.assign(state, { 34 | loadingMore: false, 35 | data: sections, 36 | }); 37 | } 38 | return Object.assign(state, { 39 | data: state.data.concat([{data, page}]), 40 | loadingMore: false, 41 | }); 42 | }, 43 | loadMoreFailed: (state) => Object.assign(state, {loadingMore: false}), 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "Home.view.tsx" 3 | } -------------------------------------------------------------------------------- /src/presentation/container/authorized/home/types.ts: -------------------------------------------------------------------------------- 1 | import {StackNavigationProp} from '@react-navigation/stack'; 2 | import {RouteProp} from '@react-navigation/native'; 3 | 4 | import {AuthorizedStoryboardParamList} from '@storyboards'; 5 | import {RootStoreState} from '@shared-state'; 6 | import {UnsplashPhoto} from '@data'; 7 | 8 | export type HomeNavigationProps = StackNavigationProp< 9 | AuthorizedStoryboardParamList, 10 | 'Home' 11 | >; 12 | 13 | export type HomeRouteProp = RouteProp; 14 | 15 | export type HomeProps = { 16 | navigation: HomeNavigationProps; 17 | route: HomeRouteProp; 18 | }; 19 | 20 | export type Section = { 21 | page: number; 22 | data: UnsplashPhoto[]; 23 | }; 24 | 25 | export type HomeState = { 26 | data: Section[]; 27 | refreshing: boolean; 28 | loadingMore: boolean; 29 | }; 30 | 31 | export type StoreStateWithHome = RootStoreState & { 32 | home?: HomeState; 33 | }; 34 | 35 | export type HomeReduxSelectionState = HomeState; 36 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home'; 2 | export * from './profile'; 3 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/profile/Profile.epic.ts: -------------------------------------------------------------------------------- 1 | import {combineEpics, Epic} from 'redux-observable'; 2 | import {filter, mergeMap, map, catchError} from 'rxjs/operators'; 3 | import {of, concat} from 'rxjs'; 4 | 5 | import {ProfileActions} from './types'; 6 | import {container} from 'tsyringe'; 7 | import {ReqresRepository} from '@data'; 8 | 9 | export const hotProfileEpic = (actions: ProfileActions): Epic => { 10 | const { 11 | fetchProfile, 12 | fetchProfileSuccess, 13 | fetchProfileStart, 14 | fetchProfileFailed, 15 | 16 | fetchFriendFailed, 17 | fetchFriendStart, 18 | fetchFriendSuccess, 19 | } = actions; 20 | const fetchProfileEpic$: Epic = (action$) => 21 | action$.pipe( 22 | filter(fetchProfile.match), 23 | mergeMap((action) => { 24 | const repo = container.resolve('ReqresRepository'); 25 | return concat( 26 | of(fetchProfileStart()), 27 | repo.getUser(action.payload).pipe( 28 | map((profile) => fetchProfileSuccess(profile)), 29 | catchError((err) => { 30 | console.warn(err); 31 | return of(fetchProfileFailed()); 32 | }), 33 | ), 34 | ); 35 | }), 36 | ); 37 | 38 | const fetchFriendEpic$: Epic = (action$) => 39 | action$.pipe( 40 | filter(fetchProfile.match), 41 | mergeMap(() => { 42 | const repo = container.resolve('ReqresRepository'); 43 | return concat( 44 | of(fetchFriendStart()), 45 | repo.listUsers().pipe( 46 | map((data) => fetchFriendSuccess(data)), 47 | catchError((err) => { 48 | console.warn(err); 49 | return of(fetchFriendFailed()); 50 | }), 51 | ), 52 | ); 53 | }), 54 | ); 55 | return combineEpics(fetchProfileEpic$, fetchFriendEpic$); 56 | }; 57 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/profile/Profile.hooks.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {useDispatch, useSelector} from 'react-redux'; 4 | import { 5 | ProfileActions, 6 | ProfileSelector, 7 | ProfileReduxSelectionState, 8 | StoreStateWithProfile, 9 | } from './types'; 10 | 11 | export function useProfileModel( 12 | actions: ProfileActions, 13 | profileSelector: ProfileSelector, 14 | profileId: string, 15 | ) { 16 | const {isLoading, profile, friends, isLoadingFriend} = useSelector< 17 | StoreStateWithProfile, 18 | ProfileReduxSelectionState 19 | >(profileSelector); 20 | const dispatch = useDispatch(); 21 | React.useEffect(() => { 22 | dispatch(actions.fetchProfile(profileId)); 23 | }, [dispatch, actions, profileId]); 24 | return { 25 | isLoading, 26 | avatar: profile?.profile_image.large, 27 | friends, 28 | isLoadingFriend, 29 | name: profile?.name, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/profile/Profile.slice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createAction, 3 | createSlice, 4 | Slice, 5 | PayloadAction, 6 | } from '@reduxjs/toolkit'; 7 | import {ProfileActions, ProfileSelector, ProfileState} from './types'; 8 | 9 | import {HotReduxComposer} from '@hocs'; 10 | import {hotProfileEpic} from './Profile.epic'; 11 | import {UnsplashUser} from '@data'; 12 | 13 | export const INITIAL_STATE: ProfileState = { 14 | isLoading: false, 15 | friends: [], 16 | isLoadingFriend: false, 17 | }; 18 | 19 | export type ProfileSlice = Slice; 20 | export const hotProfileRedux: HotReduxComposer< 21 | ProfileActions, 22 | ProfileSelector 23 | > = (name) => { 24 | const fetchProfile = createAction(`profile/${name}/fetchProfile`); 25 | const slice = createSlice({ 26 | name, 27 | initialState: INITIAL_STATE, 28 | reducers: { 29 | fetchProfileStart: (state) => { 30 | return { 31 | ...state, 32 | isLoading: true, 33 | }; 34 | }, 35 | fetchProfileSuccess: ( 36 | state, 37 | {payload: profile}: PayloadAction, 38 | ) => { 39 | return { 40 | ...state, 41 | isLoading: false, 42 | profile, 43 | }; 44 | }, 45 | fetchProfileFailed: (state) => { 46 | return { 47 | ...state, 48 | isLoading: false, 49 | }; 50 | }, 51 | fetchFriendStart: (state) => ({...state, isLoadingFriend: true}), 52 | fetchFriendSuccess: ( 53 | state, 54 | {payload}: PayloadAction, 55 | ) => ({ 56 | ...state, 57 | isLoadingFriend: false, 58 | friends: payload, 59 | }), 60 | fetchFriendFailed: (state) => ({...state, isLoadingFriend: false}), 61 | }, 62 | }); 63 | 64 | const selector: ProfileSelector = (state) => { 65 | return state[name] ?? INITIAL_STATE; 66 | }; 67 | const actions: ProfileActions = { 68 | fetchProfile, 69 | ...slice.actions, 70 | }; 71 | return { 72 | reducer: slice.reducer, 73 | actions, 74 | selector, 75 | epic: hotProfileEpic(actions), 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/profile/Profile.style.ts: -------------------------------------------------------------------------------- 1 | import {LightTheme} from '@resources'; 2 | import {Dimensions, StyleSheet} from 'react-native'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | }, 8 | listHeader: { 9 | backgroundColor: 'white', 10 | }, 11 | avatar: { 12 | width: Dimensions.get('window').width * 0.4, 13 | height: Dimensions.get('window').width * 0.4, 14 | borderRadius: Dimensions.get('window').width * 0.2, 15 | alignSelf: 'center', 16 | borderWidth: 3, 17 | borderColor: LightTheme.colorScheme.secondary, 18 | }, 19 | listView: { 20 | flex: 1, 21 | }, 22 | friendImage: { 23 | width: Dimensions.get('window').width / 3, 24 | height: Dimensions.get('window').width / 3, 25 | 26 | borderWidth: 2, 27 | borderColor: 'white', 28 | }, 29 | row: { 30 | flexDirection: 'row', 31 | }, 32 | name: { 33 | fontSize: 20, 34 | fontWeight: 'bold', 35 | padding: 20, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/profile/Profile.view.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ListRenderItemInfo, Pressable, View} from 'react-native'; 3 | // import from library 4 | import {SharedElement} from 'react-navigation-shared-element'; 5 | import Image from 'react-native-fast-image'; 6 | import {SafeAreaView} from 'react-native-safe-area-context'; 7 | // import from alias 8 | import {FlatButton, ListView, TextView} from '@components'; 9 | import {withHotEnhanceRedux} from '@hocs'; 10 | // localImport 11 | import {useProfileModel} from './Profile.hooks'; 12 | import {ProfileProps} from './types'; 13 | import {styles} from './Profile.style'; 14 | import {hotProfileRedux} from './Profile.slice'; 15 | import {UnsplashUser} from '@data'; 16 | import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; 17 | import {Header} from 'react-native-elements'; 18 | import {useTheme} from '@hooks'; 19 | 20 | const _Profile: React.FC = (props) => { 21 | const {actions, selector, route, navigation} = props; 22 | const {colorScheme} = useTheme(); 23 | const {isLoading, avatar, friends, isLoadingFriend, name} = useProfileModel( 24 | actions, 25 | selector, 26 | route.params.id, 27 | ); 28 | 29 | const goBack = React.useCallback(() => { 30 | navigation.pop(); 31 | }, [navigation]); 32 | 33 | const navigateToFriendProfile = React.useCallback( 34 | (item: UnsplashUser) => () => { 35 | navigation.push('Profile', {id: item.username}); 36 | }, 37 | [navigation], 38 | ); 39 | 40 | const renderItem = React.useCallback( 41 | ({item}: ListRenderItemInfo) => { 42 | return ( 43 | 44 | 45 | 49 | 50 | 51 | ); 52 | }, 53 | [navigateToFriendProfile], 54 | ); 55 | 56 | const keyExtractor = React.useCallback((item: UnsplashUser) => item.id, []); 57 | 58 | const renderHeader = () => { 59 | if (isLoading) { 60 | return ( 61 | <> 62 | 63 | 64 | 65 | 66 | 72 | 73 | 74 | ); 75 | } 76 | return ( 77 | 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | return ( 87 | 88 |
} 90 | backgroundColor={colorScheme.primary} 91 | /> 92 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | } 123 | stickyHeaderIndices={[0]} 124 | data={friends} 125 | renderItem={renderItem} 126 | keyExtractor={keyExtractor} 127 | /> 128 | 129 | ); 130 | }; 131 | 132 | export const Profile = withHotEnhanceRedux(hotProfileRedux)(_Profile); 133 | // Profile.sharedElements = (route: ProfileRouteProp) => [ 134 | // {id: `avatar-${route.params.id}`}, 135 | // ]; 136 | -------------------------------------------------------------------------------- /src/presentation/container/authorized/profile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "Profile.view.tsx" 3 | } -------------------------------------------------------------------------------- /src/presentation/container/authorized/profile/types.ts: -------------------------------------------------------------------------------- 1 | import {StackNavigationProp} from '@react-navigation/stack'; 2 | import {RouteProp} from '@react-navigation/native'; 3 | 4 | import {AuthorizedStoryboardParamList} from '@storyboards'; 5 | import {RootStoreState} from '@shared-state'; 6 | import { 7 | ActionCreatorWithoutPayload, 8 | ActionCreatorWithPayload, 9 | } from '@reduxjs/toolkit'; 10 | import {Selector} from 'react-redux'; 11 | import {ReduxComposeComponentProps} from '@hocs'; 12 | import {UnsplashUser} from '@data'; 13 | 14 | export type ProfileNavigationProps = StackNavigationProp< 15 | AuthorizedStoryboardParamList, 16 | 'Profile' 17 | >; 18 | 19 | export type ProfileRouteProp = RouteProp< 20 | AuthorizedStoryboardParamList, 21 | 'Profile' 22 | >; 23 | 24 | export type ProfileActions = { 25 | fetchProfile: ActionCreatorWithPayload; 26 | fetchProfileSuccess: ActionCreatorWithPayload; 27 | fetchProfileFailed: ActionCreatorWithoutPayload; 28 | fetchProfileStart: ActionCreatorWithoutPayload; 29 | 30 | fetchFriendStart: ActionCreatorWithoutPayload; 31 | fetchFriendSuccess: ActionCreatorWithPayload; 32 | fetchFriendFailed: ActionCreatorWithoutPayload; 33 | }; 34 | 35 | export type ProfileSelector = Selector< 36 | StoreStateWithProfile, 37 | ProfileReduxSelectionState 38 | >; 39 | 40 | export interface ProfileProps 41 | extends ReduxComposeComponentProps { 42 | navigation: ProfileNavigationProps; 43 | route: ProfileRouteProp; 44 | } 45 | 46 | export type ProfileState = { 47 | isLoading: boolean; 48 | profile?: UnsplashUser; 49 | friends: UnsplashUser[]; 50 | isLoadingFriend: boolean; 51 | }; 52 | 53 | export type StoreStateWithProfile = RootStoreState & 54 | { 55 | [key in string]?: ProfileState; 56 | }; 57 | 58 | export type ProfileReduxSelectionState = ProfileState & {}; 59 | -------------------------------------------------------------------------------- /src/presentation/container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication'; 2 | export * from './authorized'; 3 | -------------------------------------------------------------------------------- /src/presentation/hoc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './withHotRedux.hoc'; 2 | export * from './withHotEnhanceRedux.hoc'; 3 | -------------------------------------------------------------------------------- /src/presentation/hoc/withHotEnhanceRedux.hoc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Epic} from 'redux-observable'; 3 | import {Reducer} from 'redux'; 4 | import {container} from 'tsyringe'; 5 | import uuid from 'react-native-uuid'; 6 | 7 | import {StoreContainer} from '@shared-state'; 8 | 9 | export type ReduxCompose = { 10 | reducer: Reducer; 11 | actions: Actions; 12 | selector: Selector; 13 | epic: Epic; 14 | }; 15 | 16 | export type HotReduxComposer = ( 17 | name: string, 18 | ) => ReduxCompose; 19 | 20 | export interface ReduxComposeComponentProps { 21 | actions: Actions; 22 | selector: Selector; 23 | route: any; 24 | } 25 | 26 | export const withHotEnhanceRedux = ( 27 | composer: HotReduxComposer, 28 | ) => ( 29 | Component: React.FC> | any, 30 | ): React.ComponentType => { 31 | return class WithHotRedux extends React.PureComponent { 32 | reduxCompose: ReduxCompose; 33 | reduxKey: string; 34 | constructor(props: any) { 35 | super(props); 36 | const {reducerManager, addEpic} = container.resolve( 37 | 'StoreContainer', 38 | ); 39 | this.reduxKey = uuid.v4(); 40 | this.reduxCompose = composer(this.reduxKey); 41 | reducerManager.add(this.reduxKey, this.reduxCompose.reducer); 42 | addEpic(this.reduxCompose.epic); 43 | } 44 | 45 | componentWillUnmount() { 46 | const {reducerManager} = container.resolve( 47 | 'StoreContainer', 48 | ); 49 | reducerManager.remove(this.reduxKey); 50 | } 51 | 52 | render() { 53 | return ( 54 | 59 | ); 60 | } 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /src/presentation/hoc/withHotRedux.hoc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Epic} from 'redux-observable'; 3 | import {Reducer} from 'redux'; 4 | import {StoreContainer} from '@shared-state'; 5 | import {container} from 'tsyringe'; 6 | 7 | export const withHotRedux = ( 8 | reducerKey: string, 9 | reducer: Reducer, 10 | epic: Epic, 11 | ) => (Component: React.FC | React.ComponentType): React.ComponentType => { 12 | return class WithHotRedux extends React.PureComponent { 13 | constructor(props: any) { 14 | super(props); 15 | const {reducerManager, addEpic} = container.resolve( 16 | 'StoreContainer', 17 | ); 18 | reducerManager.add(reducerKey, reducer); 19 | addEpic(epic); 20 | } 21 | render() { 22 | return ; 23 | } 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/presentation/hook/index.ts: -------------------------------------------------------------------------------- 1 | export * from './share-state'; 2 | -------------------------------------------------------------------------------- /src/presentation/hook/share-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTheme'; 2 | -------------------------------------------------------------------------------- /src/presentation/hook/share-state/useTheme.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useColorScheme} from 'react-native'; 3 | 4 | import {useSelector, Selector, useDispatch} from 'react-redux'; 5 | 6 | import {LightTheme} from '@resources'; 7 | import {ThemeConfig} from '@core'; 8 | import {RootStoreState, setTheme} from '@shared-state'; 9 | 10 | export const themeSelector: Selector = ({ 11 | configuration: {themeConfig}, 12 | }) => themeConfig; 13 | 14 | export function useTheme() { 15 | const themeConfig = useSelector(themeSelector); 16 | const systemColorScheme = useColorScheme(); 17 | if (themeConfig === ThemeConfig.Dark) { 18 | return LightTheme; 19 | } 20 | if (themeConfig === ThemeConfig.System) { 21 | if (systemColorScheme === 'dark') { 22 | return LightTheme; 23 | } 24 | } 25 | return LightTheme; 26 | } 27 | 28 | /** 29 | * hook to theme state 30 | * @return Theme state and function to set theme 31 | */ 32 | export function useThemeWithSetter() { 33 | const theme = useTheme(); 34 | const dispatch = useDispatch(); 35 | const dispatchTheme = React.useCallback( 36 | (config: ThemeConfig) => { 37 | dispatch(setTheme(config)); 38 | }, 39 | [dispatch], 40 | ); 41 | return [theme, dispatchTheme]; 42 | } 43 | -------------------------------------------------------------------------------- /src/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './navigation'; 2 | -------------------------------------------------------------------------------- /src/presentation/navigation/AuthenticationStack.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {createStackNavigator} from '@react-navigation/stack'; 4 | 5 | import {AuthenticationStoryboardParamList} from '@storyboards'; 6 | import {SignIn} from '@containers'; 7 | 8 | const Stack = createStackNavigator(); 9 | 10 | export const AuthenticationNavigator: React.FC = () => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/presentation/navigation/AuthorizedStack.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {createSharedElementStackNavigator} from 'react-navigation-shared-element'; 4 | 5 | import {Home, Profile} from '@containers'; 6 | 7 | const Stack = createSharedElementStackNavigator(); 8 | 9 | export const AuthorizedNavigator: React.FC = () => { 10 | return ( 11 | 12 | 13 | { 17 | return [`avatar-${route.params.id}`]; 18 | }} 19 | /> 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/presentation/navigation/RootNavigator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {} from 'react-native'; 3 | 4 | import {NavigationContainer} from '@react-navigation/native'; 5 | import {useSelector, useDispatch} from 'react-redux'; 6 | import {createNativeStackNavigator} from 'react-native-screens/native-stack'; 7 | import {enableScreens} from 'react-native-screens'; 8 | 9 | import {AuthorizedNavigator} from './AuthorizedStack'; 10 | import {AuthenticationNavigator} from './AuthenticationStack'; 11 | import {RootStoreState, signInLocally} from '@shared-state'; 12 | 13 | enableScreens(); 14 | const Stack = createNativeStackNavigator(); 15 | 16 | export const RootNavigator: React.FC = () => { 17 | const isAuthorized = useSelector( 18 | ({authentication}: RootStoreState): boolean => authentication.isAuthorized, 19 | ); 20 | 21 | const dispatch = useDispatch(); 22 | 23 | React.useEffect(() => { 24 | dispatch(signInLocally()); 25 | }, [dispatch]); 26 | 27 | const renderStack = () => { 28 | if (isAuthorized) { 29 | return ( 30 | 34 | ); 35 | } 36 | return ( 37 | 41 | ); 42 | }; 43 | return ( 44 | 45 | 47 | {renderStack()} 48 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/presentation/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RootNavigator'; 2 | -------------------------------------------------------------------------------- /src/presentation/resource/index.ts: -------------------------------------------------------------------------------- 1 | export * from './values'; 2 | -------------------------------------------------------------------------------- /src/presentation/resource/values/colors.ts: -------------------------------------------------------------------------------- 1 | export const Colors = { 2 | PURPLE: 'rgb(132,37,114)', 3 | WHITE: '#FFFFFF', 4 | GRAY: '#eeeeee', 5 | OVERLAY: 'rgba(51,51,51,0.4)', 6 | LIGHT_PINK: '#ee4e9b26', 7 | PINK: '#ee4e9b96', 8 | LIGHT_ORANGE: '#d06767d6', 9 | }; 10 | -------------------------------------------------------------------------------- /src/presentation/resource/values/dimensions.ts: -------------------------------------------------------------------------------- 1 | import {Dimensions} from 'react-native'; 2 | -------------------------------------------------------------------------------- /src/presentation/resource/values/font.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const FontSize = { 4 | } -------------------------------------------------------------------------------- /src/presentation/resource/values/index.ts: -------------------------------------------------------------------------------- 1 | export * from './colors'; 2 | export * from './themes'; 3 | -------------------------------------------------------------------------------- /src/presentation/resource/values/themes.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@core'; 2 | import {Colors} from './colors'; 3 | 4 | export const LightTheme: Theme = { 5 | colorScheme: { 6 | primary: Colors.WHITE, 7 | secondary: Colors.PURPLE, 8 | onSecondary: Colors.WHITE, 9 | background: Colors.GRAY, 10 | surface: Colors.PURPLE, 11 | error: Colors.PURPLE, 12 | onBackground: '#000', 13 | onError: Colors.WHITE, 14 | onPrimary: Colors.WHITE, 15 | onSurface: Colors.WHITE, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/presentation/shared-state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redux'; 2 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/actions/authentication.action.ts: -------------------------------------------------------------------------------- 1 | import {Credential} from '@domain'; 2 | import {createAction} from '@reduxjs/toolkit'; 3 | 4 | export const signIn = createAction('authentication/singIn'); 5 | export const signInBegin = createAction('authentication/signInBegin'); 6 | export const signInSuccess = createAction('authentication/signInSuccess'); 7 | export const signInFailed = createAction('authentication/signInFailed'); 8 | 9 | export const signInLocally = createAction('authentication/signInLocally'); 10 | export const signInLocallySuccess = createAction( 11 | 'authentication/signInLocallySuccess', 12 | ); 13 | export const signInLocallyFailed = createAction( 14 | 'authentication/signInLocallyFailed', 15 | ); 16 | 17 | export const signOut = createAction('authentication/signOut'); 18 | export const signOutFailed = createAction('authentication/signOutFailed'); 19 | export const signOutSuccess = createAction('authentication/signOutSuccess'); 20 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/actions/configuration.action.ts: -------------------------------------------------------------------------------- 1 | import {createAction} from '@reduxjs/toolkit'; 2 | import {ThemeConfig} from '@core'; 3 | 4 | export const setTheme = createAction('configuration/setTheme'); 5 | export const setThemeSuccess = createAction('configuration/setThemeSuccess'); 6 | export const setThemeFailed = createAction('configuration/setThemeFailed'); 7 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.action'; 2 | export * from './configuration.action'; 3 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/epic.ts: -------------------------------------------------------------------------------- 1 | import {BehaviorSubject} from 'rxjs'; 2 | import {Action} from 'redux'; 3 | 4 | import { 5 | combineEpics, 6 | Epic, 7 | createEpicMiddleware, 8 | EpicMiddleware, 9 | } from 'redux-observable'; 10 | 11 | import {mergeMap, catchError, tap} from 'rxjs/operators'; 12 | import {RootEpicDependency, RootEpic, RootStoreState} from './types'; 13 | 14 | export function createEpicManager( 15 | dependencies: RootEpicDependency = {}, 16 | ...epics: Epic[] 17 | ): { 18 | addEpic: (epic: Epic) => void; 19 | epic$: BehaviorSubject; 20 | rootEpic: RootEpic; 21 | epicMiddleware: EpicMiddleware< 22 | Action, 23 | Action, 24 | RootStoreState, 25 | RootEpicDependency 26 | >; 27 | } { 28 | const addedEpics: Epic[] = []; 29 | const epic$ = new BehaviorSubject(combineEpics(...epics)); 30 | const addEpic = (epic: Epic) => { 31 | if (addedEpics.includes(epic)) { 32 | return; 33 | } 34 | addedEpics.push(epic); 35 | epic$.next(epic); 36 | }; 37 | const rootEpic: Epic = (action$, state$) => 38 | epic$.pipe( 39 | mergeMap((epic) => 40 | epic(action$, state$, dependencies).pipe( 41 | tap((x) => console.log(x?.type)), 42 | catchError((err, source) => { 43 | console.warn(err); 44 | return source; 45 | }), 46 | ), 47 | ), 48 | ); 49 | 50 | const epicMiddleware = createEpicMiddleware< 51 | Action, 52 | Action, 53 | RootStoreState, 54 | RootEpicDependency 55 | >(); 56 | return {epic$, rootEpic, epicMiddleware, addEpic}; 57 | } 58 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/epics/authentication.epic.ts: -------------------------------------------------------------------------------- 1 | import {Epic, combineEpics} from 'redux-observable'; 2 | import {container} from 'tsyringe'; 3 | import {AnyAction} from 'redux'; 4 | import {of, concat} from 'rxjs'; 5 | import {filter, catchError, switchMap, map} from 'rxjs/operators'; 6 | 7 | import { 8 | signInSuccess, 9 | signInFailed, 10 | signIn, 11 | signInBegin, 12 | signInLocally, 13 | signInLocallyFailed, 14 | signInLocallySuccess, 15 | signOut, 16 | signOutSuccess, 17 | } from '../actions'; 18 | 19 | import {SignInUseCase} from '@domain'; 20 | import {RootStoreState} from '../types'; 21 | 22 | const signInEpic$: Epic = (action$) => 23 | action$.pipe( 24 | filter(signIn.match), 25 | switchMap((action) => { 26 | const useCase = container.resolve('SignInUseCase'); 27 | return concat( 28 | of(signInBegin()), 29 | useCase.call(action.payload).pipe( 30 | map(signInSuccess), 31 | catchError(() => of(signInFailed())), 32 | ), 33 | ); 34 | }), 35 | ); 36 | const signInLocallyEpic$: Epic = (action$) => 37 | action$.pipe( 38 | filter(signInLocally.match), 39 | switchMap(() => { 40 | const useCase = container.resolve('SignInUseCase'); 41 | return useCase.call().pipe( 42 | map(signInLocallySuccess), 43 | catchError(() => of(signInLocallyFailed())), 44 | ); 45 | }), 46 | ); 47 | 48 | const signOutEpic$: Epic = ( 49 | action$, 50 | state$, 51 | ) => 52 | action$.pipe( 53 | filter(signOut.match), 54 | filter(() => state$.value.authentication.isAuthorized), 55 | map(signOutSuccess), 56 | ); 57 | export const authenticationEpic = combineEpics( 58 | signInEpic$, 59 | signInLocallyEpic$, 60 | signOutEpic$, 61 | ); 62 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/epics/configuration.epic.ts: -------------------------------------------------------------------------------- 1 | import {Epic, combineEpics} from 'redux-observable'; 2 | import {of} from 'rxjs'; 3 | import {filter, mergeMap} from 'rxjs/operators'; 4 | 5 | import {setTheme, setThemeSuccess} from '../actions'; 6 | 7 | const setThemeEpic$: Epic = (action$) => 8 | action$.pipe( 9 | filter(setTheme.match), 10 | mergeMap(() => of(setThemeSuccess())), 11 | ); 12 | 13 | export const configurationEpic = combineEpics(setThemeEpic$); 14 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/epics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.epic'; 2 | export * from './configuration.epic'; 3 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './store'; 2 | export * from './types'; 3 | export * from './actions'; 4 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import {Reducer, combineReducers, Action} from 'redux'; 2 | import {ReducerManger, RootStoreState} from './types'; 3 | 4 | type ReducerKey = keyof RootStoreState; 5 | type ReducerMap = {[key in keyof RootStoreState]: Reducer}; 6 | 7 | export function createReducerManager(reducerMap: ReducerMap): ReducerManger { 8 | // Create an object which maps keys to reducers 9 | const reducers: ReducerMap = { 10 | ...reducerMap, 11 | }; 12 | 13 | // Create the initial combinedReducer 14 | let combinedReducer = combineReducers(reducers); 15 | // An array which is used to delete state keys when reducers are removed 16 | let keysToRemove: ReducerKey[] = []; 17 | 18 | return { 19 | // The root reducer function exposed by this object 20 | // This will be passed to the store 21 | reduce: ( 22 | state: RootStoreState | undefined, 23 | action: Action, 24 | ): RootStoreState => { 25 | // If any reducers have been removed, clean up their state first 26 | if (state && keysToRemove.length > 0) { 27 | state = {...state}; 28 | for (let key of keysToRemove) { 29 | delete state[key]; 30 | } 31 | keysToRemove = []; 32 | } 33 | // Delegate to the combined reducer 34 | return combinedReducer(state, action); 35 | }, 36 | 37 | // Adds a new reducer with the specified key 38 | add: (key: ReducerKey, reducer: Reducer) => { 39 | if (reducers[key]) { 40 | return; 41 | } 42 | // Add the reducer to the reducer mapping 43 | reducers[key] = reducer; 44 | // Generate a new combined reducer 45 | combinedReducer = combineReducers(reducers); 46 | }, 47 | 48 | // Removes a reducer with the specified key 49 | remove: (key: ReducerKey) => { 50 | if (!reducers[key]) { 51 | return; 52 | } 53 | // Remove it from the reducer mapping 54 | delete reducers[key]; 55 | // Add the key to the list of keys to clean up 56 | keysToRemove.push(key); 57 | // Generate a new combined reducer 58 | combinedReducer = combineReducers(reducers); 59 | }, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/reducers/authentication.reducer.ts: -------------------------------------------------------------------------------- 1 | import {createReducer} from '@reduxjs/toolkit'; 2 | import { 3 | signInSuccess, 4 | signInFailed, 5 | signInBegin, 6 | signInLocally, 7 | signInLocallySuccess, 8 | signInLocallyFailed, 9 | signOutSuccess, 10 | } from '../actions'; 11 | 12 | export type AuthenticationState = { 13 | isAuthorized: boolean; 14 | isAuthenticating: boolean; 15 | isAuthenticatingLocally: boolean; 16 | }; 17 | 18 | const INITIAL_STATE: AuthenticationState = { 19 | isAuthenticating: false, 20 | isAuthorized: false, 21 | isAuthenticatingLocally: false, 22 | }; 23 | 24 | export const authenticationReducer = createReducer(INITIAL_STATE, (builder) => 25 | builder 26 | .addCase(signInBegin, (state) => 27 | Object.assign(state, {isAuthenticating: true}), 28 | ) 29 | .addCase(signInSuccess, (state) => 30 | Object.assign(state, {isAuthenticating: false, isAuthorized: true}), 31 | ) 32 | .addCase(signInFailed, (state) => 33 | Object.assign(state, {isAuthenticating: false, isAuthorized: false}), 34 | ) 35 | .addCase(signInLocally, (state) => 36 | Object.assign(state, { 37 | isAuthenticatingLocally: true, 38 | }), 39 | ) 40 | .addCase(signInLocallySuccess, (state) => 41 | Object.assign(state, { 42 | isAuthenticatingLocally: false, 43 | isAuthorized: true, 44 | }), 45 | ) 46 | .addCase(signInLocallyFailed, (state) => 47 | Object.assign(state, { 48 | isAuthenticatingLocally: false, 49 | isAuthorized: false, 50 | }), 51 | ) 52 | .addCase(signOutSuccess, (state) => 53 | Object.assign(state, { 54 | isAuthorized: false, 55 | }), 56 | ), 57 | ); 58 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/reducers/configuration.reducer.ts: -------------------------------------------------------------------------------- 1 | import {createReducer} from '@reduxjs/toolkit'; 2 | 3 | import {ThemeConfig} from '@core'; 4 | 5 | import {setTheme} from '../actions'; 6 | 7 | export type ConfigurationState = { 8 | themeConfig: ThemeConfig; 9 | }; 10 | 11 | const INITIAL_STATE: ConfigurationState = { 12 | themeConfig: ThemeConfig.System, 13 | }; 14 | 15 | export const configurationReducer = createReducer(INITIAL_STATE, (builder) => 16 | builder.addCase(setTheme, (state, {payload: themeConfig}) => 17 | Object.assign(state, {themeConfig}), 18 | ), 19 | ); 20 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/reducers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.reducer'; 2 | export * from './configuration.reducer'; 3 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/store.ts: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, Action} from 'redux'; 2 | import {BehaviorSubject} from 'rxjs'; 3 | 4 | import {StoreContainer, RootStoreState} from './types'; 5 | import {createReducerManager} from './reducer'; 6 | import {createEpicManager} from './epic'; 7 | import {configurationEpic, authenticationEpic} from './epics'; 8 | import {configurationReducer, authenticationReducer} from './reducers'; 9 | 10 | export function configureStore(): StoreContainer { 11 | const reducerManager = createReducerManager({ 12 | authentication: authenticationReducer, 13 | configuration: configurationReducer, 14 | }); 15 | const {rootEpic, epicMiddleware, epic$, addEpic} = createEpicManager( 16 | {}, 17 | authenticationEpic, 18 | configurationEpic, 19 | ); 20 | // Create a store with the root reducer function being the one exposed by the manager. 21 | 22 | const action$ = new BehaviorSubject({type: 'init'}); 23 | const reducer = ( 24 | state: RootStoreState | undefined, 25 | action: Action, 26 | ) => { 27 | action$.next(action); 28 | return reducerManager.reduce(state, action); 29 | }; 30 | const store = createStore, any, any>( 31 | reducer, 32 | applyMiddleware(epicMiddleware), 33 | ); 34 | epicMiddleware.run(rootEpic); 35 | 36 | // Optional: Put the reducer manager on the store so it is easily accessible 37 | return { 38 | reducerManager, 39 | store, 40 | epic$, 41 | action$, 42 | addEpic, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/presentation/shared-state/redux/types.ts: -------------------------------------------------------------------------------- 1 | import {Reducer, Action, Store} from 'redux'; 2 | import {Epic} from 'redux-observable'; 3 | 4 | import {AuthenticationState, ConfigurationState} from './reducers'; 5 | import {BehaviorSubject} from 'rxjs'; 6 | 7 | export type RootStoreState = { 8 | authentication: AuthenticationState; 9 | configuration: ConfigurationState; 10 | }; 11 | 12 | export type RootEpicDependency = {}; 13 | 14 | export type RootEpic = Epic; 15 | 16 | export type ReducerManger = { 17 | reduce: Reducer; 18 | add(key: string, reducer: Reducer): void; 19 | remove(key: string): void; 20 | }; 21 | 22 | export type StoreContainer = { 23 | store: Store; 24 | reducerManager: ReducerManger; 25 | epic$: BehaviorSubject; 26 | action$: BehaviorSubject; 27 | addEpic: (epic: Epic) => void; 28 | }; 29 | -------------------------------------------------------------------------------- /src/presentation/storyboard/Authentication.storyboard.ts: -------------------------------------------------------------------------------- 1 | export type AuthenticationStoryboardParamList = { 2 | SignIn: {userName?: string}; 3 | SignUp: {userName?: string}; 4 | }; 5 | -------------------------------------------------------------------------------- /src/presentation/storyboard/Authorized.storyboard.ts: -------------------------------------------------------------------------------- 1 | export type AuthorizedStoryboardParamList = { 2 | Home: undefined; 3 | Profile: {id: string}; 4 | }; 5 | -------------------------------------------------------------------------------- /src/presentation/storyboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Authentication.storyboard'; 2 | export * from './Authorized.storyboard'; 3 | --------------------------------------------------------------------------------