Surface breakdown on storage bonus (#1481)

This commit is contained in:
Neeraj Gupta 2023-11-08 16:32:22 +05:30 committed by GitHub
commit aba7f5d4b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 326 additions and 49 deletions

View file

@ -187,6 +187,8 @@ class MessageLookup extends MessageLookupByLibrary {
static String m59(count) =>
"${Intl.plural(count, zero: '', one: '1 day', other: '${count} days')}";
static String m64(endDate) => "Valid till ${endDate}";
static String m60(email) => "Verify ${email}";
static String m61(email) => "We have sent a mail to <green>${email}</green>";
@ -218,6 +220,9 @@ class MessageLookup extends MessageLookupByLibrary {
"addLocation": MessageLookupByLibrary.simpleMessage("Add location"),
"addLocationButton": MessageLookupByLibrary.simpleMessage("Add"),
"addMore": MessageLookupByLibrary.simpleMessage("Add more"),
"addOnPageSubtitle":
MessageLookupByLibrary.simpleMessage("Break up of add on storage"),
"addOns": MessageLookupByLibrary.simpleMessage("Add ons"),
"addPhotos": MessageLookupByLibrary.simpleMessage("Add photos"),
"addSelected": MessageLookupByLibrary.simpleMessage("Add selected"),
"addToAlbum": MessageLookupByLibrary.simpleMessage("Add to album"),
@ -694,8 +699,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Group nearby photos"),
"hearUsExplanation": MessageLookupByLibrary.simpleMessage(
"We don\'t track app installs. It\'d help if you told us where you found us!"),
"hearUsHint": MessageLookupByLibrary.simpleMessage(
"friend, reddit, ad, search, etc."),
"hearUsWhereTitle": MessageLookupByLibrary.simpleMessage(
"How did you hear about Ente? (optional)"),
"hidden": MessageLookupByLibrary.simpleMessage("Hidden"),
@ -1325,6 +1328,7 @@ class MessageLookup extends MessageLookupByLibrary {
"useSelectedPhoto":
MessageLookupByLibrary.simpleMessage("Use selected photo"),
"usedSpace": MessageLookupByLibrary.simpleMessage("Used space"),
"validTill": m64,
"verificationFailedPleaseTryAgain":
MessageLookupByLibrary.simpleMessage(
"Verification failed, please try again"),
@ -1342,6 +1346,7 @@ class MessageLookup extends MessageLookupByLibrary {
"videoSmallCase": MessageLookupByLibrary.simpleMessage("video"),
"viewActiveSessions":
MessageLookupByLibrary.simpleMessage("View active sessions"),
"viewAddOnButton": MessageLookupByLibrary.simpleMessage("View Add-ons"),
"viewAll": MessageLookupByLibrary.simpleMessage("View all"),
"viewAllExifData":
MessageLookupByLibrary.simpleMessage("View all EXIF data"),

View file

@ -3914,6 +3914,16 @@ class S {
);
}
/// `Valid till {endDate}`
String validTill(Object endDate) {
return Intl.message(
'Valid till $endDate',
name: 'validTill',
desc: '',
args: [endDate],
);
}
/// `Free trial valid till {endDate}.\nYou can choose a paid plan afterwards.`
String playStoreFreeTrialValidTill(Object endDate) {
return Intl.message(
@ -7725,16 +7735,6 @@ class S {
);
}
/// `friend, reddit, ad, search, etc.`
String get hearUsHint {
return Intl.message(
'friend, reddit, ad, search, etc.',
name: 'hearUsHint',
desc: '',
args: [],
);
}
/// `We don't track app installs. It'd help if you told us where you found us!`
String get hearUsExplanation {
return Intl.message(
@ -7744,6 +7744,36 @@ class S {
args: [],
);
}
/// `View Add-ons`
String get viewAddOnButton {
return Intl.message(
'View Add-ons',
name: 'viewAddOnButton',
desc: '',
args: [],
);
}
/// `Add ons`
String get addOns {
return Intl.message(
'Add ons',
name: 'addOns',
desc: '',
args: [],
);
}
/// `Break up of add on storage`
String get addOnPageSubtitle {
return Intl.message(
'Break up of add on storage',
name: 'addOnPageSubtitle',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View file

@ -557,6 +557,7 @@
"faqs": "FAQs",
"renewsOn": "Renews on {endDate}",
"freeTrialValidTill": "Free trial valid till {endDate}",
"validTill": "Valid till {endDate}",
"playStoreFreeTrialValidTill": "Free trial valid till {endDate}.\nYou can choose a paid plan afterwards.",
"subWillBeCancelledOn": "Your subscription will be cancelled on {endDate}",
"subscription": "Subscription",
@ -1104,5 +1105,8 @@
"moveToHiddenAlbum": "Move to hidden album",
"deleteConfirmDialogBody": "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"hearUsWhereTitle": "How did you hear about Ente? (optional)",
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!"
}
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
"viewAddOnButton": "View add-ons",
"addOns": "Add-ons",
"addOnPageSubtitle": "Details of add-ons"
}

View file

@ -0,0 +1,60 @@
class Bonus {
int storage;
String type;
int validTill;
bool isRevoked;
Bonus(this.storage, this.type, this.validTill, this.isRevoked);
factory Bonus.fromJson(Map<String, dynamic> json) {
return Bonus(
json['storage'],
json['type'],
json['validTill'],
json['isRevoked'],
);
}
Map<String, dynamic> toJson() {
return {
'storage': storage,
'type': type,
'validTill': validTill,
'isRevoked': isRevoked,
};
}
}
class BonusData {
static Set<String> signUpBonusTypes = {'SIGN_UP', 'REFERRAL'};
final List<Bonus> storageBonuses;
BonusData(this.storageBonuses);
List<Bonus> getAddOnBonuses() {
return storageBonuses
.where((b) => !signUpBonusTypes.contains(b.type))
.toList();
}
int totalAddOnBonus() {
return getAddOnBonuses().fold(0, (sum, bonus) => sum + bonus.storage);
}
factory BonusData.fromJson(Map<String, dynamic>? json) {
if (json == null || json['storageBonuses'] == null) {
return BonusData([]);
}
return BonusData(
(json['storageBonuses'] as List)
.map((bonus) => Bonus.fromJson(bonus))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'storageBonuses': storageBonuses.map((bonus) => bonus.toJson()).toList(),
};
}
}

View file

@ -1,3 +1,5 @@
import "package:photos/models/api/storage_bonus/bonus.dart";
class ReferralView {
PlanInfo planInfo;
String code;
@ -86,34 +88,6 @@ class ReferralStat {
}
}
class Bonus {
int storage;
String type;
int validTill;
bool isRevoked;
Bonus(this.storage, this.type, this.validTill, this.isRevoked);
// fromJson
factory Bonus.fromJson(Map<String, dynamic> json) {
return Bonus(
json['storage'],
json['type'],
json['validTill'],
json['isRevoked'],
);
}
Map<String, dynamic> toJson() {
return {
'storage': storage,
'type': type,
'validTill': validTill,
'isRevoked': isRevoked,
};
}
}
class BonusDetails {
List<ReferralStat> referralStats;
List<Bonus> bonuses;

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math';
import 'package:collection/collection.dart';
import "package:photos/models/api/storage_bonus/bonus.dart";
import 'package:photos/models/file/file_type.dart';
import 'package:photos/models/subscription.dart';
@ -14,6 +15,7 @@ class UserDetails {
final Subscription subscription;
final FamilyData? familyData;
final ProfileData? profileData;
final BonusData? bonusData;
const UserDetails(
this.email,
@ -24,6 +26,7 @@ class UserDetails {
this.subscription,
this.familyData,
this.profileData,
this.bonusData,
);
bool isPartOfFamily() {
@ -55,6 +58,12 @@ class UserDetails {
storageBonus;
}
// This is the total storage for which user has paid for.
int getPlanPlusAddonStorage() {
return (isPartOfFamily() ? familyData!.storage : subscription.storage) +
bonusData!.totalAddOnBonus();
}
factory UserDetails.fromMap(Map<String, dynamic> map) {
return UserDetails(
map['email'] as String,
@ -65,6 +74,7 @@ class UserDetails {
Subscription.fromMap(map['subscription']),
FamilyData.fromMap(map['familyData']),
ProfileData.fromJson(map['profileData']),
BonusData.fromJson(map['bonusData']),
);
}
@ -78,6 +88,7 @@ class UserDetails {
'subscription': subscription.toMap(),
'familyData': familyData?.toMap(),
'profileData': profileData?.toJson(),
'bonusData': bonusData?.toJson(),
};
}
@ -123,6 +134,7 @@ class FamilyMember {
factory FamilyMember.fromJson(String source) =>
FamilyMember.fromMap(json.decode(source));
}
class ProfileData {
bool canDisableEmailMFA;
bool isEmailMFAEnabled;
@ -135,7 +147,6 @@ class ProfileData {
this.isTwoFactorEnabled = false,
});
// Factory method to create ProfileData instance from JSON
factory ProfileData.fromJson(Map<String, dynamic>? json) {
return ProfileData(
@ -153,6 +164,7 @@ class ProfileData {
'isTwoFactorEnabled': isTwoFactorEnabled,
};
}
String toJsonString() => json.encode(toJson());
}

View file

@ -122,12 +122,14 @@ class _StorageDetailsScreenState extends State<StorageDetailsScreen> {
leftValue: convertBytesToAbsoluteGBs(
min(
widget.referralView.claimedStorage,
widget.userDetails.getTotalStorage(),
widget.userDetails
.getPlanPlusAddonStorage(),
),
),
leftUnitName: "GB",
rightValue: convertBytesToAbsoluteGBs(
widget.userDetails.getTotalStorage(),
widget.userDetails
.getPlanPlusAddonStorage(),
),
rightUnitName: "GB",
),

View file

@ -0,0 +1,123 @@
import "package:flutter/material.dart";
import "package:intl/intl.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/api/storage_bonus/bonus.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/utils/data_util.dart";
class AddOnPage extends StatelessWidget {
final BonusData bonusData;
const AddOnPage(this.bonusData, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: S.of(context).addOns,
),
flexibleSpaceCaption: S.of(context).addOnPageSubtitle,
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.of(context).pop();
},
),
],
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(delegateBuildContext, index) {
Bonus bonus = bonusData.getAddOnBonuses()[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: AddOnViewSection(
sectionName: bonus.type == 'ADD_ON_BF_2023'
? "Black friday 2023"
: bonus.type.replaceAll('_', ' '),
bonus: bonus,
),
);
},
childCount: bonusData?.getAddOnBonuses().length ?? 0,
),
),
),
],
),
);
}
}
class AddOnViewSection extends StatelessWidget {
final String sectionName;
final Bonus bonus;
const AddOnViewSection({
super.key,
required this.sectionName,
required this.bonus,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textStyle = getEnteTextTheme(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
sectionName,
style: textStyle.body.copyWith(
color: colorScheme.textMuted,
),
),
if (bonus.validTill != 0)
Text(
S.of(context).validTill(
DateFormat.yMMMd(
Localizations.localeOf(context).languageCode,
)
.format(
DateTime.fromMicrosecondsSinceEpoch(
bonus.validTill,
),
)
.toString(),
),
style: textStyle.body.copyWith(
color: colorScheme.textMuted,
),
),
],
),
const SizedBox(height: 2),
RichText(
text: TextSpan(
children: [
TextSpan(
text: convertBytesToReadableFormat(bonus.storage).toString(),
style: textStyle.h3,
),
],
),
),
const SizedBox(height: 24),
],
);
}
}

View file

@ -25,6 +25,8 @@ import 'package:photos/ui/payment/child_subscription_widget.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
import 'package:photos/ui/payment/subscription_common_widgets.dart';
import 'package:photos/ui/payment/subscription_plan_widget.dart';
import "package:photos/ui/payment/view_add_on_widget.dart";
import "package:photos/utils/data_util.dart";
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -290,7 +292,7 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
if (!widget.isOnboarding) {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: _isFreePlanUser()
@ -310,6 +312,8 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
),
),
);
widgets.add(ViewAddOnButton(_userDetails.bonusData));
widgets.add(const SizedBox(height: 80));
}
return SingleChildScrollView(
child: Column(
@ -490,7 +494,16 @@ class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
if (isActive) {
return;
}
if (_userDetails.getFamilyOrPersonalUsage() > plan.storage) {
final int addOnBonus =
_userDetails.bonusData?.totalAddOnBonus() ?? 0;
if (_userDetails.getFamilyOrPersonalUsage() >
(plan.storage + addOnBonus)) {
_logger.warning(
" familyUsage ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage())}"
" plan storage ${convertBytesToReadableFormat(plan.storage)} "
"addOnBonus ${convertBytesToReadableFormat(addOnBonus)},"
"overshooting by ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - (plan.storage + addOnBonus))}",
);
showErrorDialog(
context,
S.of(context).sorry,

View file

@ -25,6 +25,8 @@ import 'package:photos/ui/payment/payment_web_page.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
import 'package:photos/ui/payment/subscription_common_widgets.dart';
import 'package:photos/ui/payment/subscription_plan_widget.dart';
import "package:photos/ui/payment/view_add_on_widget.dart";
import "package:photos/utils/data_util.dart";
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:step_progress_indicator/step_progress_indicator.dart';
@ -252,7 +254,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
if (!widget.isOnboarding) {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).manageFamily,
@ -270,6 +272,8 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
),
),
);
widgets.add(ViewAddOnButton(_userDetails.bonusData));
widgets.add(const SizedBox(height: 80));
}
return SingleChildScrollView(
@ -446,7 +450,16 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
);
return;
}
if (_userDetails.getFamilyOrPersonalUsage() > plan.storage) {
final int addOnBonus =
_userDetails.bonusData?.totalAddOnBonus() ?? 0;
if (_userDetails.getFamilyOrPersonalUsage() >
(plan.storage + addOnBonus)) {
logger.warning(
" familyUsage ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage())}"
" plan storage ${convertBytesToReadableFormat(plan.storage)} "
"addOnBonus ${convertBytesToReadableFormat(addOnBonus)},"
"overshooting by ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - (plan.storage + addOnBonus))}",
);
showErrorDialog(
context,
S.of(context).sorry,

View file

@ -0,0 +1,41 @@
import "package:flutter/material.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/api/storage_bonus/bonus.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import "package:photos/ui/payment/add_on_page.dart";
import "package:photos/utils/navigation_util.dart";
class ViewAddOnButton extends StatelessWidget {
final BonusData? bonusData;
const ViewAddOnButton(this.bonusData, {super.key});
@override
Widget build(BuildContext context) {
if (bonusData?.getAddOnBonuses().isEmpty ?? true) {
return const SizedBox.shrink();
}
final EnteColorScheme colorScheme = getEnteColorScheme(context);
return Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).viewAddOnButton,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
routeToPage(context, AddOnPage(bonusData!));
},
),
);
}
}