Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ab test pattern for premium introduction #1231

Merged
merged 25 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/entity/remote_config_parameter.codegen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ abstract class RemoteConfigKeys {
static const discountEntitlementOffsetDay = "discountEntitlementOffsetDay";
static const discountCountdownBoundaryHour = "discountCountdownBoundaryHour";
static const releasedVersion = "releasedVersion";
static const premiumIntroductionPattern = "premiumIntroductionPattern";
}

abstract class RemoteConfigParameterDefaultValues {
Expand All @@ -19,6 +20,8 @@ abstract class RemoteConfigParameterDefaultValues {
static const discountEntitlementOffsetDay = 2;
static const discountCountdownBoundaryHour = 48;
static const releasedVersion = "202407.29.133308";
// default(A) or B or C ...
static const premiumIntroductionPattern = "default";
}

@freezed
Expand All @@ -29,6 +32,7 @@ class RemoteConfigParameter with _$RemoteConfigParameter {
@Default(RemoteConfigParameterDefaultValues.trialDeadlineDateOffsetDay) int trialDeadlineDateOffsetDay,
@Default(RemoteConfigParameterDefaultValues.discountEntitlementOffsetDay) int discountEntitlementOffsetDay,
@Default(RemoteConfigParameterDefaultValues.discountCountdownBoundaryHour) int discountCountdownBoundaryHour,
@Default(RemoteConfigParameterDefaultValues.premiumIntroductionPattern) String premiumIntroductionPattern,
}) = _RemoteConfigParameter;
RemoteConfigParameter._();
factory RemoteConfigParameter.fromJson(Map<String, dynamic> json) => _$RemoteConfigParameterFromJson(json);
Expand Down
40 changes: 33 additions & 7 deletions lib/entity/remote_config_parameter.codegen.freezed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mixin _$RemoteConfigParameter {
int get trialDeadlineDateOffsetDay => throw _privateConstructorUsedError;
int get discountEntitlementOffsetDay => throw _privateConstructorUsedError;
int get discountCountdownBoundaryHour => throw _privateConstructorUsedError;
String get premiumIntroductionPattern => throw _privateConstructorUsedError;

Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
Expand All @@ -44,7 +45,8 @@ abstract class $RemoteConfigParameterCopyWith<$Res> {
bool skipInitialSetting,
int trialDeadlineDateOffsetDay,
int discountEntitlementOffsetDay,
int discountCountdownBoundaryHour});
int discountCountdownBoundaryHour,
String premiumIntroductionPattern});
}

/// @nodoc
Expand All @@ -66,6 +68,7 @@ class _$RemoteConfigParameterCopyWithImpl<$Res,
Object? trialDeadlineDateOffsetDay = null,
Object? discountEntitlementOffsetDay = null,
Object? discountCountdownBoundaryHour = null,
Object? premiumIntroductionPattern = null,
}) {
return _then(_value.copyWith(
isPaywallFirst: null == isPaywallFirst
Expand All @@ -88,6 +91,10 @@ class _$RemoteConfigParameterCopyWithImpl<$Res,
? _value.discountCountdownBoundaryHour
: discountCountdownBoundaryHour // ignore: cast_nullable_to_non_nullable
as int,
premiumIntroductionPattern: null == premiumIntroductionPattern
? _value.premiumIntroductionPattern
: premiumIntroductionPattern // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
Expand All @@ -106,7 +113,8 @@ abstract class _$$RemoteConfigParameterImplCopyWith<$Res>
bool skipInitialSetting,
int trialDeadlineDateOffsetDay,
int discountEntitlementOffsetDay,
int discountCountdownBoundaryHour});
int discountCountdownBoundaryHour,
String premiumIntroductionPattern});
}

/// @nodoc
Expand All @@ -126,6 +134,7 @@ class __$$RemoteConfigParameterImplCopyWithImpl<$Res>
Object? trialDeadlineDateOffsetDay = null,
Object? discountEntitlementOffsetDay = null,
Object? discountCountdownBoundaryHour = null,
Object? premiumIntroductionPattern = null,
}) {
return _then(_$RemoteConfigParameterImpl(
isPaywallFirst: null == isPaywallFirst
Expand All @@ -148,6 +157,10 @@ class __$$RemoteConfigParameterImplCopyWithImpl<$Res>
? _value.discountCountdownBoundaryHour
: discountCountdownBoundaryHour // ignore: cast_nullable_to_non_nullable
as int,
premiumIntroductionPattern: null == premiumIntroductionPattern
? _value.premiumIntroductionPattern
: premiumIntroductionPattern // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
Expand All @@ -164,7 +177,9 @@ class _$RemoteConfigParameterImpl extends _RemoteConfigParameter {
this.discountEntitlementOffsetDay =
RemoteConfigParameterDefaultValues.discountEntitlementOffsetDay,
this.discountCountdownBoundaryHour =
RemoteConfigParameterDefaultValues.discountCountdownBoundaryHour})
RemoteConfigParameterDefaultValues.discountCountdownBoundaryHour,
this.premiumIntroductionPattern =
RemoteConfigParameterDefaultValues.premiumIntroductionPattern})
: super._();

factory _$RemoteConfigParameterImpl.fromJson(Map<String, dynamic> json) =>
Expand All @@ -185,10 +200,13 @@ class _$RemoteConfigParameterImpl extends _RemoteConfigParameter {
@override
@JsonKey()
final int discountCountdownBoundaryHour;
@override
@JsonKey()
final String premiumIntroductionPattern;

@override
String toString() {
return 'RemoteConfigParameter(isPaywallFirst: $isPaywallFirst, skipInitialSetting: $skipInitialSetting, trialDeadlineDateOffsetDay: $trialDeadlineDateOffsetDay, discountEntitlementOffsetDay: $discountEntitlementOffsetDay, discountCountdownBoundaryHour: $discountCountdownBoundaryHour)';
return 'RemoteConfigParameter(isPaywallFirst: $isPaywallFirst, skipInitialSetting: $skipInitialSetting, trialDeadlineDateOffsetDay: $trialDeadlineDateOffsetDay, discountEntitlementOffsetDay: $discountEntitlementOffsetDay, discountCountdownBoundaryHour: $discountCountdownBoundaryHour, premiumIntroductionPattern: $premiumIntroductionPattern)';
}

@override
Expand All @@ -211,7 +229,11 @@ class _$RemoteConfigParameterImpl extends _RemoteConfigParameter {
(identical(other.discountCountdownBoundaryHour,
discountCountdownBoundaryHour) ||
other.discountCountdownBoundaryHour ==
discountCountdownBoundaryHour));
discountCountdownBoundaryHour) &&
(identical(other.premiumIntroductionPattern,
premiumIntroductionPattern) ||
other.premiumIntroductionPattern ==
premiumIntroductionPattern));
}

@JsonKey(ignore: true)
Expand All @@ -222,7 +244,8 @@ class _$RemoteConfigParameterImpl extends _RemoteConfigParameter {
skipInitialSetting,
trialDeadlineDateOffsetDay,
discountEntitlementOffsetDay,
discountCountdownBoundaryHour);
discountCountdownBoundaryHour,
premiumIntroductionPattern);

@JsonKey(ignore: true)
@override
Expand All @@ -245,7 +268,8 @@ abstract class _RemoteConfigParameter extends RemoteConfigParameter {
final bool skipInitialSetting,
final int trialDeadlineDateOffsetDay,
final int discountEntitlementOffsetDay,
final int discountCountdownBoundaryHour}) = _$RemoteConfigParameterImpl;
final int discountCountdownBoundaryHour,
final String premiumIntroductionPattern}) = _$RemoteConfigParameterImpl;
_RemoteConfigParameter._() : super._();

factory _RemoteConfigParameter.fromJson(Map<String, dynamic> json) =
Expand All @@ -262,6 +286,8 @@ abstract class _RemoteConfigParameter extends RemoteConfigParameter {
@override
int get discountCountdownBoundaryHour;
@override
String get premiumIntroductionPattern;
@override
@JsonKey(ignore: true)
_$$RemoteConfigParameterImplCopyWith<_$RemoteConfigParameterImpl>
get copyWith => throw _privateConstructorUsedError;
Expand Down
4 changes: 4 additions & 0 deletions lib/entity/remote_config_parameter.codegen.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:pilll/components/atoms/font.dart';
import 'package:pilll/components/atoms/text_color.dart';

class PremiumIntroductionFeatures extends StatelessWidget {
const PremiumIntroductionFeatures({super.key});

@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
alignment: AlignmentDirectional.topEnd,
children: [
Image.asset(
Platform.isIOS ? "images/ios-quick-record.gif" : "images/android-quick-record.gif",
),
Positioned(
right: -27,
top: -27,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
SvgPicture.asset("images/yellow_spike.svg"),
const Text(
"人気の\n機能",
style: TextStyle(
color: TextColor.primaryDarkBlue,
fontSize: 10,
fontFamily: FontFamily.japanese,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
],
),
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import 'package:async_value_group/async_value_group.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'package:flutter/material.dart';
import 'package:pilll/entity/user.codegen.dart';
import 'package:pilll/features/premium_introduction/ab_test/b/components/features.dart';
import 'package:pilll/features/premium_introduction/components/premium_introduction_discount.dart';
import 'package:pilll/utils/analytics.dart';
import 'package:pilll/components/atoms/button.dart';
import 'package:pilll/components/atoms/color.dart';
import 'package:pilll/components/molecules/indicator.dart';
import 'package:pilll/components/page/hud.dart';
import 'package:pilll/features/premium_introduction/components/premium_introduction_footer.dart';
import 'package:pilll/features/premium_introduction/components/premium_introduction_header.dart';
import 'package:pilll/features/premium_introduction/components/premium_user_thanks.dart';
import 'package:pilll/features/premium_introduction/components/purchase_buttons.dart';
import 'package:pilll/features/error/universal_error_page.dart';
import 'package:pilll/provider/user.dart';
import 'package:pilll/provider/root.dart';
import 'package:pilll/provider/purchase.dart';
import 'package:pilll/utils/links.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:url_launcher/url_launcher.dart';

class PremiumIntroductionSheetB extends HookConsumerWidget {
const PremiumIntroductionSheetB({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return AsyncValueGroup.group2(
ref.watch(purchaseOfferingsProvider),
ref.watch(userProvider),
).when(
data: (data) => PremiumIntroductionSheetBody(
offerings: data.$1,
user: data.$2,
),
error: (error, stackTrace) => UniversalErrorPage(
error: error,
reload: () {
ref.invalidate(purchaseOfferingsProvider);
ref.invalidate(refreshAppProvider);
},
child: null,
),
loading: () => const Indicator(),
);
}
}

class PremiumIntroductionSheetBody extends HookConsumerWidget {
final Offerings offerings;
final User user;

const PremiumIntroductionSheetBody({
super.key,
required this.offerings,
required this.user,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final offeringType = ref.watch(currentOfferingTypeProvider(user));
final monthlyPackage = ref.watch(monthlyPackageProvider(user));
final annualPackage = ref.watch(annualPackageProvider(user));
final monthlyPremiumPackage = ref.watch(monthlyPremiumPackageProvider(user));

final isLoading = useState(false);

return DraggableScrollableSheet(
initialChildSize: 0.8,
builder: (context, scrollController) {
return HUD(
shown: isLoading.value,
child: Scaffold(
body: Container(
padding: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
color: PilllColors.white,
child: Stack(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.transparent,
image: DecorationImage(
image: AssetImage("images/premium_background.png"),
fit: BoxFit.cover,
),
),
padding: const EdgeInsets.only(left: 40, right: 40, bottom: 40),
width: MediaQuery.of(context).size.width,
),
SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.only(bottom: 100, top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const PremiumIntroductionHeader(),
if (user.isPremium) ...[
const SizedBox(height: 32),
const PremiumUserThanksRow(),
],
if (!user.isPremium) ...[
if (user.hasDiscountEntitlement)
if (monthlyPremiumPackage != null)
PremiumIntroductionDiscountRow(
monthlyPremiumPackage: monthlyPremiumPackage,
discountEntitlementDeadlineDate: user.discountEntitlementDeadlineDate,
),
const SizedBox(height: 12),
PurchaseButtons(
offeringType: offeringType,
monthlyPackage: monthlyPackage,
annualPackage: annualPackage,
isLoading: isLoading,
),
],
const SizedBox(height: 24),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 8),
child: Align(
alignment: Alignment.centerLeft,
child: PremiumIntroductionFeatures(),
),
),
Align(
alignment: Alignment.center,
child: AlertButton(
onPressed: () async {
analytics.logEvent(name: "pressed_premium_functions_on_sheet2");
await launchUrl(Uri.parse(preimumLink));
},
text: "プレミアム機能の詳細を見る",
),
),
const SizedBox(height: 24),
PremiumIntroductionFotter(
isLoading: isLoading,
),
],
),
),
Positioned(
left: 7,
top: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.black),
onPressed: () => Navigator.of(context).pop(),
),
),
],
),
),
),
);
},
);
}
}
Loading
Loading