mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 18:28:40 +00:00
starting with in app purchases
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
189ed26dd7
commit
2db1775d1f
37 changed files with 1585 additions and 597 deletions
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 */,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
@ -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,19 @@
|
||||||
"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": "✓ Cloud-Backup verschlüsselt (coming-soon)",
|
||||||
"year": "year",
|
"year": "Jahr",
|
||||||
"month": "month",
|
"month": "Monat",
|
||||||
|
"yearly": "Jährlich",
|
||||||
|
"monthly": "Monatlich",
|
||||||
"familyFeature1": "✓ Alles von Pro",
|
"familyFeature1": "✓ Alles von Pro",
|
||||||
"familyFeature2": "4 zusätzliche Plus Benutzer",
|
"familyFeature2": "✓ 4 zusätzliche Plus Benutzer",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -232,22 +232,24 @@
|
||||||
"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": "✓ Cloud-Backup encrypted (coming-soon)",
|
||||||
"proFeature4": "Additional features (coming-soon)",
|
"proFeature4": "Additional features (coming-soon)",
|
||||||
"familyFeature1": "✓ All from Pro",
|
"familyFeature1": "✓ All from Pro",
|
||||||
"familyFeature2": "4 additional Plus users",
|
"familyFeature2": "✓ 4 additional Plus users",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -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,13 +1355,13 @@ 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)'**
|
/// **'✓ Cloud-Backup encrypted (coming-soon)'**
|
||||||
String get proFeature3;
|
String get proFeature3;
|
||||||
|
|
||||||
/// No description provided for @proFeature4.
|
/// No description provided for @proFeature4.
|
||||||
|
|
@ -1367,7 +1379,7 @@ abstract class AppLocalizations {
|
||||||
/// 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 users'**
|
||||||
String get familyFeature2;
|
String get familyFeature2;
|
||||||
|
|
||||||
/// No description provided for @redeemUserInviteCode.
|
/// No description provided for @redeemUserInviteCode.
|
||||||
|
|
@ -1391,7 +1403,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 +1415,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.
|
||||||
|
|
|
||||||
|
|
@ -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,34 @@ 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 => '✓ Cloud-Backup verschlüsselt (coming-soon)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get familyFeature1 => '✓ Alles von Pro';
|
String get familyFeature1 => '✓ Alles von Pro';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get familyFeature2 => '4 zusätzliche Plus Benutzer';
|
String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.';
|
String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.';
|
||||||
|
|
@ -729,13 +735,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';
|
||||||
|
|
|
||||||
|
|
@ -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,17 +691,23 @@ 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 => '✓ Cloud-Backup encrypted (coming-soon)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get proFeature4 => 'Additional features (coming-soon)';
|
String get proFeature4 => 'Additional features (coming-soon)';
|
||||||
|
|
@ -710,7 +716,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get familyFeature1 => '✓ All from Pro';
|
String get familyFeature1 => '✓ All from Pro';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get familyFeature2 => '4 additional Plus users';
|
String get familyFeature2 => '✓ 4 additional Plus users';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get redeemUserInviteCode => 'Or redeem a twonly-Code.';
|
String get redeemUserInviteCode => 'Or redeem a twonly-Code.';
|
||||||
|
|
@ -723,13 +729,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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -89,6 +91,9 @@ class UserData {
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
bool hideChangeLog = false;
|
bool hideChangeLog = false;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: true)
|
||||||
|
bool updateFCMToken = true;
|
||||||
|
|
||||||
// --- BACKUP ---
|
// --- BACKUP ---
|
||||||
|
|
||||||
DateTime? nextTimeToShowBackupNotice;
|
DateTime? nextTimeToShowBackupNotice;
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
170
lib/src/providers/purchases.provider.dart
Normal file
170
lib/src/providers/purchases.provider.dart
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import 'dart:async';
|
||||||
|
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/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';
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
|
||||||
|
class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
|
PurchasesProvider() {
|
||||||
|
final purchaseUpdated = iapConnection.purchaseStream;
|
||||||
|
_subscription = purchaseUpdated.listen(
|
||||||
|
_onPurchaseUpdate,
|
||||||
|
onDone: _updateStreamOnDone,
|
||||||
|
onError: _updateStreamOnError,
|
||||||
|
);
|
||||||
|
|
||||||
|
forceIpaCheck = Timer(const Duration(seconds: 10), () {
|
||||||
|
Log.warn('Force Ipa check was not stopped. Requesting forced check...');
|
||||||
|
apiService.forceIpaCheck();
|
||||||
|
});
|
||||||
|
loadPurchases();
|
||||||
|
}
|
||||||
|
|
||||||
|
late Timer forceIpaCheck;
|
||||||
|
|
||||||
|
SubscriptionPlan plan = SubscriptionPlan.Free;
|
||||||
|
StoreState storeState = StoreState.loading;
|
||||||
|
List<PurchasableProduct> products = [];
|
||||||
|
|
||||||
|
late StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||||
|
final InAppPurchase iapConnection = IAPConnection.instance;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
await iapConnection.restorePurchases();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> buy(PurchasableProduct product) async {
|
||||||
|
Log.info('User wants to buy ${product.id}');
|
||||||
|
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:
|
||||||
|
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 {
|
||||||
|
Log.info(purchaseDetails.productID);
|
||||||
|
Log.info(purchaseDetails.verificationData.serverVerificationData);
|
||||||
|
Log.info(purchaseDetails.verificationData.source);
|
||||||
|
final res = await apiService.ipaPurchase(
|
||||||
|
purchaseDetails.productID,
|
||||||
|
purchaseDetails.verificationData.source,
|
||||||
|
purchaseDetails.verificationData.serverVerificationData,
|
||||||
|
);
|
||||||
|
return res.isSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
|
||||||
|
var validPurchase = false;
|
||||||
|
if (purchaseDetails.status == PurchaseStatus.purchased) {
|
||||||
|
Log.info('purchased: ${purchaseDetails.productID}');
|
||||||
|
validPurchase = await _verifyPurchase(purchaseDetails);
|
||||||
|
if (validPurchase) {
|
||||||
|
var plan = SubscriptionPlan.Pro;
|
||||||
|
if (purchaseDetails.productID.contains('family')) {
|
||||||
|
plan = SubscriptionPlan.Family;
|
||||||
|
}
|
||||||
|
await updateUserdata((u) {
|
||||||
|
u
|
||||||
|
..subscriptionPlan = plan.name
|
||||||
|
..subscriptionPlanIdStore = purchaseDetails.productID;
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
updatePlan(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (purchaseDetails.status == PurchaseStatus.restored) {
|
||||||
|
// there is a
|
||||||
|
forceIpaCheck.cancel();
|
||||||
|
|
||||||
|
if (gUser.subscriptionPlan != SubscriptionPlan.Family.name ||
|
||||||
|
gUser.subscriptionPlan != SubscriptionPlan.Pro.name) {
|
||||||
|
// app was installed on some one other...
|
||||||
|
// subscription is handled on the server, so on a new device the subscription comes from the server again...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,6 +113,7 @@ 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 {
|
||||||
|
|
@ -121,7 +123,7 @@ class ApiService {
|
||||||
reconnectionTimer = null;
|
reconnectionTimer = null;
|
||||||
await connect(force: true);
|
await connect(force: true);
|
||||||
});
|
});
|
||||||
_reconnectionDelay += 5;
|
_reconnectionDelay += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close(Function callback) async {
|
Future<void> close(Function callback) async {
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -375,15 +388,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 +495,19 @@ class ApiService {
|
||||||
return sendRequestSync(req);
|
return sendRequestSync(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> checkForDeletedUsernames() async {
|
||||||
|
final users = await twonlyDB.contactsDao.getContactsByUsername('[deleted]');
|
||||||
|
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,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,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 +36,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 +59,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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
if (fcmToken == null) {
|
|
||||||
Log.error('Error getting fcmToken');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||||
|
if (fcmToken == null) {
|
||||||
|
Log.error('Could not get fcm token');
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -141,57 +54,13 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
|
|
||||||
@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 +89,11 @@ 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)) ...[
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
|
|
@ -231,48 +104,14 @@ 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)) ...[
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -287,58 +126,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,86 +151,85 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = [
|
||||||
context.lang.proFeature1,
|
context.lang.proFeature1,
|
||||||
context.lang.proFeature2,
|
context.lang.proFeature2,
|
||||||
context.lang.proFeature3,
|
context.lang.proFeature3,
|
||||||
context.lang.proFeature4,
|
// context.lang.proFeature4,
|
||||||
];
|
];
|
||||||
case 'Family':
|
case 'Family':
|
||||||
features = [
|
features = [
|
||||||
context.lang.proFeature1,
|
context.lang.proFeature1,
|
||||||
context.lang.familyFeature2,
|
context.lang.familyFeature2,
|
||||||
context.lang.proFeature3,
|
context.lang.proFeature3,
|
||||||
context.lang.proFeature4,
|
// context.lang.proFeature4,
|
||||||
];
|
];
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
child: GestureDetector(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|
@ -453,29 +244,31 @@ class PlanCard extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
plan.name,
|
widget.plan.name,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (yearlyPrice != 0) const SizedBox(height: 10),
|
if (yearlyProduct != null && currentPlan != widget.plan)
|
||||||
if (yearlyPrice != 0 && paidMonthly == null)
|
const SizedBox(height: 10),
|
||||||
|
if (yearlyProduct != null &&
|
||||||
|
widget.paidMonthly == null &&
|
||||||
|
currentPlan != widget.plan)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
if (paidMonthly == null || paidMonthly!)
|
|
||||||
Text(
|
Text(
|
||||||
'${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
'${yearlyProduct.price}/${context.lang.year}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (paidMonthly == null || !paidMonthly!)
|
if (monthlyProduct != null)
|
||||||
Text(
|
Text(
|
||||||
'${localePrizing(context, monthlyPrice)}/${context.lang.month}',
|
'${monthlyProduct.price}/${context.lang.month}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
@ -484,11 +277,11 @@ class PlanCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (paidMonthly != null)
|
if (widget.paidMonthly != null)
|
||||||
Text(
|
Text(
|
||||||
(paidMonthly!)
|
(widget.paidMonthly!)
|
||||||
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'
|
? '${monthlyProduct?.price}/${context.lang.month}'
|
||||||
: '${localePrizing(context, yearlyPrice)}/${context.lang.year}',
|
: '${yearlyProduct?.price}/${context.lang.year}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|
@ -507,34 +300,57 @@ class PlanCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (refund != null && refund! > 0)
|
const SizedBox(height: 10),
|
||||||
|
if (currentPlan == widget.plan)
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
var url = 'https://apps.apple.com/account/subscriptions';
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
url =
|
||||||
|
'https://play.google.com/store/account/subscriptions?sku=${gUser.subscriptionPlanIdStore}&package=eu.twonly';
|
||||||
|
}
|
||||||
|
await launchUrl(
|
||||||
|
Uri.parse(url),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label: const Text('Manage subscription'),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (widget.onPurchase != null && monthlyProduct != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 7),
|
padding: const EdgeInsets.only(right: 10),
|
||||||
child: Text(
|
child: OutlinedButton.icon(
|
||||||
context.lang
|
onPressed: () => onButtonPressed(monthlyProduct),
|
||||||
.subscriptionRefund(localePrizing(context, refund!)),
|
label: (widget.plan == SubscriptionPlan.Free ||
|
||||||
textAlign: TextAlign.center,
|
widget.plan == SubscriptionPlan.Plus)
|
||||||
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.redeemUserInviteCodeTitle)
|
||||||
: Text(
|
: Text(
|
||||||
context.lang.upgradeToPaidPlanButton(plan.name),
|
context.lang.upgradeToPaidPlanButton(
|
||||||
|
widget.plan.name,
|
||||||
|
' (${context.lang.monthly})',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onPurchase != null && yearlyProduct != null)
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => onButtonPressed(yearlyProduct),
|
||||||
|
label: (widget.plan == SubscriptionPlan.Free ||
|
||||||
|
widget.plan == SubscriptionPlan.Plus)
|
||||||
|
? Text(context.lang.redeemUserInviteCodeTitle)
|
||||||
|
: Text(
|
||||||
|
context.lang.upgradeToPaidPlanButton(
|
||||||
|
widget.plan.name,
|
||||||
|
' (${context.lang.yearly})',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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(force: true);
|
||||||
|
} 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
24
pubspec.lock
24
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:
|
||||||
|
|
|
||||||
|
|
@ -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.75+75
|
version: 0.0.76+76
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue