mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 14:28:40 +00:00
Merge pull request #345 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Fixes multiple user-ability issues - Implemented payment system with Google Play and App Store - Fixing multiple bugs
This commit is contained in:
commit
2713f092eb
62 changed files with 8759 additions and 786 deletions
|
|
@ -66,6 +66,7 @@
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.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
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
115
ios/Podfile.lock
115
ios/Podfile.lock
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 */,
|
||||||
|
|
|
||||||
11
lib/app.dart
11
lib/app.dart
|
|
@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/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;
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
6
lib/src/constants/subscription.keys.dart
Normal file
6
lib/src/constants/subscription.keys.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
class SubscriptionKeys {
|
||||||
|
static const String proMonthly = 'pro_monthly';
|
||||||
|
static const String proYearly = 'pro_yearly';
|
||||||
|
// static const String familyMonthly = 'family_monthly';
|
||||||
|
static const String familyYearly = 'family_yearly';
|
||||||
|
}
|
||||||
|
|
@ -42,8 +42,15 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
.getSingleOrNull();
|
.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
1
lib/src/database/schemas/twonly_db/drift_schema_v4.json
Normal file
1
lib/src/database/schemas/twonly_db/drift_schema_v4.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -90,8 +90,7 @@ class GroupHistories extends Table {
|
||||||
IntColumn get contactId =>
|
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()();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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 ---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ class ErrorCode extends $pb.ProtobufEnum {
|
||||||
ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork');
|
ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork');
|
||||||
static const ErrorCode RegistrationDisabled =
|
static const ErrorCode RegistrationDisabled =
|
||||||
ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled');
|
ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled');
|
||||||
|
static const ErrorCode IPAPaymentExpired =
|
||||||
|
ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired');
|
||||||
|
|
||||||
static const $core.List<ErrorCode> values = <ErrorCode>[
|
static const $core.List<ErrorCode> values = <ErrorCode>[
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|
@ -125,6 +127,7 @@ class ErrorCode extends $pb.ProtobufEnum {
|
||||||
NewDeviceRegistered,
|
NewDeviceRegistered,
|
||||||
InvalidProofOfWork,
|
InvalidProofOfWork,
|
||||||
RegistrationDisabled,
|
RegistrationDisabled,
|
||||||
|
IPAPaymentExpired,
|
||||||
];
|
];
|
||||||
|
|
||||||
static final $core.Map<$core.int, ErrorCode> _byValue =
|
static final $core.Map<$core.int, ErrorCode> _byValue =
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ const ErrorCode$json = {
|
||||||
{'1': 'NewDeviceRegistered', '2': 1031},
|
{'1': 'NewDeviceRegistered', '2': 1031},
|
||||||
{'1': 'InvalidProofOfWork', '2': 1032},
|
{'1': 'InvalidProofOfWork', '2': 1032},
|
||||||
{'1': 'RegistrationDisabled', '2': 1033},
|
{'1': 'RegistrationDisabled', '2': 1033},
|
||||||
|
{'1': 'IPAPaymentExpired', '2': 1034},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,4 +75,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
|
||||||
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
|
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
|
||||||
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
|
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
|
||||||
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
|
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
|
||||||
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCA==');
|
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
|
||||||
|
'UEFQYXltZW50RXhwaXJlZBCKCA==');
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
13
lib/src/model/purchases/purchasable_product.dart
Normal file
13
lib/src/model/purchases/purchasable_product.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
|
|
||||||
|
enum ProductStatus { purchasable, purchased, pending }
|
||||||
|
|
||||||
|
class PurchasableProduct {
|
||||||
|
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
|
||||||
|
String get id => productDetails.id;
|
||||||
|
String get title => productDetails.title;
|
||||||
|
String get description => productDetails.description;
|
||||||
|
String get price => productDetails.price;
|
||||||
|
ProductStatus status;
|
||||||
|
ProductDetails productDetails;
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package: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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
212
lib/src/providers/purchases.provider.dart
Normal file
212
lib/src/providers/purchases.provider.dart
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/constants/subscription.keys.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
import 'package:twonly/src/model/purchases/purchasable_product.dart';
|
||||||
|
import 'package:twonly/src/services/subscription.service.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
// Gives the option to override in tests.
|
||||||
|
class IAPConnection {
|
||||||
|
static InAppPurchase? _instance;
|
||||||
|
static set instance(InAppPurchase value) {
|
||||||
|
_instance = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static InAppPurchase get instance {
|
||||||
|
_instance ??= InAppPurchase.instance;
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StoreState { loading, available, notAvailable }
|
||||||
|
|
||||||
|
Timer? globalForceIpaCheck;
|
||||||
|
|
||||||
|
class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
|
PurchasesProvider() {
|
||||||
|
final purchaseUpdated = iapConnection.purchaseStream;
|
||||||
|
_subscription = purchaseUpdated.listen(
|
||||||
|
_onPurchaseUpdate,
|
||||||
|
onDone: _updateStreamOnDone,
|
||||||
|
onError: _updateStreamOnError,
|
||||||
|
);
|
||||||
|
|
||||||
|
loadPurchases();
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionPlan plan = SubscriptionPlan.Free;
|
||||||
|
StoreState storeState = StoreState.loading;
|
||||||
|
List<PurchasableProduct> products = [];
|
||||||
|
|
||||||
|
late StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||||
|
final InAppPurchase iapConnection = IAPConnection.instance;
|
||||||
|
|
||||||
|
bool _userTriggeredBuyButton = false;
|
||||||
|
|
||||||
|
void updatePlan(SubscriptionPlan newPlan) {
|
||||||
|
plan = newPlan;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadPurchases() async {
|
||||||
|
final available = await iapConnection.isAvailable();
|
||||||
|
if (!available) {
|
||||||
|
storeState = StoreState.notAvailable;
|
||||||
|
Log.error('Store is not available');
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ids = <String>{
|
||||||
|
SubscriptionKeys.proMonthly,
|
||||||
|
SubscriptionKeys.proYearly,
|
||||||
|
SubscriptionKeys.familyYearly,
|
||||||
|
};
|
||||||
|
final response = await iapConnection.queryProductDetails(ids);
|
||||||
|
if (response.notFoundIDs.isNotEmpty) {
|
||||||
|
Log.error(response.notFoundIDs);
|
||||||
|
}
|
||||||
|
products = response.productDetails.map(PurchasableProduct.new).toList();
|
||||||
|
if (products.isEmpty) {
|
||||||
|
Log.error('Could not load any products from the store!');
|
||||||
|
}
|
||||||
|
storeState = StoreState.available;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final user = await getUser();
|
||||||
|
if (user != null && isPayingUser(planFromString(user.subscriptionPlan))) {
|
||||||
|
Log.info('Started IPA timer for verification.');
|
||||||
|
globalForceIpaCheck = Timer(const Duration(seconds: 5), () async {
|
||||||
|
Log.warn('Force Ipa check was not stopped. Requesting forced check...');
|
||||||
|
await apiService.forceIpaCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await iapConnection.restorePurchases();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> buy(PurchasableProduct product) async {
|
||||||
|
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
|
||||||
|
switch (product.id) {
|
||||||
|
// case storeKeyConsumable:
|
||||||
|
// await iapConnection.buyConsumable(purchaseParam: purchaseParam);
|
||||||
|
case SubscriptionKeys.proMonthly:
|
||||||
|
case SubscriptionKeys.proYearly:
|
||||||
|
case SubscriptionKeys.familyYearly:
|
||||||
|
_userTriggeredBuyButton = true;
|
||||||
|
Log.info('User wants to buy ${product.id}');
|
||||||
|
|
||||||
|
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
|
||||||
|
default:
|
||||||
|
throw ArgumentError.value(
|
||||||
|
product.productDetails,
|
||||||
|
'${product.id} is not a known product',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onPurchaseUpdate(
|
||||||
|
List<PurchaseDetails> purchaseDetailsList,
|
||||||
|
) async {
|
||||||
|
for (final purchaseDetails in purchaseDetailsList) {
|
||||||
|
await _handlePurchase(purchaseDetails);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
|
||||||
|
if (kDebugMode) {
|
||||||
|
Log.info(purchaseDetails.productID);
|
||||||
|
Log.info(purchaseDetails.verificationData.serverVerificationData);
|
||||||
|
// if (Platform.isIOS) {
|
||||||
|
// final data = purchaseDetails.verificationData.serverVerificationData;
|
||||||
|
// printWrapped(data);
|
||||||
|
// final datas = data.split('.')[1];
|
||||||
|
// printWrapped(datas);
|
||||||
|
// }
|
||||||
|
Log.info(purchaseDetails.verificationData.source);
|
||||||
|
}
|
||||||
|
final res = await apiService.ipaPurchase(
|
||||||
|
purchaseDetails.productID,
|
||||||
|
purchaseDetails.verificationData.source,
|
||||||
|
purchaseDetails.verificationData.serverVerificationData,
|
||||||
|
);
|
||||||
|
// plan is updated in the apiProvider, as the server updates its states and responses with
|
||||||
|
// an ok authenticated which is processed in the apiProvider...
|
||||||
|
if (res.isSuccess) {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await updateUserdata((u) {
|
||||||
|
u.subscriptionPlanIdStore = purchaseDetails.productID;
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (res.isError) {
|
||||||
|
if (res.error == ErrorCode.IPAPaymentExpired &&
|
||||||
|
_userTriggeredBuyButton &&
|
||||||
|
Platform.isIOS) {
|
||||||
|
await launchUrl(
|
||||||
|
Uri.parse('https://apps.apple.com/account/subscriptions'),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.isSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
|
||||||
|
Log.info(
|
||||||
|
'_handlePurchase: ${purchaseDetails.productID}, ${purchaseDetails.status}',
|
||||||
|
);
|
||||||
|
if (purchaseDetails.status == PurchaseStatus.purchased) {
|
||||||
|
await _verifyPurchase(purchaseDetails);
|
||||||
|
}
|
||||||
|
if (purchaseDetails.status == PurchaseStatus.restored &&
|
||||||
|
purchaseDetails.error == null) {
|
||||||
|
globalForceIpaCheck?.cancel();
|
||||||
|
|
||||||
|
final user = await getUser();
|
||||||
|
|
||||||
|
if (user != null &&
|
||||||
|
(user.subscriptionPlan != SubscriptionPlan.Family.name &&
|
||||||
|
user.subscriptionPlan != SubscriptionPlan.Pro.name)) {
|
||||||
|
for (var i = 0; i < 100; i++) {
|
||||||
|
if (apiService.isAuthenticated) {
|
||||||
|
Log.info(
|
||||||
|
'current user does not have a sub: ${purchaseDetails.productID}');
|
||||||
|
await _verifyPurchase(purchaseDetails);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purchaseDetails.status == PurchaseStatus.error) {
|
||||||
|
await iapConnection.restorePurchases();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purchaseDetails.pendingCompletePurchase) {
|
||||||
|
await iapConnection.completePurchase(purchaseDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateStreamOnDone() {
|
||||||
|
_subscription.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateStreamOnError(dynamic error) {
|
||||||
|
// Handle error here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -98,12 +98,13 @@ class ApiService {
|
||||||
unawaited(signalHandleNewServerConnection());
|
unawaited(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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -0,0 +1,570 @@
|
||||||
|
// ignore_for_file: inference_failure_on_instance_creation
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||||
|
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||||
|
import 'package:twonly/src/services/subscription.service.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||||
|
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
|
||||||
|
import 'package:twonly/src/views/settings/subscription_custom/checkout.view.dart';
|
||||||
|
import 'package:twonly/src/views/settings/subscription_custom/manage_subscription.view.dart';
|
||||||
|
import 'package:twonly/src/views/settings/subscription_custom/transaction.view.dart';
|
||||||
|
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
|
||||||
|
|
||||||
|
String localePrizing(BuildContext context, int cents) {
|
||||||
|
final myLocale = Localizations.localeOf(context);
|
||||||
|
final euros = cents / 100;
|
||||||
|
|
||||||
|
if (euros == euros.toInt()) {
|
||||||
|
return '${euros.toInt()}€';
|
||||||
|
}
|
||||||
|
|
||||||
|
return NumberFormat.currency(
|
||||||
|
locale: myLocale.toString(),
|
||||||
|
symbol: '€',
|
||||||
|
decimalDigits: 2,
|
||||||
|
).format(cents / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
|
||||||
|
final ballance = await apiService.getPlanBallance();
|
||||||
|
if (ballance != null) {
|
||||||
|
await updateUserdata((u) {
|
||||||
|
u.lastPlanBallance = ballance.writeToJson();
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
return ballance;
|
||||||
|
}
|
||||||
|
final user = await getUser();
|
||||||
|
if (user != null && user.lastPlanBallance != null && useCache) {
|
||||||
|
try {
|
||||||
|
return Response_PlanBallance.fromJson(
|
||||||
|
user.lastPlanBallance!,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('from json: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ballance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: constant_identifier_names
|
||||||
|
const int MONTHLY_PAYMENT_DAYS = 30;
|
||||||
|
// ignore: constant_identifier_names
|
||||||
|
const int YEARLY_PAYMENT_DAYS = 365;
|
||||||
|
|
||||||
|
class SubscriptionCustomView extends StatefulWidget {
|
||||||
|
const SubscriptionCustomView({super.key, this.redirectError});
|
||||||
|
|
||||||
|
final ErrorCode? redirectError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SubscriptionCustomView> createState() => _SubscriptionCustomViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SubscriptionCustomViewState extends State<SubscriptionCustomView> {
|
||||||
|
bool loaded = false;
|
||||||
|
bool testerRequested = true;
|
||||||
|
Response_PlanBallance? ballance;
|
||||||
|
String? additionalOwnerName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
unawaited(initAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initAsync() async {
|
||||||
|
ballance = await loadPlanBalance();
|
||||||
|
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
|
||||||
|
final ownerId = ballance!.additionalAccountOwnerId.toInt();
|
||||||
|
final contact = await twonlyDB.contactsDao
|
||||||
|
.getContactByUserId(ownerId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (contact != null) {
|
||||||
|
additionalOwnerName = getContactDisplayName(contact);
|
||||||
|
} else {
|
||||||
|
additionalOwnerName = ownerId.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final myLocale = Localizations.localeOf(context);
|
||||||
|
String? formattedBalance;
|
||||||
|
DateTime? nextPayment;
|
||||||
|
final currentPlan = context.watch<PurchasesProvider>().plan;
|
||||||
|
|
||||||
|
if (ballance != null) {
|
||||||
|
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000,
|
||||||
|
);
|
||||||
|
if (isPayingUser(currentPlan)) {
|
||||||
|
nextPayment = lastPaymentDateTime
|
||||||
|
.add(Duration(days: ballance!.paymentPeriodDays.toInt()));
|
||||||
|
}
|
||||||
|
final ballanceInCents =
|
||||||
|
ballance!.transactions.map((a) => a.depositCents.toInt()).sum;
|
||||||
|
formattedBalance = NumberFormat.currency(
|
||||||
|
locale: myLocale.toString(),
|
||||||
|
symbol: '€',
|
||||||
|
decimalDigits: 2,
|
||||||
|
).format(ballanceInCents / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.lang.settingsSubscription),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
if (widget.redirectError != null)
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orangeAccent,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
(widget.redirectError == ErrorCode.PlanLimitReached)
|
||||||
|
? context.lang.planLimitReached
|
||||||
|
: context.lang.planNotAllowed,
|
||||||
|
style: const TextStyle(color: Colors.black),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.primary,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
child: Text(
|
||||||
|
currentPlan.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (additionalOwnerName != null)
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
context.lang.partOfPaidPlanOf(additionalOwnerName!),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(color: Colors.orange),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isPayingUser(currentPlan))
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Text(
|
||||||
|
context.lang.upgradeToPaidPlan,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isPayingUser(currentPlan) ||
|
||||||
|
currentPlan == SubscriptionPlan.Tester)
|
||||||
|
PlanCard(
|
||||||
|
plan: SubscriptionPlan.Pro,
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return const CheckoutView(
|
||||||
|
plan: SubscriptionPlan.Pro,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await initAsync();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (currentPlan != SubscriptionPlan.Family)
|
||||||
|
PlanCard(
|
||||||
|
plan: SubscriptionPlan.Family,
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return CheckoutView(
|
||||||
|
plan: SubscriptionPlan.Family,
|
||||||
|
disableMonthlyOption:
|
||||||
|
currentPlan == SubscriptionPlan.Pro &&
|
||||||
|
ballance!.paymentPeriodDays.toInt() ==
|
||||||
|
YEARLY_PAYMENT_DAYS,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await initAsync();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!isPayingUser(currentPlan)) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Text(
|
||||||
|
context.lang.redeemUserInviteCode,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
PlanCard(
|
||||||
|
plan: SubscriptionPlan.Plus,
|
||||||
|
onTap: () async {
|
||||||
|
await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
|
||||||
|
await initAsync();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
if (currentPlan != SubscriptionPlan.Family) const Divider(),
|
||||||
|
BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.gears,
|
||||||
|
text: context.lang.manageSubscription,
|
||||||
|
subtitle: (nextPayment != null)
|
||||||
|
? Text(
|
||||||
|
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment)}',
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return ManageSubscriptionView(
|
||||||
|
ballance: ballance,
|
||||||
|
nextPayment: nextPayment,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await initAsync();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.moneyBillTransfer,
|
||||||
|
text: context.lang.transactionHistory,
|
||||||
|
subtitle: (formattedBalance != null)
|
||||||
|
? Text('${context.lang.currentBalance}: $formattedBalance')
|
||||||
|
: null,
|
||||||
|
onTap: () async {
|
||||||
|
if (formattedBalance == null) return;
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return TransactionView(
|
||||||
|
transactions: ballance?.transactions,
|
||||||
|
formattedBalance: formattedBalance!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isPayingUser(currentPlan) ||
|
||||||
|
currentPlan == SubscriptionPlan.Tester)
|
||||||
|
BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.userPlus,
|
||||||
|
text: context.lang.manageAdditionalUsers,
|
||||||
|
subtitle: loaded ? Text('${context.lang.open}: 3') : null,
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return AdditionalUsersView(
|
||||||
|
ballance: ballance,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await initAsync();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.ticket,
|
||||||
|
text: context.lang.createOrRedeemVoucher,
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return const VoucherView();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await initAsync();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) {
|
||||||
|
switch (plan) {
|
||||||
|
case SubscriptionPlan.Pro:
|
||||||
|
return paidMonthly ? 100 : 1000;
|
||||||
|
case SubscriptionPlan.Family:
|
||||||
|
return paidMonthly ? 200 : 2000;
|
||||||
|
// ignore: no_default_cases
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlanCard extends StatelessWidget {
|
||||||
|
const PlanCard({
|
||||||
|
required this.plan,
|
||||||
|
super.key,
|
||||||
|
this.refund,
|
||||||
|
this.onTap,
|
||||||
|
this.paidMonthly,
|
||||||
|
});
|
||||||
|
final SubscriptionPlan plan;
|
||||||
|
final void Function()? onTap;
|
||||||
|
final int? refund;
|
||||||
|
final bool? paidMonthly;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final yearlyPrice = getPlanPrice(plan, paidMonthly: false);
|
||||||
|
final monthlyPrice = getPlanPrice(plan, paidMonthly: true);
|
||||||
|
var features = <String>[];
|
||||||
|
|
||||||
|
switch (plan.name) {
|
||||||
|
case 'Free':
|
||||||
|
features = [context.lang.freeFeature1];
|
||||||
|
case 'Plus':
|
||||||
|
features = [context.lang.plusFeature1, context.lang.plusFeature2];
|
||||||
|
case 'Tester':
|
||||||
|
case 'Pro':
|
||||||
|
features = [
|
||||||
|
context.lang.proFeature1,
|
||||||
|
context.lang.proFeature2,
|
||||||
|
context.lang.proFeature3,
|
||||||
|
context.lang.proFeature4,
|
||||||
|
];
|
||||||
|
case 'Family':
|
||||||
|
features = [
|
||||||
|
context.lang.proFeature1,
|
||||||
|
context.lang.familyFeature2,
|
||||||
|
context.lang.proFeature3,
|
||||||
|
context.lang.proFeature4,
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
color: context.color.surfaceContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
plan.name,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (yearlyPrice != 0) const SizedBox(height: 10),
|
||||||
|
if (yearlyPrice != 0 && paidMonthly == null)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
if (paidMonthly == null || paidMonthly!)
|
||||||
|
Text(
|
||||||
|
'${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (paidMonthly == null || !paidMonthly!)
|
||||||
|
Text(
|
||||||
|
'${localePrizing(context, monthlyPrice)}/${context.lang.month}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (paidMonthly != null)
|
||||||
|
Text(
|
||||||
|
(paidMonthly!)
|
||||||
|
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'
|
||||||
|
: '${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
...features.map(
|
||||||
|
(feature) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Text(
|
||||||
|
feature,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (refund != null && refund! > 0)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 7),
|
||||||
|
child: Text(
|
||||||
|
context.lang
|
||||||
|
.subscriptionRefund(localePrizing(context, refund!)),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.primary,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onTap != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: onTap,
|
||||||
|
label: (plan == SubscriptionPlan.Free ||
|
||||||
|
plan == SubscriptionPlan.Plus)
|
||||||
|
? Text(context.lang.redeemUserInviteCodeTitle)
|
||||||
|
: Text(
|
||||||
|
context.lang
|
||||||
|
.upgradeToPaidPlanButton(plan.name, ''),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> redeemUserInviteCode(BuildContext context, String newPlan) async {
|
||||||
|
var inviteCode = '';
|
||||||
|
// ignore: inference_failure_on_function_invocation
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(context.lang.redeemUserInviteCodeTitle),
|
||||||
|
content: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (value) => setState(() {
|
||||||
|
inviteCode = value.toUpperCase();
|
||||||
|
}),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.lang.registerTwonlyCodeLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(context.lang.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final res = await apiService.redeemUserInviteCode(inviteCode);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (res.isSuccess) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.lang.redeemUserInviteCodeSuccess),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// reconnect to load new plan.
|
||||||
|
await apiService.close(() {});
|
||||||
|
await apiService.connect();
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
errorCodeToText(context, res.error as ErrorCode),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(context.lang.ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
56
pubspec.lock
56
pubspec.lock
|
|
@ -13,10 +13,10 @@ packages:
|
||||||
dependency: transitive
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6417
test/drift/twonly_db/generated/schema_v4.dart
Normal file
6417
test/drift/twonly_db/generated/schema_v4.dart
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue