Merge pull request #345 from twonlyapp/dev
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:
Tobi 2025-12-21 17:21:26 +01:00 committed by GitHub
commit 2713f092eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 8759 additions and 786 deletions

View file

@ -66,6 +66,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.android.vending.BILLING" />
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

View file

@ -209,11 +209,11 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
var pushNotificationText: [PushKind: String] = [:] var pushNotificationText: [PushKind: String] = [:]
var title = "Someone" var title = "[Unknown]"
// Define the messages based on the system language // Define the messages based on the system language
if systemLanguage.contains("de") { // German if systemLanguage.contains("de") { // German
title = "Jemand" title = "[Unbekannt]"
pushNotificationText = [ pushNotificationText = [
.text: "hat eine Nachricht{inGroup} gesendet.", .text: "hat eine Nachricht{inGroup} gesendet.",
.twonly: "hat ein twonly{inGroup} gesendet.", .twonly: "hat ein twonly{inGroup} gesendet.",

View file

@ -54,55 +54,55 @@ PODS:
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- Firebase (12.4.0): - Firebase (12.6.0):
- Firebase/Core (= 12.4.0) - Firebase/Core (= 12.6.0)
- Firebase/Core (12.4.0): - Firebase/Core (12.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAnalytics (~> 12.4.0) - FirebaseAnalytics (~> 12.6.0)
- Firebase/CoreOnly (12.4.0): - Firebase/CoreOnly (12.6.0):
- FirebaseCore (~> 12.4.0) - FirebaseCore (~> 12.6.0)
- Firebase/Messaging (12.4.0): - Firebase/Messaging (12.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0) - FirebaseMessaging (~> 12.6.0)
- firebase_core (4.2.1): - firebase_core (4.3.0):
- Firebase/CoreOnly (= 12.4.0) - Firebase/CoreOnly (= 12.6.0)
- Flutter - Flutter
- firebase_messaging (16.0.4): - firebase_messaging (16.1.0):
- Firebase/Messaging (= 12.4.0) - Firebase/Messaging (= 12.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseAnalytics (12.4.0): - FirebaseAnalytics (12.6.0):
- FirebaseAnalytics/Default (= 12.4.0) - FirebaseAnalytics/Default (= 12.6.0)
- FirebaseCore (~> 12.4.0) - FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.4.0) - FirebaseInstallations (~> 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.4.0): - FirebaseAnalytics/Default (12.6.0):
- FirebaseCore (~> 12.4.0) - FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.4.0) - FirebaseInstallations (~> 12.6.0)
- GoogleAppMeasurement/Default (= 12.4.0) - GoogleAppMeasurement/Default (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseCore (12.4.0): - FirebaseCore (12.6.0):
- FirebaseCoreInternal (~> 12.4.0) - FirebaseCoreInternal (~> 12.6.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.4.0): - FirebaseCoreInternal (12.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (12.4.0): - FirebaseInstallations (12.6.0):
- FirebaseCore (~> 12.4.0) - FirebaseCore (~> 12.6.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (12.4.0): - FirebaseMessaging (12.6.0):
- FirebaseCore (~> 12.4.0) - FirebaseCore (~> 12.6.0)
- FirebaseInstallations (~> 12.4.0) - FirebaseInstallations (~> 12.6.0)
- GoogleDataTransport (~> 10.1) - GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
@ -134,28 +134,28 @@ PODS:
- google_mlkit_commons (0.11.0): - google_mlkit_commons (0.11.0):
- Flutter - Flutter
- MLKitVision - MLKitVision
- GoogleAdsOnDeviceConversion (3.1.0): - GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.4.0): - GoogleAppMeasurement/Core (12.6.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.4.0): - GoogleAppMeasurement/Default (12.6.0):
- GoogleAdsOnDeviceConversion (~> 3.1.0) - GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.4.0) - GoogleAppMeasurement/Core (= 12.6.0)
- GoogleAppMeasurement/IdentitySupport (= 12.4.0) - GoogleAppMeasurement/IdentitySupport (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.4.0): - GoogleAppMeasurement/IdentitySupport (12.6.0):
- GoogleAppMeasurement/Core (= 12.4.0) - GoogleAppMeasurement/Core (= 12.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
@ -217,6 +217,9 @@ PODS:
- GTMSessionFetcher/Core (3.5.0) - GTMSessionFetcher/Core (3.5.0)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- libwebp (1.5.0): - libwebp (1.5.0):
- libwebp/demux (= 1.5.0) - libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0) - libwebp/mux (= 1.5.0)
@ -273,10 +276,10 @@ PODS:
- restart_app (0.0.1): - restart_app (0.0.1):
- Flutter - Flutter
- ScreenProtectorKit (1.3.1) - ScreenProtectorKit (1.3.1)
- SDWebImage (5.21.3): - SDWebImage (5.21.5):
- SDWebImage/Core (= 5.21.3) - SDWebImage/Core (= 5.21.5)
- SDWebImage/Core (5.21.3) - SDWebImage/Core (5.21.5)
- SDWebImageWebPCoder (0.14.6): - SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.56.2) - Sentry/HybridSDK (8.56.2)
@ -317,7 +320,7 @@ PODS:
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
- sqlite3/rtree - sqlite3/rtree
- sqlite3/session - sqlite3/session
- SwiftProtobuf (1.33.1) - SwiftProtobuf (1.33.3)
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
@ -353,6 +356,7 @@ DEPENDENCIES:
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`) - google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
- GoogleUtilities - GoogleUtilities
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - 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`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`) - objective_c (from `.symlinks/plugins/objective_c/ios`)
@ -447,6 +451,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/google_mlkit_commons/ios" :path: ".symlinks/plugins/google_mlkit_commons/ios"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
local_auth_darwin: local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin" :path: ".symlinks/plugins/local_auth_darwin/darwin"
no_screenshot: no_screenshot:
@ -489,14 +495,14 @@ SPEC CHECKSUMS:
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e Firebase: a451a7b61536298fd5cbfe3a746fd40443a50679
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594 firebase_core: ba00a168e719694f38960502ceb560285603d073
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde firebase_messaging: bf0e29321927edc02a563c984dbfa5b063864b15
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f FirebaseAnalytics: d0a97a0db6425e5a5d966340b87f92ca7b13a557
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 FirebaseCore: 0e38ad5d62d980a47a64b8e9301ffa311457be04
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 FirebaseCoreInternal: 69bf1306a05b8ac43004f6cc1f804bb7b05b229e
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2 FirebaseInstallations: 631b38da2e11a83daa4bfb482f79d286a5dfa7ad
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5 FirebaseMessaging: a61bc42dcab3f7a346d94bbb54dab2c9435b18b2
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
@ -506,14 +512,15 @@ SPEC CHECKSUMS:
gal: baecd024ebfd13c441269ca7404792a7152fde89 gal: baecd024ebfd13c441269ca7404792a7152fde89
google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159 google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159
google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab google_mlkit_commons: 2abe6a70e1824e431d16a51085cb475b672c8aab
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1 GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
@ -530,8 +537,8 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf
ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7 Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0 sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
@ -539,7 +546,7 @@ SPEC CHECKSUMS:
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
SwiftProtobuf: 533a18409c3ca3a6156b2b1e46afd0f69e751aba SwiftProtobuf: e1b437c8e31a4c5577b643249a0bb62ed4f02153
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a

View file

@ -18,6 +18,7 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */; }; 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, ); }; }; 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 */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */ /* 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>"; }; 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; }; 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>"; }; 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; }; 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>"; }; 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; }; 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; buildActionMask = 2147483647;
files = ( files = (
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */, CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */,
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -206,6 +209,7 @@
E5079CCEE4804DB65AA3F23F /* Frameworks */ = { E5079CCEE4804DB65AA3F23F /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D25D4D1D2EF626E30029F805 /* StoreKit.framework */,
A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */, A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */,
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */, EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */,
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */, DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */,

View file

@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.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/providers/settings.provider.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -39,8 +40,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
await setUserPlan(); await setUserPlan();
}; };
globalCallbackUpdatePlan = (SubscriptionPlan plan) async { globalCallbackUpdatePlan = (SubscriptionPlan plan) {
await context.read<CustomChangeProvider>().updatePlan(plan); context.read<PurchasesProvider>().updatePlan(plan);
}; };
unawaited(initAsync()); unawaited(initAsync());
@ -50,7 +51,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
final user = await getUser(); final user = await getUser();
if (user != null && mounted) { if (user != null && mounted) {
if (mounted) { if (mounted) {
await context.read<CustomChangeProvider>().updatePlan( context.read<PurchasesProvider>().updatePlan(
planFromString(user.subscriptionPlan), planFromString(user.subscriptionPlan),
); );
} }
@ -59,7 +60,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
Future<void> initAsync() async { Future<void> initAsync() async {
await setUserPlan(); await setUserPlan();
await apiService.connect(force: true); await apiService.connect();
await apiService.listenToNetworkChanges(); await apiService.listenToNetworkChanges();
} }
@ -70,7 +71,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (wasPaused) { if (wasPaused) {
globalIsAppInBackground = false; globalIsAppInBackground = false;
twonlyDB.markUpdated(); twonlyDB.markUpdated();
unawaited(apiService.connect(force: true)); unawaited(apiService.connect());
} }
} else if (state == AppLifecycleState.paused) { } else if (state == AppLifecycleState.paused) {
wasPaused = true; wasPaused = true;

View file

@ -10,6 +10,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.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/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.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 { void main() async {
SentryWidgetsFlutterBinding.ensureInitialized(); SentryWidgetsFlutterBinding.ensureInitialized();
await initFCMService();
final user = await getUser(); final user = await getUser();
if (user != null) { if (user != null) {
gUser = user; gUser = user;
@ -43,8 +46,6 @@ void main() async {
unawaited(performTwonlySafeBackup()); unawaited(performTwonlySafeBackup());
} }
await initFCMService();
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory = globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path; (await getApplicationSupportDirectory()).path;
@ -54,9 +55,7 @@ void main() async {
final settingsController = SettingsChangeProvider(); final settingsController = SettingsChangeProvider();
await settingsController.loadSettings(); await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations( await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
[DeviceOrientation.portraitUp],
);
unawaited(setupPushNotification()); unawaited(setupPushNotification());
@ -79,6 +78,7 @@ void main() async {
ChangeNotifierProvider(create: (_) => settingsController), ChangeNotifierProvider(create: (_) => settingsController),
ChangeNotifierProvider(create: (_) => CustomChangeProvider()), ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()), ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
ChangeNotifierProvider(create: (_) => PurchasesProvider()),
], ],
child: const App(), child: const App(),
), ),

View 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';
}

View file

@ -42,8 +42,15 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.getSingleOrNull(); .getSingleOrNull();
} }
Future<List<Contact>> getContactsByUsername(String username) async { Future<List<Contact>> getContactsByUsername(
return (select(contacts)..where((t) => t.username.equals(username))).get(); String username, {
String username2 = '_______',
}) async {
return (select(contacts)
..where(
(t) => t.username.equals(username) | t.username.equals(username2),
))
.get();
} }
Future<void> deleteContactByUserId(int userId) { Future<void> deleteContactByUserId(int userId) {
@ -58,7 +65,8 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.write(updatedValues); .write(updatedValues);
if (updatedValues.blocked.present || if (updatedValues.blocked.present ||
updatedValues.displayName.present || updatedValues.displayName.present ||
updatedValues.nickName.present) { updatedValues.nickName.present ||
updatedValues.username.present) {
final contact = await getContactByUserId(userId).getSingleOrNull(); final contact = await getContactByUserId(userId).getSingleOrNull();
if (contact != null) { if (contact != null) {
await updatePushUser(contact); await updatePushUser(contact);
@ -118,7 +126,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
Stream<List<Contact>> watchAllAcceptedContacts() { Stream<List<Contact>> watchAllAcceptedContacts() {
return (select(contacts) 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(); .watch();
} }

View file

@ -386,14 +386,18 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
int getFlameCounterFromGroup(Group? group) { int getFlameCounterFromGroup(Group? group) {
if (group == null) return 0; if (group == null) return 0;
if (group.lastMessageSend == null || group.lastMessageReceived == null) { if (group.lastMessageSend == null ||
group.lastMessageReceived == null ||
group.lastFlameCounterChange == null) {
return 0; return 0;
} }
final now = DateTime.now(); final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day); final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
if (group.lastMessageSend!.isAfter(twoDaysAgo) && if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
group.lastMessageReceived!.isAfter(twoDaysAgo)) { group.lastMessageReceived!.isAfter(twoDaysAgo) ||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
return group.flameCounter + 1; return group.flameCounter + 1;
} else { } else {
return 0; return 0;

File diff suppressed because one or more lines are too long

View file

@ -90,8 +90,7 @@ class GroupHistories extends Table {
IntColumn get contactId => IntColumn get contactId =>
integer().nullable().references(Contacts, #userId)(); integer().nullable().references(Contacts, #userId)();
IntColumn get affectedContactId => IntColumn get affectedContactId => integer().nullable()();
integer().nullable().references(Contacts, #userId)();
TextColumn get oldGroupName => text().nullable()(); TextColumn get oldGroupName => text().nullable()();
TextColumn get newGroupName => text().nullable()(); TextColumn get newGroupName => text().nullable()();

View file

@ -67,7 +67,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 3; int get schemaVersion => 4;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -92,6 +92,17 @@ class TwonlyDB extends _$TwonlyDB {
from2To3: (m, schema) async { from2To3: (m, schema) async {
await m.addColumn(schema.groups, schema.groups.draftMessage); 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,
},
),
);
},
), ),
); );
} }

View file

@ -1566,9 +1566,390 @@ class Shape17 extends i0.VersionedTable {
i1.GeneratedColumn<String> _column_100(String aliasedName) => i1.GeneratedColumn<String> _column_100(String aliasedName) =>
i1.GeneratedColumn<String>('draft_message', aliasedName, true, i1.GeneratedColumn<String>('draft_message', aliasedName, true,
type: i1.DriftSqlType.string); 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({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, 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, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -1582,6 +1963,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema); await from2To3(migrator, schema);
return 3; return 3;
case 3:
final schema = Schema4(database: database);
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -1591,9 +1977,11 @@ i0.MigrationStepWithVersion migrationSteps({
i1.OnUpgrade stepByStep({ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, 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, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
from2To3: from2To3, from2To3: from2To3,
from3To4: from3To4,
)); ));

View file

@ -197,7 +197,7 @@
"errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.", "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.", "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.", "upgradeToPaidPlan": "Upgrade auf einen kostenpflichtigen Plan.",
"upgradeToPaidPlanButton": "Auf {planId} upgraden", "upgradeToPaidPlanButton": "Auf {planId} upgraden{sufix}",
"partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!", "partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!",
"errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.", "errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.",
"errorPlanLimitReached": "Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.", "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.", "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.", "errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.",
"proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", "proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"proFeature2": "1 zusätzlicher Plus Benutzer", "proFeature2": "✓ 1 zusätzlicher Plus Benutzer",
"proFeature3": "Flammen wiederherstellen", "proFeature3": "✓ Flammen wiederherstellen",
"proFeature4": "Cloud-Backup verschlüsselt (coming-soon)", "proFeature4": "✓ twonly unterstützen",
"year": "year", "year": "Jahr",
"month": "month", "month": "Monat",
"familyFeature1": "✓ Alles von Pro", "yearly": "Jährlich",
"familyFeature2": "4 zusätzliche Plus Benutzer", "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.", "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", "plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"plusFeature2": "Zusatzfunktionen (coming-soon)", "plusFeature2": "Zusatzfunktionen (coming-soon)",
"transactionHistory": "Transaktionshistorie", "transactionHistory": "Transaktionshistorie",
"currentBalance": "Dein Guthaben", "currentBalance": "Dein Guthaben",
"manageAdditionalUsers": "Zusätzliche Benutzer verwalten", "manageAdditionalUsers": "Zusätzliche Benutzer verwalten",
@ -409,7 +413,7 @@
"notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.", "notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.",
"notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.", "notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.",
"notificationResponse": "hat dir{inGroup} geantwortet.", "notificationResponse": "hat dir{inGroup} geantwortet.",
"notificationTitleUnknownUser": "Jemand", "notificationTitleUnknownUser": "[Unbekannt]",
"notificationCategoryMessageTitle": "Nachrichten", "notificationCategoryMessageTitle": "Nachrichten",
"notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.", "notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.",
"groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.", "groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.",

View file

@ -232,22 +232,26 @@
"errorPlanNotAllowed": "This feature is not available in your current plan.", "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.", "errorPlanUpgradeNotYearly": "The plan upgrade must be paid for annually, as the current plan is also billed annually.",
"upgradeToPaidPlan": "Upgrade to a paid plan.", "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}!", "partOfPaidPlanOf": "You are part of the paid plan of {username}!",
"year": "year", "year": "year",
"yearly": "Yearly",
"month": "month", "month": "month",
"monthly": "Monthly",
"proFeature1": "✓ Unlimited media file uploads", "proFeature1": "✓ Unlimited media file uploads",
"proFeature2": "1 additional Plus user", "proFeature2": "✓ 1 additional Plus user",
"proFeature3": "Cloud-Backup encrypted (coming-soon)", "proFeature3": "✓ Restore flames",
"proFeature4": "Additional features (coming-soon)", "proFeature4": "✓ Support twonly",
"familyFeature1": "✓ All from Pro", "familyFeature1": "✓ Unlimited media file uploads",
"familyFeature2": "4 additional Plus users", "familyFeature2": "✓ 4 additional Plus user",
"familyFeature3": "✓ Restore flames",
"familyFeature4": "✓ Support twonly",
"redeemUserInviteCode": "Or redeem a twonly-Code.", "redeemUserInviteCode": "Or redeem a twonly-Code.",
"redeemUserInviteCodeTitle": "Redeem twonly-Code", "redeemUserInviteCodeTitle": "Redeem twonly-Code",
"redeemUserInviteCodeSuccess": "Your plan has been successfully adjusted.", "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", "plusFeature1": "✓ Unlimited media file uploads",
"plusFeature2": "Additional features (coming-soon)", "plusFeature2": "Additional features (coming-soon)",
"transactionHistory": "Your transaction history", "transactionHistory": "Your transaction history",
"manageSubscription": "Manage your subscription", "manageSubscription": "Manage your subscription",
"nextPayment": "Next payment", "nextPayment": "Next payment",
@ -439,7 +443,7 @@
"notificationReactionToImage": "has reacted with {reaction} to your image.", "notificationReactionToImage": "has reacted with {reaction} to your image.",
"notificationReactionToAudio": "has reacted with {reaction} to your audio message.", "notificationReactionToAudio": "has reacted with {reaction} to your audio message.",
"notificationResponse": "has responded{inGroup}.", "notificationResponse": "has responded{inGroup}.",
"notificationTitleUnknownUser": "Someone", "notificationTitleUnknownUser": "[Unknown]",
"notificationCategoryMessageTitle": "Messages", "notificationCategoryMessageTitle": "Messages",
"notificationCategoryMessageDesc": "Messages from other users.", "notificationCategoryMessageDesc": "Messages from other users.",
"groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.", "groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.",

View file

@ -1313,8 +1313,8 @@ abstract class AppLocalizations {
/// No description provided for @upgradeToPaidPlanButton. /// No description provided for @upgradeToPaidPlanButton.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Upgrade subscription to {planId}'** /// **'Upgrade to {planId}{sufix}'**
String upgradeToPaidPlanButton(Object planId); String upgradeToPaidPlanButton(Object planId, Object sufix);
/// No description provided for @partOfPaidPlanOf. /// No description provided for @partOfPaidPlanOf.
/// ///
@ -1328,12 +1328,24 @@ abstract class AppLocalizations {
/// **'year'** /// **'year'**
String get year; String get year;
/// No description provided for @yearly.
///
/// In en, this message translates to:
/// **'Yearly'**
String get yearly;
/// No description provided for @month. /// No description provided for @month.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'month'** /// **'month'**
String get month; String get month;
/// No description provided for @monthly.
///
/// In en, this message translates to:
/// **'Monthly'**
String get monthly;
/// No description provided for @proFeature1. /// No description provided for @proFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1343,33 +1355,45 @@ abstract class AppLocalizations {
/// No description provided for @proFeature2. /// No description provided for @proFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'1 additional Plus user'** /// **'1 additional Plus user'**
String get proFeature2; String get proFeature2;
/// No description provided for @proFeature3. /// No description provided for @proFeature3.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Cloud-Backup encrypted (coming-soon)'** /// **'✓ Restore flames'**
String get proFeature3; String get proFeature3;
/// No description provided for @proFeature4. /// No description provided for @proFeature4.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Additional features (coming-soon)'** /// **'✓ Support twonly'**
String get proFeature4; String get proFeature4;
/// No description provided for @familyFeature1. /// No description provided for @familyFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'All from Pro'** /// **'Unlimited media file uploads'**
String get familyFeature1; String get familyFeature1;
/// No description provided for @familyFeature2. /// No description provided for @familyFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'4 additional Plus users'** /// **'4 additional Plus user'**
String get familyFeature2; 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. /// No description provided for @redeemUserInviteCode.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1391,7 +1415,7 @@ abstract class AppLocalizations {
/// No description provided for @freeFeature1. /// No description provided for @freeFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'10 Media file uploads per day'** /// **'10 Media file uploads per day'**
String get freeFeature1; String get freeFeature1;
/// No description provided for @plusFeature1. /// No description provided for @plusFeature1.
@ -1403,7 +1427,7 @@ abstract class AppLocalizations {
/// No description provided for @plusFeature2. /// No description provided for @plusFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Additional features (coming-soon)'** /// **'Additional features (coming-soon)'**
String get plusFeature2; String get plusFeature2;
/// No description provided for @transactionHistory. /// No description provided for @transactionHistory.
@ -2555,7 +2579,7 @@ abstract class AppLocalizations {
/// No description provided for @notificationTitleUnknownUser. /// No description provided for @notificationTitleUnknownUser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Someone'** /// **'[Unknown]'**
String get notificationTitleUnknownUser; String get notificationTitleUnknownUser;
/// No description provided for @notificationCategoryMessageTitle. /// No description provided for @notificationCategoryMessageTitle.

View file

@ -685,8 +685,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get upgradeToPaidPlan => 'Upgrade auf einen kostenpflichtigen Plan.'; String get upgradeToPaidPlan => 'Upgrade auf einen kostenpflichtigen Plan.';
@override @override
String upgradeToPaidPlanButton(Object planId) { String upgradeToPaidPlanButton(Object planId, Object sufix) {
return 'Auf $planId upgraden'; return 'Auf $planId upgraden$sufix';
} }
@override @override
@ -695,28 +695,40 @@ class AppLocalizationsDe extends AppLocalizations {
} }
@override @override
String get year => 'year'; String get year => 'Jahr';
@override @override
String get month => 'month'; String get yearly => 'Jährlich';
@override
String get month => 'Monat';
@override
String get monthly => 'Monatlich';
@override @override
String get proFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; String get proFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@override @override
String get proFeature2 => '1 zusätzlicher Plus Benutzer'; String get proFeature2 => '1 zusätzlicher Plus Benutzer';
@override @override
String get proFeature3 => 'Flammen wiederherstellen'; String get proFeature3 => 'Flammen wiederherstellen';
@override @override
String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)'; String get proFeature4 => '✓ twonly unterstützen';
@override @override
String get familyFeature1 => 'Alles von Pro'; String get familyFeature1 => 'Unbegrenzte Medien-Datei-Uploads';
@override @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 @override
String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.'; String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.';
@ -729,13 +741,13 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Plan wurde erfolgreich angepasst.'; 'Dein Plan wurde erfolgreich angepasst.';
@override @override
String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag'; String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag';
@override @override
String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@override @override
String get plusFeature2 => 'Zusatzfunktionen (coming-soon)'; String get plusFeature2 => 'Zusatzfunktionen (coming-soon)';
@override @override
String get transactionHistory => 'Transaktionshistorie'; String get transactionHistory => 'Transaktionshistorie';
@ -1409,7 +1421,7 @@ class AppLocalizationsDe extends AppLocalizations {
} }
@override @override
String get notificationTitleUnknownUser => 'Jemand'; String get notificationTitleUnknownUser => '[Unbekannt]';
@override @override
String get notificationCategoryMessageTitle => 'Nachrichten'; String get notificationCategoryMessageTitle => 'Nachrichten';

View file

@ -679,8 +679,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get upgradeToPaidPlan => 'Upgrade to a paid plan.'; String get upgradeToPaidPlan => 'Upgrade to a paid plan.';
@override @override
String upgradeToPaidPlanButton(Object planId) { String upgradeToPaidPlanButton(Object planId, Object sufix) {
return 'Upgrade subscription to $planId'; return 'Upgrade to $planId$sufix';
} }
@override @override
@ -691,26 +691,38 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get year => 'year'; String get year => 'year';
@override
String get yearly => 'Yearly';
@override @override
String get month => 'month'; String get month => 'month';
@override
String get monthly => 'Monthly';
@override @override
String get proFeature1 => '✓ Unlimited media file uploads'; String get proFeature1 => '✓ Unlimited media file uploads';
@override @override
String get proFeature2 => '1 additional Plus user'; String get proFeature2 => '1 additional Plus user';
@override @override
String get proFeature3 => 'Cloud-Backup encrypted (coming-soon)'; String get proFeature3 => '✓ Restore flames';
@override @override
String get proFeature4 => 'Additional features (coming-soon)'; String get proFeature4 => '✓ Support twonly';
@override @override
String get familyFeature1 => 'All from Pro'; String get familyFeature1 => 'Unlimited media file uploads';
@override @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 @override
String get redeemUserInviteCode => 'Or redeem a twonly-Code.'; String get redeemUserInviteCode => 'Or redeem a twonly-Code.';
@ -723,13 +735,13 @@ class AppLocalizationsEn extends AppLocalizations {
'Your plan has been successfully adjusted.'; 'Your plan has been successfully adjusted.';
@override @override
String get freeFeature1 => '10 Media file uploads per day'; String get freeFeature1 => '10 Media file uploads per day';
@override @override
String get plusFeature1 => '✓ Unlimited media file uploads'; String get plusFeature1 => '✓ Unlimited media file uploads';
@override @override
String get plusFeature2 => 'Additional features (coming-soon)'; String get plusFeature2 => 'Additional features (coming-soon)';
@override @override
String get transactionHistory => 'Your transaction history'; String get transactionHistory => 'Your transaction history';
@ -1401,7 +1413,7 @@ class AppLocalizationsEn extends AppLocalizations {
} }
@override @override
String get notificationTitleUnknownUser => 'Someone'; String get notificationTitleUnknownUser => '[Unknown]';
@override @override
String get notificationCategoryMessageTitle => 'Messages'; String get notificationCategoryMessageTitle => 'Messages';

View file

@ -41,6 +41,8 @@ class UserData {
@JsonKey(defaultValue: 'Free') @JsonKey(defaultValue: 'Free')
String subscriptionPlan; String subscriptionPlan;
String? subscriptionPlanIdStore;
DateTime? lastImageSend; DateTime? lastImageSend;
int? todaysImageCounter; int? todaysImageCounter;
@ -86,8 +88,11 @@ class UserData {
List<int>? lastChangeLogHash; List<int>? lastChangeLogHash;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: true)
bool hideChangeLog = false; bool hideChangeLog = true;
@JsonKey(defaultValue: true)
bool updateFCMToken = true;
// --- BACKUP --- // --- BACKUP ---

View file

@ -20,6 +20,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..disableVideoCompression = ..disableVideoCompression =
json['disableVideoCompression'] as bool? ?? false json['disableVideoCompression'] as bool? ?? false
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
..lastImageSend = json['lastImageSend'] == null ..lastImageSend = json['lastImageSend'] == null
? null ? null
: DateTime.parse(json['lastImageSend'] as String) : DateTime.parse(json['lastImageSend'] as String)
@ -61,6 +62,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
?.map((e) => (e as num).toInt()) ?.map((e) => (e as num).toInt())
.toList() .toList()
..hideChangeLog = json['hideChangeLog'] as bool? ?? false ..hideChangeLog = json['hideChangeLog'] as bool? ?? false
..updateFCMToken = json['updateFCMToken'] as bool? ?? true
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null ..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null ? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String) : DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
@ -84,6 +86,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'disableVideoCompression': instance.disableVideoCompression, 'disableVideoCompression': instance.disableVideoCompression,
'deviceId': instance.deviceId, 'deviceId': instance.deviceId,
'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlan': instance.subscriptionPlan,
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
'lastImageSend': instance.lastImageSend?.toIso8601String(), 'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter, 'todaysImageCounter': instance.todaysImageCounter,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
@ -104,6 +107,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,
'hideChangeLog': instance.hideChangeLog, 'hideChangeLog': instance.hideChangeLog,
'updateFCMToken': instance.updateFCMToken,
'nextTimeToShowBackupNotice': 'nextTimeToShowBackupNotice':
instance.nextTimeToShowBackupNotice?.toIso8601String(), instance.nextTimeToShowBackupNotice?.toIso8601String(),
'backupServer': instance.backupServer, 'backupServer': instance.backupServer,

View file

@ -2085,6 +2085,136 @@ class ApplicationData_ReportUser extends $pb.GeneratedMessage {
void clearReason() => $_clearField(2); 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 { class ApplicationData_DeleteAccount extends $pb.GeneratedMessage {
factory ApplicationData_DeleteAccount() => create(); factory ApplicationData_DeleteAccount() => create();
@ -2153,6 +2283,8 @@ enum ApplicationData_ApplicationData {
deleteAccount, deleteAccount,
reportUser, reportUser,
changeUsername, changeUsername,
ipaPurchase,
ipaForceCheck,
notSet notSet
} }
@ -2180,6 +2312,8 @@ class ApplicationData extends $pb.GeneratedMessage {
ApplicationData_DeleteAccount? deleteAccount, ApplicationData_DeleteAccount? deleteAccount,
ApplicationData_ReportUser? reportUser, ApplicationData_ReportUser? reportUser,
ApplicationData_ChangeUsername? changeUsername, ApplicationData_ChangeUsername? changeUsername,
ApplicationData_IPAPurchase? ipaPurchase,
ApplicationData_IPAForceCheck? ipaForceCheck,
}) { }) {
final result = create(); final result = create();
if (textMessage != null) result.textMessage = textMessage; if (textMessage != null) result.textMessage = textMessage;
@ -2212,6 +2346,8 @@ class ApplicationData extends $pb.GeneratedMessage {
if (deleteAccount != null) result.deleteAccount = deleteAccount; if (deleteAccount != null) result.deleteAccount = deleteAccount;
if (reportUser != null) result.reportUser = reportUser; if (reportUser != null) result.reportUser = reportUser;
if (changeUsername != null) result.changeUsername = changeUsername; if (changeUsername != null) result.changeUsername = changeUsername;
if (ipaPurchase != null) result.ipaPurchase = ipaPurchase;
if (ipaForceCheck != null) result.ipaForceCheck = ipaForceCheck;
return result; return result;
} }
@ -2248,6 +2384,8 @@ class ApplicationData extends $pb.GeneratedMessage {
24: ApplicationData_ApplicationData.deleteAccount, 24: ApplicationData_ApplicationData.deleteAccount,
25: ApplicationData_ApplicationData.reportUser, 25: ApplicationData_ApplicationData.reportUser,
26: ApplicationData_ApplicationData.changeUsername, 26: ApplicationData_ApplicationData.changeUsername,
27: ApplicationData_ApplicationData.ipaPurchase,
28: ApplicationData_ApplicationData.ipaForceCheck,
0: ApplicationData_ApplicationData.notSet 0: ApplicationData_ApplicationData.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -2277,7 +2415,9 @@ class ApplicationData extends $pb.GeneratedMessage {
23, 23,
24, 24,
25, 25,
26 26,
27,
28
]) ])
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage', ..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
protoName: 'textMessage', protoName: 'textMessage',
@ -2361,6 +2501,13 @@ class ApplicationData extends $pb.GeneratedMessage {
26, _omitFieldNames ? '' : 'changeUsername', 26, _omitFieldNames ? '' : 'changeUsername',
protoName: 'changeUsername', protoName: 'changeUsername',
subBuilder: ApplicationData_ChangeUsername.create) 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; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2652,6 +2799,29 @@ class ApplicationData extends $pb.GeneratedMessage {
void clearChangeUsername() => $_clearField(26); void clearChangeUsername() => $_clearField(26);
@$pb.TagNumber(26) @$pb.TagNumber(26)
ApplicationData_ChangeUsername ensureChangeUsername() => $_ensure(21); 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 { class Response_PreKey extends $pb.GeneratedMessage {

View file

@ -461,6 +461,24 @@ const ApplicationData$json = {
'9': 0, '9': 0,
'10': 'changeUsername' '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': [ '3': [
ApplicationData_TextMessage$json, ApplicationData_TextMessage$json,
@ -484,6 +502,8 @@ const ApplicationData$json = {
ApplicationData_UpdateSignedPreKey$json, ApplicationData_UpdateSignedPreKey$json,
ApplicationData_DownloadDone$json, ApplicationData_DownloadDone$json,
ApplicationData_ReportUser$json, ApplicationData_ReportUser$json,
ApplicationData_IPAPurchase$json,
ApplicationData_IPAForceCheck$json,
ApplicationData_DeleteAccount$json ApplicationData_DeleteAccount$json
], ],
'8': [ '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') @$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_DeleteAccount$json = { const ApplicationData_DeleteAccount$json = {
'1': 'DeleteAccount', '1': 'DeleteAccount',
@ -713,29 +754,34 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVBY2NvdW50Ek4KCnJlcG9ydFVzZXIY' 'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVBY2NvdW50Ek4KCnJlcG9ydFVzZXIY'
'GSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZXBvcnRVc2VySABSCn' 'GSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZXBvcnRVc2VySABSCn'
'JlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsyMC5jbGllbnRfdG9fc2VydmVyLkFw' 'JlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsyMC5jbGllbnRfdG9fc2VydmVyLkFw'
'cGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZRpqCgtUZXh0TW' 'cGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZRJRCgtpcGFQdX'
'Vzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglw' 'JjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLklQQVB1cmNo'
'dXNoX2RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQn' 'YXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2sYHCABKAsyLy5jbGllbnRfdG9fc2'
'lVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUS' 'VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNrSABSDWlwYUZvcmNlQ2hlY2saagoL'
'GgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCm' 'VGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZH'
'dvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEg' 'kSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0'
'ASgDUgZ1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGn' 'VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZX'
'AKEVN3aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9u' 'JuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tl'
'dGhseRgCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2' 'bhIdCgpnb29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl'
'FsGjYKEVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3' '9pZBgBIAEoA1IGdXNlcklkGikKDVJlZGVlbVZvdWNoZXISGAoHdm91Y2hlchgBIAEoCVIHdm91'
'YWwaMAoNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCg' 'Y2hlchpwChFTd2l0Y2hUb1BheWVkUGxhbhIXCgdwbGFuX2lkGAEgASgJUgZwbGFuSWQSHwoLcG'
'tHZXRMb2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRB' 'F5X21vbnRobHkYAiABKAhSCnBheU1vbnRobHkSIQoMYXV0b19yZW5ld2FsGAMgASgIUgthdXRv'
'ZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdG' 'UmVuZXdhbBo2ChFVcGRhdGVQbGFuT3B0aW9ucxIhCgxhdXRvX3JlbmV3YWwYASABKAhSC2F1dG'
'lvbmFsQ29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRp' '9SZW5ld2FsGjAKDUNyZWF0ZVZvdWNoZXISHwoLdmFsdWVfY2VudHMYASABKA1SCnZhbHVlQ2Vu'
'dGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlck' 'dHMaDQoLR2V0TG9jYXRpb24aDQoLR2V0Vm91Y2hlcnMaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFw'
'lkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIX' 'oVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUKE0dldEN1cnJlbnRQbGFuSW5mb3MaNwoUUmVkZWVt'
'Cgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZW' 'QWRkaXRpb25hbENvZGUSHwoLaW52aXRlX2NvZGUYAiABKAlSCmludml0ZUNvZGUaLwoUUmVtb3'
'RfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgM' 'ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNC'
'UgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZF' 'eVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2'
'ByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxS' 'VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQ'
'DWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg' 'c2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleR'
'5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhoPCg1EZWxldGVBY2NvdW50' 'gCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVz'
'QhEKD0FwcGxpY2F0aW9uRGF0YQ=='); 'aWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGA'
'EgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgB'
'IAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2'
'hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291'
'cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQU'
'ZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudEIRCg9BcHBsaWNhdGlvbkRhdGE=');
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {

View file

@ -87,6 +87,8 @@ class ErrorCode extends $pb.ProtobufEnum {
ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork'); ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork');
static const ErrorCode RegistrationDisabled = static const ErrorCode RegistrationDisabled =
ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled'); ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled');
static const ErrorCode IPAPaymentExpired =
ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired');
static const $core.List<ErrorCode> values = <ErrorCode>[ static const $core.List<ErrorCode> values = <ErrorCode>[
Unknown, Unknown,
@ -125,6 +127,7 @@ class ErrorCode extends $pb.ProtobufEnum {
NewDeviceRegistered, NewDeviceRegistered,
InvalidProofOfWork, InvalidProofOfWork,
RegistrationDisabled, RegistrationDisabled,
IPAPaymentExpired,
]; ];
static final $core.Map<$core.int, ErrorCode> _byValue = static final $core.Map<$core.int, ErrorCode> _byValue =

View file

@ -54,6 +54,7 @@ const ErrorCode$json = {
{'1': 'NewDeviceRegistered', '2': 1031}, {'1': 'NewDeviceRegistered', '2': 1031},
{'1': 'InvalidProofOfWork', '2': 1032}, {'1': 'InvalidProofOfWork', '2': 1032},
{'1': 'RegistrationDisabled', '2': 1033}, {'1': 'RegistrationDisabled', '2': 1033},
{'1': 'IPAPaymentExpired', '2': 1034},
], ],
}; };
@ -74,4 +75,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW' 'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu' 'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh' 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCA=='); 'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
'UEFQYXltZW50RXhwaXJlZBCKCA==');

View file

@ -92,7 +92,14 @@ class ServerToClient extends $pb.GeneratedMessage {
V0 ensureV0() => $_ensure(0); 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 { class V0 extends $pb.GeneratedMessage {
factory V0({ factory V0({
@ -101,6 +108,7 @@ class V0 extends $pb.GeneratedMessage {
NewMessage? newMessage, NewMessage? newMessage,
$core.bool? requestNewPreKeys, $core.bool? requestNewPreKeys,
$0.ErrorCode? error, $0.ErrorCode? error,
NewMessages? newMessages,
}) { }) {
final result = create(); final result = create();
if (seq != null) result.seq = seq; if (seq != null) result.seq = seq;
@ -108,6 +116,7 @@ class V0 extends $pb.GeneratedMessage {
if (newMessage != null) result.newMessage = newMessage; if (newMessage != null) result.newMessage = newMessage;
if (requestNewPreKeys != null) result.requestNewPreKeys = requestNewPreKeys; if (requestNewPreKeys != null) result.requestNewPreKeys = requestNewPreKeys;
if (error != null) result.error = error; if (error != null) result.error = error;
if (newMessages != null) result.newMessages = newMessages;
return result; return result;
} }
@ -125,6 +134,7 @@ class V0 extends $pb.GeneratedMessage {
3: V0_Kind.newMessage, 3: V0_Kind.newMessage,
4: V0_Kind.requestNewPreKeys, 4: V0_Kind.requestNewPreKeys,
6: V0_Kind.error, 6: V0_Kind.error,
7: V0_Kind.newMessages,
0: V0_Kind.notSet 0: V0_Kind.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -132,7 +142,7 @@ class V0 extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [2, 3, 4, 6]) ..oo(0, [2, 3, 4, 6, 7])
..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6, ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO) defaultOrMaker: $fixnum.Int64.ZERO)
..aOM<Response>(2, _omitFieldNames ? '' : 'response', ..aOM<Response>(2, _omitFieldNames ? '' : 'response',
@ -145,6 +155,8 @@ class V0 extends $pb.GeneratedMessage {
defaultOrMaker: $0.ErrorCode.Unknown, defaultOrMaker: $0.ErrorCode.Unknown,
valueOf: $0.ErrorCode.valueOf, valueOf: $0.ErrorCode.valueOf,
enumValues: $0.ErrorCode.values) enumValues: $0.ErrorCode.values)
..aOM<NewMessages>(7, _omitFieldNames ? '' : 'newMessages',
protoName: 'newMessages', subBuilder: NewMessages.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$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); $core.bool hasError() => $_has(4);
@$pb.TagNumber(6) @$pb.TagNumber(6)
void clearError() => $_clearField(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 { class NewMessage extends $pb.GeneratedMessage {
@ -287,6 +310,58 @@ class NewMessage extends $pb.GeneratedMessage {
void clearFromUserId() => $_clearField(2); 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 { class Response_Authenticated extends $pb.GeneratedMessage {
factory Response_Authenticated({ factory Response_Authenticated({
$core.String? plan, $core.String? plan,

View file

@ -61,6 +61,15 @@ const V0$json = {
'9': 0, '9': 0,
'10': 'newMessage' '10': 'newMessage'
}, },
{
'1': 'newMessages',
'3': 7,
'4': 1,
'5': 11,
'6': '.server_to_client.NewMessages',
'9': 0,
'10': 'newMessages'
},
{ {
'1': 'RequestNewPreKeys', '1': 'RequestNewPreKeys',
'3': 4, '3': 4,
@ -88,9 +97,10 @@ const V0$json = {
final $typed_data.Uint8List v0Descriptor = $convert.base64Decode( final $typed_data.Uint8List v0Descriptor = $convert.base64Decode(
'CgJWMBIQCgNzZXEYASABKARSA3NlcRI4CghyZXNwb25zZRgCIAEoCzIaLnNlcnZlcl90b19jbG' 'CgJWMBIQCgNzZXEYASABKARSA3NlcRI4CghyZXNwb25zZRgCIAEoCzIaLnNlcnZlcl90b19jbG'
'llbnQuUmVzcG9uc2VIAFIIcmVzcG9uc2USPgoKbmV3TWVzc2FnZRgDIAEoCzIcLnNlcnZlcl90' 'llbnQuUmVzcG9uc2VIAFIIcmVzcG9uc2USPgoKbmV3TWVzc2FnZRgDIAEoCzIcLnNlcnZlcl90'
'b19jbGllbnQuTmV3TWVzc2FnZUgAUgpuZXdNZXNzYWdlEi4KEVJlcXVlc3ROZXdQcmVLZXlzGA' 'b19jbGllbnQuTmV3TWVzc2FnZUgAUgpuZXdNZXNzYWdlEkEKC25ld01lc3NhZ2VzGAcgASgLMh'
'QgASgISABSEVJlcXVlc3ROZXdQcmVLZXlzEigKBWVycm9yGAYgASgOMhAuZXJyb3IuRXJyb3JD' '0uc2VydmVyX3RvX2NsaWVudC5OZXdNZXNzYWdlc0gAUgtuZXdNZXNzYWdlcxIuChFSZXF1ZXN0'
'b2RlSABSBWVycm9yQgYKBEtpbmQ='); 'TmV3UHJlS2V5cxgEIAEoCEgAUhFSZXF1ZXN0TmV3UHJlS2V5cxIoCgVlcnJvchgGIAEoDjIQLm'
'Vycm9yLkVycm9yQ29kZUgAUgVlcnJvckIGCgRLaW5k');
@$core.Deprecated('Use newMessageDescriptor instead') @$core.Deprecated('Use newMessageDescriptor instead')
const NewMessage$json = { const NewMessage$json = {
@ -106,6 +116,26 @@ final $typed_data.Uint8List newMessageDescriptor = $convert.base64Decode(
'CgpOZXdNZXNzYWdlEiAKDGZyb21fdXNlcl9pZBgCIAEoA1IKZnJvbVVzZXJJZBISCgRib2R5GA' 'CgpOZXdNZXNzYWdlEiAKDGZyb21fdXNlcl9pZBgCIAEoA1IKZnJvbVVzZXJJZBISCgRib2R5GA'
'EgASgMUgRib2R5'); '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') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {
'1': 'Response', '1': 'Response',

View 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;
}

View file

@ -1,17 +1,10 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/src/services/subscription.service.dart';
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
bool _isConnected = false; bool _isConnected = false;
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
SubscriptionPlan plan = SubscriptionPlan.Free;
Future<void> updateConnectionState(bool update) async { Future<void> updateConnectionState(bool update) async {
_isConnected = update; _isConnected = update;
notifyListeners(); notifyListeners();
} }
Future<void> updatePlan(SubscriptionPlan newPlan) async {
plan = newPlan;
notifyListeners();
}
} }

View 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
}
}

View file

@ -98,12 +98,13 @@ class ApiService {
unawaited(signalHandleNewServerConnection()); unawaited(signalHandleNewServerConnection());
unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchGroupStatesForUnjoinedGroups());
unawaited(fetchMissingGroupPublicKey()); unawaited(fetchMissingGroupPublicKey());
unawaited(checkForDeletedUsernames());
} }
} }
Future<void> onConnected() async { Future<void> onConnected() async {
await authenticate(); await authenticate();
_reconnectionDelay = 5; _reconnectionDelay = 1;
globalCallbackConnectionState(isConnected: true); globalCallbackConnectionState(isConnected: true);
} }
@ -112,16 +113,21 @@ class ApiService {
isAuthenticated = false; isAuthenticated = false;
globalCallbackConnectionState(isConnected: false); globalCallbackConnectionState(isConnected: false);
await twonlyDB.mediaFilesDao.resetPendingDownloadState(); await twonlyDB.mediaFilesDao.resetPendingDownloadState();
await startReconnectionTimer();
} }
Future<void> startReconnectionTimer() async { Future<void> startReconnectionTimer() async {
if (reconnectionTimer?.isActive ?? false) {
return;
}
reconnectionTimer?.cancel(); reconnectionTimer?.cancel();
reconnectionTimer ??= Log.info('Starting reconnection timer with $_reconnectionDelay s delay');
Timer(Duration(seconds: _reconnectionDelay), () async { reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async {
Log.info('Reconnection timer triggered');
reconnectionTimer = null; reconnectionTimer = null;
await connect(force: true); await connect();
}); });
_reconnectionDelay += 5; _reconnectionDelay = 3;
} }
Future<void> close(Function callback) async { Future<void> close(Function callback) async {
@ -143,18 +149,13 @@ class ApiService {
.onConnectivityChanged .onConnectivityChanged
.listen((List<ConnectivityResult> result) async { .listen((List<ConnectivityResult> result) async {
if (!result.contains(ConnectivityResult.none)) { if (!result.contains(ConnectivityResult.none)) {
await connect(force: true); await connect();
} }
// Received changes in available connectivity types! // Received changes in available connectivity types!
}); });
} }
Future<bool> connect({bool force = false}) async { Future<bool> connect() async {
if (reconnectionTimer != null && !force) {
return false;
}
reconnectionTimer?.cancel();
reconnectionTimer = null;
return lockConnecting.protect<bool>(() async { return lockConnecting.protect<bool>(() async {
if (_channel != null) { if (_channel != null) {
return true; return true;
@ -290,6 +291,7 @@ class ApiService {
if (_channel == null) { if (_channel == null) {
Log.warn('sending request while api is not connected'); Log.warn('sending request while api is not connected');
if (!await connect()) { if (!await connect()) {
Log.warn('could not connected again');
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
} }
if (_channel == null) { if (_channel == null) {
@ -300,6 +302,17 @@ class ApiService {
_channel!.sink.add(requestBytes); _channel!.sink.add(requestBytes);
final res = asResult(await _waitForResponse(seq)); 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) { if (res.isError) {
Log.warn('Got error from server: ${res.error}'); Log.warn('Got error from server: ${res.error}');
if (res.error == ErrorCode.AppVersionOutdated) { if (res.error == ErrorCode.AppVersionOutdated) {
@ -339,9 +352,8 @@ class ApiService {
if (contact != null) { if (contact != null) {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contactId, contactId,
ContactsCompanion( const ContactsCompanion(
accountDeleted: const Value(true), accountDeleted: Value(true),
username: Value('${contact.username} (${contact.userId})'),
), ),
); );
} }
@ -375,15 +387,6 @@ class ApiService {
final result = await sendRequestSync(req, authenticated: false); final result = await sendRequestSync(req, authenticated: false);
if (result.isSuccess) { 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'); Log.info('websocket is authenticated');
unawaited(onAuthenticated()); unawaited(onAuthenticated());
return true; return true;
@ -491,6 +494,20 @@ class ApiService {
return sendRequestSync(req); 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 { Future<Response_UserData?> getUserById(int userId) async {
final get = ApplicationData_GetUserById()..userId = Int64(userId); final get = ApplicationData_GetUserById()..userId = Int64(userId);
final appData = ApplicationData()..getUserById = get; final appData = ApplicationData()..getUserById = get;
@ -662,6 +679,22 @@ class ApiService {
return sendRequestSync(req); 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 { Future<Result> changeUsername(String username) async {
final get = ApplicationData_ChangeUsername()..username = username; final get = ApplicationData_ChangeUsername()..username = username;
final appData = ApplicationData()..changeUsername = get; final appData = ApplicationData()..changeUsername = get;
@ -669,6 +702,15 @@ class ApiService {
return sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> forceIpaCheck() async {
final req = createClientToServerFromApplicationData(
ApplicationData(
ipaForceCheck: ApplicationData_IPAForceCheck(),
),
);
return sendRequestSync(req);
}
Future<Result> updateSignedPreKey( Future<Result> updateSignedPreKey(
int signedPreKeyId, int signedPreKeyId,
Uint8List signedPreKey, Uint8List signedPreKey,
@ -722,6 +764,28 @@ class ApiService {
return null; 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( Future<Result> sendTextMessage(
int target, int target,
Uint8List msg, Uint8List msg,

View file

@ -117,6 +117,10 @@ Future<void> handleGroupUpdate(
final group = (await twonlyDB.groupsDao.getGroup(groupId))!; final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
if (!group.isDirectChat) {
unawaited(fetchGroupState(group));
}
switch (actionType) { switch (actionType) {
case GroupActionType.updatedGroupName: case GroupActionType.updatedGroupName:
await twonlyDB.groupsDao.insertGroupAction( await twonlyDB.groupsDao.insertGroupAction(
@ -173,10 +177,6 @@ Future<void> handleGroupUpdate(
case GroupActionType.createdGroup: case GroupActionType.createdGroup:
break; break;
} }
if (!group.isDirectChat) {
unawaited(fetchGroupState(group));
}
} }
Future<bool> handleGroupJoin( Future<bool> handleGroupJoin(

View file

@ -128,7 +128,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
); );
final mediaService = MediaFileService(media); final mediaService = MediaFileService(media);
await mediaService.setUploadState(UploadState.uploaded); await mediaService.setUploadState(UploadState.uploading);
// In all other cases just try the upload again... // In all other cases just try the upload again...
await startBackgroundMediaUpload(mediaService); await startBackgroundMediaUpload(mediaService);
} }

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.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/client_to_server.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; 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/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/client2client/contact.c2c.dart'; import 'package:twonly/src/services/api/client2client/contact.c2c.dart';
import 'package:twonly/src/services/api/client2client/groups.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()) { if (msg.v0.hasRequestNewPreKeys()) {
response = await handleRequestNewPreKey(); response = await handleRequestNewPreKey();
} else if (msg.v0.hasNewMessage()) { } else if (msg.v0.hasNewMessage()) {
final body = Uint8List.fromList(msg.v0.newMessage.body); await handleClient2ClientMessage(msg.v0.newMessage);
final fromUserId = msg.v0.newMessage.fromUserId.toInt(); } else if (msg.v0.hasNewMessages()) {
await handleClient2ClientMessage(fromUserId, body); for (final newMessage in msg.v0.newMessages.newMessages) {
await handleClient2ClientMessage(newMessage);
}
} else { } else {
Log.error('Unknown server message: $msg'); Log.error('Unknown server message: $msg');
} }
@ -56,7 +60,10 @@ DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
Mutex protectReceiptCheck = Mutex(); 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 message = Message.fromBuffer(body);
final receiptId = message.receiptId; final receiptId = message.receiptId;
@ -112,13 +119,17 @@ Future<void> handleClient2ClientMessage(int fromUserId, Uint8List body) async {
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull() == .getSingleOrNull() ==
null) { 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 /// 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 /// can be inserted into the receipts database
await twonlyDB.contactsDao.insertContact( await twonlyDB.contactsDao.insertContact(
ContactsCompanion( ContactsCompanion(
userId: Value(fromUserId), userId: Value(fromUserId),
deletedByUser: const Value(true), deletedByUser: const Value(true),
username: const Value('[deleted]'), username: Value(
user == null ? '[Unknown]' : utf8.decode(user.username),
),
), ),
); );
} }

View file

@ -1,5 +1,6 @@
// ignore_for_file: unreachable_from_main // ignore_for_file: unreachable_from_main
import 'dart:async';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:firebase_core/firebase_core.dart'; 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/constants/secure_storage_keys.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
import '../../firebase_options.dart'; import '../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future<void> initFCMAfterAuthenticated() async { Future<void> checkForTokenUpdates() async {
if (globalIsAppInBackground) return;
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm); final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
try { try {
if (Platform.isIOS) { 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) { if (apnsToken == null) {
Log.error('Error getting apnsToken'); Log.error('Could not get APNS token even after 20s...');
return; return;
} }
} }
final fcmToken = await FirebaseMessaging.instance.getToken(); final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) { if (fcmToken == null) {
Log.error('Error getting fcmToken'); Log.error('Could not get fcm token');
return; return;
} }
Log.info('Loaded fcm token');
if (storedToken == null || fcmToken != storedToken) { 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); await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
} }
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) async { 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); await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
}).onError((err) { }).onError((err) {
Log.error('could not listen on token refresh'); 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 { Future<void> initFCMService() async {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
unawaited(checkForTokenUpdates());
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// You may set the permission requests to "provisional" which allows the user to choose what type // 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(); await FirebaseMessaging.instance.requestPermission();
// For apple platforms, ensure the APNS token is available before making any FCM plugin API calls // For apple platforms, ensure the APNS token is available before making any FCM plugin API calls
if (Platform.isIOS) { // if (Platform.isIOS) {
final apnsToken = await FirebaseMessaging.instance.getAPNSToken(); // final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
if (apnsToken == null) { // if (apnsToken == null) {
return; // return;
} // }
} // }
FirebaseMessaging.onMessage.listen(handleRemoteMessage); FirebaseMessaging.onMessage.listen(handleRemoteMessage);
} }

View file

@ -175,8 +175,7 @@ Future<void> showLocalPushNotification(
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
pushUser.userId.toInt() % pushUser.userId.toInt() %
// ignore: avoid_js_rounded_ints 2147483647, // Invalid argument (id): must fit within the size of a 32-bit integer
2373257871630019505, // Invalid argument (id): must fit within the size of a 32-bit integer
title, title,
body, body,
notificationDetails, notificationDetails,

View file

@ -303,6 +303,15 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
return pushNotification; return pushNotification;
} }
Future<void> requestNewPushKeysForUser(int toUserId) async {
await sendCipherText(
toUserId,
EncryptedContent()
..pushKeys = (EncryptedContent_PushKeys()
..type = EncryptedContent_PushKeys_Type.REQUEST),
);
}
/// this will trigger a push notification /// this will trigger a push notification
/// push notification only containing the message kind and username /// push notification only containing the message kind and username
Future<Uint8List?> encryptPushNotification( Future<Uint8List?> encryptPushNotification(
@ -326,15 +335,16 @@ Future<Uint8List?> encryptPushNotification(
// this will be enforced after every app uses this system... :/ // this will be enforced after every app uses this system... :/
// return null; // return null;
Log.warn('Using insecure key as the receiver does not send a push key!'); Log.warn('Using insecure key as the receiver does not send a push key!');
await requestNewPushKeysForUser(toUserId);
await sendCipherText(
toUserId,
EncryptedContent()
..pushKeys = (EncryptedContent_PushKeys()
..type = EncryptedContent_PushKeys_Type.REQUEST),
);
} }
} else { } else {
final createdAt = DateTime.fromMillisecondsSinceEpoch(
pushUser.pushKeys.last.createdAtUnixTimestamp.toInt(),
);
final timeBefore = DateTime.now().subtract(const Duration(days: 8));
if (createdAt.isBefore(timeBefore)) {
await requestNewPushKeysForUser(toUserId);
}
try { try {
key = pushUser.pushKeys.last.key; key = pushUser.pushKeys.last.key;
keyId = pushUser.pushKeys.last.id.toInt(); keyId = pushUser.pushKeys.last.id.toInt();

View file

@ -90,6 +90,9 @@ Future<String> readLast1000Lines() async {
Future<void> _writeLogToFile(LogRecord record) async { Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');
if (!logFile.existsSync()) {
logFile.createSync(recursive: true);
}
// Prepare the log message // Prepare the log message
final logMessage = final logMessage =

View file

@ -361,3 +361,8 @@ String getAvatarSvg(Uint8List avatarSvgCompressed) {
final raw = gzip.decode(avatarSvgCompressed); final raw = gzip.decode(avatarSvgCompressed);
return utf8.decode(raw); return utf8.decode(raw);
} }
void printWrapped(String text) {
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
pattern.allMatches(text).forEach((match) => print(match.group(0)));
}

View file

@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/json/userdata.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/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -40,7 +40,7 @@ Future<void> updateUsersPlan(
BuildContext context, BuildContext context,
SubscriptionPlan plan, SubscriptionPlan plan,
) async { ) async {
context.read<CustomChangeProvider>().plan = plan; context.read<PurchasesProvider>().plan = plan;
await updateUserdata((user) { await updateUserdata((user) {
user.subscriptionPlan = plan.name; user.subscriptionPlan = plan.name;
@ -48,7 +48,7 @@ Future<void> updateUsersPlan(
}); });
if (!context.mounted) return; if (!context.mounted) return;
await context.read<CustomChangeProvider>().updatePlan(plan); context.read<PurchasesProvider>().updatePlan(plan);
} }
Mutex updateProtection = Mutex(); Mutex updateProtection = Mutex();

View file

@ -820,15 +820,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
...widget.mainCameraController.scannedNewProfiles.values ...widget.mainCameraController.scannedNewProfiles.values
.map( .map(
(c) { (c) {
if (c.isLoading) return Container();
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
if (c.isLoading) return;
c.isLoading = true; c.isLoading = true;
widget.mainCameraController.setState(); widget.mainCameraController.setState();
await addNewContactFromPublicProfile(c.profile); await addNewContactFromPublicProfile(c.profile);
widget.mainCameraController.scannedNewProfiles
.remove(c.profile.userId.toInt());
widget.mainCameraController.setState();
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),

View file

@ -52,8 +52,9 @@ class MainCameraController {
} catch (e) { } catch (e) {
Log.warn(e); Log.warn(e);
} }
await cameraController?.dispose(); final cameraControllerTemp = cameraController;
cameraController = null; cameraController = null;
await cameraControllerTemp?.dispose();
initCameraStarted = false; initCameraStarted = false;
selectedCameraDetails = SelectedCameraDetails(); selectedCameraDetails = SelectedCameraDetails();
} }
@ -217,11 +218,13 @@ class MainCameraController {
const ContactsCompanion(verified: Value(true)), const ContactsCompanion(verified: Value(true)),
); );
} }
await HapticFeedback.heavyImpact();
} }
} }
} else { } else {
if (profile.username != gUser.username) { if (profile.username != gUser.username) {
if (scannedNewProfiles[profile.userId.toInt()] == null) { if (scannedNewProfiles[profile.userId.toInt()] == null) {
await HapticFeedback.heavyImpact();
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile( scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
profile: profile, profile: profile,
); );

View file

@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.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/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -93,7 +94,7 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected; final isConnected = context.watch<CustomChangeProvider>().isConnected;
final plan = context.watch<CustomChangeProvider>().plan; final plan = context.watch<PurchasesProvider>().plan;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
@ -203,7 +204,7 @@ class _ChatListViewState extends State<ChatListView> {
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await apiService.close(() {}); await apiService.close(() {});
await apiService.connect(force: true); await apiService.connect();
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
}, },
child: (_groupsNotPinned.isEmpty && child: (_groupsNotPinned.isEmpty &&

View file

@ -159,7 +159,7 @@ class _UserListItem extends State<GroupListItem> {
} }
Future<void> onTap() async { Future<void> onTap() async {
if (_currentMessage == null) { if (_currentMessage == null && widget.group.totalMediaCounter == 0) {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(

View file

@ -113,6 +113,7 @@ class _MessageInputState extends State<MessageInput> {
} }
setState(() { setState(() {
_recordingState = RecordingState.recording; _recordingState = RecordingState.recording;
_currentDuration = 0;
}); });
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
final audioTmpPath = final audioTmpPath =
@ -220,82 +221,96 @@ class _MessageInputState extends State<MessageInput> {
), ),
), ),
Expanded( Expanded(
child: (_recordingState == RecordingState.recording) child: Stack(
? Row( children: [
children: [ TextField(
const Padding( controller: _textFieldController,
padding: EdgeInsets.only( focusNode: widget.textFieldFocus,
top: 14, keyboardType: TextInputType.multiline,
bottom: 14, showCursor:
left: 12, _recordingState != RecordingState.recording,
right: 8, maxLines: 4,
), minLines: 1,
child: FaIcon( onChanged: (value) async {
FontAwesomeIcons.microphone, setState(() {});
size: 20, await twonlyDB.groupsDao.updateGroup(
color: Colors.red, widget.group.groupId,
), GroupsCompanion(
draftMessage:
Value(_textFieldController.text),
), ),
const SizedBox(width: 10), );
Text( },
formatMsToMinSec( onSubmitted: (_) {
_currentDuration, _sendMessage();
), },
style: const TextStyle( style: const TextStyle(fontSize: 17),
color: Colors.white, decoration: InputDecoration(
fontSize: 12, hintText: context.lang.chatListDetailInput,
), contentPadding: EdgeInsets.zero,
), border: InputBorder.none,
if (!_audioRecordingLock) ...[ ),
SizedBox( ),
width: (100 - _cancelSlideOffset) % 101, if (_recordingState == RecordingState.recording)
), Container(
Text( decoration: BoxDecoration(
context.lang.voiceMessageSlideToCancel, color: context.color.surfaceContainer,
), borderRadius: BorderRadius.circular(20),
] else ...[ ),
Expanded( child: Row(
child: Container(), children: [
), const Padding(
GestureDetector( padding: EdgeInsets.only(
onTap: _cancelAudioRecording, top: 14,
child: Text( bottom: 14,
context.lang.voiceMessageCancel, left: 12,
style: const TextStyle( right: 8,
color: Colors.red, ),
), child: FaIcon(
FontAwesomeIcons.microphone,
size: 20,
color: Colors.red,
), ),
), ),
const SizedBox(width: 20), const SizedBox(width: 10),
], Text(
], formatMsToMinSec(
) _currentDuration,
: TextField( ),
controller: _textFieldController, style: TextStyle(
focusNode: widget.textFieldFocus, color: isDarkMode(context)
keyboardType: TextInputType.multiline, ? Colors.white
maxLines: 4, : Colors.black,
minLines: 1, fontSize: 12,
onChanged: (value) async { ),
setState(() {});
await twonlyDB.groupsDao.updateGroup(
widget.group.groupId,
GroupsCompanion(
draftMessage:
Value(_textFieldController.text),
), ),
); if (!_audioRecordingLock) ...[
}, SizedBox(
onSubmitted: (_) { width: (100 - _cancelSlideOffset) % 101,
_sendMessage(); ),
}, Text(
style: const TextStyle(fontSize: 17), context.lang.voiceMessageSlideToCancel,
decoration: InputDecoration( ),
hintText: context.lang.chatListDetailInput, ] else ...[
contentPadding: EdgeInsets.zero, Expanded(
border: InputBorder.none, child: Container(),
),
GestureDetector(
onTap: _cancelAudioRecording,
child: Text(
context.lang.voiceMessageCancel,
style: const TextStyle(
color: Colors.red,
),
),
),
const SizedBox(width: 20),
],
],
), ),
), ),
],
),
), ),
if (_textFieldController.text == '') if (_textFieldController.text == '')
GestureDetector( GestureDetector(
@ -355,7 +370,9 @@ class _MessageInputState extends State<MessageInput> {
height: 60, height: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90), borderRadius: BorderRadius.circular(90),
color: Colors.black, color: isDarkMode(context)
? Colors.black
: Colors.white,
), ),
child: const Center( child: const Center(
child: Column( child: Column(

View file

@ -5,6 +5,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/better_list_title.dart';
@ -71,6 +72,9 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
); );
return; return;
} }
Log.info(
'Restoring flames from ${_directChat!.flameCounter} to ${_directChat!.maxFlameCounter}',
);
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
_groupId, _groupId,
GroupsCompanion( GroupsCompanion(

View file

@ -37,6 +37,9 @@ class _MediaViewSizingState extends State<MediaViewSizing> {
final aspectRatioWidth = availableWidth; final aspectRatioWidth = availableWidth;
final aspectRatioHeight = (aspectRatioWidth * 16) / 9; final aspectRatioHeight = (aspectRatioWidth * 16) / 9;
if (aspectRatioHeight > availableHeight) {
needToDownSizeImage = true;
}
if (widget.requiredHeight != null) { if (widget.requiredHeight != null) {
if (aspectRatioHeight < availableHeight) { if (aspectRatioHeight < availableHeight) {
if ((screenSize.height - widget.requiredHeight!) < aspectRatioHeight) { if ((screenSize.height - widget.requiredHeight!) < aspectRatioHeight) {

View file

@ -53,19 +53,19 @@ class OnboardingView extends StatelessWidget {
), ),
), ),
), ),
PageViewModel( // PageViewModel(
title: context.lang.onboardingSendTwonliesTitle, // title: context.lang.onboardingSendTwonliesTitle,
body: context.lang.onboardingSendTwonliesBody, // body: context.lang.onboardingSendTwonliesBody,
image: Center( // image: Center(
child: Padding( // child: Padding(
padding: const EdgeInsets.only(top: 100), // padding: const EdgeInsets.only(top: 100),
child: Lottie.asset( // child: Lottie.asset(
'assets/animations/twonlies.json', // 'assets/animations/twonlies.json',
repeat: false, // repeat: false,
), // ),
), // ),
), // ),
), // ),
PageViewModel( PageViewModel(
title: context.lang.onboardingNotProductTitle, title: context.lang.onboardingNotProductTitle,
bodyWidget: Column( bodyWidget: Column(

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.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/account/refund_credits.view.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
class AccountView extends StatefulWidget { class AccountView extends StatefulWidget {
const AccountView({super.key}); const AccountView({super.key});
@ -31,7 +29,7 @@ class _AccountViewState extends State<AccountView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final ballance = await loadPlanBalance(useCache: false); final ballance = await apiService.loadPlanBalance(useCache: false);
if (ballance == null || !mounted) return; if (ballance == null || !mounted) return;
var ballanceInCents = ballance.transactions var ballanceInCents = ballance.transactions
.where( .where(

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// import 'package:font_awesome_flutter/font_awesome_flutter.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'; // import 'package:url_launcher/url_launcher.dart';
class RefundCreditsView extends StatefulWidget { class RefundCreditsView extends StatefulWidget {

View file

@ -5,6 +5,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/avatar_icon.component.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/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/account.view.dart';
import 'package:twonly/src/views/settings/appearance.view.dart'; import 'package:twonly/src/views/settings/appearance.view.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart';
@ -82,13 +83,22 @@ class _SettingsMainViewState extends State<SettingsMainView> {
), ),
), ),
), ),
// Align( Align(
// alignment: Alignment.centerRight, alignment: Alignment.centerRight,
// child: IconButton( child: IconButton(
// onPressed: () {}, onPressed: () {
// icon: FaIcon(FontAwesomeIcons.qrcode), Navigator.push(
// ), context,
// ) MaterialPageRoute(
builder: (context) {
return const PublicProfileView();
},
),
);
},
icon: const FaIcon(FontAwesomeIcons.qrcode),
),
)
], ],
), ),
), ),

View file

@ -11,7 +11,7 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.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 { Future<List<Response_AddAccountsInvite>?> loadAdditionalUserInvites() async {
final ballance = await apiService.getAdditionalUserInvites(); final ballance = await apiService.getAdditionalUserInvites();
@ -65,12 +65,9 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var plusInvites = <Response_AddAccountsInvite>[]; var plusInvites = <Response_AddAccountsInvite>[];
var freeInvites = <Response_AddAccountsInvite>[];
if (additionalInvites != null) { if (additionalInvites != null) {
plusInvites = plusInvites =
additionalInvites!.where((x) => x.planId == 'Plus').toList(); additionalInvites!.where((x) => x.planId == 'Plus').toList();
freeInvites =
additionalInvites!.where((x) => x.planId == 'Free').toList();
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -95,11 +92,10 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
), ),
), ),
if (plusInvites.isNotEmpty) if (plusInvites.isNotEmpty)
ListTile( Text(
title: Text( context.lang.additionalUsersPlusTokens,
context.lang.additionalUsersPlusTokens, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 16),
),
), ),
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -111,23 +107,6 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
children: plusInvites.map(AdditionalUserInvite.new).toList(), 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( final remove = await showAlertDialog(
context, context,
'Remove this additional user', '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) { if (remove) {
final res = await apiService final res = await apiService

View file

@ -1,111 +1,24 @@
// ignore_for_file: inference_failure_on_instance_creation // ignore_for_file: inference_failure_on_instance_creation
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.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/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.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/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.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/components/better_list_title.dart';
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart'; import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
import 'package:twonly/src/views/settings/subscription/checkout.view.dart'; import 'package:url_launcher/url_launcher.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;
}
class SubscriptionView extends StatefulWidget { class SubscriptionView extends StatefulWidget {
const SubscriptionView({super.key, this.redirectError}); const SubscriptionView({super.key});
final ErrorCode? redirectError;
@override @override
State<SubscriptionView> createState() => _SubscriptionViewState(); State<SubscriptionView> createState() => _SubscriptionViewState();
@ -124,7 +37,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
ballance = await loadPlanBalance(); ballance = await apiService.loadPlanBalance();
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) { if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
final ownerId = ballance!.additionalAccountOwnerId.toInt(); final ownerId = ballance!.additionalAccountOwnerId.toInt();
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
@ -137,61 +50,18 @@ class _SubscriptionViewState extends State<SubscriptionView> {
} }
} }
setState(() {}); setState(() {});
await apiService.forceIpaCheck();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final myLocale = Localizations.localeOf(context); final currentPlan = context.watch<PurchasesProvider>().plan;
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!);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsSubscription), title: Text(context.lang.settingsSubscription),
), ),
body: ListView( body: ListView(
children: [ 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(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: Center( child: Center(
@ -220,7 +90,12 @@ class _SubscriptionViewState extends State<SubscriptionView> {
style: const TextStyle(color: Colors.orange), style: const TextStyle(color: Colors.orange),
), ),
), ),
if (!isPayingUser(currentPlan)) if (isPayingUser(currentPlan))
PlanCard(
plan: currentPlan,
),
if (!isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester) ...[
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
@ -231,48 +106,16 @@ class _SubscriptionViewState extends State<SubscriptionView> {
), ),
), ),
), ),
if (!isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester)
PlanCard( PlanCard(
plan: SubscriptionPlan.Pro, plan: SubscriptionPlan.Pro,
onTap: () async { onPurchase: initAsync,
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CheckoutView(
plan: SubscriptionPlan.Pro,
);
},
),
);
await initAsync();
},
), ),
if (currentPlan != SubscriptionPlan.Family)
PlanCard( PlanCard(
plan: SubscriptionPlan.Family, plan: SubscriptionPlan.Family,
refund: refund, onPurchase: initAsync,
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();
},
), ),
if (!isPayingUser(currentPlan)) ...[ ],
if (currentPlan == SubscriptionPlan.Free) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: Padding( child: Padding(
@ -287,58 +130,11 @@ class _SubscriptionViewState extends State<SubscriptionView> {
const SizedBox(height: 10), const SizedBox(height: 10),
PlanCard( PlanCard(
plan: SubscriptionPlan.Plus, plan: SubscriptionPlan.Plus,
onTap: () async { onPurchase: initAsync,
await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
await initAsync();
},
), ),
], ],
const SizedBox(height: 10), const SizedBox(height: 10),
if (currentPlan != SubscriptionPlan.Family) const Divider(), 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) || if (isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester) currentPlan == SubscriptionPlan.Tester)
BetterListTile( BetterListTile(
@ -359,64 +155,72 @@ class _SubscriptionViewState extends State<SubscriptionView> {
await initAsync(); 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}) { class PlanCard extends StatefulWidget {
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({ const PlanCard({
required this.plan, required this.plan,
super.key, super.key,
this.refund, this.onPurchase,
this.onTap,
this.paidMonthly, this.paidMonthly,
}); });
final SubscriptionPlan plan; final SubscriptionPlan plan;
final void Function()? onTap; final void Function()? onPurchase;
final int? refund;
final bool? paidMonthly; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final yearlyPrice = getPlanPrice(plan, paidMonthly: false); final products = context.watch<PurchasesProvider>().products;
final monthlyPrice = getPlanPrice(plan, paidMonthly: true); 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>[]; var features = <String>[];
switch (plan.name) { switch (widget.plan.name) {
case 'Free': case 'Free':
features = [context.lang.freeFeature1]; features = [context.lang.freeFeature1];
case 'Plus': case 'Plus':
features = [context.lang.plusFeature1, context.lang.plusFeature2]; features = [context.lang.plusFeature1]; //, context.lang.plusFeature2];
case 'Tester': case 'Tester':
case 'Pro': case 'Pro':
features = [ features = [
@ -424,117 +228,135 @@ class PlanCard extends StatelessWidget {
context.lang.proFeature2, context.lang.proFeature2,
context.lang.proFeature3, context.lang.proFeature3,
context.lang.proFeature4, context.lang.proFeature4,
// context.lang.proFeature4,
]; ];
case 'Family': case 'Family':
features = [ features = [
context.lang.proFeature1, context.lang.familyFeature1,
context.lang.familyFeature2, context.lang.familyFeature2,
context.lang.proFeature3, context.lang.familyFeature3,
context.lang.proFeature4, context.lang.familyFeature4,
]; ];
default: default:
} }
return Padding( return Padding(
padding: const EdgeInsets.only(left: 16, right: 16), padding: const EdgeInsets.only(left: 16, right: 16),
child: GestureDetector( child: Card(
onTap: onTap, elevation: 4,
child: Card( shape: RoundedRectangleBorder(
elevation: 4, borderRadius: BorderRadius.circular(10),
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(10), color: context.color.surfaceContainer,
), child: Padding(
color: context.color.surfaceContainer, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Padding( child: Column(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), children: [
child: Column( Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Text(
children: [ 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( Text(
plan.name, (widget.paidMonthly!)
? '${getFormattedPrice(monthlyProduct!)}/${context.lang.month}'
: '${getFormattedPrice(yearlyProduct!)}/${context.lang.year}',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
if (yearlyPrice != 0) const SizedBox(height: 10), ],
if (yearlyPrice != 0 && paidMonthly == null) ),
Column( const SizedBox(height: 10),
children: [ ...features.map(
if (paidMonthly == null || paidMonthly!) (feature) => Padding(
Text( padding: const EdgeInsets.symmetric(vertical: 2),
'${localePrizing(context, yearlyPrice)}/${context.lang.year}', child: Text(
textAlign: TextAlign.center, feature,
style: const TextStyle( textAlign: TextAlign.center,
fontSize: 20, ),
fontWeight: FontWeight.bold, ),
), ),
), const SizedBox(height: 10),
if (paidMonthly == null || !paidMonthly!) if (currentPlan == widget.plan)
Text( FilledButton.icon(
'${localePrizing(context, monthlyPrice)}/${context.lang.month}', onPressed: () async {
textAlign: TextAlign.center, var url = 'https://apps.apple.com/account/subscriptions';
style: const TextStyle( if (Platform.isAndroid) {
fontSize: 16, url =
color: Colors.grey, 'https://play.google.com/store/account/subscriptions?sku=${gUser.subscriptionPlanIdStore}&package=eu.twonly';
), }
), await launchUrl(
], Uri.parse(url),
), mode: LaunchMode.externalApplication,
if (paidMonthly != null) );
Text( },
(paidMonthly!) label: const Text('Manage subscription'),
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}' ),
: '${localePrizing(context, yearlyPrice)}/${context.lang.year}', if (widget.onPurchase != null && monthlyProduct != null)
textAlign: TextAlign.center, OutlinedButton.icon(
style: const TextStyle( onPressed: () => onButtonPressed(monthlyProduct),
fontSize: 20, label: (widget.plan == SubscriptionPlan.Free ||
fontWeight: FontWeight.bold, widget.plan == SubscriptionPlan.Plus)
? Text(context.lang.redeemUserInviteCodeTitle)
: Text(
context.lang.upgradeToPaidPlanButton(
widget.plan.name,
' (${context.lang.monthly})',
),
), ),
),
],
), ),
const SizedBox(height: 10), if (widget.onPurchase != null &&
...features.map( (yearlyProduct != null ||
(feature) => Padding( currentPlan == SubscriptionPlan.Free))
padding: const EdgeInsets.symmetric(vertical: 2), FilledButton.icon(
child: Text( onPressed: () => onButtonPressed(yearlyProduct),
feature, label: (widget.plan == SubscriptionPlan.Free ||
textAlign: TextAlign.center, 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. // reconnect to load new plan.
await apiService.close(() {}); await apiService.close(() {});
await apiService.connect(force: true); await apiService.connect();
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

View file

@ -1,19 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.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_custom/select_payment.view.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
class CheckoutView extends StatefulWidget { class CheckoutView extends StatefulWidget {
const CheckoutView({ const CheckoutView({
required this.plan, required this.plan,
super.key, super.key,
this.refund,
this.disableMonthlyOption, this.disableMonthlyOption,
}); });
final SubscriptionPlan plan; final SubscriptionPlan plan;
final int? refund;
final bool? disableMonthlyOption; final bool? disableMonthlyOption;
@override @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(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Card( child: Card(
@ -133,7 +108,6 @@ class _CheckoutViewState extends State<CheckoutView> {
return SelectPaymentView( return SelectPaymentView(
plan: widget.plan, plan: widget.plan,
payMonthly: paidMonthly, payMonthly: paidMonthly,
refund: widget.refund,
); );
}, },
), ),

View file

@ -1,15 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.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/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.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/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.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 { class ManageSubscriptionView extends StatefulWidget {
const ManageSubscriptionView({ const ManageSubscriptionView({
@ -65,7 +64,7 @@ class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final plan = context.read<CustomChangeProvider>().plan; final plan = context.watch<PurchasesProvider>().plan;
final myLocale = Localizations.localeOf(context); final myLocale = Localizations.localeOf(context);
final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS; final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS;
return Scaffold( return Scaffold(

View file

@ -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/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.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_custom/subscription.view.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'; import 'package:url_launcher/url_launcher.dart';
class SelectPaymentView extends StatefulWidget { class SelectPaymentView extends StatefulWidget {
@ -17,13 +17,11 @@ class SelectPaymentView extends StatefulWidget {
this.plan, this.plan,
this.payMonthly, this.payMonthly,
this.valueInCents, this.valueInCents,
this.refund,
}); });
final SubscriptionPlan? plan; final SubscriptionPlan? plan;
final bool? payMonthly; final bool? payMonthly;
final int? valueInCents; final int? valueInCents;
final int? refund;
@override @override
State<SelectPaymentView> createState() => _SelectPaymentViewState(); 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(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Card( child: Card(

View file

@ -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),
),
],
);
},
);
}

View file

@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a" sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.64" version: "1.3.65"
adaptive_number: adaptive_number:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
@ -516,10 +516,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c" sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.1" version: "4.3.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -532,34 +532,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_web name: firebase_core_web
sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398 sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" version: "3.3.1"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: "22086f857d2340f5d973776cfd542d3fb30cf98e1c643c3aa4a7520bb12745bb" sha256: "1ad663fbb6758acec09d7e84a2e6478265f0a517f40ef77c573efd5e0089f400"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "16.0.4" version: "16.1.0"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: a59920cbf2eb7c83d34a5f354331210ffec116b216dc72d864d8b8eb983ca398 sha256: ea620e841fbcec62a96984295fc628f53ef5a8da4f53238159719ed0af7db834
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.4" version: "4.7.5"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: "1183e40e6fd2a279a628951cc3b639fcf5ffe7589902632db645011eb70ebefb" sha256: "7d0fb6256202515bba8489a3d69c6bc9d52d69a4999bad789053b486c8e7323e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.1"
fixnum: fixnum:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1005,6 +1005,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.2" 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: intl:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.0.74+74 version: 0.0.78+78
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0
@ -47,8 +47,8 @@ dependencies:
# Trustworthy publishers # Trustworthy publishers
firebase_core: ^4.2.0 # firebase.google.com firebase_core: ^4.3.0 # firebase.google.com
firebase_messaging: ^16.0.3 # firebase.google.com firebase_messaging: ^16.1.0 # firebase.google.com
json_annotation: ^4.9.0 # google.dev json_annotation: ^4.9.0 # google.dev
protobuf: ^4.0.0 # google.dev protobuf: ^4.0.0 # google.dev
scrollable_positioned_list: ^0.3.8 # google.dev scrollable_positioned_list: ^0.3.8 # google.dev
@ -87,6 +87,8 @@ dependencies:
screenshot: ^3.0.0 screenshot: ^3.0.0
sentry_flutter: ^9.8.0 sentry_flutter: ^9.8.0
app_links: ^7.0.0 app_links: ^7.0.0
in_app_purchase: ^3.2.3
# Overwritten by self-controlled repository # Overwritten by self-controlled repository
emoji_picker_flutter: ^4.3.0 emoji_picker_flutter: ^4.3.0
@ -162,6 +164,7 @@ dev_dependencies:
sdk: flutter sdk: flutter
json_serializable: ^6.8.0 json_serializable: ^6.8.0
very_good_analysis: ^10.0.0 very_good_analysis: ^10.0.0
in_app_purchase_platform_interface: ^1.4.0
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true

View file

@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1; import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2; import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3; import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v2.DatabaseAtV2(db); return v2.DatabaseAtV2(db);
case 3: case 3:
return v3.DatabaseAtV3(db); return v3.DatabaseAtV3(db);
case 4:
return v4.DatabaseAtV4(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3]; static const versions = const [1, 2, 3, 4];
} }

File diff suppressed because it is too large Load diff