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..7f8f111d 100644
--- a/__mocks__/expo-image.tsx
+++ b/__mocks__/expo-image.tsx
@@ -1,8 +1,10 @@
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: any) => {
- return ;
+const Image = (props: ImageProps) => {
+ return ;
};
export { Image };
diff --git a/__mocks__/react-native-mmkv.ts b/__mocks__/react-native-mmkv.ts
index dff0b16c..b283c699 100644
--- a/__mocks__/react-native-mmkv.ts
+++ b/__mocks__/react-native-mmkv.ts
@@ -19,7 +19,9 @@ const createMockStorage = () => {
return existed;
},
clearAll: () => {
- Object.keys(data).forEach((key) => delete data[key]);
+ for (const key of Object.keys(data)) {
+ delete data[key];
+ }
},
};
};
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..bf468441 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 * 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,10 @@ class ErrorMonitoringClass {
Sentry.captureException(exception);
}
- message(message: string, context?: CaptureContext | SeverityLevel) {
+ message(
+ message: string,
+ context?: Parameters[1],
+ ) {
Sentry.captureMessage(message, context);
}
@@ -119,7 +119,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..4ecc782f 100644
--- a/src/infra/monitoring/performanceMonitoring.ts
+++ b/src/infra/monitoring/performanceMonitoring.ts
@@ -1,18 +1,17 @@
-/* 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);
+ return Sentry.startSpan(context, callback);
}
- startIndependentTransaction(context: StartSpanOptions) {
- Sentry.startInactiveSpan(context);
+ startIndependentTransaction(
+ context: Parameters[0],
+ ) {
+ return 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]);
};