mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
Merge pull request #345 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Fixes multiple user-ability issues - Implemented payment system with Google Play and App Store - Fixing multiple bugs
This commit is contained in:
commit
2713f092eb
62 changed files with 8759 additions and 786 deletions
|
|
@ -66,6 +66,7 @@
|
|||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
|
|
|||
|
|
@ -209,11 +209,11 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
|
|||
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
||||
|
||||
var pushNotificationText: [PushKind: String] = [:]
|
||||
var title = "Someone"
|
||||
var title = "[Unknown]"
|
||||
|
||||
// Define the messages based on the system language
|
||||
if systemLanguage.contains("de") { // German
|
||||
title = "Jemand"
|
||||
title = "[Unbekannt]"
|
||||
pushNotificationText = [
|
||||
.text: "hat eine Nachricht{inGroup} gesendet.",
|
||||
.twonly: "hat ein twonly{inGroup} gesendet.",
|
||||
|
|
|
|||
115
ios/Podfile.lock
115
ios/Podfile.lock
|
|
@ -54,55 +54,55 @@ PODS:
|
|||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase (12.4.0):
|
||||
- Firebase/Core (= 12.4.0)
|
||||
- Firebase/Core (12.4.0):
|
||||
- Firebase (12.6.0):
|
||||
- Firebase/Core (= 12.6.0)
|
||||
- Firebase/Core (12.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 12.4.0)
|
||||
- Firebase/CoreOnly (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- Firebase/Messaging (12.4.0):
|
||||
- FirebaseAnalytics (~> 12.6.0)
|
||||
- Firebase/CoreOnly (12.6.0):
|
||||
- FirebaseCore (~> 12.6.0)
|
||||
- Firebase/Messaging (12.6.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.4.0)
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (= 12.4.0)
|
||||
- FirebaseMessaging (~> 12.6.0)
|
||||
- firebase_core (4.3.0):
|
||||
- Firebase/CoreOnly (= 12.6.0)
|
||||
- Flutter
|
||||
- firebase_messaging (16.0.4):
|
||||
- Firebase/Messaging (= 12.4.0)
|
||||
- firebase_messaging (16.1.0):
|
||||
- Firebase/Messaging (= 12.6.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (12.4.0):
|
||||
- FirebaseAnalytics/Default (= 12.4.0)
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- FirebaseAnalytics (12.6.0):
|
||||
- FirebaseAnalytics/Default (= 12.6.0)
|
||||
- FirebaseCore (~> 12.6.0)
|
||||
- FirebaseInstallations (~> 12.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/Default (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleAppMeasurement/Default (= 12.4.0)
|
||||
- FirebaseAnalytics/Default (12.6.0):
|
||||
- FirebaseCore (~> 12.6.0)
|
||||
- FirebaseInstallations (~> 12.6.0)
|
||||
- GoogleAppMeasurement/Default (= 12.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (12.4.0):
|
||||
- FirebaseCoreInternal (~> 12.4.0)
|
||||
- FirebaseCore (12.6.0):
|
||||
- FirebaseCoreInternal (~> 12.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreInternal (12.4.0):
|
||||
- FirebaseCoreInternal (12.6.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseInstallations (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (12.6.0):
|
||||
- FirebaseCore (~> 12.6.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- FirebaseMessaging (12.6.0):
|
||||
- FirebaseCore (~> 12.6.0)
|
||||
- FirebaseInstallations (~> 12.6.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
|
|
@ -134,28 +134,28 @@ PODS:
|
|||
- google_mlkit_commons (0.11.0):
|
||||
- Flutter
|
||||
- MLKitVision
|
||||
- GoogleAdsOnDeviceConversion (3.1.0):
|
||||
- GoogleAdsOnDeviceConversion (3.2.0):
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Core (12.4.0):
|
||||
- GoogleAppMeasurement/Core (12.6.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Default (12.4.0):
|
||||
- GoogleAdsOnDeviceConversion (~> 3.1.0)
|
||||
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.4.0)
|
||||
- GoogleAppMeasurement/Default (12.6.0):
|
||||
- GoogleAdsOnDeviceConversion (~> 3.2.0)
|
||||
- GoogleAppMeasurement/Core (= 12.6.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.4.0):
|
||||
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.6.0):
|
||||
- GoogleAppMeasurement/Core (= 12.6.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
|
|
@ -217,6 +217,9 @@ PODS:
|
|||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- in_app_purchase_storekit (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- libwebp (1.5.0):
|
||||
- libwebp/demux (= 1.5.0)
|
||||
- libwebp/mux (= 1.5.0)
|
||||
|
|
@ -273,10 +276,10 @@ PODS:
|
|||
- restart_app (0.0.1):
|
||||
- Flutter
|
||||
- ScreenProtectorKit (1.3.1)
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
- SDWebImageWebPCoder (0.14.6):
|
||||
- SDWebImage (5.21.5):
|
||||
- SDWebImage/Core (= 5.21.5)
|
||||
- SDWebImage/Core (5.21.5)
|
||||
- SDWebImageWebPCoder (0.15.0):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.17)
|
||||
- Sentry/HybridSDK (8.56.2)
|
||||
|
|
@ -317,7 +320,7 @@ PODS:
|
|||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- sqlite3/session
|
||||
- SwiftProtobuf (1.33.1)
|
||||
- SwiftProtobuf (1.33.3)
|
||||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
|
@ -353,6 +356,7 @@ DEPENDENCIES:
|
|||
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
||||
- GoogleUtilities
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- no_screenshot (from `.symlinks/plugins/no_screenshot/ios`)
|
||||
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||
|
|
@ -447,6 +451,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_purchase_storekit:
|
||||
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
|
||||
local_auth_darwin:
|
||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||
no_screenshot:
|
||||
|
|
@ -489,14 +495,14 @@ SPEC CHECKSUMS:
|
|||
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
|
||||
ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
|
||||
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
|
||||
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
|
||||
Firebase: a451a7b61536298fd5cbfe3a746fd40443a50679
|
||||
firebase_core: ba00a168e719694f38960502ceb560285603d073
|
||||
firebase_messaging: bf0e29321927edc02a563c984dbfa5b063864b15
|
||||
FirebaseAnalytics: d0a97a0db6425e5a5d966340b87f92ca7b13a557
|
||||
FirebaseCore: 0e38ad5d62d980a47a64b8e9301ffa311457be04
|
||||
FirebaseCoreInternal: 69bf1306a05b8ac43004f6cc1f804bb7b05b229e
|
||||
FirebaseInstallations: 631b38da2e11a83daa4bfb482f79d286a5dfa7ad
|
||||
FirebaseMessaging: a61bc42dcab3f7a346d94bbb54dab2c9435b18b2
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
|
||||
|
|
@ -506,14 +512,15 @@ SPEC CHECKSUMS:
|
|||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159
|
||||
google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab
|
||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
||||
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
||||
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
|
|
@ -530,8 +537,8 @@ SPEC CHECKSUMS:
|
|||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf
|
||||
ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
||||
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
||||
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
|
||||
sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
|
|
@ -539,7 +546,7 @@ SPEC CHECKSUMS:
|
|||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
||||
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
||||
SwiftProtobuf: 533a18409c3ca3a6156b2b1e46afd0f69e751aba
|
||||
SwiftProtobuf: e1b437c8e31a4c5577b643249a0bb62ed4f02153
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */; };
|
||||
D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D21FCEA42D9F2B750088701D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25D4D1D2EF626E30029F805 /* StoreKit.framework */; };
|
||||
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -89,6 +90,7 @@
|
|||
B3B27B7FBEEA31DB7793A0C2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
D21FCEA42D9F2B750088701D /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D2265DD42D920142000D99BB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
D25D4D1D2EF626E30029F805 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
|
@ -130,6 +132,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */,
|
||||
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -206,6 +209,7 @@
|
|||
E5079CCEE4804DB65AA3F23F /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D25D4D1D2EF626E30029F805 /* StoreKit.framework */,
|
||||
A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */,
|
||||
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */,
|
||||
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */,
|
||||
|
|
|
|||
11
lib/app.dart
11
lib/app.dart
|
|
@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -39,8 +40,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
await setUserPlan();
|
||||
};
|
||||
|
||||
globalCallbackUpdatePlan = (SubscriptionPlan plan) async {
|
||||
await context.read<CustomChangeProvider>().updatePlan(plan);
|
||||
globalCallbackUpdatePlan = (SubscriptionPlan plan) {
|
||||
context.read<PurchasesProvider>().updatePlan(plan);
|
||||
};
|
||||
|
||||
unawaited(initAsync());
|
||||
|
|
@ -50,7 +51,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
final user = await getUser();
|
||||
if (user != null && mounted) {
|
||||
if (mounted) {
|
||||
await context.read<CustomChangeProvider>().updatePlan(
|
||||
context.read<PurchasesProvider>().updatePlan(
|
||||
planFromString(user.subscriptionPlan),
|
||||
);
|
||||
}
|
||||
|
|
@ -59,7 +60,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
|
||||
Future<void> initAsync() async {
|
||||
await setUserPlan();
|
||||
await apiService.connect(force: true);
|
||||
await apiService.connect();
|
||||
await apiService.listenToNetworkChanges();
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
if (wasPaused) {
|
||||
globalIsAppInBackground = false;
|
||||
twonlyDB.markUpdated();
|
||||
unawaited(apiService.connect(force: true));
|
||||
unawaited(apiService.connect());
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
wasPaused = true;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/image_editor.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/api.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart';
|
||||
|
|
@ -25,6 +26,8 @@ import 'package:twonly/src/utils/storage.dart';
|
|||
void main() async {
|
||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await initFCMService();
|
||||
|
||||
final user = await getUser();
|
||||
if (user != null) {
|
||||
gUser = user;
|
||||
|
|
@ -43,8 +46,6 @@ void main() async {
|
|||
unawaited(performTwonlySafeBackup());
|
||||
}
|
||||
|
||||
await initFCMService();
|
||||
|
||||
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
|
||||
globalApplicationSupportDirectory =
|
||||
(await getApplicationSupportDirectory()).path;
|
||||
|
|
@ -54,9 +55,7 @@ void main() async {
|
|||
final settingsController = SettingsChangeProvider();
|
||||
|
||||
await settingsController.loadSettings();
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp],
|
||||
);
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
|
||||
unawaited(setupPushNotification());
|
||||
|
||||
|
|
@ -79,6 +78,7 @@ void main() async {
|
|||
ChangeNotifierProvider(create: (_) => settingsController),
|
||||
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
|
||||
ChangeNotifierProvider(create: (_) => PurchasesProvider()),
|
||||
],
|
||||
child: const App(),
|
||||
),
|
||||
|
|
|
|||
6
lib/src/constants/subscription.keys.dart
Normal file
6
lib/src/constants/subscription.keys.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class SubscriptionKeys {
|
||||
static const String proMonthly = 'pro_monthly';
|
||||
static const String proYearly = 'pro_yearly';
|
||||
// static const String familyMonthly = 'family_monthly';
|
||||
static const String familyYearly = 'family_yearly';
|
||||
}
|
||||
|
|
@ -42,8 +42,15 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<Contact>> getContactsByUsername(String username) async {
|
||||
return (select(contacts)..where((t) => t.username.equals(username))).get();
|
||||
Future<List<Contact>> getContactsByUsername(
|
||||
String username, {
|
||||
String username2 = '_______',
|
||||
}) async {
|
||||
return (select(contacts)
|
||||
..where(
|
||||
(t) => t.username.equals(username) | t.username.equals(username2),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<void> deleteContactByUserId(int userId) {
|
||||
|
|
@ -58,7 +65,8 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
.write(updatedValues);
|
||||
if (updatedValues.blocked.present ||
|
||||
updatedValues.displayName.present ||
|
||||
updatedValues.nickName.present) {
|
||||
updatedValues.nickName.present ||
|
||||
updatedValues.username.present) {
|
||||
final contact = await getContactByUserId(userId).getSingleOrNull();
|
||||
if (contact != null) {
|
||||
await updatePushUser(contact);
|
||||
|
|
@ -118,7 +126,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
|
||||
Stream<List<Contact>> watchAllAcceptedContacts() {
|
||||
return (select(contacts)
|
||||
..where((t) => t.blocked.equals(false) & t.accepted.equals(true)))
|
||||
..where(
|
||||
(t) =>
|
||||
t.blocked.equals(false) &
|
||||
t.accepted.equals(true) &
|
||||
t.accountDeleted.equals(false),
|
||||
))
|
||||
.watch();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -386,14 +386,18 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
|
||||
int getFlameCounterFromGroup(Group? group) {
|
||||
if (group == null) return 0;
|
||||
if (group.lastMessageSend == null || group.lastMessageReceived == null) {
|
||||
if (group.lastMessageSend == null ||
|
||||
group.lastMessageReceived == null ||
|
||||
group.lastFlameCounterChange == null) {
|
||||
return 0;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
|
||||
if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
|
||||
group.lastMessageReceived!.isAfter(twoDaysAgo)) {
|
||||
group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
||||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
|
||||
return group.flameCounter + 1;
|
||||
} else {
|
||||
return 0;
|
||||
|
|
|
|||
1
lib/src/database/schemas/twonly_db/drift_schema_v4.json
Normal file
1
lib/src/database/schemas/twonly_db/drift_schema_v4.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -90,8 +90,7 @@ class GroupHistories extends Table {
|
|||
IntColumn get contactId =>
|
||||
integer().nullable().references(Contacts, #userId)();
|
||||
|
||||
IntColumn get affectedContactId =>
|
||||
integer().nullable().references(Contacts, #userId)();
|
||||
IntColumn get affectedContactId => integer().nullable()();
|
||||
|
||||
TextColumn get oldGroupName => text().nullable()();
|
||||
TextColumn get newGroupName => text().nullable()();
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 4;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
|
|
@ -92,6 +92,17 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
from2To3: (m, schema) async {
|
||||
await m.addColumn(schema.groups, schema.groups.draftMessage);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.groupHistories,
|
||||
columnTransformer: {
|
||||
schema.groupHistories.affectedContactId:
|
||||
schema.groupHistories.affectedContactId,
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1566,9 +1566,390 @@ class Shape17 extends i0.VersionedTable {
|
|||
i1.GeneratedColumn<String> _column_100(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('draft_message', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
|
||||
final class Schema4 extends i0.VersionedSchema {
|
||||
Schema4({required super.database}) : super(version: 4);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
contacts,
|
||||
groups,
|
||||
mediaFiles,
|
||||
messages,
|
||||
messageHistories,
|
||||
reactions,
|
||||
groupMembers,
|
||||
receipts,
|
||||
receivedReceipts,
|
||||
signalIdentityKeyStores,
|
||||
signalPreKeyStores,
|
||||
signalSenderKeyStores,
|
||||
signalSessionStores,
|
||||
signalContactPreKeys,
|
||||
signalContactSignedPreKeys,
|
||||
messageActions,
|
||||
groupHistories,
|
||||
];
|
||||
late final Shape0 contacts = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'contacts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(user_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_4,
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape17 groups = Shape17(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'groups',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(group_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_22,
|
||||
_column_23,
|
||||
_column_24,
|
||||
_column_100,
|
||||
_column_25,
|
||||
_column_26,
|
||||
_column_27,
|
||||
_column_12,
|
||||
_column_28,
|
||||
_column_29,
|
||||
_column_30,
|
||||
_column_31,
|
||||
_column_32,
|
||||
_column_33,
|
||||
_column_34,
|
||||
_column_35,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape2 mediaFiles = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'media_files',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(media_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
_column_45,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape3 messages = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'messages',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(message_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
_column_37,
|
||||
_column_53,
|
||||
_column_54,
|
||||
_column_55,
|
||||
_column_56,
|
||||
_column_46,
|
||||
_column_57,
|
||||
_column_58,
|
||||
_column_59,
|
||||
_column_60,
|
||||
_column_12,
|
||||
_column_61,
|
||||
_column_62,
|
||||
_column_63,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape4 messageHistories = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_64,
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_53,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape5 reactions = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'reactions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(message_id, sender_id, emoji)',
|
||||
],
|
||||
columns: [
|
||||
_column_65,
|
||||
_column_67,
|
||||
_column_68,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape6 groupMembers = Shape6(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_members',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(group_id, contact_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_50,
|
||||
_column_69,
|
||||
_column_70,
|
||||
_column_71,
|
||||
_column_72,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape7 receipts = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(receipt_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_73,
|
||||
_column_74,
|
||||
_column_75,
|
||||
_column_76,
|
||||
_column_77,
|
||||
_column_78,
|
||||
_column_79,
|
||||
_column_80,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape8 receivedReceipts = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'received_receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(receipt_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_73,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape9 signalIdentityKeyStores = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_identity_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(device_id, name)',
|
||||
],
|
||||
columns: [
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape10 signalPreKeyStores = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_pre_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(pre_key_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape11 signalSenderKeyStores = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_sender_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(sender_key_name)',
|
||||
],
|
||||
columns: [
|
||||
_column_86,
|
||||
_column_87,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape12 signalSessionStores = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_session_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(device_id, name)',
|
||||
],
|
||||
columns: [
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_88,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape13 signalContactPreKeys = Shape13(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_contact_pre_keys',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(contact_id, pre_key_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_74,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape14 signalContactSignedPreKeys = Shape14(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_contact_signed_pre_keys',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(contact_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_74,
|
||||
_column_89,
|
||||
_column_90,
|
||||
_column_91,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape15 messageActions = Shape15(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_actions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(message_id, contact_id, type)',
|
||||
],
|
||||
columns: [
|
||||
_column_65,
|
||||
_column_92,
|
||||
_column_37,
|
||||
_column_93,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape16 groupHistories = Shape16(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(group_history_id)',
|
||||
],
|
||||
columns: [
|
||||
_column_94,
|
||||
_column_50,
|
||||
_column_95,
|
||||
_column_101,
|
||||
_column_97,
|
||||
_column_98,
|
||||
_column_99,
|
||||
_column_37,
|
||||
_column_93,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_101(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('affected_contact_id', aliasedName, true,
|
||||
type: i1.DriftSqlType.int);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -1582,6 +1963,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
case 3:
|
||||
final schema = Schema4(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from3To4(migrator, schema);
|
||||
return 4;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
|
|
@ -1591,9 +1977,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
from3To4: from3To4,
|
||||
));
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@
|
|||
"errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.",
|
||||
"errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.",
|
||||
"upgradeToPaidPlan": "Upgrade auf einen kostenpflichtigen Plan.",
|
||||
"upgradeToPaidPlanButton": "Auf {planId} upgraden",
|
||||
"upgradeToPaidPlanButton": "Auf {planId} upgraden{sufix}",
|
||||
"partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!",
|
||||
"errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.",
|
||||
"errorPlanLimitReached": "Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.",
|
||||
|
|
@ -205,17 +205,21 @@
|
|||
"errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.",
|
||||
"errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.",
|
||||
"proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
|
||||
"proFeature2": "1 zusätzlicher Plus Benutzer",
|
||||
"proFeature3": "Flammen wiederherstellen",
|
||||
"proFeature4": "Cloud-Backup verschlüsselt (coming-soon)",
|
||||
"year": "year",
|
||||
"month": "month",
|
||||
"familyFeature1": "✓ Alles von Pro",
|
||||
"familyFeature2": "4 zusätzliche Plus Benutzer",
|
||||
"proFeature2": "✓ 1 zusätzlicher Plus Benutzer",
|
||||
"proFeature3": "✓ Flammen wiederherstellen",
|
||||
"proFeature4": "✓ twonly unterstützen",
|
||||
"year": "Jahr",
|
||||
"month": "Monat",
|
||||
"yearly": "Jährlich",
|
||||
"monthly": "Monatlich",
|
||||
"familyFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
|
||||
"familyFeature2": "✓ 4 zusätzliche Plus Benutzer",
|
||||
"familyFeature3": "✓ Flammen wiederherstellen",
|
||||
"familyFeature4": "✓ twonly unterstützen",
|
||||
"redeemUserInviteCode": "Oder löse einen twonly-Code ein.",
|
||||
"freeFeature1": "10 Medien-Datei-Uploads pro Tag",
|
||||
"freeFeature1": "✓ 10 Medien-Datei-Uploads pro Tag",
|
||||
"plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
|
||||
"plusFeature2": "Zusatzfunktionen (coming-soon)",
|
||||
"plusFeature2": "✓ Zusatzfunktionen (coming-soon)",
|
||||
"transactionHistory": "Transaktionshistorie",
|
||||
"currentBalance": "Dein Guthaben",
|
||||
"manageAdditionalUsers": "Zusätzliche Benutzer verwalten",
|
||||
|
|
@ -409,7 +413,7 @@
|
|||
"notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.",
|
||||
"notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.",
|
||||
"notificationResponse": "hat dir{inGroup} geantwortet.",
|
||||
"notificationTitleUnknownUser": "Jemand",
|
||||
"notificationTitleUnknownUser": "[Unbekannt]",
|
||||
"notificationCategoryMessageTitle": "Nachrichten",
|
||||
"notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.",
|
||||
"groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.",
|
||||
|
|
|
|||
|
|
@ -232,22 +232,26 @@
|
|||
"errorPlanNotAllowed": "This feature is not available in your current plan.",
|
||||
"errorPlanUpgradeNotYearly": "The plan upgrade must be paid for annually, as the current plan is also billed annually.",
|
||||
"upgradeToPaidPlan": "Upgrade to a paid plan.",
|
||||
"upgradeToPaidPlanButton": "Upgrade subscription to {planId}",
|
||||
"upgradeToPaidPlanButton": "Upgrade to {planId}{sufix}",
|
||||
"partOfPaidPlanOf": "You are part of the paid plan of {username}!",
|
||||
"year": "year",
|
||||
"yearly": "Yearly",
|
||||
"month": "month",
|
||||
"monthly": "Monthly",
|
||||
"proFeature1": "✓ Unlimited media file uploads",
|
||||
"proFeature2": "1 additional Plus user",
|
||||
"proFeature3": "Cloud-Backup encrypted (coming-soon)",
|
||||
"proFeature4": "Additional features (coming-soon)",
|
||||
"familyFeature1": "✓ All from Pro",
|
||||
"familyFeature2": "4 additional Plus users",
|
||||
"proFeature2": "✓ 1 additional Plus user",
|
||||
"proFeature3": "✓ Restore flames",
|
||||
"proFeature4": "✓ Support twonly",
|
||||
"familyFeature1": "✓ Unlimited media file uploads",
|
||||
"familyFeature2": "✓ 4 additional Plus user",
|
||||
"familyFeature3": "✓ Restore flames",
|
||||
"familyFeature4": "✓ Support twonly",
|
||||
"redeemUserInviteCode": "Or redeem a twonly-Code.",
|
||||
"redeemUserInviteCodeTitle": "Redeem twonly-Code",
|
||||
"redeemUserInviteCodeSuccess": "Your plan has been successfully adjusted.",
|
||||
"freeFeature1": "10 Media file uploads per day",
|
||||
"freeFeature1": "✓ 10 Media file uploads per day",
|
||||
"plusFeature1": "✓ Unlimited media file uploads",
|
||||
"plusFeature2": "Additional features (coming-soon)",
|
||||
"plusFeature2": "✓ Additional features (coming-soon)",
|
||||
"transactionHistory": "Your transaction history",
|
||||
"manageSubscription": "Manage your subscription",
|
||||
"nextPayment": "Next payment",
|
||||
|
|
@ -439,7 +443,7 @@
|
|||
"notificationReactionToImage": "has reacted with {reaction} to your image.",
|
||||
"notificationReactionToAudio": "has reacted with {reaction} to your audio message.",
|
||||
"notificationResponse": "has responded{inGroup}.",
|
||||
"notificationTitleUnknownUser": "Someone",
|
||||
"notificationTitleUnknownUser": "[Unknown]",
|
||||
"notificationCategoryMessageTitle": "Messages",
|
||||
"notificationCategoryMessageDesc": "Messages from other users.",
|
||||
"groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.",
|
||||
|
|
|
|||
|
|
@ -1313,8 +1313,8 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @upgradeToPaidPlanButton.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Upgrade subscription to {planId}'**
|
||||
String upgradeToPaidPlanButton(Object planId);
|
||||
/// **'Upgrade to {planId}{sufix}'**
|
||||
String upgradeToPaidPlanButton(Object planId, Object sufix);
|
||||
|
||||
/// No description provided for @partOfPaidPlanOf.
|
||||
///
|
||||
|
|
@ -1328,12 +1328,24 @@ abstract class AppLocalizations {
|
|||
/// **'year'**
|
||||
String get year;
|
||||
|
||||
/// No description provided for @yearly.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yearly'**
|
||||
String get yearly;
|
||||
|
||||
/// No description provided for @month.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'month'**
|
||||
String get month;
|
||||
|
||||
/// No description provided for @monthly.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Monthly'**
|
||||
String get monthly;
|
||||
|
||||
/// No description provided for @proFeature1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1343,33 +1355,45 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @proFeature2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'1 additional Plus user'**
|
||||
/// **'✓ 1 additional Plus user'**
|
||||
String get proFeature2;
|
||||
|
||||
/// No description provided for @proFeature3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cloud-Backup encrypted (coming-soon)'**
|
||||
/// **'✓ Restore flames'**
|
||||
String get proFeature3;
|
||||
|
||||
/// No description provided for @proFeature4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Additional features (coming-soon)'**
|
||||
/// **'✓ Support twonly'**
|
||||
String get proFeature4;
|
||||
|
||||
/// No description provided for @familyFeature1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'✓ All from Pro'**
|
||||
/// **'✓ Unlimited media file uploads'**
|
||||
String get familyFeature1;
|
||||
|
||||
/// No description provided for @familyFeature2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'4 additional Plus users'**
|
||||
/// **'✓ 4 additional Plus user'**
|
||||
String get familyFeature2;
|
||||
|
||||
/// No description provided for @familyFeature3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'✓ Restore flames'**
|
||||
String get familyFeature3;
|
||||
|
||||
/// No description provided for @familyFeature4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'✓ Support twonly'**
|
||||
String get familyFeature4;
|
||||
|
||||
/// No description provided for @redeemUserInviteCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1391,7 +1415,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @freeFeature1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'10 Media file uploads per day'**
|
||||
/// **'✓ 10 Media file uploads per day'**
|
||||
String get freeFeature1;
|
||||
|
||||
/// No description provided for @plusFeature1.
|
||||
|
|
@ -1403,7 +1427,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @plusFeature2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Additional features (coming-soon)'**
|
||||
/// **'✓ Additional features (coming-soon)'**
|
||||
String get plusFeature2;
|
||||
|
||||
/// No description provided for @transactionHistory.
|
||||
|
|
@ -2555,7 +2579,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @notificationTitleUnknownUser.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Someone'**
|
||||
/// **'[Unknown]'**
|
||||
String get notificationTitleUnknownUser;
|
||||
|
||||
/// No description provided for @notificationCategoryMessageTitle.
|
||||
|
|
|
|||
|
|
@ -685,8 +685,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get upgradeToPaidPlan => 'Upgrade auf einen kostenpflichtigen Plan.';
|
||||
|
||||
@override
|
||||
String upgradeToPaidPlanButton(Object planId) {
|
||||
return 'Auf $planId upgraden';
|
||||
String upgradeToPaidPlanButton(Object planId, Object sufix) {
|
||||
return 'Auf $planId upgraden$sufix';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -695,28 +695,40 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get year => 'year';
|
||||
String get year => 'Jahr';
|
||||
|
||||
@override
|
||||
String get month => 'month';
|
||||
String get yearly => 'Jährlich';
|
||||
|
||||
@override
|
||||
String get month => 'Monat';
|
||||
|
||||
@override
|
||||
String get monthly => 'Monatlich';
|
||||
|
||||
@override
|
||||
String get proFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
|
||||
|
||||
@override
|
||||
String get proFeature2 => '1 zusätzlicher Plus Benutzer';
|
||||
String get proFeature2 => '✓ 1 zusätzlicher Plus Benutzer';
|
||||
|
||||
@override
|
||||
String get proFeature3 => 'Flammen wiederherstellen';
|
||||
String get proFeature3 => '✓ Flammen wiederherstellen';
|
||||
|
||||
@override
|
||||
String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)';
|
||||
String get proFeature4 => '✓ twonly unterstützen';
|
||||
|
||||
@override
|
||||
String get familyFeature1 => '✓ Alles von Pro';
|
||||
String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
|
||||
|
||||
@override
|
||||
String get familyFeature2 => '4 zusätzliche Plus Benutzer';
|
||||
String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer';
|
||||
|
||||
@override
|
||||
String get familyFeature3 => '✓ Flammen wiederherstellen';
|
||||
|
||||
@override
|
||||
String get familyFeature4 => '✓ twonly unterstützen';
|
||||
|
||||
@override
|
||||
String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.';
|
||||
|
|
@ -729,13 +741,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Dein Plan wurde erfolgreich angepasst.';
|
||||
|
||||
@override
|
||||
String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag';
|
||||
String get freeFeature1 => '✓ 10 Medien-Datei-Uploads pro Tag';
|
||||
|
||||
@override
|
||||
String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
|
||||
|
||||
@override
|
||||
String get plusFeature2 => 'Zusatzfunktionen (coming-soon)';
|
||||
String get plusFeature2 => '✓ Zusatzfunktionen (coming-soon)';
|
||||
|
||||
@override
|
||||
String get transactionHistory => 'Transaktionshistorie';
|
||||
|
|
@ -1409,7 +1421,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get notificationTitleUnknownUser => 'Jemand';
|
||||
String get notificationTitleUnknownUser => '[Unbekannt]';
|
||||
|
||||
@override
|
||||
String get notificationCategoryMessageTitle => 'Nachrichten';
|
||||
|
|
|
|||
|
|
@ -679,8 +679,8 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get upgradeToPaidPlan => 'Upgrade to a paid plan.';
|
||||
|
||||
@override
|
||||
String upgradeToPaidPlanButton(Object planId) {
|
||||
return 'Upgrade subscription to $planId';
|
||||
String upgradeToPaidPlanButton(Object planId, Object sufix) {
|
||||
return 'Upgrade to $planId$sufix';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -691,26 +691,38 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get year => 'year';
|
||||
|
||||
@override
|
||||
String get yearly => 'Yearly';
|
||||
|
||||
@override
|
||||
String get month => 'month';
|
||||
|
||||
@override
|
||||
String get monthly => 'Monthly';
|
||||
|
||||
@override
|
||||
String get proFeature1 => '✓ Unlimited media file uploads';
|
||||
|
||||
@override
|
||||
String get proFeature2 => '1 additional Plus user';
|
||||
String get proFeature2 => '✓ 1 additional Plus user';
|
||||
|
||||
@override
|
||||
String get proFeature3 => 'Cloud-Backup encrypted (coming-soon)';
|
||||
String get proFeature3 => '✓ Restore flames';
|
||||
|
||||
@override
|
||||
String get proFeature4 => 'Additional features (coming-soon)';
|
||||
String get proFeature4 => '✓ Support twonly';
|
||||
|
||||
@override
|
||||
String get familyFeature1 => '✓ All from Pro';
|
||||
String get familyFeature1 => '✓ Unlimited media file uploads';
|
||||
|
||||
@override
|
||||
String get familyFeature2 => '4 additional Plus users';
|
||||
String get familyFeature2 => '✓ 4 additional Plus user';
|
||||
|
||||
@override
|
||||
String get familyFeature3 => '✓ Restore flames';
|
||||
|
||||
@override
|
||||
String get familyFeature4 => '✓ Support twonly';
|
||||
|
||||
@override
|
||||
String get redeemUserInviteCode => 'Or redeem a twonly-Code.';
|
||||
|
|
@ -723,13 +735,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
'Your plan has been successfully adjusted.';
|
||||
|
||||
@override
|
||||
String get freeFeature1 => '10 Media file uploads per day';
|
||||
String get freeFeature1 => '✓ 10 Media file uploads per day';
|
||||
|
||||
@override
|
||||
String get plusFeature1 => '✓ Unlimited media file uploads';
|
||||
|
||||
@override
|
||||
String get plusFeature2 => 'Additional features (coming-soon)';
|
||||
String get plusFeature2 => '✓ Additional features (coming-soon)';
|
||||
|
||||
@override
|
||||
String get transactionHistory => 'Your transaction history';
|
||||
|
|
@ -1401,7 +1413,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get notificationTitleUnknownUser => 'Someone';
|
||||
String get notificationTitleUnknownUser => '[Unknown]';
|
||||
|
||||
@override
|
||||
String get notificationCategoryMessageTitle => 'Messages';
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ class UserData {
|
|||
|
||||
@JsonKey(defaultValue: 'Free')
|
||||
String subscriptionPlan;
|
||||
|
||||
String? subscriptionPlanIdStore;
|
||||
DateTime? lastImageSend;
|
||||
int? todaysImageCounter;
|
||||
|
||||
|
|
@ -86,8 +88,11 @@ class UserData {
|
|||
|
||||
List<int>? lastChangeLogHash;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool hideChangeLog = false;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool hideChangeLog = true;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool updateFCMToken = true;
|
||||
|
||||
// --- BACKUP ---
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..disableVideoCompression =
|
||||
json['disableVideoCompression'] as bool? ?? false
|
||||
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
||||
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
|
||||
..lastImageSend = json['lastImageSend'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastImageSend'] as String)
|
||||
|
|
@ -61,6 +62,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..hideChangeLog = json['hideChangeLog'] as bool? ?? false
|
||||
..updateFCMToken = json['updateFCMToken'] as bool? ?? true
|
||||
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
|
||||
? null
|
||||
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
|
||||
|
|
@ -84,6 +86,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'disableVideoCompression': instance.disableVideoCompression,
|
||||
'deviceId': instance.deviceId,
|
||||
'subscriptionPlan': instance.subscriptionPlan,
|
||||
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
|
||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||
'todaysImageCounter': instance.todaysImageCounter,
|
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||
|
|
@ -104,6 +107,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
|
||||
'lastChangeLogHash': instance.lastChangeLogHash,
|
||||
'hideChangeLog': instance.hideChangeLog,
|
||||
'updateFCMToken': instance.updateFCMToken,
|
||||
'nextTimeToShowBackupNotice':
|
||||
instance.nextTimeToShowBackupNotice?.toIso8601String(),
|
||||
'backupServer': instance.backupServer,
|
||||
|
|
|
|||
|
|
@ -2085,6 +2085,136 @@ class ApplicationData_ReportUser extends $pb.GeneratedMessage {
|
|||
void clearReason() => $_clearField(2);
|
||||
}
|
||||
|
||||
class ApplicationData_IPAPurchase extends $pb.GeneratedMessage {
|
||||
factory ApplicationData_IPAPurchase({
|
||||
$core.String? productId,
|
||||
$core.String? source,
|
||||
$core.String? verificationData,
|
||||
}) {
|
||||
final result = create();
|
||||
if (productId != null) result.productId = productId;
|
||||
if (source != null) result.source = source;
|
||||
if (verificationData != null) result.verificationData = verificationData;
|
||||
return result;
|
||||
}
|
||||
|
||||
ApplicationData_IPAPurchase._();
|
||||
|
||||
factory ApplicationData_IPAPurchase.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ApplicationData_IPAPurchase.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ApplicationData.IPAPurchase',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'productId')
|
||||
..aOS(2, _omitFieldNames ? '' : 'source')
|
||||
..aOS(3, _omitFieldNames ? '' : 'verificationData')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_IPAPurchase clone() =>
|
||||
ApplicationData_IPAPurchase()..mergeFromMessage(this);
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_IPAPurchase copyWith(
|
||||
void Function(ApplicationData_IPAPurchase) updates) =>
|
||||
super.copyWith(
|
||||
(message) => updates(message as ApplicationData_IPAPurchase))
|
||||
as ApplicationData_IPAPurchase;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_IPAPurchase create() =>
|
||||
ApplicationData_IPAPurchase._();
|
||||
@$core.override
|
||||
ApplicationData_IPAPurchase createEmptyInstance() => create();
|
||||
static $pb.PbList<ApplicationData_IPAPurchase> createRepeated() =>
|
||||
$pb.PbList<ApplicationData_IPAPurchase>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_IPAPurchase getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<ApplicationData_IPAPurchase>(create);
|
||||
static ApplicationData_IPAPurchase? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get productId => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set productId($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasProductId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearProductId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get source => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set source($core.String value) => $_setString(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasSource() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearSource() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.String get verificationData => $_getSZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set verificationData($core.String value) => $_setString(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasVerificationData() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearVerificationData() => $_clearField(3);
|
||||
}
|
||||
|
||||
class ApplicationData_IPAForceCheck extends $pb.GeneratedMessage {
|
||||
factory ApplicationData_IPAForceCheck() => create();
|
||||
|
||||
ApplicationData_IPAForceCheck._();
|
||||
|
||||
factory ApplicationData_IPAForceCheck.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ApplicationData_IPAForceCheck.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ApplicationData.IPAForceCheck',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_IPAForceCheck clone() =>
|
||||
ApplicationData_IPAForceCheck()..mergeFromMessage(this);
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_IPAForceCheck copyWith(
|
||||
void Function(ApplicationData_IPAForceCheck) updates) =>
|
||||
super.copyWith(
|
||||
(message) => updates(message as ApplicationData_IPAForceCheck))
|
||||
as ApplicationData_IPAForceCheck;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_IPAForceCheck create() =>
|
||||
ApplicationData_IPAForceCheck._();
|
||||
@$core.override
|
||||
ApplicationData_IPAForceCheck createEmptyInstance() => create();
|
||||
static $pb.PbList<ApplicationData_IPAForceCheck> createRepeated() =>
|
||||
$pb.PbList<ApplicationData_IPAForceCheck>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_IPAForceCheck getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<ApplicationData_IPAForceCheck>(create);
|
||||
static ApplicationData_IPAForceCheck? _defaultInstance;
|
||||
}
|
||||
|
||||
class ApplicationData_DeleteAccount extends $pb.GeneratedMessage {
|
||||
factory ApplicationData_DeleteAccount() => create();
|
||||
|
||||
|
|
@ -2153,6 +2283,8 @@ enum ApplicationData_ApplicationData {
|
|||
deleteAccount,
|
||||
reportUser,
|
||||
changeUsername,
|
||||
ipaPurchase,
|
||||
ipaForceCheck,
|
||||
notSet
|
||||
}
|
||||
|
||||
|
|
@ -2180,6 +2312,8 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
ApplicationData_DeleteAccount? deleteAccount,
|
||||
ApplicationData_ReportUser? reportUser,
|
||||
ApplicationData_ChangeUsername? changeUsername,
|
||||
ApplicationData_IPAPurchase? ipaPurchase,
|
||||
ApplicationData_IPAForceCheck? ipaForceCheck,
|
||||
}) {
|
||||
final result = create();
|
||||
if (textMessage != null) result.textMessage = textMessage;
|
||||
|
|
@ -2212,6 +2346,8 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
if (deleteAccount != null) result.deleteAccount = deleteAccount;
|
||||
if (reportUser != null) result.reportUser = reportUser;
|
||||
if (changeUsername != null) result.changeUsername = changeUsername;
|
||||
if (ipaPurchase != null) result.ipaPurchase = ipaPurchase;
|
||||
if (ipaForceCheck != null) result.ipaForceCheck = ipaForceCheck;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -2248,6 +2384,8 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
24: ApplicationData_ApplicationData.deleteAccount,
|
||||
25: ApplicationData_ApplicationData.reportUser,
|
||||
26: ApplicationData_ApplicationData.changeUsername,
|
||||
27: ApplicationData_ApplicationData.ipaPurchase,
|
||||
28: ApplicationData_ApplicationData.ipaForceCheck,
|
||||
0: ApplicationData_ApplicationData.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
|
|
@ -2277,7 +2415,9 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
23,
|
||||
24,
|
||||
25,
|
||||
26
|
||||
26,
|
||||
27,
|
||||
28
|
||||
])
|
||||
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
|
||||
protoName: 'textMessage',
|
||||
|
|
@ -2361,6 +2501,13 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
26, _omitFieldNames ? '' : 'changeUsername',
|
||||
protoName: 'changeUsername',
|
||||
subBuilder: ApplicationData_ChangeUsername.create)
|
||||
..aOM<ApplicationData_IPAPurchase>(27, _omitFieldNames ? '' : 'ipaPurchase',
|
||||
protoName: 'ipaPurchase',
|
||||
subBuilder: ApplicationData_IPAPurchase.create)
|
||||
..aOM<ApplicationData_IPAForceCheck>(
|
||||
28, _omitFieldNames ? '' : 'ipaForceCheck',
|
||||
protoName: 'ipaForceCheck',
|
||||
subBuilder: ApplicationData_IPAForceCheck.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -2652,6 +2799,29 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
void clearChangeUsername() => $_clearField(26);
|
||||
@$pb.TagNumber(26)
|
||||
ApplicationData_ChangeUsername ensureChangeUsername() => $_ensure(21);
|
||||
|
||||
@$pb.TagNumber(27)
|
||||
ApplicationData_IPAPurchase get ipaPurchase => $_getN(22);
|
||||
@$pb.TagNumber(27)
|
||||
set ipaPurchase(ApplicationData_IPAPurchase value) => $_setField(27, value);
|
||||
@$pb.TagNumber(27)
|
||||
$core.bool hasIpaPurchase() => $_has(22);
|
||||
@$pb.TagNumber(27)
|
||||
void clearIpaPurchase() => $_clearField(27);
|
||||
@$pb.TagNumber(27)
|
||||
ApplicationData_IPAPurchase ensureIpaPurchase() => $_ensure(22);
|
||||
|
||||
@$pb.TagNumber(28)
|
||||
ApplicationData_IPAForceCheck get ipaForceCheck => $_getN(23);
|
||||
@$pb.TagNumber(28)
|
||||
set ipaForceCheck(ApplicationData_IPAForceCheck value) =>
|
||||
$_setField(28, value);
|
||||
@$pb.TagNumber(28)
|
||||
$core.bool hasIpaForceCheck() => $_has(23);
|
||||
@$pb.TagNumber(28)
|
||||
void clearIpaForceCheck() => $_clearField(28);
|
||||
@$pb.TagNumber(28)
|
||||
ApplicationData_IPAForceCheck ensureIpaForceCheck() => $_ensure(23);
|
||||
}
|
||||
|
||||
class Response_PreKey extends $pb.GeneratedMessage {
|
||||
|
|
|
|||
|
|
@ -461,6 +461,24 @@ const ApplicationData$json = {
|
|||
'9': 0,
|
||||
'10': 'changeUsername'
|
||||
},
|
||||
{
|
||||
'1': 'ipaPurchase',
|
||||
'3': 27,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.client_to_server.ApplicationData.IPAPurchase',
|
||||
'9': 0,
|
||||
'10': 'ipaPurchase'
|
||||
},
|
||||
{
|
||||
'1': 'ipaForceCheck',
|
||||
'3': 28,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.client_to_server.ApplicationData.IPAForceCheck',
|
||||
'9': 0,
|
||||
'10': 'ipaForceCheck'
|
||||
},
|
||||
],
|
||||
'3': [
|
||||
ApplicationData_TextMessage$json,
|
||||
|
|
@ -484,6 +502,8 @@ const ApplicationData$json = {
|
|||
ApplicationData_UpdateSignedPreKey$json,
|
||||
ApplicationData_DownloadDone$json,
|
||||
ApplicationData_ReportUser$json,
|
||||
ApplicationData_IPAPurchase$json,
|
||||
ApplicationData_IPAForceCheck$json,
|
||||
ApplicationData_DeleteAccount$json
|
||||
],
|
||||
'8': [
|
||||
|
|
@ -668,6 +688,27 @@ const ApplicationData_ReportUser$json = {
|
|||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use applicationDataDescriptor instead')
|
||||
const ApplicationData_IPAPurchase$json = {
|
||||
'1': 'IPAPurchase',
|
||||
'2': [
|
||||
{'1': 'product_id', '3': 1, '4': 1, '5': 9, '10': 'productId'},
|
||||
{'1': 'source', '3': 2, '4': 1, '5': 9, '10': 'source'},
|
||||
{
|
||||
'1': 'verification_data',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 9,
|
||||
'10': 'verificationData'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use applicationDataDescriptor instead')
|
||||
const ApplicationData_IPAForceCheck$json = {
|
||||
'1': 'IPAForceCheck',
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use applicationDataDescriptor instead')
|
||||
const ApplicationData_DeleteAccount$json = {
|
||||
'1': 'DeleteAccount',
|
||||
|
|
@ -713,29 +754,34 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
|
|||
'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVBY2NvdW50Ek4KCnJlcG9ydFVzZXIY'
|
||||
'GSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZXBvcnRVc2VySABSCn'
|
||||
'JlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsyMC5jbGllbnRfdG9fc2VydmVyLkFw'
|
||||
'cGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZRpqCgtUZXh0TW'
|
||||
'Vzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglw'
|
||||
'dXNoX2RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQn'
|
||||
'lVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUS'
|
||||
'GgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCm'
|
||||
'dvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEg'
|
||||
'ASgDUgZ1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGn'
|
||||
'AKEVN3aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9u'
|
||||
'dGhseRgCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2'
|
||||
'FsGjYKEVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3'
|
||||
'YWwaMAoNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCg'
|
||||
'tHZXRMb2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRB'
|
||||
'ZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdG'
|
||||
'lvbmFsQ29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRp'
|
||||
'dGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlck'
|
||||
'lkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIX'
|
||||
'Cgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZW'
|
||||
'RfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgM'
|
||||
'UgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZF'
|
||||
'ByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxS'
|
||||
'DWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg'
|
||||
'5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhoPCg1EZWxldGVBY2NvdW50'
|
||||
'QhEKD0FwcGxpY2F0aW9uRGF0YQ==');
|
||||
'cGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZRJRCgtpcGFQdX'
|
||||
'JjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLklQQVB1cmNo'
|
||||
'YXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2sYHCABKAsyLy5jbGllbnRfdG9fc2'
|
||||
'VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNrSABSDWlwYUZvcmNlQ2hlY2saagoL'
|
||||
'VGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZH'
|
||||
'kSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0'
|
||||
'VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZX'
|
||||
'JuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tl'
|
||||
'bhIdCgpnb29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl'
|
||||
'9pZBgBIAEoA1IGdXNlcklkGikKDVJlZGVlbVZvdWNoZXISGAoHdm91Y2hlchgBIAEoCVIHdm91'
|
||||
'Y2hlchpwChFTd2l0Y2hUb1BheWVkUGxhbhIXCgdwbGFuX2lkGAEgASgJUgZwbGFuSWQSHwoLcG'
|
||||
'F5X21vbnRobHkYAiABKAhSCnBheU1vbnRobHkSIQoMYXV0b19yZW5ld2FsGAMgASgIUgthdXRv'
|
||||
'UmVuZXdhbBo2ChFVcGRhdGVQbGFuT3B0aW9ucxIhCgxhdXRvX3JlbmV3YWwYASABKAhSC2F1dG'
|
||||
'9SZW5ld2FsGjAKDUNyZWF0ZVZvdWNoZXISHwoLdmFsdWVfY2VudHMYASABKA1SCnZhbHVlQ2Vu'
|
||||
'dHMaDQoLR2V0TG9jYXRpb24aDQoLR2V0Vm91Y2hlcnMaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFw'
|
||||
'oVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUKE0dldEN1cnJlbnRQbGFuSW5mb3MaNwoUUmVkZWVt'
|
||||
'QWRkaXRpb25hbENvZGUSHwoLaW52aXRlX2NvZGUYAiABKAlSCmludml0ZUNvZGUaLwoUUmVtb3'
|
||||
'ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNC'
|
||||
'eVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2'
|
||||
'VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQ'
|
||||
'c2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleR'
|
||||
'gCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVz'
|
||||
'aWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGA'
|
||||
'EgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgB'
|
||||
'IAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2'
|
||||
'hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291'
|
||||
'cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQU'
|
||||
'ZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudEIRCg9BcHBsaWNhdGlvbkRhdGE=');
|
||||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response$json = {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork');
|
||||
static const ErrorCode RegistrationDisabled =
|
||||
ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled');
|
||||
static const ErrorCode IPAPaymentExpired =
|
||||
ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired');
|
||||
|
||||
static const $core.List<ErrorCode> values = <ErrorCode>[
|
||||
Unknown,
|
||||
|
|
@ -125,6 +127,7 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
NewDeviceRegistered,
|
||||
InvalidProofOfWork,
|
||||
RegistrationDisabled,
|
||||
IPAPaymentExpired,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ErrorCode> _byValue =
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const ErrorCode$json = {
|
|||
{'1': 'NewDeviceRegistered', '2': 1031},
|
||||
{'1': 'InvalidProofOfWork', '2': 1032},
|
||||
{'1': 'RegistrationDisabled', '2': 1033},
|
||||
{'1': 'IPAPaymentExpired', '2': 1034},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -74,4 +75,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
|
|||
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
|
||||
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
|
||||
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
|
||||
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCA==');
|
||||
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
|
||||
'UEFQYXltZW50RXhwaXJlZBCKCA==');
|
||||
|
|
|
|||
|
|
@ -92,7 +92,14 @@ class ServerToClient extends $pb.GeneratedMessage {
|
|||
V0 ensureV0() => $_ensure(0);
|
||||
}
|
||||
|
||||
enum V0_Kind { response, newMessage, requestNewPreKeys, error, notSet }
|
||||
enum V0_Kind {
|
||||
response,
|
||||
newMessage,
|
||||
requestNewPreKeys,
|
||||
error,
|
||||
newMessages,
|
||||
notSet
|
||||
}
|
||||
|
||||
class V0 extends $pb.GeneratedMessage {
|
||||
factory V0({
|
||||
|
|
@ -101,6 +108,7 @@ class V0 extends $pb.GeneratedMessage {
|
|||
NewMessage? newMessage,
|
||||
$core.bool? requestNewPreKeys,
|
||||
$0.ErrorCode? error,
|
||||
NewMessages? newMessages,
|
||||
}) {
|
||||
final result = create();
|
||||
if (seq != null) result.seq = seq;
|
||||
|
|
@ -108,6 +116,7 @@ class V0 extends $pb.GeneratedMessage {
|
|||
if (newMessage != null) result.newMessage = newMessage;
|
||||
if (requestNewPreKeys != null) result.requestNewPreKeys = requestNewPreKeys;
|
||||
if (error != null) result.error = error;
|
||||
if (newMessages != null) result.newMessages = newMessages;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +134,7 @@ class V0 extends $pb.GeneratedMessage {
|
|||
3: V0_Kind.newMessage,
|
||||
4: V0_Kind.requestNewPreKeys,
|
||||
6: V0_Kind.error,
|
||||
7: V0_Kind.newMessages,
|
||||
0: V0_Kind.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
|
|
@ -132,7 +142,7 @@ class V0 extends $pb.GeneratedMessage {
|
|||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
||||
createEmptyInstance: create)
|
||||
..oo(0, [2, 3, 4, 6])
|
||||
..oo(0, [2, 3, 4, 6, 7])
|
||||
..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6,
|
||||
defaultOrMaker: $fixnum.Int64.ZERO)
|
||||
..aOM<Response>(2, _omitFieldNames ? '' : 'response',
|
||||
|
|
@ -145,6 +155,8 @@ class V0 extends $pb.GeneratedMessage {
|
|||
defaultOrMaker: $0.ErrorCode.Unknown,
|
||||
valueOf: $0.ErrorCode.valueOf,
|
||||
enumValues: $0.ErrorCode.values)
|
||||
..aOM<NewMessages>(7, _omitFieldNames ? '' : 'newMessages',
|
||||
protoName: 'newMessages', subBuilder: NewMessages.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -217,6 +229,17 @@ class V0 extends $pb.GeneratedMessage {
|
|||
$core.bool hasError() => $_has(4);
|
||||
@$pb.TagNumber(6)
|
||||
void clearError() => $_clearField(6);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
NewMessages get newMessages => $_getN(5);
|
||||
@$pb.TagNumber(7)
|
||||
set newMessages(NewMessages value) => $_setField(7, value);
|
||||
@$pb.TagNumber(7)
|
||||
$core.bool hasNewMessages() => $_has(5);
|
||||
@$pb.TagNumber(7)
|
||||
void clearNewMessages() => $_clearField(7);
|
||||
@$pb.TagNumber(7)
|
||||
NewMessages ensureNewMessages() => $_ensure(5);
|
||||
}
|
||||
|
||||
class NewMessage extends $pb.GeneratedMessage {
|
||||
|
|
@ -287,6 +310,58 @@ class NewMessage extends $pb.GeneratedMessage {
|
|||
void clearFromUserId() => $_clearField(2);
|
||||
}
|
||||
|
||||
class NewMessages extends $pb.GeneratedMessage {
|
||||
factory NewMessages({
|
||||
$core.Iterable<NewMessage>? newMessages,
|
||||
}) {
|
||||
final result = create();
|
||||
if (newMessages != null) result.newMessages.addAll(newMessages);
|
||||
return result;
|
||||
}
|
||||
|
||||
NewMessages._();
|
||||
|
||||
factory NewMessages.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory NewMessages.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'NewMessages',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
||||
createEmptyInstance: create)
|
||||
..pc<NewMessage>(
|
||||
1, _omitFieldNames ? '' : 'newMessages', $pb.PbFieldType.PM,
|
||||
protoName: 'newMessages', subBuilder: NewMessage.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
NewMessages clone() => NewMessages()..mergeFromMessage(this);
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
NewMessages copyWith(void Function(NewMessages) updates) =>
|
||||
super.copyWith((message) => updates(message as NewMessages))
|
||||
as NewMessages;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static NewMessages create() => NewMessages._();
|
||||
@$core.override
|
||||
NewMessages createEmptyInstance() => create();
|
||||
static $pb.PbList<NewMessages> createRepeated() => $pb.PbList<NewMessages>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static NewMessages getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<NewMessages>(create);
|
||||
static NewMessages? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$pb.PbList<NewMessage> get newMessages => $_getList(0);
|
||||
}
|
||||
|
||||
class Response_Authenticated extends $pb.GeneratedMessage {
|
||||
factory Response_Authenticated({
|
||||
$core.String? plan,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,15 @@ const V0$json = {
|
|||
'9': 0,
|
||||
'10': 'newMessage'
|
||||
},
|
||||
{
|
||||
'1': 'newMessages',
|
||||
'3': 7,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.server_to_client.NewMessages',
|
||||
'9': 0,
|
||||
'10': 'newMessages'
|
||||
},
|
||||
{
|
||||
'1': 'RequestNewPreKeys',
|
||||
'3': 4,
|
||||
|
|
@ -88,9 +97,10 @@ const V0$json = {
|
|||
final $typed_data.Uint8List v0Descriptor = $convert.base64Decode(
|
||||
'CgJWMBIQCgNzZXEYASABKARSA3NlcRI4CghyZXNwb25zZRgCIAEoCzIaLnNlcnZlcl90b19jbG'
|
||||
'llbnQuUmVzcG9uc2VIAFIIcmVzcG9uc2USPgoKbmV3TWVzc2FnZRgDIAEoCzIcLnNlcnZlcl90'
|
||||
'b19jbGllbnQuTmV3TWVzc2FnZUgAUgpuZXdNZXNzYWdlEi4KEVJlcXVlc3ROZXdQcmVLZXlzGA'
|
||||
'QgASgISABSEVJlcXVlc3ROZXdQcmVLZXlzEigKBWVycm9yGAYgASgOMhAuZXJyb3IuRXJyb3JD'
|
||||
'b2RlSABSBWVycm9yQgYKBEtpbmQ=');
|
||||
'b19jbGllbnQuTmV3TWVzc2FnZUgAUgpuZXdNZXNzYWdlEkEKC25ld01lc3NhZ2VzGAcgASgLMh'
|
||||
'0uc2VydmVyX3RvX2NsaWVudC5OZXdNZXNzYWdlc0gAUgtuZXdNZXNzYWdlcxIuChFSZXF1ZXN0'
|
||||
'TmV3UHJlS2V5cxgEIAEoCEgAUhFSZXF1ZXN0TmV3UHJlS2V5cxIoCgVlcnJvchgGIAEoDjIQLm'
|
||||
'Vycm9yLkVycm9yQ29kZUgAUgVlcnJvckIGCgRLaW5k');
|
||||
|
||||
@$core.Deprecated('Use newMessageDescriptor instead')
|
||||
const NewMessage$json = {
|
||||
|
|
@ -106,6 +116,26 @@ final $typed_data.Uint8List newMessageDescriptor = $convert.base64Decode(
|
|||
'CgpOZXdNZXNzYWdlEiAKDGZyb21fdXNlcl9pZBgCIAEoA1IKZnJvbVVzZXJJZBISCgRib2R5GA'
|
||||
'EgASgMUgRib2R5');
|
||||
|
||||
@$core.Deprecated('Use newMessagesDescriptor instead')
|
||||
const NewMessages$json = {
|
||||
'1': 'NewMessages',
|
||||
'2': [
|
||||
{
|
||||
'1': 'newMessages',
|
||||
'3': 1,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.server_to_client.NewMessage',
|
||||
'10': 'newMessages'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `NewMessages`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List newMessagesDescriptor = $convert.base64Decode(
|
||||
'CgtOZXdNZXNzYWdlcxI+CgtuZXdNZXNzYWdlcxgBIAMoCzIcLnNlcnZlcl90b19jbGllbnQuTm'
|
||||
'V3TWVzc2FnZVILbmV3TWVzc2FnZXM=');
|
||||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response$json = {
|
||||
'1': 'Response',
|
||||
|
|
|
|||
13
lib/src/model/purchases/purchasable_product.dart
Normal file
13
lib/src/model/purchases/purchasable_product.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
|
||||
enum ProductStatus { purchasable, purchased, pending }
|
||||
|
||||
class PurchasableProduct {
|
||||
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
|
||||
String get id => productDetails.id;
|
||||
String get title => productDetails.title;
|
||||
String get description => productDetails.description;
|
||||
String get price => productDetails.price;
|
||||
ProductStatus status;
|
||||
ProductDetails productDetails;
|
||||
}
|
||||
|
|
@ -1,17 +1,10 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
|
||||
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
bool _isConnected = false;
|
||||
bool get isConnected => _isConnected;
|
||||
SubscriptionPlan plan = SubscriptionPlan.Free;
|
||||
Future<void> updateConnectionState(bool update) async {
|
||||
_isConnected = update;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updatePlan(SubscriptionPlan newPlan) async {
|
||||
plan = newPlan;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
212
lib/src/providers/purchases.provider.dart
Normal file
212
lib/src/providers/purchases.provider.dart
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:twonly/globals.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/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
// Gives the option to override in tests.
|
||||
class IAPConnection {
|
||||
static InAppPurchase? _instance;
|
||||
static set instance(InAppPurchase value) {
|
||||
_instance = value;
|
||||
}
|
||||
|
||||
static InAppPurchase get instance {
|
||||
_instance ??= InAppPurchase.instance;
|
||||
return _instance!;
|
||||
}
|
||||
}
|
||||
|
||||
enum StoreState { loading, available, notAvailable }
|
||||
|
||||
Timer? globalForceIpaCheck;
|
||||
|
||||
class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
PurchasesProvider() {
|
||||
final purchaseUpdated = iapConnection.purchaseStream;
|
||||
_subscription = purchaseUpdated.listen(
|
||||
_onPurchaseUpdate,
|
||||
onDone: _updateStreamOnDone,
|
||||
onError: _updateStreamOnError,
|
||||
);
|
||||
|
||||
loadPurchases();
|
||||
}
|
||||
|
||||
SubscriptionPlan plan = SubscriptionPlan.Free;
|
||||
StoreState storeState = StoreState.loading;
|
||||
List<PurchasableProduct> products = [];
|
||||
|
||||
late StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
final InAppPurchase iapConnection = IAPConnection.instance;
|
||||
|
||||
bool _userTriggeredBuyButton = false;
|
||||
|
||||
void updatePlan(SubscriptionPlan newPlan) {
|
||||
plan = newPlan;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadPurchases() async {
|
||||
final available = await iapConnection.isAvailable();
|
||||
if (!available) {
|
||||
storeState = StoreState.notAvailable;
|
||||
Log.error('Store is not available');
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
const ids = <String>{
|
||||
SubscriptionKeys.proMonthly,
|
||||
SubscriptionKeys.proYearly,
|
||||
SubscriptionKeys.familyYearly,
|
||||
};
|
||||
final response = await iapConnection.queryProductDetails(ids);
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
Log.error(response.notFoundIDs);
|
||||
}
|
||||
products = response.productDetails.map(PurchasableProduct.new).toList();
|
||||
if (products.isEmpty) {
|
||||
Log.error('Could not load any products from the store!');
|
||||
}
|
||||
storeState = StoreState.available;
|
||||
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();
|
||||
}
|
||||
|
||||
Future<void> buy(PurchasableProduct product) async {
|
||||
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
|
||||
switch (product.id) {
|
||||
// case storeKeyConsumable:
|
||||
// await iapConnection.buyConsumable(purchaseParam: purchaseParam);
|
||||
case SubscriptionKeys.proMonthly:
|
||||
case SubscriptionKeys.proYearly:
|
||||
case SubscriptionKeys.familyYearly:
|
||||
_userTriggeredBuyButton = true;
|
||||
Log.info('User wants to buy ${product.id}');
|
||||
|
||||
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
default:
|
||||
throw ArgumentError.value(
|
||||
product.productDetails,
|
||||
'${product.id} is not a known product',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPurchaseUpdate(
|
||||
List<PurchaseDetails> purchaseDetailsList,
|
||||
) async {
|
||||
for (final purchaseDetails in purchaseDetailsList) {
|
||||
await _handlePurchase(purchaseDetails);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
|
||||
if (kDebugMode) {
|
||||
Log.info(purchaseDetails.productID);
|
||||
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(
|
||||
purchaseDetails.productID,
|
||||
purchaseDetails.verificationData.source,
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
|
||||
Log.info(
|
||||
'_handlePurchase: ${purchaseDetails.productID}, ${purchaseDetails.status}',
|
||||
);
|
||||
if (purchaseDetails.status == PurchaseStatus.purchased) {
|
||||
await _verifyPurchase(purchaseDetails);
|
||||
}
|
||||
if (purchaseDetails.status == PurchaseStatus.restored &&
|
||||
purchaseDetails.error == null) {
|
||||
globalForceIpaCheck?.cancel();
|
||||
|
||||
final user = await getUser();
|
||||
|
||||
if (user != null &&
|
||||
(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (purchaseDetails.status == PurchaseStatus.error) {
|
||||
await iapConnection.restorePurchases();
|
||||
}
|
||||
|
||||
if (purchaseDetails.pendingCompletePurchase) {
|
||||
await iapConnection.completePurchase(purchaseDetails);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateStreamOnDone() {
|
||||
_subscription.cancel();
|
||||
}
|
||||
|
||||
void _updateStreamOnError(dynamic error) {
|
||||
// Handle error here
|
||||
}
|
||||
}
|
||||
|
|
@ -98,12 +98,13 @@ class ApiService {
|
|||
unawaited(signalHandleNewServerConnection());
|
||||
unawaited(fetchGroupStatesForUnjoinedGroups());
|
||||
unawaited(fetchMissingGroupPublicKey());
|
||||
unawaited(checkForDeletedUsernames());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onConnected() async {
|
||||
await authenticate();
|
||||
_reconnectionDelay = 5;
|
||||
_reconnectionDelay = 1;
|
||||
globalCallbackConnectionState(isConnected: true);
|
||||
}
|
||||
|
||||
|
|
@ -112,16 +113,21 @@ class ApiService {
|
|||
isAuthenticated = false;
|
||||
globalCallbackConnectionState(isConnected: false);
|
||||
await twonlyDB.mediaFilesDao.resetPendingDownloadState();
|
||||
await startReconnectionTimer();
|
||||
}
|
||||
|
||||
Future<void> startReconnectionTimer() async {
|
||||
if (reconnectionTimer?.isActive ?? false) {
|
||||
return;
|
||||
}
|
||||
reconnectionTimer?.cancel();
|
||||
reconnectionTimer ??=
|
||||
Timer(Duration(seconds: _reconnectionDelay), () async {
|
||||
Log.info('Starting reconnection timer with $_reconnectionDelay s delay');
|
||||
reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async {
|
||||
Log.info('Reconnection timer triggered');
|
||||
reconnectionTimer = null;
|
||||
await connect(force: true);
|
||||
await connect();
|
||||
});
|
||||
_reconnectionDelay += 5;
|
||||
_reconnectionDelay = 3;
|
||||
}
|
||||
|
||||
Future<void> close(Function callback) async {
|
||||
|
|
@ -143,18 +149,13 @@ class ApiService {
|
|||
.onConnectivityChanged
|
||||
.listen((List<ConnectivityResult> result) async {
|
||||
if (!result.contains(ConnectivityResult.none)) {
|
||||
await connect(force: true);
|
||||
await connect();
|
||||
}
|
||||
// Received changes in available connectivity types!
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> connect({bool force = false}) async {
|
||||
if (reconnectionTimer != null && !force) {
|
||||
return false;
|
||||
}
|
||||
reconnectionTimer?.cancel();
|
||||
reconnectionTimer = null;
|
||||
Future<bool> connect() async {
|
||||
return lockConnecting.protect<bool>(() async {
|
||||
if (_channel != null) {
|
||||
return true;
|
||||
|
|
@ -290,6 +291,7 @@ class ApiService {
|
|||
if (_channel == null) {
|
||||
Log.warn('sending request while api is not connected');
|
||||
if (!await connect()) {
|
||||
Log.warn('could not connected again');
|
||||
return Result.error(ErrorCode.InternalError);
|
||||
}
|
||||
if (_channel == null) {
|
||||
|
|
@ -300,6 +302,17 @@ class ApiService {
|
|||
_channel!.sink.add(requestBytes);
|
||||
|
||||
final res = asResult(await _waitForResponse(seq));
|
||||
if (res.isSuccess) {
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasAuthenticated()) {
|
||||
final authenticated = ok.authenticated;
|
||||
await updateUserdata((user) {
|
||||
user.subscriptionPlan = authenticated.plan;
|
||||
return user;
|
||||
});
|
||||
globalCallbackUpdatePlan(planFromString(authenticated.plan));
|
||||
}
|
||||
}
|
||||
if (res.isError) {
|
||||
Log.warn('Got error from server: ${res.error}');
|
||||
if (res.error == ErrorCode.AppVersionOutdated) {
|
||||
|
|
@ -339,9 +352,8 @@ class ApiService {
|
|||
if (contact != null) {
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contactId,
|
||||
ContactsCompanion(
|
||||
accountDeleted: const Value(true),
|
||||
username: Value('${contact.username} (${contact.userId})'),
|
||||
const ContactsCompanion(
|
||||
accountDeleted: Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -375,15 +387,6 @@ class ApiService {
|
|||
final result = await sendRequestSync(req, authenticated: false);
|
||||
|
||||
if (result.isSuccess) {
|
||||
final ok = result.value as server.Response_Ok;
|
||||
if (ok.hasAuthenticated()) {
|
||||
final authenticated = ok.authenticated;
|
||||
await updateUserdata((user) {
|
||||
user.subscriptionPlan = authenticated.plan;
|
||||
return user;
|
||||
});
|
||||
globalCallbackUpdatePlan(planFromString(authenticated.plan));
|
||||
}
|
||||
Log.info('websocket is authenticated');
|
||||
unawaited(onAuthenticated());
|
||||
return true;
|
||||
|
|
@ -491,6 +494,20 @@ class ApiService {
|
|||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<void> checkForDeletedUsernames() async {
|
||||
final users = await twonlyDB.contactsDao
|
||||
.getContactsByUsername('[deleted]', username2: '[Unknown]');
|
||||
for (final user in users) {
|
||||
final userData = await getUserById(user.userId);
|
||||
if (userData != null) {
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
user.userId,
|
||||
ContactsCompanion(username: Value(utf8.decode(userData.username))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response_UserData?> getUserById(int userId) async {
|
||||
final get = ApplicationData_GetUserById()..userId = Int64(userId);
|
||||
final appData = ApplicationData()..getUserById = get;
|
||||
|
|
@ -662,6 +679,22 @@ class ApiService {
|
|||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> ipaPurchase(
|
||||
String productId,
|
||||
String source,
|
||||
String verificationData,
|
||||
) async {
|
||||
final appData = ApplicationData(
|
||||
ipaPurchase: ApplicationData_IPAPurchase(
|
||||
productId: productId,
|
||||
source: source,
|
||||
verificationData: verificationData,
|
||||
),
|
||||
);
|
||||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> changeUsername(String username) async {
|
||||
final get = ApplicationData_ChangeUsername()..username = username;
|
||||
final appData = ApplicationData()..changeUsername = get;
|
||||
|
|
@ -669,6 +702,15 @@ class ApiService {
|
|||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> forceIpaCheck() async {
|
||||
final req = createClientToServerFromApplicationData(
|
||||
ApplicationData(
|
||||
ipaForceCheck: ApplicationData_IPAForceCheck(),
|
||||
),
|
||||
);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> updateSignedPreKey(
|
||||
int signedPreKeyId,
|
||||
Uint8List signedPreKey,
|
||||
|
|
@ -722,6 +764,28 @@ class ApiService {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
|
||||
final ballance = await getPlanBallance();
|
||||
if (ballance != null) {
|
||||
await updateUserdata((u) {
|
||||
u.lastPlanBallance = ballance.writeToJson();
|
||||
return u;
|
||||
});
|
||||
return ballance;
|
||||
}
|
||||
final user = await getUser();
|
||||
if (user != null && user.lastPlanBallance != null && useCache) {
|
||||
try {
|
||||
return Response_PlanBallance.fromJson(
|
||||
user.lastPlanBallance!,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('from json: $e');
|
||||
}
|
||||
}
|
||||
return ballance;
|
||||
}
|
||||
|
||||
Future<Result> sendTextMessage(
|
||||
int target,
|
||||
Uint8List msg,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,10 @@ Future<void> handleGroupUpdate(
|
|||
|
||||
final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
|
||||
|
||||
if (!group.isDirectChat) {
|
||||
unawaited(fetchGroupState(group));
|
||||
}
|
||||
|
||||
switch (actionType) {
|
||||
case GroupActionType.updatedGroupName:
|
||||
await twonlyDB.groupsDao.insertGroupAction(
|
||||
|
|
@ -173,10 +177,6 @@ Future<void> handleGroupUpdate(
|
|||
case GroupActionType.createdGroup:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!group.isDirectChat) {
|
||||
unawaited(fetchGroupState(group));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> handleGroupJoin(
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
);
|
||||
final mediaService = MediaFileService(media);
|
||||
|
||||
await mediaService.setUploadState(UploadState.uploaded);
|
||||
await mediaService.setUploadState(UploadState.uploading);
|
||||
// In all other cases just try the upload again...
|
||||
await startBackgroundMediaUpload(mediaService);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
|
@ -10,6 +11,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart
|
|||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||
as server;
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/client2client/contact.c2c.dart';
|
||||
import 'package:twonly/src/services/api/client2client/groups.c2c.dart';
|
||||
|
|
@ -35,9 +37,11 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
|||
if (msg.v0.hasRequestNewPreKeys()) {
|
||||
response = await handleRequestNewPreKey();
|
||||
} else if (msg.v0.hasNewMessage()) {
|
||||
final body = Uint8List.fromList(msg.v0.newMessage.body);
|
||||
final fromUserId = msg.v0.newMessage.fromUserId.toInt();
|
||||
await handleClient2ClientMessage(fromUserId, body);
|
||||
await handleClient2ClientMessage(msg.v0.newMessage);
|
||||
} else if (msg.v0.hasNewMessages()) {
|
||||
for (final newMessage in msg.v0.newMessages.newMessages) {
|
||||
await handleClient2ClientMessage(newMessage);
|
||||
}
|
||||
} else {
|
||||
Log.error('Unknown server message: $msg');
|
||||
}
|
||||
|
|
@ -56,7 +60,10 @@ DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
|
|||
|
||||
Mutex protectReceiptCheck = Mutex();
|
||||
|
||||
Future<void> handleClient2ClientMessage(int fromUserId, Uint8List body) async {
|
||||
Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
||||
final body = Uint8List.fromList(newMessage.body);
|
||||
final fromUserId = newMessage.fromUserId.toInt();
|
||||
|
||||
final message = Message.fromBuffer(body);
|
||||
final receiptId = message.receiptId;
|
||||
|
||||
|
|
@ -112,13 +119,17 @@ Future<void> handleClient2ClientMessage(int fromUserId, Uint8List body) async {
|
|||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull() ==
|
||||
null) {
|
||||
final user = await apiService.getUserById(fromUserId);
|
||||
|
||||
/// In case the user does not exists, just create a dummy user which was deleted by the user, so the message
|
||||
/// can be inserted into the receipts database
|
||||
await twonlyDB.contactsDao.insertContact(
|
||||
ContactsCompanion(
|
||||
userId: Value(fromUserId),
|
||||
deletedByUser: const Value(true),
|
||||
username: const Value('[deleted]'),
|
||||
username: Value(
|
||||
user == null ? '[Unknown]' : utf8.decode(user.username),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// ignore_for_file: unreachable_from_main
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
|
|
@ -9,39 +10,50 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
import '../../firebase_options.dart';
|
||||
|
||||
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
||||
|
||||
Future<void> initFCMAfterAuthenticated() async {
|
||||
if (globalIsAppInBackground) return;
|
||||
|
||||
Future<void> checkForTokenUpdates() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
|
||||
|
||||
try {
|
||||
if (Platform.isIOS) {
|
||||
final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
for (var i = 0; i < 20; i++) {
|
||||
if (apnsToken != null) break;
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
}
|
||||
if (apnsToken == null) {
|
||||
Log.error('Error getting apnsToken');
|
||||
Log.error('Could not get APNS token even after 20s...');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
if (fcmToken == null) {
|
||||
Log.error('Error getting fcmToken');
|
||||
Log.error('Could not get fcm token');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info('Loaded fcm token');
|
||||
if (storedToken == null || fcmToken != storedToken) {
|
||||
await apiService.updateFCMToken(fcmToken);
|
||||
await updateUserdata((u) {
|
||||
u.updateFCMToken = true;
|
||||
return u;
|
||||
});
|
||||
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) async {
|
||||
await apiService.updateFCMToken(fcmToken);
|
||||
await updateUserdata((u) {
|
||||
u.updateFCMToken = true;
|
||||
return u;
|
||||
});
|
||||
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
|
||||
}).onError((err) {
|
||||
Log.error('could not listen on token refresh');
|
||||
|
|
@ -51,11 +63,30 @@ Future<void> initFCMAfterAuthenticated() async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> initFCMAfterAuthenticated() async {
|
||||
if (gUser.updateFCMToken) {
|
||||
const storage = FlutterSecureStorage();
|
||||
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
|
||||
if (storedToken != null) {
|
||||
final res = await apiService.updateFCMToken(storedToken);
|
||||
if (res.isSuccess) {
|
||||
Log.info('Uploaded new fmt token!');
|
||||
await updateUserdata((u) {
|
||||
u.updateFCMToken = false;
|
||||
return u;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initFCMService() async {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
unawaited(checkForTokenUpdates());
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||
|
||||
// You may set the permission requests to "provisional" which allows the user to choose what type
|
||||
|
|
@ -65,12 +96,12 @@ Future<void> initFCMService() async {
|
|||
await FirebaseMessaging.instance.requestPermission();
|
||||
|
||||
// For apple platforms, ensure the APNS token is available before making any FCM plugin API calls
|
||||
if (Platform.isIOS) {
|
||||
final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
if (apnsToken == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if (Platform.isIOS) {
|
||||
// final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
// if (apnsToken == null) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,8 +175,7 @@ Future<void> showLocalPushNotification(
|
|||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
pushUser.userId.toInt() %
|
||||
// ignore: avoid_js_rounded_ints
|
||||
2373257871630019505, // Invalid argument (id): must fit within the size of a 32-bit integer
|
||||
2147483647, // Invalid argument (id): must fit within the size of a 32-bit integer
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
|
|
|
|||
|
|
@ -303,6 +303,15 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
|||
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
|
||||
/// push notification only containing the message kind and username
|
||||
Future<Uint8List?> encryptPushNotification(
|
||||
|
|
@ -326,15 +335,16 @@ Future<Uint8List?> encryptPushNotification(
|
|||
// this will be enforced after every app uses this system... :/
|
||||
// return null;
|
||||
Log.warn('Using insecure key as the receiver does not send a push key!');
|
||||
|
||||
await sendCipherText(
|
||||
toUserId,
|
||||
EncryptedContent()
|
||||
..pushKeys = (EncryptedContent_PushKeys()
|
||||
..type = EncryptedContent_PushKeys_Type.REQUEST),
|
||||
);
|
||||
await requestNewPushKeysForUser(toUserId);
|
||||
}
|
||||
} 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 {
|
||||
key = pushUser.pushKeys.last.key;
|
||||
keyId = pushUser.pushKeys.last.id.toInt();
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ Future<String> readLast1000Lines() async {
|
|||
Future<void> _writeLogToFile(LogRecord record) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
if (!logFile.existsSync()) {
|
||||
logFile.createSync(recursive: true);
|
||||
}
|
||||
|
||||
// Prepare the log message
|
||||
final logMessage =
|
||||
|
|
|
|||
|
|
@ -361,3 +361,8 @@ String getAvatarSvg(Uint8List avatarSvgCompressed) {
|
|||
final raw = gzip.decode(avatarSvgCompressed);
|
||||
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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ Future<void> updateUsersPlan(
|
|||
BuildContext context,
|
||||
SubscriptionPlan plan,
|
||||
) async {
|
||||
context.read<CustomChangeProvider>().plan = plan;
|
||||
context.read<PurchasesProvider>().plan = plan;
|
||||
|
||||
await updateUserdata((user) {
|
||||
user.subscriptionPlan = plan.name;
|
||||
|
|
@ -48,7 +48,7 @@ Future<void> updateUsersPlan(
|
|||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
await context.read<CustomChangeProvider>().updatePlan(plan);
|
||||
context.read<PurchasesProvider>().updatePlan(plan);
|
||||
}
|
||||
|
||||
Mutex updateProtection = Mutex();
|
||||
|
|
|
|||
|
|
@ -820,15 +820,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
...widget.mainCameraController.scannedNewProfiles.values
|
||||
.map(
|
||||
(c) {
|
||||
if (c.isLoading) return Container();
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (c.isLoading) return;
|
||||
c.isLoading = true;
|
||||
widget.mainCameraController.setState();
|
||||
await addNewContactFromPublicProfile(c.profile);
|
||||
widget.mainCameraController.scannedNewProfiles
|
||||
.remove(c.profile.userId.toInt());
|
||||
widget.mainCameraController.setState();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
|
|
|
|||
|
|
@ -52,8 +52,9 @@ class MainCameraController {
|
|||
} catch (e) {
|
||||
Log.warn(e);
|
||||
}
|
||||
await cameraController?.dispose();
|
||||
final cameraControllerTemp = cameraController;
|
||||
cameraController = null;
|
||||
await cameraControllerTemp?.dispose();
|
||||
initCameraStarted = false;
|
||||
selectedCameraDetails = SelectedCameraDetails();
|
||||
}
|
||||
|
|
@ -217,11 +218,13 @@ class MainCameraController {
|
|||
const ContactsCompanion(verified: Value(true)),
|
||||
);
|
||||
}
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (profile.username != gUser.username) {
|
||||
if (scannedNewProfiles[profile.userId.toInt()] == null) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
|
||||
profile: profile,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -93,7 +94,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||
final plan = context.watch<CustomChangeProvider>().plan;
|
||||
final plan = context.watch<PurchasesProvider>().plan;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
|
|
@ -203,7 +204,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await apiService.close(() {});
|
||||
await apiService.connect(force: true);
|
||||
await apiService.connect();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: (_groupsNotPinned.isEmpty &&
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class _UserListItem extends State<GroupListItem> {
|
|||
}
|
||||
|
||||
Future<void> onTap() async {
|
||||
if (_currentMessage == null) {
|
||||
if (_currentMessage == null && widget.group.totalMediaCounter == 0) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||
}
|
||||
setState(() {
|
||||
_recordingState = RecordingState.recording;
|
||||
_currentDuration = 0;
|
||||
});
|
||||
await HapticFeedback.heavyImpact();
|
||||
final audioTmpPath =
|
||||
|
|
@ -220,82 +221,96 @@ class _MessageInputState extends State<MessageInput> {
|
|||
),
|
||||
),
|
||||
Expanded(
|
||||
child: (_recordingState == RecordingState.recording)
|
||||
? Row(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 14,
|
||||
bottom: 14,
|
||||
left: 12,
|
||||
right: 8,
|
||||
),
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.microphone,
|
||||
size: 20,
|
||||
color: Colors.red,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _textFieldController,
|
||||
focusNode: widget.textFieldFocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
showCursor:
|
||||
_recordingState != RecordingState.recording,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
onChanged: (value) async {
|
||||
setState(() {});
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
widget.group.groupId,
|
||||
GroupsCompanion(
|
||||
draftMessage:
|
||||
Value(_textFieldController.text),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
formatMsToMinSec(
|
||||
_currentDuration,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (!_audioRecordingLock) ...[
|
||||
SizedBox(
|
||||
width: (100 - _cancelSlideOffset) % 101,
|
||||
),
|
||||
Text(
|
||||
context.lang.voiceMessageSlideToCancel,
|
||||
),
|
||||
] else ...[
|
||||
Expanded(
|
||||
child: Container(),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _cancelAudioRecording,
|
||||
child: Text(
|
||||
context.lang.voiceMessageCancel,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
},
|
||||
style: const TextStyle(fontSize: 17),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.chatListDetailInput,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
if (_recordingState == RecordingState.recording)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 14,
|
||||
bottom: 14,
|
||||
left: 12,
|
||||
right: 8,
|
||||
),
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.microphone,
|
||||
size: 20,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
],
|
||||
],
|
||||
)
|
||||
: TextField(
|
||||
controller: _textFieldController,
|
||||
focusNode: widget.textFieldFocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
onChanged: (value) async {
|
||||
setState(() {});
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
widget.group.groupId,
|
||||
GroupsCompanion(
|
||||
draftMessage:
|
||||
Value(_textFieldController.text),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
formatMsToMinSec(
|
||||
_currentDuration,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: isDarkMode(context)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
},
|
||||
style: const TextStyle(fontSize: 17),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.chatListDetailInput,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
if (!_audioRecordingLock) ...[
|
||||
SizedBox(
|
||||
width: (100 - _cancelSlideOffset) % 101,
|
||||
),
|
||||
Text(
|
||||
context.lang.voiceMessageSlideToCancel,
|
||||
),
|
||||
] else ...[
|
||||
Expanded(
|
||||
child: Container(),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _cancelAudioRecording,
|
||||
child: Text(
|
||||
context.lang.voiceMessageCancel,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_textFieldController.text == '')
|
||||
GestureDetector(
|
||||
|
|
@ -355,7 +370,9 @@ class _MessageInputState extends State<MessageInput> {
|
|||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(90),
|
||||
color: Colors.black,
|
||||
color: isDarkMode(context)
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
|
|
@ -71,6 +72,9 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
|
|||
);
|
||||
return;
|
||||
}
|
||||
Log.info(
|
||||
'Restoring flames from ${_directChat!.flameCounter} to ${_directChat!.maxFlameCounter}',
|
||||
);
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
_groupId,
|
||||
GroupsCompanion(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ class _MediaViewSizingState extends State<MediaViewSizing> {
|
|||
|
||||
final aspectRatioWidth = availableWidth;
|
||||
final aspectRatioHeight = (aspectRatioWidth * 16) / 9;
|
||||
if (aspectRatioHeight > availableHeight) {
|
||||
needToDownSizeImage = true;
|
||||
}
|
||||
if (widget.requiredHeight != null) {
|
||||
if (aspectRatioHeight < availableHeight) {
|
||||
if ((screenSize.height - widget.requiredHeight!) < aspectRatioHeight) {
|
||||
|
|
|
|||
|
|
@ -53,19 +53,19 @@ class OnboardingView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
PageViewModel(
|
||||
title: context.lang.onboardingSendTwonliesTitle,
|
||||
body: context.lang.onboardingSendTwonliesBody,
|
||||
image: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
child: Lottie.asset(
|
||||
'assets/animations/twonlies.json',
|
||||
repeat: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// PageViewModel(
|
||||
// title: context.lang.onboardingSendTwonliesTitle,
|
||||
// body: context.lang.onboardingSendTwonliesBody,
|
||||
// image: Center(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(top: 100),
|
||||
// child: Lottie.asset(
|
||||
// 'assets/animations/twonlies.json',
|
||||
// repeat: false,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
PageViewModel(
|
||||
title: context.lang.onboardingNotProductTitle,
|
||||
bodyWidget: Column(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -11,7 +10,6 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/settings/account/refund_credits.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
|
||||
class AccountView extends StatefulWidget {
|
||||
const AccountView({super.key});
|
||||
|
|
@ -31,7 +29,7 @@ class _AccountViewState extends State<AccountView> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final ballance = await loadPlanBalance(useCache: false);
|
||||
final ballance = await apiService.loadPlanBalance(useCache: false);
|
||||
if (ballance == null || !mounted) return;
|
||||
var ballanceInCents = ballance.transactions
|
||||
.where(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
// import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/voucher.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
|
||||
// import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class RefundCreditsView extends StatefulWidget {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/public_profile.view.dart';
|
||||
import 'package:twonly/src/views/settings/account.view.dart';
|
||||
import 'package:twonly/src/views/settings/appearance.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
|
|
@ -82,13 +83,22 @@ class _SettingsMainViewState extends State<SettingsMainView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// Align(
|
||||
// alignment: Alignment.centerRight,
|
||||
// child: IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: FaIcon(FontAwesomeIcons.qrcode),
|
||||
// ),
|
||||
// )
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const PublicProfileView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.qrcode),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'package:twonly/src/utils/log.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
|
||||
|
||||
Future<List<Response_AddAccountsInvite>?> loadAdditionalUserInvites() async {
|
||||
final ballance = await apiService.getAdditionalUserInvites();
|
||||
|
|
@ -65,12 +65,9 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var plusInvites = <Response_AddAccountsInvite>[];
|
||||
var freeInvites = <Response_AddAccountsInvite>[];
|
||||
if (additionalInvites != null) {
|
||||
plusInvites =
|
||||
additionalInvites!.where((x) => x.planId == 'Plus').toList();
|
||||
freeInvites =
|
||||
additionalInvites!.where((x) => x.planId == 'Free').toList();
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -95,11 +92,10 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
|
|||
),
|
||||
),
|
||||
if (plusInvites.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.lang.additionalUsersPlusTokens,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
Text(
|
||||
context.lang.additionalUsersPlusTokens,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -111,23 +107,6 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
|
|||
children: plusInvites.map(AdditionalUserInvite.new).toList(),
|
||||
),
|
||||
),
|
||||
if (freeInvites.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.lang.additionalUsersFreeTokens,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 16 / 5,
|
||||
shrinkWrap: true,
|
||||
children: freeInvites.map(AdditionalUserInvite.new).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -200,7 +179,7 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
|
|||
final remove = await showAlertDialog(
|
||||
context,
|
||||
'Remove this additional user',
|
||||
'The additional user will automatically be downgraded to the preview plan after removal and you will receive a new invitation code to give to another person.',
|
||||
'The additional user will automatically be downgraded to the free plan after removal and you will receive a new invitation code to give to another person.',
|
||||
);
|
||||
if (remove) {
|
||||
final res = await apiService
|
||||
|
|
|
|||
|
|
@ -1,111 +1,24 @@
|
|||
// ignore_for_file: inference_failure_on_instance_creation
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/model/purchases/purchasable_product.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/checkout.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/manage_subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/transaction.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/voucher.view.dart';
|
||||
|
||||
String localePrizing(BuildContext context, int cents) {
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
final euros = cents / 100;
|
||||
|
||||
if (euros == euros.toInt()) {
|
||||
return '${euros.toInt()}€';
|
||||
}
|
||||
|
||||
return NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(cents / 100);
|
||||
}
|
||||
|
||||
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
|
||||
final ballance = await apiService.getPlanBallance();
|
||||
if (ballance != null) {
|
||||
await updateUserdata((u) {
|
||||
u.lastPlanBallance = ballance.writeToJson();
|
||||
return u;
|
||||
});
|
||||
return ballance;
|
||||
}
|
||||
final user = await getUser();
|
||||
if (user != null && user.lastPlanBallance != null && useCache) {
|
||||
try {
|
||||
return Response_PlanBallance.fromJson(
|
||||
user.lastPlanBallance!,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('from json: $e');
|
||||
}
|
||||
}
|
||||
return ballance;
|
||||
}
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
const int MONTHLY_PAYMENT_DAYS = 30;
|
||||
// ignore: constant_identifier_names
|
||||
const int YEARLY_PAYMENT_DAYS = 365;
|
||||
|
||||
int calculateRefund(Response_PlanBallance current) {
|
||||
var refund = getPlanPrice(SubscriptionPlan.Pro, paidMonthly: true);
|
||||
|
||||
if (current.paymentPeriodDays == YEARLY_PAYMENT_DAYS) {
|
||||
final elapsedDays = DateTime.now()
|
||||
.difference(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
current.lastPaymentDoneUnixTimestamp.toInt() * 1000,
|
||||
),
|
||||
)
|
||||
.inDays;
|
||||
if (elapsedDays < current.paymentPeriodDays.toInt()) {
|
||||
// User has yearly plan with 10€
|
||||
// used it half a year and wants now to upgrade => gets 5€ discount...
|
||||
// math.ceil(((365-(365/2))/365)*10)
|
||||
// => 5€
|
||||
|
||||
refund = (((YEARLY_PAYMENT_DAYS - elapsedDays) / YEARLY_PAYMENT_DAYS) *
|
||||
getPlanPrice(SubscriptionPlan.Pro, paidMonthly: false) /
|
||||
100)
|
||||
.ceil() *
|
||||
100;
|
||||
}
|
||||
} else {
|
||||
final elapsedDays = DateTime.now()
|
||||
.difference(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
current.lastPaymentDoneUnixTimestamp.toInt() * 1000,
|
||||
),
|
||||
)
|
||||
.inDays;
|
||||
if (elapsedDays > 14) {
|
||||
refund = 0;
|
||||
}
|
||||
}
|
||||
return refund;
|
||||
}
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SubscriptionView extends StatefulWidget {
|
||||
const SubscriptionView({super.key, this.redirectError});
|
||||
|
||||
final ErrorCode? redirectError;
|
||||
const SubscriptionView({super.key});
|
||||
|
||||
@override
|
||||
State<SubscriptionView> createState() => _SubscriptionViewState();
|
||||
|
|
@ -124,7 +37,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
ballance = await loadPlanBalance();
|
||||
ballance = await apiService.loadPlanBalance();
|
||||
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
|
||||
final ownerId = ballance!.additionalAccountOwnerId.toInt();
|
||||
final contact = await twonlyDB.contactsDao
|
||||
|
|
@ -137,61 +50,18 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
}
|
||||
}
|
||||
setState(() {});
|
||||
await apiService.forceIpaCheck();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
String? formattedBalance;
|
||||
DateTime? nextPayment;
|
||||
final currentPlan = context.read<CustomChangeProvider>().plan;
|
||||
|
||||
if (ballance != null) {
|
||||
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
|
||||
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000,
|
||||
);
|
||||
if (isPayingUser(currentPlan)) {
|
||||
nextPayment = lastPaymentDateTime
|
||||
.add(Duration(days: ballance!.paymentPeriodDays.toInt()));
|
||||
}
|
||||
final ballanceInCents =
|
||||
ballance!.transactions.map((a) => a.depositCents.toInt()).sum;
|
||||
formattedBalance = NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(ballanceInCents / 100);
|
||||
}
|
||||
|
||||
var refund = 0;
|
||||
if (currentPlan == SubscriptionPlan.Pro && ballance != null) {
|
||||
refund = calculateRefund(ballance!);
|
||||
}
|
||||
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsSubscription),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
if (widget.redirectError != null)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orangeAccent,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Text(
|
||||
(widget.redirectError == ErrorCode.PlanLimitReached)
|
||||
? context.lang.planLimitReached
|
||||
: context.lang.planNotAllowed,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
|
|
@ -220,7 +90,12 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
style: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
if (!isPayingUser(currentPlan))
|
||||
if (isPayingUser(currentPlan))
|
||||
PlanCard(
|
||||
plan: currentPlan,
|
||||
),
|
||||
if (!isPayingUser(currentPlan) ||
|
||||
currentPlan == SubscriptionPlan.Tester) ...[
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
|
|
@ -231,48 +106,16 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!isPayingUser(currentPlan) ||
|
||||
currentPlan == SubscriptionPlan.Tester)
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Pro,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const CheckoutView(
|
||||
plan: SubscriptionPlan.Pro,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
onPurchase: initAsync,
|
||||
),
|
||||
if (currentPlan != SubscriptionPlan.Family)
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Family,
|
||||
refund: refund,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CheckoutView(
|
||||
plan: SubscriptionPlan.Family,
|
||||
refund: (refund > 0) ? refund : null,
|
||||
disableMonthlyOption:
|
||||
currentPlan == SubscriptionPlan.Pro &&
|
||||
ballance!.paymentPeriodDays.toInt() ==
|
||||
YEARLY_PAYMENT_DAYS,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
onPurchase: initAsync,
|
||||
),
|
||||
if (!isPayingUser(currentPlan)) ...[
|
||||
],
|
||||
if (currentPlan == SubscriptionPlan.Free) ...[
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: Padding(
|
||||
|
|
@ -287,58 +130,11 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
const SizedBox(height: 10),
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Plus,
|
||||
onTap: () async {
|
||||
await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
|
||||
await initAsync();
|
||||
},
|
||||
onPurchase: initAsync,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
if (currentPlan != SubscriptionPlan.Family) const Divider(),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.gears,
|
||||
text: context.lang.manageSubscription,
|
||||
subtitle: (nextPayment != null)
|
||||
? Text(
|
||||
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment)}',
|
||||
)
|
||||
: null,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ManageSubscriptionView(
|
||||
ballance: ballance,
|
||||
nextPayment: nextPayment,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.moneyBillTransfer,
|
||||
text: context.lang.transactionHistory,
|
||||
subtitle: (formattedBalance != null)
|
||||
? Text('${context.lang.currentBalance}: $formattedBalance')
|
||||
: null,
|
||||
onTap: () async {
|
||||
if (formattedBalance == null) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return TransactionView(
|
||||
transactions: ballance?.transactions,
|
||||
formattedBalance: formattedBalance!,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isPayingUser(currentPlan) ||
|
||||
currentPlan == SubscriptionPlan.Tester)
|
||||
BetterListTile(
|
||||
|
|
@ -359,64 +155,72 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
await initAsync();
|
||||
},
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.ticket,
|
||||
text: context.lang.createOrRedeemVoucher,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const VoucherView();
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.Pro:
|
||||
return paidMonthly ? 100 : 1000;
|
||||
case SubscriptionPlan.Family:
|
||||
return paidMonthly ? 200 : 2000;
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class PlanCard extends StatelessWidget {
|
||||
class PlanCard extends StatefulWidget {
|
||||
const PlanCard({
|
||||
required this.plan,
|
||||
super.key,
|
||||
this.refund,
|
||||
this.onTap,
|
||||
this.onPurchase,
|
||||
this.paidMonthly,
|
||||
});
|
||||
final SubscriptionPlan plan;
|
||||
final void Function()? onTap;
|
||||
final int? refund;
|
||||
final void Function()? onPurchase;
|
||||
final bool? paidMonthly;
|
||||
|
||||
@override
|
||||
State<PlanCard> createState() => _PlanCardState();
|
||||
}
|
||||
|
||||
String getFormattedPrice(PurchasableProduct product) {
|
||||
if (product.price.contains('€')) {
|
||||
return product.price.replaceAll(',00', '').replaceAll('.00', '');
|
||||
}
|
||||
return product.price;
|
||||
}
|
||||
|
||||
class _PlanCardState extends State<PlanCard> {
|
||||
Future<void> onButtonPressed(PurchasableProduct? product) async {
|
||||
if (widget.onPurchase == null) return;
|
||||
if (widget.plan == SubscriptionPlan.Free ||
|
||||
widget.plan == SubscriptionPlan.Plus) {
|
||||
await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
|
||||
widget.onPurchase!();
|
||||
return;
|
||||
}
|
||||
if (product == null) return;
|
||||
await context.read<PurchasesProvider>().buy(product);
|
||||
widget.onPurchase!();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final yearlyPrice = getPlanPrice(plan, paidMonthly: false);
|
||||
final monthlyPrice = getPlanPrice(plan, paidMonthly: true);
|
||||
final products = context.watch<PurchasesProvider>().products;
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
PurchasableProduct? yearlyProduct;
|
||||
PurchasableProduct? monthlyProduct;
|
||||
|
||||
for (final product in products) {
|
||||
if (product.id.toLowerCase().startsWith(widget.plan.name.toLowerCase())) {
|
||||
if (product.id.toLowerCase().contains('monthly')) {
|
||||
monthlyProduct = product;
|
||||
} else if (product.id.toLowerCase().contains('yearly')) {
|
||||
yearlyProduct = product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var features = <String>[];
|
||||
|
||||
switch (plan.name) {
|
||||
switch (widget.plan.name) {
|
||||
case 'Free':
|
||||
features = [context.lang.freeFeature1];
|
||||
case 'Plus':
|
||||
features = [context.lang.plusFeature1, context.lang.plusFeature2];
|
||||
features = [context.lang.plusFeature1]; //, context.lang.plusFeature2];
|
||||
case 'Tester':
|
||||
case 'Pro':
|
||||
features = [
|
||||
|
|
@ -424,117 +228,135 @@ class PlanCard extends StatelessWidget {
|
|||
context.lang.proFeature2,
|
||||
context.lang.proFeature3,
|
||||
context.lang.proFeature4,
|
||||
// context.lang.proFeature4,
|
||||
];
|
||||
case 'Family':
|
||||
features = [
|
||||
context.lang.proFeature1,
|
||||
context.lang.familyFeature1,
|
||||
context.lang.familyFeature2,
|
||||
context.lang.proFeature3,
|
||||
context.lang.proFeature4,
|
||||
context.lang.familyFeature3,
|
||||
context.lang.familyFeature4,
|
||||
];
|
||||
default:
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
widget.plan.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (yearlyProduct != null && currentPlan != widget.plan)
|
||||
const SizedBox(height: 10),
|
||||
if (yearlyProduct != null &&
|
||||
widget.paidMonthly == null &&
|
||||
currentPlan != widget.plan)
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'${getFormattedPrice(yearlyProduct)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (monthlyProduct != null)
|
||||
Text(
|
||||
'${getFormattedPrice(monthlyProduct)}/${context.lang.month}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.paidMonthly != null)
|
||||
Text(
|
||||
plan.name,
|
||||
(widget.paidMonthly!)
|
||||
? '${getFormattedPrice(monthlyProduct!)}/${context.lang.month}'
|
||||
: '${getFormattedPrice(yearlyProduct!)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (yearlyPrice != 0) const SizedBox(height: 10),
|
||||
if (yearlyPrice != 0 && paidMonthly == null)
|
||||
Column(
|
||||
children: [
|
||||
if (paidMonthly == null || paidMonthly!)
|
||||
Text(
|
||||
'${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (paidMonthly == null || !paidMonthly!)
|
||||
Text(
|
||||
'${localePrizing(context, monthlyPrice)}/${context.lang.month}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (paidMonthly != null)
|
||||
Text(
|
||||
(paidMonthly!)
|
||||
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'
|
||||
: '${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...features.map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Text(
|
||||
feature,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (currentPlan == widget.plan)
|
||||
FilledButton.icon(
|
||||
onPressed: () async {
|
||||
var url = 'https://apps.apple.com/account/subscriptions';
|
||||
if (Platform.isAndroid) {
|
||||
url =
|
||||
'https://play.google.com/store/account/subscriptions?sku=${gUser.subscriptionPlanIdStore}&package=eu.twonly';
|
||||
}
|
||||
await launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
label: const Text('Manage subscription'),
|
||||
),
|
||||
if (widget.onPurchase != null && monthlyProduct != null)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => onButtonPressed(monthlyProduct),
|
||||
label: (widget.plan == SubscriptionPlan.Free ||
|
||||
widget.plan == SubscriptionPlan.Plus)
|
||||
? Text(context.lang.redeemUserInviteCodeTitle)
|
||||
: Text(
|
||||
context.lang.upgradeToPaidPlanButton(
|
||||
widget.plan.name,
|
||||
' (${context.lang.monthly})',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...features.map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Text(
|
||||
feature,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (widget.onPurchase != null &&
|
||||
(yearlyProduct != null ||
|
||||
currentPlan == SubscriptionPlan.Free))
|
||||
FilledButton.icon(
|
||||
onPressed: () => onButtonPressed(yearlyProduct),
|
||||
label: (widget.plan == SubscriptionPlan.Free ||
|
||||
widget.plan == SubscriptionPlan.Plus)
|
||||
? Text(context.lang.redeemUserInviteCodeTitle)
|
||||
: Text(
|
||||
context.lang.upgradeToPaidPlanButton(
|
||||
widget.plan.name,
|
||||
' (${context.lang.yearly})',
|
||||
),
|
||||
),
|
||||
),
|
||||
if (refund != null && refund! > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 7),
|
||||
child: Text(
|
||||
context.lang
|
||||
.subscriptionRefund(localePrizing(context, refund!)),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: FilledButton.icon(
|
||||
onPressed: onTap,
|
||||
label: (plan == SubscriptionPlan.Free ||
|
||||
plan == SubscriptionPlan.Plus)
|
||||
? Text(context.lang.redeemUserInviteCodeTitle)
|
||||
: Text(
|
||||
context.lang.upgradeToPaidPlanButton(plan.name),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -593,7 +415,7 @@ Future<void> redeemUserInviteCode(BuildContext context, String newPlan) async {
|
|||
);
|
||||
// reconnect to load new plan.
|
||||
await apiService.close(() {});
|
||||
await apiService.connect(force: true);
|
||||
await apiService.connect();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/select_payment.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/select_payment.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
|
||||
|
||||
class CheckoutView extends StatefulWidget {
|
||||
const CheckoutView({
|
||||
required this.plan,
|
||||
super.key,
|
||||
this.refund,
|
||||
this.disableMonthlyOption,
|
||||
});
|
||||
|
||||
final SubscriptionPlan plan;
|
||||
final int? refund;
|
||||
final bool? disableMonthlyOption;
|
||||
|
||||
@override
|
||||
|
|
@ -76,29 +74,6 @@ class _CheckoutViewState extends State<CheckoutView> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (widget.refund != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.refund,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'+${localePrizing(context, widget.refund!)}',
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(color: context.color.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Card(
|
||||
|
|
@ -133,7 +108,6 @@ class _CheckoutViewState extends State<CheckoutView> {
|
|||
return SelectPaymentView(
|
||||
plan: widget.plan,
|
||||
payMonthly: paidMonthly,
|
||||
refund: widget.refund,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
|
||||
|
||||
class ManageSubscriptionView extends StatefulWidget {
|
||||
const ManageSubscriptionView({
|
||||
|
|
@ -65,7 +64,7 @@ class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final plan = context.read<CustomChangeProvider>().plan;
|
||||
final plan = context.watch<PurchasesProvider>().plan;
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS;
|
||||
return Scaffold(
|
||||
|
|
@ -7,8 +7,8 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart';
|
|||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/voucher.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SelectPaymentView extends StatefulWidget {
|
||||
|
|
@ -17,13 +17,11 @@ class SelectPaymentView extends StatefulWidget {
|
|||
this.plan,
|
||||
this.payMonthly,
|
||||
this.valueInCents,
|
||||
this.refund,
|
||||
});
|
||||
|
||||
final SubscriptionPlan? plan;
|
||||
final bool? payMonthly;
|
||||
final int? valueInCents;
|
||||
final int? refund;
|
||||
|
||||
@override
|
||||
State<SelectPaymentView> createState() => _SelectPaymentViewState();
|
||||
|
|
@ -190,30 +188,6 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (widget.refund != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Card(
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.refund,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'+${localePrizing(context, widget.refund!)}',
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(color: context.color.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Card(
|
||||
|
|
@ -0,0 +1,570 @@
|
|||
// ignore_for_file: inference_failure_on_instance_creation
|
||||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/checkout.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/manage_subscription.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/transaction.view.dart';
|
||||
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
|
||||
|
||||
String localePrizing(BuildContext context, int cents) {
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
final euros = cents / 100;
|
||||
|
||||
if (euros == euros.toInt()) {
|
||||
return '${euros.toInt()}€';
|
||||
}
|
||||
|
||||
return NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(cents / 100);
|
||||
}
|
||||
|
||||
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
|
||||
final ballance = await apiService.getPlanBallance();
|
||||
if (ballance != null) {
|
||||
await updateUserdata((u) {
|
||||
u.lastPlanBallance = ballance.writeToJson();
|
||||
return u;
|
||||
});
|
||||
return ballance;
|
||||
}
|
||||
final user = await getUser();
|
||||
if (user != null && user.lastPlanBallance != null && useCache) {
|
||||
try {
|
||||
return Response_PlanBallance.fromJson(
|
||||
user.lastPlanBallance!,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('from json: $e');
|
||||
}
|
||||
}
|
||||
return ballance;
|
||||
}
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
const int MONTHLY_PAYMENT_DAYS = 30;
|
||||
// ignore: constant_identifier_names
|
||||
const int YEARLY_PAYMENT_DAYS = 365;
|
||||
|
||||
class SubscriptionCustomView extends StatefulWidget {
|
||||
const SubscriptionCustomView({super.key, this.redirectError});
|
||||
|
||||
final ErrorCode? redirectError;
|
||||
|
||||
@override
|
||||
State<SubscriptionCustomView> createState() => _SubscriptionCustomViewState();
|
||||
}
|
||||
|
||||
class _SubscriptionCustomViewState extends State<SubscriptionCustomView> {
|
||||
bool loaded = false;
|
||||
bool testerRequested = true;
|
||||
Response_PlanBallance? ballance;
|
||||
String? additionalOwnerName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
ballance = await loadPlanBalance();
|
||||
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
|
||||
final ownerId = ballance!.additionalAccountOwnerId.toInt();
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(ownerId)
|
||||
.getSingleOrNull();
|
||||
if (contact != null) {
|
||||
additionalOwnerName = getContactDisplayName(contact);
|
||||
} else {
|
||||
additionalOwnerName = ownerId.toString();
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final myLocale = Localizations.localeOf(context);
|
||||
String? formattedBalance;
|
||||
DateTime? nextPayment;
|
||||
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||
|
||||
if (ballance != null) {
|
||||
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
|
||||
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000,
|
||||
);
|
||||
if (isPayingUser(currentPlan)) {
|
||||
nextPayment = lastPaymentDateTime
|
||||
.add(Duration(days: ballance!.paymentPeriodDays.toInt()));
|
||||
}
|
||||
final ballanceInCents =
|
||||
ballance!.transactions.map((a) => a.depositCents.toInt()).sum;
|
||||
formattedBalance = NumberFormat.currency(
|
||||
locale: myLocale.toString(),
|
||||
symbol: '€',
|
||||
decimalDigits: 2,
|
||||
).format(ballanceInCents / 100);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsSubscription),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
if (widget.redirectError != null)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orangeAccent,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Text(
|
||||
(widget.redirectError == ErrorCode.PlanLimitReached)
|
||||
? context.lang.planLimitReached
|
||||
: context.lang.planNotAllowed,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
child: Text(
|
||||
currentPlan.name,
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (additionalOwnerName != null)
|
||||
Center(
|
||||
child: Text(
|
||||
context.lang.partOfPaidPlanOf(additionalOwnerName!),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
if (!isPayingUser(currentPlan))
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Text(
|
||||
context.lang.upgradeToPaidPlan,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isPayingUser(currentPlan) ||
|
||||
currentPlan == SubscriptionPlan.Tester)
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Pro,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const CheckoutView(
|
||||
plan: SubscriptionPlan.Pro,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
if (currentPlan != SubscriptionPlan.Family)
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Family,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CheckoutView(
|
||||
plan: SubscriptionPlan.Family,
|
||||
disableMonthlyOption:
|
||||
currentPlan == SubscriptionPlan.Pro &&
|
||||
ballance!.paymentPeriodDays.toInt() ==
|
||||
YEARLY_PAYMENT_DAYS,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
if (!isPayingUser(currentPlan)) ...[
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Text(
|
||||
context.lang.redeemUserInviteCode,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
PlanCard(
|
||||
plan: SubscriptionPlan.Plus,
|
||||
onTap: () async {
|
||||
await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
if (currentPlan != SubscriptionPlan.Family) const Divider(),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.gears,
|
||||
text: context.lang.manageSubscription,
|
||||
subtitle: (nextPayment != null)
|
||||
? Text(
|
||||
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment)}',
|
||||
)
|
||||
: null,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ManageSubscriptionView(
|
||||
ballance: ballance,
|
||||
nextPayment: nextPayment,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.moneyBillTransfer,
|
||||
text: context.lang.transactionHistory,
|
||||
subtitle: (formattedBalance != null)
|
||||
? Text('${context.lang.currentBalance}: $formattedBalance')
|
||||
: null,
|
||||
onTap: () async {
|
||||
if (formattedBalance == null) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return TransactionView(
|
||||
transactions: ballance?.transactions,
|
||||
formattedBalance: formattedBalance!,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isPayingUser(currentPlan) ||
|
||||
currentPlan == SubscriptionPlan.Tester)
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.userPlus,
|
||||
text: context.lang.manageAdditionalUsers,
|
||||
subtitle: loaded ? Text('${context.lang.open}: 3') : null,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return AdditionalUsersView(
|
||||
ballance: ballance,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.ticket,
|
||||
text: context.lang.createOrRedeemVoucher,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const VoucherView();
|
||||
},
|
||||
),
|
||||
);
|
||||
await initAsync();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.Pro:
|
||||
return paidMonthly ? 100 : 1000;
|
||||
case SubscriptionPlan.Family:
|
||||
return paidMonthly ? 200 : 2000;
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class PlanCard extends StatelessWidget {
|
||||
const PlanCard({
|
||||
required this.plan,
|
||||
super.key,
|
||||
this.refund,
|
||||
this.onTap,
|
||||
this.paidMonthly,
|
||||
});
|
||||
final SubscriptionPlan plan;
|
||||
final void Function()? onTap;
|
||||
final int? refund;
|
||||
final bool? paidMonthly;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final yearlyPrice = getPlanPrice(plan, paidMonthly: false);
|
||||
final monthlyPrice = getPlanPrice(plan, paidMonthly: true);
|
||||
var features = <String>[];
|
||||
|
||||
switch (plan.name) {
|
||||
case 'Free':
|
||||
features = [context.lang.freeFeature1];
|
||||
case 'Plus':
|
||||
features = [context.lang.plusFeature1, context.lang.plusFeature2];
|
||||
case 'Tester':
|
||||
case 'Pro':
|
||||
features = [
|
||||
context.lang.proFeature1,
|
||||
context.lang.proFeature2,
|
||||
context.lang.proFeature3,
|
||||
context.lang.proFeature4,
|
||||
];
|
||||
case 'Family':
|
||||
features = [
|
||||
context.lang.proFeature1,
|
||||
context.lang.familyFeature2,
|
||||
context.lang.proFeature3,
|
||||
context.lang.proFeature4,
|
||||
];
|
||||
default:
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
color: context.color.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
plan.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (yearlyPrice != 0) const SizedBox(height: 10),
|
||||
if (yearlyPrice != 0 && paidMonthly == null)
|
||||
Column(
|
||||
children: [
|
||||
if (paidMonthly == null || paidMonthly!)
|
||||
Text(
|
||||
'${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (paidMonthly == null || !paidMonthly!)
|
||||
Text(
|
||||
'${localePrizing(context, monthlyPrice)}/${context.lang.month}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (paidMonthly != null)
|
||||
Text(
|
||||
(paidMonthly!)
|
||||
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'
|
||||
: '${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...features.map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Text(
|
||||
feature,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (refund != null && refund! > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 7),
|
||||
child: Text(
|
||||
context.lang
|
||||
.subscriptionRefund(localePrizing(context, refund!)),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: context.color.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: FilledButton.icon(
|
||||
onPressed: onTap,
|
||||
label: (plan == SubscriptionPlan.Free ||
|
||||
plan == SubscriptionPlan.Plus)
|
||||
? Text(context.lang.redeemUserInviteCodeTitle)
|
||||
: Text(
|
||||
context.lang
|
||||
.upgradeToPaidPlanButton(plan.name, ''),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> redeemUserInviteCode(BuildContext context, String newPlan) async {
|
||||
var inviteCode = '';
|
||||
// ignore: inference_failure_on_function_invocation
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.lang.redeemUserInviteCodeTitle),
|
||||
content: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: TextField(
|
||||
onChanged: (value) => setState(() {
|
||||
inviteCode = value.toUpperCase();
|
||||
}),
|
||||
decoration: InputDecoration(
|
||||
labelText: context.lang.registerTwonlyCodeLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(context.lang.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final res = await apiService.redeemUserInviteCode(inviteCode);
|
||||
if (!context.mounted) return;
|
||||
if (res.isSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.lang.redeemUserInviteCodeSuccess),
|
||||
),
|
||||
);
|
||||
// reconnect to load new plan.
|
||||
await apiService.close(() {});
|
||||
await apiService.connect();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
errorCodeToText(context, res.error as ErrorCode),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(context.lang.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
56
pubspec.lock
56
pubspec.lock
|
|
@ -13,10 +13,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a"
|
||||
sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.64"
|
||||
version: "1.3.65"
|
||||
adaptive_number:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
|
|
@ -516,10 +516,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c"
|
||||
sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
version: "4.3.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -532,34 +532,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398
|
||||
sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.3.1"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "22086f857d2340f5d973776cfd542d3fb30cf98e1c643c3aa4a7520bb12745bb"
|
||||
sha256: "1ad663fbb6758acec09d7e84a2e6478265f0a517f40ef77c573efd5e0089f400"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.0.4"
|
||||
version: "16.1.0"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: a59920cbf2eb7c83d34a5f354331210ffec116b216dc72d864d8b8eb983ca398
|
||||
sha256: ea620e841fbcec62a96984295fc628f53ef5a8da4f53238159719ed0af7db834
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.4"
|
||||
version: "4.7.5"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "1183e40e6fd2a279a628951cc3b639fcf5ffe7589902632db645011eb70ebefb"
|
||||
sha256: "7d0fb6256202515bba8489a3d69c6bc9d52d69a4999bad789053b486c8e7323e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.1.1"
|
||||
fixnum:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1005,6 +1005,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
in_app_purchase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_purchase
|
||||
sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
in_app_purchase_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_android
|
||||
sha256: abb254ae159a5a9d4f867795ecb076864faeba59ce015ab81d4cca380f23df45
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0+8"
|
||||
in_app_purchase_platform_interface:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: in_app_purchase_platform_interface
|
||||
sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
in_app_purchase_storekit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_storekit
|
||||
sha256: f7cbbd7fb47ab5a4fb736fc3f20ae81a4f6def0af9297b3c525ca727761e2589
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.7"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
|||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.0.74+74
|
||||
version: 0.0.78+78
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
|
|
@ -47,8 +47,8 @@ dependencies:
|
|||
|
||||
|
||||
# Trustworthy publishers
|
||||
firebase_core: ^4.2.0 # firebase.google.com
|
||||
firebase_messaging: ^16.0.3 # firebase.google.com
|
||||
firebase_core: ^4.3.0 # firebase.google.com
|
||||
firebase_messaging: ^16.1.0 # firebase.google.com
|
||||
json_annotation: ^4.9.0 # google.dev
|
||||
protobuf: ^4.0.0 # google.dev
|
||||
scrollable_positioned_list: ^0.3.8 # google.dev
|
||||
|
|
@ -87,6 +87,8 @@ dependencies:
|
|||
screenshot: ^3.0.0
|
||||
sentry_flutter: ^9.8.0
|
||||
app_links: ^7.0.0
|
||||
in_app_purchase: ^3.2.3
|
||||
|
||||
|
||||
# Overwritten by self-controlled repository
|
||||
emoji_picker_flutter: ^4.3.0
|
||||
|
|
@ -162,6 +164,7 @@ dev_dependencies:
|
|||
sdk: flutter
|
||||
json_serializable: ^6.8.0
|
||||
very_good_analysis: ^10.0.0
|
||||
in_app_purchase_platform_interface: ^1.4.0
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
|
|||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v3.dart' as v3;
|
||||
import 'schema_v4.dart' as v4;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
|
|
@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||
return v2.DatabaseAtV2(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
case 4:
|
||||
return v4.DatabaseAtV4(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3];
|
||||
static const versions = const [1, 2, 3, 4];
|
||||
}
|
||||
|
|
|
|||
6417
test/drift/twonly_db/generated/schema_v4.dart
Normal file
6417
test/drift/twonly_db/generated/schema_v4.dart
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue