diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c6c289..9709356 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: branches: [ main ] env: - FLUTTER_VERSION: '3.0.5' + FLUTTER_VERSION: '3.29.3' jobs: test: diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 7f0f1a5..7e82f6a 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,21 +22,18 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + ndkVersion = "27.0.12077973" + namespace = "com.example.example" compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -47,7 +45,7 @@ android { applicationId "com.example.example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 19 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -65,7 +63,3 @@ android { flutter { source '../..' } - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/example/android/build.gradle b/example/android/build.gradle index e394c2e..bc157bd 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.22' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index cb24abd..efdcc4a 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bc..7aa4b71 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" apply true + id "com.android.application" version "8.9.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.20" apply false +} + +include ":app" \ No newline at end of file diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift deleted file mode 100644 index 70693e4..0000000 --- a/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/example/pubspec.lock b/example/pubspec.lock index 82be9ee..600f5aa 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,74 +5,98 @@ packages: dependency: transitive description: name: app_links - sha256: eb83c2b15b78a66db04e95132678e910fcdb8dc3a9b0aed0c138f50b2bef0dae + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "3.4.5" + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" args: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" connectivity_plus: dependency: transitive description: name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.1.4" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "2.0.1" crowdin_sdk: dependency: "direct main" description: @@ -84,50 +108,50 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.8" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.11" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -137,10 +161,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -156,22 +180,30 @@ packages: description: flutter source: sdk version: "0.0.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -180,30 +212,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - js: - dependency: transitive - description: - name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.dev" - source: hosted - version: "0.6.5" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -216,18 +240,18 @@ packages: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.1" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -240,10 +264,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" nm: dependency: transitive description: @@ -256,122 +280,122 @@ packages: dependency: transitive description: name: oauth2 - sha256: "1e8376c222651904caf7785fd2fa01b1e2be608c94bec842a94e116deca88f13" + sha256: c84470642cbb2bec450ccab2f8520c079cd1ca546a76ffd5c40589e07f4e8bf4 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" - process: + version: "2.1.8" + shared_preferences: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "4.2.4" - shared_preferences: + version: "2.5.3" + shared_preferences_android: dependency: transitive description: - name: shared_preferences - sha256: "82c1ae2a70b5b0236bab13dcad98bc1c0c88ddfb4ef2b7b8080b55868404b8c3" + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.0.4" - shared_preferences_linux: + version: "2.4.10" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_linux - sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.1.3" - shared_preferences_macos: + version: "2.5.4" + shared_preferences_linux: dependency: transitive description: - name: shared_preferences_macos - sha256: "81b6a60b2d27020eb0fc41f4cebc91353047309967901a79ee8203e40c42ed46" + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -381,122 +405,122 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" url_launcher: dependency: transitive description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" vector_math: dependency: transitive description: @@ -509,50 +533,58 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" - web_socket_channel: + version: "14.3.1" + web: dependency: transitive description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "2.4.0" - win32: + version: "1.0.1" + web_socket_channel: dependency: transitive description: - name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.5.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/lib/src/crowdin.dart b/lib/src/crowdin.dart index d6c9ad1..bfd44bb 100644 --- a/lib/src/crowdin.dart +++ b/lib/src/crowdin.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:crowdin_sdk/src/crowdin_api.dart'; +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; import 'package:crowdin_sdk/src/crowdin_storage.dart'; import 'package:crowdin_sdk/src/crowdin_extractor.dart'; import 'package:crowdin_sdk/src/crowdin_mapper.dart'; @@ -82,7 +83,9 @@ class Crowdin { }) async { await _storage.init(); - _timestampCached = _storage.getTranslationTimestampFromStorage(); + CrowdinRequestLimiter().init(_storage); + + _timestampCached = _storage.getTranslationTimestamp(); _distributionHash = distributionHash; CrowdinLogger.printLog('distributionHash $_distributionHash'); @@ -158,7 +161,8 @@ class Crowdin { } } - if (!await _isConnectionTypeAllowed(_connectionType)) { + if (!await _isConnectionTypeAllowed(_connectionType) || + CrowdinRequestLimiter().pauseRequests) { _arb = null; return; // return from function if connection type is forbidden for downloading translations } @@ -171,7 +175,7 @@ class Crowdin { try { if (!canUpdate) { - distribution = _storage.getTranslationFromStorage(locale); + distribution = _storage.getTranslation(locale); if (distribution != null) { _arb = AppResourceBundle(distribution); if (_withRealTimeUpdates) { @@ -197,7 +201,7 @@ class Crowdin { /// todo remove when distribution file locale will be fixed distribution['@@locale'] = locale.toString(); - _storage.setDistributionToStorage( + _storage.setDistribution( jsonEncode(distribution), ); _arb = AppResourceBundle(distribution); @@ -208,7 +212,7 @@ class Crowdin { } if (_timestamp != null && _timestamp != _timestampCached) { - _storage.setTranslationTimeStampStorage(_timestamp!); + _storage.setTranslationTimeStamp(_timestamp!); _timestampCached = _timestamp; } } diff --git a/lib/src/crowdin_api.dart b/lib/src/crowdin_api.dart index 2b6b830..ee9a43f 100644 --- a/lib/src/crowdin_api.dart +++ b/lib/src/crowdin_api.dart @@ -1,21 +1,29 @@ import 'dart:convert'; +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; import 'crowdin_logger.dart'; class CrowdinApi { + @visibleForTesting + http.Client client = http.Client(); + @visibleForTesting + CrowdinRequestLimiter requestLimiter = CrowdinRequestLimiter(); + Future?> loadTranslations({ required String distributionHash, required String timeStamp, String? path, }) async { try { - var response = await http.get( + var response = await client.crowdinGet( Uri.https('distributions.crowdin.net', '/$distributionHash$path', {'timestamp': timeStamp}), ); - Map responseDecoded = + if (response == null) return null; + Map? responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded; } catch (ex) { @@ -29,13 +37,21 @@ class CrowdinApi { required String distributionHash, }) async { try { - var response = await http.get( + var response = await client.crowdinGet( Uri.parse( 'https://distributions.crowdin.net/$distributionHash/manifest.json'), ); - Map responseDecoded = - jsonDecode(utf8.decode(response.bodyBytes)); - return responseDecoded; + if (response == null) { + return null; + } else if (response.statusCode >= 400 && response.statusCode < 500) { + requestLimiter.incrementErrorCounter(); + return null; + } else { + Map responseDecoded = + jsonDecode(utf8.decode(response.bodyBytes)); + requestLimiter.reset(); + return responseDecoded; + } } catch (ex) { CrowdinLogger.printLog( "something went wrong. Crowdin couldn't download manifest file. Next exception occurred: $ex"); @@ -48,8 +64,9 @@ class CrowdinApi { required String mappingFilePath, }) async { try { - var response = await http.get(Uri.parse( + var response = await client.crowdinGet(Uri.parse( 'https://distributions.crowdin.net/$distributionHash$mappingFilePath')); + if (response == null) return null; Map responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded; @@ -68,10 +85,11 @@ class CrowdinApi { try { String organizationDomain = organizationName != null ? '$organizationName.' : ''; - var response = await http.get( + var response = await client.crowdinGet( Uri.parse( 'https://${organizationDomain}api.crowdin.com/api/v2/distributions/metadata?hash=$distributionHash'), headers: {'Authorization': 'Bearer $accessToken'}); + if (response == null) return null; Map responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded; @@ -90,7 +108,7 @@ class CrowdinApi { try { String organizationDomain = organizationName != null ? '$organizationName.' : ''; - var response = await http.post( + var response = await client.crowdinPost( Uri.parse( 'https://${organizationDomain}api.crowdin.com/api/v2/user/websocket-ticket'), headers: { @@ -101,6 +119,7 @@ class CrowdinApi { "event": event, "context": {"mode": "translate"} })); + if (response == null) return null; Map responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded['data']['ticket']; @@ -111,3 +130,19 @@ class CrowdinApi { } } } + +extension _CrowdinHttpInterceptorExtension on http.Client { + Future crowdinGet(Uri url, + {Map? headers}) async { + return CrowdinRequestLimiter().pauseRequests + ? null + : get(url, headers: headers); + } + + Future crowdinPost(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return CrowdinRequestLimiter().pauseRequests + ? null + : post(url, headers: headers, body: body, encoding: encoding); + } +} diff --git a/lib/src/crowdin_request_limiter.dart b/lib/src/crowdin_request_limiter.dart new file mode 100644 index 0000000..95959e2 --- /dev/null +++ b/lib/src/crowdin_request_limiter.dart @@ -0,0 +1,109 @@ +import 'package:crowdin_sdk/src/crowdin_storage.dart'; +import 'package:intl/intl.dart'; + +const int maxErrors = 10; +const int maxDaysInRow = 3; + +class CrowdinRequestLimiter { + CrowdinRequestLimiter._(); + + static final CrowdinRequestLimiter _instance = CrowdinRequestLimiter._(); + + factory CrowdinRequestLimiter() { + return _instance; + } + + late CrowdinStorage _storage; + + final DateFormat _formatter = DateFormat('yyyy-MM-dd'); + Map _errorMap = {}; + bool _pauseRequests = false; + bool _stopPermanently = false; + + bool get pauseRequests => + _stopPermanently || _pauseRequests || _checkIsPausedForToday(); + + init(CrowdinStorage storage) { + _storage = storage; + _stopPermanently = _storage.getIsPausedPermanently() ?? false; + _errorMap = _storage.getErrorMap() ?? {}; + } + + /// Checks if the requests should be paused for today based on the error count. + bool _checkIsPausedForToday() { + String currentDateString = _formatter.format(DateTime.now()); + if (_errorMap[currentDateString] != null && + _errorMap[currentDateString]! >= maxErrors) { + _pauseRequests = true; + return true; + } else { + _pauseRequests = false; + return false; + } + } + + /// Increments the error counter for the current date. + void incrementErrorCounter() { + DateFormat formatter = DateFormat('yyyy-MM-dd'); + String currentDateString = formatter.format(DateTime.now()); + if (_errorMap[currentDateString] != null) { + if (_errorMap[currentDateString]! < maxErrors) { + _errorMap[currentDateString] = _errorMap[currentDateString]! + 1; + } + if (_errorMap[currentDateString]! >= maxErrors) { + checkPausedDays(currentDateString); + } + } else { + _errorMap[currentDateString] = 1; + } + _storage.setErrorMap(_cleanErrorMapFromUnusedDays()); + } + + reset() { + if (!_stopPermanently) { + _pauseRequests = false; + _errorMap = {}; + _storage.setErrorMap(_errorMap); + } + } + + /// Checks if the number of errors in the last `maxDaysInRow` days exceeds `maxErrors`. + void checkPausedDays(String newDate) { + int daysInRow = 0; + if (_errorMap.length >= maxDaysInRow) { + DateTime currentDate = DateTime.parse(newDate); + for (String date in _errorMap.keys) { + if (DateTime.parse(date).isAfter( + currentDate.add(const Duration(days: -maxDaysInRow))) && + _errorMap[date]! >= maxErrors) { + daysInRow++; + _pauseRequests = true; + } + } + if (daysInRow >= maxDaysInRow) { + _errorMap.clear(); + _stopRequestsPermanently(); + } + } + } + + /// Cleans the error map from unused days, keeping only the last `maxDaysInRow` days. + Map _cleanErrorMapFromUnusedDays() { + DateTime currentDate = DateTime.now(); + _errorMap.removeWhere((date, _) { + DateTime dateTime = DateTime.parse(date); + return dateTime + .isBefore(currentDate.subtract(const Duration(days: maxDaysInRow))); + }); + _storage.setErrorMap(_errorMap); + return _errorMap; + } + + /// Permanently stops requests by setting the pause flag and updating the storage. + void _stopRequestsPermanently() { + _pauseRequests = true; + _stopPermanently = true; + _storage.setIsPausedPermanently(true); + _storage.setErrorMap(_errorMap); + } +} diff --git a/lib/src/crowdin_storage.dart b/lib/src/crowdin_storage.dart index e98db9b..6545e60 100644 --- a/lib/src/crowdin_storage.dart +++ b/lib/src/crowdin_storage.dart @@ -1,11 +1,14 @@ import 'dart:convert'; import 'dart:ui'; +import 'package:crowdin_sdk/src/crowdin_logger.dart'; import 'package:crowdin_sdk/src/exceptions/crowdin_exceptions.dart'; import 'package:shared_preferences/shared_preferences.dart'; String _kCrowdinTexts = 'crowdin_texts'; String _kTranslationTimestamp = 'translation_timestamp'; +String _kIsPausedPermanently = 'is_paused_permanently'; +String _kErrorMap = 'errorMap'; class CrowdinStorage { CrowdinStorage(); @@ -17,7 +20,7 @@ class CrowdinStorage { return _sharedPrefs; } - Future setTranslationTimeStampStorage(int? timestamp) async { + Future setTranslationTimeStamp(int? timestamp) async { try { if (_sharedPrefs.containsKey(_kTranslationTimestamp)) { await _sharedPrefs.remove(_kTranslationTimestamp); @@ -28,7 +31,7 @@ class CrowdinStorage { } } - int? getTranslationTimestampFromStorage() { + int? getTranslationTimestamp() { try { int? translationTimestamp = _sharedPrefs.getInt(_kTranslationTimestamp); return translationTimestamp; @@ -37,7 +40,7 @@ class CrowdinStorage { } } - Future setDistributionToStorage(String distribution) async { + Future setDistribution(String distribution) async { try { if (_sharedPrefs.containsKey(_kCrowdinTexts)) { await _sharedPrefs.remove(_kCrowdinTexts); @@ -48,7 +51,7 @@ class CrowdinStorage { } } - Map? getTranslationFromStorage(Locale locale) { + Map? getTranslation(Locale locale) { try { String? distributionStr = _sharedPrefs.getString(_kCrowdinTexts); if (distributionStr != null) { @@ -65,4 +68,43 @@ class CrowdinStorage { } return null; } + + void setIsPausedPermanently(bool shouldPause) { + try { + _sharedPrefs.setBool(_kIsPausedPermanently, shouldPause); + } catch (ex) { + throw CrowdinException("Can't store the isPausedPermanently value"); + } + } + + bool? getIsPausedPermanently() { + try { + bool? isPausedPermanently = _sharedPrefs.getBool(_kIsPausedPermanently); + return isPausedPermanently; + } catch (ex) { + throw CrowdinException("Can't get isPausedPermanently from storage"); + } + } + + void setErrorMap(Map errorMap) { + try { + _sharedPrefs.setString(_kErrorMap, jsonEncode(errorMap)); + } catch (ex) { + throw CrowdinException("Can't store the errorMap"); + } + } + + Map? getErrorMap() { + try { + String? errorMapString = _sharedPrefs.getString(_kErrorMap); + if (errorMapString != null) { + Map decodedMap = jsonDecode(errorMapString); + return decodedMap.map((k, v) => MapEntry(k, v as int)); + } + return errorMapString != null ? jsonDecode(errorMapString) : null; + } catch (ex) { + CrowdinLogger.printLog("Can't get errorMap from storage"); + return null; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 8cad679..3213f6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: 1.0.4 + mocktail: ^1.0.0 platforms: android: diff --git a/test/crowdin_api_test.dart b/test/crowdin_api_test.dart new file mode 100644 index 0000000..4c330bb --- /dev/null +++ b/test/crowdin_api_test.dart @@ -0,0 +1,136 @@ +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; +import 'package:crowdin_sdk/src/crowdin_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:crowdin_sdk/src/crowdin_api.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'crowdin_request_limiter_test.dart'; + +class MockHttpClient extends Mock implements http.Client {} + +void main() { + late CrowdinApi crowdinApi; + late MockHttpClient mockHttpClient; + late CrowdinRequestLimiter requestLimiter; + late SharedPreferences sharedPrefs; + late CrowdinStorage storage; + + setUp(() async { + mockHttpClient = MockHttpClient(); + crowdinApi = CrowdinApi(); + requestLimiter = CrowdinRequestLimiter(); + SharedPreferences.setMockInitialValues({}); + registerFallbackValue(Uri()); + crowdinApi.requestLimiter = requestLimiter; + crowdinApi.client = mockHttpClient; + sharedPrefs = await SharedPreferences.getInstance(); + storage = CrowdinStorage(); + await storage.init(); + }); + + tearDown(() async { + await sharedPrefs.clear(); + }); + + group('CrowdinApi', () { + test('loadTranslations returns decoded response on success', () async { + final uri = Uri.https( + 'distributions.crowdin.net', '/hash/path', {'timestamp': '12345'}); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('{"key": "value"}', 200), + ); + + final result = await crowdinApi.loadTranslations( + distributionHash: 'hash', + timeStamp: '12345', + path: '/path', + ); + + expect(result, {'key': 'value'}); + }); + + test('getMapping returns decoded response on success', () async { + final uri = Uri.parse('https://distributions.crowdin.net/hash/path'); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('{"key": "value"}', 200), + ); + + final result = await crowdinApi.getMapping( + distributionHash: 'hash', + mappingFilePath: '/path', + ); + + expect(result, {'key': 'value'}); + }); + + test('getMetadata returns decoded response on success', () async { + final uri = Uri.parse( + 'https://api.crowdin.com/api/v2/distributions/metadata?hash=hash'); + when(() => mockHttpClient.get(uri, headers: any(named: 'headers'))) + .thenAnswer( + (_) async => http.Response('{"data": {"key": "value"}}', 200), + ); + + final result = await crowdinApi.getMetadata( + accessToken: 'token', + distributionHash: 'hash', + ); + + expect(result, { + 'data': {'key': 'value'} + }); + }); + + test('getWebsocketTicket returns ticket on success', () async { + final uri = + Uri.parse('https://api.crowdin.com/api/v2/user/websocket-ticket'); + when(() => mockHttpClient.post(uri, + headers: any(named: 'headers'), body: any(named: 'body'))).thenAnswer( + (_) async => http.Response('{"data": {"ticket": "ticket_value"}}', 200), + ); + + final result = await crowdinApi.getWebsocketTicket( + accessToken: 'token', + event: 'event_name', + ); + + expect(result, 'ticket_value'); + }); + + test('getManifest returns null and increment error count on 400 status', + () async { + await requestLimiter.init(storage); + + final uri = + Uri.parse('https://distributions.crowdin.net/hash/manifest.json'); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('', 400), + ); + + final result = await crowdinApi.getManifest(distributionHash: 'hash'); + + expect(result, isNull); + expect(storage.getErrorMap(), {getTodayDateString(): 1}); + }); + + test( + 'getManifest returns null and do not call request when requests paused', + () async { + storage.setIsPausedPermanently(true); + await requestLimiter.init(storage); + + final uri = + Uri.parse('https://distributions.crowdin.net/hash/manifest.json'); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('', 200), + ); + + final result = await crowdinApi.getManifest(distributionHash: 'hash'); + + verifyNever(() => mockHttpClient.get(any())); + expect(result, isNull); + }); + }); +} diff --git a/test/crowdin_request_limiter_test.dart b/test/crowdin_request_limiter_test.dart new file mode 100644 index 0000000..8279f89 --- /dev/null +++ b/test/crowdin_request_limiter_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; +import 'package:crowdin_sdk/src/crowdin_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MockCrowdinStorage extends Mock implements CrowdinStorage {} + +final DateFormat _formatter = DateFormat('yyyy-MM-dd'); + +@visibleForTesting +String getTodayDateString() { + return _formatter.format(DateTime.now()); +} + +void main() { + late CrowdinRequestLimiter requestLimiter; + late CrowdinStorage storage; + late SharedPreferences sharedPrefs; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + sharedPrefs = await SharedPreferences.getInstance(); + storage = CrowdinStorage(); + requestLimiter = CrowdinRequestLimiter(); + await storage.init(); + }); + + tearDown(() async { + await sharedPrefs.clear(); + }); + + test('should initialize with storage values', () async { + storage.setIsPausedPermanently(true); + await requestLimiter.init(storage); + expect(storage.getIsPausedPermanently(), true); + expect(requestLimiter.pauseRequests, true); + }); + + test('should increment error counter', () { + requestLimiter.init(storage); + requestLimiter.incrementErrorCounter(); + expect(storage.getErrorMap(), {getTodayDateString(): 1}); + requestLimiter.incrementErrorCounter(); + expect(storage.getErrorMap(), {getTodayDateString(): 2}); + }); + + test('should pause requests after max errors in a day', () async { + storage.setErrorMap({getTodayDateString(): 10}); + await requestLimiter.init(storage); + expect(requestLimiter.pauseRequests, true); + }); + + test('should reset error map and pause state', () async { + storage.setErrorMap({getTodayDateString(): 10}); + await requestLimiter.init(storage); + expect(requestLimiter.pauseRequests, true); + requestLimiter.reset(); + expect(requestLimiter.pauseRequests, false); + }); + + test('should stop requests permanently after max days in a row', () async { + storage.setErrorMap({ + _formatter.format(DateTime.now()): 10, + _formatter.format(DateTime.now().subtract(const Duration(days: 1))): 10, + _formatter.format(DateTime.now().subtract(const Duration(days: 2))): 10, + }); + await requestLimiter.init(storage); + requestLimiter.incrementErrorCounter(); + expect(requestLimiter.pauseRequests, true); + }); +} diff --git a/test/crowdin_storage_test.dart b/test/crowdin_storage_test.dart index 9c11dae..64f0832 100644 --- a/test/crowdin_storage_test.dart +++ b/test/crowdin_storage_test.dart @@ -24,9 +24,8 @@ void main() { test('set and get translation timestamp', () async { const int timestamp = 123456; - await crowdinStorage.setTranslationTimeStampStorage(timestamp); - final int? retrievedTimestamp = - crowdinStorage.getTranslationTimestampFromStorage(); + await crowdinStorage.setTranslationTimeStamp(timestamp); + final int? retrievedTimestamp = crowdinStorage.getTranslationTimestamp(); expect(retrievedTimestamp, equals(timestamp)); }); @@ -36,33 +35,30 @@ void main() { final Map expectedDistribution = jsonDecode(distributionJson); - await crowdinStorage.setDistributionToStorage(distributionJson); + await crowdinStorage.setDistribution(distributionJson); final Map? retrievedDistribution = - crowdinStorage.getTranslationFromStorage(const Locale('en', 'US')); + crowdinStorage.getTranslation(const Locale('en', 'US')); expect(retrievedDistribution, equals(expectedDistribution)); }); test('get exception in case of empty distribution ', () async { - await crowdinStorage.setDistributionToStorage(''); + await crowdinStorage.setDistribution(''); - expect( - () => crowdinStorage - .getTranslationFromStorage(const Locale('en', 'US')), + expect(() => crowdinStorage.getTranslation(const Locale('en', 'US')), throwsA(const TypeMatcher())); }); test('get null if timestamp is missed', () async { - final int? retrievedTimestamp = - crowdinStorage.getTranslationTimestampFromStorage(); + final int? retrievedTimestamp = crowdinStorage.getTranslationTimestamp(); expect(retrievedTimestamp, isNull); }); test('get null if distribution is missed', () async { final Map? retrievedDistribution = - crowdinStorage.getTranslationFromStorage(const Locale('en', 'US')); + crowdinStorage.getTranslation(const Locale('en', 'US')); expect(retrievedDistribution, isNull); }); @@ -70,10 +66,10 @@ void main() { test('get null if distribution locale mismatched', () async { const String distributionJson = '{"@@locale": "en_US", "hello_world": "Hello, world!"}'; - await crowdinStorage.setDistributionToStorage(distributionJson); + await crowdinStorage.setDistribution(distributionJson); final Map? retrievedDistribution = - crowdinStorage.getTranslationFromStorage(const Locale('es', 'ES')); + crowdinStorage.getTranslation(const Locale('es', 'ES')); expect(retrievedDistribution, isNull); });