From c56f26971975d7a81a7bf437a34b4a4450ab30c8 Mon Sep 17 00:00:00 2001 From: tsyirvo Date: Sat, 14 Feb 2026 16:17:25 +0100 Subject: [PATCH 1/3] feat: misc code updates --- .rnstorybook/main.ts | 2 +- __mocks__/expo-image.tsx | 3 +- __mocks__/react-native-mmkv.ts | 8 ++-- app.config.ts | 5 +-- src/domain/constants/config.ts | 2 + .../SubscriptionContext.ts | 2 + .../SubscriptionContextProvider.tsx | 24 ++++++++-- src/infra/analytics/analytics.ts | 1 - src/infra/date/date.ts | 4 +- src/infra/date/date.types.ts | 44 +++++++++---------- src/infra/date/hooks/useDates.ts | 4 +- src/infra/logger/logger.types.ts | 2 +- src/infra/monitoring/errorMonitoring.ts | 9 ++-- src/infra/monitoring/performanceMonitoring.ts | 9 ++-- src/infra/purchase/purchase.ts | 4 ++ src/infra/storage/productTrackingStorage.ts | 2 +- .../appUpdateNeeded/AppUpdateNeeded.tsx | 5 +-- .../StoreUpdateAvailableBanner.tsx | 5 +-- src/shared/hooks/useAppState.ts | 2 +- 19 files changed, 73 insertions(+), 64 deletions(-) diff --git a/.rnstorybook/main.ts b/.rnstorybook/main.ts index b216da72..70642504 100644 --- a/.rnstorybook/main.ts +++ b/.rnstorybook/main.ts @@ -1,4 +1,4 @@ -import { StorybookConfig } from '@storybook/react-native'; +import type { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: ['../src/**/*.stories.?(ts|tsx|js|jsx)'], diff --git a/__mocks__/expo-image.tsx b/__mocks__/expo-image.tsx index f8bd7167..e467b6fd 100644 --- a/__mocks__/expo-image.tsx +++ b/__mocks__/expo-image.tsx @@ -1,7 +1,8 @@ +import type { ViewProps } from 'react-native'; import { View } from 'react-native'; // Mock the Image component from expo-image -const Image = (props: any) => { +const Image = (props: ViewProps) => { return ; }; diff --git a/__mocks__/react-native-mmkv.ts b/__mocks__/react-native-mmkv.ts index dff0b16c..26548cd1 100644 --- a/__mocks__/react-native-mmkv.ts +++ b/__mocks__/react-native-mmkv.ts @@ -19,14 +19,14 @@ const createMockStorage = () => { return existed; }, clearAll: () => { - Object.keys(data).forEach((key) => delete data[key]); + for (const key of Object.keys(data)) { + delete data[key]; + } }, }; }; -export const createMMKV = jest - .fn() - .mockImplementation(() => createMockStorage()); +export const createMMKV = jest.fn().mockImplementation(() => createMockStorage()); export type MMKV = ReturnType; diff --git a/app.config.ts b/app.config.ts index 98a09700..09d5cfff 100644 --- a/app.config.ts +++ b/app.config.ts @@ -100,10 +100,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ ios: { supportsTablet: false, bundleIdentifier: Env.BUNDLE_ID, - // TODO(prod): Add correct associated domain config - associatedDomains: ['applinks:rnstarter.onelink.me'], - // TODO(prod): Add correct app store URL - appStoreUrl: `https://apps.apple.com/app/XXX/${Env.ITUNES_ITEM_ID}`, + appStoreUrl: `https://apps.apple.com/app/${Env.ITUNES_ITEM_ID}`, infoPlist: { UIBackgroundModes: ['remote-notification'], CFBundleAllowMixedLocalizations: true, diff --git a/src/domain/constants/config.ts b/src/domain/constants/config.ts index 79533159..6f288b44 100644 --- a/src/domain/constants/config.ts +++ b/src/domain/constants/config.ts @@ -16,6 +16,7 @@ const androidVersionCode = Constants.expoConfig?.android?.versionCode const runtimeVersion = Constants.expoConfig?.runtimeVersion; const iosBundleIdentifier = Constants.expoConfig?.ios?.bundleIdentifier ?? ''; const androidPackageName = Constants.expoConfig?.android?.package ?? ''; +const itunesItemId = Env.ITUNES_ITEM_ID; const apiURL = Env.API_URL; const isStorybookEnabled = Env.STORYBOOK_ENABLED === 'true'; @@ -36,6 +37,7 @@ export const config = { buildNumber: IS_IOS ? iosbuildNumber : androidVersionCode, runtimeVersion, bundleId: IS_IOS ? iosBundleIdentifier : androidPackageName, + itunesItemId, apiURL, isStorybookEnabled, // SDKs diff --git a/src/domain/contexts/subscriptionContext/SubscriptionContext.ts b/src/domain/contexts/subscriptionContext/SubscriptionContext.ts index 10ef71fd..5ff24419 100644 --- a/src/domain/contexts/subscriptionContext/SubscriptionContext.ts +++ b/src/domain/contexts/subscriptionContext/SubscriptionContext.ts @@ -4,9 +4,11 @@ import { PurchasesOffering } from 'react-native-purchases'; const SubscriptionContext = createContext<{ isPayingUser: boolean | null; offeringToDisplay: PurchasesOffering | null; + handleSetIsPayingUser: (value: boolean) => void; }>({ isPayingUser: null, offeringToDisplay: null, + handleSetIsPayingUser: () => null, }); export default SubscriptionContext; diff --git a/src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx b/src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx index 3d15340e..608a5fdd 100644 --- a/src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx +++ b/src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { PurchasesOffering } from 'react-native-purchases'; import { hasActiveEntitlements } from '$domain/subscription'; @@ -7,6 +7,8 @@ import { Logger } from '$infra/logger'; import { Purchase } from '$infra/purchase'; import { useRunOnMount } from '$shared/hooks'; +import { useAuthContext } from '../authContext'; + import SubscriptionContext from './SubscriptionContext'; interface SubscriptionContextProviderProps { @@ -20,11 +22,20 @@ export const SubscriptionContextProvider = ({ const [offeringToDisplay, setOfferingToDisplay] = useState(null); + const { user } = useAuthContext(); + const { getFlagPayloadSync } = useGetRemoteConfigSync(); - useRunOnMount(() => { + useEffect(() => { const fetchIsPayingUser = async () => { + if (!user) { + setIsPayingUser(false); + + return; + } + try { + await Purchase.setUser(user); const isPayingUser = await Purchase.isPayingUser(); setIsPayingUser(isPayingUser); @@ -40,7 +51,7 @@ export const SubscriptionContextProvider = ({ }; void fetchIsPayingUser(); - }); + }, [user]); useRunOnMount(() => { return Purchase.customerListener((customerInfo) => { @@ -81,12 +92,17 @@ export const SubscriptionContextProvider = ({ void fetchOfferingToDisplay(); }); + const handleSetIsPayingUser = useCallback((value: boolean) => { + setIsPayingUser(value); + }, []); + const value = useMemo( () => ({ isPayingUser, offeringToDisplay, + handleSetIsPayingUser, }), - [isPayingUser, offeringToDisplay], + [isPayingUser, offeringToDisplay, handleSetIsPayingUser], ); return ( diff --git a/src/infra/analytics/analytics.ts b/src/infra/analytics/analytics.ts index 1f73809c..23fbefa3 100644 --- a/src/infra/analytics/analytics.ts +++ b/src/infra/analytics/analytics.ts @@ -9,7 +9,6 @@ class AnalyticsClass { } /* ***** ***** User related ***** ***** */ - // TODO(prod): Add user properties setUser(user: User) { productTrackingClient.identify(user.id, { email: user.email, diff --git a/src/infra/date/date.ts b/src/infra/date/date.ts index 5ae69909..76c8dbac 100644 --- a/src/infra/date/date.ts +++ b/src/infra/date/date.ts @@ -3,7 +3,7 @@ import dayjsLocalizedFormat from 'dayjs/plugin/localizedFormat'; import relativeTime from 'dayjs/plugin/relativeTime'; import dayjsUtc from 'dayjs/plugin/utc'; -import type { DateFormats } from './date.types'; +import type { DateFormat } from './date.types'; import 'dayjs/locale/fr'; import 'dayjs/locale/en'; @@ -25,7 +25,7 @@ export const formatDate = ({ format, }: { date: dayjs.ConfigType; - format: DateFormats; + format: DateFormat; }) => dayjs.utc(date).format(format); export const formatDateRelative = ({ diff --git a/src/infra/date/date.types.ts b/src/infra/date/date.types.ts index 44b66d83..213f6f58 100644 --- a/src/infra/date/date.types.ts +++ b/src/infra/date/date.types.ts @@ -1,22 +1,22 @@ -export enum DateFormats { - /** 8:02 PM */ - TIME = 'LT', - /** 08/16/2018 */ - SHORT_DATE = 'L', - /** Aug 16, 2018 */ - MEDIUM_DATE = 'll', - /** August 16, 2018 */ - LONG_DATE = 'LL', - /** August 16, 2018 8:02 PM */ - LONG_DATE_WITH_TIME = 'LLL', - /** Sunday-Saturday */ - DAY = 'dddd', - /** January-December */ - MONTH = 'MMMM', - /** 16 Aout 2018 */ - DATE_FR = 'DD MMMM YYYY', - /** August 16, 2018 */ - DATE_EN = 'MMM Do, YYYY', - /** August 2018 */ - DATE_WITHOUT_DAY = 'MMMM YYYY', -} +export const DateFormats = { + /** 3:30 PM (US) | 15:30 (FR) */ + TIME: 'LT', + /** 01/11/2026 (US) | 11/01/2026 (FR) */ + SHORT_DATE: 'L', + /** Jan 11, 2026 (US) | 11 janv. 2026 (FR) */ + MEDIUM_DATE: 'll', + /** January 11, 2026 (US) | 11 janvier 2026 (FR) */ + LONG_DATE: 'LL', + /** January 11, 2026 3:30 PM (US) | 11 janvier 2026 15:30 (FR) */ + LONG_DATE_WITH_TIME: 'LLL', + /** 1/11/26 - 3:30 PM (US) | 11/1/26 - 15:30 (FR) */ + SHORT_DATE_WITH_TIME: 'l - LT', + /** Sunday (US) | dimanche (FR) */ + DAY: 'dddd', + /** January (US) | janvier (FR) */ + MONTH: 'MMMM', + /** January 2026 (US) | janvier 2026 (FR) */ + MONTH_YEAR: 'MMMM YYYY', +} as const; + +export type DateFormat = (typeof DateFormats)[keyof typeof DateFormats]; diff --git a/src/infra/date/hooks/useDates.ts b/src/infra/date/hooks/useDates.ts index 7ecee479..8b1ae343 100644 --- a/src/infra/date/hooks/useDates.ts +++ b/src/infra/date/hooks/useDates.ts @@ -7,14 +7,14 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { formatDate, formatDateRelative } from '../date'; -import { DateFormats } from '../date.types'; +import { DateFormat } from '../date.types'; export const useFormatDate = ({ date, format, }: { date: dayjs.ConfigType; - format: DateFormats; + format: DateFormat; }): string => { const { i18n } = useTranslation(); diff --git a/src/infra/logger/logger.types.ts b/src/infra/logger/logger.types.ts index 9afcf1b4..20f492ca 100644 --- a/src/infra/logger/logger.types.ts +++ b/src/infra/logger/logger.types.ts @@ -1,4 +1,4 @@ -import type { SeverityLevel } from '@sentry/core'; +import type { SeverityLevel } from '@sentry/react-native'; /* ***** ***** Toast Message ***** ***** */ diff --git a/src/infra/monitoring/errorMonitoring.ts b/src/infra/monitoring/errorMonitoring.ts index 8d50c07d..3c50fe10 100644 --- a/src/infra/monitoring/errorMonitoring.ts +++ b/src/infra/monitoring/errorMonitoring.ts @@ -1,8 +1,5 @@ -/* eslint-disable @typescript-eslint/no-deprecated */ - -import type { Event, Scope } from '@sentry/react-native'; + import type { Event, Scope } from '@sentry/react-native'; import * as Sentry from '@sentry/react-native'; -import type { Breadcrumb, CaptureContext, SeverityLevel } from '@sentry/types'; import * as Application from 'expo-application'; import Constants from 'expo-constants'; import * as Device from 'expo-device'; @@ -107,7 +104,7 @@ class ErrorMonitoringClass { Sentry.captureException(exception); } - message(message: string, context?: CaptureContext | SeverityLevel) { + message(message: string, context?: Parameters[1]) { Sentry.captureMessage(message, context); } @@ -119,7 +116,7 @@ class ErrorMonitoringClass { Sentry.setContext(name, context); } - breadcrumbs(breadcrumb: Breadcrumb) { + breadcrumbs(breadcrumb: Sentry.Breadcrumb) { Sentry.addBreadcrumb(breadcrumb); } diff --git a/src/infra/monitoring/performanceMonitoring.ts b/src/infra/monitoring/performanceMonitoring.ts index 1db3a89e..3673db6a 100644 --- a/src/infra/monitoring/performanceMonitoring.ts +++ b/src/infra/monitoring/performanceMonitoring.ts @@ -1,17 +1,14 @@ -/* eslint-disable @typescript-eslint/no-deprecated */ - import * as Sentry from '@sentry/react-native'; -import type { StartSpanOptions, Span } from '@sentry/types'; class PerformanceMonitoringClass { startTransaction( - context: StartSpanOptions, - callback: (span: Span | undefined) => T, + context: Parameters[0], + callback: (span: Sentry.Span | undefined) => T, ) { Sentry.startSpan(context, callback); } - startIndependentTransaction(context: StartSpanOptions) { + startIndependentTransaction(context: Parameters[0]) { Sentry.startInactiveSpan(context); } } diff --git a/src/infra/purchase/purchase.ts b/src/infra/purchase/purchase.ts index c5c6e5e6..1b648b5e 100644 --- a/src/infra/purchase/purchase.ts +++ b/src/infra/purchase/purchase.ts @@ -71,6 +71,10 @@ class PurchaseClass { await RevenueCat.restorePurchases(); } + async syncPurchases() { + await RevenueCat.syncPurchases(); + } + async makePurchase(purchasedPackage: PurchasesPackage) { try { await RevenueCat.purchasePackage(purchasedPackage); diff --git a/src/infra/storage/productTrackingStorage.ts b/src/infra/storage/productTrackingStorage.ts index 53fbd96d..70b8c04e 100644 --- a/src/infra/storage/productTrackingStorage.ts +++ b/src/infra/storage/productTrackingStorage.ts @@ -2,7 +2,7 @@ import { createMMKV } from 'react-native-mmkv'; import { storageKeys } from '$domain/constants'; -export const ProductTrackingStorage = createMMKV({ +const ProductTrackingStorage = createMMKV({ id: storageKeys.productTrackingStorage.id, }); diff --git a/src/shared/components/appUpdateNeeded/AppUpdateNeeded.tsx b/src/shared/components/appUpdateNeeded/AppUpdateNeeded.tsx index 1027c8fa..17715d24 100644 --- a/src/shared/components/appUpdateNeeded/AppUpdateNeeded.tsx +++ b/src/shared/components/appUpdateNeeded/AppUpdateNeeded.tsx @@ -38,12 +38,9 @@ export const AppUpdateNeeded = () => { const onPress = async () => { try { - // TODO(prod): Replace with real iTunes item ID - const itunesItemId = ''; - await Linking.openURL( IS_IOS - ? `https://apps.apple.com/app/apple-store/id${itunesItemId}` + ? `https://apps.apple.com/app/${config.itunesItemId}` : `market://details?id=${config.bundleId}&showAllReviews=true`, ); } catch (error) { diff --git a/src/shared/components/storeUpdateAvailableBanner/StoreUpdateAvailableBanner.tsx b/src/shared/components/storeUpdateAvailableBanner/StoreUpdateAvailableBanner.tsx index b48c8f77..976fd09f 100644 --- a/src/shared/components/storeUpdateAvailableBanner/StoreUpdateAvailableBanner.tsx +++ b/src/shared/components/storeUpdateAvailableBanner/StoreUpdateAvailableBanner.tsx @@ -11,12 +11,9 @@ export const StoreUpdateAvailableBanner = () => { const onPress = async () => { try { - // TODO(prod): Replace with real iTunes item ID - const itunesItemId = ''; - await Linking.openURL( IS_IOS - ? `https://apps.apple.com/app/apple-store/id${itunesItemId}` + ? `https://apps.apple.com/app/${config.itunesItemId}` : `market://details?id=${config.bundleId}&showAllReviews=true`, ); } catch (error) { diff --git a/src/shared/hooks/useAppState.ts b/src/shared/hooks/useAppState.ts index 1442ed80..599efbbf 100644 --- a/src/shared/hooks/useAppState.ts +++ b/src/shared/hooks/useAppState.ts @@ -37,5 +37,5 @@ export const useAppState = ({ return () => { listener.remove(); }; - }, [appState, handleAppStateChange]); + }, [handleAppStateChange]); }; From befa5033eaaea2e9730ec1f07805118aeb9999c3 Mon Sep 17 00:00:00 2001 From: tsyirvo Date: Sun, 15 Feb 2026 16:11:31 +0100 Subject: [PATCH 2/3] chore: handle PR comments --- __mocks__/expo-image.tsx | 7 ++++--- __mocks__/react-native-mmkv.ts | 4 +++- src/infra/monitoring/errorMonitoring.ts | 7 +++++-- src/infra/monitoring/performanceMonitoring.ts | 4 +++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/__mocks__/expo-image.tsx b/__mocks__/expo-image.tsx index e467b6fd..7f8f111d 100644 --- a/__mocks__/expo-image.tsx +++ b/__mocks__/expo-image.tsx @@ -1,9 +1,10 @@ -import type { ViewProps } from 'react-native'; import { View } from 'react-native'; +import type { ImageProps } from 'expo-image'; +import type { ViewProps } from 'react-native'; // Mock the Image component from expo-image -const Image = (props: ViewProps) => { - return ; +const Image = (props: ImageProps) => { + return ; }; export { Image }; diff --git a/__mocks__/react-native-mmkv.ts b/__mocks__/react-native-mmkv.ts index 26548cd1..b283c699 100644 --- a/__mocks__/react-native-mmkv.ts +++ b/__mocks__/react-native-mmkv.ts @@ -26,7 +26,9 @@ const createMockStorage = () => { }; }; -export const createMMKV = jest.fn().mockImplementation(() => createMockStorage()); +export const createMMKV = jest + .fn() + .mockImplementation(() => createMockStorage()); export type MMKV = ReturnType; diff --git a/src/infra/monitoring/errorMonitoring.ts b/src/infra/monitoring/errorMonitoring.ts index 3c50fe10..bf468441 100644 --- a/src/infra/monitoring/errorMonitoring.ts +++ b/src/infra/monitoring/errorMonitoring.ts @@ -1,4 +1,4 @@ - import type { Event, Scope } from '@sentry/react-native'; +import type { Event, Scope } from '@sentry/react-native'; import * as Sentry from '@sentry/react-native'; import * as Application from 'expo-application'; import Constants from 'expo-constants'; @@ -104,7 +104,10 @@ class ErrorMonitoringClass { Sentry.captureException(exception); } - message(message: string, context?: Parameters[1]) { + message( + message: string, + context?: Parameters[1], + ) { Sentry.captureMessage(message, context); } diff --git a/src/infra/monitoring/performanceMonitoring.ts b/src/infra/monitoring/performanceMonitoring.ts index 3673db6a..9e56b946 100644 --- a/src/infra/monitoring/performanceMonitoring.ts +++ b/src/infra/monitoring/performanceMonitoring.ts @@ -8,7 +8,9 @@ class PerformanceMonitoringClass { Sentry.startSpan(context, callback); } - startIndependentTransaction(context: Parameters[0]) { + startIndependentTransaction( + context: Parameters[0], + ) { Sentry.startInactiveSpan(context); } } From 111272fd1b4e008e27bd98f2b9ea09506c2ca5fc Mon Sep 17 00:00:00 2001 From: tsyirvo Date: Sun, 15 Feb 2026 16:21:05 +0100 Subject: [PATCH 3/3] refactor: return sentry spans --- src/infra/monitoring/performanceMonitoring.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infra/monitoring/performanceMonitoring.ts b/src/infra/monitoring/performanceMonitoring.ts index 9e56b946..4ecc782f 100644 --- a/src/infra/monitoring/performanceMonitoring.ts +++ b/src/infra/monitoring/performanceMonitoring.ts @@ -5,13 +5,13 @@ class PerformanceMonitoringClass { context: Parameters[0], callback: (span: Sentry.Span | undefined) => T, ) { - Sentry.startSpan(context, callback); + return Sentry.startSpan(context, callback); } startIndependentTransaction( context: Parameters[0], ) { - Sentry.startInactiveSpan(context); + return Sentry.startInactiveSpan(context); } }