Skip to content

Commit a603d71

Browse files
committed
feat: implement free trial conversion optimization with A/B testing (Smartdevs17#558)
1 parent a1f0795 commit a603d71

8 files changed

Lines changed: 1586 additions & 0 deletions

File tree

backend/services/trialService.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import {
2+
TrialConfig,
3+
ABTestAssignment,
4+
ConversionFunnelEvent,
5+
TrialReminderSchedule,
6+
TrialStatus,
7+
TrialDuration,
8+
TrialFeatureAccess,
9+
PaymentRequirement,
10+
} from '../../src/types/trial';
11+
12+
interface BackendTrialConfig {
13+
id: string;
14+
subscriptionId: string;
15+
duration: TrialDuration;
16+
featureAccess: TrialFeatureAccess;
17+
paymentRequirement: PaymentRequirement;
18+
abTestId?: string;
19+
status: TrialStatus;
20+
startDate?: string;
21+
endDate?: string;
22+
convertedAt?: string;
23+
reminderScheduleId?: string;
24+
metadata?: Record<string, any>;
25+
createdAt: string;
26+
updatedAt: string;
27+
}
28+
29+
interface BackendABTestAssignment {
30+
id: string;
31+
abTestId: string;
32+
userId: string;
33+
variantName: string;
34+
assignedAt: string;
35+
cohort?: string;
36+
}
37+
38+
interface BackendConversionFunnelEvent {
39+
id: string;
40+
trialConfigId: string;
41+
eventType: ConversionFunnelEvent['eventType'];
42+
userId: string;
43+
variantName?: string;
44+
timestamp: string;
45+
metadata?: Record<string, any>;
46+
}
47+
48+
export class BackendTrialService {
49+
private trialConfigs: Map<string, BackendTrialConfig> = new Map();
50+
private assignments: Map<string, BackendABTestAssignment> = new Map();
51+
private funnelEvents: Map<string, BackendConversionFunnelEvent> = new Map();
52+
53+
createTrialConfig(
54+
subscriptionId: string,
55+
duration: TrialDuration,
56+
featureAccess: TrialFeatureAccess,
57+
paymentRequirement: PaymentRequirement,
58+
abTestId?: string
59+
): BackendTrialConfig {
60+
const now = new Date().toISOString();
61+
const endDate = new Date();
62+
switch (duration) {
63+
case TrialDuration.SEVEN_DAYS:
64+
endDate.setDate(endDate.getDate() + 7);
65+
break;
66+
case TrialDuration.FOURTEEN_DAYS:
67+
endDate.setDate(endDate.getDate() + 14);
68+
break;
69+
case TrialDuration.TWENTY_ONE_DAYS:
70+
endDate.setDate(endDate.getDate() + 21);
71+
break;
72+
case TrialDuration.THIRTY_DAYS:
73+
endDate.setDate(endDate.getDate() + 30);
74+
break;
75+
}
76+
77+
const config: BackendTrialConfig = {
78+
id: `${subscriptionId}-${Date.now()}`,
79+
subscriptionId,
80+
duration,
81+
featureAccess,
82+
paymentRequirement,
83+
abTestId,
84+
status: TrialStatus.ACTIVE,
85+
startDate: now,
86+
endDate: endDate.toISOString(),
87+
createdAt: now,
88+
updatedAt: now,
89+
};
90+
91+
this.trialConfigs.set(config.id, config);
92+
return config;
93+
}
94+
95+
validateTrialConfig(config: Partial<BackendTrialConfig>): { valid: boolean; errors: string[] } {
96+
const errors: string[] = [];
97+
98+
if (!config.subscriptionId) {
99+
errors.push('Subscription ID is required');
100+
}
101+
if (!config.duration) {
102+
errors.push('Trial duration is required');
103+
}
104+
if (!config.featureAccess) {
105+
errors.push('Feature access is required');
106+
}
107+
if (!config.paymentRequirement) {
108+
errors.push('Payment requirement is required');
109+
}
110+
111+
return { valid: errors.length === 0, errors };
112+
}
113+
114+
assignABTest(abTestId: string, userId: string, variantName: string, cohort?: string): BackendABTestAssignment {
115+
const assignment: BackendABTestAssignment = {
116+
id: `${abTestId}-${userId}-${Date.now()}`,
117+
abTestId,
118+
userId,
119+
variantName,
120+
assignedAt: new Date().toISOString(),
121+
cohort,
122+
};
123+
124+
this.assignments.set(assignment.id, assignment);
125+
return assignment;
126+
}
127+
128+
processFunnelEvent(event: Omit<BackendConversionFunnelEvent, 'id' | 'timestamp'>): BackendConversionFunnelEvent {
129+
const funnelEvent: BackendConversionFunnelEvent = {
130+
...event,
131+
id: `evt-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
132+
timestamp: new Date().toISOString(),
133+
};
134+
135+
this.funnelEvents.set(funnelEvent.id, funnelEvent);
136+
return funnelEvent;
137+
}
138+
139+
getTrialConfig(id: string): BackendTrialConfig | undefined {
140+
return this.trialConfigs.get(id);
141+
}
142+
143+
getTrialConfigBySubscription(subscriptionId: string): BackendTrialConfig | undefined {
144+
for (const config of this.trialConfigs.values()) {
145+
if (config.subscriptionId === subscriptionId) {
146+
return config;
147+
}
148+
}
149+
return undefined;
150+
}
151+
152+
getAssignmentsForTest(abTestId: string): BackendABTestAssignment[] {
153+
return Array.from(this.assignments.values()).filter((a) => a.abTestId === abTestId);
154+
}
155+
156+
getFunnelEvents(trialConfigId: string): BackendConversionFunnelEvent[] {
157+
return Array.from(this.funnelEvents.values()).filter((e) => e.trialConfigId === trialConfigId);
158+
}
159+
160+
getConversionStats(abTestId?: string): { totalTrials: number; convertedTrials: number; conversionRate: number } {
161+
const configs = abTestId
162+
? Array.from(this.trialConfigs.values()).filter((c) => c.abTestId === abTestId)
163+
: Array.from(this.trialConfigs.values());
164+
165+
const totalTrials = configs.length;
166+
const convertedTrials = configs.filter((c) => c.status === TrialStatus.CONVERTED).length;
167+
const conversionRate = totalTrials > 0 ? convertedTrials / totalTrials : 0;
168+
169+
return { totalTrials, convertedTrials, conversionRate };
170+
}
171+
172+
convertTrial(trialId: string): BackendTrialConfig | undefined {
173+
const config = this.trialConfigs.get(trialId);
174+
if (!config) return undefined;
175+
176+
config.status = TrialStatus.CONVERTED;
177+
config.convertedAt = new Date().toISOString();
178+
config.updatedAt = new Date().toISOString();
179+
return config;
180+
}
181+
182+
expireTrial(trialId: string): BackendTrialConfig | undefined {
183+
const config = this.trialConfigs.get(trialId);
184+
if (!config) return undefined;
185+
186+
config.status = TrialStatus.EXPIRED;
187+
config.updatedAt = new Date().toISOString();
188+
return config;
189+
}
190+
}
191+
192+
export const backendTrialService = new BackendTrialService();

src/navigation/AppNavigator.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const PaymentMethodsScreen = lazyScreen(() =>
7777
}))
7878
);
7979
const AnalyticsDashboard = lazyScreen(() => import('../../app/screens/AnalyticsDashboard'));
80+
const TrialDetailsScreen = lazyScreen(() => import('../screens/TrialDetailsScreen'));
8081

8182
const Tab = createBottomTabNavigator<TabParamList>();
8283
const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -199,6 +200,11 @@ const HomeStack = () => (
199200
component={IntegrationGuidesScreen}
200201
options={{ headerShown: false }}
201202
/>
203+
<Stack.Screen
204+
name="TrialDetails"
205+
component={TrialDetailsScreen}
206+
options={{ title: 'Trial Details', headerShown: true }}
207+
/>
202208
</Stack.Navigator>
203209
);
204210

src/navigation/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type RootStackParamList = {
4848
ChangePlan: { subscriptionId: string };
4949
PaymentMethods: undefined;
5050
AnalyticsDashboard: undefined;
51+
TrialDetails: undefined;
5152
NotFound: { reason?: string };
5253
};
5354

0 commit comments

Comments
 (0)