From e116d4e77480b61523899eae80218f1af9930c84 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 19 Sep 2022 11:33:06 +0200 Subject: [PATCH 1/2] [_]: Add useCases utilities to replace redux --- .../@rudderstack/rudder-sdk-react-native.ts | 3 + __mocks__/react-native-device-info.ts | 3 + __mocks__/react-native-permissions.ts | 5 + jest.config.ts | 4 +- package.json | 3 + shim.js | 2 +- src/hooks/common/useUseCase/index.ts | 1 + .../common/useUseCase/useUseCase.spec.tsx | 15 +++ src/hooks/common/useUseCase/useUseCase.tsx | 66 +++++++++++++ src/services/ErrorService.ts | 4 +- src/services/common/errors/base.ts | 31 +++++++ src/services/common/errors/index.ts | 1 + yarn.lock | 93 +++++++++++++++++-- 13 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 __mocks__/@rudderstack/rudder-sdk-react-native.ts create mode 100644 __mocks__/react-native-device-info.ts create mode 100644 __mocks__/react-native-permissions.ts create mode 100644 src/hooks/common/useUseCase/index.ts create mode 100644 src/hooks/common/useUseCase/useUseCase.spec.tsx create mode 100644 src/hooks/common/useUseCase/useUseCase.tsx create mode 100644 src/services/common/errors/base.ts create mode 100644 src/services/common/errors/index.ts diff --git a/__mocks__/@rudderstack/rudder-sdk-react-native.ts b/__mocks__/@rudderstack/rudder-sdk-react-native.ts new file mode 100644 index 000000000..a9d9c7f38 --- /dev/null +++ b/__mocks__/@rudderstack/rudder-sdk-react-native.ts @@ -0,0 +1,3 @@ +jest.mock('@rudderstack/rudder-sdk-react-native', () => { + return {}; +}); diff --git a/__mocks__/react-native-device-info.ts b/__mocks__/react-native-device-info.ts new file mode 100644 index 000000000..d289a349e --- /dev/null +++ b/__mocks__/react-native-device-info.ts @@ -0,0 +1,3 @@ +import mockRNDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'; + +jest.mock('react-native-device-info', () => mockRNDeviceInfo); diff --git a/__mocks__/react-native-permissions.ts b/__mocks__/react-native-permissions.ts new file mode 100644 index 000000000..6a0718aee --- /dev/null +++ b/__mocks__/react-native-permissions.ts @@ -0,0 +1,5 @@ +import mock from 'react-native-permissions/mock'; + +jest.mock('react-native-permissions', () => { + return mock; +}); diff --git a/jest.config.ts b/jest.config.ts index 8792e61e1..59d532ce2 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -18,12 +18,14 @@ const untranspiledModulePatterns = [ 'react-native-svg', 'rn-fetch-blob', '@internxt/rn-crypto', + '@rudderstack', ]; const config: Config.InitialOptions = { preset: 'jest-expo', verbose: true, - testRegex: '\\.spec\\.ts$', + testRegex: ['\\.spec\\.ts$', '\\.spec\\.tsx$'], + setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], transformIgnorePatterns: [`node_modules/(?!${untranspiledModulePatterns.join('|')})`], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], }; diff --git a/package.json b/package.json index 8c5172002..053a75524 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@reduxjs/toolkit": "^1.6.2", "@rudderstack/rudder-sdk-react-native": "^1.5.1", "@sentry/react-native": "^3.4.2", + "@testing-library/react-hooks": "^8.0.1", "@tradle/react-native-http": "^2.0.1", "@types/luxon": "^3.0.1", "@types/unorm": "^1.3.28", @@ -155,6 +156,8 @@ "@babel/preset-env": "^7.1.6", "@internxt/eslint-config-internxt": "^1.0.8", "@internxt/prettier-config": "^1.0.2", + "@testing-library/jest-native": "^4.0.12", + "@testing-library/react-native": "^11.1.0", "@types/async": "^3.2.6", "@types/jest": "^28.1.1", "@types/lodash": "^4.14.165", diff --git a/shim.js b/shim.js index 75314eabc..b54ec9343 100644 --- a/shim.js +++ b/shim.js @@ -16,7 +16,7 @@ if (typeof Buffer === 'undefined') global.Buffer = require('buffer').Buffer; // global.location = global.location || { port: 80 } const isDev = typeof __DEV__ === 'boolean' && __DEV__; -process.env['NODE_ENV'] = isDev ? 'development' : 'production'; +process.env['NODE_ENV'] = process.env['NODE_ENV'] || isDev ? 'development' : 'production'; if (typeof localStorage !== 'undefined') { localStorage.debug = isDev ? '*' : ''; } diff --git a/src/hooks/common/useUseCase/index.ts b/src/hooks/common/useUseCase/index.ts new file mode 100644 index 000000000..e8b072ed2 --- /dev/null +++ b/src/hooks/common/useUseCase/index.ts @@ -0,0 +1 @@ +export * from './useUseCase'; diff --git a/src/hooks/common/useUseCase/useUseCase.spec.tsx b/src/hooks/common/useUseCase/useUseCase.spec.tsx new file mode 100644 index 000000000..ddfb5b2b8 --- /dev/null +++ b/src/hooks/common/useUseCase/useUseCase.spec.tsx @@ -0,0 +1,15 @@ +import { renderHook, waitFor } from '@testing-library/react-native'; +import { useUseCase } from './useUseCase'; + +describe('useUseCase hook', () => { + test('Should resolve with data once the use case function is completed without errors', async () => { + const mockedUseCase = jest.fn(async () => { + return true; + }); + const { result } = renderHook(() => useUseCase(mockedUseCase)); + + await waitFor(() => expect(mockedUseCase).toHaveBeenCalledTimes(1)); + const [data] = result.current; + expect(data).toBe(true); + }); +}); diff --git a/src/hooks/common/useUseCase/useUseCase.tsx b/src/hooks/common/useUseCase/useUseCase.tsx new file mode 100644 index 000000000..16c7d8883 --- /dev/null +++ b/src/hooks/common/useUseCase/useUseCase.tsx @@ -0,0 +1,66 @@ +import { NotificationType } from '../../../types'; +import { useEffect, useState } from 'react'; +import notificationsService from 'src/services/NotificationsService'; +import { DisplayableError } from 'src/services/common/errors/base'; + +export type UseCaseResult = { + data: T | null; + error: E | null; + loading: boolean; +}; + +interface UseUseCaseConfig { + lazy: boolean; +} + +export function useUseCase( + useCase: () => Promise, + config?: UseUseCaseConfig, +): [T | null, boolean, E | null, () => Promise] { + const [state, setState] = useState>({ data: null, error: null, loading: true }); + + useEffect(() => { + if (!config?.lazy) { + (async () => { + await executeUseCase(); + })(); + } + }, []); + + const processError = (error: unknown) => { + if (error instanceof DisplayableError) { + notificationsService.show({ + text1: error.userFriendlyMessage, + type: NotificationType.Error, + }); + } + }; + + const resetState = () => { + setState({ ...state, error: null }); + }; + + const executeUseCase = async () => { + if (state.data || state.error) { + resetState(); + } + + useCase() + .then((result) => { + setState({ + ...state, + data: result, + loading: false, + }); + }) + .catch((error) => { + processError(error); + setState({ + ...state, + error, + loading: false, + }); + }); + }; + return [state.data, state.loading, state.error, executeUseCase]; +} diff --git a/src/services/ErrorService.ts b/src/services/ErrorService.ts index ea1b5f1f1..1b46bc0b8 100644 --- a/src/services/ErrorService.ts +++ b/src/services/ErrorService.ts @@ -2,12 +2,12 @@ import AppError from '../types'; import sentryService from './SentryService'; import { Severity } from '@sentry/react-native'; -interface GlobalErrorContext { +export interface GlobalErrorContext { email: string; userId: string; } -interface ErrorContext extends GlobalErrorContext { +export interface ErrorContext extends GlobalErrorContext { level: Severity; // Tagname and value of the tag such environment: dev or things like that tags: { [tagName: string]: string }; diff --git a/src/services/common/errors/base.ts b/src/services/common/errors/base.ts new file mode 100644 index 000000000..bad6ce1cc --- /dev/null +++ b/src/services/common/errors/base.ts @@ -0,0 +1,31 @@ +import errorService, { ErrorContext } from 'src/services/ErrorService'; + +export interface ReportableErrorOptions { + error: unknown; + context?: ErrorContext; +} +export class ReportableError extends Error { + private options?: ReportableErrorOptions; + constructor(options?: ReportableErrorOptions) { + super(options ? (options.error as Error).message : 'NO_MESSAGE'); + this.options = options; + this.init(); + } + + private init() { + if (this.options) { + errorService.reportError(this.options.error as Error, this.options.context || {}); + } + } +} + +export class DisplayableError extends ReportableError { + public userFriendlyMessage: string; + constructor({ userFriendlyMessage, errorToReport }: { userFriendlyMessage: string; errorToReport?: Error }) { + super({ + error: errorToReport, + }); + + this.userFriendlyMessage = userFriendlyMessage; + } +} diff --git a/src/services/common/errors/index.ts b/src/services/common/errors/index.ts new file mode 100644 index 000000000..8a185aaec --- /dev/null +++ b/src/services/common/errors/index.ts @@ -0,0 +1 @@ +export * from './base'; diff --git a/yarn.lock b/yarn.lock index 837995a5e..33bc84e9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1823,6 +1823,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.7.7": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.14.0": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" @@ -1837,13 +1844,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.7.7": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" - integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.0.0", "@babel/template@^7.16.0", "@babel/template@^7.3.3", "@babel/template@^7.8.6": version "7.16.0" resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz" @@ -2545,6 +2545,13 @@ dependencies: "@sinclair/typebox" "^0.24.1" +"@jest/schemas@^29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" + integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== + dependencies: + "@sinclair/typebox" "^0.24.1" + "@jest/source-map@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" @@ -3312,6 +3319,32 @@ merge-deep "^3.0.2" svgo "^1.2.2" +"@testing-library/jest-native@^4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@testing-library/jest-native/-/jest-native-4.0.12.tgz#9669a2456bf8f7ac907fca879d157fd0f29e6cb8" + integrity sha512-SjH3mLpYPLt14F2av98172nbGHrOlThKWxbSQrc9ZOsgl8mlMvWkQnFEheQooiLpZwrkoi+P48+dDMU7VaRR3A== + dependencies: + chalk "^4.1.2" + jest-diff "^29.0.1" + jest-matcher-utils "^29.0.1" + pretty-format "^29.0.1" + redent "^3.0.0" + +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + +"@testing-library/react-native@^11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-11.1.0.tgz#50aaa9c026e4beb02c07607fb0db5f4478cdd625" + integrity sha512-syVlE9fM0tZF4MmEE09R4BoGRH6lMNAbozuluGjS2HT8rXt3unRXb8GwpLmT+xCTq9+1lnQiXobTJm8/w12Zbg== + dependencies: + pretty-format "^29.0.3" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -5805,6 +5838,11 @@ diff-sequences@^28.1.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== +diff-sequences@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.0.0.tgz#bae49972ef3933556bcb0800b72e8579d19d9e4f" + integrity sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -8113,6 +8151,16 @@ jest-diff@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" +jest-diff@^29.0.1, jest-diff@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.0.3.tgz#41cc02409ad1458ae1bf7684129a3da2856341ac" + integrity sha512-+X/AIF5G/vX9fWK+Db9bi9BQas7M9oBME7egU7psbn4jlszLFCu0dW63UgeE6cs/GANq4fLaT+8sGHQQ0eCUfg== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.0.0" + jest-get-type "^29.0.0" + pretty-format "^29.0.3" + jest-docblock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" @@ -8186,6 +8234,11 @@ jest-get-type@^28.0.2: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== +jest-get-type@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.0.0.tgz#843f6c50a1b778f7325df1129a0fd7aa713aef80" + integrity sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw== + jest-haste-map@^26.5.2, jest-haste-map@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" @@ -8278,6 +8331,16 @@ jest-matcher-utils@^28.0.0: jest-get-type "^28.0.2" pretty-format "^28.1.3" +jest-matcher-utils@^29.0.1: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.0.3.tgz#b8305fd3f9e27cdbc210b21fc7dbba92d4e54560" + integrity sha512-RsR1+cZ6p1hDV4GSCQTg+9qjeotQCgkaleIKLK7dm+U4V/H2bWedU3RAtLm8+mANzZ7eDV33dMar4pejd7047w== + dependencies: + chalk "^4.0.0" + jest-diff "^29.0.3" + jest-get-type "^29.0.0" + pretty-format "^29.0.3" + jest-message-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" @@ -10521,6 +10584,15 @@ pretty-format@^28.0.0, pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.0.1, pretty-format@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.0.3.tgz#23d5f8cabc9cbf209a77d49409d093d61166a811" + integrity sha512-cHudsvQr1K5vNVLbvYF/nv3Qy/F/BcEKxGuIeMiVMRHxPOO1RxXooP8g/ZrwAp7Dx+KdMZoOc7NxLHhMrP2f9Q== + dependencies: + "@jest/schemas" "^29.0.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + prettysize@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/prettysize/-/prettysize-2.0.0.tgz" @@ -10798,6 +10870,13 @@ react-dom@17.0.1: object-assign "^4.1.1" scheduler "^0.20.1" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-freeze@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.0.tgz#b21c65fe1783743007c8c9a2952b1c8879a77354" From 2d22291b235f3d2454d6dc2cb5d684dd2996784c Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 19 Sep 2022 11:45:40 +0200 Subject: [PATCH 2/2] [_]: Use async/await syntax for better readability --- src/hooks/common/useUseCase/useUseCase.tsx | 30 ++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/hooks/common/useUseCase/useUseCase.tsx b/src/hooks/common/useUseCase/useUseCase.tsx index 16c7d8883..5531f17c0 100644 --- a/src/hooks/common/useUseCase/useUseCase.tsx +++ b/src/hooks/common/useUseCase/useUseCase.tsx @@ -44,23 +44,21 @@ export function useUseCase( if (state.data || state.error) { resetState(); } - - useCase() - .then((result) => { - setState({ - ...state, - data: result, - loading: false, - }); - }) - .catch((error) => { - processError(error); - setState({ - ...state, - error, - loading: false, - }); + try { + const result = await useCase(); + setState({ + ...state, + data: result, + loading: false, + }); + } catch (error) { + processError(error); + setState({ + ...state, + error: error as E, + loading: false, }); + } }; return [state.data, state.loading, state.error, executeUseCase]; }