Skip to content

Commit

Permalink
Bug fixes and minor validations
Browse files Browse the repository at this point in the history
  • Loading branch information
Wolfteam committed Feb 28, 2022
1 parent 3d896ec commit b46fd31
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 72 deletions.
61 changes: 53 additions & 8 deletions lib/application/donations/donations_bloc.dart
@@ -1,5 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:shiori/domain/extensions/string_extensions.dart';
import 'package:shiori/domain/models/models.dart';
import 'package:shiori/domain/services/network_service.dart';
import 'package:shiori/domain/services/purchase_service.dart';
Expand All @@ -14,49 +15,93 @@ class DonationsBloc extends Bloc<DonationsEvent, DonationsState> {

static int maxUserIdLength = 20;

//The user id must be something like 12345_xyz
static String appUserIdRegex = '([0-9]{5,10}[_][A-Za-z])';

DonationsBloc(this._purchaseService, this._networkService) : super(const DonationsState.loading());

@override
Stream<DonationsState> mapEventToState(DonationsEvent event) async* {
if (!await _networkService.isInternetAvailable()) {
yield const DonationsState.initial(packages: [], isInitialized: false, noInternetConnection: true);
yield const DonationsState.initial(packages: [], isInitialized: false, noInternetConnection: true, canMakePurchases: false);
return;
}

if (!await _purchaseService.isPlatformSupported()) {
yield const DonationsState.initial(packages: [], isInitialized: false, noInternetConnection: false);
yield const DonationsState.initial(packages: [], isInitialized: false, noInternetConnection: false, canMakePurchases: false);
return;
}

if (!_purchaseService.isInitialized) {
await _purchaseService.init();
}

final currentState = state;
yield const DonationsState.loading();

final s = await event.map(
init: (_) => _init(),
restorePurchases: (e) => _restorePurchases(e.userId),
purchase: (e) => _purchase(e),
);

yield s;

if ((s is _PurchaseCompleted && s.error) || (s is _RestoreCompleted && s.error)) {
yield currentState;
}
yield await s.maybeMap(
purchaseCompleted: (state) async {
if (state.error) {
return _init();
}
return state;
},
restoreCompleted: (state) async {
if (state.error) {
return _init();
}
return state;
},
orElse: () async => s,
);
}

Future<DonationsState> _init() async {
final canMakePurchases = await _purchaseService.canMakePurchases();
if (!canMakePurchases) {
return DonationsState.initial(
packages: [],
isInitialized: _purchaseService.isInitialized,
noInternetConnection: false,
canMakePurchases: false,
);
}
final packages = await _purchaseService.getInAppPurchases();
return DonationsState.initial(packages: packages, isInitialized: _purchaseService.isInitialized, noInternetConnection: false);
return DonationsState.initial(
packages: packages,
isInitialized: _purchaseService.isInitialized,
noInternetConnection: false,
canMakePurchases: canMakePurchases,
);
}

Future<DonationsState> _restorePurchases(String userId) async {
if (!RegExp(appUserIdRegex).hasMatch(userId)) {
throw Exception('AppUserId is not valid');
}
final restored = await _purchaseService.restorePurchases(userId);
return DonationsState.restoreCompleted(error: !restored);
}

Future<DonationsState> _purchase(_Purchase e) async {
if (!RegExp(appUserIdRegex).hasMatch(e.userId)) {
throw Exception('AppUserId is not valid');
}

if (e.identifier.isNullEmptyOrWhitespace) {
throw Exception('Invalid package identifier');
}

if (e.offeringIdentifier.isNullEmptyOrWhitespace) {
throw Exception('Invalid offering identifier');
}

final succeed = await _purchaseService.purchase(e.userId, e.identifier, e.offeringIdentifier);
return DonationsState.purchaseCompleted(error: !succeed);
}
Expand Down
1 change: 1 addition & 0 deletions lib/application/donations/donations_state.dart
Expand Up @@ -8,6 +8,7 @@ class DonationsState with _$DonationsState {
required List<PackageItemModel> packages,
required bool isInitialized,
required bool noInternetConnection,
required bool canMakePurchases,
}) = _InitialState;

const factory DonationsState.purchaseCompleted({
Expand Down
3 changes: 3 additions & 0 deletions lib/domain/enums/app_unlocked_feature.dart
@@ -0,0 +1,3 @@
enum AppUnlockedFeature {
darkAmoledTheme,
}
1 change: 1 addition & 0 deletions lib/domain/enums/enums.dart
Expand Up @@ -4,6 +4,7 @@ export 'app_notification_item_type.dart';
export 'app_notification_type.dart';
export 'app_server_reset_time_type.dart';
export 'app_theme_type.dart';
export 'app_unlocked_feature.dart';
export 'artifact_farming_time_type.dart';
export 'artifact_filter_type.dart';
export 'artifact_type.dart';
Expand Down
5 changes: 5 additions & 0 deletions lib/domain/services/purchase_service.dart
@@ -1,3 +1,4 @@
import 'package:shiori/domain/enums/enums.dart';
import 'package:shiori/domain/models/models.dart';

abstract class PurchaseService {
Expand All @@ -7,11 +8,15 @@ abstract class PurchaseService {

Future<bool> isPlatformSupported();

Future<bool> canMakePurchases();

Future<bool> logIn(String userId);

Future<List<PackageItemModel>> getInAppPurchases();

Future<bool> purchase(String userId, String identifier, String offeringIdentifier);

Future<bool> restorePurchases(String userId, {String? entitlementIdentifier});

Future<List<AppUnlockedFeature>> getUnlockedFeatures();
}
55 changes: 48 additions & 7 deletions lib/infrastructure/purchase_service.dart
Expand Up @@ -4,6 +4,8 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:shiori/domain/enums/enums.dart';
import 'package:shiori/domain/extensions/string_extensions.dart';
import 'package:shiori/domain/models/models.dart';
import 'package:shiori/domain/services/logging_service.dart';
import 'package:shiori/domain/services/purchase_service.dart';
Expand Down Expand Up @@ -58,6 +60,16 @@ class PurchaseServiceImpl implements PurchaseService {
return Future.value(false);
}

@override
Future<bool> canMakePurchases() async {
try {
return await Purchases.canMakePayments();
} catch (e, s) {
_handleError('canMakePurchases', e, s);
return false;
}
}

@override
Future<bool> logIn(String userId) async {
try {
Expand Down Expand Up @@ -117,19 +129,48 @@ class PurchaseServiceImpl implements PurchaseService {
}

try {
final transactions = await Purchases.restoreTransactions();
if (entitlementIdentifier == null) {
return transactions.entitlements.active.isNotEmpty;
}

final entitlement = transactions.entitlements.active.values.firstWhereOrNull((el) => el.identifier == entitlementIdentifier);
return entitlement != null;
final features = await _getUnlockedFeatures(entitlementIdentifier: entitlementIdentifier);
return features.isNotEmpty;
} catch (e, s) {
_handleError('restorePurchases', e, s);
return false;
}
}

@override
Future<List<AppUnlockedFeature>> getUnlockedFeatures() async {
try {
if (!await isPlatformSupported()) {
return [];
}

if (await Purchases.isAnonymous) {
return [];
}

final features = await _getUnlockedFeatures();
return features;
} catch (e, s) {
_handleError('getUnlockedFeatures', e, s);
return [];
}
}

Future<List<AppUnlockedFeature>> _getUnlockedFeatures({String? entitlementIdentifier}) async {
try {
final transactions = await Purchases.restoreTransactions();
if (entitlementIdentifier.isNotNullEmptyOrWhitespace) {
final activeEntitlements = transactions.entitlements.active.values.any((el) => el.isActive);
return activeEntitlements ? AppUnlockedFeature.values : [];
}

final entitlement = transactions.entitlements.active.values.firstWhereOrNull((el) => el.identifier == entitlementIdentifier && el.isActive);
return entitlement != null ? AppUnlockedFeature.values : [];
} catch (e) {
rethrow;
}
}

void _handleError(String methodName, dynamic e, StackTrace s) {
if (e is PlatformException) {
final errorCode = PurchasesErrorHelper.getErrorCode(e);
Expand Down

0 comments on commit b46fd31

Please sign in to comment.