} = {};
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 | {{$name}}StoreContainer>
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 |
--------------------------------------------------------------------------------