mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 08:18:41 +00:00
adds google and apple payment #162
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
074ead8b4f
commit
fa953b8928
19 changed files with 130 additions and 69 deletions
|
|
@ -209,11 +209,11 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
|
||||||
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
||||||
|
|
||||||
var pushNotificationText: [PushKind: String] = [:]
|
var pushNotificationText: [PushKind: String] = [:]
|
||||||
var title = "Someone"
|
var title = "[Unknown]"
|
||||||
|
|
||||||
// Define the messages based on the system language
|
// Define the messages based on the system language
|
||||||
if systemLanguage.contains("de") { // German
|
if systemLanguage.contains("de") { // German
|
||||||
title = "Jemand"
|
title = "[Unbekannt]"
|
||||||
pushNotificationText = [
|
pushNotificationText = [
|
||||||
.text: "hat eine Nachricht{inGroup} gesendet.",
|
.text: "hat eine Nachricht{inGroup} gesendet.",
|
||||||
.twonly: "hat ein twonly{inGroup} gesendet.",
|
.twonly: "hat ein twonly{inGroup} gesendet.",
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
await setUserPlan();
|
await setUserPlan();
|
||||||
await apiService.connect(force: true);
|
await apiService.connect();
|
||||||
await apiService.listenToNetworkChanges();
|
await apiService.listenToNetworkChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||||
if (wasPaused) {
|
if (wasPaused) {
|
||||||
globalIsAppInBackground = false;
|
globalIsAppInBackground = false;
|
||||||
twonlyDB.markUpdated();
|
twonlyDB.markUpdated();
|
||||||
unawaited(apiService.connect(force: true));
|
unawaited(apiService.connect());
|
||||||
}
|
}
|
||||||
} else if (state == AppLifecycleState.paused) {
|
} else if (state == AppLifecycleState.paused) {
|
||||||
wasPaused = true;
|
wasPaused = true;
|
||||||
|
|
|
||||||
|
|
@ -411,7 +411,7 @@
|
||||||
"notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.",
|
"notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.",
|
||||||
"notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.",
|
"notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.",
|
||||||
"notificationResponse": "hat dir{inGroup} geantwortet.",
|
"notificationResponse": "hat dir{inGroup} geantwortet.",
|
||||||
"notificationTitleUnknownUser": "Jemand",
|
"notificationTitleUnknownUser": "[Unbekannt]",
|
||||||
"notificationCategoryMessageTitle": "Nachrichten",
|
"notificationCategoryMessageTitle": "Nachrichten",
|
||||||
"notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.",
|
"notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.",
|
||||||
"groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.",
|
"groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.",
|
||||||
|
|
|
||||||
|
|
@ -441,7 +441,7 @@
|
||||||
"notificationReactionToImage": "has reacted with {reaction} to your image.",
|
"notificationReactionToImage": "has reacted with {reaction} to your image.",
|
||||||
"notificationReactionToAudio": "has reacted with {reaction} to your audio message.",
|
"notificationReactionToAudio": "has reacted with {reaction} to your audio message.",
|
||||||
"notificationResponse": "has responded{inGroup}.",
|
"notificationResponse": "has responded{inGroup}.",
|
||||||
"notificationTitleUnknownUser": "Someone",
|
"notificationTitleUnknownUser": "[Unknown]",
|
||||||
"notificationCategoryMessageTitle": "Messages",
|
"notificationCategoryMessageTitle": "Messages",
|
||||||
"notificationCategoryMessageDesc": "Messages from other users.",
|
"notificationCategoryMessageDesc": "Messages from other users.",
|
||||||
"groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.",
|
"groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.",
|
||||||
|
|
|
||||||
|
|
@ -2567,7 +2567,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @notificationTitleUnknownUser.
|
/// No description provided for @notificationTitleUnknownUser.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Someone'**
|
/// **'[Unknown]'**
|
||||||
String get notificationTitleUnknownUser;
|
String get notificationTitleUnknownUser;
|
||||||
|
|
||||||
/// No description provided for @notificationCategoryMessageTitle.
|
/// No description provided for @notificationCategoryMessageTitle.
|
||||||
|
|
|
||||||
|
|
@ -1415,7 +1415,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get notificationTitleUnknownUser => 'Jemand';
|
String get notificationTitleUnknownUser => '[Unbekannt]';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get notificationCategoryMessageTitle => 'Nachrichten';
|
String get notificationCategoryMessageTitle => 'Nachrichten';
|
||||||
|
|
|
||||||
|
|
@ -1407,7 +1407,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get notificationTitleUnknownUser => 'Someone';
|
String get notificationTitleUnknownUser => '[Unknown]';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get notificationCategoryMessageTitle => 'Messages';
|
String get notificationCategoryMessageTitle => 'Messages';
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,8 @@ class UserData {
|
||||||
|
|
||||||
List<int>? lastChangeLogHash;
|
List<int>? lastChangeLogHash;
|
||||||
|
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: true)
|
||||||
bool hideChangeLog = false;
|
bool hideChangeLog = true;
|
||||||
|
|
||||||
@JsonKey(defaultValue: true)
|
@JsonKey(defaultValue: true)
|
||||||
bool updateFCMToken = true;
|
bool updateFCMToken = true;
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ class ErrorCode extends $pb.ProtobufEnum {
|
||||||
ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork');
|
ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork');
|
||||||
static const ErrorCode RegistrationDisabled =
|
static const ErrorCode RegistrationDisabled =
|
||||||
ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled');
|
ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled');
|
||||||
|
static const ErrorCode IPAPaymentExpired =
|
||||||
|
ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired');
|
||||||
|
|
||||||
static const $core.List<ErrorCode> values = <ErrorCode>[
|
static const $core.List<ErrorCode> values = <ErrorCode>[
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|
@ -125,6 +127,7 @@ class ErrorCode extends $pb.ProtobufEnum {
|
||||||
NewDeviceRegistered,
|
NewDeviceRegistered,
|
||||||
InvalidProofOfWork,
|
InvalidProofOfWork,
|
||||||
RegistrationDisabled,
|
RegistrationDisabled,
|
||||||
|
IPAPaymentExpired,
|
||||||
];
|
];
|
||||||
|
|
||||||
static final $core.Map<$core.int, ErrorCode> _byValue =
|
static final $core.Map<$core.int, ErrorCode> _byValue =
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ const ErrorCode$json = {
|
||||||
{'1': 'NewDeviceRegistered', '2': 1031},
|
{'1': 'NewDeviceRegistered', '2': 1031},
|
||||||
{'1': 'InvalidProofOfWork', '2': 1032},
|
{'1': 'InvalidProofOfWork', '2': 1032},
|
||||||
{'1': 'RegistrationDisabled', '2': 1033},
|
{'1': 'RegistrationDisabled', '2': 1033},
|
||||||
|
{'1': 'IPAPaymentExpired', '2': 1034},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,4 +75,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
|
||||||
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
|
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
|
||||||
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
|
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
|
||||||
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
|
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
|
||||||
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCA==');
|
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
|
||||||
|
'UEFQYXltZW50RXhwaXJlZBCKCA==');
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/constants/subscription.keys.dart';
|
import 'package:twonly/src/constants/subscription.keys.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/model/purchases/purchasable_product.dart';
|
import 'package:twonly/src/model/purchases/purchasable_product.dart';
|
||||||
import 'package:twonly/src/services/subscription.service.dart';
|
import 'package:twonly/src/services/subscription.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
// Gives the option to override in tests.
|
// Gives the option to override in tests.
|
||||||
class IAPConnection {
|
class IAPConnection {
|
||||||
|
|
@ -23,6 +26,8 @@ class IAPConnection {
|
||||||
|
|
||||||
enum StoreState { loading, available, notAvailable }
|
enum StoreState { loading, available, notAvailable }
|
||||||
|
|
||||||
|
Timer? globalForceIpaCheck;
|
||||||
|
|
||||||
class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
PurchasesProvider() {
|
PurchasesProvider() {
|
||||||
final purchaseUpdated = iapConnection.purchaseStream;
|
final purchaseUpdated = iapConnection.purchaseStream;
|
||||||
|
|
@ -32,15 +37,9 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
onError: _updateStreamOnError,
|
onError: _updateStreamOnError,
|
||||||
);
|
);
|
||||||
|
|
||||||
forceIpaCheck = Timer(const Duration(seconds: 10), () {
|
|
||||||
Log.warn('Force Ipa check was not stopped. Requesting forced check...');
|
|
||||||
apiService.forceIpaCheck();
|
|
||||||
});
|
|
||||||
loadPurchases();
|
loadPurchases();
|
||||||
}
|
}
|
||||||
|
|
||||||
late Timer forceIpaCheck;
|
|
||||||
|
|
||||||
SubscriptionPlan plan = SubscriptionPlan.Free;
|
SubscriptionPlan plan = SubscriptionPlan.Free;
|
||||||
StoreState storeState = StoreState.loading;
|
StoreState storeState = StoreState.loading;
|
||||||
List<PurchasableProduct> products = [];
|
List<PurchasableProduct> products = [];
|
||||||
|
|
@ -48,6 +47,8 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
late StreamSubscription<List<PurchaseDetails>> _subscription;
|
late StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||||
final InAppPurchase iapConnection = IAPConnection.instance;
|
final InAppPurchase iapConnection = IAPConnection.instance;
|
||||||
|
|
||||||
|
bool _userTriggeredBuyButton = false;
|
||||||
|
|
||||||
void updatePlan(SubscriptionPlan newPlan) {
|
void updatePlan(SubscriptionPlan newPlan) {
|
||||||
plan = newPlan;
|
plan = newPlan;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
@ -77,11 +78,19 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
storeState = StoreState.available;
|
storeState = StoreState.available;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
final user = await getUser();
|
||||||
|
if (user != null && isPayingUser(planFromString(user.subscriptionPlan))) {
|
||||||
|
Log.info('Started IPA timer for verification.');
|
||||||
|
globalForceIpaCheck = Timer(const Duration(seconds: 5), () async {
|
||||||
|
Log.warn('Force Ipa check was not stopped. Requesting forced check...');
|
||||||
|
await apiService.forceIpaCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await iapConnection.restorePurchases();
|
await iapConnection.restorePurchases();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> buy(PurchasableProduct product) async {
|
Future<void> buy(PurchasableProduct product) async {
|
||||||
Log.info('User wants to buy ${product.id}');
|
|
||||||
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
|
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
|
||||||
switch (product.id) {
|
switch (product.id) {
|
||||||
// case storeKeyConsumable:
|
// case storeKeyConsumable:
|
||||||
|
|
@ -89,6 +98,9 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
case SubscriptionKeys.proMonthly:
|
case SubscriptionKeys.proMonthly:
|
||||||
case SubscriptionKeys.proYearly:
|
case SubscriptionKeys.proYearly:
|
||||||
case SubscriptionKeys.familyYearly:
|
case SubscriptionKeys.familyYearly:
|
||||||
|
_userTriggeredBuyButton = true;
|
||||||
|
Log.info('User wants to buy ${product.id}');
|
||||||
|
|
||||||
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
|
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value(
|
throw ArgumentError.value(
|
||||||
|
|
@ -108,44 +120,70 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
|
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
|
||||||
Log.info(purchaseDetails.productID);
|
if (kDebugMode) {
|
||||||
Log.info(purchaseDetails.verificationData.serverVerificationData);
|
Log.info(purchaseDetails.productID);
|
||||||
Log.info(purchaseDetails.verificationData.source);
|
Log.info(purchaseDetails.verificationData.serverVerificationData);
|
||||||
|
// if (Platform.isIOS) {
|
||||||
|
// final data = purchaseDetails.verificationData.serverVerificationData;
|
||||||
|
// printWrapped(data);
|
||||||
|
// final datas = data.split('.')[1];
|
||||||
|
// printWrapped(datas);
|
||||||
|
// }
|
||||||
|
Log.info(purchaseDetails.verificationData.source);
|
||||||
|
}
|
||||||
final res = await apiService.ipaPurchase(
|
final res = await apiService.ipaPurchase(
|
||||||
purchaseDetails.productID,
|
purchaseDetails.productID,
|
||||||
purchaseDetails.verificationData.source,
|
purchaseDetails.verificationData.source,
|
||||||
purchaseDetails.verificationData.serverVerificationData,
|
purchaseDetails.verificationData.serverVerificationData,
|
||||||
);
|
);
|
||||||
|
// plan is updated in the apiProvider, as the server updates its states and responses with
|
||||||
|
// an ok authenticated which is processed in the apiProvider...
|
||||||
|
if (res.isSuccess) {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await updateUserdata((u) {
|
||||||
|
u.subscriptionPlanIdStore = purchaseDetails.productID;
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (res.isError) {
|
||||||
|
if (res.error == ErrorCode.IPAPaymentExpired &&
|
||||||
|
_userTriggeredBuyButton &&
|
||||||
|
Platform.isIOS) {
|
||||||
|
await launchUrl(
|
||||||
|
Uri.parse('https://apps.apple.com/account/subscriptions'),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return res.isSuccess;
|
return res.isSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
|
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
|
||||||
var validPurchase = false;
|
Log.info(
|
||||||
|
'_handlePurchase: ${purchaseDetails.productID}, ${purchaseDetails.status}',
|
||||||
|
);
|
||||||
if (purchaseDetails.status == PurchaseStatus.purchased) {
|
if (purchaseDetails.status == PurchaseStatus.purchased) {
|
||||||
Log.info('purchased: ${purchaseDetails.productID}');
|
await _verifyPurchase(purchaseDetails);
|
||||||
validPurchase = await _verifyPurchase(purchaseDetails);
|
|
||||||
if (validPurchase) {
|
|
||||||
var plan = SubscriptionPlan.Pro;
|
|
||||||
if (purchaseDetails.productID.contains('family')) {
|
|
||||||
plan = SubscriptionPlan.Family;
|
|
||||||
}
|
|
||||||
await updateUserdata((u) {
|
|
||||||
u
|
|
||||||
..subscriptionPlan = plan.name
|
|
||||||
..subscriptionPlanIdStore = purchaseDetails.productID;
|
|
||||||
return u;
|
|
||||||
});
|
|
||||||
updatePlan(plan);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (purchaseDetails.status == PurchaseStatus.restored) {
|
if (purchaseDetails.status == PurchaseStatus.restored &&
|
||||||
// there is a
|
purchaseDetails.error == null) {
|
||||||
forceIpaCheck.cancel();
|
globalForceIpaCheck?.cancel();
|
||||||
|
|
||||||
if (gUser.subscriptionPlan != SubscriptionPlan.Family.name ||
|
final user = await getUser();
|
||||||
gUser.subscriptionPlan != SubscriptionPlan.Pro.name) {
|
|
||||||
// app was installed on some one other...
|
if (user != null &&
|
||||||
// subscription is handled on the server, so on a new device the subscription comes from the server again...
|
(user.subscriptionPlan != SubscriptionPlan.Family.name &&
|
||||||
|
user.subscriptionPlan != SubscriptionPlan.Pro.name)) {
|
||||||
|
for (var i = 0; i < 100; i++) {
|
||||||
|
if (apiService.isAuthenticated) {
|
||||||
|
Log.info(
|
||||||
|
'current user does not have a sub: ${purchaseDetails.productID}');
|
||||||
|
await _verifyPurchase(purchaseDetails);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,17 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startReconnectionTimer() async {
|
Future<void> startReconnectionTimer() async {
|
||||||
|
if (reconnectionTimer?.isActive ?? false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
reconnectionTimer?.cancel();
|
reconnectionTimer?.cancel();
|
||||||
reconnectionTimer ??=
|
Log.info('Starting reconnection timer with $_reconnectionDelay s delay');
|
||||||
Timer(Duration(seconds: _reconnectionDelay), () async {
|
reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async {
|
||||||
|
Log.info('Reconnection timer triggered');
|
||||||
reconnectionTimer = null;
|
reconnectionTimer = null;
|
||||||
await connect(force: true);
|
await connect();
|
||||||
});
|
});
|
||||||
_reconnectionDelay += 2;
|
_reconnectionDelay = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close(Function callback) async {
|
Future<void> close(Function callback) async {
|
||||||
|
|
@ -145,18 +149,13 @@ class ApiService {
|
||||||
.onConnectivityChanged
|
.onConnectivityChanged
|
||||||
.listen((List<ConnectivityResult> result) async {
|
.listen((List<ConnectivityResult> result) async {
|
||||||
if (!result.contains(ConnectivityResult.none)) {
|
if (!result.contains(ConnectivityResult.none)) {
|
||||||
await connect(force: true);
|
await connect();
|
||||||
}
|
}
|
||||||
// Received changes in available connectivity types!
|
// Received changes in available connectivity types!
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> connect({bool force = false}) async {
|
Future<bool> connect() async {
|
||||||
if (reconnectionTimer != null && !force) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
reconnectionTimer?.cancel();
|
|
||||||
reconnectionTimer = null;
|
|
||||||
return lockConnecting.protect<bool>(() async {
|
return lockConnecting.protect<bool>(() async {
|
||||||
if (_channel != null) {
|
if (_channel != null) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -292,6 +291,7 @@ class ApiService {
|
||||||
if (_channel == null) {
|
if (_channel == null) {
|
||||||
Log.warn('sending request while api is not connected');
|
Log.warn('sending request while api is not connected');
|
||||||
if (!await connect()) {
|
if (!await connect()) {
|
||||||
|
Log.warn('could not connected again');
|
||||||
return Result.error(ErrorCode.InternalError);
|
return Result.error(ErrorCode.InternalError);
|
||||||
}
|
}
|
||||||
if (_channel == null) {
|
if (_channel == null) {
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,15 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
||||||
return pushNotification;
|
return pushNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> requestNewPushKeysForUser(int toUserId) async {
|
||||||
|
await sendCipherText(
|
||||||
|
toUserId,
|
||||||
|
EncryptedContent()
|
||||||
|
..pushKeys = (EncryptedContent_PushKeys()
|
||||||
|
..type = EncryptedContent_PushKeys_Type.REQUEST),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// this will trigger a push notification
|
/// this will trigger a push notification
|
||||||
/// push notification only containing the message kind and username
|
/// push notification only containing the message kind and username
|
||||||
Future<Uint8List?> encryptPushNotification(
|
Future<Uint8List?> encryptPushNotification(
|
||||||
|
|
@ -326,15 +335,16 @@ Future<Uint8List?> encryptPushNotification(
|
||||||
// this will be enforced after every app uses this system... :/
|
// this will be enforced after every app uses this system... :/
|
||||||
// return null;
|
// return null;
|
||||||
Log.warn('Using insecure key as the receiver does not send a push key!');
|
Log.warn('Using insecure key as the receiver does not send a push key!');
|
||||||
|
await requestNewPushKeysForUser(toUserId);
|
||||||
await sendCipherText(
|
|
||||||
toUserId,
|
|
||||||
EncryptedContent()
|
|
||||||
..pushKeys = (EncryptedContent_PushKeys()
|
|
||||||
..type = EncryptedContent_PushKeys_Type.REQUEST),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
final createdAt = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
pushUser.pushKeys.last.createdAtUnixTimestamp.toInt(),
|
||||||
|
);
|
||||||
|
final timeBefore = DateTime.now().subtract(const Duration(days: 8));
|
||||||
|
if (createdAt.isBefore(timeBefore)) {
|
||||||
|
await requestNewPushKeysForUser(toUserId);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
key = pushUser.pushKeys.last.key;
|
key = pushUser.pushKeys.last.key;
|
||||||
keyId = pushUser.pushKeys.last.id.toInt();
|
keyId = pushUser.pushKeys.last.id.toInt();
|
||||||
|
|
|
||||||
|
|
@ -361,3 +361,8 @@ String getAvatarSvg(Uint8List avatarSvgCompressed) {
|
||||||
final raw = gzip.decode(avatarSvgCompressed);
|
final raw = gzip.decode(avatarSvgCompressed);
|
||||||
return utf8.decode(raw);
|
return utf8.decode(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void printWrapped(String text) {
|
||||||
|
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
|
||||||
|
pattern.allMatches(text).forEach((match) => print(match.group(0)));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,9 @@ class MainCameraController {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.warn(e);
|
Log.warn(e);
|
||||||
}
|
}
|
||||||
await cameraController?.dispose();
|
final cameraControllerTemp = cameraController;
|
||||||
cameraController = null;
|
cameraController = null;
|
||||||
|
await cameraControllerTemp?.dispose();
|
||||||
initCameraStarted = false;
|
initCameraStarted = false;
|
||||||
selectedCameraDetails = SelectedCameraDetails();
|
selectedCameraDetails = SelectedCameraDetails();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await apiService.close(() {});
|
await apiService.close(() {});
|
||||||
await apiService.connect(force: true);
|
await apiService.connect();
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
},
|
},
|
||||||
child: (_groupsNotPinned.isEmpty &&
|
child: (_groupsNotPinned.isEmpty &&
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
await apiService.forceIpaCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -93,7 +94,8 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
PlanCard(
|
PlanCard(
|
||||||
plan: currentPlan,
|
plan: currentPlan,
|
||||||
),
|
),
|
||||||
if (!isPayingUser(currentPlan)) ...[
|
if (!isPayingUser(currentPlan) ||
|
||||||
|
currentPlan == SubscriptionPlan.Tester) ...[
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
|
|
@ -404,7 +406,7 @@ Future<void> redeemUserInviteCode(BuildContext context, String newPlan) async {
|
||||||
);
|
);
|
||||||
// reconnect to load new plan.
|
// reconnect to load new plan.
|
||||||
await apiService.close(() {});
|
await apiService.close(() {});
|
||||||
await apiService.connect(force: true);
|
await apiService.connect();
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
|
||||||
|
|
@ -548,7 +548,7 @@ Future<void> redeemUserInviteCode(BuildContext context, String newPlan) async {
|
||||||
);
|
);
|
||||||
// reconnect to load new plan.
|
// reconnect to load new plan.
|
||||||
await apiService.close(() {});
|
await apiService.close(() {});
|
||||||
await apiService.connect(force: true);
|
await apiService.connect();
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.0.76+76
|
version: 0.0.77+77
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.6.0
|
sdk: ^3.6.0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue