diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index adc2a922..4445be92 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -17,6 +17,7 @@ import { StyleSheet } from 'react-native-unistyles';
import { config } from '$domain/constants';
import { AuthContextProvider } from '$domain/contexts';
+import { SubscriptionContextProvider } from '$domain/contexts/subscriptionContext';
import { useAppFocusManager } from '$infra/api';
import { persistOptions, queryClient } from '$infra/api/queryClient';
import { ErrorMonitoring } from '$infra/monitoring';
@@ -81,29 +82,35 @@ const RootLayout = () => {
- <>
-
-
-
-
+
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
- >
+
+ >
+
diff --git a/src/domain/contexts/authContext/AuthContextProvider.tsx b/src/domain/contexts/authContext/AuthContextProvider.tsx
index 7cbb318d..535beeba 100644
--- a/src/domain/contexts/authContext/AuthContextProvider.tsx
+++ b/src/domain/contexts/authContext/AuthContextProvider.tsx
@@ -40,7 +40,7 @@ export const AuthContextProvider = ({ children }: AuthContextProviderProps) => {
ErrorMonitoring.setUser(user);
- await Purchase.setUser(user.id);
+ await Purchase.setUser(user);
Notifications.setUser(user.id);
Notifications.setUserEmail(user.email);
diff --git a/src/domain/contexts/subscriptionContext/SubscriptionContext.ts b/src/domain/contexts/subscriptionContext/SubscriptionContext.ts
new file mode 100644
index 00000000..10ef71fd
--- /dev/null
+++ b/src/domain/contexts/subscriptionContext/SubscriptionContext.ts
@@ -0,0 +1,12 @@
+import { createContext } from 'react';
+import { PurchasesOffering } from 'react-native-purchases';
+
+const SubscriptionContext = createContext<{
+ isPayingUser: boolean | null;
+ offeringToDisplay: PurchasesOffering | null;
+}>({
+ isPayingUser: null,
+ offeringToDisplay: null,
+});
+
+export default SubscriptionContext;
diff --git a/src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx b/src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
new file mode 100644
index 00000000..3d15340e
--- /dev/null
+++ b/src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
@@ -0,0 +1,97 @@
+import { useMemo, useState } from 'react';
+import { PurchasesOffering } from 'react-native-purchases';
+
+import { hasActiveEntitlements } from '$domain/subscription';
+import { OfferingFlagType, useGetRemoteConfigSync } from '$infra/featureFlags';
+import { Logger } from '$infra/logger';
+import { Purchase } from '$infra/purchase';
+import { useRunOnMount } from '$shared/hooks';
+
+import SubscriptionContext from './SubscriptionContext';
+
+interface SubscriptionContextProviderProps {
+ children: React.ReactNode;
+}
+
+export const SubscriptionContextProvider = ({
+ children,
+}: SubscriptionContextProviderProps) => {
+ const [isPayingUser, setIsPayingUser] = useState(null);
+ const [offeringToDisplay, setOfferingToDisplay] =
+ useState(null);
+
+ const { getFlagPayloadSync } = useGetRemoteConfigSync();
+
+ useRunOnMount(() => {
+ const fetchIsPayingUser = async () => {
+ try {
+ const isPayingUser = await Purchase.isPayingUser();
+
+ setIsPayingUser(isPayingUser);
+ } catch (error) {
+ Logger.error({
+ error,
+ level: 'warning',
+ message: 'Failed to fetch user subscription status',
+ });
+
+ setIsPayingUser(false);
+ }
+ };
+
+ void fetchIsPayingUser();
+ });
+
+ useRunOnMount(() => {
+ return Purchase.customerListener((customerInfo) => {
+ setIsPayingUser(hasActiveEntitlements(customerInfo));
+ });
+ });
+
+ useRunOnMount(() => {
+ const fetchOfferingToDisplay = async () => {
+ try {
+ const offering = await Purchase.getOfferings();
+ const payload = getFlagPayloadSync(
+ 'offering-to-display',
+ );
+
+ if (payload?.type === 'offering' && payload.offering) {
+ const remotelySelectedOffering = offering.all[payload.offering];
+
+ if (remotelySelectedOffering) {
+ setOfferingToDisplay(remotelySelectedOffering);
+ } else {
+ setOfferingToDisplay(offering.current);
+ }
+ } else {
+ setOfferingToDisplay(offering.current);
+ }
+ } catch (error) {
+ Logger.error({
+ error,
+ level: 'warning',
+ message: 'Failed to fetch offering to display',
+ });
+
+ setOfferingToDisplay(null);
+ }
+ };
+
+ void fetchOfferingToDisplay();
+ });
+
+ const value = useMemo(
+ () => ({
+ isPayingUser,
+ offeringToDisplay,
+ }),
+ [isPayingUser, offeringToDisplay],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/domain/contexts/subscriptionContext/index.ts b/src/domain/contexts/subscriptionContext/index.ts
new file mode 100644
index 00000000..0951b8de
--- /dev/null
+++ b/src/domain/contexts/subscriptionContext/index.ts
@@ -0,0 +1,2 @@
+export * from './SubscriptionContextProvider';
+export * from './useSubscriptionContext';
diff --git a/src/domain/contexts/subscriptionContext/useSubscriptionContext.ts b/src/domain/contexts/subscriptionContext/useSubscriptionContext.ts
new file mode 100644
index 00000000..98733c09
--- /dev/null
+++ b/src/domain/contexts/subscriptionContext/useSubscriptionContext.ts
@@ -0,0 +1,9 @@
+import { useContext } from 'react';
+
+import SubscriptionContext from './SubscriptionContext';
+
+export const useSubscriptionContext = () => {
+ const value = useContext(SubscriptionContext);
+
+ return value;
+};
diff --git a/src/domain/subscription/index.ts b/src/domain/subscription/index.ts
new file mode 100644
index 00000000..04bca77e
--- /dev/null
+++ b/src/domain/subscription/index.ts
@@ -0,0 +1 @@
+export * from './utils';
diff --git a/src/domain/subscription/utils/hasActiveEntitlements.ts b/src/domain/subscription/utils/hasActiveEntitlements.ts
new file mode 100644
index 00000000..87a96762
--- /dev/null
+++ b/src/domain/subscription/utils/hasActiveEntitlements.ts
@@ -0,0 +1,5 @@
+import { CustomerInfo } from 'react-native-purchases';
+
+export const hasActiveEntitlements = (customerInfo: CustomerInfo): boolean => {
+ return Object.entries(customerInfo.entitlements.active).length > 0;
+};
diff --git a/src/domain/subscription/utils/index.ts b/src/domain/subscription/utils/index.ts
new file mode 100644
index 00000000..dd850d65
--- /dev/null
+++ b/src/domain/subscription/utils/index.ts
@@ -0,0 +1 @@
+export * from './hasActiveEntitlements';
diff --git a/src/infra/analytics/analytics.ts b/src/infra/analytics/analytics.ts
index 272f46cf..1f73809c 100644
--- a/src/infra/analytics/analytics.ts
+++ b/src/infra/analytics/analytics.ts
@@ -41,16 +41,6 @@ class AnalyticsClass {
});
}
- /* ***** ***** Revenue ***** ***** */
-
- trackRevenue(revenueData: {
- productId: AnalyticsType.ProductIds;
- price: number;
- revenueType: AnalyticsType.RevenueTypes;
- }) {
- this.trackEvent('purchase', revenueData);
- }
-
/* ***** ***** Events ***** ***** */
trackEvent(
diff --git a/src/infra/analytics/analytics.types.ts b/src/infra/analytics/analytics.types.ts
index b13fa32e..d87954a6 100644
--- a/src/infra/analytics/analytics.types.ts
+++ b/src/infra/analytics/analytics.types.ts
@@ -24,16 +24,6 @@ export namespace AnalyticsType {
| 'deep-link-opened'
| 'purchase';
- export type ProductIds =
- | 'monthly-subscription'
- | 'monthly-subscription-30-off'
- | 'monthly-subscription-50-off'
- | 'yearly-subscription'
- | 'yearly-subscription-30-off'
- | 'yearly-subscription-50-off';
-
- export type RevenueTypes = 'purchase';
-
export type JsonType =
| string
| number
diff --git a/src/infra/featureFlags/defaultFlags.ts b/src/infra/featureFlags/defaultFlags.ts
index 645f188c..13d26656 100644
--- a/src/infra/featureFlags/defaultFlags.ts
+++ b/src/infra/featureFlags/defaultFlags.ts
@@ -1,6 +1,7 @@
import type {
AvailableFeatureFlags,
AvailableRemoteConfig,
+ OfferingFlagType,
VersionFlagType,
} from './featureFlags.types';
@@ -13,8 +14,9 @@ export const defaultFeatureFlags: Record<
export const defaultRemoteConfig: Record<
AvailableRemoteConfig,
- VersionFlagType
+ VersionFlagType | OfferingFlagType
> = {
'last-supported-app-version': { type: 'version', version: '2.0.0' },
'latest-released-app-version': { type: 'version', version: '2.1.0' },
+ 'offering-to-display': { type: 'offering', offering: 'default' },
};
diff --git a/src/infra/featureFlags/featureFlags.types.ts b/src/infra/featureFlags/featureFlags.types.ts
index 26c8b3e6..b4080bec 100644
--- a/src/infra/featureFlags/featureFlags.types.ts
+++ b/src/infra/featureFlags/featureFlags.types.ts
@@ -2,13 +2,19 @@ export type AvailableFeatureFlags = 'is-maintenance-mode';
export type AvailableRemoteConfig =
| 'last-supported-app-version'
- | 'latest-released-app-version';
+ | 'latest-released-app-version'
+ | 'offering-to-display';
export type BooleanFeatureFlags = Extract<
AvailableFeatureFlags,
'is-maintenance-mode'
>;
+export interface OfferingFlagType {
+ type: 'offering';
+ offering: string;
+}
+
export interface VersionFlagType {
type: 'version';
version: string;
diff --git a/src/infra/purchase/purchase.ts b/src/infra/purchase/purchase.ts
index aea63e16..c5c6e5e6 100644
--- a/src/infra/purchase/purchase.ts
+++ b/src/infra/purchase/purchase.ts
@@ -3,9 +3,13 @@ import type {
LOG_LEVEL,
PurchasesPackage,
} from 'react-native-purchases';
-import RevenueCat from 'react-native-purchases';
+import RevenueCat, {
+ LOG_LEVEL as PURCHASES_LOG_LEVEL,
+} from 'react-native-purchases';
import { IS_IOS, config } from '$domain/constants';
+import { User } from '$domain/entities';
+import { hasActiveEntitlements } from '$domain/subscription';
import { ErrorMonitoring } from '$infra/monitoring';
const API_KEY = IS_IOS
@@ -17,6 +21,8 @@ class PurchaseClass {
init() {
RevenueCat.configure({ apiKey: API_KEY });
+
+ void this.setLogLevel(PURCHASES_LOG_LEVEL.ERROR);
}
async setLogLevel(logLevel: LOG_LEVEL) {
@@ -25,8 +31,12 @@ class PurchaseClass {
/* ***** ***** User related ***** ***** */
- async setUser(appUserID: string) {
- await RevenueCat.logIn(appUserID);
+ async setUser(user: User) {
+ await RevenueCat.logIn(user.id);
+ await RevenueCat.setEmail(user.email);
+ await this.setAttributes({
+ $posthogUserId: user.id,
+ });
}
async clearUser() {
@@ -45,12 +55,16 @@ class PurchaseClass {
return await RevenueCat.getCustomerInfo();
}
+ async isPayingUser() {
+ const customerInfo = await this.getUserInformations();
+
+ return hasActiveEntitlements(customerInfo);
+ }
+
/* ***** ***** RevenueCat ***** ***** */
async getOfferings() {
- const offerings = await RevenueCat.getOfferings();
-
- return offerings.current;
+ return await RevenueCat.getOfferings();
}
async restorePurchases() {
@@ -69,6 +83,10 @@ class PurchaseClass {
customerListener(callback: (customerInfo: CustomerInfo) => void) {
RevenueCat.addCustomerInfoUpdateListener(callback);
+
+ return () => {
+ RevenueCat.removeCustomerInfoUpdateListener(callback);
+ };
}
}