From 94dd151c678f7283a99e46ebd7284e67a7b3db13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:32:10 +0200 Subject: [PATCH 001/129] [Oztechan/Global#12] Update submodule/basemob digest to d32e735 (#3085) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodule/basemob b/submodule/basemob index 90068f6b2c..d32e73565a 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit 90068f6b2cca4cf1024d77d55f94a5110de487cf +Subproject commit d32e73565ad3319fa9f0845d9641879868edf1d2 From 7b65512d5263a4e065320df6712a1a5179ba9138 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 19 Jan 2024 19:16:53 +0200 Subject: [PATCH 002/129] [Oztechan/CCC#3095] Add PERSONAL_ACCESS_TOKEN to called workflow's secret (#3099) * [Oztechan/CCC#3095] Remove secret inheritance for submodules * [Oztechan/CCC#3095] Remove secret inheritance for submodules * [Oztechan/CCC#3095] Add PERSONAL_ACCESS_TOKEN to called workflow's secret --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index d32e73565a..57f855edc8 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit d32e73565ad3319fa9f0845d9641879868edf1d2 +Subproject commit 57f855edc8fa59e3555792190abe053b617506db diff --git a/submodule/logmob b/submodule/logmob index 4eeda537f6..208429fb04 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 4eeda537f6d2478c06c34fdbb2ccad213c6d9a2f +Subproject commit 208429fb0422ccb65e16790dd30ba41a2c98be6d diff --git a/submodule/parsermob b/submodule/parsermob index ce439290f1..ad3ab47d4a 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit ce439290f1155bdff504d3d5d93fc9874cfcc80b +Subproject commit ad3ab47d4af48d805fa5b170b4ef74d38fc83c74 diff --git a/submodule/scopemob b/submodule/scopemob index c87536c9ed..28bfa81bff 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit c87536c9ed31ecc6c43707d1704f4571b6859518 +Subproject commit 28bfa81bffcb47a2571f8c70e591c06db0e9dd67 From b89443ebe8acd7293f805966c98be5f212d55bf0 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 19 Jan 2024 19:43:31 +0200 Subject: [PATCH 003/129] [Oztechan/CCC#3093] Move DistributeIOS to ubuntu-latest (#3094) * [Oztechan/CCC#3093] Move DistributeIOS to ubuntu-latest * [Oztechan/CCC#3093] Move DistributeIOS to ubuntu-latest * [Oztechan/CCC#3093] Move DistributeIOS to ubuntu-latest --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6376dd66a8..4241f70d84 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -212,7 +212,7 @@ jobs: run: echo "status=success" >> $GITHUB_OUTPUT DistributeIOS: - runs-on: macos-13 + runs-on: ubuntu-latest needs: [ XCodeBuild ] if: github.event_name == 'push' outputs: @@ -227,9 +227,12 @@ jobs: name: iOSArtifacts path: ios + # was necessary to use chown to fix permission issues in linux machines - name: Distribute working-directory: ios - run: fastlane distribute + run: | + sudo chown -R $(whoami) /var/lib/gems/3.0.0 + fastlane distribute - name: Delete iOS IPA uses: geekyeggo/delete-artifact@v4.0.0 From cbecc6c2450cabd3d0275d83ab295b245aed8f7c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:06:54 +0200 Subject: [PATCH 004/129] [Oztechan/Global#12] Update submodule/basemob digest to ca65949 (#3096) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodule/basemob b/submodule/basemob index 57f855edc8..ca65949683 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit 57f855edc8fa59e3555792190abe053b617506db +Subproject commit ca65949683f811a0a7fdf6a726a085d11e685114 From 42164414e151f3b3ad6075e43f77851420e138bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:31:39 +0200 Subject: [PATCH 005/129] [Oztechan/Global#12] Update Oztechan/Global digest to 79ab473 (#3101) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index d9c305da44..3dc47de6aa 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -14,7 +14,7 @@ on: jobs: ProjectAutomations: - uses: Oztechan/Global/.github/workflows/reusable-project.yml@a3db254980b0b8f7931b1f591676a3a9473bfcfb + uses: Oztechan/Global/.github/workflows/reusable-project.yml@79ab47368a8b426c8b178e1b32fef58239b3a8c6 with: project_id: 2 secrets: inherit From 3ddbe1956db3b7f27505d65fc7d54d24c0f59c3b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:32:11 +0200 Subject: [PATCH 006/129] [Oztechan/Global#12] Update geekyeggo/delete-artifact action to v4.1.0 (#3073) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4241f70d84..98e8dc04c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -144,7 +144,7 @@ jobs: file: huawei/release/app-huawei-release.apk - name: Delete Android Artifacts - uses: geekyeggo/delete-artifact@v4.0.0 + uses: geekyeggo/delete-artifact@v4.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} name: androidArtifacts @@ -235,7 +235,7 @@ jobs: fastlane distribute - name: Delete iOS IPA - uses: geekyeggo/delete-artifact@v4.0.0 + uses: geekyeggo/delete-artifact@v4.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} name: iOSArtifacts @@ -319,7 +319,7 @@ jobs: -Dsonar.coverage.jacoco.xmlReportPaths=build/report.xml - name: Delete Coverage Report - uses: geekyeggo/delete-artifact@v4.0.0 + uses: geekyeggo/delete-artifact@v4.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} name: coverageReport diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d957149562..68bc7d58df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,7 +138,7 @@ jobs: status: completed - name: Delete App Bundle - uses: geekyeggo/delete-artifact@v4.0.0 + uses: geekyeggo/delete-artifact@v4.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} name: googleBundle @@ -170,7 +170,7 @@ jobs: file-name: "app-huawei-release" - name: Delete App Bundle - uses: geekyeggo/delete-artifact@v4.0.0 + uses: geekyeggo/delete-artifact@v4.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} name: huaweiBundle @@ -202,7 +202,7 @@ jobs: SOURCE: "artifact/" - name: Delete Backend Jar - uses: geekyeggo/delete-artifact@v4.0.0 + uses: geekyeggo/delete-artifact@v4.1.0 with: token: ${{ secrets.GITHUB_TOKEN }} name: backendJar From e96206a516a5dea32323c348f15a865d67e4ad98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:32:27 +0200 Subject: [PATCH 007/129] [Oztechan/Global#12] Update actions/upload-artifact action to v4.2.0 (#3069) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 98e8dc04c5..8dfd82f1b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,7 +95,7 @@ jobs: arguments: assemble - name: Upload Android Artifacts - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.2.0 if: github.event_name == 'push' with: name: androidArtifacts @@ -195,7 +195,7 @@ jobs: run: fastlane build - name: Upload iOS Artifacts - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.2.0 if: github.event_name == 'push' with: name: iOSArtifacts @@ -267,7 +267,7 @@ jobs: arguments: check koverMergedXmlReport --parallel - name: Upload Coverage Report - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.2.0 with: name: coverageReport path: build/reports/kover/merged/xml/report.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68bc7d58df..cc6c3b814e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,19 +90,19 @@ jobs: arguments: :android:app:bundleRelease :backend:app:jar --parallel - name: Upload Google App Bundle - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.2.0 with: name: googleBundle path: android/app/build/outputs/bundle/googleRelease/app-google-release.aab - name: Upload Huawei App Bundle - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.2.0 with: name: huaweiBundle path: android/app/build/outputs/bundle/huaweiRelease/app-huawei-release.aab - name: Upload Backend Jar - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.2.0 with: name: backendJar path: backend/app/build/libs/app-*.jar From 2e404fd06a9cd1172219f6b808a349620f7427e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:32:52 +0200 Subject: [PATCH 008/129] [Oztechan/Global#12] Update Git Submodules (#3102) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/submodule/logmob b/submodule/logmob index 208429fb04..ef43772e40 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 208429fb0422ccb65e16790dd30ba41a2c98be6d +Subproject commit ef43772e4080e1c325a23d95db9e3a41e793998c diff --git a/submodule/parsermob b/submodule/parsermob index ad3ab47d4a..09c3df4e47 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit ad3ab47d4af48d805fa5b170b4ef74d38fc83c74 +Subproject commit 09c3df4e477d41b973a525d16e4126fdbd496aed diff --git a/submodule/scopemob b/submodule/scopemob index 28bfa81bff..c1ee149204 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 28bfa81bffcb47a2571f8c70e591c06db0e9dd67 +Subproject commit c1ee1492046ef19744232b7ec11dad7171fbbd98 From 772ee56d8af6d00549ee3f69f08b2801ff2a2aeb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:11:52 +0200 Subject: [PATCH 009/129] [Oztechan/Global#12] Update dependency com.google.devtools.ksp to v1.9.22-1.0.17 (#3098) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4cfb44bfbe..636eb080f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "1.9.22" -ksp = "1.9.22-1.0.16" +ksp = "1.9.22-1.0.17" detekt = "1.23.4" androidGradlePlugin = "8.2.1" composeCompiler = "1.5.8" From 3dbcf75775b3e410605ce68b92c450401c057277 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 20 Jan 2024 17:42:51 +0200 Subject: [PATCH 010/129] [Oztechan/Global#4] Global config update synced file(s) with Oztechan/Global (#3100) * [Oztechan/Global#4] Global config update synced local 'docs/' with remote 'docs/' * [Oztechan/Global#4] Global config update synced local 'docs/' with remote 'docs/' From c550606ea41e204a31136440168fdf9aa8f18c89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:39:59 +0200 Subject: [PATCH 011/129] [Oztechan/Global#12] Update Git Submodules (#3104) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index ca65949683..1748897e5a 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit ca65949683f811a0a7fdf6a726a085d11e685114 +Subproject commit 1748897e5abd3992d14ff080e4aaa61824f5e689 diff --git a/submodule/logmob b/submodule/logmob index ef43772e40..b4b596b307 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit ef43772e4080e1c325a23d95db9e3a41e793998c +Subproject commit b4b596b307a125e653a7ff62a5063fb9490864ee diff --git a/submodule/parsermob b/submodule/parsermob index 09c3df4e47..5746b11d92 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 09c3df4e477d41b973a525d16e4126fdbd496aed +Subproject commit 5746b11d92e03e8cb8ab20ee3b810378e444722d diff --git a/submodule/scopemob b/submodule/scopemob index c1ee149204..1698405f6a 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit c1ee1492046ef19744232b7ec11dad7171fbbd98 +Subproject commit 1698405f6a820e861c1d8f9fa6566e6889074c28 From b64c2b6b275b9b7640bf8485b7a853e997f0f557 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:11:09 +0200 Subject: [PATCH 012/129] [Oztechan/Global#12] Update Oztechan/Global digest to 3c57e3c (#3103) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 3dc47de6aa..130edaaf17 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -14,7 +14,7 @@ on: jobs: ProjectAutomations: - uses: Oztechan/Global/.github/workflows/reusable-project.yml@79ab47368a8b426c8b178e1b32fef58239b3a8c6 + uses: Oztechan/Global/.github/workflows/reusable-project.yml@3c57e3c625e947620957a7ea80dae7541e54481f with: project_id: 2 secrets: inherit From 0e70c3c3839c00e65ee303cdee76808f4586f068 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:22:54 +0300 Subject: [PATCH 013/129] [Oztechan/Global#12] Lock file maintenance (#3109) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 472820fff7..c69c7e602e 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -8,7 +8,7 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.881.0) + aws-partitions (1.883.0) aws-sdk-core (3.190.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) From e9388d698c20284e61d5bfe175c07267ff25b583 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 22:56:40 +0300 Subject: [PATCH 014/129] [Oztechan/Global#12] Update Git Submodules (#3105) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index 1748897e5a..ab675bd87f 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit 1748897e5abd3992d14ff080e4aaa61824f5e689 +Subproject commit ab675bd87f5e625aa63f22888d8b117d0ac472ce diff --git a/submodule/logmob b/submodule/logmob index b4b596b307..563116668f 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit b4b596b307a125e653a7ff62a5063fb9490864ee +Subproject commit 563116668f008402c4d5b93e635b80dc801475fe diff --git a/submodule/parsermob b/submodule/parsermob index 5746b11d92..2c65ea4e61 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 5746b11d92e03e8cb8ab20ee3b810378e444722d +Subproject commit 2c65ea4e612139381d8d8f896ee7f2882c250486 diff --git a/submodule/scopemob b/submodule/scopemob index 1698405f6a..5161c3a44d 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 1698405f6a820e861c1d8f9fa6566e6889074c28 +Subproject commit 5161c3a44d725004b443c32afc24ff608657363e From 983c1f0dc6b6de93b969fb13d6b301a5a66f6db3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 22:58:18 +0300 Subject: [PATCH 015/129] [Oztechan/Global#12] Update dependency fastlane to v2.219.0 (#3097) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index c69c7e602e..2e5b561c04 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -109,11 +109,11 @@ GEM google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) + google-apis-androidpublisher_v3 (0.55.0) + google-apis-core (>= 0.12.0, < 2.a) + google-apis-core (0.12.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) httpclient (>= 2.8.1, < 3.a) mini_mime (~> 1.0) representable (~> 3.0) @@ -123,10 +123,10 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-firebaseappdistribution_v1alpha (0.2.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) + google-apis-iamcredentials_v1 (0.18.0) + google-apis-core (>= 0.12.0, < 2.a) + google-apis-playcustomapp_v1 (0.14.0) + google-apis-core (>= 0.12.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) google-cloud-core (1.6.1) From 8ccde9fa193819ca51372d1b20d2b268cd9c805b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:53:23 +0300 Subject: [PATCH 016/129] [Oztechan/Global#12] Update androidGradlePlugin to v8.2.2 (#3110) * [Oztechan/Global#12] Update androidGradlePlugin to v8.2.2 Co-authored-by: Mustafa Ozhan * [Oztechan/Global#12] Update Git Submodules (#3112) * [Oztechan/Global#12] Update dependency fastlane to v2.219.0 (#3097) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan * [Oztechan/Global#12] Update Git Submodules Co-authored-by: Mustafa Ozhan --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- submodule/basemob | 2 +- submodule/logmob | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 636eb080f1..b16dfa049b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "1.9.22" ksp = "1.9.22-1.0.17" detekt = "1.23.4" -androidGradlePlugin = "8.2.1" +androidGradlePlugin = "8.2.2" composeCompiler = "1.5.8" compose = "1.5.4" glance = "1.0.0" diff --git a/submodule/basemob b/submodule/basemob index ab675bd87f..bea3e84402 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit ab675bd87f5e625aa63f22888d8b117d0ac472ce +Subproject commit bea3e84402c24c66591d6889929278cea29da76f diff --git a/submodule/logmob b/submodule/logmob index 563116668f..bc33faa916 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 563116668f008402c4d5b93e635b80dc801475fe +Subproject commit bc33faa9162e70938f28b4ebe82e1feefda81c24 From 035b5c8843b46c2f2f6cdaae7fe6a26d66bd8caa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:11:03 +0300 Subject: [PATCH 017/129] [Oztechan/Global#12] Update gradle/gradle-build-action action to v2.12.0 (#3115) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8dfd82f1b9..73a5ccca14 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -90,7 +90,7 @@ jobs: distribution: 'temurin' - name: Assemble - uses: gradle/gradle-build-action@v2.11.1 + uses: gradle/gradle-build-action@v2.12.0 with: arguments: assemble @@ -262,7 +262,7 @@ jobs: distribution: 'temurin' - name: Run Quality Jobs - uses: gradle/gradle-build-action@v2.11.1 + uses: gradle/gradle-build-action@v2.12.0 with: arguments: check koverMergedXmlReport --parallel @@ -346,7 +346,7 @@ jobs: distribution: 'temurin' - name: Detekt - uses: gradle/gradle-build-action@v2.11.1 + uses: gradle/gradle-build-action@v2.12.0 with: arguments: detektAll diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc6c3b814e..b0697ea700 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: distribution: 'temurin' - name: Generate Artifacts - uses: gradle/gradle-build-action@v2.11.1 + uses: gradle/gradle-build-action@v2.12.0 with: arguments: :android:app:bundleRelease :backend:app:jar --parallel From bc1073916a876aab5921d23d9a9efd050c608d63 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:11:18 +0300 Subject: [PATCH 018/129] [Oztechan/Global#12] Update actions/upload-artifact action to v4.3.0 (#3111) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 73a5ccca14..c247ead2c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,7 +95,7 @@ jobs: arguments: assemble - name: Upload Android Artifacts - uses: actions/upload-artifact@v4.2.0 + uses: actions/upload-artifact@v4.3.0 if: github.event_name == 'push' with: name: androidArtifacts @@ -195,7 +195,7 @@ jobs: run: fastlane build - name: Upload iOS Artifacts - uses: actions/upload-artifact@v4.2.0 + uses: actions/upload-artifact@v4.3.0 if: github.event_name == 'push' with: name: iOSArtifacts @@ -267,7 +267,7 @@ jobs: arguments: check koverMergedXmlReport --parallel - name: Upload Coverage Report - uses: actions/upload-artifact@v4.2.0 + uses: actions/upload-artifact@v4.3.0 with: name: coverageReport path: build/reports/kover/merged/xml/report.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0697ea700..b08a0876f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,19 +90,19 @@ jobs: arguments: :android:app:bundleRelease :backend:app:jar --parallel - name: Upload Google App Bundle - uses: actions/upload-artifact@v4.2.0 + uses: actions/upload-artifact@v4.3.0 with: name: googleBundle path: android/app/build/outputs/bundle/googleRelease/app-google-release.aab - name: Upload Huawei App Bundle - uses: actions/upload-artifact@v4.2.0 + uses: actions/upload-artifact@v4.3.0 with: name: huaweiBundle path: android/app/build/outputs/bundle/huaweiRelease/app-huawei-release.aab - name: Upload Backend Jar - uses: actions/upload-artifact@v4.2.0 + uses: actions/upload-artifact@v4.3.0 with: name: backendJar path: backend/app/build/libs/app-*.jar From 9d82059fa65c3b3be84d04fcf14427bc50f7a988 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 25 Jan 2024 20:03:26 +0300 Subject: [PATCH 019/129] [Oztechan/CCC#3107] Move to shared publishing task from Global repo (#3108) * [Oztechan/CCC#3107] Move to shared publishing task from Global repo * [Oztechan/CCC#3107] Move to shared publishing task from Global repo * [Oztechan/CCC#3107] Move to shared publishing task from Global repo --- .github/workflows/project.yml | 2 +- .github/workflows/publish.yml | 34 ++++------------------------------ submodule/basemob | 2 +- submodule/logmob | 2 +- 4 files changed, 7 insertions(+), 33 deletions(-) diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 130edaaf17..1d227d0d05 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -14,7 +14,7 @@ on: jobs: ProjectAutomations: - uses: Oztechan/Global/.github/workflows/reusable-project.yml@3c57e3c625e947620957a7ea80dae7541e54481f + uses: Oztechan/Global/.github/workflows/reusable-project.yml@v1.0.1 with: project_id: 2 secrets: inherit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c5c305aa78..d22fe2aa76 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,33 +8,7 @@ on: jobs: PublishRelease: - runs-on: ubuntu-latest - outputs: - status: ${{ steps.status.outputs.status }} - steps: - - - name: PublishRelease - uses: marvinpinto/action-automatic-releases@v1.2.1 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - prerelease: false - - - name: Set Job Status - id: status - run: echo "status=success" >> $GITHUB_OUTPUT - - Notify: - runs-on: ubuntu-latest - needs: [ PublishRelease ] - if: always() - steps: - - - name: Notify slack fail - if: false == (needs.PublishRelease.outputs.status == 'success') - uses: voxmedia/github-action-slack-notify-build@v1.6.0 - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - with: - channel: ccc-github - status: FAILED - color: danger \ No newline at end of file + uses: Oztechan/Global/.github/workflows/reusable-publish.yml@v1.0.1 + with: + slack_channel: "ccc-github" + secrets: inherit diff --git a/submodule/basemob b/submodule/basemob index bea3e84402..ab675bd87f 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit bea3e84402c24c66591d6889929278cea29da76f +Subproject commit ab675bd87f5e625aa63f22888d8b117d0ac472ce diff --git a/submodule/logmob b/submodule/logmob index bc33faa916..563116668f 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit bc33faa9162e70938f28b4ebe82e1feefda81c24 +Subproject commit 563116668f008402c4d5b93e635b80dc801475fe From 64243563b89e02d49811beae86208815138ea147 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:49:06 +0300 Subject: [PATCH 020/129] [Oztechan/Global#12] Update Git Submodules (#3113) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index ab675bd87f..bea3e84402 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit ab675bd87f5e625aa63f22888d8b117d0ac472ce +Subproject commit bea3e84402c24c66591d6889929278cea29da76f diff --git a/submodule/logmob b/submodule/logmob index 563116668f..bc33faa916 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 563116668f008402c4d5b93e635b80dc801475fe +Subproject commit bc33faa9162e70938f28b4ebe82e1feefda81c24 From 7073c6d9d1f9b89e8f271b4aa4e34d38bcc69ad1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 16:38:54 +0300 Subject: [PATCH 021/129] [Oztechan/Global#12] Update codecov/codecov-action action to v3.1.5 (#3116) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c247ead2c9..9b8b794e21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -296,7 +296,7 @@ jobs: path: build - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3.1.4 + uses: codecov/codecov-action@v3.1.5 with: token: ${{ secrets.CODECOV_TOKEN }} files: build/report.xml From 818b434367b2b258e76b409d9e94a466571e7a66 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 14:33:40 +0300 Subject: [PATCH 022/129] [Oztechan/Global#12] Update plugin com.gradle.enterprise to v3.16.2 (#3118) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 527792c63d..f540ebd157 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,7 +23,7 @@ dependencyResolutionManagement { } plugins { - id("com.gradle.enterprise") version ("3.16.1") + id("com.gradle.enterprise") version ("3.16.2") } gradleEnterprise { From 9dc2da67ba39e0a07f50e0e196487f03635dd0d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 15:48:52 +0300 Subject: [PATCH 023/129] [Oztechan/Global#12] Update compose to v1.6.0 (#3114) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b16dfa049b..b79772009a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "1.9.22-1.0.17" detekt = "1.23.4" androidGradlePlugin = "8.2.2" composeCompiler = "1.5.8" -compose = "1.5.4" +compose = "1.6.0" glance = "1.0.0" material3 = "1.1.2" androidDesugaring = "2.0.4" From 3e6895f8e40c7b3ce2230ff22f2235a6387f770b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 15:50:45 +0300 Subject: [PATCH 024/129] [Oztechan/Global#12] Update Git Submodules (#3119) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index bea3e84402..ee2c619c63 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit bea3e84402c24c66591d6889929278cea29da76f +Subproject commit ee2c619c63b0ac68faa82708ef62a87e0960866a diff --git a/submodule/logmob b/submodule/logmob index bc33faa916..e996eca715 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit bc33faa9162e70938f28b4ebe82e1feefda81c24 +Subproject commit e996eca715cf3c53dc78acb9561bd39e5796036a diff --git a/submodule/parsermob b/submodule/parsermob index 2c65ea4e61..83474a00f4 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 2c65ea4e612139381d8d8f896ee7f2882c250486 +Subproject commit 83474a00f448c9b04ed3697a13cff6335c2f5701 diff --git a/submodule/scopemob b/submodule/scopemob index 5161c3a44d..8e0e888df0 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 5161c3a44d725004b443c32afc24ff608657363e +Subproject commit 8e0e888df0d45494d8330505bf2febb8746ab8aa From 9c53434f533db4a904f1e20d5a749101963b81b0 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 28 Jan 2024 22:25:14 +0300 Subject: [PATCH 025/129] [Oztechan/CCC#3120] Move lastInput to Suspend (#3121) (cherry picked from commit 1a748cb6eae9b8165cb334633f533192c1b143cb) --- .../client-storage-calculation.gradle.kts | 1 + .../storage/calculation/CalculationStorage.kt | 3 ++- .../calculation/CalculationStorageImpl.kt | 12 ++++++---- .../calculation/CalculationStorageTest.kt | 23 ++++++++++++------- .../calculator/CalculatorViewModel.kt | 4 ++-- .../calculator/CalculatorViewModelTest.kt | 8 +++---- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/client/storage/calculation/client-storage-calculation.gradle.kts b/client/storage/calculation/client-storage-calculation.gradle.kts index 024143a5fb..faaa819225 100644 --- a/client/storage/calculation/client-storage-calculation.gradle.kts +++ b/client/storage/calculation/client-storage-calculation.gradle.kts @@ -20,6 +20,7 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) + implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt index 6156eb61e8..1c70e72208 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt @@ -5,5 +5,6 @@ interface CalculationStorage { var precision: Int - var lastInput: String + suspend fun getLastInput(): String + suspend fun setLastInput(value: String) } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index fc22c4f67d..ef220532a3 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -1,9 +1,11 @@ package com.oztechan.ccc.client.storage.calculation import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.SuspendPersistence class CalculationStorageImpl( - private val persistence: Persistence + private val persistence: Persistence, + private val suspendPersistence: SuspendPersistence ) : CalculationStorage { override var currentBase @@ -14,9 +16,11 @@ class CalculationStorageImpl( get() = persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) set(value) = persistence.setValue(KEY_PRECISION, value) - override var lastInput: String - get() = persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) - set(value) = persistence.setValue(KEY_LAST_INPUT, value) + override suspend fun getLastInput(): String = + suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) + + override suspend fun setLastInput(value: String) = + suspendPersistence.setSuspend(KEY_LAST_INPUT, value) companion object { internal const val DEFAULT_CURRENT_BASE = "" diff --git a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt index d37a563dde..8df4c26b7b 100644 --- a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt +++ b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt @@ -1,6 +1,7 @@ package com.oztechan.ccc.client.storage.calculation import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.SuspendPersistence import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_CURRENT_BASE import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_LAST_INPUT import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_PRECISION @@ -9,22 +10,28 @@ import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Compan import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.KEY_PRECISION import io.mockative.Mock import io.mockative.classOf +import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.configure import io.mockative.every import io.mockative.mock import io.mockative.verify +import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals internal class CalculationStorageTest { private val subject: CalculationStorage by lazy { - CalculationStorageImpl(persistence) + CalculationStorageImpl(persistence, suspendPersistence) } @Mock private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } + @Mock + private val suspendPersistence = mock(classOf()) + // defaults @Test fun `default currentBase`() { @@ -49,13 +56,13 @@ internal class CalculationStorageTest { } @Test - fun `default lastInput`() { - every { persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + fun `get default lastInput`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } .returns(DEFAULT_LAST_INPUT) - assertEquals(DEFAULT_LAST_INPUT, subject.lastInput) + assertEquals(DEFAULT_LAST_INPUT, subject.getLastInput()) - verify { persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + coVerify { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } .wasInvoked() } @@ -79,11 +86,11 @@ internal class CalculationStorageTest { } @Test - fun `set lastInput`() { + fun `set lastInput`() = runTest { val mockValue = "mock" - subject.lastInput = mockValue + subject.setLastInput(mockValue) - verify { persistence.setValue(KEY_LAST_INPUT, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_LAST_INPUT, mockValue) } .wasInvoked() } } diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 9a74ee324b..605a77dc4f 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -74,7 +74,7 @@ class CalculatorViewModel( copy( currencyList = currencyDataSource.getActiveCurrencies(), base = calculationStorage.currentBase, - input = calculationStorage.lastInput, + input = calculationStorage.getLastInput(), loading = true ) } @@ -103,7 +103,7 @@ class CalculatorViewModel( .distinctUntilChanged() .onEach { Logger.d { "CalculatorViewModel observeInput $it" } - calculationStorage.lastInput = it + calculationStorage.setLastInput(it) calculateOutput(it) } .launchIn(viewModelScope) diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index 5d54e96010..5964ad2675 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -98,9 +98,6 @@ internal class CalculatorViewModelTest { every { calculationStorage.currentBase } .returns(currency1.code) - every { calculationStorage.lastInput } - .returns("") - every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) @@ -111,6 +108,9 @@ internal class CalculatorViewModelTest { .returns(shouldShowAds) runTest { + coEvery { calculationStorage.getLastInput() } + .returns("") + coEvery { currencyDataSource.getActiveCurrencies() } .returns(currencyList) @@ -157,7 +157,7 @@ internal class CalculatorViewModelTest { every { calculationStorage.currentBase } .returns(currency1.code) - every { calculationStorage.lastInput } + coEvery { calculationStorage.getLastInput() } .returns(mock) viewModel.state.firstOrNull().let { From 26af2a18701d5a7509ffb217401213708c215f41 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Mon, 29 Jan 2024 22:39:28 +0300 Subject: [PATCH 026/129] [Oztechan/CCC#3123] Move precision to Suspend (#3124) --- .../viewmodel/widget/WidgetViewModel.kt | 4 +- .../viewmodel/widget/WidgetViewModelTest.kt | 11 ++-- .../storage/calculation/CalculationStorage.kt | 3 +- .../calculation/CalculationStorageImpl.kt | 8 ++- .../calculation/CalculationStorageTest.kt | 14 ++--- .../calculator/CalculatorViewModel.kt | 59 ++++++++++--------- .../calculator/CalculatorViewModelTest.kt | 8 +-- .../viewmodel/settings/SettingsViewModel.kt | 25 ++++---- .../settings/SettingsViewModelTest.kt | 10 ++-- 9 files changed, 81 insertions(+), 61 deletions(-) diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt index 0e749ce2d9..4a05da0448 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt @@ -66,7 +66,9 @@ class WidgetViewModel( currencyDataSource.getActiveCurrencies() .filterNot { it.code == calculationStorage.currentBase } .onEach { - it.rate = conversion.getRateFromCode(it.code)?.getFormatted(calculationStorage.precision).orEmpty() + it.rate = conversion.getRateFromCode(it.code) + ?.getFormatted(calculationStorage.getPrecision()) + .orEmpty() } .take(MAXIMUM_NUMBER_OF_CURRENCY) .let { currencyList -> diff --git a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt index 7896fa31ce..7a2dd2a433 100644 --- a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt +++ b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt @@ -87,10 +87,10 @@ internal class WidgetViewModelTest { every { calculationStorage.currentBase } .returns(base) - every { calculationStorage.precision } - .returns(3) - runTest { + coEvery { calculationStorage.getPrecision() } + .returns(3) + coEvery { backendApiService.getConversion(base) } .returns(conversion) @@ -203,7 +203,10 @@ internal class WidgetViewModelTest { it.currencyList.forEach { currency -> conversion.getRateFromCode(currency.code).let { rate -> assertNotNull(rate) - assertEquals(rate.getFormatted(calculationStorage.precision), currency.rate) + assertEquals( + rate.getFormatted(calculationStorage.getPrecision()), + currency.rate + ) } } } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt index 1c70e72208..6596ca39f2 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt @@ -3,7 +3,8 @@ package com.oztechan.ccc.client.storage.calculation interface CalculationStorage { var currentBase: String - var precision: Int + suspend fun getPrecision(): Int + suspend fun setPrecision(value: Int) suspend fun getLastInput(): String suspend fun setLastInput(value: String) diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index ef220532a3..857a2a15c5 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -12,9 +12,11 @@ class CalculationStorageImpl( get() = persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) set(value) = persistence.setValue(KEY_CURRENT_BASE, value) - override var precision: Int - get() = persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) - set(value) = persistence.setValue(KEY_PRECISION, value) + override suspend fun getPrecision(): Int = + suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) + + override suspend fun setPrecision(value: Int) = + suspendPersistence.setSuspend(KEY_PRECISION, value) override suspend fun getLastInput(): String = suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) diff --git a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt index 8df4c26b7b..cccae43119 100644 --- a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt +++ b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt @@ -45,13 +45,13 @@ internal class CalculationStorageTest { } @Test - fun `default precision`() { - every { persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) } + fun `default precision`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } .returns(DEFAULT_PRECISION) - assertEquals(DEFAULT_PRECISION, subject.precision) + assertEquals(DEFAULT_PRECISION, subject.getPrecision()) - verify { persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) } + coVerify { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } .wasInvoked() } @@ -77,11 +77,11 @@ internal class CalculationStorageTest { } @Test - fun `set precision`() { + fun `set precision`() = runTest { val mockValue = Random.nextInt() - subject.precision = mockValue + subject.setPrecision(mockValue) - verify { persistence.setValue(KEY_PRECISION, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_PRECISION, mockValue) } .wasInvoked() } diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 605a77dc4f..a16ff8b07b 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -53,7 +53,8 @@ class CalculatorViewModel( private val conversionDataSource: ConversionDataSource, adControlRepository: AdControlRepository, private val analyticsManager: AnalyticsManager -) : BaseSEEDViewModel(), CalculatorEvent { +) : BaseSEEDViewModel(), + CalculatorEvent { // region SEED private val _state = MutableStateFlow(CalculatorState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) @@ -108,7 +109,7 @@ class CalculatorViewModel( } .launchIn(viewModelScope) - private fun updateConversion() { + private suspend fun updateConversion() { _state.update { copy(loading = true) } data.conversion?.let { @@ -120,15 +121,15 @@ class CalculatorViewModel( } } - private fun updateConversionSuccess(conversion: Conversion) = conversion.copy(date = nowAsDateString()) - .let { - data.conversion = it - calculateConversions(it, ConversionState.Online(it.date)) - - viewModelScope.launch { - conversionDataSource.insertConversion(it) + private fun updateConversionSuccess(conversion: Conversion) = + conversion.copy(date = nowAsDateString()) + .let { + data.conversion = it + viewModelScope.launch { + calculateConversions(it, ConversionState.Online(it.date)) + conversionDataSource.insertConversion(it) + } } - } private fun updateConversionFailed(t: Throwable) = viewModelScope.launchIgnored { Logger.w(t) { "CalculatorViewModel updateConversionFailed" } @@ -145,11 +146,14 @@ class CalculatorViewModel( } } - private fun calculateConversions(conversion: Conversion?, conversionState: ConversionState) = _state.update { + private suspend fun calculateConversions( + conversion: Conversion?, + conversionState: ConversionState + ) = _state.update { copy( currencyList = _state.value.currencyList.onEach { it.rate = conversion.calculateRate(it.code, _state.value.output) - .getFormatted(calculationStorage.precision) + .getFormatted(calculationStorage.getPrecision()) .toStandardDigits() }, conversionState = conversionState, @@ -160,7 +164,7 @@ class CalculatorViewModel( private fun calculateOutput(input: String) = viewModelScope.launch { val output = data.parser .calculate(input.toSupportedCharacters(), MAXIMUM_FLOATING_POINT) - .mapTo { if (isFinite()) getFormatted(calculationStorage.precision) else "" } + .mapTo { if (isFinite()) getFormatted(calculationStorage.getPrecision()) else "" } _state.update { copy(output = output) } @@ -184,22 +188,23 @@ class CalculatorViewModel( } } - private fun currentBaseChanged(newBase: String, shouldTrack: Boolean = false) = viewModelScope.launchIgnored { - data.conversion = null - calculationStorage.currentBase = newBase - _state.update { - copy( - base = newBase, - input = _state.value.input, - symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() - ) - } + private fun currentBaseChanged(newBase: String, shouldTrack: Boolean = false) = + viewModelScope.launchIgnored { + data.conversion = null + calculationStorage.currentBase = newBase + _state.update { + copy( + base = newBase, + input = _state.value.input, + symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() + ) + } - if (shouldTrack) { - analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) - analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) + if (shouldTrack) { + analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) + analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) + } } - } // region Event override fun onKeyPress(key: String) { diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index 5964ad2675..326c143756 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -101,9 +101,6 @@ internal class CalculatorViewModelTest { every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) - every { calculationStorage.precision } - .returns(3) - every { adControlRepository.shouldShowBannerAd() } .returns(shouldShowAds) @@ -111,6 +108,9 @@ internal class CalculatorViewModelTest { coEvery { calculationStorage.getLastInput() } .returns("") + coEvery { calculationStorage.getPrecision() } + .returns(3) + coEvery { currencyDataSource.getActiveCurrencies() } .returns(currencyList) @@ -182,7 +182,7 @@ internal class CalculatorViewModelTest { val result = currencyList.onEach { currency -> currency.rate = conversion.calculateRate(currency.code, it.output) - .getFormatted(calculationStorage.precision) + .getFormatted(calculationStorage.getPrecision()) .toStandardDigits() } diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index 0095ad5d6e..2a03b6adce 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch @Suppress("TooManyFunctions", "LongParameterList") class SettingsViewModel( @@ -57,13 +58,15 @@ class SettingsViewModel( // endregion init { - _state.update { - copy( - appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.appTheme), - premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), - precision = calculationStorage.precision, - version = appConfigRepository.getVersion() - ) + viewModelScope.launch { + _state.update { + copy( + appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.appTheme), + premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), + precision = calculationStorage.getPrecision(), + version = appConfigRepository.getVersion() + ) + } } currencyDataSource.getActiveCurrenciesFlow() @@ -167,10 +170,12 @@ class SettingsViewModel( _effect.emit(SettingsEffect.SelectPrecision) } - override fun onPrecisionSelect(index: Int) { + override fun onPrecisionSelect(index: Int) = viewModelScope.launchIgnored { Logger.d { "SettingsViewModel onPrecisionSelect $index" } - calculationStorage.precision = index.indexToNumber() - _state.update { copy(precision = index.indexToNumber()) } + index.indexToNumber().let { + calculationStorage.setPrecision(it) + _state.update { copy(precision = it) } + } } override fun onThemeChange(theme: AppTheme) = viewModelScope.launchIgnored { diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index 9025da99be..e1de64f52a 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -121,9 +121,6 @@ internal class SettingsViewModelTest { every { appStorage.premiumEndDate } .returns(0) - every { calculationStorage.precision } - .returns(mockedPrecision) - every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) @@ -138,6 +135,11 @@ internal class SettingsViewModelTest { every { appConfigRepository.getVersion() } .returns(version) + + runTest { + coEvery { calculationStorage.getPrecision() } + .returns(mockedPrecision) + } } // init @@ -389,7 +391,7 @@ internal class SettingsViewModelTest { println("-----") - verify { calculationStorage.precision = value.indexToNumber() } + coVerify { calculationStorage.setPrecision(value.indexToNumber()) } .wasInvoked() } } From da9aa1a8a6c2a9c5328ef832ef8aa3bd8d66d276 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 21:36:03 +0300 Subject: [PATCH 027/129] [Oztechan/Global#12] Update Git Submodules (#3126) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index ee2c619c63..f259cf97a4 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit ee2c619c63b0ac68faa82708ef62a87e0960866a +Subproject commit f259cf97a45f21bbf22e43aa5f1ba19738e8758b diff --git a/submodule/logmob b/submodule/logmob index e996eca715..b60a01d696 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit e996eca715cf3c53dc78acb9561bd39e5796036a +Subproject commit b60a01d6966a682df9cd5c34aabced38f3ce2f15 diff --git a/submodule/parsermob b/submodule/parsermob index 83474a00f4..d67c54d05f 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 83474a00f448c9b04ed3697a13cff6335c2f5701 +Subproject commit d67c54d05f208c146c44be999a095b24bf21c333 diff --git a/submodule/scopemob b/submodule/scopemob index 8e0e888df0..d583199816 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 8e0e888df0d45494d8330505bf2febb8746ab8aa +Subproject commit d5831998165a0eaab317a0d2e5e5bcc7f62d1ba6 From 7154bc2cb34252a28cb6c7128b79bb73b8c7a314 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:42:38 +0300 Subject: [PATCH 028/129] [Oztechan/Global#12] Update Git Submodules (#3129) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodule/parsermob b/submodule/parsermob index d67c54d05f..d13240e937 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit d67c54d05f208c146c44be999a095b24bf21c333 +Subproject commit d13240e937b0512d3d0fd23dcf5662fcd12a1f40 diff --git a/submodule/scopemob b/submodule/scopemob index d583199816..735dab5692 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit d5831998165a0eaab317a0d2e5e5bcc7f62d1ba6 +Subproject commit 735dab569213171280e0d48159c53754e9291e29 From de4de42f963fc846d1de47dae33cc5cc17ef0668 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 31 Jan 2024 14:51:12 +0300 Subject: [PATCH 029/129] [Oztechan/CCC#3122] Move currentBase to Suspend (#3127) * [Oztechan/CCC#3122] Move currentBase to Suspend * [Oztechan/CCC#3122] Move currentBase to Suspend --- .../android/viewmodel/widget/WidgetSEED.kt | 2 +- .../viewmodel/widget/WidgetViewModel.kt | 23 +++++++++----- .../viewmodel/widget/WidgetViewModelTest.kt | 29 +++++++++-------- .../storage/calculation/CalculationStorage.kt | 3 +- .../calculation/CalculationStorageImpl.kt | 10 +++--- .../calculation/CalculationStorageTest.kt | 25 ++++++--------- .../calculator/CalculatorViewModel.kt | 10 +++--- .../calculator/CalculatorViewModelTest.kt | 10 +++--- .../currencies/CurrenciesViewModel.kt | 10 ++++-- .../currencies/CurrenciesViewModelTest.kt | 31 ++++++++++--------- 10 files changed, 80 insertions(+), 73 deletions(-) diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt index 37c6517ce9..85296584a4 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt @@ -10,7 +10,7 @@ import com.oztechan.ccc.common.core.model.Currency data class WidgetState( var currencyList: List = listOf(), var lastUpdate: String = "", - var currentBase: String, + var currentBase: String = "", var isPremium: Boolean ) : BaseState diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt index 4a05da0448..ba3a0ad245 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt @@ -30,7 +30,6 @@ class WidgetViewModel( // region SEED private val _state = MutableStateFlow( WidgetState( - currentBase = calculationStorage.currentBase, isPremium = appStorage.premiumEndDate.isNotPassed() ) ) @@ -44,12 +43,22 @@ class WidgetViewModel( override val data = WidgetData() // endregion - private fun refreshWidgetData() { + init { + viewModelScope.launchIgnored { + _state.update { + it.copy( + currentBase = calculationStorage.getBase() + ) + } + } + } + + private suspend fun refreshWidgetData() { _state.update { it.copy( currencyList = listOf(), lastUpdate = "", - currentBase = calculationStorage.currentBase, + currentBase = calculationStorage.getBase(), isPremium = appStorage.premiumEndDate.isNotPassed() ) } @@ -61,10 +70,10 @@ class WidgetViewModel( private fun getFreshWidgetData() = viewModelScope.launch { val conversion = backendApiService - .getConversion(calculationStorage.currentBase) + .getConversion(calculationStorage.getBase()) currencyDataSource.getActiveCurrencies() - .filterNot { it.code == calculationStorage.currentBase } + .filterNot { it.code == calculationStorage.getBase() } .onEach { it.rate = conversion.getRateFromCode(it.code) ?.getFormatted(calculationStorage.getPrecision()) @@ -86,7 +95,7 @@ class WidgetViewModel( val newBaseIndex = activeCurrencies .map { it.code } - .indexOf(calculationStorage.currentBase) + .indexOf(calculationStorage.getBase()) .let { if (isToNext) { it + 1 @@ -97,7 +106,7 @@ class WidgetViewModel( (it + activeCurrencies.size) % activeCurrencies.size // it handles index -1 and index size +1 } - calculationStorage.currentBase = activeCurrencies[newBaseIndex].code + calculationStorage.setBase(activeCurrencies[newBaseIndex].code) } // region Event diff --git a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt index 7a2dd2a433..5db00af2bd 100644 --- a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt +++ b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt @@ -20,7 +20,6 @@ import io.mockative.coVerify import io.mockative.configure import io.mockative.every import io.mockative.mock -import io.mockative.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onSubscription @@ -84,13 +83,13 @@ internal class WidgetViewModelTest { every { appStorage.premiumEndDate } .returns(mockEndDate) - every { calculationStorage.currentBase } - .returns(base) - runTest { coEvery { calculationStorage.getPrecision() } .returns(3) + coEvery { calculationStorage.getBase() } + .returns(base) + coEvery { backendApiService.getConversion(base) } .returns(conversion) @@ -102,7 +101,7 @@ internal class WidgetViewModelTest { @Test fun `ArrayIndexOutOfBoundsException never thrown`() = runTest { // first currency - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(firstBase) coEvery { backendApiService.getConversion(firstBase) } @@ -119,7 +118,7 @@ internal class WidgetViewModelTest { } // middle currency - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(base) coEvery { backendApiService.getConversion(base) } @@ -136,7 +135,7 @@ internal class WidgetViewModelTest { } // last currency - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(lastBase) coEvery { backendApiService.getConversion(lastBase) } @@ -223,7 +222,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasNotInvoked() - verify { calculationStorage.currentBase = any() } + coVerify { calculationStorage.setBase(any()) } .wasNotInvoked() } @@ -239,10 +238,10 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - verify { calculationStorage.currentBase = lastBase } + coVerify { calculationStorage.setBase(lastBase) } .wasInvoked() - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(lastBase) viewModel.event.onNextClick() @@ -250,7 +249,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - verify { calculationStorage.currentBase = firstBase } + coVerify { calculationStorage.setBase(firstBase) } .wasInvoked() } @@ -265,10 +264,10 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - verify { calculationStorage.currentBase = firstBase } + coVerify { calculationStorage.setBase(firstBase) } .wasInvoked() - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(firstBase) viewModel.event.onPreviousClick() @@ -276,7 +275,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - verify { calculationStorage.currentBase = lastBase } + coVerify { calculationStorage.setBase(lastBase) } .wasInvoked() } @@ -293,7 +292,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - verify { calculationStorage.currentBase } + coVerify { calculationStorage.getBase() } .wasInvoked() } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt index 6596ca39f2..864c90c5dc 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt @@ -1,7 +1,8 @@ package com.oztechan.ccc.client.storage.calculation interface CalculationStorage { - var currentBase: String + suspend fun getBase(): String + suspend fun setBase(value: String) suspend fun getPrecision(): Int suspend fun setPrecision(value: Int) diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index 857a2a15c5..2d5203317b 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -1,16 +1,16 @@ package com.oztechan.ccc.client.storage.calculation -import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.core.persistence.SuspendPersistence class CalculationStorageImpl( - private val persistence: Persistence, private val suspendPersistence: SuspendPersistence ) : CalculationStorage { - override var currentBase - get() = persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) - set(value) = persistence.setValue(KEY_CURRENT_BASE, value) + override suspend fun getBase(): String = + suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) + + override suspend fun setBase(value: String) = + suspendPersistence.setSuspend(KEY_CURRENT_BASE, value) override suspend fun getPrecision(): Int = suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) diff --git a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt index cccae43119..03c68cb5fe 100644 --- a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt +++ b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt @@ -1,6 +1,5 @@ package com.oztechan.ccc.client.storage.calculation -import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.core.persistence.SuspendPersistence import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_CURRENT_BASE import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_LAST_INPUT @@ -12,10 +11,7 @@ import io.mockative.Mock import io.mockative.classOf import io.mockative.coEvery import io.mockative.coVerify -import io.mockative.configure -import io.mockative.every import io.mockative.mock -import io.mockative.verify import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test @@ -23,29 +19,26 @@ import kotlin.test.assertEquals internal class CalculationStorageTest { private val subject: CalculationStorage by lazy { - CalculationStorageImpl(persistence, suspendPersistence) + CalculationStorageImpl(suspendPersistence) } - @Mock - private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } - @Mock private val suspendPersistence = mock(classOf()) // defaults @Test - fun `default currentBase`() { - every { persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + fun `get default currentBase`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } .returns(DEFAULT_CURRENT_BASE) - assertEquals(DEFAULT_CURRENT_BASE, subject.currentBase) + assertEquals(DEFAULT_CURRENT_BASE, subject.getBase()) - verify { persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + coVerify { suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } .wasInvoked() } @Test - fun `default precision`() = runTest { + fun `get default precision`() = runTest { coEvery { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } .returns(DEFAULT_PRECISION) @@ -68,11 +61,11 @@ internal class CalculationStorageTest { // setters @Test - fun `set currentBase`() { + fun `set currentBase`() = runTest { val mockValue = "mock" - subject.currentBase = mockValue + subject.setBase(mockValue) - verify { persistence.setValue(KEY_CURRENT_BASE, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_CURRENT_BASE, mockValue) } .wasInvoked() } diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index a16ff8b07b..96510e7adc 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -74,7 +74,7 @@ class CalculatorViewModel( _state.update { copy( currencyList = currencyDataSource.getActiveCurrencies(), - base = calculationStorage.currentBase, + base = calculationStorage.getBase(), input = calculationStorage.getLastInput(), loading = true ) @@ -115,7 +115,7 @@ class CalculatorViewModel( data.conversion?.let { calculateConversions(it, ConversionState.Cached(it.date)) } ?: viewModelScope.launch { - runCatching { backendApiService.getConversion(calculationStorage.currentBase) } + runCatching { backendApiService.getConversion(calculationStorage.getBase()) } .onFailure(::updateConversionFailed) .onSuccess(::updateConversionSuccess) } @@ -134,7 +134,7 @@ class CalculatorViewModel( private fun updateConversionFailed(t: Throwable) = viewModelScope.launchIgnored { Logger.w(t) { "CalculatorViewModel updateConversionFailed" } conversionDataSource.getConversionByBase( - calculationStorage.currentBase + calculationStorage.getBase() )?.let { calculateConversions(it, ConversionState.Offline(it.date)) } ?: run { @@ -191,7 +191,7 @@ class CalculatorViewModel( private fun currentBaseChanged(newBase: String, shouldTrack: Boolean = false) = viewModelScope.launchIgnored { data.conversion = null - calculationStorage.currentBase = newBase + calculationStorage.setBase(newBase) _state.update { copy( base = newBase, @@ -251,7 +251,7 @@ class CalculatorViewModel( _effect.emit( CalculatorEffect.ShowConversion( currency.getConversionStringFromBase( - calculationStorage.currentBase, + calculationStorage.getBase(), data.conversion ), currency.code diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index 326c143756..b472ec8848 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -95,9 +95,6 @@ internal class CalculatorViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { calculationStorage.currentBase } - .returns(currency1.code) - every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) @@ -105,6 +102,9 @@ internal class CalculatorViewModelTest { .returns(shouldShowAds) runTest { + coEvery { calculationStorage.getBase() } + .returns(currency1.code) + coEvery { calculationStorage.getLastInput() } .returns("") @@ -154,7 +154,7 @@ internal class CalculatorViewModelTest { fun `init sets the latest base and input`() = runTest { val mock = "mock" - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(currency1.code) coEvery { calculationStorage.getLastInput() } @@ -492,7 +492,7 @@ internal class CalculatorViewModelTest { @Test fun onBaseChanged() = runTest { - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(currency1.code) coEvery { backendApiService.getConversion(currency1.code) } diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index c85b33524d..5d15805015 100755 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -66,7 +66,11 @@ class CurrenciesViewModel( currencyList.filter { it.isActive } .let { - analyticsManager.setUserProperty(UserProperty.CurrencyCount(it.count().toString())) + analyticsManager.setUserProperty( + UserProperty.CurrencyCount( + it.count().toString() + ) + ) } }.launchIn(viewModelScope) @@ -79,7 +83,7 @@ class CurrenciesViewModel( ?.whetherNot { appStorage.firstRun } ?.run { _effect.emit(CurrenciesEffect.FewCurrency) } - private suspend fun verifyCurrentBase() = calculationStorage.currentBase.either( + private suspend fun verifyCurrentBase() = calculationStorage.getBase().either( { isEmpty() }, { base -> state.value.currencyList @@ -89,7 +93,7 @@ class CurrenciesViewModel( )?.mapTo { state.value.currencyList.firstOrNull { it.isActive }?.code.orEmpty() }?.let { newBase -> - calculationStorage.currentBase = newBase + calculationStorage.setBase(newBase) analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) diff --git a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt index 924ddd4bf6..b1afb9088d 100644 --- a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt +++ b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt @@ -14,6 +14,7 @@ import com.oztechan.ccc.client.storage.calculation.CalculationStorage import com.oztechan.ccc.common.core.model.Currency import io.mockative.Mock import io.mockative.classOf +import io.mockative.coEvery import io.mockative.coVerify import io.mockative.configure import io.mockative.every @@ -92,11 +93,13 @@ internal class CurrenciesViewModelTest { every { appStorage.firstRun } .returns(false) - every { calculationStorage.currentBase } - .returns(currency1.code) - every { adControlRepository.shouldShowBannerAd() } .returns(shouldShowAds) + + runTest { + coEvery { calculationStorage.getBase() } + .returns(currency1.code) + } } // Analytics @@ -171,7 +174,7 @@ internal class CurrenciesViewModelTest { @Test fun `don't show FewCurrency effect if there is MINIMUM_ACTIVE_CURRENCY and not firstRun`() = runTest { - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns("") // in order to get ChangeBase effect, have to have an effect to finish test every { currencyDataSource.getCurrenciesFlow() } @@ -193,7 +196,7 @@ internal class CurrenciesViewModelTest { every { appStorage.firstRun } .returns(true) - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns("") // in order to get ChangeBase effect, have to have an effect to finish test every { currencyDataSource.getCurrenciesFlow() } @@ -232,7 +235,7 @@ internal class CurrenciesViewModelTest { } ) - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns("") viewModel.effect.firstOrNull().let { @@ -240,7 +243,7 @@ internal class CurrenciesViewModelTest { assertEquals(firstActiveBase, it.newBase) } - verify { calculationStorage.currentBase = firstActiveBase } + coVerify { calculationStorage.setBase(firstActiveBase) } .wasInvoked() } @@ -257,7 +260,7 @@ internal class CurrenciesViewModelTest { } ) - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns(currency1.code) // not active one viewModel.effect.firstOrNull().let { @@ -265,26 +268,24 @@ internal class CurrenciesViewModelTest { assertEquals(currency2.code, it.newBase) } - verify { calculationStorage.currentBase = currency2.code } + coVerify { calculationStorage.setBase(currency2.code) } .wasInvoked() } // Event @Test - fun updateAllCurrenciesState() { + fun updateAllCurrenciesState() = runTest { every { appStorage.firstRun } .returns(false) - every { calculationStorage.currentBase } + coEvery { calculationStorage.getBase() } .returns("EUR") val mockValue = Random.nextBoolean() viewModel.event.updateAllCurrenciesState(mockValue) - runTest { - coVerify { currencyDataSource.updateCurrencyStates(mockValue) } - .wasInvoked() - } + coVerify { currencyDataSource.updateCurrencyStates(mockValue) } + .wasInvoked() } @Test From 00c10be7379d3af24f7592f8bccbad4c40c2fd2e Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 31 Jan 2024 18:29:40 +0300 Subject: [PATCH 030/129] [Oztechan/CCC#3128] Move appTheme to Suspend (#3135) --- .../ui/mobile/content/main/MainActivity.kt | 9 ++-- .../storage/app/client-storage-app.gradle.kts | 1 + .../ccc/client/storage/app/AppStorage.kt | 3 +- .../ccc/client/storage/app/AppStorageImpl.kt | 12 ++++-- .../ccc/client/storage/app/AppStorageTest.kt | 23 +++++++---- .../ccc/client/viewmodel/main/MainSEED.kt | 2 +- .../client/viewmodel/main/MainViewModel.kt | 41 +++++++++++-------- .../viewmodel/main/MainViewModelTest.kt | 22 ++++++---- .../viewmodel/settings/SettingsViewModel.kt | 4 +- .../settings/SettingsViewModelTest.kt | 8 ++-- 10 files changed, 76 insertions(+), 49 deletions(-) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt index 37acae6c32..071009ec02 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger import com.github.submob.basemob.activity.BaseActivity +import com.github.submob.scopemob.whether import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.ui.mobile.BuildConfig import com.oztechan.ccc.android.ui.mobile.R @@ -57,9 +58,11 @@ class MainActivity : BaseActivity() { setDestination(if (shouldOnboardUser) R.id.sliderFragment else R.id.calculatorFragment) // if dark mode is supported use theming according to user preference - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - AppCompatDelegate.setDefaultNightMode(getThemeMode(it.appTheme)) - } + it.appTheme + ?.whether { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q } + ?.let { appTheme -> + AppCompatDelegate.setDefaultNightMode(getThemeMode(appTheme)) + } } }.launchIn(lifecycleScope) diff --git a/client/storage/app/client-storage-app.gradle.kts b/client/storage/app/client-storage-app.gradle.kts index 5b4d634d95..3d1aab333e 100644 --- a/client/storage/app/client-storage-app.gradle.kts +++ b/client/storage/app/client-storage-app.gradle.kts @@ -20,6 +20,7 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) + implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt index cdb333376c..e3a9f83f22 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt @@ -3,7 +3,8 @@ package com.oztechan.ccc.client.storage.app interface AppStorage { var firstRun: Boolean - var appTheme: Int + suspend fun getAppTheme(): Int + suspend fun setAppTheme(value: Int) var premiumEndDate: Long diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt index 84d334c55e..e382a77128 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt @@ -1,18 +1,22 @@ package com.oztechan.ccc.client.storage.app import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.SuspendPersistence internal class AppStorageImpl( - private val persistence: Persistence + private val persistence: Persistence, + private val suspendPersistence: SuspendPersistence ) : AppStorage { override var firstRun get() = persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) set(value) = persistence.setValue(KEY_FIRST_RUN, value) - override var appTheme - get() = persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) - set(value) = persistence.setValue(KEY_APP_THEME, value) + override suspend fun getAppTheme(): Int = + suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) + + override suspend fun setAppTheme(value: Int) = + suspendPersistence.setSuspend(KEY_APP_THEME, value) override var premiumEndDate get() = persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) diff --git a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt index 724b462ba0..30a00cf6b7 100644 --- a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt +++ b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt @@ -1,6 +1,7 @@ package com.oztechan.ccc.client.storage.app import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.SuspendPersistence import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_APP_THEME import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_FIRST_RUN import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_PREMIUM_END_DATE @@ -11,10 +12,13 @@ import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.KEY_PREMIUM_ import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.KEY_SESSION_COUNT import io.mockative.Mock import io.mockative.classOf +import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.configure import io.mockative.every import io.mockative.mock import io.mockative.verify +import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -22,12 +26,15 @@ import kotlin.test.assertEquals internal class AppStorageTest { private val subject: AppStorage by lazy { - AppStorageImpl(persistence) + AppStorageImpl(persistence, suspendPersistence) } @Mock private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } + @Mock + private val suspendPersistence = mock(classOf()) + // defaults @Test fun `default firstRun`() { @@ -41,13 +48,13 @@ internal class AppStorageTest { } @Test - fun `default appTheme`() { - every { persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) } + fun `get default appTheme`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) } .returns(DEFAULT_APP_THEME) - assertEquals(DEFAULT_APP_THEME, subject.appTheme) + assertEquals(DEFAULT_APP_THEME, subject.getAppTheme()) - verify { persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) } + coVerify { suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) } .wasInvoked() } @@ -84,11 +91,11 @@ internal class AppStorageTest { } @Test - fun `set appTheme`() { + fun `set appTheme`() = runTest { val mockValue = Random.nextInt() - subject.appTheme = mockValue + subject.setAppTheme(mockValue) - verify { persistence.setValue(KEY_APP_THEME, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_APP_THEME, mockValue) } .wasInvoked() } diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt index b3ae057a6a..8d19973820 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.Job // State data class MainState( var shouldOnboardUser: Boolean, - var appTheme: Int + var appTheme: Int? = null ) : BaseState // Effect diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt index 3aa79de6a7..dd5dff6e32 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt @@ -11,6 +11,7 @@ import com.oztechan.ccc.client.core.analytics.model.UserProperty import com.oztechan.ccc.client.core.shared.model.AppTheme import com.oztechan.ccc.client.core.shared.util.isNotPassed import com.oztechan.ccc.client.core.viewmodel.BaseSEEDViewModel +import com.oztechan.ccc.client.core.viewmodel.util.launchIgnored import com.oztechan.ccc.client.core.viewmodel.util.update import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository import com.oztechan.ccc.client.repository.appconfig.AppConfigRepository @@ -35,8 +36,7 @@ class MainViewModel( // region SEED private val _state = MutableStateFlow( MainState( - shouldOnboardUser = appStorage.firstRun, - appTheme = appStorage.appTheme + shouldOnboardUser = appStorage.firstRun ) ) override val state: StateFlow = _state.asStateFlow() @@ -50,22 +50,29 @@ class MainViewModel( // endregion init { - with(analyticsManager) { - setUserProperty( - UserProperty.IsPremium( - appStorage.premiumEndDate.isNotPassed().toString() + viewModelScope.launch { + _state.update { + copy( + appTheme = appStorage.getAppTheme() ) - ) - setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) - setUserProperty( - UserProperty.AppTheme( - AppTheme.getAnalyticsThemeName( - appStorage.appTheme, - appConfigRepository.getDeviceType() + } + with(analyticsManager) { + setUserProperty( + UserProperty.IsPremium( + appStorage.premiumEndDate.isNotPassed().toString() ) ) - ) - setUserProperty(UserProperty.DevicePlatform(appConfigRepository.getDeviceType().name)) + setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) + setUserProperty( + UserProperty.AppTheme( + AppTheme.getAnalyticsThemeName( + appStorage.getAppTheme(), + appConfigRepository.getDeviceType() + ) + ) + ) + setUserProperty(UserProperty.DevicePlatform(appConfigRepository.getDeviceType().name)) + } } } @@ -121,13 +128,13 @@ class MainViewModel( data.adVisibility = false } - override fun onResume() { + override fun onResume() = viewModelScope.launchIgnored { Logger.d { "MainViewModel onResume" } _state.update { copy( shouldOnboardUser = appStorage.firstRun, - appTheme = appStorage.appTheme + appTheme = appStorage.getAppTheme() ) } diff --git a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt index c916d619a7..57812b2a9f 100644 --- a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt +++ b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt @@ -21,6 +21,8 @@ import com.oztechan.ccc.client.repository.appconfig.AppConfigRepository import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf +import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.configure import io.mockative.every import io.mockative.mock @@ -84,9 +86,6 @@ internal class MainViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { appStorage.appTheme } - .returns(appThemeValue) - every { appStorage.premiumEndDate } .returns(nowAsLong()) @@ -101,11 +100,16 @@ internal class MainViewModelTest { every { appStorage.firstRun } .returns(isFirstRun) + + runTest { + coEvery { appStorage.getAppTheme() } + .returns(appThemeValue) + } } // Analytics @Test - fun ifUserPropertiesSetCorrect() { + fun ifUserPropertiesSetCorrect() = runTest { viewModel // init verify { @@ -118,11 +122,11 @@ internal class MainViewModelTest { .wasInvoked() verify { analyticsManager.setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) } .wasInvoked() - verify { + coVerify { analyticsManager.setUserProperty( UserProperty.AppTheme( AppTheme.getAnalyticsThemeName( - appStorage.appTheme, + appStorage.getAppTheme(), mockDevice ) ) @@ -145,7 +149,7 @@ internal class MainViewModelTest { verify { appStorage.firstRun } .wasInvoked() - verify { appStorage.appTheme } + coVerify { appStorage.getAppTheme() } .wasInvoked() } @@ -396,7 +400,7 @@ internal class MainViewModelTest { val newAppThemeValue = appThemeValue + 10 val newIsFirstRun = isFirstRun.not() - every { appStorage.appTheme } + coEvery { appStorage.getAppTheme() } .returns(newAppThemeValue) every { appStorage.firstRun } @@ -414,7 +418,7 @@ internal class MainViewModelTest { verify { appStorage.firstRun } .wasInvoked() - verify { appStorage.appTheme } + coVerify { appStorage.getAppTheme() } .wasInvoked() } } diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index 2a03b6adce..7cd871ed71 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -61,7 +61,7 @@ class SettingsViewModel( viewModelScope.launch { _state.update { copy( - appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.appTheme), + appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.getAppTheme()), premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), precision = calculationStorage.getPrecision(), version = appConfigRepository.getVersion() @@ -181,7 +181,7 @@ class SettingsViewModel( override fun onThemeChange(theme: AppTheme) = viewModelScope.launchIgnored { Logger.d { "SettingsViewModel onThemeChange $theme" } _state.update { copy(appThemeType = theme) } - appStorage.appTheme = theme.themeValue + appStorage.setAppTheme(theme.themeValue) _effect.emit(SettingsEffect.ChangeTheme(theme.themeValue)) } // endregion diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index e1de64f52a..e7a542934a 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -115,9 +115,6 @@ internal class SettingsViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { appStorage.appTheme } - .returns(-1) - every { appStorage.premiumEndDate } .returns(0) @@ -139,6 +136,9 @@ internal class SettingsViewModelTest { runTest { coEvery { calculationStorage.getPrecision() } .returns(mockedPrecision) + + coEvery { appStorage.getAppTheme() } + .returns(-1) } } @@ -408,7 +408,7 @@ internal class SettingsViewModelTest { assertEquals(mockTheme.themeValue, it.themeValue) } - verify { appStorage.appTheme = mockTheme.themeValue } + coVerify { appStorage.setAppTheme(mockTheme.themeValue) } .wasInvoked() } } From 2ff3edf759c6e9c4c2053cd25192cf753dcb1c73 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 31 Jan 2024 22:55:20 +0300 Subject: [PATCH 031/129] [Oztechan/CCC#2942] Move firstRun to Suspend (#3137) * [Oztechan/CCC#2942] Move firstRun to Suspend * [Oztechan/CCC#2942] Move firstRun to Suspend * [Oztechan/CCC#2942] Move firstRun to Suspend * [Oztechan/CCC#2942] Move firstRun to Suspend * [Oztechan/CCC#2942] Move firstRun to Suspend * [Oztechan/CCC#2942] Move firstRun to Suspend * [Oztechan/CCC#2942] Move firstRun to Suspend --- .../ui/mobile/content/main/MainActivity.kt | 4 +- .../client-repository-adcontrol.gradle.kts | 1 + .../adcontrol/AdControlRepository.kt | 2 +- .../adcontrol/AdControlRepositoryImpl.kt | 2 +- .../adcontrol/AdControlRepositoryTest.kt | 283 +++++++++--------- .../ccc/client/storage/app/AppStorage.kt | 3 +- .../ccc/client/storage/app/AppStorageImpl.kt | 7 +- .../ccc/client/storage/app/AppStorageTest.kt | 14 +- .../viewmodel/calculator/CalculatorSEED.kt | 2 +- .../calculator/CalculatorViewModel.kt | 6 +- .../calculator/CalculatorViewModelTest.kt | 8 +- .../viewmodel/currencies/CurrenciesSEED.kt | 4 +- .../currencies/CurrenciesViewModel.kt | 20 +- .../currencies/CurrenciesViewModelTest.kt | 22 +- .../ccc/client/viewmodel/main/MainSEED.kt | 2 +- .../client/viewmodel/main/MainViewModel.kt | 11 +- .../viewmodel/main/MainViewModelTest.kt | 12 +- .../client/viewmodel/settings/SettingsSEED.kt | 2 +- .../viewmodel/settings/SettingsViewModel.kt | 6 +- .../settings/SettingsViewModelTest.kt | 8 +- .../client/viewmodel/watchers/WatchersSEED.kt | 2 +- .../viewmodel/watchers/WatchersViewModel.kt | 28 +- .../watchers/WatchersViewModelTest.kt | 8 +- ios/CCC/UI/Main/MainView.swift | 7 +- 24 files changed, 247 insertions(+), 217 deletions(-) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt index 071009ec02..48726f8ccb 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt @@ -55,7 +55,9 @@ class MainActivity : BaseActivity() { .flowWithLifecycle(lifecycle) .onEach { with(it) { - setDestination(if (shouldOnboardUser) R.id.sliderFragment else R.id.calculatorFragment) + shouldOnboardUser?.let { shouldOnboardUser -> + setDestination(if (shouldOnboardUser) R.id.sliderFragment else R.id.calculatorFragment) + } // if dark mode is supported use theming according to user preference it.appTheme diff --git a/client/repository/adcontrol/client-repository-adcontrol.gradle.kts b/client/repository/adcontrol/client-repository-adcontrol.gradle.kts index 3cddcc191c..3f265ab01d 100644 --- a/client/repository/adcontrol/client-repository-adcontrol.gradle.kts +++ b/client/repository/adcontrol/client-repository-adcontrol.gradle.kts @@ -23,6 +23,7 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) + implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt index e4957fd268..c9df7df1a9 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt @@ -1,7 +1,7 @@ package com.oztechan.ccc.client.repository.adcontrol interface AdControlRepository { - fun shouldShowBannerAd(): Boolean + suspend fun shouldShowBannerAd(): Boolean fun shouldShowInterstitialAd(): Boolean } diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt index cf4fc5c4dc..60163b466b 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt @@ -8,7 +8,7 @@ internal class AdControlRepositoryImpl( private val appStorage: AppStorage, private val adConfigService: AdConfigService ) : AdControlRepository { - override fun shouldShowBannerAd() = !appStorage.firstRun && + override suspend fun shouldShowBannerAd() = !appStorage.isFirstRun() && appStorage.premiumEndDate.isPassed() && appStorage.sessionCount > adConfigService.config.bannerAdSessionCount diff --git a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt index ed906268aa..4c2e0bfdc3 100644 --- a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt +++ b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt @@ -6,9 +6,12 @@ import com.oztechan.ccc.client.core.shared.util.nowAsLong import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf +import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify +import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.BeforeTest import kotlin.test.Test @@ -37,212 +40,220 @@ internal class AdControlRepositoryTest { } @Test - fun `shouldShowBannerAd is false when firstRun and not premiumExpired and sessionCount smaller than banner 000`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when firstRun and not premiumExpired and sessionCount smaller than banner 000`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(true) + coEvery { appStorage.isFirstRun() } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount smaller than banner 100`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount smaller than banner 100`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(false) + coEvery { appStorage.isFirstRun() } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount smaller than banner 010`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount smaller than banner 010`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(true) + coEvery { appStorage.isFirstRun() } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + not premiumExpired + sessionCount bigger than banner 001`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when firstRun + not premiumExpired + sessionCount bigger than banner 001`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(true) + coEvery { appStorage.isFirstRun() } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount bigger than banner 011`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount bigger than banner 011`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(true) + coEvery { appStorage.isFirstRun() } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount bigger than banner 101`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount bigger than banner 101`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(false) + coEvery { appStorage.isFirstRun() } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + premiumExpired + sessionCount smaller than banner 110`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when not firstRun + premiumExpired + sessionCount smaller than banner 110`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(false) + coEvery { appStorage.isFirstRun() } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { appStorage.sessionCount } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() - } + verify { adConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowBannerAd is true when not firstRun + premiumExpired + sessionCount bigger than banner 111`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is true when not firstRun + premiumExpired + sessionCount bigger than banner 111`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - every { appStorage.firstRun } - .returns(false) + coEvery { appStorage.isFirstRun() } + .returns(false) - assertTrue { subject.shouldShowBannerAd() } + assertTrue { subject.shouldShowBannerAd() } - verify { appStorage.firstRun } - .wasInvoked() + coVerify { appStorage.isFirstRun() } + .wasInvoked() - verify { appStorage.premiumEndDate } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { appStorage.sessionCount } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() - } + verify { adConfigService.config } + .wasInvoked() + } @Test fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() { diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt index e3a9f83f22..22598210bf 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt @@ -1,7 +1,8 @@ package com.oztechan.ccc.client.storage.app interface AppStorage { - var firstRun: Boolean + suspend fun isFirstRun(): Boolean + suspend fun setFirstRun(value: Boolean) suspend fun getAppTheme(): Int suspend fun setAppTheme(value: Int) diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt index e382a77128..3a085be850 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt @@ -7,10 +7,11 @@ internal class AppStorageImpl( private val persistence: Persistence, private val suspendPersistence: SuspendPersistence ) : AppStorage { + override suspend fun isFirstRun(): Boolean = + suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) - override var firstRun - get() = persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) - set(value) = persistence.setValue(KEY_FIRST_RUN, value) + override suspend fun setFirstRun(value: Boolean) = + suspendPersistence.setSuspend(KEY_FIRST_RUN, value) override suspend fun getAppTheme(): Int = suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) diff --git a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt index 30a00cf6b7..b2577f7b99 100644 --- a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt +++ b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt @@ -37,13 +37,13 @@ internal class AppStorageTest { // defaults @Test - fun `default firstRun`() { - every { persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } + fun `get default firstRun`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } .returns(DEFAULT_FIRST_RUN) - assertEquals(DEFAULT_FIRST_RUN, subject.firstRun) + assertEquals(DEFAULT_FIRST_RUN, subject.isFirstRun()) - verify { persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } + coVerify { suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } .wasInvoked() } @@ -82,11 +82,11 @@ internal class AppStorageTest { // setters @Test - fun `set firstRun`() { + fun `set firstRun`() = runTest { val mockedValue = Random.nextBoolean() - subject.firstRun = mockedValue + subject.setFirstRun(mockedValue) - verify { persistence.setValue(KEY_FIRST_RUN, mockedValue) } + coVerify { suspendPersistence.setSuspend(KEY_FIRST_RUN, mockedValue) } .wasInvoked() } diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt index 172bb59b64..0e45a3f41b 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt @@ -11,7 +11,7 @@ import com.oztechan.ccc.common.core.model.Currency // State data class CalculatorState( - val isBannerAdVisible: Boolean, + val isBannerAdVisible: Boolean = false, val input: String = "", val base: String = "", val currencyList: List = listOf(), diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 96510e7adc..9a08c46602 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -56,8 +56,7 @@ class CalculatorViewModel( ) : BaseSEEDViewModel(), CalculatorEvent { // region SEED - private val _state = - MutableStateFlow(CalculatorState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) + private val _state = MutableStateFlow(CalculatorState()) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -76,7 +75,8 @@ class CalculatorViewModel( currencyList = currencyDataSource.getActiveCurrencies(), base = calculationStorage.getBase(), input = calculationStorage.getLastInput(), - loading = true + loading = true, + isBannerAdVisible = adControlRepository.shouldShowBannerAd() ) } updateConversion() diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index b472ec8848..c1c3e1403b 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -98,10 +98,10 @@ internal class CalculatorViewModelTest { every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) - every { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) - runTest { + coEvery { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) + coEvery { calculationStorage.getBase() } .returns(currency1.code) @@ -146,7 +146,7 @@ internal class CalculatorViewModelTest { assertEquals(shouldShowAds, it.isBannerAdVisible) } - verify { adControlRepository.shouldShowBannerAd() } + coVerify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt index e6f2d24700..00dbc55711 100644 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt @@ -8,8 +8,8 @@ import com.oztechan.ccc.common.core.model.Currency // State data class CurrenciesState( - val isBannerAdVisible: Boolean, - val isOnboardingVisible: Boolean, + val isBannerAdVisible: Boolean = false, + val isOnboardingVisible: Boolean = false, val currencyList: List = listOf(), val loading: Boolean = true, val selectionVisibility: Boolean = false diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index 5d15805015..4ae616561c 100755 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch @Suppress("TooManyFunctions") class CurrenciesViewModel( @@ -38,12 +39,7 @@ class CurrenciesViewModel( ) : BaseSEEDViewModel(), CurrenciesEvent { // region SEED - private val _state = MutableStateFlow( - CurrenciesState( - isBannerAdVisible = adControlRepository.shouldShowBannerAd(), - isOnboardingVisible = appStorage.firstRun - ) - ) + private val _state = MutableStateFlow(CurrenciesState()) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -55,6 +51,14 @@ class CurrenciesViewModel( // endregion init { + viewModelScope.launch { + _state.update { + copy( + isOnboardingVisible = appStorage.isFirstRun(), + isBannerAdVisible = adControlRepository.shouldShowBannerAd() + ) + } + } currencyDataSource.getCurrenciesFlow() .onEach { currencyList -> @@ -80,7 +84,7 @@ class CurrenciesViewModel( private suspend fun verifyListSize() = data.unFilteredList .filter { it.isActive } .whether { it.size < MINIMUM_ACTIVE_CURRENCY } - ?.whetherNot { appStorage.firstRun } + ?.whetherNot { appStorage.isFirstRun() } ?.run { _effect.emit(CurrenciesEffect.FewCurrency) } private suspend fun verifyCurrentBase() = calculationStorage.getBase().either( @@ -131,7 +135,7 @@ class CurrenciesViewModel( .whether { it < MINIMUM_ACTIVE_CURRENCY } ?.let { _effect.emit(CurrenciesEffect.FewCurrency) } ?: run { - appStorage.firstRun = false + appStorage.setFirstRun(false) _state.update { copy(isOnboardingVisible = false) } filterList("") _effect.emit(CurrenciesEffect.OpenCalculator) diff --git a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt index b1afb9088d..7e14139c02 100644 --- a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt +++ b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt @@ -90,13 +90,13 @@ internal class CurrenciesViewModelTest { every { currencyDataSource.getCurrenciesFlow() } .returns(currencyListFlow) - every { appStorage.firstRun } - .returns(false) + runTest { + coEvery { appStorage.isFirstRun() } + .returns(false) - every { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) + coEvery { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) - runTest { coEvery { calculationStorage.getBase() } .returns(currency1.code) } @@ -148,10 +148,10 @@ internal class CurrenciesViewModelTest { assertEquals(shouldShowAds, it.isBannerAdVisible) } - verify { adControlRepository.shouldShowBannerAd() } + coVerify { adControlRepository.shouldShowBannerAd() } .wasInvoked() - verify { appStorage.firstRun } + coVerify { appStorage.isFirstRun() } .wasInvoked() } @@ -193,7 +193,7 @@ internal class CurrenciesViewModelTest { @Test fun `don't show FewCurrency effect if there is less than MINIMUM_ACTIVE_CURRENCY it is firstRun`() = runTest { - every { appStorage.firstRun } + coEvery { appStorage.isFirstRun() } .returns(true) coEvery { calculationStorage.getBase() } @@ -275,7 +275,7 @@ internal class CurrenciesViewModelTest { // Event @Test fun updateAllCurrenciesState() = runTest { - every { appStorage.firstRun } + coEvery { appStorage.isFirstRun() } .returns(false) coEvery { calculationStorage.getBase() } @@ -398,7 +398,7 @@ internal class CurrenciesViewModelTest { @Test fun onDoneClick() = runTest { - every { appStorage.firstRun } + coEvery { appStorage.isFirstRun() } .returns(true) // where there is single currency @@ -425,7 +425,7 @@ internal class CurrenciesViewModelTest { assertFalse { viewModel.state.value.isOnboardingVisible } - verify { appStorage.firstRun = false } + coVerify { appStorage.setFirstRun(false) } .wasInvoked() } diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt index 8d19973820..adc2a944ef 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.Job // State data class MainState( - var shouldOnboardUser: Boolean, + var shouldOnboardUser: Boolean? = null, var appTheme: Int? = null ) : BaseState diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt index dd5dff6e32..6a8cad7987 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt @@ -34,11 +34,7 @@ class MainViewModel( analyticsManager: AnalyticsManager, ) : BaseSEEDViewModel(), MainEvent { // region SEED - private val _state = MutableStateFlow( - MainState( - shouldOnboardUser = appStorage.firstRun - ) - ) + private val _state = MutableStateFlow(MainState()) override val state: StateFlow = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -53,7 +49,8 @@ class MainViewModel( viewModelScope.launch { _state.update { copy( - appTheme = appStorage.getAppTheme() + appTheme = appStorage.getAppTheme(), + shouldOnboardUser = appStorage.isFirstRun() ) } with(analyticsManager) { @@ -133,7 +130,7 @@ class MainViewModel( _state.update { copy( - shouldOnboardUser = appStorage.firstRun, + shouldOnboardUser = appStorage.isFirstRun(), appTheme = appStorage.getAppTheme() ) } diff --git a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt index 57812b2a9f..dac72473fc 100644 --- a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt +++ b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt @@ -98,10 +98,10 @@ internal class MainViewModelTest { every { adControlRepository.shouldShowInterstitialAd() } .returns(false) - every { appStorage.firstRun } - .returns(isFirstRun) - runTest { + coEvery { appStorage.isFirstRun() } + .returns(isFirstRun) + coEvery { appStorage.getAppTheme() } .returns(appThemeValue) } @@ -146,7 +146,7 @@ internal class MainViewModelTest { assertEquals(appThemeValue, it.appTheme) } - verify { appStorage.firstRun } + coVerify { appStorage.isFirstRun() } .wasInvoked() coVerify { appStorage.getAppTheme() } @@ -403,7 +403,7 @@ internal class MainViewModelTest { coEvery { appStorage.getAppTheme() } .returns(newAppThemeValue) - every { appStorage.firstRun } + coEvery { appStorage.isFirstRun() } .returns(newIsFirstRun) viewModel.state @@ -415,7 +415,7 @@ internal class MainViewModelTest { assertEquals(newAppThemeValue, it.appTheme) } - verify { appStorage.firstRun } + coVerify { appStorage.isFirstRun() } .wasInvoked() coVerify { appStorage.getAppTheme() } diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt index 67cad2f71a..3a031065e1 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt @@ -9,7 +9,7 @@ import com.oztechan.ccc.client.viewmodel.settings.model.PremiumStatus // State data class SettingsState( - val isBannerAdVisible: Boolean, + val isBannerAdVisible: Boolean = false, val activeCurrencyCount: Int = 0, val activeWatcherCount: Int = 0, val appThemeType: AppTheme = AppTheme.SYSTEM_DEFAULT, diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index 7cd871ed71..727eede8e1 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -45,8 +45,7 @@ class SettingsViewModel( private val analyticsManager: AnalyticsManager ) : BaseSEEDViewModel(), SettingsEvent { // region SEED - private val _state = - MutableStateFlow(SettingsState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) + private val _state = MutableStateFlow(SettingsState()) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -64,7 +63,8 @@ class SettingsViewModel( appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.getAppTheme()), premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), precision = calculationStorage.getPrecision(), - version = appConfigRepository.getVersion() + version = appConfigRepository.getVersion(), + isBannerAdVisible = adControlRepository.shouldShowBannerAd() ) } } diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index e7a542934a..d3de552b95 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -124,9 +124,6 @@ internal class SettingsViewModelTest { every { watcherDataSource.getWatchersFlow() } .returns(flowOf(watcherLists)) - every { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) - every { appConfigRepository.getDeviceType() } .returns(Device.IOS) @@ -134,6 +131,9 @@ internal class SettingsViewModelTest { .returns(version) runTest { + coEvery { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) + coEvery { calculationStorage.getPrecision() } .returns(mockedPrecision) @@ -155,7 +155,7 @@ internal class SettingsViewModelTest { assertEquals(shouldShowAds, it.isBannerAdVisible) } - verify { adControlRepository.shouldShowBannerAd() } + coVerify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } diff --git a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt index 02845f1d61..f5799306ed 100644 --- a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt +++ b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt @@ -7,7 +7,7 @@ import com.oztechan.ccc.client.core.viewmodel.BaseState import com.oztechan.ccc.common.core.model.Watcher data class WatchersState( - val isBannerAdVisible: Boolean, + val isBannerAdVisible: Boolean = false, val watcherList: List = emptyList() ) : BaseState diff --git a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt index 78f3998f34..7b8fc7c910 100644 --- a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt +++ b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt @@ -29,8 +29,7 @@ class WatchersViewModel( private val analyticsManager: AnalyticsManager ) : BaseSEEDViewModel(), WatchersEvent { // region SEED - private val _state = - MutableStateFlow(WatchersState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) + private val _state = MutableStateFlow(WatchersState()) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -42,6 +41,13 @@ class WatchersViewModel( // endregion init { + viewModelScope.launch { + _state.update { + copy( + isBannerAdVisible = adControlRepository.shouldShowBannerAd() + ) + } + } watcherDataSource.getWatchersFlow() .onEach { _state.update { copy(watcherList = it) } @@ -70,10 +76,11 @@ class WatchersViewModel( watcherDataSource.updateWatcherBaseById(newBase, watcher.id) } - override fun onTargetChanged(watcher: Watcher, newTarget: String) = viewModelScope.launchIgnored { - Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } - watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) - } + override fun onTargetChanged(watcher: Watcher, newTarget: String) = + viewModelScope.launchIgnored { + Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } + watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) + } override fun onAddClick() = viewModelScope.launchIgnored { Logger.d { "WatcherViewModel onAddClick" } @@ -95,10 +102,11 @@ class WatchersViewModel( watcherDataSource.deleteWatcher(watcher.id) } - override fun onRelationChange(watcher: Watcher, isGreater: Boolean) = viewModelScope.launchIgnored { - Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } - watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) - } + override fun onRelationChange(watcher: Watcher, isGreater: Boolean) = + viewModelScope.launchIgnored { + Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } + watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) + } override fun onRateChange(watcher: Watcher, rate: String): String { Logger.d { "WatcherViewModel onRateChange $watcher $rate" } diff --git a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt index 5508cec52a..8943b008d5 100644 --- a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt +++ b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt @@ -73,8 +73,10 @@ internal class WatchersViewModelTest { every { watcherDataSource.getWatchersFlow() } .returns(flowOf(watcherList)) - every { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) + runTest { + coEvery { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) + } } // init @@ -86,7 +88,7 @@ internal class WatchersViewModelTest { assertEquals(watcherList, it.watcherList) } - verify { adControlRepository.shouldShowBannerAd() } + coVerify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } diff --git a/ios/CCC/UI/Main/MainView.swift b/ios/CCC/UI/Main/MainView.swift index dbac5a7b80..955ad4a2b2 100644 --- a/ios/CCC/UI/Main/MainView.swift +++ b/ios/CCC/UI/Main/MainView.swift @@ -18,10 +18,13 @@ struct MainView: View { transitionType: .default, easing: Animation.easeInOut ) { - if state.shouldOnboardUser { + switch state.shouldOnboardUser { + case true: IntroSlideRootView() - } else { + case false: CalculatorRootView() + default: + EmptyView() } } } From bd0f4b9c0c0cdc55b351050212d4927d585256c0 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 31 Jan 2024 23:33:42 +0300 Subject: [PATCH 032/129] [Oztechan/CCC#3138] Move premiumEndDate to Suspend (#3140) --- .../android/viewmodel/widget/WidgetSEED.kt | 2 +- .../viewmodel/widget/WidgetViewModel.kt | 11 +- .../viewmodel/widget/WidgetViewModelTest.kt | 23 ++- .../adcontrol/AdControlRepository.kt | 2 +- .../adcontrol/AdControlRepositoryImpl.kt | 4 +- .../adcontrol/AdControlRepositoryTest.kt | 140 +++++++++--------- .../ccc/client/storage/app/AppStorage.kt | 3 +- .../ccc/client/storage/app/AppStorageImpl.kt | 8 +- .../ccc/client/storage/app/AppStorageTest.kt | 14 +- .../client/viewmodel/main/MainViewModel.kt | 2 +- .../viewmodel/main/MainViewModelTest.kt | 29 ++-- .../viewmodel/premium/PremiumViewModel.kt | 40 ++--- .../viewmodel/premium/PremiumViewModelTest.kt | 24 ++- .../viewmodel/settings/SettingsViewModel.kt | 4 +- .../settings/SettingsViewModelTest.kt | 18 +-- 15 files changed, 163 insertions(+), 161 deletions(-) diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt index 85296584a4..25561a7d14 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt @@ -11,7 +11,7 @@ data class WidgetState( var currencyList: List = listOf(), var lastUpdate: String = "", var currentBase: String = "", - var isPremium: Boolean + var isPremium: Boolean = true ) : BaseState // Event diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt index ba3a0ad245..f51ef727ea 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt @@ -28,11 +28,7 @@ class WidgetViewModel( ) : BaseSEEDViewModel(), WidgetEvent { // region SEED - private val _state = MutableStateFlow( - WidgetState( - isPremium = appStorage.premiumEndDate.isNotPassed() - ) - ) + private val _state = MutableStateFlow(WidgetState()) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -47,7 +43,8 @@ class WidgetViewModel( viewModelScope.launchIgnored { _state.update { it.copy( - currentBase = calculationStorage.getBase() + currentBase = calculationStorage.getBase(), + isPremium = appStorage.getPremiumEndDate().isNotPassed() ) } } @@ -59,7 +56,7 @@ class WidgetViewModel( currencyList = listOf(), lastUpdate = "", currentBase = calculationStorage.getBase(), - isPremium = appStorage.premiumEndDate.isNotPassed() + isPremium = appStorage.getPremiumEndDate().isNotPassed() ) } diff --git a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt index 5db00af2bd..b8cdf94e6e 100644 --- a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt +++ b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt @@ -18,7 +18,6 @@ import io.mockative.classOf import io.mockative.coEvery import io.mockative.coVerify import io.mockative.configure -import io.mockative.every import io.mockative.mock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull @@ -80,10 +79,10 @@ internal class WidgetViewModelTest { val mockEndDate = Random.nextLong() - every { appStorage.premiumEndDate } - .returns(mockEndDate) - runTest { + coEvery { appStorage.getPremiumEndDate() } + .returns(mockEndDate) + coEvery { calculationStorage.getPrecision() } .returns(3) @@ -157,13 +156,13 @@ internal class WidgetViewModelTest { viewModel.state.firstOrNull().let { assertNotNull(it) assertEquals(base, it.currentBase) - assertEquals(appStorage.premiumEndDate.isNotPassed(), it.isPremium) + assertEquals(appStorage.getPremiumEndDate().isNotPassed(), it.isPremium) } } @Test fun `if user is premium api call and db query are invoked`() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -177,7 +176,7 @@ internal class WidgetViewModelTest { @Test fun `if user is not premium no api call and db query are not invoked`() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -192,7 +191,7 @@ internal class WidgetViewModelTest { @Test fun `when onRefreshClick called all the conversion rates for currentBase is calculated`() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.state.onSubscription { @@ -214,7 +213,7 @@ internal class WidgetViewModelTest { @Test fun `when onRefreshClick called with null, base is not updated`() = runTest { // to not invoke getFreshWidgetData - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -230,7 +229,7 @@ internal class WidgetViewModelTest { @Test fun onNextClick() = runTest { // when onNextClick, base is updated next or the first active currency - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onNextClick() @@ -256,7 +255,7 @@ internal class WidgetViewModelTest { @Test fun onPreviousClick() = runTest { // when onRefreshClick, base is updated previous or the last active currency - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onPreviousClick() @@ -281,7 +280,7 @@ internal class WidgetViewModelTest { @Test fun onRefreshClick() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt index c9df7df1a9..4aed693e02 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt @@ -3,5 +3,5 @@ package com.oztechan.ccc.client.repository.adcontrol interface AdControlRepository { suspend fun shouldShowBannerAd(): Boolean - fun shouldShowInterstitialAd(): Boolean + suspend fun shouldShowInterstitialAd(): Boolean } diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt index 60163b466b..a1e14ffdd5 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt @@ -9,9 +9,9 @@ internal class AdControlRepositoryImpl( private val adConfigService: AdConfigService ) : AdControlRepository { override suspend fun shouldShowBannerAd() = !appStorage.isFirstRun() && - appStorage.premiumEndDate.isPassed() && + appStorage.getPremiumEndDate().isPassed() && appStorage.sessionCount > adConfigService.config.bannerAdSessionCount - override fun shouldShowInterstitialAd() = appStorage.premiumEndDate.isPassed() && + override suspend fun shouldShowInterstitialAd() = appStorage.getPremiumEndDate().isPassed() && appStorage.sessionCount > adConfigService.config.interstitialAdSessionCount } diff --git a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt index 4c2e0bfdc3..a6db43d6e3 100644 --- a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt +++ b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt @@ -45,7 +45,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -56,7 +56,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() verify { appStorage.sessionCount } @@ -72,7 +72,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -83,7 +83,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasInvoked() verify { appStorage.sessionCount } @@ -99,7 +99,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -110,7 +110,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() verify { appStorage.sessionCount } @@ -126,7 +126,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -137,7 +137,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() verify { appStorage.sessionCount } @@ -153,7 +153,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -164,7 +164,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() verify { appStorage.sessionCount } @@ -180,7 +180,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -191,7 +191,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasInvoked() verify { appStorage.sessionCount } @@ -207,7 +207,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount - 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -218,7 +218,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasInvoked() verify { appStorage.sessionCount } @@ -234,7 +234,7 @@ internal class AdControlRepositoryTest { every { appStorage.sessionCount } .returns(mockedSessionCount + 1L) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) coEvery { appStorage.isFirstRun() } @@ -245,7 +245,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.isFirstRun() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasInvoked() verify { appStorage.sessionCount } @@ -256,82 +256,86 @@ internal class AdControlRepositoryTest { } @Test - fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount.toLong() + 1) + fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() + 1) - every { appStorage.premiumEndDate } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + coEvery { appStorage.getPremiumEndDate() } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - verify { appStorage.premiumEndDate } - .wasInvoked() + coVerify { appStorage.getPremiumEndDate() } + .wasInvoked() - verify { adConfigService.config.interstitialAdSessionCount } - .wasNotInvoked() + verify { adConfigService.config.interstitialAdSessionCount } + .wasNotInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() - } + verify { appStorage.sessionCount } + .wasNotInvoked() + } @Test - fun `shouldShowInterstitialAd returns true when session count bigger than remote and premiumExpired 11`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount.toLong() + 1) + fun `shouldShowInterstitialAd returns true when session count bigger than remote and premiumExpired 11`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() + 1) - every { appStorage.premiumEndDate } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + coEvery { appStorage.getPremiumEndDate() } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - assertTrue { subject.shouldShowInterstitialAd() } + assertTrue { subject.shouldShowInterstitialAd() } - verify { appStorage.premiumEndDate } - .wasInvoked() + coVerify { appStorage.getPremiumEndDate() } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() + verify { adConfigService.config } + .wasInvoked() - verify { appStorage.sessionCount } - .wasInvoked() - } + verify { appStorage.sessionCount } + .wasInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumExpired 00`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount.toLong() - 1) + fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumExpired 00`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() - 1) - every { appStorage.premiumEndDate } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + coEvery { appStorage.getPremiumEndDate() } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - verify { appStorage.premiumEndDate } - .wasInvoked() + coVerify { appStorage.getPremiumEndDate() } + .wasInvoked() - verify { adConfigService.config } - .wasNotInvoked() + verify { adConfigService.config } + .wasNotInvoked() - verify { appStorage.sessionCount } - .wasNotInvoked() - } + verify { appStorage.sessionCount } + .wasNotInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumNotExpired 10`() { - every { appStorage.sessionCount } - .returns(mockedSessionCount.toLong() - 1) + fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumNotExpired 10`() = + runTest { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() - 1) - every { appStorage.premiumEndDate } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + coEvery { appStorage.getPremiumEndDate() } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - verify { appStorage.premiumEndDate } - .wasInvoked() + coVerify { appStorage.getPremiumEndDate() } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() + verify { adConfigService.config } + .wasInvoked() - verify { appStorage.sessionCount } - .wasInvoked() - } + verify { appStorage.sessionCount } + .wasInvoked() + } } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt index 22598210bf..b7536715a1 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt @@ -7,7 +7,8 @@ interface AppStorage { suspend fun getAppTheme(): Int suspend fun setAppTheme(value: Int) - var premiumEndDate: Long + suspend fun getPremiumEndDate(): Long + suspend fun setPremiumEndDate(value: Long) var sessionCount: Long } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt index 3a085be850..8a9a6683c0 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt @@ -19,9 +19,11 @@ internal class AppStorageImpl( override suspend fun setAppTheme(value: Int) = suspendPersistence.setSuspend(KEY_APP_THEME, value) - override var premiumEndDate - get() = persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) - set(value) = persistence.setValue(KEY_PREMIUM_END_DATE, value) + override suspend fun getPremiumEndDate(): Long = + suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) + + override suspend fun setPremiumEndDate(value: Long) = + suspendPersistence.setSuspend(KEY_PREMIUM_END_DATE, value) override var sessionCount: Long get() = persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) diff --git a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt index b2577f7b99..2ada8a1bfa 100644 --- a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt +++ b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt @@ -59,13 +59,13 @@ internal class AppStorageTest { } @Test - fun `default premiumEndDate`() { - every { persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } + fun `get default premiumEndDate`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } .returns(DEFAULT_PREMIUM_END_DATE) - assertEquals(DEFAULT_PREMIUM_END_DATE, subject.premiumEndDate) + assertEquals(DEFAULT_PREMIUM_END_DATE, subject.getPremiumEndDate()) - verify { persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } + coVerify { suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } .wasInvoked() } @@ -100,11 +100,11 @@ internal class AppStorageTest { } @Test - fun `set premiumEndDate`() { + fun `set premiumEndDate`() = runTest { val mockValue = Random.nextLong() - subject.premiumEndDate = mockValue + subject.setPremiumEndDate(mockValue) - verify { persistence.setValue(KEY_PREMIUM_END_DATE, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_PREMIUM_END_DATE, mockValue) } .wasInvoked() } diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt index 6a8cad7987..b09d8b58f8 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt @@ -56,7 +56,7 @@ class MainViewModel( with(analyticsManager) { setUserProperty( UserProperty.IsPremium( - appStorage.premiumEndDate.isNotPassed().toString() + appStorage.getPremiumEndDate().isNotPassed().toString() ) ) setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) diff --git a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt index dac72473fc..d4a406d18a 100644 --- a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt +++ b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt @@ -86,19 +86,19 @@ internal class MainViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { appStorage.premiumEndDate } - .returns(nowAsLong()) - every { appStorage.sessionCount } .returns(1L) every { appConfigRepository.getDeviceType() } .returns(mockDevice) - every { adControlRepository.shouldShowInterstitialAd() } - .returns(false) - runTest { + coEvery { appStorage.getPremiumEndDate() } + .returns(nowAsLong()) + + coEvery { adControlRepository.shouldShowInterstitialAd() } + .returns(false) + coEvery { appStorage.isFirstRun() } .returns(isFirstRun) @@ -112,16 +112,17 @@ internal class MainViewModelTest { fun ifUserPropertiesSetCorrect() = runTest { viewModel // init - verify { + coVerify { analyticsManager.setUserProperty( UserProperty.IsPremium( - appStorage.premiumEndDate.isNotPassed().toString() + appStorage.getPremiumEndDate().isNotPassed().toString() ) ) - } - .wasInvoked() + }.wasInvoked() + verify { analyticsManager.setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) } .wasInvoked() + coVerify { analyticsManager.setUserProperty( UserProperty.AppTheme( @@ -218,13 +219,13 @@ internal class MainViewModelTest { every { appConfigRepository.checkAppUpdate(false) } .returns(null) - every { adControlRepository.shouldShowInterstitialAd() } + coEvery { adControlRepository.shouldShowInterstitialAd() } .returns(true) every { appConfigRepository.shouldShowAppReview() } .returns(true) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) viewModel.effect.onSubscription { @@ -243,10 +244,10 @@ internal class MainViewModelTest { verify { reviewConfigService.config } .wasInvoked() - verify { adControlRepository.shouldShowInterstitialAd() } + coVerify { adControlRepository.shouldShowInterstitialAd() } .wasInvoked() - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasInvoked() } diff --git a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt index 23b033f183..3812352e7e 100644 --- a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt +++ b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt @@ -44,29 +44,31 @@ class PremiumViewModel( ) = viewModelScope.launchIgnored { Logger.d { "PremiumViewModel onPremiumActivated ${adType?.data?.duration.orEmpty()}" } adType?.let { - appStorage.premiumEndDate = it.calculatePremiumEnd(startDate) + appStorage.setPremiumEndDate(it.calculatePremiumEnd(startDate)) _effect.emit(PremiumEffect.PremiumActivated(it, isRestorePurchase)) } } - override fun onRestorePurchase(oldPurchaseList: List) { - Logger.d { "PremiumViewModel onRestorePurchase" } - oldPurchaseList - .maxByOrNull { - it.type.calculatePremiumEnd(it.date) - }?.whether( - { type.calculatePremiumEnd(date).isNotPassed() }, - { date > appStorage.premiumEndDate }, - { PremiumType.getPurchaseIds().any { id -> id == type.data.id } } - )?.run { - onPremiumActivated( - adType = PremiumType.getById(type.data.id), - startDate = this.date, - isRestorePurchase = true - ) - _state.update { copy(loading = false) } - } - } + override fun onRestorePurchase(oldPurchaseList: List) = + viewModelScope.launchIgnored { + Logger.d { "PremiumViewModel onRestorePurchase" } + val premiumEndDate = appStorage.getPremiumEndDate() + oldPurchaseList + .maxByOrNull { + it.type.calculatePremiumEnd(it.date) + }?.whether( + { type.calculatePremiumEnd(date).isNotPassed() }, + { date > premiumEndDate }, + { PremiumType.getPurchaseIds().any { id -> id == type.data.id } } + )?.run { + onPremiumActivated( + adType = PremiumType.getById(type.data.id), + startDate = this.date, + isRestorePurchase = true + ) + _state.update { copy(loading = false) } + } + } override fun onAddPurchaseMethods(premiumDataList: List) { Logger.d { "PremiumViewModel onAddPurchaseMethods" } diff --git a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt index dccfb57903..0d615c37bc 100644 --- a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt +++ b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt @@ -14,10 +14,10 @@ import com.oztechan.ccc.client.viewmodel.premium.model.PremiumType import com.oztechan.ccc.client.viewmodel.premium.util.calculatePremiumEnd import io.mockative.Mock import io.mockative.classOf +import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.configure -import io.mockative.every import io.mockative.mock -import io.mockative.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onSubscription @@ -62,7 +62,7 @@ internal class PremiumViewModelTest { @Test fun onPremiumActivated() = runTest { viewModel.event.onPremiumActivated(null) - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() PremiumType.values().forEach { premiumType -> @@ -74,7 +74,7 @@ internal class PremiumViewModelTest { assertEquals(premiumType, it.premiumType) assertFalse { it.isRestorePurchase } - verify { appStorage.premiumEndDate = premiumType.calculatePremiumEnd(now) } + coVerify { appStorage.setPremiumEndDate(premiumType.calculatePremiumEnd(now)) } .wasInvoked() } } @@ -82,7 +82,7 @@ internal class PremiumViewModelTest { @Test fun onRestorePurchase() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(0) val now = nowAsLong() @@ -99,34 +99,30 @@ internal class PremiumViewModelTest { assertTrue { it.isRestorePurchase } assertFalse { viewModel.state.value.loading } - verify { appStorage.premiumEndDate = it.premiumType.calculatePremiumEnd(now) } + coVerify { appStorage.setPremiumEndDate(it.premiumType.calculatePremiumEnd(now)) } .wasInvoked() } // onRestorePurchase shouldn't do anything if all the old purchases out of dated var oldPurchase = OldPurchase(nowAsLong(), PremiumType.MONTH) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) viewModel.event.onRestorePurchase(listOf(oldPurchase)) - verify { - appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) - } + coVerify { appStorage.setPremiumEndDate(oldPurchase.type.calculatePremiumEnd(oldPurchase.date)) } .wasNotInvoked() // onRestorePurchase shouldn't do anything if the old purchase is already expired oldPurchase = OldPurchase(nowAsLong() - (32.days.inWholeMilliseconds), PremiumType.MONTH) - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(0) viewModel.event.onRestorePurchase(listOf(oldPurchase)) - verify { - appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) - } + coVerify { appStorage.setPremiumEndDate(oldPurchase.type.calculatePremiumEnd(oldPurchase.date)) } .wasNotInvoked() } diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index 727eede8e1..b6ebf556cf 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -61,7 +61,7 @@ class SettingsViewModel( _state.update { copy( appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.getAppTheme()), - premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), + premiumStatus = appStorage.getPremiumEndDate().toPremiumStatus(), precision = calculationStorage.getPrecision(), version = appConfigRepository.getVersion(), isBannerAdVisible = adControlRepository.shouldShowBannerAd() @@ -141,7 +141,7 @@ class SettingsViewModel( override fun onPremiumClick() = viewModelScope.launchIgnored { Logger.d { "SettingsViewModel onPremiumClick" } - if (appStorage.premiumEndDate.isPassed()) { + if (appStorage.getPremiumEndDate().isPassed()) { _effect.emit(SettingsEffect.Premium) } else { _effect.emit(SettingsEffect.AlreadyPremium) diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index d3de552b95..10d9bcdcae 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -115,9 +115,6 @@ internal class SettingsViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { appStorage.premiumEndDate } - .returns(0) - every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) @@ -131,6 +128,9 @@ internal class SettingsViewModelTest { .returns(version) runTest { + coEvery { appStorage.getPremiumEndDate() } + .returns(0) + coEvery { adControlRepository.shouldShowBannerAd() } .returns(shouldShowAds) @@ -161,7 +161,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is never set PremiumStatus is NeverActivated`() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(0) viewModel.state.firstOrNull().let { @@ -172,7 +172,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is passed PremiumStatus is Expired`() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.state.firstOrNull().let { @@ -183,7 +183,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is not passed PremiumStatus is Active`() = runTest { - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.state.firstOrNull().let { @@ -323,10 +323,10 @@ internal class SettingsViewModelTest { assertIs(it) } - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasInvoked() - every { appStorage.premiumEndDate } + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.effect.onSubscription { @@ -335,7 +335,7 @@ internal class SettingsViewModelTest { assertIs(it) } - verify { appStorage.premiumEndDate } + coVerify { appStorage.getPremiumEndDate() } .wasInvoked() } From e2eaa85b581c08bb8cb6756800ca8a791ae1a59e Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 1 Feb 2024 09:19:01 +0300 Subject: [PATCH 033/129] [Oztechan/CCC#3141] Move sessionCount to Suspend (#3142) --- .../adcontrol/AdControlRepositoryImpl.kt | 4 +- .../adcontrol/AdControlRepositoryTest.kt | 48 +++++----- .../client-repository-appconfig.gradle.kts | 1 + .../appconfig/AppConfigRepository.kt | 2 +- .../appconfig/AppConfigRepositoryImpl.kt | 4 +- .../appconfig/AppConfigRepositoryTest.kt | 78 ++++++++------- .../ccc/client/storage/app/AppStorage.kt | 3 +- .../ccc/client/storage/app/AppStorageImpl.kt | 10 +- .../ccc/client/storage/app/AppStorageTest.kt | 23 ++--- .../client/viewmodel/main/MainViewModel.kt | 8 +- .../viewmodel/main/MainViewModelTest.kt | 95 ++++++++++--------- 11 files changed, 141 insertions(+), 135 deletions(-) diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt index a1e14ffdd5..e102aa1365 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt @@ -10,8 +10,8 @@ internal class AdControlRepositoryImpl( ) : AdControlRepository { override suspend fun shouldShowBannerAd() = !appStorage.isFirstRun() && appStorage.getPremiumEndDate().isPassed() && - appStorage.sessionCount > adConfigService.config.bannerAdSessionCount + appStorage.getSessionCount() > adConfigService.config.bannerAdSessionCount override suspend fun shouldShowInterstitialAd() = appStorage.getPremiumEndDate().isPassed() && - appStorage.sessionCount > adConfigService.config.interstitialAdSessionCount + appStorage.getSessionCount() > adConfigService.config.interstitialAdSessionCount } diff --git a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt index a6db43d6e3..512740a6b8 100644 --- a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt +++ b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt @@ -42,7 +42,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is false when firstRun and not premiumExpired and sessionCount smaller than banner 000`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount - 1L) coEvery { appStorage.getPremiumEndDate() } @@ -59,7 +59,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() verify { adConfigService.config } @@ -69,7 +69,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount smaller than banner 100`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount - 1L) coEvery { appStorage.getPremiumEndDate() } @@ -86,7 +86,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() verify { adConfigService.config } @@ -96,7 +96,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount smaller than banner 010`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount - 1L) coEvery { appStorage.getPremiumEndDate() } @@ -113,7 +113,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() verify { adConfigService.config } @@ -123,7 +123,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is false when firstRun + not premiumExpired + sessionCount bigger than banner 001`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount + 1L) coEvery { appStorage.getPremiumEndDate() } @@ -140,7 +140,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() verify { adConfigService.config } @@ -150,7 +150,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount bigger than banner 011`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount + 1L) coEvery { appStorage.getPremiumEndDate() } @@ -167,7 +167,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasNotInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() verify { adConfigService.config } @@ -177,7 +177,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount bigger than banner 101`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount + 1L) coEvery { appStorage.getPremiumEndDate() } @@ -194,7 +194,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() verify { adConfigService.config } @@ -204,7 +204,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is false when not firstRun + premiumExpired + sessionCount smaller than banner 110`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount - 1L) coEvery { appStorage.getPremiumEndDate() } @@ -221,7 +221,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasInvoked() verify { adConfigService.config } @@ -231,7 +231,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowBannerAd is true when not firstRun + premiumExpired + sessionCount bigger than banner 111`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount + 1L) coEvery { appStorage.getPremiumEndDate() } @@ -248,7 +248,7 @@ internal class AdControlRepositoryTest { coVerify { appStorage.getPremiumEndDate() } .wasInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasInvoked() verify { adConfigService.config } @@ -258,7 +258,7 @@ internal class AdControlRepositoryTest { @Test fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount.toLong() + 1) coEvery { appStorage.getPremiumEndDate() } @@ -272,14 +272,14 @@ internal class AdControlRepositoryTest { verify { adConfigService.config.interstitialAdSessionCount } .wasNotInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() } @Test fun `shouldShowInterstitialAd returns true when session count bigger than remote and premiumExpired 11`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount.toLong() + 1) coEvery { appStorage.getPremiumEndDate() } @@ -293,14 +293,14 @@ internal class AdControlRepositoryTest { verify { adConfigService.config } .wasInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasInvoked() } @Test fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumExpired 00`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount.toLong() - 1) coEvery { appStorage.getPremiumEndDate() } @@ -314,14 +314,14 @@ internal class AdControlRepositoryTest { verify { adConfigService.config } .wasNotInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasNotInvoked() } @Test fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumNotExpired 10`() = runTest { - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockedSessionCount.toLong() - 1) coEvery { appStorage.getPremiumEndDate() } @@ -335,7 +335,7 @@ internal class AdControlRepositoryTest { verify { adConfigService.config } .wasInvoked() - verify { appStorage.sessionCount } + coVerify { appStorage.getSessionCount() } .wasInvoked() } } diff --git a/client/repository/appconfig/client-repository-appconfig.gradle.kts b/client/repository/appconfig/client-repository-appconfig.gradle.kts index b4f49c5b0a..46eedb0a02 100644 --- a/client/repository/appconfig/client-repository-appconfig.gradle.kts +++ b/client/repository/appconfig/client-repository-appconfig.gradle.kts @@ -33,6 +33,7 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) + implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt index 21bd936fe5..e05c5ee075 100644 --- a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt +++ b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt @@ -10,7 +10,7 @@ interface AppConfigRepository { fun checkAppUpdate(isAppUpdateShown: Boolean): Boolean? - fun shouldShowAppReview(): Boolean + suspend fun shouldShowAppReview(): Boolean fun getVersion(): String } diff --git a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt index f71b37eea8..f018586c0b 100644 --- a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt +++ b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt @@ -27,8 +27,8 @@ internal class AppConfigRepositoryImpl( it.updateForceVersion > BuildKonfig.versionCode } ?: run { null } // do not show - override fun shouldShowAppReview(): Boolean = reviewConfigService.config - .whether { appStorage.sessionCount > it.appReviewSessionCount } + override suspend fun shouldShowAppReview(): Boolean = reviewConfigService.config + .whether { appStorage.getSessionCount() > it.appReviewSessionCount } ?.mapTo { true } ?: false diff --git a/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt b/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt index b3b39abc03..a050858b72 100644 --- a/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt +++ b/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt @@ -8,9 +8,12 @@ import com.oztechan.ccc.client.core.shared.Device import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf +import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify +import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -108,61 +111,64 @@ internal class AppConfigRepositoryTest { } @Test - fun `shouldShowAppReview should return true when sessionCount is biggerThan remote sessionCount`() { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return true when sessionCount is biggerThan remote sessionCount`() = + runTest { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - every { appStorage.sessionCount } - .returns(mockInteger.toLong() + 1) + coEvery { appStorage.getSessionCount() } + .returns(mockInteger.toLong() + 1) - assertTrue { subject.shouldShowAppReview() } + assertTrue { subject.shouldShowAppReview() } - verify { appStorage.sessionCount } - .wasInvoked() + coVerify { appStorage.getSessionCount() } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowAppReview should return false when sessionCount is less than remote sessionCount`() { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return false when sessionCount is less than remote sessionCount`() = + runTest { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - every { appStorage.sessionCount } - .returns(mockInteger.toLong() - 1) + coEvery { appStorage.getSessionCount() } + .returns(mockInteger.toLong() - 1) - assertFalse { subject.shouldShowAppReview() } + assertFalse { subject.shouldShowAppReview() } - verify { appStorage.sessionCount } - .wasInvoked() + coVerify { appStorage.getSessionCount() } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowAppReview should return false when sessionCount is equal to remote sessionCount`() { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return false when sessionCount is equal to remote sessionCount`() = + runTest { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - every { appStorage.sessionCount } - .returns(mockInteger.toLong()) + coEvery { appStorage.getSessionCount() } + .returns(mockInteger.toLong()) - assertFalse { subject.shouldShowAppReview() } + assertFalse { subject.shouldShowAppReview() } - verify { appStorage.sessionCount } - .wasInvoked() + coVerify { appStorage.getSessionCount() } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test fun getVersion() { diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt index b7536715a1..34f4e1d378 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt @@ -10,5 +10,6 @@ interface AppStorage { suspend fun getPremiumEndDate(): Long suspend fun setPremiumEndDate(value: Long) - var sessionCount: Long + suspend fun getSessionCount(): Long + suspend fun setSessionCount(value: Long) } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt index 8a9a6683c0..df3a65c687 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt @@ -1,10 +1,8 @@ package com.oztechan.ccc.client.storage.app -import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.core.persistence.SuspendPersistence internal class AppStorageImpl( - private val persistence: Persistence, private val suspendPersistence: SuspendPersistence ) : AppStorage { override suspend fun isFirstRun(): Boolean = @@ -25,9 +23,11 @@ internal class AppStorageImpl( override suspend fun setPremiumEndDate(value: Long) = suspendPersistence.setSuspend(KEY_PREMIUM_END_DATE, value) - override var sessionCount: Long - get() = persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) - set(value) = persistence.setValue(KEY_SESSION_COUNT, value) + override suspend fun getSessionCount(): Long = + suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) + + override suspend fun setSessionCount(value: Long) = + suspendPersistence.setSuspend(KEY_SESSION_COUNT, value) companion object { internal const val KEY_FIRST_RUN = "firs_run" diff --git a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt index 2ada8a1bfa..76ddf4c883 100644 --- a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt +++ b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt @@ -1,6 +1,5 @@ package com.oztechan.ccc.client.storage.app -import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.core.persistence.SuspendPersistence import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_APP_THEME import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_FIRST_RUN @@ -14,10 +13,7 @@ import io.mockative.Mock import io.mockative.classOf import io.mockative.coEvery import io.mockative.coVerify -import io.mockative.configure -import io.mockative.every import io.mockative.mock -import io.mockative.verify import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test @@ -26,12 +22,9 @@ import kotlin.test.assertEquals internal class AppStorageTest { private val subject: AppStorage by lazy { - AppStorageImpl(persistence, suspendPersistence) + AppStorageImpl(suspendPersistence) } - @Mock - private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } - @Mock private val suspendPersistence = mock(classOf()) @@ -70,13 +63,13 @@ internal class AppStorageTest { } @Test - fun `default sessionCount`() { - every { persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } + fun `get default sessionCount`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } .returns(DEFAULT_SESSION_COUNT) - assertEquals(DEFAULT_SESSION_COUNT, subject.sessionCount) + assertEquals(DEFAULT_SESSION_COUNT, subject.getSessionCount()) - verify { persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } + coVerify { suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } .wasInvoked() } @@ -109,11 +102,11 @@ internal class AppStorageTest { } @Test - fun `set sessionCount`() { + fun `set sessionCount`() = runTest { val mockValue = Random.nextLong() - subject.sessionCount = mockValue + subject.setSessionCount(mockValue) - verify { persistence.setValue(KEY_SESSION_COUNT, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_SESSION_COUNT, mockValue) } .wasInvoked() } } diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt index b09d8b58f8..6dde5b9be5 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt @@ -59,7 +59,7 @@ class MainViewModel( appStorage.getPremiumEndDate().isNotPassed().toString() ) ) - setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) + setUserProperty(UserProperty.SessionCount(appStorage.getSessionCount().toString())) setUserProperty( UserProperty.AppTheme( AppTheme.getAnalyticsThemeName( @@ -88,9 +88,9 @@ class MainViewModel( } } - private fun adjustSessionCount() { + private suspend fun adjustSessionCount() { if (data.isNewSession) { - appStorage.sessionCount++ + appStorage.setSessionCount(appStorage.getSessionCount() + 1) data.isNewSession = false } } @@ -109,7 +109,7 @@ class MainViewModel( } } - private fun checkReview() { + private suspend fun checkReview() { if (appConfigRepository.shouldShowAppReview()) { viewModelScope.launch { delay(reviewConfigService.config.appReviewDialogDelay) diff --git a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt index d4a406d18a..ec161cfbc4 100644 --- a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt +++ b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt @@ -86,13 +86,13 @@ internal class MainViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { appStorage.sessionCount } - .returns(1L) - every { appConfigRepository.getDeviceType() } .returns(mockDevice) runTest { + coEvery { appStorage.getSessionCount() } + .returns(1L) + coEvery { appStorage.getPremiumEndDate() } .returns(nowAsLong()) @@ -120,7 +120,13 @@ internal class MainViewModelTest { ) }.wasInvoked() - verify { analyticsManager.setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) } + coVerify { + analyticsManager.setUserProperty( + UserProperty.SessionCount( + appStorage.getSessionCount().toString() + ) + ) + } .wasInvoked() coVerify { @@ -163,7 +169,7 @@ internal class MainViewModelTest { } @Test - fun `onResume adjustSessionCount`() = with(viewModel) { + fun `onResume adjustSessionCount`() = runTest { val mockSessionCount = Random.nextLong() every { reviewConfigService.config } @@ -172,7 +178,7 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } @@ -181,26 +187,26 @@ internal class MainViewModelTest { every { appConfigRepository.checkAppUpdate(true) } .returns(false) - every { appConfigRepository.shouldShowAppReview() } + coEvery { appConfigRepository.shouldShowAppReview() } .returns(true) every { appConfigRepository.getMarketLink() } .returns("") - assertTrue { data.isNewSession } + assertTrue { viewModel.data.isNewSession } - event.onResume() + viewModel.event.onResume() - verify { appStorage.sessionCount = mockSessionCount + 1 } + coVerify { appStorage.setSessionCount(mockSessionCount + 1) } .wasInvoked() - assertFalse { data.isNewSession } + assertFalse { viewModel.data.isNewSession } - event.onResume() + viewModel.event.onResume() - verify { appStorage.sessionCount = mockSessionCount + 1 } + coVerify { appStorage.setSessionCount(mockSessionCount + 1) } .wasNotInvoked() - assertFalse { data.isNewSession } + assertFalse { viewModel.data.isNewSession } } @Test @@ -213,7 +219,7 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } @@ -222,7 +228,7 @@ internal class MainViewModelTest { coEvery { adControlRepository.shouldShowInterstitialAd() } .returns(true) - every { appConfigRepository.shouldShowAppReview() } + coEvery { appConfigRepository.shouldShowAppReview() } .returns(true) coEvery { appStorage.getPremiumEndDate() } @@ -252,32 +258,31 @@ internal class MainViewModelTest { } @Test - fun `onResume checkAppUpdate nothing happens when check update returns null`() = - with(viewModel) { - val mockSessionCount = Random.nextLong() + fun `onResume checkAppUpdate nothing happens when check update returns null`() = runTest { + val mockSessionCount = Random.nextLong() - every { reviewConfigService.config } - .returns(ReviewConfig(0, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(0, 0L)) - every { adConfigService.config } - .returns(AdConfig(0, 0, 0L, 0L)) + every { adConfigService.config } + .returns(AdConfig(0, 0, 0L, 0L)) - every { appStorage.sessionCount } - .returns(mockSessionCount) + coEvery { appStorage.getSessionCount() } + .returns(mockSessionCount) - every { appConfigRepository.checkAppUpdate(false) } - .returns(null) + every { appConfigRepository.checkAppUpdate(false) } + .returns(null) - every { appConfigRepository.shouldShowAppReview() } - .returns(true) + coEvery { appConfigRepository.shouldShowAppReview() } + .returns(true) - event.onResume() + viewModel.event.onResume() - assertFalse { data.isAppUpdateShown } + assertFalse { viewModel.data.isAppUpdateShown } - verify { appConfigRepository.checkAppUpdate(false) } - .wasInvoked() - } + verify { appConfigRepository.checkAppUpdate(false) } + .wasInvoked() + } @Test fun `onResume checkAppUpdate app review should ask when check update returns not null`() = @@ -285,7 +290,7 @@ internal class MainViewModelTest { val mockSessionCount = Random.nextLong() val mockBoolean = Random.nextBoolean() - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockSessionCount) every { adConfigService.config } @@ -297,7 +302,7 @@ internal class MainViewModelTest { every { reviewConfigService.config } .returns(ReviewConfig(0, 0L)) - every { appConfigRepository.shouldShowAppReview() } + coEvery { appConfigRepository.shouldShowAppReview() } .returns(true) every { appConfigRepository.getMarketLink() } @@ -329,13 +334,13 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - every { appConfigRepository.shouldShowAppReview() } + coEvery { appConfigRepository.shouldShowAppReview() } .returns(true) viewModel.effect.onSubscription { @@ -344,7 +349,7 @@ internal class MainViewModelTest { assertIs(it) } - verify { appConfigRepository.shouldShowAppReview() } + coVerify { appConfigRepository.shouldShowAppReview() } .wasInvoked() verify { reviewConfigService.config } @@ -353,7 +358,7 @@ internal class MainViewModelTest { @Test fun `onResume checkReview should do nothing when shouldShowAppReview returns false`() = - with(viewModel) { + runTest { val mockSessionCount = Random.nextLong() every { reviewConfigService.config } @@ -362,18 +367,18 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - every { appStorage.sessionCount } + coEvery { appStorage.getSessionCount() } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - every { appConfigRepository.shouldShowAppReview() } + coEvery { appConfigRepository.shouldShowAppReview() } .returns(false) - onResume() + viewModel.onResume() - verify { appConfigRepository.shouldShowAppReview() } + coVerify { appConfigRepository.shouldShowAppReview() } .wasInvoked() } @@ -382,7 +387,7 @@ internal class MainViewModelTest { every { appConfigRepository.checkAppUpdate(false) } .returns(false) - every { appConfigRepository.shouldShowAppReview() } + coEvery { appConfigRepository.shouldShowAppReview() } .returns(true) every { adConfigService.config } From 6aace2d8a28f8030ce384450655ad8e18b74deac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:20:28 +0300 Subject: [PATCH 034/129] [Oztechan/Global#12] Update andymckay/cancel-action action to v0.4 (#3131) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b8b794e21..7f83e3046b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -105,7 +105,7 @@ jobs: - name: Cancel other jobs if this fails if: failure() - uses: andymckay/cancel-action@0.3 + uses: andymckay/cancel-action@0.4 - name: Set Job Status id: status @@ -205,7 +205,7 @@ jobs: - name: Cancel other jobs if this fails if: failure() - uses: andymckay/cancel-action@0.3 + uses: andymckay/cancel-action@0.4 - name: Set Job Status id: status @@ -274,7 +274,7 @@ jobs: - name: Cancel other jobs if this fails if: failure() - uses: andymckay/cancel-action@0.3 + uses: andymckay/cancel-action@0.4 - name: Set Job Status id: status @@ -359,7 +359,7 @@ jobs: - name: Cancel other jobs if this fails if: failure() - uses: andymckay/cancel-action@0.3 + uses: andymckay/cancel-action@0.4 - name: Set Job Status id: status From 5f4251ea3648956dde331d9678972c157edf96f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:21:30 +0300 Subject: [PATCH 035/129] [Oztechan/Global#12] Lock file maintenance (#3125) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 2e5b561c04..a29c6d454d 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -9,16 +9,16 @@ GEM atomos (0.1.3) aws-eventstream (1.3.0) aws-partitions (1.883.0) - aws-sdk-core (3.190.3) + aws-sdk-core (3.191.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.76.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.142.0) - aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -111,7 +111,7 @@ GEM gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.55.0) google-apis-core (>= 0.12.0, < 2.a) - google-apis-core (0.12.0) + google-apis-core (0.13.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 1.9) httpclient (>= 2.8.1, < 3.a) @@ -127,23 +127,24 @@ GEM google-apis-core (>= 0.12.0, < 2.a) google-apis-playcustomapp_v1 (0.14.0) google-apis-core (>= 0.12.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.33.0) + google-apis-core (>= 0.12.0, < 2.a) google-cloud-core (1.6.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (2.1.0) faraday (>= 1.0, < 3.a) google-cloud-errors (1.3.1) - google-cloud-storage (1.47.0) + google-cloud-storage (1.48.1) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) + google-apis-core (~> 0.13) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (~> 0.33) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.9.1) + googleauth (1.9.2) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) @@ -200,7 +201,7 @@ GEM xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) From c59e1a5c575377141ed7b27ba461d76ea7578a49 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 1 Feb 2024 20:39:39 +0300 Subject: [PATCH 036/129] [Oztechan/CCC#3144] Remove Persistence and use DI dispatchers in Coroutine based Persistences (#3145) * [Oztechan/CCC#3144] Remove Persistence and use DI dispatchers in Coroutine based Persistences * [Oztechan/CCC#3144] Remove Persistence and use DI dispatchers in Coroutine based Persistences --- .../client-core-persistence.gradle.kts | 1 + .../di/ClientCorePersistenceModule.android.kt | 24 ++--- .../client/core/persistence/Persistence.kt | 6 -- .../core/persistence/PersistenceImpl.kt | 26 ------ .../core/persistence/PersistenceTest.kt | 93 ------------------- .../di/ClientCorePersistenceModule.ios.kt | 23 ++--- 6 files changed, 17 insertions(+), 156 deletions(-) delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt delete mode 100644 client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt diff --git a/client/core/persistence/client-core-persistence.gradle.kts b/client/core/persistence/client-core-persistence.gradle.kts index e9fb57e403..2aaadca6ba 100644 --- a/client/core/persistence/client-core-persistence.gradle.kts +++ b/client/core/persistence/client-core-persistence.gradle.kts @@ -16,6 +16,7 @@ kotlin { sourceSets { commonMain.dependencies { + implementation(project(Modules.Common.Core.infrastructure)) libs.common.apply { implementation(koinCore) implementation(coroutines) diff --git a/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt b/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt index 8267a5748c..2ef23b2967 100644 --- a/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt +++ b/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt @@ -4,19 +4,15 @@ import android.content.Context import android.content.SharedPreferences import com.oztechan.ccc.client.core.persistence.FlowPersistence import com.oztechan.ccc.client.core.persistence.FlowPersistenceImpl -import com.oztechan.ccc.client.core.persistence.Persistence -import com.oztechan.ccc.client.core.persistence.PersistenceImpl import com.oztechan.ccc.client.core.persistence.SuspendPersistence import com.oztechan.ccc.client.core.persistence.SuspendPersistenceImpl +import com.oztechan.ccc.common.core.infrastructure.di.DISPATCHER_IO import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings -import com.russhwolf.settings.coroutines.FlowSettings -import com.russhwolf.settings.coroutines.SuspendSettings import com.russhwolf.settings.coroutines.toFlowSettings import com.russhwolf.settings.coroutines.toSuspendSettings -import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named import org.koin.dsl.module private const val KEY_APPLICATION_PREFERENCES = "application_preferences" @@ -25,17 +21,13 @@ actual val clientCorePersistenceModule = module { singleOf(::provideSharedPreferences) single { SharedPreferencesSettings(get()) } - single { get() } - @Suppress("OPT_IN_USAGE") - single { get().toSuspendSettings() } - @Suppress("OPT_IN_USAGE") - single { get().toFlowSettings() } - singleOf(::PersistenceImpl) { bind() } - @Suppress("OPT_IN_USAGE") - singleOf(::FlowPersistenceImpl) { bind() } - @Suppress("OPT_IN_USAGE") - singleOf(::SuspendPersistenceImpl) { bind() } + single { + FlowPersistenceImpl(get().toFlowSettings(get(named(DISPATCHER_IO)))) + } + single { + SuspendPersistenceImpl(get().toSuspendSettings(get(named(DISPATCHER_IO)))) + } } private fun provideSharedPreferences( diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt deleted file mode 100644 index 77002313a1..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -interface Persistence { - fun getValue(key: String, defaultValue: T): T - fun setValue(key: String, value: T) -} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt deleted file mode 100644 index f5f6216942..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.russhwolf.settings.Settings - -internal class PersistenceImpl(private val settings: Settings) : Persistence { - - @Suppress("UNCHECKED_CAST") - override fun getValue(key: String, defaultValue: T): T = when (defaultValue) { - is Long -> settings.getLong(key, defaultValue) as T - is String -> settings.getString(key, defaultValue) as T - is Int -> settings.getInt(key, defaultValue) as T - is Boolean -> settings.getBoolean(key, defaultValue) as T - is Float -> settings.getFloat(key, defaultValue) as T - else -> throw UnsupportedPersistenceException() - } - - override fun setValue(key: String, value: T) = when (value) { - is Long -> settings.putLong(key, value) - is String -> settings.putString(key, value) - is Int -> settings.putInt(key, value) - is Boolean -> settings.putBoolean(key, value) - is Float -> settings.putFloat(key, value) - else -> throw UnsupportedPersistenceException() - } -} diff --git a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt deleted file mode 100644 index 0c7ca6ec93..0000000000 --- a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.KEY -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockBoolean -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockFloat -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockInt -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockLong -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockString -import com.russhwolf.settings.Settings -import io.mockative.Mock -import io.mockative.classOf -import io.mockative.configure -import io.mockative.every -import io.mockative.mock -import io.mockative.verify -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -internal class PersistenceTest { - private val persistence: Persistence by lazy { - PersistenceImpl(settings) - } - - @Mock - private val settings = configure(mock(classOf())) { - stubsUnitByDefault = true - } - - @Test - fun `getValue returns the same type`() { - every { settings.getFloat(KEY, mockFloat) } - .returns(mockFloat) - every { settings.getBoolean(KEY, mockBoolean) } - .returns(mockBoolean) - every { settings.getInt(KEY, mockInt) } - .returns(mockInt) - every { settings.getString(KEY, mockString) } - .returns(mockString) - every { settings.getLong(KEY, mockLong) } - .returns(mockLong) - - assertEquals(mockFloat, persistence.getValue(KEY, mockFloat)) - assertEquals(mockBoolean, persistence.getValue(KEY, mockBoolean)) - assertEquals(mockInt, persistence.getValue(KEY, mockInt)) - assertEquals(mockString, persistence.getValue(KEY, mockString)) - assertEquals(mockLong, persistence.getValue(KEY, mockLong)) - - verify { settings.getFloat(KEY, mockFloat) } - .wasInvoked() - verify { settings.getBoolean(KEY, mockBoolean) } - .wasInvoked() - verify { settings.getInt(KEY, mockInt) } - .wasInvoked() - verify { settings.getString(KEY, mockString) } - .wasInvoked() - verify { settings.getLong(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `setValue sets the same type`() { - persistence.setValue(KEY, mockFloat) - persistence.setValue(KEY, mockBoolean) - persistence.setValue(KEY, mockInt) - persistence.setValue(KEY, mockString) - persistence.setValue(KEY, mockLong) - - verify { settings.putFloat(KEY, mockFloat) } - .wasInvoked() - verify { settings.putBoolean(KEY, mockBoolean) } - .wasInvoked() - verify { settings.putInt(KEY, mockInt) } - .wasInvoked() - verify { settings.putString(KEY, mockString) } - .wasInvoked() - verify { settings.putLong(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `UnsupportedPersistenceException throw when unsupported type tried to saved or read`() { - val mockObject = object {} - - assertFailsWith(UnsupportedPersistenceException::class) { - persistence.setValue(KEY, mockObject) - } - assertFailsWith(UnsupportedPersistenceException::class) { - persistence.getValue(KEY, mockObject) - } - } -} diff --git a/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt b/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt index 8c33b91b86..d5e7ce4790 100644 --- a/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt +++ b/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt @@ -2,34 +2,27 @@ package com.oztechan.ccc.client.core.persistence.di import com.oztechan.ccc.client.core.persistence.FlowPersistence import com.oztechan.ccc.client.core.persistence.FlowPersistenceImpl -import com.oztechan.ccc.client.core.persistence.Persistence -import com.oztechan.ccc.client.core.persistence.PersistenceImpl import com.oztechan.ccc.client.core.persistence.SuspendPersistence import com.oztechan.ccc.client.core.persistence.SuspendPersistenceImpl +import com.oztechan.ccc.common.core.infrastructure.di.DISPATCHER_IO import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.Settings -import com.russhwolf.settings.coroutines.FlowSettings -import com.russhwolf.settings.coroutines.SuspendSettings import com.russhwolf.settings.coroutines.toFlowSettings import com.russhwolf.settings.coroutines.toSuspendSettings -import org.koin.core.module.dsl.bind -import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named import org.koin.dsl.module actual val clientCorePersistenceModule = module { single { NSUserDefaultsSettings(get().userDefaults) } - single { get() } - @Suppress("OPT_IN_USAGE") - single { get().toSuspendSettings() } - @Suppress("OPT_IN_USAGE") - single { get().toFlowSettings() } - singleOf(::PersistenceImpl) { bind() } @Suppress("OPT_IN_USAGE") - singleOf(::FlowPersistenceImpl) { bind() } + single { + FlowPersistenceImpl(get().toFlowSettings(get(named(DISPATCHER_IO)))) + } @Suppress("OPT_IN_USAGE") - singleOf(::SuspendPersistenceImpl) { bind() } + single { + SuspendPersistenceImpl(get().toSuspendSettings(get(named(DISPATCHER_IO)))) + } } From a93beffdc3f1e990a6c85b603e2874ec822070b5 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 2 Feb 2024 11:50:16 +0300 Subject: [PATCH 037/129] [Oztechan/CCC#3146] Make implementation classes internal (#3147) --- .../ccc/client/core/persistence/FlowPersistenceImpl.kt | 2 +- .../client/core/persistence/SuspendPersistenceImpl.kt | 3 ++- .../storage/calculation/CalculationStorageImpl.kt | 2 +- .../kotlin/com/oztechan/ccc/test/ConventionTest.kt | 10 ++++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt index 4ad3115c03..e99fbe013b 100644 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt +++ b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt @@ -5,7 +5,7 @@ import com.russhwolf.settings.coroutines.FlowSettings import kotlinx.coroutines.flow.Flow @Suppress("OPT_IN_USAGE") -class FlowPersistenceImpl(private val flowSettings: FlowSettings) : FlowPersistence { +internal class FlowPersistenceImpl(private val flowSettings: FlowSettings) : FlowPersistence { @Suppress("UNCHECKED_CAST") override fun getFlow(key: String, defaultValue: T): Flow = when (defaultValue) { diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt index 80cda13286..9c905f492e 100644 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt +++ b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt @@ -4,7 +4,8 @@ import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceExce import com.russhwolf.settings.coroutines.SuspendSettings @Suppress("OPT_IN_USAGE") -class SuspendPersistenceImpl(private val suspendSettings: SuspendSettings) : SuspendPersistence { +internal class SuspendPersistenceImpl(private val suspendSettings: SuspendSettings) : + SuspendPersistence { @Suppress("UNCHECKED_CAST") override suspend fun getSuspend(key: String, defaultValue: T): T = diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index 2d5203317b..0b4c158741 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -2,7 +2,7 @@ package com.oztechan.ccc.client.storage.calculation import com.oztechan.ccc.client.core.persistence.SuspendPersistence -class CalculationStorageImpl( +internal class CalculationStorageImpl( private val suspendPersistence: SuspendPersistence ) : CalculationStorage { diff --git a/test/src/test/kotlin/com/oztechan/ccc/test/ConventionTest.kt b/test/src/test/kotlin/com/oztechan/ccc/test/ConventionTest.kt index 4ba38480a4..0bc328f821 100644 --- a/test/src/test/kotlin/com/oztechan/ccc/test/ConventionTest.kt +++ b/test/src/test/kotlin/com/oztechan/ccc/test/ConventionTest.kt @@ -6,6 +6,7 @@ import com.lemonappdev.konsist.api.declaration.KoFunctionDeclaration import com.lemonappdev.konsist.api.declaration.KoPropertyDeclaration import com.lemonappdev.konsist.api.ext.list.indexOfFirstInstance import com.lemonappdev.konsist.api.ext.list.indexOfLastInstance +import com.lemonappdev.konsist.api.ext.list.withNameEndingWith import com.lemonappdev.konsist.api.verify.assertFalse import com.lemonappdev.konsist.api.verify.assertTrue import org.junit.Test @@ -106,4 +107,13 @@ internal class ConventionTest { } } } + + @Test + fun `Impl classes should be internal`() { + Konsist + .scopeFromProject() + .classes() + .withNameEndingWith("Impl") + .assertTrue { it.hasInternalModifier } + } } From 887cac55f20fa21104fa87d5b9572698eaba2257 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:52:10 +0300 Subject: [PATCH 038/129] [Oztechan/Global#12] Update detekt to v1.23.5 (#3133) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b79772009a..81192df5e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "1.9.22" ksp = "1.9.22-1.0.17" -detekt = "1.23.4" +detekt = "1.23.5" androidGradlePlugin = "8.2.2" composeCompiler = "1.5.8" compose = "1.6.0" From 832ed872f67e9a378dc0ea1defb2604a5e939e2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:34:02 +0300 Subject: [PATCH 039/129] [Oztechan/Global#12] Update easingthemes/ssh-deploy action to v5.0.1 (#3132) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b08a0876f3..f6f31e0c08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -193,7 +193,7 @@ jobs: path: artifact - name: Deploy to Server - uses: easingthemes/ssh-deploy@v5.0.0 + uses: easingthemes/ssh-deploy@v5.0.1 env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} From c8debfc7a8a8b98b4edd5c2a6b71678bb7e558e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 12:07:50 +0300 Subject: [PATCH 040/129] [Oztechan/Global#12] Update codecov/codecov-action action to v4 (#3139) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f83e3046b..220e8edfa0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -296,7 +296,7 @@ jobs: path: build - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: build/report.xml From 0c354ff08ed5a70d97b31c0e737e583b3e8b7e4b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 12:11:05 +0300 Subject: [PATCH 041/129] [Oztechan/Global#12] Update Git Submodules (#3152) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodule/parsermob b/submodule/parsermob index d13240e937..c5bde92d11 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit d13240e937b0512d3d0fd23dcf5662fcd12a1f40 +Subproject commit c5bde92d11fa95cc383a70362727adfc6fd7c60f diff --git a/submodule/scopemob b/submodule/scopemob index 735dab5692..c5db9a2b56 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 735dab569213171280e0d48159c53754e9291e29 +Subproject commit c5db9a2b568d8dacc77213648cc88e500267ecd3 From c32a8c49a3dd49d84e12e52864e9efff7143a49c Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 3 Feb 2024 22:15:47 +0300 Subject: [PATCH 042/129] [Oztechan/CCC#3153] Add test cases for Viewmodel states (#3154) --- .../android/viewmodel/widget/WidgetViewModelTest.kt | 5 ++++- .../viewmodel/calculator/CalculatorViewModelTest.kt | 5 ++++- .../viewmodel/currencies/CurrenciesViewModelTest.kt | 1 + .../client/viewmodel/premium/PremiumViewModelTest.kt | 10 ++++++++++ .../client/viewmodel/settings/SettingsViewModelTest.kt | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt index b8cdf94e6e..f6897b02dc 100644 --- a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt +++ b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt @@ -31,6 +31,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlin.test.assertTrue import kotlin.time.Duration.Companion.days internal class WidgetViewModelTest { @@ -152,9 +153,11 @@ internal class WidgetViewModelTest { } @Test - fun `init sets isPremium and currentBase`() = runTest { + fun `init updates states correctly`() = runTest { viewModel.state.firstOrNull().let { assertNotNull(it) + assertTrue { it.currencyList.isEmpty() } + assertEquals("", it.lastUpdate) assertEquals(base, it.currentBase) assertEquals(appStorage.getPremiumEndDate().isNotPassed(), it.isPremium) } diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index c1c3e1403b..c43f3609c5 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -140,10 +140,13 @@ internal class CalculatorViewModelTest { assertNotNull(it) assertEquals(currency1.code, it.base) assertEquals("", it.input) + assertEquals("", it.output) + assertEquals(currency1.symbol, it.symbol) assertIs(it.conversionState) assertEquals(ConversionState.Online(nowAsDateString()), it.conversionState) assertEquals(currencyList, it.currencyList) assertEquals(shouldShowAds, it.isBannerAdVisible) + assertFalse { it.loading } } coVerify { adControlRepository.shouldShowBannerAd() } @@ -151,7 +154,7 @@ internal class CalculatorViewModelTest { } @Test - fun `init sets the latest base and input`() = runTest { + fun `init updates the latest base and input`() = runTest { val mock = "mock" coEvery { calculationStorage.getBase() } diff --git a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt index 7e14139c02..bfde0752b2 100644 --- a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt +++ b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt @@ -146,6 +146,7 @@ internal class CurrenciesViewModelTest { assertFalse { it.isOnboardingVisible } // mocked false assertEquals(currencyList.toMutableList(), viewModel.data.unFilteredList) assertEquals(shouldShowAds, it.isBannerAdVisible) + assertFalse { it.loading } } coVerify { adControlRepository.shouldShowBannerAd() } diff --git a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt index 0d615c37bc..5088a4b136 100644 --- a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt +++ b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt @@ -52,6 +52,16 @@ internal class PremiumViewModelTest { Dispatchers.setMain(UnconfinedTestDispatcher()) } + @Test + fun `init updates states correctly`() = runTest { + val premiumTypes: List = listOf(PremiumType.VIDEO) + viewModel.state.firstOrNull().let { + assertNotNull(it) + assertTrue { it.loading } + assertEquals(premiumTypes, it.premiumTypes) + } + } + // SEED @Test fun `check data is null`() { diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index 10d9bcdcae..3c796d3e1a 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -153,6 +153,7 @@ internal class SettingsViewModelTest { assertEquals(mockedPrecision, it.precision) assertEquals(version, it.version) assertEquals(shouldShowAds, it.isBannerAdVisible) + assertIs(it.premiumStatus) } coVerify { adControlRepository.shouldShowBannerAd() } From 6130625a253790fe05c3422d06a1cc2a5d7f23a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:33:52 +0300 Subject: [PATCH 043/129] [Oztechan/Global#12] Lock file maintenance (#3158) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index a29c6d454d..6a5ce6a640 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -8,7 +8,7 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.883.0) + aws-partitions (1.887.0) aws-sdk-core (3.191.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -109,7 +109,7 @@ GEM google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.55.0) + google-apis-androidpublisher_v3 (0.56.0) google-apis-core (>= 0.12.0, < 2.a) google-apis-core (0.13.0) addressable (~> 2.5, >= 2.5.1) @@ -132,7 +132,7 @@ GEM google-cloud-core (1.6.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.0) + google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) google-cloud-errors (1.3.1) google-cloud-storage (1.48.1) From 8c399f122214368da4c250db7ab9b41540e3e899 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 22:08:04 +0300 Subject: [PATCH 044/129] [Oztechan/Global#12] Update Git Submodules (#3159) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index f259cf97a4..dd0323fc98 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit f259cf97a45f21bbf22e43aa5f1ba19738e8758b +Subproject commit dd0323fc98e931080bb07c5a08fec5554b7740e1 diff --git a/submodule/logmob b/submodule/logmob index b60a01d696..c7f6d9d6f9 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit b60a01d6966a682df9cd5c34aabced38f3ce2f15 +Subproject commit c7f6d9d6f94ecef0de4ec0bc60918ab5407b2406 diff --git a/submodule/parsermob b/submodule/parsermob index c5bde92d11..3e5a4ebd89 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit c5bde92d11fa95cc383a70362727adfc6fd7c60f +Subproject commit 3e5a4ebd89b9d1f775ff384214faec1e70169954 diff --git a/submodule/scopemob b/submodule/scopemob index c5db9a2b56..6169262cac 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit c5db9a2b568d8dacc77213648cc88e500267ecd3 +Subproject commit 6169262cac6ccc7645bbadfba9db91e501d340c2 From c6f044c23ef0fb5c9e92fcc5f5e080f114c7c8d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 22:09:18 +0300 Subject: [PATCH 045/129] [Oztechan/Global#12] Update dependency gradle to v8.6 (#3151) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 829e1a5a9a..7fc84becca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 6a1a9ab757745f738d204c550ed4b88346ad7248 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:42:44 +0300 Subject: [PATCH 046/129] [Oztechan/Global#12] Update ktor to v2.3.8 (#3134) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81192df5e3..b1342fc461 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ koinCore = "3.5.3" koinCompose = "3.5.3" koinAndroid = "3.5.3" koinKtor = "3.5.3" -ktor = "2.3.7" +ktor = "2.3.8" multiplatformSettings = "1.1.1" firebaseAnalytics = "21.5.0" firebaseRemoteConfig = "21.6.0" From 8298244f534502a31141153c384b5b97ce1b8a24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:10:39 +0300 Subject: [PATCH 047/129] [Oztechan/Global#12] Update dependency com.huawei.hms:ads-prime to v3.4.69.300 (#3150) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1342fc461..7a78de6731 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ firebasePer = "20.5.1" firebasePerPlugin = "1.4.2" firebaseCrashlyticsPlugin = "2.9.9" googleAds = "22.6.0" -huaweiAds = "3.4.68.301" +huaweiAds = "3.4.69.300" huaweiOsm = "1.3.35" navigation = "2.7.6" playCore = "1.10.3" From 193362f7f59b4fc8ce84074616649f3de77d071b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:12:05 +0300 Subject: [PATCH 048/129] [Oztechan/Global#12] Update submodule/logmob digest to 044b09e (#3162) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/logmob | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodule/logmob b/submodule/logmob index c7f6d9d6f9..044b09eced 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit c7f6d9d6f94ecef0de4ec0bc60918ab5407b2406 +Subproject commit 044b09ecede874bd7b828598ec3899af5e41a7e9 From ea3c1950401b07882d107448f5993383ee82e2b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:12:27 +0300 Subject: [PATCH 049/129] [Oztechan/Global#12] Update dependency co.touchlab:kermit to v2.0.3 (#3155) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a78de6731..459dadb1d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ kover = "0.6.1" rootBeer = "0.1.0" mockative = "2.0.1" anrWatchDog = "1.4.0" -kermit = "2.0.2" +kermit = "2.0.3" konsist = "0.13.0" [libraries] From 22d0bdf6036f9ef81d2445f70d842ec76db1a1c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:36:09 +0300 Subject: [PATCH 050/129] [Oztechan/Global#12] Update actions/upload-artifact action to v4.3.1 (#3161) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 220e8edfa0..0e8c1da7a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,7 +95,7 @@ jobs: arguments: assemble - name: Upload Android Artifacts - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 if: github.event_name == 'push' with: name: androidArtifacts @@ -195,7 +195,7 @@ jobs: run: fastlane build - name: Upload iOS Artifacts - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 if: github.event_name == 'push' with: name: iOSArtifacts @@ -267,7 +267,7 @@ jobs: arguments: check koverMergedXmlReport --parallel - name: Upload Coverage Report - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 with: name: coverageReport path: build/reports/kover/merged/xml/report.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6f31e0c08..24ee7d1669 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,19 +90,19 @@ jobs: arguments: :android:app:bundleRelease :backend:app:jar --parallel - name: Upload Google App Bundle - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 with: name: googleBundle path: android/app/build/outputs/bundle/googleRelease/app-google-release.aab - name: Upload Huawei App Bundle - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 with: name: huaweiBundle path: android/app/build/outputs/bundle/huaweiRelease/app-huawei-release.aab - name: Upload Backend Jar - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.3.1 with: name: backendJar path: backend/app/build/libs/app-*.jar From f52f078c016494be23cfd8650c02860ba02305fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:36:27 +0300 Subject: [PATCH 051/129] [Oztechan/Global#12] Update actions/download-artifact action to v4.1.2 (#3160) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e8c1da7a2..f3902af23b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,7 +123,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Download Android Artifacts - uses: actions/download-artifact@v4.1.1 + uses: actions/download-artifact@v4.1.2 with: name: androidArtifacts @@ -222,7 +222,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Download iOS IPA - uses: actions/download-artifact@v4.1.1 + uses: actions/download-artifact@v4.1.2 with: name: iOSArtifacts path: ios @@ -290,7 +290,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Download Coverage Report - uses: actions/download-artifact@v4.1.1 + uses: actions/download-artifact@v4.1.2 with: name: coverageReport path: build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24ee7d1669..ee0b079756 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,7 +119,7 @@ jobs: steps: - name: Download App Bundle - uses: actions/download-artifact@v4.1.1 + uses: actions/download-artifact@v4.1.2 with: name: googleBundle @@ -155,7 +155,7 @@ jobs: steps: - name: Download App Bundle - uses: actions/download-artifact@v4.1.1 + uses: actions/download-artifact@v4.1.2 with: name: huaweiBundle @@ -187,7 +187,7 @@ jobs: steps: - name: Download Backend Jar - uses: actions/download-artifact@v4.1.1 + uses: actions/download-artifact@v4.1.2 with: name: backendJar path: artifact From b5ee3faf34d8e8ca5f5faaf0ec745d397fd7be6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 13:18:58 +0300 Subject: [PATCH 052/129] [Oztechan/Global#12] Update dependency androidx.compose.compiler:compiler to v1.5.9 (#3164) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 459dadb1d9..2cae61cad6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin = "1.9.22" ksp = "1.9.22-1.0.17" detekt = "1.23.5" androidGradlePlugin = "8.2.2" -composeCompiler = "1.5.8" +composeCompiler = "1.5.9" compose = "1.6.0" glance = "1.0.0" material3 = "1.1.2" From fa12c8a718adc7ca4280fe90579598f8a1f3c6e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 21:44:40 +0300 Subject: [PATCH 053/129] [Oztechan/Global#12] Update compose to v1.6.1 (#3163) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2cae61cad6..bf16966e9b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "1.9.22-1.0.17" detekt = "1.23.5" androidGradlePlugin = "8.2.2" composeCompiler = "1.5.9" -compose = "1.6.0" +compose = "1.6.1" glance = "1.0.0" material3 = "1.1.2" androidDesugaring = "2.0.4" From a5a764789e868e9a6587b4870bf787fe9facaa48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 21:44:59 +0300 Subject: [PATCH 054/129] [Oztechan/Global#12] Update dependency androidx.compose.material3:material3 to v1.2.0 (#3166) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf16966e9b..84392a875c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidGradlePlugin = "8.2.2" composeCompiler = "1.5.9" compose = "1.6.1" glance = "1.0.0" -material3 = "1.1.2" +material3 = "1.2.0" androidDesugaring = "2.0.4" androidMaterial = "1.11.0" composeActivity = "1.8.2" From 7ed9b1de1830107b7dde4c72bb1d0d4f1c8d96d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:56:20 +0300 Subject: [PATCH 055/129] [Oztechan/Global#12] Update gradle/gradle-build-action action to v3 (#3136) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3902af23b..721ea51649 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -90,7 +90,7 @@ jobs: distribution: 'temurin' - name: Assemble - uses: gradle/gradle-build-action@v2.12.0 + uses: gradle/gradle-build-action@v3.0.0 with: arguments: assemble @@ -262,7 +262,7 @@ jobs: distribution: 'temurin' - name: Run Quality Jobs - uses: gradle/gradle-build-action@v2.12.0 + uses: gradle/gradle-build-action@v3.0.0 with: arguments: check koverMergedXmlReport --parallel @@ -346,7 +346,7 @@ jobs: distribution: 'temurin' - name: Detekt - uses: gradle/gradle-build-action@v2.12.0 + uses: gradle/gradle-build-action@v3.0.0 with: arguments: detektAll diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee0b079756..6cbd6b411e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: distribution: 'temurin' - name: Generate Artifacts - uses: gradle/gradle-build-action@v2.12.0 + uses: gradle/gradle-build-action@v3.0.0 with: arguments: :android:app:bundleRelease :backend:app:jar --parallel From 6916e0a7c78fac5480d7b6de797df66c845e9a01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:57:49 +0300 Subject: [PATCH 056/129] [Oztechan/Global#12] Update navigation to v2.7.7 (#3165) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 84392a875c..590afc9162 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ firebaseCrashlyticsPlugin = "2.9.9" googleAds = "22.6.0" huaweiAds = "3.4.69.300" huaweiOsm = "1.3.35" -navigation = "2.7.6" +navigation = "2.7.7" playCore = "1.10.3" kotlinXDateTime = "0.5.0" coroutines = "1.7.3" From f00144e78df7fb4973c2d5522d7bd28fcc1508b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:21:56 +0300 Subject: [PATCH 057/129] [Oztechan/Global#12] Update Git Submodules (#3173) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index dd0323fc98..db1b805427 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit dd0323fc98e931080bb07c5a08fec5554b7740e1 +Subproject commit db1b8054277c4c32188693b3a64b4ac5179f8e34 diff --git a/submodule/logmob b/submodule/logmob index 044b09eced..36d6281c05 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 044b09ecede874bd7b828598ec3899af5e41a7e9 +Subproject commit 36d6281c05f05556d5eef6f3376f3bd1b1018447 From 93828e43d0834f4abbe139feb578ec9e46d0aaee Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 11 Feb 2024 23:09:42 +0300 Subject: [PATCH 058/129] [Oztechan/CCC#3156] Add test cases for Viewmodel datas (#3157) * [Oztechan/CCC#3156] Add test cases for Viewmodel datas * [Oztechan/CCC#3156] Add test cases for Viewmodel datas --- .../android/viewmodel/widget/WidgetViewModelTest.kt | 5 +++++ .../viewmodel/calculator/CalculatorViewModelTest.kt | 7 +++++++ .../viewmodel/currencies/CurrenciesViewModelTest.kt | 7 +++++++ .../ccc/client/viewmodel/main/MainViewModelTest.kt | 10 ++++++++++ .../client/viewmodel/premium/PremiumViewModelTest.kt | 2 +- .../selectcurrency/SelectCurrencyViewModelTest.kt | 2 +- .../client/viewmodel/settings/SettingsViewModelTest.kt | 7 +++++++ .../client/viewmodel/watchers/WatchersViewModelTest.kt | 5 +++++ 8 files changed, 43 insertions(+), 2 deletions(-) diff --git a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt index f6897b02dc..a4034663fb 100644 --- a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt +++ b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt @@ -163,6 +163,11 @@ internal class WidgetViewModelTest { } } + @Test + fun `init updates data correctly`() { + assertNotNull(viewModel.data) + } + @Test fun `if user is premium api call and db query are invoked`() = runTest { coEvery { appStorage.getPremiumEndDate() } diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index c43f3609c5..317d2d458e 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -170,6 +170,13 @@ internal class CalculatorViewModelTest { } } + @Test + fun `init updates data correctly`() { + assertNotNull(viewModel.data) + assertNotNull(viewModel.data.conversion) + assertNotNull(viewModel.data.parser) + } + @Test fun `when api fails and there is conversion in db then conversion rates are calculated`() = runTest { diff --git a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt index bfde0752b2..9a8fa489e1 100644 --- a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt +++ b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt @@ -156,6 +156,13 @@ internal class CurrenciesViewModelTest { .wasInvoked() } + @Test + fun `init updates data correctly`() { + assertNotNull(viewModel.data) + assertEquals(currencyList, viewModel.data.unFilteredList) + assertTrue { viewModel.data.query.isEmpty() } + } + @Test fun `show FewCurrency effect if there is less than MINIMUM_ACTIVE_CURRENCY and not firstRun`() = runTest { diff --git a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt index ec161cfbc4..3fb6fc8a11 100644 --- a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt +++ b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt @@ -160,6 +160,16 @@ internal class MainViewModelTest { .wasInvoked() } + @Test + fun `init updates data correctly`() { + assertNotNull(viewModel.data) + assertFalse { viewModel.data.adVisibility } + assertFalse { viewModel.data.isAppUpdateShown } + assertTrue { viewModel.data.isNewSession } + assertNotNull(viewModel.data.adJob) + assertTrue { viewModel.data.adJob.isActive } + } + // event @Test fun onPause() = with(viewModel) { diff --git a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt index 5088a4b136..49169fb441 100644 --- a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt +++ b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt @@ -64,7 +64,7 @@ internal class PremiumViewModelTest { // SEED @Test - fun `check data is null`() { + fun `init updates data correctly`() { assertNull(viewModel.data) } diff --git a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt index ea5830d8dd..fd18fa17fc 100644 --- a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt +++ b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt @@ -56,7 +56,7 @@ internal class SelectCurrencyViewModelTest { // SEED @Test - fun `check data is null`() { + fun `init updates data correctly`() { assertNull(subject.data) } diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index 3c796d3e1a..af709a90be 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -42,6 +42,7 @@ import kotlin.random.Random import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -160,6 +161,12 @@ internal class SettingsViewModelTest { .wasInvoked() } + @Test + fun `init updates data correctly`() { + assertNotNull(viewModel.data) + assertFalse { viewModel.data.synced } + } + @Test fun `when premiumEndDate is never set PremiumStatus is NeverActivated`() = runTest { coEvery { appStorage.getPremiumEndDate() } diff --git a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt index 8943b008d5..66369f1fd6 100644 --- a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt +++ b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt @@ -92,6 +92,11 @@ internal class WatchersViewModelTest { .wasInvoked() } + @Test + fun `init updates data correctly`() { + assertNotNull(viewModel.data) + } + // Analytics @Test fun ifUserPropertiesSetCorrect() { From 056e433312d3ed93d29ff85185ba72100c8179e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:29:19 +0300 Subject: [PATCH 059/129] [Oztechan/Global#12] Update Git Submodules (#3174) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index db1b805427..5e10192375 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit db1b8054277c4c32188693b3a64b4ac5179f8e34 +Subproject commit 5e10192375319f2845c98acebcc05e0c31a64919 diff --git a/submodule/logmob b/submodule/logmob index 36d6281c05..6c0ac8873f 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 36d6281c05f05556d5eef6f3376f3bd1b1018447 +Subproject commit 6c0ac8873fc5ac61518721889f7d5468c128f181 diff --git a/submodule/parsermob b/submodule/parsermob index 3e5a4ebd89..d1616d61fb 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 3e5a4ebd89b9d1f775ff384214faec1e70169954 +Subproject commit d1616d61fb575225be5f13ba3d678099fadb0703 diff --git a/submodule/scopemob b/submodule/scopemob index 6169262cac..562ec2da88 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 6169262cac6ccc7645bbadfba9db91e501d340c2 +Subproject commit 562ec2da887b60de6240b60f85b6899c32e9ed9c From dd2bdc55988367fe38959cbb9daf64d609bd56db Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Tue, 13 Feb 2024 18:55:25 +0300 Subject: [PATCH 060/129] [Oztechan/CCC#3176] Add Flow Property for currentBase (#3177) --- .../client-storage-calculation.gradle.kts | 6 ++++- .../storage/calculation/CalculationStorage.kt | 3 +++ .../calculation/CalculationStorageImpl.kt | 7 +++++- .../calculation/CalculationStorageTest.kt | 24 ++++++++++++++++--- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/client/storage/calculation/client-storage-calculation.gradle.kts b/client/storage/calculation/client-storage-calculation.gradle.kts index faaa819225..2ccc3b6831 100644 --- a/client/storage/calculation/client-storage-calculation.gradle.kts +++ b/client/storage/calculation/client-storage-calculation.gradle.kts @@ -14,8 +14,12 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.common.koinCore) implementation(project(Modules.Client.Core.persistence)) + + libs.common.apply { + implementation(koinCore) + implementation(coroutines) + } } commonTest.dependencies { libs.common.apply { diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt index 864c90c5dc..9e02d18b68 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt @@ -1,6 +1,9 @@ package com.oztechan.ccc.client.storage.calculation +import kotlinx.coroutines.flow.Flow + interface CalculationStorage { + fun getBaseFlow(): Flow suspend fun getBase(): String suspend fun setBase(value: String) diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index 0b4c158741..be847f8b5b 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -1,10 +1,15 @@ package com.oztechan.ccc.client.storage.calculation +import com.oztechan.ccc.client.core.persistence.FlowPersistence import com.oztechan.ccc.client.core.persistence.SuspendPersistence +import kotlinx.coroutines.flow.Flow internal class CalculationStorageImpl( - private val suspendPersistence: SuspendPersistence + private val suspendPersistence: SuspendPersistence, + private val flowPersistence: FlowPersistence ) : CalculationStorage { + override fun getBaseFlow(): Flow = + flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) override suspend fun getBase(): String = suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) diff --git a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt index 03c68cb5fe..d01db16d9e 100644 --- a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt +++ b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt @@ -1,5 +1,6 @@ package com.oztechan.ccc.client.storage.calculation +import com.oztechan.ccc.client.core.persistence.FlowPersistence import com.oztechan.ccc.client.core.persistence.SuspendPersistence import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_CURRENT_BASE import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_LAST_INPUT @@ -11,7 +12,10 @@ import io.mockative.Mock import io.mockative.classOf import io.mockative.coEvery import io.mockative.coVerify +import io.mockative.every import io.mockative.mock +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test @@ -19,15 +23,29 @@ import kotlin.test.assertEquals internal class CalculationStorageTest { private val subject: CalculationStorage by lazy { - CalculationStorageImpl(suspendPersistence) + CalculationStorageImpl(suspendPersistence, flowPersistence) } @Mock private val suspendPersistence = mock(classOf()) + @Mock + private val flowPersistence = mock(classOf()) + + @Test + fun getBaseFlow() = runTest { + every { flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + .returns(flowOf(DEFAULT_CURRENT_BASE)) + + assertEquals(DEFAULT_CURRENT_BASE, subject.getBaseFlow().first()) + + coVerify { flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + .wasInvoked() + } + // defaults @Test - fun `get default currentBase`() = runTest { + fun `get default base`() = runTest { coEvery { suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } .returns(DEFAULT_CURRENT_BASE) @@ -61,7 +79,7 @@ internal class CalculationStorageTest { // setters @Test - fun `set currentBase`() = runTest { + fun `set base`() = runTest { val mockValue = "mock" subject.setBase(mockValue) From fc2f955348c0cfcc67c5a00d7a7b4c7b0108b5a7 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 14 Feb 2024 00:01:32 +0300 Subject: [PATCH 061/129] [Oztechan/CCC#3178] Replace CurrencyChange effect with DismissDialog (#3179) --- .../selectcurrency/SelectCurrencyBottomSheet.kt | 11 +---------- .../client-viewmodel-selectcurrency.gradle.kts | 4 ++++ .../viewmodel/selectcurrency/SelectCurrencySEED.kt | 2 +- .../selectcurrency/SelectCurrencyViewModel.kt | 7 +++++-- .../di/ClientViewModelSelectCurrencyModule.kt | 2 +- .../selectcurrency/SelectCurrencyViewModelTest.kt | 13 ++++++++++--- .../UI/SelectCurrency/SelectCurrencyRootView.swift | 3 +-- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt index 39876fc548..9adbe26e06 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt @@ -10,9 +10,7 @@ import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger import com.github.submob.basemob.bottomsheet.BaseVBBottomSheetDialogFragment import com.oztechan.ccc.android.ui.mobile.R -import com.oztechan.ccc.android.ui.mobile.content.calculator.CalculatorFragment.Companion.CHANGE_BASE_EVENT import com.oztechan.ccc.android.ui.mobile.databinding.BottomSheetSelectCurrencyBinding -import com.oztechan.ccc.android.ui.mobile.util.setNavigationResult import com.oztechan.ccc.android.ui.mobile.util.visibleIf import com.oztechan.ccc.client.core.analytics.AnalyticsManager import com.oztechan.ccc.client.core.analytics.model.ScreenName @@ -85,14 +83,7 @@ class SelectCurrencyBottomSheet : .onEach { viewEffect -> Logger.i { "SelectCurrencyBottomSheet observeEffects ${viewEffect::class.simpleName}" } when (viewEffect) { - is SelectCurrencyEffect.CurrencyChange -> { - setNavigationResult( - R.id.calculatorFragment, - viewEffect.newBase, - CHANGE_BASE_EVENT - ) - dismissDialog() - } + is SelectCurrencyEffect.DismissDialog -> dismissDialog() SelectCurrencyEffect.OpenCurrencies -> navigate( R.id.selectCurrencyBottomSheet, diff --git a/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts b/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts index bd79ec9ace..1dc8354b3a 100644 --- a/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts +++ b/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts @@ -29,6 +29,10 @@ kotlin { implementation(project(currency)) } + Modules.Client.Storage.apply { + implementation(project(calculation)) + } + Modules.Common.Core.apply { implementation(project(model)) } diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt index 468d37c91a..f407f918a8 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt @@ -20,6 +20,6 @@ interface SelectCurrencyEvent : BaseEvent { // Effect sealed class SelectCurrencyEffect : BaseEffect { - data class CurrencyChange(val newBase: String) : SelectCurrencyEffect() + data object DismissDialog : SelectCurrencyEffect() data object OpenCurrencies : SelectCurrencyEffect() } diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt index f8e5a2f820..4067d25acf 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt @@ -10,6 +10,7 @@ import com.oztechan.ccc.client.core.viewmodel.BaseSEEDViewModel import com.oztechan.ccc.client.core.viewmodel.util.launchIgnored import com.oztechan.ccc.client.core.viewmodel.util.update import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource +import com.oztechan.ccc.client.storage.calculation.CalculationStorage import com.oztechan.ccc.common.core.model.Currency import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -19,7 +20,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class SelectCurrencyViewModel( - currencyDataSource: CurrencyDataSource + currencyDataSource: CurrencyDataSource, + private val calculationStorage: CalculationStorage, ) : BaseSEEDViewModel(), SelectCurrencyEvent { // region SEED private val _state = MutableStateFlow(SelectCurrencyState()) @@ -49,7 +51,8 @@ class SelectCurrencyViewModel( // region Event override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { Logger.d { "SelectCurrencyViewModel onItemClick ${currency.code}" } - _effect.emit(SelectCurrencyEffect.CurrencyChange(currency.code)) + calculationStorage.setBase(currency.code) + _effect.emit(SelectCurrencyEffect.DismissDialog) } override fun onSelectClick() = viewModelScope.launchIgnored { diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt index 44356c362b..03e94aad88 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt @@ -5,5 +5,5 @@ import com.oztechan.ccc.client.viewmodel.selectcurrency.SelectCurrencyViewModel import org.koin.dsl.module val clientViewModelSelectCurrencyModule = module { - viewModelDefinition { SelectCurrencyViewModel(get()) } + viewModelDefinition { SelectCurrencyViewModel(get(), get()) } } diff --git a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt index fd18fa17fc..2d01022064 100644 --- a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt +++ b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt @@ -6,8 +6,10 @@ package com.oztechan.ccc.client.viewmodel.selectcurrency import co.touchlab.kermit.CommonWriter import co.touchlab.kermit.Logger import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource +import com.oztechan.ccc.client.storage.calculation.CalculationStorage import io.mockative.Mock import io.mockative.classOf +import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify @@ -31,12 +33,15 @@ import com.oztechan.ccc.common.core.model.Currency as CurrencyCommon internal class SelectCurrencyViewModelTest { private val subject: SelectCurrencyViewModel by lazy { - SelectCurrencyViewModel(currencyDataSource) + SelectCurrencyViewModel(currencyDataSource, calculationStorage) } @Mock private val currencyDataSource = mock(classOf()) + @Mock + private val calculationStorage = mock(classOf()) + private val currencyDollar = CurrencyCommon("USD", "Dollar", "$", "", true) private val currencyEuro = CurrencyCommon("Eur", "Euro", "", "", true) @@ -97,8 +102,10 @@ internal class SelectCurrencyViewModelTest { subject.effect.onSubscription { subject.event.onItemClick(currencyDollar) }.firstOrNull().let { - assertIs(it) - assertEquals(currencyDollar.code, it.newBase) + assertIs(it) + + coVerify { calculationStorage.setBase(currencyDollar.code) } + .wasInvoked() } } diff --git a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift index 7523992979..db1a7126e8 100644 --- a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift +++ b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift @@ -42,8 +42,7 @@ struct SelectCurrencyRootView: View { private func onEffect(effect: SelectCurrencyEffect) { logger.i(message: { "SelectCurrencyRootView onEffect \(effect.description)" }) switch effect { - case let currencyChangeEffect as SelectCurrencyEffect.CurrencyChange: - onCurrencySelected(currencyChangeEffect.newBase) + case let currencyChangeEffect as SelectCurrencyEffect.DismissDialog: isBarShown = false case is SelectCurrencyEffect.OpenCurrencies: navigationStack.push(CurrenciesRootView(onBaseChange: onCurrencySelected)) From 6fab011d587a844df04ad8957b266a6de092af5a Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 14 Feb 2024 19:29:46 +0300 Subject: [PATCH 062/129] [Oztechan/CCC#3180] Remove ChangeBase Effect (#3181) * [Oztechan/CCC#3180] Remove ChangeBase Effect * [Oztechan/CCC#3180] Remove ChangeBase Effect --- .../content/currencies/CurrenciesFragment.kt | 8 ---- .../viewmodel/currencies/CurrenciesSEED.kt | 1 - .../currencies/CurrenciesViewModel.kt | 2 - .../currencies/CurrenciesViewModelTest.kt | 38 ++++++------------- .../UI/Calculator/CalculatorRootView.swift | 9 ++--- .../UI/Currencies/CurrenciesRootView.swift | 4 -- .../SelectCurrencyRootView.swift | 4 +- ios/CCC/UI/Settings/SettingsRootView.swift | 4 +- .../UI/Slides/BugReportSlideRootView.swift | 2 +- ios/CCC/UI/Watchers/WatchersRootView.swift | 20 +--------- 10 files changed, 20 insertions(+), 72 deletions(-) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt index 040d7c868b..2d608ea0fa 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt @@ -18,12 +18,10 @@ import com.github.submob.basemob.fragment.BaseVBFragment import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.ui.mobile.BuildConfig import com.oztechan.ccc.android.ui.mobile.R -import com.oztechan.ccc.android.ui.mobile.content.calculator.CalculatorFragment.Companion.CHANGE_BASE_EVENT import com.oztechan.ccc.android.ui.mobile.databinding.FragmentCurrenciesBinding import com.oztechan.ccc.android.ui.mobile.util.destroyBanner import com.oztechan.ccc.android.ui.mobile.util.hideKeyboard import com.oztechan.ccc.android.ui.mobile.util.setBannerAd -import com.oztechan.ccc.android.ui.mobile.util.setNavigationResult import com.oztechan.ccc.android.ui.mobile.util.showSnack import com.oztechan.ccc.android.ui.mobile.util.visibleIf import com.oztechan.ccc.client.core.analytics.AnalyticsManager @@ -133,12 +131,6 @@ class CurrenciesFragment : BaseVBFragment() { findNavController().popBackStack() view?.hideKeyboard() } - - is CurrenciesEffect.ChangeBase -> setNavigationResult( - R.id.calculatorFragment, - viewEffect.newBase, - CHANGE_BASE_EVENT - ) } }.launchIn(viewLifecycleOwner.lifecycleScope) diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt index 00dbc55711..f64d91f436 100644 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt @@ -30,7 +30,6 @@ sealed class CurrenciesEffect : BaseEffect { data object FewCurrency : CurrenciesEffect() data object OpenCalculator : CurrenciesEffect() data object Back : CurrenciesEffect() - data class ChangeBase(val newBase: String) : CurrenciesEffect() } // Data diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index 4ae616561c..6c01dae5c4 100755 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -101,8 +101,6 @@ class CurrenciesViewModel( analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) - - _effect.emit(CurrenciesEffect.ChangeBase(newBase)) } private fun filterList(txt: String) = data.unFilteredList diff --git a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt index 9a8fa489e1..8f7b9ad2a4 100644 --- a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt +++ b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt @@ -193,9 +193,10 @@ internal class CurrenciesViewModelTest { } ) - viewModel.effect.firstOrNull().let { - assertIs(it) - } + viewModel // init + + coVerify { calculationStorage.setBase(currency1.code) } + .wasNotInvoked() } @Test @@ -215,9 +216,10 @@ internal class CurrenciesViewModelTest { } ) - viewModel.effect.firstOrNull().let { - assertIs(it) - } + viewModel // init + + coVerify { calculationStorage.setBase(currency1.code) } + .wasNotInvoked() } @Test @@ -236,20 +238,12 @@ internal class CurrenciesViewModelTest { val firstActiveBase = currency1.code // first active currency every { currencyDataSource.getCurrenciesFlow() } - .returns( - flow { - delay(1.seconds.inWholeMilliseconds) - emit(currencyList) - } - ) + .returns(flowOf(currencyList)) coEvery { calculationStorage.getBase() } .returns("") - viewModel.effect.firstOrNull().let { - assertIs(it) - assertEquals(firstActiveBase, it.newBase) - } + viewModel // init coVerify { calculationStorage.setBase(firstActiveBase) } .wasInvoked() @@ -261,20 +255,12 @@ internal class CurrenciesViewModelTest { currency1 = currency1.copy(isActive = false) // make first item in list not active every { currencyDataSource.getCurrenciesFlow() } - .returns( - flow { - delay(1.seconds.inWholeMilliseconds) - emit(listOf(currency1, currency2, currency3)) - } - ) + .returns(flowOf(listOf(currency1, currency2, currency3))) coEvery { calculationStorage.getBase() } .returns(currency1.code) // not active one - viewModel.effect.firstOrNull().let { - assertIs(it) - assertEquals(currency2.code, it.newBase) - } + viewModel // init coVerify { calculationStorage.setBase(currency2.code) } .wasInvoked() diff --git a/ios/CCC/UI/Calculator/CalculatorRootView.swift b/ios/CCC/UI/Calculator/CalculatorRootView.swift index a90d357d79..5c91afaf0e 100644 --- a/ios/CCC/UI/Calculator/CalculatorRootView.swift +++ b/ios/CCC/UI/Calculator/CalculatorRootView.swift @@ -62,7 +62,7 @@ struct CalculatorRootView: View { text: String(\.choose_at_least_two_currency), buttonText: String(\.select), buttonAction: { - navigationStack.push(CurrenciesRootView(onBaseChange: { observable.event.onBaseChange(base: $0) })) + navigationStack.push(CurrenciesRootView()) } ) } @@ -80,10 +80,7 @@ struct CalculatorRootView: View { .sheet( isPresented: $isBarShown, content: { - SelectCurrencyRootView( - isBarShown: $isBarShown, - onCurrencySelected: { observable.event.onBaseChange(base: $0) } - ).environmentObject(navigationStack) + SelectCurrencyRootView(isBarShown: $isBarShown).environmentObject(navigationStack) } ) .onAppear { @@ -108,7 +105,7 @@ struct CalculatorRootView: View { case is CalculatorEffect.OpenBar: isBarShown = true case is CalculatorEffect.OpenSettings: - navigationStack.push(SettingsRootView(onBaseChange: { observable.event.onBaseChange(base: $0) })) + navigationStack.push(SettingsRootView()) case is CalculatorEffect.ShowPasteRequest: isPasteRequestSnackShown.toggle() case let copyToClipboardEffect as CalculatorEffect.CopyToClipboard: diff --git a/ios/CCC/UI/Currencies/CurrenciesRootView.swift b/ios/CCC/UI/Currencies/CurrenciesRootView.swift index 71e6e6e156..f78f7e3bff 100644 --- a/ios/CCC/UI/Currencies/CurrenciesRootView.swift +++ b/ios/CCC/UI/Currencies/CurrenciesRootView.swift @@ -24,8 +24,6 @@ struct CurrenciesRootView: View { private let analyticsManager: AnalyticsManager = koin.get() - var onBaseChange: (String) -> Void - var body: some View { CurrenciesView( event: observable.event, @@ -50,8 +48,6 @@ struct CurrenciesRootView: View { navigationStack.push(CalculatorRootView()) case is CurrenciesEffect.Back: navigationStack.pop() - case let changeBaseEffect as CurrenciesEffect.ChangeBase: - onBaseChange(changeBaseEffect.newBase) default: logger.i(message: { "CurrenciesRootView unknown effect" }) } diff --git a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift index db1a7126e8..053ef0258a 100644 --- a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift +++ b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift @@ -24,8 +24,6 @@ struct SelectCurrencyRootView: View { private let analyticsManager: AnalyticsManager = koin.get() - var onCurrencySelected: (String) -> Void - var body: some View { SelectCurrencyView( event: observable.event, @@ -45,7 +43,7 @@ struct SelectCurrencyRootView: View { case let currencyChangeEffect as SelectCurrencyEffect.DismissDialog: isBarShown = false case is SelectCurrencyEffect.OpenCurrencies: - navigationStack.push(CurrenciesRootView(onBaseChange: onCurrencySelected)) + navigationStack.push(CurrenciesRootView()) default: logger.i(message: { "SelectCurrencyRootView unknown effect" }) } diff --git a/ios/CCC/UI/Settings/SettingsRootView.swift b/ios/CCC/UI/Settings/SettingsRootView.swift index 42427f48a7..29199dd007 100644 --- a/ios/CCC/UI/Settings/SettingsRootView.swift +++ b/ios/CCC/UI/Settings/SettingsRootView.swift @@ -31,8 +31,6 @@ struct SettingsRootView: View { private let analyticsManager: AnalyticsManager = koin.get() - var onBaseChange: ((String) -> Void) - var body: some View { SettingsView( event: observable.event, @@ -74,7 +72,7 @@ struct SettingsRootView: View { case is SettingsEffect.Back: navigationStack.pop() case is SettingsEffect.OpenCurrencies: - navigationStack.push(CurrenciesRootView(onBaseChange: onBaseChange)) + navigationStack.push(CurrenciesRootView()) case is SettingsEffect.OpenWatchers: navigationStack.push(WatchersRootView()) case is SettingsEffect.FeedBack: diff --git a/ios/CCC/UI/Slides/BugReportSlideRootView.swift b/ios/CCC/UI/Slides/BugReportSlideRootView.swift index 593e24c6e9..e4ba1d9f65 100644 --- a/ios/CCC/UI/Slides/BugReportSlideRootView.swift +++ b/ios/CCC/UI/Slides/BugReportSlideRootView.swift @@ -24,7 +24,7 @@ struct BugReportSlideRootView: View { subTitle1: String(\.slide_bug_report_text_1), subTitle2: String(\.slide_bug_report_text_2), buttonText: String(\.got_it), - buttonAction: { navigationStack.push(CurrenciesRootView(onBaseChange: { _ in })) } + buttonAction: { navigationStack.push(CurrenciesRootView()) } ) .onAppear { analyticsManager.trackScreen(screenName: ScreenName.Slider(position: 2)) diff --git a/ios/CCC/UI/Watchers/WatchersRootView.swift b/ios/CCC/UI/Watchers/WatchersRootView.swift index a8983c2f84..8dc046a903 100644 --- a/ios/CCC/UI/Watchers/WatchersRootView.swift +++ b/ios/CCC/UI/Watchers/WatchersRootView.swift @@ -49,29 +49,13 @@ struct WatchersRootView: View { .sheet( isPresented: $baseBarInfo.isShown, content: { - SelectCurrencyRootView( - isBarShown: $baseBarInfo.isShown, - onCurrencySelected: { - observable.event.onBaseChanged( - watcher: baseBarInfo.watcher!, - newBase: $0 - ) - } - ).environmentObject(navigationStack) + SelectCurrencyRootView(isBarShown: $baseBarInfo.isShown).environmentObject(navigationStack) } ) .sheet( isPresented: $targetBarInfo.isShown, content: { - SelectCurrencyRootView( - isBarShown: $targetBarInfo.isShown, - onCurrencySelected: { - observable.event.onTargetChanged( - watcher: targetBarInfo.watcher!, - newTarget: $0 - ) - } - ).environmentObject(navigationStack) + SelectCurrencyRootView(isBarShown: $targetBarInfo.isShown).environmentObject(navigationStack) } ) .onAppear { From 917be17cd2882fbec0370799814a25b3341a19cc Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 14 Feb 2024 20:14:35 +0300 Subject: [PATCH 063/129] [Oztechan/CCC#3183] Remove onBaseChange event (#3184) * [Oztechan/CCC#3183] Remove onBaseChange event * [Oztechan/CCC#3183] Remove onBaseChange event --- .../content/calculator/CalculatorFragment.kt | 1 - .../viewmodel/calculator/CalculatorSEED.kt | 1 - .../calculator/CalculatorViewModel.kt | 6 ----- .../calculator/CalculatorViewModelTest.kt | 24 ------------------- 4 files changed, 32 deletions(-) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt index e1ed3b5424..4aea3206b1 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt @@ -82,7 +82,6 @@ class CalculatorFragment : BaseVBFragment() { R.id.calculatorFragment )?.observe(viewLifecycleOwner) { Logger.i { "CalculatorFragment observeNavigationResults $it" } - calculatorViewModel.event.onBaseChange(it) } private fun FragmentCalculatorBinding.initViews() = viewLifecycleOwner.lifecycleScope.launch { diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt index 0e45a3f41b..5cb8c1bb21 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt @@ -32,7 +32,6 @@ interface CalculatorEvent : BaseEvent { fun onPasteToInput(text: String) fun onBarClick() fun onSettingsClicked() - fun onBaseChange(base: String) } // Effect diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 9a08c46602..39f52ed7c6 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -300,11 +300,5 @@ class CalculatorViewModel( Logger.d { "CalculatorViewModel onSettingsClicked" } _effect.emit(CalculatorEffect.OpenSettings) } - - override fun onBaseChange(base: String) { - Logger.d { "CalculatorViewModel onBaseChange $base" } - currentBaseChanged(base) - calculateOutput(_state.value.input) - } // endregion } diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index 317d2d458e..54f1cbcebf 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -499,28 +499,4 @@ internal class CalculatorViewModelTest { assertEquals(key, it.input) } } - - @Test - fun onBaseChanged() = runTest { - coEvery { calculationStorage.getBase() } - .returns(currency1.code) - - coEvery { backendApiService.getConversion(currency1.code) } - .returns(conversion) - - viewModel.state.onSubscription { - viewModel.event.onBaseChange(currency1.code) - }.firstOrNull().let { - assertNotNull(it) - assertNotNull(viewModel.data.conversion) - assertEquals(currency1.code, viewModel.data.conversion!!.base) - assertEquals(currency1.code, it.base) - - verify { analyticsManager.trackEvent(Event.BaseChange(Param.Base(currency1.code))) } - .wasInvoked() - - verify { analyticsManager.setUserProperty(UserProperty.BaseCurrency(currency1.code)) } - .wasInvoked() - } - } } From 8227aa45ab8b8932c32225dfe3bb98e7384a4cce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:33:08 +0300 Subject: [PATCH 064/129] [Oztechan/Global#12] Update dependency fastlane-plugin-firebase_app_distribution to v0.9.0 (#3167) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 6a5ce6a640..758bc413a5 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -105,7 +105,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-firebase_app_distribution (0.8.1) + fastlane-plugin-firebase_app_distribution (0.9.0) google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) @@ -144,7 +144,7 @@ GEM google-cloud-core (~> 1.6) googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.9.2) + googleauth (1.10.0) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) @@ -161,7 +161,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.0) nanaimo (0.3.0) naturally (2.2.1) optparse (0.4.0) From ba2563542195f3cfe3216bba44e61623ae03cfcd Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 15 Feb 2024 23:50:08 +0300 Subject: [PATCH 065/129] [Oztechan/CCC#3185] Remove navigation result logics (#3186) * [Oztechan/CCC#3183] Remove onBaseChange event * [Oztechan/CCC#3185] Remove navigation result logics * [Oztechan/CCC#3185] Remove navigation result logics --- .../content/calculator/CalculatorFragment.kt | 13 ------------ .../android/ui/mobile/util/ViewExtensions.kt | 21 ------------------- 2 files changed, 34 deletions(-) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt index 4aea3206b1..d9fe1d548e 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt @@ -19,7 +19,6 @@ import com.oztechan.ccc.android.ui.mobile.util.copyToClipBoard import com.oztechan.ccc.android.ui.mobile.util.dataState import com.oztechan.ccc.android.ui.mobile.util.destroyBanner import com.oztechan.ccc.android.ui.mobile.util.getFromClipBoard -import com.oztechan.ccc.android.ui.mobile.util.getNavigationResult import com.oztechan.ccc.android.ui.mobile.util.setBackgroundByName import com.oztechan.ccc.android.ui.mobile.util.setBannerAd import com.oztechan.ccc.android.ui.mobile.util.showSnack @@ -57,7 +56,6 @@ class CalculatorFragment : BaseVBFragment() { binding.observeStates() binding.setListeners() observeEffects() - observeNavigationResults() } override fun onResume() { @@ -77,13 +75,6 @@ class CalculatorFragment : BaseVBFragment() { super.onDestroyView() } - private fun observeNavigationResults() = getNavigationResult( - CHANGE_BASE_EVENT, - R.id.calculatorFragment - )?.observe(viewLifecycleOwner) { - Logger.i { "CalculatorFragment observeNavigationResults $it" } - } - private fun FragmentCalculatorBinding.initViews() = viewLifecycleOwner.lifecycleScope.launch { recyclerViewMain.adapter = calculatorAdapter } @@ -207,8 +198,4 @@ class CalculatorFragment : BaseVBFragment() { private fun Button.setKeyboardListener() = setOnClickListener { calculatorViewModel.event.onKeyPress(text.toString()) } - - companion object { - const val CHANGE_BASE_EVENT = "change_base" - } } diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt index f355573f7a..077759904a 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt @@ -19,10 +19,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController import com.github.submob.scopemob.castTo -import com.github.submob.scopemob.whether import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.core.ad.BannerAdView import com.oztechan.ccc.android.ui.mobile.R @@ -73,24 +70,6 @@ fun View.animateHeight(startHeight: Int, endHeight: Int) { startAnimation(animation) } -fun Fragment.getNavigationResult( - key: String, - destinationId: Int -) = findNavController() - .currentBackStackEntry - ?.whether { it.destination.id == destinationId } - ?.savedStateHandle - ?.getLiveData(key) - -fun Fragment.setNavigationResult( - destinationId: Int, - result: T, - key: String -) = findNavController() - .previousBackStackEntry - ?.whether { it.destination.id == destinationId } - ?.savedStateHandle?.set(key, result) - fun View?.visibleIf(visible: Boolean, bringFront: Boolean = false) = this?.apply { if (visible) { isVisible = true From 09563c6eabb26dd3d3f29253941469d5313c1619 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:41:55 +0300 Subject: [PATCH 066/129] [Oztechan/Global#12] Update submodule/logmob digest to c575c24 (#3188) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/logmob | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodule/logmob b/submodule/logmob index 6c0ac8873f..c575c24888 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 6c0ac8873fc5ac61518721889f7d5468c128f181 +Subproject commit c575c24888685f38aa31702ca3c9a05a9019900c From 8a492e5406f3594a38f41537a9c6b51cbcb06183 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:35:17 +0300 Subject: [PATCH 067/129] [Oztechan/Global#12] Update dependency com.google.firebase:firebase-config to v21.6.1 (#3169) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 590afc9162..fa584a3a58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ koinKtor = "3.5.3" ktor = "2.3.8" multiplatformSettings = "1.1.1" firebaseAnalytics = "21.5.0" -firebaseRemoteConfig = "21.6.0" +firebaseRemoteConfig = "21.6.1" googleServices = "4.4.0" firebasePer = "20.5.1" firebasePerPlugin = "1.4.2" From b887141a2f26a561f0f13d934c602fce27c63c0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:14:12 +0100 Subject: [PATCH 068/129] [Oztechan/Global#12] Lock file maintenance (#3175) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 758bc413a5..562123fd4f 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.887.0) - aws-sdk-core (3.191.0) + aws-partitions (1.889.0) + aws-sdk-core (3.191.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) @@ -109,7 +109,7 @@ GEM google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.56.0) + google-apis-androidpublisher_v3 (0.57.0) google-apis-core (>= 0.12.0, < 2.a) google-apis-core (0.13.0) addressable (~> 2.5, >= 2.5.1) From 309596010d77cfde5ccb0e3ece0cde068393c9ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:39:34 +0100 Subject: [PATCH 069/129] [Oztechan/Global#12] Update Git Submodules (#3193) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index 5e10192375..c7b19bb6a0 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit 5e10192375319f2845c98acebcc05e0c31a64919 +Subproject commit c7b19bb6a0f2b29bd742fa399cddca38217a02b1 diff --git a/submodule/logmob b/submodule/logmob index c575c24888..b1a546f9e2 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit c575c24888685f38aa31702ca3c9a05a9019900c +Subproject commit b1a546f9e2d6240bf050c46d86312bebe08a2217 diff --git a/submodule/parsermob b/submodule/parsermob index d1616d61fb..eca7ecd501 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit d1616d61fb575225be5f13ba3d678099fadb0703 +Subproject commit eca7ecd501f8ae271c175c5f762c95e15dd97f10 diff --git a/submodule/scopemob b/submodule/scopemob index 562ec2da88..30590dfdf5 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 562ec2da887b60de6240b60f85b6899c32e9ed9c +Subproject commit 30590dfdf55ccd4c52b285d75fabdfe28a25e48a From 8f58a3c75ee50d9c0de6eab9f169fc6d2fa32df8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:39:49 +0100 Subject: [PATCH 070/129] [Oztechan/Global#12] Update gradle/gradle-build-action action to v3.1.0 (#3182) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 721ea51649..02e35cdaaf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -90,7 +90,7 @@ jobs: distribution: 'temurin' - name: Assemble - uses: gradle/gradle-build-action@v3.0.0 + uses: gradle/gradle-build-action@v3.1.0 with: arguments: assemble @@ -262,7 +262,7 @@ jobs: distribution: 'temurin' - name: Run Quality Jobs - uses: gradle/gradle-build-action@v3.0.0 + uses: gradle/gradle-build-action@v3.1.0 with: arguments: check koverMergedXmlReport --parallel @@ -346,7 +346,7 @@ jobs: distribution: 'temurin' - name: Detekt - uses: gradle/gradle-build-action@v3.0.0 + uses: gradle/gradle-build-action@v3.1.0 with: arguments: detektAll diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cbd6b411e..6c84c9a24f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: distribution: 'temurin' - name: Generate Artifacts - uses: gradle/gradle-build-action@v3.0.0 + uses: gradle/gradle-build-action@v3.1.0 with: arguments: :android:app:bundleRelease :backend:app:jar --parallel From 47f5d83b4cf073ebe34a7e85f502941c01d44f81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:43:34 +0100 Subject: [PATCH 071/129] [Oztechan/Global#12] Update dependency com.google.firebase:firebase-analytics to v21.5.1 (#3168) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa584a3a58..03ccc214dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ koinAndroid = "3.5.3" koinKtor = "3.5.3" ktor = "2.3.8" multiplatformSettings = "1.1.1" -firebaseAnalytics = "21.5.0" +firebaseAnalytics = "21.5.1" firebaseRemoteConfig = "21.6.1" googleServices = "4.4.0" firebasePer = "20.5.1" From 5c7a9c7cc1e58c9130f9eae0883e7e71402d9c2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:44:42 +0100 Subject: [PATCH 072/129] [Oztechan/Global#12] Lock file maintenance (#3195) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 562123fd4f..3007dee3f0 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -1,18 +1,21 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.889.0) - aws-sdk-core (3.191.1) + aws-partitions (1.893.0) + aws-sdk-core (3.191.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) + base64 jmespath (~> 1, >= 1.6.1) aws-sdk-kms (1.77.0) aws-sdk-core (~> 3, >= 3.191.0) @@ -24,6 +27,7 @@ GEM aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -144,7 +148,7 @@ GEM google-cloud-core (~> 1.6) googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.10.0) + googleauth (1.11.0) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) @@ -157,13 +161,15 @@ GEM httpclient (2.8.3) jmespath (1.6.2) json (2.7.1) - jwt (2.7.1) + jwt (2.8.0) + base64 mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.0) nanaimo (0.3.0) naturally (2.2.1) + nkf (0.2.0) optparse (0.4.0) os (1.1.4) plist (3.7.1) @@ -179,7 +185,7 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.18.0) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) From afcfb79d25195302be05091daf67d310fd2584c5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 21:17:42 +0100 Subject: [PATCH 073/129] [Oztechan/Global#12] Update dependency com.google.firebase:firebase-perf to v20.5.2 (#3170) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03ccc214dd..ce670cd550 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ multiplatformSettings = "1.1.1" firebaseAnalytics = "21.5.1" firebaseRemoteConfig = "21.6.1" googleServices = "4.4.0" -firebasePer = "20.5.1" +firebasePer = "20.5.2" firebasePerPlugin = "1.4.2" firebaseCrashlyticsPlugin = "2.9.9" googleAds = "22.6.0" From 2a27341f9db03364cbb3b8522d1902abe196d09c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:58:58 +0100 Subject: [PATCH 074/129] [Oztechan/Global#12] Update dependency com.google.gms.google-services to v4.4.1 (#3171) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce670cd550..4140013a13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ ktor = "2.3.8" multiplatformSettings = "1.1.1" firebaseAnalytics = "21.5.1" firebaseRemoteConfig = "21.6.1" -googleServices = "4.4.0" +googleServices = "4.4.1" firebasePer = "20.5.2" firebasePerPlugin = "1.4.2" firebaseCrashlyticsPlugin = "2.9.9" From ebff94ebc0d56dfe1abd494c0c8ed72daed338d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 20:29:06 +0100 Subject: [PATCH 075/129] [Oztechan/Global#12] Update r0adkll/upload-google-play action to v1.1.3 (#3172) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c84c9a24f..fbbc3f0a13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,7 +129,7 @@ jobs: gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch service_account.json.asc > service_account.json - name: Upload Artifact to Google Play Console - uses: r0adkll/upload-google-play@v1.1.2 + uses: r0adkll/upload-google-play@v1.1.3 with: serviceAccountJson: service_account.json packageName: mustafaozhan.github.com.mycurrencies From ff11365a37f06f8ae13d96090e43453830cc2dab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:17:21 +0100 Subject: [PATCH 076/129] [Oztechan/Global#12] Update dependency androidx.compose.compiler:compiler to v1.5.10 (#3197) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4140013a13..dc560ca366 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin = "1.9.22" ksp = "1.9.22-1.0.17" detekt = "1.23.5" androidGradlePlugin = "8.2.2" -composeCompiler = "1.5.9" +composeCompiler = "1.5.10" compose = "1.6.1" glance = "1.0.0" material3 = "1.2.0" From a1579ff4e74a926595f54ce3fce1c87857531551 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:29:27 +0100 Subject: [PATCH 077/129] [Oztechan/Global#12] Update codecov/codecov-action action to v4.0.2 (#3198) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 02e35cdaaf..2b36573f16 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -296,7 +296,7 @@ jobs: path: build - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.0.1 + uses: codecov/codecov-action@v4.0.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: build/report.xml From 73dab3f103912d441d1665f13c1d54a6e34407b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:37:55 +0100 Subject: [PATCH 078/129] [Oztechan/Global#12] Update Git Submodules (#3199) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodule/parsermob b/submodule/parsermob index eca7ecd501..a30123bb23 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit eca7ecd501f8ae271c175c5f762c95e15dd97f10 +Subproject commit a30123bb236d875e570cc6b12ee118ec31ff2b6b diff --git a/submodule/scopemob b/submodule/scopemob index 30590dfdf5..0025635a8c 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 30590dfdf55ccd4c52b285d75fabdfe28a25e48a +Subproject commit 0025635a8c0150fc5a449f619e280c19b2193e26 From 1dde09eecb77366c4c46c5e71dd09515e961ba1f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:38:18 +0100 Subject: [PATCH 079/129] [Oztechan/Global#12] Update compose to v1.6.2 (#3196) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc560ca366..6532423dcb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "1.9.22-1.0.17" detekt = "1.23.5" androidGradlePlugin = "8.2.2" composeCompiler = "1.5.10" -compose = "1.6.1" +compose = "1.6.2" glance = "1.0.0" material3 = "1.2.0" androidDesugaring = "2.0.4" From 29cade90c226b0be7bf2961003f37e97011d5302 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:38:43 +0100 Subject: [PATCH 080/129] [Oztechan/Global#12] Update easingthemes/ssh-deploy action to v5.0.2 (#3194) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbbc3f0a13..fd00942971 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -193,7 +193,7 @@ jobs: path: artifact - name: Deploy to Server - uses: easingthemes/ssh-deploy@v5.0.1 + uses: easingthemes/ssh-deploy@v5.0.2 env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} From bab98bf1ee3241c858f02f59a3e1995f5ffd68a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:03:59 +0100 Subject: [PATCH 081/129] [Oztechan/Global#12] Update coroutines to v1.8.0 (#3187) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6532423dcb..6a3223469b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ huaweiOsm = "1.3.35" navigation = "2.7.7" playCore = "1.10.3" kotlinXDateTime = "0.5.0" -coroutines = "1.7.3" +coroutines = "1.8.0" billing = "6.1.0" leakCanary = "2.13" sqlDelight = "1.5.5" From 69dcb718aed6580a886581ca5401a06f472ff0a1 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 24 Feb 2024 23:23:17 +0100 Subject: [PATCH 082/129] [Oztechan/CCC#3148] Remove default destination for Android (#3149) --- android/ui/mobile/src/main/res/navigation/main_graph.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/ui/mobile/src/main/res/navigation/main_graph.xml b/android/ui/mobile/src/main/res/navigation/main_graph.xml index 6f40016b67..969e245a5b 100644 --- a/android/ui/mobile/src/main/res/navigation/main_graph.xml +++ b/android/ui/mobile/src/main/res/navigation/main_graph.xml @@ -4,8 +4,7 @@ + android:id="@+id/main_graph"> Date: Sun, 25 Feb 2024 12:46:10 +0100 Subject: [PATCH 083/129] [Oztechan/Global#12] Update dependency fastlane to v2.219.0 (#3200) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 3007dee3f0..bac976bd56 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -10,12 +10,11 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.893.0) - aws-sdk-core (3.191.2) + aws-partitions (1.894.0) + aws-sdk-core (3.191.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) - base64 jmespath (~> 1, >= 1.6.1) aws-sdk-kms (1.77.0) aws-sdk-core (~> 3, >= 3.191.0) @@ -115,7 +114,7 @@ GEM gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.57.0) google-apis-core (>= 0.12.0, < 2.a) - google-apis-core (0.13.0) + google-apis-core (0.14.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 1.9) httpclient (>= 2.8.1, < 3.a) @@ -139,7 +138,7 @@ GEM google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) google-cloud-errors (1.3.1) - google-cloud-storage (1.48.1) + google-cloud-storage (1.49.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-core (~> 0.13) From 221faa33558a06b2eff2ed902beb13d79d7d6a9e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:06:58 +0100 Subject: [PATCH 084/129] [Oztechan/Global#12] Update dependency fastlane to v2.219.0 (#3201) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index bac976bd56..83adc56817 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -112,8 +112,8 @@ GEM google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.57.0) - google-apis-core (>= 0.12.0, < 2.a) + google-apis-androidpublisher_v3 (0.58.0) + google-apis-core (>= 0.14.0, < 2.a) google-apis-core (0.14.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 1.9) From c8deea204f19343328c62088c28de6c3e8a6e1af Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 25 Feb 2024 23:07:10 +0100 Subject: [PATCH 085/129] [Oztechan/CCC#3202] Specify gem versions (#3203) --- ios/Gemfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Gemfile b/ios/Gemfile index 0d55ed8d9f..68d227694d 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" -gem "fastlane" -gem "fastlane-plugin-firebase_app_distribution" -gem "xcode-install" +gem "fastlane", "2.218.0" +gem "fastlane-plugin-firebase_app_distribution", "0.8.1" +gem "xcode-install", "2.8.1" From 703baedf3dfb2397e3f2d8729e9d95e63d5c934f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:23:50 +0100 Subject: [PATCH 086/129] [Oztechan/Global#12] Update dependency fastlane to v2.219.0 (#3204) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile | 2 +- ios/Gemfile.lock | 49 ++++++++++++++++++++++++------------------------ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/ios/Gemfile b/ios/Gemfile index 68d227694d..0ad67aee3e 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" -gem "fastlane", "2.218.0" +gem "fastlane", "2.219.0" gem "fastlane-plugin-firebase_app_distribution", "0.8.1" gem "xcode-install", "2.8.1" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 83adc56817..ed30371d6c 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -68,7 +68,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.3.0) - fastlane (2.218.0) + fastlane (2.219.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -87,6 +87,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -108,15 +109,15 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-firebase_app_distribution (0.9.0) + fastlane-plugin-firebase_app_distribution (0.8.1) google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.58.0) - google-apis-core (>= 0.14.0, < 2.a) - google-apis-core (0.14.0) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) - googleauth (~> 1.9) + googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) mini_mime (~> 1.0) representable (~> 3.0) @@ -126,30 +127,28 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-firebaseappdistribution_v1alpha (0.2.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-iamcredentials_v1 (0.18.0) - google-apis-core (>= 0.12.0, < 2.a) - google-apis-playcustomapp_v1 (0.14.0) - google-apis-core (>= 0.12.0, < 2.a) - google-apis-storage_v1 (0.33.0) - google-apis-core (>= 0.12.0, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) google-cloud-core (1.6.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.1) - faraday (>= 1.0, < 3.a) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.1) - google-cloud-storage (1.49.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-core (~> 0.13) - google-apis-iamcredentials_v1 (~> 0.18) - google-apis-storage_v1 (~> 0.33) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) - googleauth (~> 1.9) + googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.11.0) - faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) @@ -223,9 +222,9 @@ PLATFORMS x86_64-linux DEPENDENCIES - fastlane - fastlane-plugin-firebase_app_distribution - xcode-install + fastlane (= 2.219.0) + fastlane-plugin-firebase_app_distribution (= 0.8.1) + xcode-install (= 2.8.1) BUNDLED WITH 2.4.3 From 4e4efc9663380762280c9ce75b6e3f502aa95915 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 25 Feb 2024 23:24:14 +0100 Subject: [PATCH 087/129] [Oztechan/CCC#3189] Move to new getBaseFlow logic (#3190) * [Oztechan/CCC#3183] Remove onBaseChange event * [Oztechan/CCC#3185] Remove navigation result logics * [Oztechan/CCC#3185] Remove navigation result logics * [Oztechan/CCC#3189] Move to new getBaseFlow logic --- .../calculator/CalculatorViewModel.kt | 87 +++++++++---------- .../calculator/CalculatorViewModelTest.kt | 28 ++++++ 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 39f52ed7c6..3483a79112 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -42,7 +42,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @Suppress("TooManyFunctions") @@ -68,21 +67,24 @@ class CalculatorViewModel( // endregion init { - currencyDataSource.getActiveCurrenciesFlow() - .onStart { - _state.update { - copy( - currencyList = currencyDataSource.getActiveCurrencies(), - base = calculationStorage.getBase(), - input = calculationStorage.getLastInput(), - loading = true, - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } - updateConversion() - observeBase() - observeInput() + viewModelScope.launch { + _state.update { + copy( + currencyList = currencyDataSource.getActiveCurrencies(), + base = calculationStorage.getBase(), + input = calculationStorage.getLastInput(), + symbol = currencyDataSource.getCurrencyByCode(calculationStorage.getBase())?.symbol.orEmpty(), + loading = true, + isBannerAdVisible = adControlRepository.shouldShowBannerAd() + ) } + + observeInput() + updateConversion() + calculateOutput(calculationStorage.getLastInput()) + } + + currencyDataSource.getActiveCurrenciesFlow() .onEach { Logger.d { "CalculatorViewModel currencyList changed: ${it.joinToString(",")}" } _state.update { copy(currencyList = it) } @@ -90,15 +92,14 @@ class CalculatorViewModel( analyticsManager.setUserProperty(UserProperty.CurrencyCount(it.count().toString())) } .launchIn(viewModelScope) - } - private fun observeBase() = state.map { it.base } - .distinctUntilChanged() - .onEach { - Logger.d { "CalculatorViewModel observeBase $it" } - currentBaseChanged(it, true) - } - .launchIn(viewModelScope) + calculationStorage.getBaseFlow() + .onEach { + Logger.d { "CalculatorViewModel base changed: $it" } + currentBaseChanged(it) + } + .launchIn(viewModelScope) + } private fun observeInput() = state.map { it.input } .distinctUntilChanged() @@ -188,24 +189,22 @@ class CalculatorViewModel( } } - private fun currentBaseChanged(newBase: String, shouldTrack: Boolean = false) = - viewModelScope.launchIgnored { - data.conversion = null - calculationStorage.setBase(newBase) - _state.update { - copy( - base = newBase, - input = _state.value.input, - symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() - ) - } - - if (shouldTrack) { - analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) - analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) - } + private fun currentBaseChanged(newBase: String) = viewModelScope.launchIgnored { + data.conversion = null + _state.update { + copy( + base = newBase, + input = _state.value.input, + symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() + ) } + analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) + analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) + + calculateOutput(_state.value.input) + } + // region Event override fun onKeyPress(key: String) { Logger.d { "CalculatorViewModel onKeyPress $key" } @@ -223,10 +222,10 @@ class CalculatorViewModel( } } - override fun onItemClick(currency: Currency) = with(currency) { + override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { Logger.d { "CalculatorViewModel onItemClick ${currency.code}" } - val newInput = rate.toSupportedCharacters().let { + val newInput = currency.rate.toSupportedCharacters().let { if (it.last() == CHAR_DOT) { it.dropLast(1) } else { @@ -235,11 +234,9 @@ class CalculatorViewModel( } _state.update { - copy( - base = code, - input = newInput - ) + copy(input = newInput) } + calculationStorage.setBase(currency.code) } override fun onItemImageLongClick(currency: Currency) { diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index 54f1cbcebf..eebd54d0d0 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -98,6 +98,9 @@ internal class CalculatorViewModelTest { every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) + every { calculationStorage.getBaseFlow() } + .returns(flowOf(currency1.code)) + runTest { coEvery { adControlRepository.shouldShowBannerAd() } .returns(shouldShowAds) @@ -177,6 +180,31 @@ internal class CalculatorViewModelTest { assertNotNull(viewModel.data.parser) } + @Test + fun `base changes are observed correctly`() = runTest { + coEvery { calculationStorage.getBase() } + .returns(currency1.code) + + coEvery { calculationStorage.getBaseFlow() } + .returns(flowOf(currency1.code)) + + coEvery { backendApiService.getConversion(currency1.code) } + .returns(conversion) + + viewModel.state.firstOrNull().let { + assertNotNull(it) + assertNotNull(viewModel.data.conversion) + assertEquals(currency1.code, viewModel.data.conversion!!.base) + assertEquals(currency1.code, it.base) + + verify { analyticsManager.trackEvent(Event.BaseChange(Param.Base(currency1.code))) } + .wasInvoked() + + verify { analyticsManager.setUserProperty(UserProperty.BaseCurrency(currency1.code)) } + .wasInvoked() + } + } + @Test fun `when api fails and there is conversion in db then conversion rates are calculated`() = runTest { From 6514748e0ca846171ea608a05b39cecd850739ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:26:09 +0100 Subject: [PATCH 088/129] [Oztechan/Global#12] Update dependency fastlane-plugin-firebase_app_distribution to v0.9.0 (#3205) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile | 2 +- ios/Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Gemfile b/ios/Gemfile index 0ad67aee3e..de7ddb3108 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" gem "fastlane", "2.219.0" -gem "fastlane-plugin-firebase_app_distribution", "0.8.1" +gem "fastlane-plugin-firebase_app_distribution", "0.9.0" gem "xcode-install", "2.8.1" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index ed30371d6c..6bd298e287 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -109,7 +109,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-firebase_app_distribution (0.8.1) + fastlane-plugin-firebase_app_distribution (0.9.0) google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) gh_inspector (1.1.3) @@ -223,7 +223,7 @@ PLATFORMS DEPENDENCIES fastlane (= 2.219.0) - fastlane-plugin-firebase_app_distribution (= 0.8.1) + fastlane-plugin-firebase_app_distribution (= 0.9.0) xcode-install (= 2.8.1) BUNDLED WITH From 8c12628d81f5838753bc8fb9eb2d11a41145f7ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:49:01 +0100 Subject: [PATCH 089/129] [Oztechan/Global#12] Update codecov/codecov-action action to v4.1.0 (#3206) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2b36573f16..4ed4f7bf1a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -296,7 +296,7 @@ jobs: path: build - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.0.2 + uses: codecov/codecov-action@v4.1.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: build/report.xml From cdbc5cac9b5f12147a4a6f4678e134a9f52feaff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:54:04 +0100 Subject: [PATCH 090/129] [Oztechan/Global#12] Update dependency com.huawei.hms:ads-prime to v3.4.69.301 (#3208) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a3223469b..e05b0415ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ firebasePer = "20.5.2" firebasePerPlugin = "1.4.2" firebaseCrashlyticsPlugin = "2.9.9" googleAds = "22.6.0" -huaweiAds = "3.4.69.300" +huaweiAds = "3.4.69.301" huaweiOsm = "1.3.35" navigation = "2.7.7" playCore = "1.10.3" From d8958dc719e705d57650176ea18dc8c324faf4ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:46:56 +0100 Subject: [PATCH 091/129] [Oztechan/Global#12] Update dependency com.google.devtools.ksp to v1.9.22-1.0.18 (#3212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e05b0415ad..9381a3030e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "1.9.22" -ksp = "1.9.22-1.0.17" +ksp = "1.9.22-1.0.18" detekt = "1.23.5" androidGradlePlugin = "8.2.2" composeCompiler = "1.5.10" From 8b0545b44a09506b4040fafa4674f745a986cbd6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:02:29 +0100 Subject: [PATCH 092/129] [Oztechan/Global#12] Update dependency com.google.firebase:firebase-config to v21.6.2 (#3211) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9381a3030e..7d6985e142 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ koinKtor = "3.5.3" ktor = "2.3.8" multiplatformSettings = "1.1.1" firebaseAnalytics = "21.5.1" -firebaseRemoteConfig = "21.6.1" +firebaseRemoteConfig = "21.6.2" googleServices = "4.4.1" firebasePer = "20.5.2" firebasePerPlugin = "1.4.2" From 09bce121a2145d6c858743f1ed35d615aaddc5da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:42:56 +0100 Subject: [PATCH 093/129] [Oztechan/Global#12] Update actions/download-artifact action to v4.1.4 (#3207) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ed4f7bf1a..019385c5fd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,7 +123,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Download Android Artifacts - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.4 with: name: androidArtifacts @@ -222,7 +222,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Download iOS IPA - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.4 with: name: iOSArtifacts path: ios @@ -290,7 +290,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Download Coverage Report - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.4 with: name: coverageReport path: build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd00942971..60ba1c6b1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,7 +119,7 @@ jobs: steps: - name: Download App Bundle - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.4 with: name: googleBundle @@ -155,7 +155,7 @@ jobs: steps: - name: Download App Bundle - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.4 with: name: huaweiBundle @@ -187,7 +187,7 @@ jobs: steps: - name: Download Backend Jar - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.4 with: name: backendJar path: artifact From 14d411cd264b75bc7b3370d8790dfc8e953f55b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 20:01:53 +0100 Subject: [PATCH 094/129] [Oztechan/Global#12] Update actions/setup-java action to v4.1.0 (#3210) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 019385c5fd..f141555a51 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -84,7 +84,7 @@ jobs: gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch agconnect-services.json.asc > android/app/src/release/agconnect-services.json - name: Set up JDK 17 - uses: actions/setup-java@v4.0.0 + uses: actions/setup-java@v4.1.0 with: java-version: 17 distribution: 'temurin' @@ -185,7 +185,7 @@ jobs: gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch Release.xcconfig.asc > ios/CCC/Resources/Release/Config.xcconfig - name: Set up JDK 17 - uses: actions/setup-java@v4.0.0 + uses: actions/setup-java@v4.1.0 with: java-version: 17 distribution: 'temurin' @@ -256,7 +256,7 @@ jobs: submodules: 'recursive' - name: Set up JDK 17 - uses: actions/setup-java@v4.0.0 + uses: actions/setup-java@v4.1.0 with: java-version: 17 distribution: 'temurin' @@ -340,7 +340,7 @@ jobs: submodules: 'recursive' - name: Set up JDK 17 - uses: actions/setup-java@v4.0.0 + uses: actions/setup-java@v4.1.0 with: java-version: 17 distribution: 'temurin' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60ba1c6b1b..4eef0a81d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,7 +79,7 @@ jobs: gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch agconnect-services.json.asc > android/app/src/release/agconnect-services.json - name: Set up JDK 17 - uses: actions/setup-java@v4.0.0 + uses: actions/setup-java@v4.1.0 with: java-version: 17 distribution: 'temurin' @@ -223,7 +223,7 @@ jobs: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4.0.0 + uses: actions/setup-java@v4.1.0 with: java-version: 17 distribution: 'temurin' From c9e87535b5e834a206f16eb5fcdbc12778e62649 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 20:02:06 +0100 Subject: [PATCH 095/129] [Oztechan/Global#12] Update easingthemes/ssh-deploy action to v5.0.3 (#3209) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4eef0a81d0..c91f29c884 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -193,7 +193,7 @@ jobs: path: artifact - name: Deploy to Server - uses: easingthemes/ssh-deploy@v5.0.2 + uses: easingthemes/ssh-deploy@v5.0.3 env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} From 51819d0ed3fa606bd64c9ceac513d3abfd99d8f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 19:15:17 +0100 Subject: [PATCH 096/129] [Oztechan/Global#12] Update dependency com.google.firebase:firebase-config to v21.6.3 (#3222) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d6985e142..2123ee753f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ koinKtor = "3.5.3" ktor = "2.3.8" multiplatformSettings = "1.1.1" firebaseAnalytics = "21.5.1" -firebaseRemoteConfig = "21.6.2" +firebaseRemoteConfig = "21.6.3" googleServices = "4.4.1" firebasePer = "20.5.2" firebasePerPlugin = "1.4.2" From 386865cb960cec08cde85af4cea82ea0594628c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 19:42:53 +0100 Subject: [PATCH 097/129] [Oztechan/Global#12] Update dependency com.google.android.gms:play-services-ads to v23 (#3223) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2123ee753f..f68b7aee40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ googleServices = "4.4.1" firebasePer = "20.5.2" firebasePerPlugin = "1.4.2" firebaseCrashlyticsPlugin = "2.9.9" -googleAds = "22.6.0" +googleAds = "23.0.0" huaweiAds = "3.4.69.301" huaweiOsm = "1.3.35" navigation = "2.7.7" From 7300ce9f86218df324ba7ba815ec898706cbf200 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 22:17:05 +0100 Subject: [PATCH 098/129] [Oztechan/Global#12] Update ktor to v2.3.9 (#3216) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f68b7aee40..72435d5489 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ koinCore = "3.5.3" koinCompose = "3.5.3" koinAndroid = "3.5.3" koinKtor = "3.5.3" -ktor = "2.3.8" +ktor = "2.3.9" multiplatformSettings = "1.1.1" firebaseAnalytics = "21.5.1" firebaseRemoteConfig = "21.6.3" From 04ec34fd1e9f14d0a7b6cb658104cdeae60c9fb2 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 8 Mar 2024 23:05:23 +0100 Subject: [PATCH 099/129] [Oztechan/Global#3224] Revert android gradle version updates in submodules (#3225) * [Oztechan/Global#3224] Revert android gradle version updates in submodules * [Oztechan/Global#3224] Revert android gradle version updates in submodules --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index c7b19bb6a0..dd9864500d 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit c7b19bb6a0f2b29bd742fa399cddca38217a02b1 +Subproject commit dd9864500de7fd28b5a680aeac55a310e0c89fa5 diff --git a/submodule/logmob b/submodule/logmob index b1a546f9e2..8bdb0e5a78 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit b1a546f9e2d6240bf050c46d86312bebe08a2217 +Subproject commit 8bdb0e5a78426a19e5df873d74c2f6d1479e2d74 diff --git a/submodule/parsermob b/submodule/parsermob index a30123bb23..17444b7fcc 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit a30123bb236d875e570cc6b12ee118ec31ff2b6b +Subproject commit 17444b7fcc2bde244856c5aa761fbd03c13ac4bf diff --git a/submodule/scopemob b/submodule/scopemob index 0025635a8c..f6d9e0f02a 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 0025635a8c0150fc5a449f619e280c19b2193e26 +Subproject commit f6d9e0f02ac2222636204f16cf2e8e71df494d08 From 55d0e433b296da9d8502348d1667ee105eb1e7bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 15:39:13 +0100 Subject: [PATCH 100/129] [Oztechan/Global#12] Update dependency androidx.compose.material3:material3 to v1.2.1 (#3218) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 72435d5489..e1244e7531 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidGradlePlugin = "8.2.2" composeCompiler = "1.5.10" compose = "1.6.2" glance = "1.0.0" -material3 = "1.2.0" +material3 = "1.2.1" androidDesugaring = "2.0.4" androidMaterial = "1.11.0" composeActivity = "1.8.2" From aee04152c393d3456b937e7530e310b6c2685e35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 15:39:35 +0100 Subject: [PATCH 101/129] [Oztechan/Global#12] Update dependency com.android.billingclient:billing to v6.2.0 (#3219) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1244e7531..4a09581df3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ navigation = "2.7.7" playCore = "1.10.3" kotlinXDateTime = "0.5.0" coroutines = "1.8.0" -billing = "6.1.0" +billing = "6.2.0" leakCanary = "2.13" sqlDelight = "1.5.5" lifecycle = "2.7.0" From abb65c2c1fabfb2004cf02e18799e3b64d13b888 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 15:39:42 +0100 Subject: [PATCH 102/129] [Oztechan/Global#12] Update dependency com.huawei.hms:ads-prime to v3.4.69.302 (#3220) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a09581df3..29b79cd47a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ firebasePer = "20.5.2" firebasePerPlugin = "1.4.2" firebaseCrashlyticsPlugin = "2.9.9" googleAds = "23.0.0" -huaweiAds = "3.4.69.301" +huaweiAds = "3.4.69.302" huaweiOsm = "1.3.35" navigation = "2.7.7" playCore = "1.10.3" From 1da0187d74b0a555069ee18bfd265a5b9c054788 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:45:19 +0100 Subject: [PATCH 103/129] [Oztechan/Global#12] Lock file maintenance (#3228) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 6bd298e287..5b3aaf5971 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -7,10 +7,10 @@ GEM rexml addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.894.0) + aws-partitions (1.896.0) aws-sdk-core (3.191.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -133,12 +133,12 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.7.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) + google-cloud-errors (1.4.0) google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -159,7 +159,7 @@ GEM httpclient (2.8.3) jmespath (1.6.2) json (2.7.1) - jwt (2.8.0) + jwt (2.8.1) base64 mini_magick (4.12.0) mini_mime (1.1.5) From 621b6acd2094581c620a33300a91384186c53d41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 22:39:01 +0100 Subject: [PATCH 104/129] [Oztechan/Global#12] Update compose to v1.6.3 (#3217) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29b79cd47a..3f0c929a8c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "1.9.22-1.0.18" detekt = "1.23.5" androidGradlePlugin = "8.2.2" composeCompiler = "1.5.10" -compose = "1.6.2" +compose = "1.6.3" glance = "1.0.0" material3 = "1.2.1" androidDesugaring = "2.0.4" From f025927104e4960531d818f71cc87abef8caa338 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:32:59 +0100 Subject: [PATCH 105/129] [Oztechan/Global#12] Update Git Submodules (#3215) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index dd9864500d..e017248bea 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit dd9864500de7fd28b5a680aeac55a310e0c89fa5 +Subproject commit e017248beac645f0ac54e3543d813625d841baa7 diff --git a/submodule/logmob b/submodule/logmob index 8bdb0e5a78..6d38a642ec 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 8bdb0e5a78426a19e5df873d74c2f6d1479e2d74 +Subproject commit 6d38a642ec9767d88b62096ef3f442d4374d03c6 diff --git a/submodule/parsermob b/submodule/parsermob index 17444b7fcc..116c8b0165 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 17444b7fcc2bde244856c5aa761fbd03c13ac4bf +Subproject commit 116c8b0165d92835822dcb47f8dc7d1fec5ec75c diff --git a/submodule/scopemob b/submodule/scopemob index f6d9e0f02a..a54dd3cde5 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit f6d9e0f02ac2222636204f16cf2e8e71df494d08 +Subproject commit a54dd3cde59f3d88b58f094d8ad0f4a2ccc81a5e From 396b95e837cde58fb28da1047e4f1a811b09ba05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:33:14 +0100 Subject: [PATCH 106/129] [Oztechan/Global#12] Update actions/setup-java action to v4.2.1 (#3230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- .github/workflows/main.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f141555a51..6062a62d6d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -84,7 +84,7 @@ jobs: gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch agconnect-services.json.asc > android/app/src/release/agconnect-services.json - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@v4.2.1 with: java-version: 17 distribution: 'temurin' @@ -185,7 +185,7 @@ jobs: gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch Release.xcconfig.asc > ios/CCC/Resources/Release/Config.xcconfig - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@v4.2.1 with: java-version: 17 distribution: 'temurin' @@ -256,7 +256,7 @@ jobs: submodules: 'recursive' - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@v4.2.1 with: java-version: 17 distribution: 'temurin' @@ -340,7 +340,7 @@ jobs: submodules: 'recursive' - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@v4.2.1 with: java-version: 17 distribution: 'temurin' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c91f29c884..22451c5da5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,7 +79,7 @@ jobs: gpg -d --passphrase "${{ secrets.SECRET_PASSWORD }}" --batch agconnect-services.json.asc > android/app/src/release/agconnect-services.json - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@v4.2.1 with: java-version: 17 distribution: 'temurin' @@ -223,7 +223,7 @@ jobs: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@v4.2.1 with: java-version: 17 distribution: 'temurin' From e5c80a5460af0cb8bdb7ca8e2582c1a10129eedb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 23:00:57 +0100 Subject: [PATCH 107/129] [Oztechan/Global#12] Update dependency com.lemonappdev:konsist to v0.14.0 (#3229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f0c929a8c..42dfd511c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ rootBeer = "0.1.0" mockative = "2.0.1" anrWatchDog = "1.4.0" kermit = "2.0.3" -konsist = "0.13.0" +konsist = "0.14.0" [libraries] # COMMON From 409d077266add8e2eaf91e392cb2c3c6587c2d79 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:45:27 +0100 Subject: [PATCH 108/129] [Oztechan/Global#12] Update Git Submodules (#3231) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index e017248bea..13fe6ad1ab 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit e017248beac645f0ac54e3543d813625d841baa7 +Subproject commit 13fe6ad1abb6ba0731392ceda64c8c59dfa902b4 diff --git a/submodule/logmob b/submodule/logmob index 6d38a642ec..4fff698afa 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 6d38a642ec9767d88b62096ef3f442d4374d03c6 +Subproject commit 4fff698afa2e0c401e5c7f373aa9b5c273184eca diff --git a/submodule/parsermob b/submodule/parsermob index 116c8b0165..e56ff1b6e9 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 116c8b0165d92835822dcb47f8dc7d1fec5ec75c +Subproject commit e56ff1b6e9011c25063104b552e7444a5c0a6b1a diff --git a/submodule/scopemob b/submodule/scopemob index a54dd3cde5..644db5a442 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit a54dd3cde59f3d88b58f094d8ad0f4a2ccc81a5e +Subproject commit 644db5a44271c15c972a07165e899c4ec3a703d1 From 721600b183965f210d98fe5710e1f605721d719f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Mar 2024 16:54:31 +0100 Subject: [PATCH 109/129] [Oztechan/Global#12] Update Git Submodules (#3232) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodule/parsermob b/submodule/parsermob index e56ff1b6e9..3041ab8c6c 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit e56ff1b6e9011c25063104b552e7444a5c0a6b1a +Subproject commit 3041ab8c6ce2191fc798581b9fbe0115b224c8cc diff --git a/submodule/scopemob b/submodule/scopemob index 644db5a442..f51d488ded 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 644db5a44271c15c972a07165e899c4ec3a703d1 +Subproject commit f51d488ded59bd2f3f7935b870fd2bfb6b308af1 From 5c263025e491e82a47859265fee08805edb34626 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:27:11 +0100 Subject: [PATCH 110/129] [Oztechan/Global#12] Lock file maintenance (#3233) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 5b3aaf5971..9e16c5e9a1 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -10,8 +10,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.896.0) - aws-sdk-core (3.191.3) + aws-partitions (1.898.0) + aws-sdk-core (3.191.4) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) @@ -19,7 +19,7 @@ GEM aws-sdk-kms (1.77.0) aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.143.0) + aws-sdk-s3 (1.145.0) aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) @@ -38,7 +38,7 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.109.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) From 87203135419059de4f08e71eb2c72bc53594ff56 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 20 Mar 2024 11:44:36 +0100 Subject: [PATCH 111/129] [Oztechan/CCC#3235] Use exact versions of SPM dependencies (#3236) --- ios/CCC.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index cd637617a7..54b35017af 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -801,32 +801,32 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/exyte/PopupView.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + kind = exactVersion; + version = 1.3.1; }; }; 5C3EB6D628775AFF001E822A /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/googleads/swift-package-manager-google-mobile-ads.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 9.0.0; + kind = exactVersion; + version = 9.14.0; }; }; 5C5C0BA22874B8450061AEF9 /* XCRemoteSwiftPackageReference "swiftui-navigation-stack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/matteopuc/swiftui-navigation-stack"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.6; + kind = exactVersion; + version = 1.0.6; }; }; 5CF0622C28AD93CE00C579F6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 9.0.0; + kind = exactVersion; + version = 9.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ From cddee813808ad7a53c7330bb98e53eb4a2906016 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 20 Mar 2024 11:51:04 +0100 Subject: [PATCH 112/129] [Oztechan/CCC#3237] Add Package.swift for Renovate (#3238) --- ios/Package.swift | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 ios/Package.swift diff --git a/ios/Package.swift b/ios/Package.swift new file mode 100644 index 0000000000..e2f6530ac3 --- /dev/null +++ b/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "CCC", + products: [ + .library( + name: "CCC", + targets: ["CCC"]), + ], + dependencies: [ + .package( + url: "https://github.com/firebase/firebase-ios-sdk", + from: "9.6.0" + ), + .package( + url: "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", + from: "9.14.0" + ), + .package( + url: "https://github.com/exyte/PopupView.git", + from: "1.3.1" + ), + .package( + url: "https://github.com/matteopuc/swiftui-navigation-stack", + from: "1.0.6" + ) + ], + targets: [ + .target(name: "CCC") + ] +) From b2ade7fe858d15f3d0bd625e463076159485930f Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Wed, 20 Mar 2024 14:37:32 +0100 Subject: [PATCH 113/129] [Oztechan/CCC#3241] Add Package.swift into project structure and fix swiftlint issues (#3242) --- ios/CCC.xcodeproj/project.pbxproj | 2 ++ ios/Package.swift | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index 54b35017af..fbde359f50 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ 5C9C75C72603A36A00D66FDD /* ToolbarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButton.swift; sourceTree = ""; }; 5CACB69328A7EF1800A2D13C /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 5CB954BE26932408007632DC /* AdaptiveBannerAdView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveBannerAdView.swift; sourceTree = ""; }; + 5CB9E3CD2BAAF88400B1F1A9 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 5CBFC9462AC2E43800E2CE61 /* KermitExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KermitExt.swift; sourceTree = ""; }; 5CCFB5872A9B9F13002DF46B /* BugReportSlideRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportSlideRootView.swift; sourceTree = ""; }; 5CCFB5892A9B9F8F002DF46B /* IntroSlideRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroSlideRootView.swift; sourceTree = ""; }; @@ -368,6 +369,7 @@ isa = PBXGroup; children = ( 7555FF7D242A565900829871 /* CCC */, + 5CB9E3CD2BAAF88400B1F1A9 /* Package.swift */, 7555FF7C242A565900829871 /* Products */, ); sourceTree = ""; diff --git a/ios/Package.swift b/ios/Package.swift index e2f6530ac3..983025ab60 100644 --- a/ios/Package.swift +++ b/ios/Package.swift @@ -8,7 +8,8 @@ let package = Package( products: [ .library( name: "CCC", - targets: ["CCC"]), + targets: ["CCC"] + ) ], dependencies: [ .package( From 3eb929cd519b8418e05c2d2e445c2ad40ea3ef72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 18:57:10 +0100 Subject: [PATCH 114/129] [Oztechan/Global#12] Update dependency googleads/swift-package-manager-google-mobile-ads to v11 (#3243) * [Oztechan/Global#12] Update dependency googleads/swift-package-manager-google-mobile-ads to v11 Co-authored-by: Mustafa Ozhan * [Oztechan/Global#12] Update dependency googleads/swift-package-manager-google-mobile-ads to v11 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- ios/CCC.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 16 ++++++++-------- ios/Package.swift | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index fbde359f50..fe907b674c 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -812,7 +812,7 @@ repositoryURL = "https://github.com/googleads/swift-package-manager-google-mobile-ads.git"; requirement = { kind = exactVersion; - version = 9.14.0; + version = 11.2.0; }; }; 5C5C0BA22874B8450061AEF9 /* XCRemoteSwiftPackageReference "swiftui-navigation-stack" */ = { diff --git a/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved index dd3b1af3fe..6ba4fa5a3f 100644 --- a/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "98a00258d4518b7521253a70b7f70bb76d2120fe", - "version" : "9.2.4" + "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", + "version" : "9.4.0" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "58d03d22beae762eaddbd30cb5a61af90d4b309f", - "version" : "7.11.3" + "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55", + "version" : "7.13.1" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a", - "version" : "2.2.0" + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", "state" : { - "revision" : "78f4c64d0f1900e54529e4c78075d663ca67d6e0", - "version" : "9.14.0" + "revision" : "5ff977255c2ba5844e7e9da779f1f2a5a00e0028", + "version" : "11.2.0" } }, { diff --git a/ios/Package.swift b/ios/Package.swift index 983025ab60..3ba3e338b7 100644 --- a/ios/Package.swift +++ b/ios/Package.swift @@ -18,7 +18,7 @@ let package = Package( ), .package( url: "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", - from: "9.14.0" + from: "11.2.0" ), .package( url: "https://github.com/exyte/PopupView.git", From 48d3391027308e19c8671105f40c6fcda7abfad1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 19:28:34 +0100 Subject: [PATCH 115/129] [Oztechan/Global#12] Update dependency firebase/firebase-ios-sdk to v10 (#3240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- ios/CCC.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 61 +++++++++++-------- ios/Package.swift | 2 +- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index fe907b674c..61a6dd8a08 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -828,7 +828,7 @@ repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; requirement = { kind = exactVersion; - version = 9.6.0; + version = 10.23.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6ba4fa5a3f..a28111c72b 100644 --- a/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/CCC.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,21 +1,21 @@ { "pins" : [ { - "identity" : "abseil-cpp-swiftpm", + "identity" : "abseil-cpp-binary", "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", + "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1", - "version" : "0.20220203.2" + "revision" : "7ce7be095bc3ed3c98b009532fe2d7698c132614", + "version" : "1.2024011601.0" } }, { - "identity" : "boringssl-swiftpm", + "identity" : "app-check", "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/boringssl-SwiftPM.git", + "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", - "version" : "0.9.1" + "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", + "version" : "10.18.1" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "7e80c25b51c2ffa238879b07fbfc5baa54bb3050", - "version" : "9.6.0" + "revision" : "fcf5ced6dae2d43fced2581e673cc3b59bdb8ffa", + "version" : "10.23.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "c1cfde8067668027b23a42c29d11c246152fe046", - "version" : "9.6.0" + "revision" : "6ec4ca62b00a665fa09b594fab897753a8c635fa", + "version" : "10.23.0" } }, { @@ -55,12 +55,12 @@ } }, { - "identity" : "grpc-ios", + "identity" : "grpc-binary", "kind" : "remoteSourceControl", - "location" : "https://github.com/grpc/grpc-ios.git", + "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6", - "version" : "1.44.3-grpc" + "revision" : "67043f6389d0e28b38fa02d1c6952afeb04d807f", + "version" : "1.62.1" } }, { @@ -68,8 +68,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "5ccda3981422a84186387dbb763ba739178b529c", - "version" : "2.3.0" + "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", + "version" : "3.3.1" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" } }, { @@ -77,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/leveldb.git", "state" : { - "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", - "version" : "1.22.2" + "revision" : "43aaef65e0c665daadf848761d560e446d350d3d", + "version" : "1.22.4" } }, { @@ -86,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/nanopb.git", "state" : { - "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", - "version" : "2.30909.0" + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" } }, { @@ -122,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/swift-package-manager-google-user-messaging-platform.git", "state" : { - "revision" : "3b924ce3313a5fd2fc6a8dc889a8c38f76890fe3", - "version" : "2.0.0" + "revision" : "cfe8b2ae16b9bc81f4cdf1d1a12a01a452489c32", + "version" : "2.3.0" } }, { @@ -131,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "f25867a208f459d3c5a06935dceb9083b11cd539", - "version" : "1.22.0" + "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", + "version" : "1.25.2" } }, { diff --git a/ios/Package.swift b/ios/Package.swift index 3ba3e338b7..f2249ec3b7 100644 --- a/ios/Package.swift +++ b/ios/Package.swift @@ -14,7 +14,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/firebase/firebase-ios-sdk", - from: "9.6.0" + from: "10.23.0" ), .package( url: "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", From 375da22506004af9a1337254eef32a7c936cd8eb Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 21 Mar 2024 15:39:06 +0100 Subject: [PATCH 116/129] [Oztechan/CCC#3191] Add Flow Property for lastInput (#3192) * [Oztechan/CCC#3183] Remove onBaseChange event * [Oztechan/CCC#3185] Remove navigation result logics * [Oztechan/CCC#3185] Remove navigation result logics * [Oztechan/CCC#3189] Move to new getBaseFlow logic * [Oztechan/CCC#3191] Add Flow Property for lastInput --- .../storage/calculation/CalculationStorage.kt | 7 +-- .../calculation/CalculationStorageImpl.kt | 17 ++++--- .../calculation/CalculationStorageTest.kt | 47 ++++++++++++------- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt index 9e02d18b68..a4f7c1b7c6 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt @@ -7,9 +7,10 @@ interface CalculationStorage { suspend fun getBase(): String suspend fun setBase(value: String) - suspend fun getPrecision(): Int - suspend fun setPrecision(value: Int) - + fun getLastInputFlow(): Flow suspend fun getLastInput(): String suspend fun setLastInput(value: String) + + suspend fun getPrecision(): Int + suspend fun setPrecision(value: Int) } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index be847f8b5b..4ab52552b4 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -17,11 +17,8 @@ internal class CalculationStorageImpl( override suspend fun setBase(value: String) = suspendPersistence.setSuspend(KEY_CURRENT_BASE, value) - override suspend fun getPrecision(): Int = - suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) - - override suspend fun setPrecision(value: Int) = - suspendPersistence.setSuspend(KEY_PRECISION, value) + override fun getLastInputFlow(): Flow = + flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) override suspend fun getLastInput(): String = suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) @@ -29,13 +26,19 @@ internal class CalculationStorageImpl( override suspend fun setLastInput(value: String) = suspendPersistence.setSuspend(KEY_LAST_INPUT, value) + override suspend fun getPrecision(): Int = + suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) + + override suspend fun setPrecision(value: Int) = + suspendPersistence.setSuspend(KEY_PRECISION, value) + companion object { internal const val DEFAULT_CURRENT_BASE = "" - internal const val DEFAULT_PRECISION = 3 internal const val DEFAULT_LAST_INPUT = "" + internal const val DEFAULT_PRECISION = 3 internal const val KEY_CURRENT_BASE = "current_base" - internal const val KEY_PRECISION = "precision" internal const val KEY_LAST_INPUT = "last_input" + internal const val KEY_PRECISION = "precision" } } diff --git a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt index d01db16d9e..801e7ef5ae 100644 --- a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt +++ b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt @@ -43,6 +43,17 @@ internal class CalculationStorageTest { .wasInvoked() } + @Test + fun getLastInputFlow() = runTest { + every { flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + .returns(flowOf(DEFAULT_LAST_INPUT)) + + assertEquals(DEFAULT_LAST_INPUT, subject.getLastInputFlow().first()) + + coVerify { flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + .wasInvoked() + } + // defaults @Test fun `get default base`() = runTest { @@ -56,24 +67,24 @@ internal class CalculationStorageTest { } @Test - fun `get default precision`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } - .returns(DEFAULT_PRECISION) + fun `get default lastInput`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + .returns(DEFAULT_LAST_INPUT) - assertEquals(DEFAULT_PRECISION, subject.getPrecision()) + assertEquals(DEFAULT_LAST_INPUT, subject.getLastInput()) - coVerify { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } + coVerify { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } .wasInvoked() } @Test - fun `get default lastInput`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } - .returns(DEFAULT_LAST_INPUT) + fun `get default precision`() = runTest { + coEvery { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } + .returns(DEFAULT_PRECISION) - assertEquals(DEFAULT_LAST_INPUT, subject.getLastInput()) + assertEquals(DEFAULT_PRECISION, subject.getPrecision()) - coVerify { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + coVerify { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } .wasInvoked() } @@ -88,20 +99,20 @@ internal class CalculationStorageTest { } @Test - fun `set precision`() = runTest { - val mockValue = Random.nextInt() - subject.setPrecision(mockValue) + fun `set lastInput`() = runTest { + val mockValue = "mock" + subject.setLastInput(mockValue) - coVerify { suspendPersistence.setSuspend(KEY_PRECISION, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_LAST_INPUT, mockValue) } .wasInvoked() } @Test - fun `set lastInput`() = runTest { - val mockValue = "mock" - subject.setLastInput(mockValue) + fun `set precision`() = runTest { + val mockValue = Random.nextInt() + subject.setPrecision(mockValue) - coVerify { suspendPersistence.setSuspend(KEY_LAST_INPUT, mockValue) } + coVerify { suspendPersistence.setSuspend(KEY_PRECISION, mockValue) } .wasInvoked() } } From 367c4fefc0ce0f23cd72b3c87b4c23d5d918c79d Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Thu, 21 Mar 2024 22:35:27 +0100 Subject: [PATCH 117/129] [Oztechan/Global#3246] Use combine for combining different datasources for calculator (#3247) --- .../calculator/CalculatorViewModel.kt | 64 +++++++++---------- .../calculator/CalculatorViewModelTest.kt | 45 +++++++------ 2 files changed, 52 insertions(+), 57 deletions(-) diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 3483a79112..064d94ae06 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -38,10 +38,10 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @Suppress("TooManyFunctions") @@ -67,6 +67,32 @@ class CalculatorViewModel( // endregion init { + combine( + currencyDataSource.getActiveCurrenciesFlow(), + calculationStorage.getBaseFlow(), + state.map { it.input }.distinctUntilChanged(), + ) { activeCurrencies, base, input -> + Logger.d { + """ + CalculatorViewModel combined values: + base: $base, input: $input, activeCurrencies: ${activeCurrencies.joinToString(",") { it.code }} + """.trimIndent() + } + + _state.update { copy(currencyList = activeCurrencies) } + + analyticsManager.setUserProperty( + UserProperty.CurrencyCount(activeCurrencies.count().toString()) + ) + + if (state.value.base != base) { + currentBaseChanged(base, input) + } + + calculateOutput(input) + calculationStorage.setLastInput(input) + }.launchIn(viewModelScope) + viewModelScope.launch { _state.update { copy( @@ -78,38 +104,9 @@ class CalculatorViewModel( isBannerAdVisible = adControlRepository.shouldShowBannerAd() ) } - - observeInput() - updateConversion() - calculateOutput(calculationStorage.getLastInput()) } - - currencyDataSource.getActiveCurrenciesFlow() - .onEach { - Logger.d { "CalculatorViewModel currencyList changed: ${it.joinToString(",")}" } - _state.update { copy(currencyList = it) } - - analyticsManager.setUserProperty(UserProperty.CurrencyCount(it.count().toString())) - } - .launchIn(viewModelScope) - - calculationStorage.getBaseFlow() - .onEach { - Logger.d { "CalculatorViewModel base changed: $it" } - currentBaseChanged(it) - } - .launchIn(viewModelScope) } - private fun observeInput() = state.map { it.input } - .distinctUntilChanged() - .onEach { - Logger.d { "CalculatorViewModel observeInput $it" } - calculationStorage.setLastInput(it) - calculateOutput(it) - } - .launchIn(viewModelScope) - private suspend fun updateConversion() { _state.update { copy(loading = true) } @@ -189,20 +186,19 @@ class CalculatorViewModel( } } - private fun currentBaseChanged(newBase: String) = viewModelScope.launchIgnored { + private fun currentBaseChanged(newBase: String, input: String) = viewModelScope.launchIgnored { + Logger.d { "CalculatorViewModel currentBaseChanged $newBase" } data.conversion = null _state.update { copy( base = newBase, - input = _state.value.input, + input = input, symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() ) } analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) - - calculateOutput(_state.value.input) } // region Event diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index eebd54d0d0..fca71f857a 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -47,6 +47,8 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue internal class CalculatorViewModelTest { @@ -149,7 +151,7 @@ internal class CalculatorViewModelTest { assertEquals(ConversionState.Online(nowAsDateString()), it.conversionState) assertEquals(currencyList, it.currencyList) assertEquals(shouldShowAds, it.isBannerAdVisible) - assertFalse { it.loading } + assertTrue { it.loading } } coVerify { adControlRepository.shouldShowBannerAd() } @@ -256,33 +258,30 @@ internal class CalculatorViewModelTest { } @Test - fun `when api fails and there is no offline and no enough currency few currency effect emitted`() = - runTest { - coEvery { backendApiService.getConversion(currency1.code) } - .throws(Exception()) - - coEvery { conversionDataSource.getConversionByBase(currency1.code) } - .returns(null) - - every { currencyDataSource.getActiveCurrenciesFlow() } - .returns(flowOf(listOf(currency1))) + fun `when there is few currency app doesn't make API call or search in DB`() = runTest { + every { currencyDataSource.getActiveCurrenciesFlow() } + .returns(flowOf(listOf(currency1))) - viewModel.effect.onSubscription { - viewModel.event.onKeyPress("1") // trigger api call - }.firstOrNull().let { - assertIs(it) + viewModel.effect.onSubscription { + viewModel.event.onKeyPress("1") // trigger api call + }.firstOrNull().let { + assertIs(it) - viewModel.state.value.let { state -> - assertNotNull(state) - assertFalse { state.loading } - assertEquals(ConversionState.Error, state.conversionState) - } + viewModel.state.value.let { state -> + assertNotNull(state) + assertFalse { state.loading } + assertNull(viewModel.data.conversion) + assertEquals(ConversionState.None, state.conversionState) } - - coVerify { conversionDataSource.getConversionByBase(currency1.code) } - .wasInvoked() } + coVerify { conversionDataSource.getConversionByBase(currency1.code) } + .wasNotInvoked() + + coVerify { backendApiService.getConversion(currency1.code) } + .wasNotInvoked() + } + @Test fun `when input is too long it should drop the last digit and give TooBigInput effect`() = runTest { From 348973687579b19a3f9b735c0eb098572c9b112a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:16:24 +0100 Subject: [PATCH 118/129] [Oztechan/Global#12] Update dependency com.google.firebase:firebase-analytics to v21.6.1 (#3245) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42dfd511c8..3b1a59b7ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ koinAndroid = "3.5.3" koinKtor = "3.5.3" ktor = "2.3.9" multiplatformSettings = "1.1.1" -firebaseAnalytics = "21.5.1" +firebaseAnalytics = "21.6.1" firebaseRemoteConfig = "21.6.3" googleServices = "4.4.1" firebasePer = "20.5.2" From 7f1b3644303843fb6d6cf7d1fd9a9820ef3c8e25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 18:48:33 +0100 Subject: [PATCH 119/129] [Oztechan/Global#12] Update dependency gradle to v8.7 (#3248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 29 ++-- gradlew.bat | 184 +++++++++++------------ 4 files changed, 111 insertions(+), 105 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vSTxF-Vi3+ZOI=Thq2} zyQgjYY1_7^ZQHh{?P))4+qUiQJLi1&{yE>h?~jU%tjdV0h|FENbM3X(KnJdPKc?~k zh=^Ixv*+smUll!DTWH!jrV*wSh*(mx0o6}1@JExzF(#9FXgmTXVoU+>kDe68N)dkQ zH#_98Zv$}lQwjKL@yBd;U(UD0UCl322=pav<=6g>03{O_3oKTq;9bLFX1ia*lw;#K zOiYDcBJf)82->83N_Y(J7Kr_3lE)hAu;)Q(nUVydv+l+nQ$?|%MWTy`t>{havFSQloHwiIkGK9YZ79^9?AZo0ZyQlVR#}lF%dn5n%xYksXf8gnBm=wO7g_^! zauQ-bH1Dc@3ItZ-9D_*pH}p!IG7j8A_o94#~>$LR|TFq zZ-b00*nuw|-5C2lJDCw&8p5N~Z1J&TrcyErds&!l3$eSz%`(*izc;-?HAFD9AHb-| z>)id`QCrzRws^9(#&=pIx9OEf2rmlob8sK&xPCWS+nD~qzU|qG6KwA{zbikcfQrdH z+ zQg>O<`K4L8rN7`GJB0*3<3`z({lWe#K!4AZLsI{%z#ja^OpfjU{!{)x0ZH~RB0W5X zTwN^w=|nA!4PEU2=LR05x~}|B&ZP?#pNgDMwD*ajI6oJqv!L81gu=KpqH22avXf0w zX3HjbCI!n9>l046)5rr5&v5ja!xkKK42zmqHzPx$9Nn_MZk`gLeSLgC=LFf;H1O#B zn=8|^1iRrujHfbgA+8i<9jaXc;CQBAmQvMGQPhFec2H1knCK2x!T`e6soyrqCamX% zTQ4dX_E*8so)E*TB$*io{$c6X)~{aWfaqdTh=xEeGvOAN9H&-t5tEE-qso<+C!2>+ zskX51H-H}#X{A75wqFe-J{?o8Bx|>fTBtl&tcbdR|132Ztqu5X0i-pisB-z8n71%q%>EF}yy5?z=Ve`}hVh{Drv1YWL zW=%ug_&chF11gDv3D6B)Tz5g54H0mDHNjuKZ+)CKFk4Z|$RD zfRuKLW`1B>B?*RUfVd0+u8h3r-{@fZ{k)c!93t1b0+Q9vOaRnEn1*IL>5Z4E4dZ!7 ztp4GP-^1d>8~LMeb}bW!(aAnB1tM_*la=Xx)q(I0Y@__Zd$!KYb8T2VBRw%e$iSdZ zkwdMwd}eV9q*;YvrBFTv1>1+}{H!JK2M*C|TNe$ZSA>UHKk);wz$(F$rXVc|sI^lD zV^?_J!3cLM;GJuBMbftbaRUs$;F}HDEDtIeHQ)^EJJ1F9FKJTGH<(Jj`phE6OuvE) zqK^K`;3S{Y#1M@8yRQwH`?kHMq4tHX#rJ>5lY3DM#o@or4&^_xtBC(|JpGTfrbGkA z2Tu+AyT^pHannww!4^!$5?@5v`LYy~T`qs7SYt$JgrY(w%C+IWA;ZkwEF)u5sDvOK zGk;G>Mh&elvXDcV69J_h02l&O;!{$({fng9Rlc3ID#tmB^FIG^w{HLUpF+iB`|
NnX)EH+Nua)3Y(c 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7fc84becca..e7646dead0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421c..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f13..7101f8e467 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From da3eb99acb82c07a333e06039a0e9b30fc5c9eda Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 22 Mar 2024 20:09:05 +0100 Subject: [PATCH 120/129] [Oztechan/CCC#3249] Remove compose compiler from TOML (#3250) --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b1a59b7ab..015591e566 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,7 +71,6 @@ android-androidDesugaring = { module = "com.android.tools:desugar_jdk_libs", ver android-androidMaterial = { module = "com.google.android.material:material", version.ref = "androidMaterial" } android-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } android-constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" } -android-composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" } android-composeToolingPreview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } android-composeTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } android-composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } From 6755d6996ba402687eaeafb96c626623e91fe0d8 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Fri, 22 Mar 2024 20:33:54 +0100 Subject: [PATCH 121/129] [Oztechan/CCC#3251] Prevent multiple Api call when base change on click and add test cases (#3252) --- .../viewmodel/calculator/CalculatorViewModel.kt | 11 +++++++---- .../viewmodel/calculator/CalculatorViewModelTest.kt | 12 ++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 064d94ae06..758c08bac8 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -34,6 +34,7 @@ import com.oztechan.ccc.client.viewmodel.calculator.util.getConversionStringFrom import com.oztechan.ccc.common.core.model.Conversion import com.oztechan.ccc.common.core.model.Currency import com.oztechan.ccc.common.datasource.conversion.ConversionDataSource +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -73,10 +74,8 @@ class CalculatorViewModel( state.map { it.input }.distinctUntilChanged(), ) { activeCurrencies, base, input -> Logger.d { - """ - CalculatorViewModel combined values: - base: $base, input: $input, activeCurrencies: ${activeCurrencies.joinToString(",") { it.code }} - """.trimIndent() + "CalculatorViewModel combined: " + + "base: $base, input: $input, activeCurrencies: ${activeCurrencies.joinToString(",") { it.code }}" } _state.update { copy(currencyList = activeCurrencies) } @@ -232,6 +231,10 @@ class CalculatorViewModel( _state.update { copy(input = newInput) } + + @Suppress("MagicNumber") + delay(100) + calculationStorage.setBase(currency.code) } diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index fca71f857a..2c15b82690 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -34,6 +34,7 @@ import io.mockative.every import io.mockative.mock import io.mockative.verify import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onSubscription @@ -368,32 +369,42 @@ internal class CalculatorViewModelTest { var currency = currency1 viewModel.state.onSubscription { viewModel.event.onItemClick(currency1) + delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency1.code, it.base) assertEquals(currency1.rate, it.input) } + coVerify { calculationStorage.setBase(currency1.code) } + .wasInvoked() + // when last digit is . it should be removed currency = currency.copy(rate = "123.") viewModel.state.onSubscription { viewModel.event.onItemClick(currency) + delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency.code, it.base) assertEquals("123", it.input) } + coVerify { calculationStorage.setBase(currency.code) } + .wasInvoked() currency = currency.copy(rate = "123 456.78") viewModel.state.onSubscription { viewModel.event.onItemClick(currency) + delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency.code, it.base) assertEquals("123456.78", it.input) } + coVerify { calculationStorage.setBase(currency.code) } + .wasInvoked() } @Test @@ -436,6 +447,7 @@ internal class CalculatorViewModelTest { val output = "5" viewModel.effect.onSubscription { viewModel.event.onKeyPress(output) + delay(100) viewModel.event.onOutputLongClick() }.firstOrNull().let { assertEquals(CalculatorEffect.CopyToClipboard(output), it) From 92ac1b356357980b8d49b5634eddf8f9fc20800f Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 23 Mar 2024 11:12:38 +0100 Subject: [PATCH 122/129] [Oztechan/CCC#3253] Clean up Swift code (#3254) --- ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift | 2 +- ios/CCC/UI/Watchers/WatcherItem.swift | 3 --- ios/CCC/UI/Watchers/WatchersView.swift | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift index 053ef0258a..209512be66 100644 --- a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift +++ b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift @@ -40,7 +40,7 @@ struct SelectCurrencyRootView: View { private func onEffect(effect: SelectCurrencyEffect) { logger.i(message: { "SelectCurrencyRootView onEffect \(effect.description)" }) switch effect { - case let currencyChangeEffect as SelectCurrencyEffect.DismissDialog: + case is SelectCurrencyEffect.DismissDialog: isBarShown = false case is SelectCurrencyEffect.OpenCurrencies: navigationStack.push(CurrenciesRootView()) diff --git a/ios/CCC/UI/Watchers/WatcherItem.swift b/ios/CCC/UI/Watchers/WatcherItem.swift index 22324b5fcb..d938bc3f7f 100644 --- a/ios/CCC/UI/Watchers/WatcherItem.swift +++ b/ios/CCC/UI/Watchers/WatcherItem.swift @@ -16,9 +16,6 @@ struct WatcherItem: View { @State private var relationSelection = 0 @State private var amount = "" - @Binding var isBaseBarShown: Bool - @Binding var isTargetBarShown: Bool - let watcher: Provider.Watcher let event: WatchersEvent diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index 61bde8024e..4fb2b0420d 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -33,8 +33,6 @@ struct WatchersView: View { Form { List(state.watcherList, id: \.id) { watcher in WatcherItem( - isBaseBarShown: $baseBarInfo.isShown, - isTargetBarShown: $targetBarInfo.isShown, watcher: watcher, event: event ) From a57dbb5b30304d24c5b689f32d73d8496f729128 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sat, 23 Mar 2024 12:42:49 +0100 Subject: [PATCH 123/129] [Oztechan/CCC#3255] Add compose compiler version check into account by its artifact URL (#3256) --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 015591e566..8c8a2a62ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,6 +71,7 @@ android-androidDesugaring = { module = "com.android.tools:desugar_jdk_libs", ver android-androidMaterial = { module = "com.google.android.material:material", version.ref = "androidMaterial" } android-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } android-constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" } +android-composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" } # not used dependency, workaround for receiving kotlin compose compiler updates by renovate: https://github.com/renovatebot/renovate/issues/18354 android-composeToolingPreview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } android-composeTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } android-composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } From 5b7e5edaa6352c22d794eef2db390814803e35a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Mar 2024 18:53:02 +0100 Subject: [PATCH 124/129] [Oztechan/Global#12] Update submodule/logmob digest to ab67f77 (#3257) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/logmob | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodule/logmob b/submodule/logmob index 4fff698afa..ab67f77eb6 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 4fff698afa2e0c401e5c7f373aa9b5c273184eca +Subproject commit ab67f77eb6cddeac0f04ccc898dd307410c18a42 From 2386841f42f1ee16905f7d0b3b789aaf5cbeab93 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 00:03:25 +0100 Subject: [PATCH 125/129] [Oztechan/Global#12] Update Git Submodules (#3258) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index 13fe6ad1ab..21b089edbd 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit 13fe6ad1abb6ba0731392ceda64c8c59dfa902b4 +Subproject commit 21b089edbd0e4fa959808b6250c4e44284345d55 diff --git a/submodule/logmob b/submodule/logmob index ab67f77eb6..57130206a9 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit ab67f77eb6cddeac0f04ccc898dd307410c18a42 +Subproject commit 57130206a9383ae55b468d8e5025a4685cced625 diff --git a/submodule/parsermob b/submodule/parsermob index 3041ab8c6c..e91ce3fa68 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 3041ab8c6ce2191fc798581b9fbe0115b224c8cc +Subproject commit e91ce3fa68bb8e9dcd1ab72f30c6275fa1e366a3 diff --git a/submodule/scopemob b/submodule/scopemob index f51d488ded..652feed7c1 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit f51d488ded59bd2f3f7935b870fd2bfb6b308af1 +Subproject commit 652feed7c17df7cb5226903cc6628948f7f82acc From 81adf43f9c3aad377f6f8e3af0c6a5d7dcbffc23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:45:47 +0100 Subject: [PATCH 126/129] [Oztechan/Global#12] Update kotlin (#3221) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c8a2a62ed..bd0546fd61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -kotlin = "1.9.22" -ksp = "1.9.22-1.0.18" +kotlin = "1.9.23" +ksp = "1.9.23-1.0.19" detekt = "1.23.5" androidGradlePlugin = "8.2.2" -composeCompiler = "1.5.10" +composeCompiler = "1.5.11" compose = "1.6.3" glance = "1.0.0" material3 = "1.2.1" From 8b47958f9a1cebff9891f0f3901799111ac6dda4 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 24 Mar 2024 22:08:44 +0100 Subject: [PATCH 127/129] [Oztechan/CCC#3260] Use IO dispatcher for iOS and remove platform specific dispatchers (#3261) --- .../di/CommonCoreInfrastructureModule.kt | 16 +++++++++++++++- .../di/CommonCoreInfrastructureModule.ios.kt | 18 ------------------ .../di/CommonCoreInfrastructureModule.jvm.kt | 18 ------------------ 3 files changed, 15 insertions(+), 37 deletions(-) delete mode 100644 common/core/infrastructure/src/iosMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.ios.kt delete mode 100644 common/core/infrastructure/src/jvmMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.jvm.kt diff --git a/common/core/infrastructure/src/commonMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.kt b/common/core/infrastructure/src/commonMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.kt index a57a1e2154..10991b1694 100644 --- a/common/core/infrastructure/src/commonMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.kt +++ b/common/core/infrastructure/src/commonMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.kt @@ -1,6 +1,13 @@ package com.oztechan.ccc.common.core.infrastructure.di +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.IO import org.koin.core.module.Module +import org.koin.core.qualifier.named +import org.koin.dsl.module // names const val DISPATCHER_IO = "DISPATCHER_IO" @@ -8,4 +15,11 @@ const val DISPATCHER_MAIN = "DISPATCHER_MAIN" const val DISPATCHER_UNCONFINED = "DISPATCHER_UNCONFINED" const val DISPATCHER_DEFAULT = "DISPATCHER_DEFAULT" -expect val commonCoreInfrastructureModule: Module +@Suppress("OPT_IN_USAGE") +val commonCoreInfrastructureModule: Module = module { + single { GlobalScope } + single(named(DISPATCHER_MAIN)) { Dispatchers.Main } + single(named(DISPATCHER_IO)) { Dispatchers.IO } + single(named(DISPATCHER_UNCONFINED)) { Dispatchers.Unconfined } + single(named(DISPATCHER_DEFAULT)) { Dispatchers.Default } +} diff --git a/common/core/infrastructure/src/iosMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.ios.kt b/common/core/infrastructure/src/iosMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.ios.kt deleted file mode 100644 index 82b3100f68..0000000000 --- a/common/core/infrastructure/src/iosMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.ios.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.oztechan.ccc.common.core.infrastructure.di - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import org.koin.core.module.Module -import org.koin.core.qualifier.named -import org.koin.dsl.module - -actual val commonCoreInfrastructureModule: Module = module { - @Suppress("OPT_IN_USAGE") - single { GlobalScope } - single(named(DISPATCHER_MAIN)) { Dispatchers.Main } - single(named(DISPATCHER_IO)) { Dispatchers.Default } - single(named(DISPATCHER_UNCONFINED)) { Dispatchers.Unconfined } - single(named(DISPATCHER_DEFAULT)) { Dispatchers.Default } -} diff --git a/common/core/infrastructure/src/jvmMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.jvm.kt b/common/core/infrastructure/src/jvmMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.jvm.kt deleted file mode 100644 index 1ad96d0416..0000000000 --- a/common/core/infrastructure/src/jvmMain/kotlin/com/oztechan/ccc/common/core/infrastructure/di/CommonCoreInfrastructureModule.jvm.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.oztechan.ccc.common.core.infrastructure.di - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import org.koin.core.module.Module -import org.koin.core.qualifier.named -import org.koin.dsl.module - -@Suppress("InjectDispatcher", "OPT_IN_USAGE") -actual val commonCoreInfrastructureModule: Module = module { - single { GlobalScope } - single(named(DISPATCHER_MAIN)) { Dispatchers.Main } - single(named(DISPATCHER_IO)) { Dispatchers.IO } - single(named(DISPATCHER_UNCONFINED)) { Dispatchers.Unconfined } - single(named(DISPATCHER_DEFAULT)) { Dispatchers.Default } -} From e7872066a31d049e582aa414ad37487ad2222177 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 22:09:45 +0100 Subject: [PATCH 128/129] [Oztechan/Global#12] Update Git Submodules (#3262) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Mustafa Ozhan --- submodule/basemob | 2 +- submodule/logmob | 2 +- submodule/parsermob | 2 +- submodule/scopemob | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodule/basemob b/submodule/basemob index 21b089edbd..e77c1d34ea 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit 21b089edbd0e4fa959808b6250c4e44284345d55 +Subproject commit e77c1d34eaa7c65e6a02ad7f08eba95c134f5a37 diff --git a/submodule/logmob b/submodule/logmob index 57130206a9..90766ea849 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 57130206a9383ae55b468d8e5025a4685cced625 +Subproject commit 90766ea8492fdc6841ec6175ea77092744c807c4 diff --git a/submodule/parsermob b/submodule/parsermob index e91ce3fa68..e11efbe33c 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit e91ce3fa68bb8e9dcd1ab72f30c6275fa1e366a3 +Subproject commit e11efbe33c28857fcb6e682af8624431fb544a96 diff --git a/submodule/scopemob b/submodule/scopemob index 652feed7c1..11c327d51c 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit 652feed7c17df7cb5226903cc6628948f7f82acc +Subproject commit 11c327d51c2e300605a92c5d3521e7afbc703eaa From 694dcac46be416e51aefed36fbdfdc3758b3db70 Mon Sep 17 00:00:00 2001 From: Mustafa Ozhan Date: Sun, 24 Mar 2024 22:49:48 +0100 Subject: [PATCH 129/129] [Oztechan/CCC#2854] Remove experimental multiplatform settings (#3259) * Revert "[Oztechan/CCC#3253] Clean up Swift code (#3254)" This reverts commit 92ac1b356357980b8d49b5634eddf8f9fc20800f. * Revert "[Oztechan/CCC#3251] Prevent multiple Api call when base change on click and add test cases (#3252)" This reverts commit 6755d6996ba402687eaeafb96c626623e91fe0d8. * Revert "[Oztechan/CCC#3191] Add Flow Property for lastInput (#3192)" This reverts commit 375da22506004af9a1337254eef32a7c936cd8eb. * Revert "[Oztechan/CCC#3189] Move to new getBaseFlow logic (#3190)" This reverts commit 4e4efc96 * Revert "[Oztechan/CCC#3185] Remove navigation result logics (#3186)" This reverts commit ba2563542195f3cfe3216bba44e61623ae03cfcd. * Revert "[Oztechan/CCC#3183] Remove onBaseChange event (#3184)" This reverts commit 917be17cd2882fbec0370799814a25b3341a19cc. * Revert "[Oztechan/CCC#3180] Remove ChangeBase Effect (#3181)" This reverts commit 6fab011d587a844df04ad8957b266a6de092af5a. * Revert "[Oztechan/CCC#3178] Replace CurrencyChange effect with DismissDialog (#3179)" This reverts commit fc2f955348c0cfcc67c5a00d7a7b4c7b0108b5a7. * Revert "[Oztechan/CCC#3176] Add Flow Property for currentBase (#3177)" This reverts commit dd2bdc55988367fe38959cbb9daf64d609bd56db. * Revert "[Oztechan/CCC#3144] Remove Persistence and use DI dispatchers in Coroutine based Persistences (#3145)" This reverts commit c59e1a5c575377141ed7b27ba461d76ea7578a49. * Revert "[Oztechan/CCC#3141] Move sessionCount to Suspend (#3142)" This reverts commit e2eaa85b581c08bb8cb6756800ca8a791ae1a59e. * Revert "[Oztechan/CCC#3138] Move premiumEndDate to Suspend (#3140)" This reverts commit bd0f4b9c0c0cdc55b351050212d4927d585256c0. * Revert "[Oztechan/CCC#2942] Move firstRun to Suspend (#3137)" This reverts commit 2ff3edf759c6e9c4c2053cd25192cf753dcb1c73. * Revert "[Oztechan/CCC#3128] Move appTheme to Suspend (#3135)" This reverts commit 00c10be7379d3af24f7592f8bccbad4c40c2fd2e. * Revert "[Oztechan/CCC#3122] Move currentBase to Suspend (#3127)" This reverts commit de4de42f * Revert "[Oztechan/CCC#3123] Move precision to Suspend (#3124)" This reverts commit 26af2a18 * Revert "[Oztechan/CCC#3120] Move lastInput to Suspend (#3121)" This reverts commit 9c53434f533db4a904f1e20d5a749101963b81b0. * Revert "[Oztechan/CCC#3038] Add missing Settings Koin definition for native target (#3039)" This reverts commit 98870c38a3cf204bce2cadc40b8def491ba9d148. * Revert "[Oztechan/CCC#2871] Add error test case for FlowPersistence (#2872)" This reverts commit c1f8711b3f2bd36edb6574aebf8c315a63c6b4fc. * Revert "[Oztechan/CCC#2867] Create test for FlowPersistence (#2868)" This reverts commit 47ea4f95 * Revert "[Oztechan/CCC#2865] Create test for SuspendPersistence (#2866)" This reverts commit fdc952c58894b84d3b32e3bd50bb29e2fede40f0. * Revert "[Oztechan/CCC#2869] Opt-in for ExperimentalSettingsApi (#2870)" This reverts commit ef9f1877746c949f279fce027d48ab657136d728. * Revert "[Oztechan/CCC#2863] Use relevant settings for persistencs (#2864)" This reverts commit 00696db7 * Revert "[Oztechan/CCC#2857] Add Suspension support for multiplatform settings (#2859)" This reverts commit 5eaa8b15 * Revert "[Oztechan/CCC#2855] Add Flow support for multiplatform settings (#2856)" This reverts commit 9b529096 * [Oztechan/CCC#2854] Remove experimental multiplatform settings * [Oztechan/CCC#2854] Remove experimental multiplatform settings * [Oztechan/CCC#2854] Remove experimental multiplatform settings --- .../content/calculator/CalculatorFragment.kt | 14 + .../content/currencies/CurrenciesFragment.kt | 8 + .../ui/mobile/content/main/MainActivity.kt | 13 +- .../SelectCurrencyBottomSheet.kt | 11 +- .../android/ui/mobile/util/ViewExtensions.kt | 21 + .../android/viewmodel/widget/WidgetSEED.kt | 4 +- .../viewmodel/widget/WidgetViewModel.kt | 38 +- .../viewmodel/widget/WidgetViewModelTest.kt | 59 ++- .../client-core-persistence.gradle.kts | 12 - .../di/ClientCorePersistenceModule.android.kt | 24 +- .../core/persistence/FlowPersistence.kt | 7 - .../core/persistence/FlowPersistenceImpl.kt | 19 - .../client/core/persistence/Persistence.kt | 6 + .../core/persistence/PersistenceImpl.kt | 25 ++ .../core/persistence/SuspendPersistence.kt | 6 - .../persistence/SuspendPersistenceImpl.kt | 29 -- .../core/persistence/FlowPersistenceTest.kt | 75 ---- .../core/persistence/PersistenceTest.kt | 91 ++++ .../persistence/SuspendPersistenceTest.kt | 96 ----- .../di/ClientCorePersistenceModule.ios.kt | 26 +- .../client-repository-adcontrol.gradle.kts | 1 - .../adcontrol/AdControlRepository.kt | 4 +- .../adcontrol/AdControlRepositoryImpl.kt | 10 +- .../adcontrol/AdControlRepositoryTest.kt | 391 +++++++++--------- .../client-repository-appconfig.gradle.kts | 1 - .../appconfig/AppConfigRepository.kt | 2 +- .../appconfig/AppConfigRepositoryImpl.kt | 4 +- .../appconfig/AppConfigRepositoryTest.kt | 78 ++-- .../storage/app/client-storage-app.gradle.kts | 1 - .../ccc/client/storage/app/AppStorage.kt | 12 +- .../ccc/client/storage/app/AppStorageImpl.kt | 35 +- .../ccc/client/storage/app/AppStorageTest.kt | 68 +-- .../client-storage-calculation.gradle.kts | 7 +- .../storage/calculation/CalculationStorage.kt | 13 +- .../calculation/CalculationStorageImpl.kt | 40 +- .../calculation/CalculationStorageTest.kt | 89 ++-- .../viewmodel/calculator/CalculatorSEED.kt | 3 +- .../calculator/CalculatorViewModel.kt | 161 ++++---- .../calculator/CalculatorViewModelTest.kt | 139 +++---- .../viewmodel/currencies/CurrenciesSEED.kt | 5 +- .../currencies/CurrenciesViewModel.kt | 26 +- .../currencies/CurrenciesViewModelTest.kt | 87 ++-- .../ccc/client/viewmodel/main/MainSEED.kt | 4 +- .../client/viewmodel/main/MainViewModel.kt | 54 ++- .../viewmodel/main/MainViewModelTest.kt | 150 ++++--- .../viewmodel/premium/PremiumViewModel.kt | 40 +- .../viewmodel/premium/PremiumViewModelTest.kt | 24 +- ...client-viewmodel-selectcurrency.gradle.kts | 4 - .../selectcurrency/SelectCurrencySEED.kt | 2 +- .../selectcurrency/SelectCurrencyViewModel.kt | 7 +- .../di/ClientViewModelSelectCurrencyModule.kt | 2 +- .../SelectCurrencyViewModelTest.kt | 13 +- .../client/viewmodel/settings/SettingsSEED.kt | 2 +- .../viewmodel/settings/SettingsViewModel.kt | 33 +- .../settings/SettingsViewModelTest.kt | 44 +- .../client/viewmodel/watchers/WatchersSEED.kt | 2 +- .../viewmodel/watchers/WatchersViewModel.kt | 28 +- .../watchers/WatchersViewModelTest.kt | 8 +- gradle/libs.versions.toml | 1 - .../UI/Calculator/CalculatorRootView.swift | 9 +- .../UI/Currencies/CurrenciesRootView.swift | 4 + ios/CCC/UI/Main/MainView.swift | 7 +- .../SelectCurrencyRootView.swift | 7 +- ios/CCC/UI/Settings/SettingsRootView.swift | 4 +- .../UI/Slides/BugReportSlideRootView.swift | 2 +- ios/CCC/UI/Watchers/WatcherItem.swift | 3 + ios/CCC/UI/Watchers/WatchersRootView.swift | 20 +- ios/CCC/UI/Watchers/WatchersView.swift | 2 + 68 files changed, 1017 insertions(+), 1220 deletions(-) delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt create mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt create mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt delete mode 100644 client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt delete mode 100644 client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt create mode 100644 client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt delete mode 100644 client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt index d9fe1d548e..e1ed3b5424 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/calculator/CalculatorFragment.kt @@ -19,6 +19,7 @@ import com.oztechan.ccc.android.ui.mobile.util.copyToClipBoard import com.oztechan.ccc.android.ui.mobile.util.dataState import com.oztechan.ccc.android.ui.mobile.util.destroyBanner import com.oztechan.ccc.android.ui.mobile.util.getFromClipBoard +import com.oztechan.ccc.android.ui.mobile.util.getNavigationResult import com.oztechan.ccc.android.ui.mobile.util.setBackgroundByName import com.oztechan.ccc.android.ui.mobile.util.setBannerAd import com.oztechan.ccc.android.ui.mobile.util.showSnack @@ -56,6 +57,7 @@ class CalculatorFragment : BaseVBFragment() { binding.observeStates() binding.setListeners() observeEffects() + observeNavigationResults() } override fun onResume() { @@ -75,6 +77,14 @@ class CalculatorFragment : BaseVBFragment() { super.onDestroyView() } + private fun observeNavigationResults() = getNavigationResult( + CHANGE_BASE_EVENT, + R.id.calculatorFragment + )?.observe(viewLifecycleOwner) { + Logger.i { "CalculatorFragment observeNavigationResults $it" } + calculatorViewModel.event.onBaseChange(it) + } + private fun FragmentCalculatorBinding.initViews() = viewLifecycleOwner.lifecycleScope.launch { recyclerViewMain.adapter = calculatorAdapter } @@ -198,4 +208,8 @@ class CalculatorFragment : BaseVBFragment() { private fun Button.setKeyboardListener() = setOnClickListener { calculatorViewModel.event.onKeyPress(text.toString()) } + + companion object { + const val CHANGE_BASE_EVENT = "change_base" + } } diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt index 2d608ea0fa..040d7c868b 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/currencies/CurrenciesFragment.kt @@ -18,10 +18,12 @@ import com.github.submob.basemob.fragment.BaseVBFragment import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.ui.mobile.BuildConfig import com.oztechan.ccc.android.ui.mobile.R +import com.oztechan.ccc.android.ui.mobile.content.calculator.CalculatorFragment.Companion.CHANGE_BASE_EVENT import com.oztechan.ccc.android.ui.mobile.databinding.FragmentCurrenciesBinding import com.oztechan.ccc.android.ui.mobile.util.destroyBanner import com.oztechan.ccc.android.ui.mobile.util.hideKeyboard import com.oztechan.ccc.android.ui.mobile.util.setBannerAd +import com.oztechan.ccc.android.ui.mobile.util.setNavigationResult import com.oztechan.ccc.android.ui.mobile.util.showSnack import com.oztechan.ccc.android.ui.mobile.util.visibleIf import com.oztechan.ccc.client.core.analytics.AnalyticsManager @@ -131,6 +133,12 @@ class CurrenciesFragment : BaseVBFragment() { findNavController().popBackStack() view?.hideKeyboard() } + + is CurrenciesEffect.ChangeBase -> setNavigationResult( + R.id.calculatorFragment, + viewEffect.newBase, + CHANGE_BASE_EVENT + ) } }.launchIn(viewLifecycleOwner.lifecycleScope) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt index 48726f8ccb..37acae6c32 100755 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/main/MainActivity.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger import com.github.submob.basemob.activity.BaseActivity -import com.github.submob.scopemob.whether import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.ui.mobile.BuildConfig import com.oztechan.ccc.android.ui.mobile.R @@ -55,16 +54,12 @@ class MainActivity : BaseActivity() { .flowWithLifecycle(lifecycle) .onEach { with(it) { - shouldOnboardUser?.let { shouldOnboardUser -> - setDestination(if (shouldOnboardUser) R.id.sliderFragment else R.id.calculatorFragment) - } + setDestination(if (shouldOnboardUser) R.id.sliderFragment else R.id.calculatorFragment) // if dark mode is supported use theming according to user preference - it.appTheme - ?.whether { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q } - ?.let { appTheme -> - AppCompatDelegate.setDefaultNightMode(getThemeMode(appTheme)) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AppCompatDelegate.setDefaultNightMode(getThemeMode(it.appTheme)) + } } }.launchIn(lifecycleScope) diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt index 9adbe26e06..39876fc548 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/selectcurrency/SelectCurrencyBottomSheet.kt @@ -10,7 +10,9 @@ import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger import com.github.submob.basemob.bottomsheet.BaseVBBottomSheetDialogFragment import com.oztechan.ccc.android.ui.mobile.R +import com.oztechan.ccc.android.ui.mobile.content.calculator.CalculatorFragment.Companion.CHANGE_BASE_EVENT import com.oztechan.ccc.android.ui.mobile.databinding.BottomSheetSelectCurrencyBinding +import com.oztechan.ccc.android.ui.mobile.util.setNavigationResult import com.oztechan.ccc.android.ui.mobile.util.visibleIf import com.oztechan.ccc.client.core.analytics.AnalyticsManager import com.oztechan.ccc.client.core.analytics.model.ScreenName @@ -83,7 +85,14 @@ class SelectCurrencyBottomSheet : .onEach { viewEffect -> Logger.i { "SelectCurrencyBottomSheet observeEffects ${viewEffect::class.simpleName}" } when (viewEffect) { - is SelectCurrencyEffect.DismissDialog -> dismissDialog() + is SelectCurrencyEffect.CurrencyChange -> { + setNavigationResult( + R.id.calculatorFragment, + viewEffect.newBase, + CHANGE_BASE_EVENT + ) + dismissDialog() + } SelectCurrencyEffect.OpenCurrencies -> navigate( R.id.selectCurrencyBottomSheet, diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt index 077759904a..f355573f7a 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/ViewExtensions.kt @@ -19,7 +19,10 @@ import androidx.core.content.ContextCompat import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import com.github.submob.scopemob.castTo +import com.github.submob.scopemob.whether import com.oztechan.ccc.android.core.ad.AdManager import com.oztechan.ccc.android.core.ad.BannerAdView import com.oztechan.ccc.android.ui.mobile.R @@ -70,6 +73,24 @@ fun View.animateHeight(startHeight: Int, endHeight: Int) { startAnimation(animation) } +fun Fragment.getNavigationResult( + key: String, + destinationId: Int +) = findNavController() + .currentBackStackEntry + ?.whether { it.destination.id == destinationId } + ?.savedStateHandle + ?.getLiveData(key) + +fun Fragment.setNavigationResult( + destinationId: Int, + result: T, + key: String +) = findNavController() + .previousBackStackEntry + ?.whether { it.destination.id == destinationId } + ?.savedStateHandle?.set(key, result) + fun View?.visibleIf(visible: Boolean, bringFront: Boolean = false) = this?.apply { if (visible) { isVisible = true diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt index 25561a7d14..37c6517ce9 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetSEED.kt @@ -10,8 +10,8 @@ import com.oztechan.ccc.common.core.model.Currency data class WidgetState( var currencyList: List = listOf(), var lastUpdate: String = "", - var currentBase: String = "", - var isPremium: Boolean = true + var currentBase: String, + var isPremium: Boolean ) : BaseState // Event diff --git a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt index f51ef727ea..b655b763ca 100644 --- a/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt +++ b/android/viewmodel/widget/src/main/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModel.kt @@ -28,7 +28,12 @@ class WidgetViewModel( ) : BaseSEEDViewModel(), WidgetEvent { // region SEED - private val _state = MutableStateFlow(WidgetState()) + private val _state = MutableStateFlow( + WidgetState( + currentBase = calculationStorage.currentBase, + isPremium = appStorage.premiumEndDate.isNotPassed() + ) + ) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -39,24 +44,13 @@ class WidgetViewModel( override val data = WidgetData() // endregion - init { - viewModelScope.launchIgnored { - _state.update { - it.copy( - currentBase = calculationStorage.getBase(), - isPremium = appStorage.getPremiumEndDate().isNotPassed() - ) - } - } - } - - private suspend fun refreshWidgetData() { + private fun refreshWidgetData() { _state.update { it.copy( currencyList = listOf(), lastUpdate = "", - currentBase = calculationStorage.getBase(), - isPremium = appStorage.getPremiumEndDate().isNotPassed() + currentBase = calculationStorage.currentBase, + isPremium = appStorage.premiumEndDate.isNotPassed() ) } @@ -67,14 +61,14 @@ class WidgetViewModel( private fun getFreshWidgetData() = viewModelScope.launch { val conversion = backendApiService - .getConversion(calculationStorage.getBase()) + .getConversion(calculationStorage.currentBase) currencyDataSource.getActiveCurrencies() - .filterNot { it.code == calculationStorage.getBase() } + .filterNot { it.code == calculationStorage.currentBase } .onEach { - it.rate = conversion.getRateFromCode(it.code) - ?.getFormatted(calculationStorage.getPrecision()) - .orEmpty() + it.rate = + conversion.getRateFromCode(it.code)?.getFormatted(calculationStorage.precision) + .orEmpty() } .take(MAXIMUM_NUMBER_OF_CURRENCY) .let { currencyList -> @@ -92,7 +86,7 @@ class WidgetViewModel( val newBaseIndex = activeCurrencies .map { it.code } - .indexOf(calculationStorage.getBase()) + .indexOf(calculationStorage.currentBase) .let { if (isToNext) { it + 1 @@ -103,7 +97,7 @@ class WidgetViewModel( (it + activeCurrencies.size) % activeCurrencies.size // it handles index -1 and index size +1 } - calculationStorage.setBase(activeCurrencies[newBaseIndex].code) + calculationStorage.currentBase = activeCurrencies[newBaseIndex].code } // region Event diff --git a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt index a4034663fb..4d47accedd 100644 --- a/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt +++ b/android/viewmodel/widget/src/test/kotlin/com/oztechan/ccc/android/viewmodel/widget/WidgetViewModelTest.kt @@ -18,7 +18,9 @@ import io.mockative.classOf import io.mockative.coEvery import io.mockative.coVerify import io.mockative.configure +import io.mockative.every import io.mockative.mock +import io.mockative.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onSubscription @@ -80,16 +82,16 @@ internal class WidgetViewModelTest { val mockEndDate = Random.nextLong() - runTest { - coEvery { appStorage.getPremiumEndDate() } - .returns(mockEndDate) + every { appStorage.premiumEndDate } + .returns(mockEndDate) - coEvery { calculationStorage.getPrecision() } - .returns(3) + every { calculationStorage.currentBase } + .returns(base) - coEvery { calculationStorage.getBase() } - .returns(base) + every { calculationStorage.precision } + .returns(3) + runTest { coEvery { backendApiService.getConversion(base) } .returns(conversion) @@ -101,7 +103,7 @@ internal class WidgetViewModelTest { @Test fun `ArrayIndexOutOfBoundsException never thrown`() = runTest { // first currency - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(firstBase) coEvery { backendApiService.getConversion(firstBase) } @@ -118,7 +120,7 @@ internal class WidgetViewModelTest { } // middle currency - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(base) coEvery { backendApiService.getConversion(base) } @@ -135,7 +137,7 @@ internal class WidgetViewModelTest { } // last currency - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(lastBase) coEvery { backendApiService.getConversion(lastBase) } @@ -159,7 +161,7 @@ internal class WidgetViewModelTest { assertTrue { it.currencyList.isEmpty() } assertEquals("", it.lastUpdate) assertEquals(base, it.currentBase) - assertEquals(appStorage.getPremiumEndDate().isNotPassed(), it.isPremium) + assertEquals(appStorage.premiumEndDate.isNotPassed(), it.isPremium) } } @@ -170,7 +172,7 @@ internal class WidgetViewModelTest { @Test fun `if user is premium api call and db query are invoked`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -184,7 +186,7 @@ internal class WidgetViewModelTest { @Test fun `if user is not premium no api call and db query are not invoked`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -199,7 +201,7 @@ internal class WidgetViewModelTest { @Test fun `when onRefreshClick called all the conversion rates for currentBase is calculated`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.state.onSubscription { @@ -209,10 +211,7 @@ internal class WidgetViewModelTest { it.currencyList.forEach { currency -> conversion.getRateFromCode(currency.code).let { rate -> assertNotNull(rate) - assertEquals( - rate.getFormatted(calculationStorage.getPrecision()), - currency.rate - ) + assertEquals(rate.getFormatted(calculationStorage.precision), currency.rate) } } } @@ -221,7 +220,7 @@ internal class WidgetViewModelTest { @Test fun `when onRefreshClick called with null, base is not updated`() = runTest { // to not invoke getFreshWidgetData - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -229,7 +228,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasNotInvoked() - coVerify { calculationStorage.setBase(any()) } + verify { calculationStorage.currentBase = any() } .wasNotInvoked() } @@ -237,7 +236,7 @@ internal class WidgetViewModelTest { @Test fun onNextClick() = runTest { // when onNextClick, base is updated next or the first active currency - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onNextClick() @@ -245,10 +244,10 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(lastBase) } + verify { calculationStorage.currentBase = lastBase } .wasInvoked() - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(lastBase) viewModel.event.onNextClick() @@ -256,14 +255,14 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(firstBase) } + verify { calculationStorage.currentBase = firstBase } .wasInvoked() } @Test fun onPreviousClick() = runTest { // when onRefreshClick, base is updated previous or the last active currency - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.event.onPreviousClick() @@ -271,10 +270,10 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(firstBase) } + verify { calculationStorage.currentBase = firstBase } .wasInvoked() - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(firstBase) viewModel.event.onPreviousClick() @@ -282,13 +281,13 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.setBase(lastBase) } + verify { calculationStorage.currentBase = lastBase } .wasInvoked() } @Test fun onRefreshClick() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.event.onRefreshClick() @@ -299,7 +298,7 @@ internal class WidgetViewModelTest { coVerify { currencyDataSource.getActiveCurrencies() } .wasInvoked() - coVerify { calculationStorage.getBase() } + verify { calculationStorage.currentBase } .wasInvoked() } diff --git a/client/core/persistence/client-core-persistence.gradle.kts b/client/core/persistence/client-core-persistence.gradle.kts index 2aaadca6ba..ada4a720d1 100644 --- a/client/core/persistence/client-core-persistence.gradle.kts +++ b/client/core/persistence/client-core-persistence.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { libs.plugins.apply { alias(kotlinMultiplatform) @@ -16,29 +14,19 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(project(Modules.Common.Core.infrastructure)) libs.common.apply { implementation(koinCore) - implementation(coroutines) implementation(multiplatformSettings) - implementation(multiplatformSettingsCoroutines) } } commonTest.dependencies { libs.common.apply { implementation(test) implementation(mockative) - implementation(coroutinesTest) } } } } -// todo remove after https://github.com/russhwolf/multiplatform-settings/issues/119 -tasks.withType { - kotlinOptions { - freeCompilerArgs += "-opt-in=com.russhwolf.settings.ExperimentalSettingsApi" - } -} dependencies { configurations diff --git a/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt b/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt index 2ef23b2967..784241f725 100644 --- a/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt +++ b/client/core/persistence/src/androidMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.android.kt @@ -2,32 +2,20 @@ package com.oztechan.ccc.client.core.persistence.di import android.content.Context import android.content.SharedPreferences -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.FlowPersistenceImpl -import com.oztechan.ccc.client.core.persistence.SuspendPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistenceImpl -import com.oztechan.ccc.common.core.infrastructure.di.DISPATCHER_IO -import com.russhwolf.settings.ObservableSettings +import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.PersistenceImpl +import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings -import com.russhwolf.settings.coroutines.toFlowSettings -import com.russhwolf.settings.coroutines.toSuspendSettings +import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf -import org.koin.core.qualifier.named import org.koin.dsl.module private const val KEY_APPLICATION_PREFERENCES = "application_preferences" actual val clientCorePersistenceModule = module { singleOf(::provideSharedPreferences) - - single { SharedPreferencesSettings(get()) } - - single { - FlowPersistenceImpl(get().toFlowSettings(get(named(DISPATCHER_IO)))) - } - single { - SuspendPersistenceImpl(get().toSuspendSettings(get(named(DISPATCHER_IO)))) - } + single { SharedPreferencesSettings(get()) } + singleOf(::PersistenceImpl) { bind() } } private fun provideSharedPreferences( diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt deleted file mode 100644 index 78830b67b1..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistence.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import kotlinx.coroutines.flow.Flow - -interface FlowPersistence { - fun getFlow(key: String, defaultValue: T): Flow -} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt deleted file mode 100644 index e99fbe013b..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceImpl.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.russhwolf.settings.coroutines.FlowSettings -import kotlinx.coroutines.flow.Flow - -@Suppress("OPT_IN_USAGE") -internal class FlowPersistenceImpl(private val flowSettings: FlowSettings) : FlowPersistence { - @Suppress("UNCHECKED_CAST") - override fun getFlow(key: String, defaultValue: T): Flow = - when (defaultValue) { - is Long -> flowSettings.getLongFlow(key, defaultValue) as Flow - is String -> flowSettings.getStringFlow(key, defaultValue) as Flow - is Int -> flowSettings.getIntFlow(key, defaultValue) as Flow - is Boolean -> flowSettings.getBooleanFlow(key, defaultValue) as Flow - is Float -> flowSettings.getFloatFlow(key, defaultValue) as Flow - else -> throw UnsupportedPersistenceException() - } -} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt new file mode 100644 index 0000000000..77002313a1 --- /dev/null +++ b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/Persistence.kt @@ -0,0 +1,6 @@ +package com.oztechan.ccc.client.core.persistence + +interface Persistence { + fun getValue(key: String, defaultValue: T): T + fun setValue(key: String, value: T) +} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt new file mode 100644 index 0000000000..a2747caf61 --- /dev/null +++ b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceImpl.kt @@ -0,0 +1,25 @@ +package com.oztechan.ccc.client.core.persistence + +import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException +import com.russhwolf.settings.Settings + +internal class PersistenceImpl(private val settings: Settings) : Persistence { + @Suppress("UNCHECKED_CAST") + override fun getValue(key: String, defaultValue: T): T = when (defaultValue) { + is Long -> settings.getLong(key, defaultValue) as T + is String -> settings.getString(key, defaultValue) as T + is Int -> settings.getInt(key, defaultValue) as T + is Boolean -> settings.getBoolean(key, defaultValue) as T + is Float -> settings.getFloat(key, defaultValue) as T + else -> throw UnsupportedPersistenceException() + } + + override fun setValue(key: String, value: T) = when (value) { + is Long -> settings.putLong(key, value) + is String -> settings.putString(key, value) + is Int -> settings.putInt(key, value) + is Boolean -> settings.putBoolean(key, value) + is Float -> settings.putFloat(key, value) + else -> throw UnsupportedPersistenceException() + } +} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt deleted file mode 100644 index bb4e617c04..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistence.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -interface SuspendPersistence { - suspend fun getSuspend(key: String, defaultValue: T): T - suspend fun setSuspend(key: String, value: T) -} diff --git a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt b/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt deleted file mode 100644 index 9c905f492e..0000000000 --- a/client/core/persistence/src/commonMain/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.russhwolf.settings.coroutines.SuspendSettings - -@Suppress("OPT_IN_USAGE") -internal class SuspendPersistenceImpl(private val suspendSettings: SuspendSettings) : - SuspendPersistence { - - @Suppress("UNCHECKED_CAST") - override suspend fun getSuspend(key: String, defaultValue: T): T = - when (defaultValue) { - is Long -> suspendSettings.getLong(key, defaultValue) as T - is String -> suspendSettings.getString(key, defaultValue) as T - is Int -> suspendSettings.getInt(key, defaultValue) as T - is Boolean -> suspendSettings.getBoolean(key, defaultValue) as T - is Float -> suspendSettings.getFloat(key, defaultValue) as T - else -> throw UnsupportedPersistenceException() - } - - override suspend fun setSuspend(key: String, value: T) = when (value) { - is Long -> suspendSettings.putLong(key, value) - is String -> suspendSettings.putString(key, value) - is Int -> suspendSettings.putInt(key, value) - is Boolean -> suspendSettings.putBoolean(key, value) - is Float -> suspendSettings.putFloat(key, value) - else -> throw UnsupportedPersistenceException() - } -} diff --git a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt deleted file mode 100644 index 48df14a797..0000000000 --- a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/FlowPersistenceTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.KEY -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockBoolean -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockFloat -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockInt -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockLong -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockString -import com.russhwolf.settings.coroutines.FlowSettings -import io.mockative.Mock -import io.mockative.classOf -import io.mockative.configure -import io.mockative.every -import io.mockative.mock -import io.mockative.verify -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -@Suppress("OPT_IN_USAGE") -internal class FlowPersistenceTest { - private val flowPersistence: FlowPersistence by lazy { - FlowPersistenceImpl(flowSettings) - } - - @Mock - private val flowSettings = configure(mock(classOf())) { - stubsUnitByDefault = true - } - - @Test - fun `getFlow returns the same type`() = runTest { - every { flowSettings.getFloatFlow(KEY, mockFloat) } - .returns(flowOf(mockFloat)) - every { flowSettings.getBooleanFlow(KEY, mockBoolean) } - .returns(flowOf(mockBoolean)) - every { flowSettings.getIntFlow(KEY, mockInt) } - .returns(flowOf(mockInt)) - every { flowSettings.getStringFlow(KEY, mockString) } - .returns(flowOf(mockString)) - every { flowSettings.getLongFlow(KEY, mockLong) } - .returns(flowOf(mockLong)) - - assertEquals(mockFloat, flowPersistence.getFlow(KEY, mockFloat).firstOrNull()) - assertEquals(mockBoolean, flowPersistence.getFlow(KEY, mockBoolean).firstOrNull()) - assertEquals(mockInt, flowPersistence.getFlow(KEY, mockInt).firstOrNull()) - assertEquals(mockString, flowPersistence.getFlow(KEY, mockString).firstOrNull()) - assertEquals(mockLong, flowPersistence.getFlow(KEY, mockLong).firstOrNull()) - - verify { flowSettings.getFloatFlow(KEY, mockFloat) } - .wasInvoked() - verify { flowSettings.getBooleanFlow(KEY, mockBoolean) } - .wasInvoked() - verify { flowSettings.getIntFlow(KEY, mockInt) } - .wasInvoked() - verify { flowSettings.getStringFlow(KEY, mockString) } - .wasInvoked() - verify { flowSettings.getLongFlow(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `UnsupportedPersistenceException throw when unsupported type tried to read`() = - runTest { - val mockObject = object {} - - assertFailsWith(UnsupportedPersistenceException::class) { - flowPersistence.getFlow(KEY, mockObject) - } - } -} diff --git a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt new file mode 100644 index 0000000000..fbbccc21bc --- /dev/null +++ b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/PersistenceTest.kt @@ -0,0 +1,91 @@ +package com.oztechan.ccc.client.core.persistence + +import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.KEY +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockBoolean +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockFloat +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockInt +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockLong +import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockString +import com.russhwolf.settings.Settings +import io.mockative.Mock +import io.mockative.classOf +import io.mockative.configure +import io.mockative.every +import io.mockative.mock +import io.mockative.verify +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +internal class PersistenceTest { + private val persistence: Persistence by lazy { + PersistenceImpl(settings) + } + + @Mock + private val settings = configure(mock(classOf())) { stubsUnitByDefault = true } + + @Test + fun `getValue returns the same type`() { + every { settings.getFloat(KEY, mockFloat) } + .returns(mockFloat) + every { settings.getBoolean(KEY, mockBoolean) } + .returns(mockBoolean) + every { settings.getInt(KEY, mockInt) } + .returns(mockInt) + every { settings.getString(KEY, mockString) } + .returns(mockString) + every { settings.getLong(KEY, mockLong) } + .returns(mockLong) + + assertEquals(mockFloat, persistence.getValue(KEY, mockFloat)) + assertEquals(mockBoolean, persistence.getValue(KEY, mockBoolean)) + assertEquals(mockInt, persistence.getValue(KEY, mockInt)) + assertEquals(mockString, persistence.getValue(KEY, mockString)) + assertEquals(mockLong, persistence.getValue(KEY, mockLong)) + + verify { settings.getFloat(KEY, mockFloat) } + .wasInvoked() + verify { settings.getBoolean(KEY, mockBoolean) } + .wasInvoked() + verify { settings.getInt(KEY, mockInt) } + .wasInvoked() + verify { settings.getString(KEY, mockString) } + .wasInvoked() + verify { settings.getLong(KEY, mockLong) } + .wasInvoked() + } + + @Test + fun `setValue sets the same type`() { + persistence.setValue(KEY, mockFloat) + persistence.setValue(KEY, mockBoolean) + persistence.setValue(KEY, mockInt) + persistence.setValue(KEY, mockString) + persistence.setValue(KEY, mockLong) + + verify { settings.putFloat(KEY, mockFloat) } + .wasInvoked() + verify { settings.putBoolean(KEY, mockBoolean) } + .wasInvoked() + verify { settings.putInt(KEY, mockInt) } + .wasInvoked() + verify { settings.putString(KEY, mockString) } + .wasInvoked() + verify { settings.putLong(KEY, mockLong) } + .wasInvoked() + } + + @Test + fun `setValue throw UnsupportedPersistenceException when unsupported type tried to saved or read`() { + val mockObject = object {} + + assertFailsWith(UnsupportedPersistenceException::class) { + persistence.setValue(KEY, mockObject) + } + assertFailsWith(UnsupportedPersistenceException::class) { + persistence.getValue(KEY, mockObject) + } + } +} diff --git a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt b/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt deleted file mode 100644 index 6cc32d4b29..0000000000 --- a/client/core/persistence/src/commonTest/kotlin/com/oztechan/ccc/client/core/persistence/SuspendPersistenceTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.oztechan.ccc.client.core.persistence - -import com.oztechan.ccc.client.core.persistence.error.UnsupportedPersistenceException -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.KEY -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockBoolean -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockFloat -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockInt -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockLong -import com.oztechan.ccc.client.core.persistence.fakes.Fakes.mockString -import com.russhwolf.settings.coroutines.SuspendSettings -import io.mockative.Mock -import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify -import io.mockative.configure -import io.mockative.mock -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -@Suppress("OPT_IN_USAGE") -internal class SuspendPersistenceTest { - private val suspendPersistence: SuspendPersistence by lazy { - SuspendPersistenceImpl(suspendSettings) - } - - @Mock - private val suspendSettings = configure(mock(classOf())) { - stubsUnitByDefault = true - } - - @Test - fun `getSuspend returns the same type`() = runTest { - coEvery { suspendSettings.getFloat(KEY, mockFloat) } - .returns(mockFloat) - coEvery { suspendSettings.getBoolean(KEY, mockBoolean) } - .returns(mockBoolean) - coEvery { suspendSettings.getInt(KEY, mockInt) } - .returns(mockInt) - coEvery { suspendSettings.getString(KEY, mockString) } - .returns(mockString) - coEvery { suspendSettings.getLong(KEY, mockLong) } - .returns(mockLong) - - assertEquals(mockFloat, suspendPersistence.getSuspend(KEY, mockFloat)) - assertEquals(mockBoolean, suspendPersistence.getSuspend(KEY, mockBoolean)) - assertEquals(mockInt, suspendPersistence.getSuspend(KEY, mockInt)) - assertEquals(mockString, suspendPersistence.getSuspend(KEY, mockString)) - assertEquals(mockLong, suspendPersistence.getSuspend(KEY, mockLong)) - - coVerify { suspendSettings.getFloat(KEY, mockFloat) } - .wasInvoked() - coVerify { suspendSettings.getBoolean(KEY, mockBoolean) } - .wasInvoked() - coVerify { suspendSettings.getInt(KEY, mockInt) } - .wasInvoked() - coVerify { suspendSettings.getString(KEY, mockString) } - .wasInvoked() - coVerify { suspendSettings.getLong(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `setSuspend sets the same type`() = runTest { - suspendPersistence.setSuspend(KEY, mockFloat) - suspendPersistence.setSuspend(KEY, mockBoolean) - suspendPersistence.setSuspend(KEY, mockInt) - suspendPersistence.setSuspend(KEY, mockString) - suspendPersistence.setSuspend(KEY, mockLong) - - coVerify { suspendSettings.putFloat(KEY, mockFloat) } - .wasInvoked() - coVerify { suspendSettings.putBoolean(KEY, mockBoolean) } - .wasInvoked() - coVerify { suspendSettings.putInt(KEY, mockInt) } - .wasInvoked() - coVerify { suspendSettings.putString(KEY, mockString) } - .wasInvoked() - coVerify { suspendSettings.putLong(KEY, mockLong) } - .wasInvoked() - } - - @Test - fun `UnsupportedPersistenceException throw when unsupported type tried to saved or read`() = - runTest { - val mockObject = object {} - - assertFailsWith(UnsupportedPersistenceException::class) { - suspendPersistence.setSuspend(KEY, mockObject) - } - assertFailsWith(UnsupportedPersistenceException::class) { - suspendPersistence.getSuspend(KEY, mockObject) - } - } -} diff --git a/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt b/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt index d5e7ce4790..1e01c68c4a 100644 --- a/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt +++ b/client/core/persistence/src/iosMain/kotlin/com/oztechan/ccc/client/core/persistence/di/ClientCorePersistenceModule.ios.kt @@ -1,28 +1,16 @@ package com.oztechan.ccc.client.core.persistence.di -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.FlowPersistenceImpl -import com.oztechan.ccc.client.core.persistence.SuspendPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistenceImpl -import com.oztechan.ccc.common.core.infrastructure.di.DISPATCHER_IO +import com.oztechan.ccc.client.core.persistence.Persistence +import com.oztechan.ccc.client.core.persistence.PersistenceImpl import com.russhwolf.settings.NSUserDefaultsSettings -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.toFlowSettings -import com.russhwolf.settings.coroutines.toSuspendSettings -import org.koin.core.qualifier.named +import com.russhwolf.settings.Settings +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module actual val clientCorePersistenceModule = module { - single { + single { NSUserDefaultsSettings(get().userDefaults) } - - @Suppress("OPT_IN_USAGE") - single { - FlowPersistenceImpl(get().toFlowSettings(get(named(DISPATCHER_IO)))) - } - @Suppress("OPT_IN_USAGE") - single { - SuspendPersistenceImpl(get().toSuspendSettings(get(named(DISPATCHER_IO)))) - } + singleOf(::PersistenceImpl) { bind() } } diff --git a/client/repository/adcontrol/client-repository-adcontrol.gradle.kts b/client/repository/adcontrol/client-repository-adcontrol.gradle.kts index 3f265ab01d..3cddcc191c 100644 --- a/client/repository/adcontrol/client-repository-adcontrol.gradle.kts +++ b/client/repository/adcontrol/client-repository-adcontrol.gradle.kts @@ -23,7 +23,6 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt index 4aed693e02..e4957fd268 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepository.kt @@ -1,7 +1,7 @@ package com.oztechan.ccc.client.repository.adcontrol interface AdControlRepository { - suspend fun shouldShowBannerAd(): Boolean + fun shouldShowBannerAd(): Boolean - suspend fun shouldShowInterstitialAd(): Boolean + fun shouldShowInterstitialAd(): Boolean } diff --git a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt index e102aa1365..cf4fc5c4dc 100644 --- a/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt +++ b/client/repository/adcontrol/src/commonMain/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryImpl.kt @@ -8,10 +8,10 @@ internal class AdControlRepositoryImpl( private val appStorage: AppStorage, private val adConfigService: AdConfigService ) : AdControlRepository { - override suspend fun shouldShowBannerAd() = !appStorage.isFirstRun() && - appStorage.getPremiumEndDate().isPassed() && - appStorage.getSessionCount() > adConfigService.config.bannerAdSessionCount + override fun shouldShowBannerAd() = !appStorage.firstRun && + appStorage.premiumEndDate.isPassed() && + appStorage.sessionCount > adConfigService.config.bannerAdSessionCount - override suspend fun shouldShowInterstitialAd() = appStorage.getPremiumEndDate().isPassed() && - appStorage.getSessionCount() > adConfigService.config.interstitialAdSessionCount + override fun shouldShowInterstitialAd() = appStorage.premiumEndDate.isPassed() && + appStorage.sessionCount > adConfigService.config.interstitialAdSessionCount } diff --git a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt index 512740a6b8..ed906268aa 100644 --- a/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt +++ b/client/repository/adcontrol/src/commonTest/kotlin/com/oztechan/ccc/client/repository/adcontrol/AdControlRepositoryTest.kt @@ -6,12 +6,9 @@ import com.oztechan.ccc.client.core.shared.util.nowAsLong import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify -import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.BeforeTest import kotlin.test.Test @@ -40,302 +37,290 @@ internal class AdControlRepositoryTest { } @Test - fun `shouldShowBannerAd is false when firstRun and not premiumExpired and sessionCount smaller than banner 000`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when firstRun and not premiumExpired and sessionCount smaller than banner 000`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount smaller than banner 100`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount smaller than banner 100`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount smaller than banner 010`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount smaller than banner 010`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + not premiumExpired + sessionCount bigger than banner 001`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when firstRun + not premiumExpired + sessionCount bigger than banner 001`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount bigger than banner 011`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when firstRun + premiumExpired + sessionCount bigger than banner 011`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(true) + every { appStorage.firstRun } + .returns(true) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasNotInvoked() + verify { appStorage.premiumEndDate } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount bigger than banner 101`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is false when not firstRun + not premiumExpired + sessionCount bigger than banner 101`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() + verify { appStorage.sessionCount } + .wasNotInvoked() - verify { adConfigService.config } - .wasNotInvoked() - } + verify { adConfigService.config } + .wasNotInvoked() + } @Test - fun `shouldShowBannerAd is false when not firstRun + premiumExpired + sessionCount smaller than banner 110`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount - 1L) + fun `shouldShowBannerAd is false when not firstRun + premiumExpired + sessionCount smaller than banner 110`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount - 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertFalse { subject.shouldShowBannerAd() } + assertFalse { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() - } + verify { adConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowBannerAd is true when not firstRun + premiumExpired + sessionCount bigger than banner 111`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount + 1L) + fun `shouldShowBannerAd is true when not firstRun + premiumExpired + sessionCount bigger than banner 111`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount + 1L) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - assertTrue { subject.shouldShowBannerAd() } + assertTrue { subject.shouldShowBannerAd() } - coVerify { appStorage.isFirstRun() } - .wasInvoked() + verify { appStorage.firstRun } + .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() - } + verify { adConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() + 1) + fun `shouldShowInterstitialAd returns false when session count bigger than remote and premiumNotExpired 01`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() + 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config.interstitialAdSessionCount } - .wasNotInvoked() + verify { adConfigService.config.interstitialAdSessionCount } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() - } + verify { appStorage.sessionCount } + .wasNotInvoked() + } @Test - fun `shouldShowInterstitialAd returns true when session count bigger than remote and premiumExpired 11`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() + 1) + fun `shouldShowInterstitialAd returns true when session count bigger than remote and premiumExpired 11`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() + 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - assertTrue { subject.shouldShowInterstitialAd() } + assertTrue { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() + verify { adConfigService.config } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() - } + verify { appStorage.sessionCount } + .wasInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumExpired 00`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() - 1) + fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumExpired 00`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() - 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config } - .wasNotInvoked() + verify { adConfigService.config } + .wasNotInvoked() - coVerify { appStorage.getSessionCount() } - .wasNotInvoked() - } + verify { appStorage.sessionCount } + .wasNotInvoked() + } @Test - fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumNotExpired 10`() = - runTest { - coEvery { appStorage.getSessionCount() } - .returns(mockedSessionCount.toLong() - 1) + fun `shouldShowInterstitialAd returns false when session count smaller than remote and premiumNotExpired 10`() { + every { appStorage.sessionCount } + .returns(mockedSessionCount.toLong() - 1) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) + every { appStorage.premiumEndDate } + .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) - assertFalse { subject.shouldShowInterstitialAd() } + assertFalse { subject.shouldShowInterstitialAd() } - coVerify { appStorage.getPremiumEndDate() } - .wasInvoked() + verify { appStorage.premiumEndDate } + .wasInvoked() - verify { adConfigService.config } - .wasInvoked() + verify { adConfigService.config } + .wasInvoked() - coVerify { appStorage.getSessionCount() } - .wasInvoked() - } + verify { appStorage.sessionCount } + .wasInvoked() + } } diff --git a/client/repository/appconfig/client-repository-appconfig.gradle.kts b/client/repository/appconfig/client-repository-appconfig.gradle.kts index 46eedb0a02..b4f49c5b0a 100644 --- a/client/repository/appconfig/client-repository-appconfig.gradle.kts +++ b/client/repository/appconfig/client-repository-appconfig.gradle.kts @@ -33,7 +33,6 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt index e05c5ee075..21bd936fe5 100644 --- a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt +++ b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepository.kt @@ -10,7 +10,7 @@ interface AppConfigRepository { fun checkAppUpdate(isAppUpdateShown: Boolean): Boolean? - suspend fun shouldShowAppReview(): Boolean + fun shouldShowAppReview(): Boolean fun getVersion(): String } diff --git a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt index f018586c0b..f71b37eea8 100644 --- a/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt +++ b/client/repository/appconfig/src/commonMain/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryImpl.kt @@ -27,8 +27,8 @@ internal class AppConfigRepositoryImpl( it.updateForceVersion > BuildKonfig.versionCode } ?: run { null } // do not show - override suspend fun shouldShowAppReview(): Boolean = reviewConfigService.config - .whether { appStorage.getSessionCount() > it.appReviewSessionCount } + override fun shouldShowAppReview(): Boolean = reviewConfigService.config + .whether { appStorage.sessionCount > it.appReviewSessionCount } ?.mapTo { true } ?: false diff --git a/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt b/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt index a050858b72..b3b39abc03 100644 --- a/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt +++ b/client/repository/appconfig/src/commonTest/kotlin/com/oztechan/ccc/client/repository/appconfig/AppConfigRepositoryTest.kt @@ -8,12 +8,9 @@ import com.oztechan.ccc.client.core.shared.Device import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify -import kotlinx.coroutines.test.runTest import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -111,64 +108,61 @@ internal class AppConfigRepositoryTest { } @Test - fun `shouldShowAppReview should return true when sessionCount is biggerThan remote sessionCount`() = - runTest { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return true when sessionCount is biggerThan remote sessionCount`() { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockInteger.toLong() + 1) + every { appStorage.sessionCount } + .returns(mockInteger.toLong() + 1) - assertTrue { subject.shouldShowAppReview() } + assertTrue { subject.shouldShowAppReview() } - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowAppReview should return false when sessionCount is less than remote sessionCount`() = - runTest { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return false when sessionCount is less than remote sessionCount`() { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockInteger.toLong() - 1) + every { appStorage.sessionCount } + .returns(mockInteger.toLong() - 1) - assertFalse { subject.shouldShowAppReview() } + assertFalse { subject.shouldShowAppReview() } - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test - fun `shouldShowAppReview should return false when sessionCount is equal to remote sessionCount`() = - runTest { - val mockInteger = Random.nextInt() + fun `shouldShowAppReview should return false when sessionCount is equal to remote sessionCount`() { + val mockInteger = Random.nextInt() - every { reviewConfigService.config } - .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(appReviewSessionCount = mockInteger, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockInteger.toLong()) + every { appStorage.sessionCount } + .returns(mockInteger.toLong()) - assertFalse { subject.shouldShowAppReview() } + assertFalse { subject.shouldShowAppReview() } - coVerify { appStorage.getSessionCount() } - .wasInvoked() + verify { appStorage.sessionCount } + .wasInvoked() - verify { reviewConfigService.config } - .wasInvoked() - } + verify { reviewConfigService.config } + .wasInvoked() + } @Test fun getVersion() { diff --git a/client/storage/app/client-storage-app.gradle.kts b/client/storage/app/client-storage-app.gradle.kts index 3d1aab333e..5b4d634d95 100644 --- a/client/storage/app/client-storage-app.gradle.kts +++ b/client/storage/app/client-storage-app.gradle.kts @@ -20,7 +20,6 @@ kotlin { commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt index 34f4e1d378..cdb333376c 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorage.kt @@ -1,15 +1,11 @@ package com.oztechan.ccc.client.storage.app interface AppStorage { - suspend fun isFirstRun(): Boolean - suspend fun setFirstRun(value: Boolean) + var firstRun: Boolean - suspend fun getAppTheme(): Int - suspend fun setAppTheme(value: Int) + var appTheme: Int - suspend fun getPremiumEndDate(): Long - suspend fun setPremiumEndDate(value: Long) + var premiumEndDate: Long - suspend fun getSessionCount(): Long - suspend fun setSessionCount(value: Long) + var sessionCount: Long } diff --git a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt index df3a65c687..84d334c55e 100644 --- a/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt +++ b/client/storage/app/src/commonMain/kotlin/com/oztechan/ccc/client/storage/app/AppStorageImpl.kt @@ -1,33 +1,26 @@ package com.oztechan.ccc.client.storage.app -import com.oztechan.ccc.client.core.persistence.SuspendPersistence +import com.oztechan.ccc.client.core.persistence.Persistence internal class AppStorageImpl( - private val suspendPersistence: SuspendPersistence + private val persistence: Persistence ) : AppStorage { - override suspend fun isFirstRun(): Boolean = - suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) - override suspend fun setFirstRun(value: Boolean) = - suspendPersistence.setSuspend(KEY_FIRST_RUN, value) + override var firstRun + get() = persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) + set(value) = persistence.setValue(KEY_FIRST_RUN, value) - override suspend fun getAppTheme(): Int = - suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) + override var appTheme + get() = persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) + set(value) = persistence.setValue(KEY_APP_THEME, value) - override suspend fun setAppTheme(value: Int) = - suspendPersistence.setSuspend(KEY_APP_THEME, value) + override var premiumEndDate + get() = persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) + set(value) = persistence.setValue(KEY_PREMIUM_END_DATE, value) - override suspend fun getPremiumEndDate(): Long = - suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) - - override suspend fun setPremiumEndDate(value: Long) = - suspendPersistence.setSuspend(KEY_PREMIUM_END_DATE, value) - - override suspend fun getSessionCount(): Long = - suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) - - override suspend fun setSessionCount(value: Long) = - suspendPersistence.setSuspend(KEY_SESSION_COUNT, value) + override var sessionCount: Long + get() = persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) + set(value) = persistence.setValue(KEY_SESSION_COUNT, value) companion object { internal const val KEY_FIRST_RUN = "firs_run" diff --git a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt index 76ddf4c883..724b462ba0 100644 --- a/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt +++ b/client/storage/app/src/commonTest/kotlin/com/oztechan/ccc/client/storage/app/AppStorageTest.kt @@ -1,6 +1,6 @@ package com.oztechan.ccc.client.storage.app -import com.oztechan.ccc.client.core.persistence.SuspendPersistence +import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_APP_THEME import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_FIRST_RUN import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.DEFAULT_PREMIUM_END_DATE @@ -11,10 +11,10 @@ import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.KEY_PREMIUM_ import com.oztechan.ccc.client.storage.app.AppStorageImpl.Companion.KEY_SESSION_COUNT import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify +import io.mockative.configure +import io.mockative.every import io.mockative.mock -import kotlinx.coroutines.test.runTest +import io.mockative.verify import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -22,91 +22,91 @@ import kotlin.test.assertEquals internal class AppStorageTest { private val subject: AppStorage by lazy { - AppStorageImpl(suspendPersistence) + AppStorageImpl(persistence) } @Mock - private val suspendPersistence = mock(classOf()) + private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } // defaults @Test - fun `get default firstRun`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } + fun `default firstRun`() { + every { persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } .returns(DEFAULT_FIRST_RUN) - assertEquals(DEFAULT_FIRST_RUN, subject.isFirstRun()) + assertEquals(DEFAULT_FIRST_RUN, subject.firstRun) - coVerify { suspendPersistence.getSuspend(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } + verify { persistence.getValue(KEY_FIRST_RUN, DEFAULT_FIRST_RUN) } .wasInvoked() } @Test - fun `get default appTheme`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) } + fun `default appTheme`() { + every { persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) } .returns(DEFAULT_APP_THEME) - assertEquals(DEFAULT_APP_THEME, subject.getAppTheme()) + assertEquals(DEFAULT_APP_THEME, subject.appTheme) - coVerify { suspendPersistence.getSuspend(KEY_APP_THEME, DEFAULT_APP_THEME) } + verify { persistence.getValue(KEY_APP_THEME, DEFAULT_APP_THEME) } .wasInvoked() } @Test - fun `get default premiumEndDate`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } + fun `default premiumEndDate`() { + every { persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } .returns(DEFAULT_PREMIUM_END_DATE) - assertEquals(DEFAULT_PREMIUM_END_DATE, subject.getPremiumEndDate()) + assertEquals(DEFAULT_PREMIUM_END_DATE, subject.premiumEndDate) - coVerify { suspendPersistence.getSuspend(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } + verify { persistence.getValue(KEY_PREMIUM_END_DATE, DEFAULT_PREMIUM_END_DATE) } .wasInvoked() } @Test - fun `get default sessionCount`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } + fun `default sessionCount`() { + every { persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } .returns(DEFAULT_SESSION_COUNT) - assertEquals(DEFAULT_SESSION_COUNT, subject.getSessionCount()) + assertEquals(DEFAULT_SESSION_COUNT, subject.sessionCount) - coVerify { suspendPersistence.getSuspend(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } + verify { persistence.getValue(KEY_SESSION_COUNT, DEFAULT_SESSION_COUNT) } .wasInvoked() } // setters @Test - fun `set firstRun`() = runTest { + fun `set firstRun`() { val mockedValue = Random.nextBoolean() - subject.setFirstRun(mockedValue) + subject.firstRun = mockedValue - coVerify { suspendPersistence.setSuspend(KEY_FIRST_RUN, mockedValue) } + verify { persistence.setValue(KEY_FIRST_RUN, mockedValue) } .wasInvoked() } @Test - fun `set appTheme`() = runTest { + fun `set appTheme`() { val mockValue = Random.nextInt() - subject.setAppTheme(mockValue) + subject.appTheme = mockValue - coVerify { suspendPersistence.setSuspend(KEY_APP_THEME, mockValue) } + verify { persistence.setValue(KEY_APP_THEME, mockValue) } .wasInvoked() } @Test - fun `set premiumEndDate`() = runTest { + fun `set premiumEndDate`() { val mockValue = Random.nextLong() - subject.setPremiumEndDate(mockValue) + subject.premiumEndDate = mockValue - coVerify { suspendPersistence.setSuspend(KEY_PREMIUM_END_DATE, mockValue) } + verify { persistence.setValue(KEY_PREMIUM_END_DATE, mockValue) } .wasInvoked() } @Test - fun `set sessionCount`() = runTest { + fun `set sessionCount`() { val mockValue = Random.nextLong() - subject.setSessionCount(mockValue) + subject.sessionCount = mockValue - coVerify { suspendPersistence.setSuspend(KEY_SESSION_COUNT, mockValue) } + verify { persistence.setValue(KEY_SESSION_COUNT, mockValue) } .wasInvoked() } } diff --git a/client/storage/calculation/client-storage-calculation.gradle.kts b/client/storage/calculation/client-storage-calculation.gradle.kts index 2ccc3b6831..024143a5fb 100644 --- a/client/storage/calculation/client-storage-calculation.gradle.kts +++ b/client/storage/calculation/client-storage-calculation.gradle.kts @@ -14,17 +14,12 @@ kotlin { sourceSets { commonMain.dependencies { + implementation(libs.common.koinCore) implementation(project(Modules.Client.Core.persistence)) - - libs.common.apply { - implementation(koinCore) - implementation(coroutines) - } } commonTest.dependencies { libs.common.apply { implementation(test) - implementation(coroutinesTest) implementation(mockative) } } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt index a4f7c1b7c6..6156eb61e8 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorage.kt @@ -1,16 +1,9 @@ package com.oztechan.ccc.client.storage.calculation -import kotlinx.coroutines.flow.Flow - interface CalculationStorage { - fun getBaseFlow(): Flow - suspend fun getBase(): String - suspend fun setBase(value: String) + var currentBase: String - fun getLastInputFlow(): Flow - suspend fun getLastInput(): String - suspend fun setLastInput(value: String) + var precision: Int - suspend fun getPrecision(): Int - suspend fun setPrecision(value: Int) + var lastInput: String } diff --git a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt index 4ab52552b4..03a0a59a85 100644 --- a/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt +++ b/client/storage/calculation/src/commonMain/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageImpl.kt @@ -1,44 +1,30 @@ package com.oztechan.ccc.client.storage.calculation -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistence -import kotlinx.coroutines.flow.Flow +import com.oztechan.ccc.client.core.persistence.Persistence internal class CalculationStorageImpl( - private val suspendPersistence: SuspendPersistence, - private val flowPersistence: FlowPersistence + private val persistence: Persistence ) : CalculationStorage { - override fun getBaseFlow(): Flow = - flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) - override suspend fun getBase(): String = - suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) + override var currentBase + get() = persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) + set(value) = persistence.setValue(KEY_CURRENT_BASE, value) - override suspend fun setBase(value: String) = - suspendPersistence.setSuspend(KEY_CURRENT_BASE, value) + override var precision: Int + get() = persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) + set(value) = persistence.setValue(KEY_PRECISION, value) - override fun getLastInputFlow(): Flow = - flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) - - override suspend fun getLastInput(): String = - suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) - - override suspend fun setLastInput(value: String) = - suspendPersistence.setSuspend(KEY_LAST_INPUT, value) - - override suspend fun getPrecision(): Int = - suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) - - override suspend fun setPrecision(value: Int) = - suspendPersistence.setSuspend(KEY_PRECISION, value) + override var lastInput: String + get() = persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) + set(value) = persistence.setValue(KEY_LAST_INPUT, value) companion object { internal const val DEFAULT_CURRENT_BASE = "" - internal const val DEFAULT_LAST_INPUT = "" internal const val DEFAULT_PRECISION = 3 + internal const val DEFAULT_LAST_INPUT = "" internal const val KEY_CURRENT_BASE = "current_base" - internal const val KEY_LAST_INPUT = "last_input" internal const val KEY_PRECISION = "precision" + internal const val KEY_LAST_INPUT = "last_input" } } diff --git a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt index 801e7ef5ae..d37a563dde 100644 --- a/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt +++ b/client/storage/calculation/src/commonTest/kotlin/com/oztechan/ccc/client/storage/calculation/CalculationStorageTest.kt @@ -1,7 +1,6 @@ package com.oztechan.ccc.client.storage.calculation -import com.oztechan.ccc.client.core.persistence.FlowPersistence -import com.oztechan.ccc.client.core.persistence.SuspendPersistence +import com.oztechan.ccc.client.core.persistence.Persistence import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_CURRENT_BASE import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_LAST_INPUT import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.DEFAULT_PRECISION @@ -10,109 +9,81 @@ import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Compan import com.oztechan.ccc.client.storage.calculation.CalculationStorageImpl.Companion.KEY_PRECISION import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify +import io.mockative.configure import io.mockative.every import io.mockative.mock -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest +import io.mockative.verify import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals internal class CalculationStorageTest { private val subject: CalculationStorage by lazy { - CalculationStorageImpl(suspendPersistence, flowPersistence) + CalculationStorageImpl(persistence) } @Mock - private val suspendPersistence = mock(classOf()) - - @Mock - private val flowPersistence = mock(classOf()) - - @Test - fun getBaseFlow() = runTest { - every { flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } - .returns(flowOf(DEFAULT_CURRENT_BASE)) - - assertEquals(DEFAULT_CURRENT_BASE, subject.getBaseFlow().first()) - - coVerify { flowPersistence.getFlow(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } - .wasInvoked() - } - - @Test - fun getLastInputFlow() = runTest { - every { flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } - .returns(flowOf(DEFAULT_LAST_INPUT)) - - assertEquals(DEFAULT_LAST_INPUT, subject.getLastInputFlow().first()) - - coVerify { flowPersistence.getFlow(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } - .wasInvoked() - } + private val persistence = configure(mock(classOf())) { stubsUnitByDefault = true } // defaults @Test - fun `get default base`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + fun `default currentBase`() { + every { persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } .returns(DEFAULT_CURRENT_BASE) - assertEquals(DEFAULT_CURRENT_BASE, subject.getBase()) + assertEquals(DEFAULT_CURRENT_BASE, subject.currentBase) - coVerify { suspendPersistence.getSuspend(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } + verify { persistence.getValue(KEY_CURRENT_BASE, DEFAULT_CURRENT_BASE) } .wasInvoked() } @Test - fun `get default lastInput`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } - .returns(DEFAULT_LAST_INPUT) + fun `default precision`() { + every { persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) } + .returns(DEFAULT_PRECISION) - assertEquals(DEFAULT_LAST_INPUT, subject.getLastInput()) + assertEquals(DEFAULT_PRECISION, subject.precision) - coVerify { suspendPersistence.getSuspend(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + verify { persistence.getValue(KEY_PRECISION, DEFAULT_PRECISION) } .wasInvoked() } @Test - fun `get default precision`() = runTest { - coEvery { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } - .returns(DEFAULT_PRECISION) + fun `default lastInput`() { + every { persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } + .returns(DEFAULT_LAST_INPUT) - assertEquals(DEFAULT_PRECISION, subject.getPrecision()) + assertEquals(DEFAULT_LAST_INPUT, subject.lastInput) - coVerify { suspendPersistence.getSuspend(KEY_PRECISION, DEFAULT_PRECISION) } + verify { persistence.getValue(KEY_LAST_INPUT, DEFAULT_LAST_INPUT) } .wasInvoked() } // setters @Test - fun `set base`() = runTest { + fun `set currentBase`() { val mockValue = "mock" - subject.setBase(mockValue) + subject.currentBase = mockValue - coVerify { suspendPersistence.setSuspend(KEY_CURRENT_BASE, mockValue) } + verify { persistence.setValue(KEY_CURRENT_BASE, mockValue) } .wasInvoked() } @Test - fun `set lastInput`() = runTest { - val mockValue = "mock" - subject.setLastInput(mockValue) + fun `set precision`() { + val mockValue = Random.nextInt() + subject.precision = mockValue - coVerify { suspendPersistence.setSuspend(KEY_LAST_INPUT, mockValue) } + verify { persistence.setValue(KEY_PRECISION, mockValue) } .wasInvoked() } @Test - fun `set precision`() = runTest { - val mockValue = Random.nextInt() - subject.setPrecision(mockValue) + fun `set lastInput`() { + val mockValue = "mock" + subject.lastInput = mockValue - coVerify { suspendPersistence.setSuspend(KEY_PRECISION, mockValue) } + verify { persistence.setValue(KEY_LAST_INPUT, mockValue) } .wasInvoked() } } diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt index 5cb8c1bb21..172bb59b64 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorSEED.kt @@ -11,7 +11,7 @@ import com.oztechan.ccc.common.core.model.Currency // State data class CalculatorState( - val isBannerAdVisible: Boolean = false, + val isBannerAdVisible: Boolean, val input: String = "", val base: String = "", val currencyList: List = listOf(), @@ -32,6 +32,7 @@ interface CalculatorEvent : BaseEvent { fun onPasteToInput(text: String) fun onBarClick() fun onSettingsClicked() + fun onBaseChange(base: String) } // Effect diff --git a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt index 758c08bac8..cbc7bc9d8c 100644 --- a/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt +++ b/client/viewmodel/calculator/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModel.kt @@ -34,15 +34,15 @@ import com.oztechan.ccc.client.viewmodel.calculator.util.getConversionStringFrom import com.oztechan.ccc.common.core.model.Conversion import com.oztechan.ccc.common.core.model.Currency import com.oztechan.ccc.common.datasource.conversion.ConversionDataSource -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @Suppress("TooManyFunctions") @@ -56,7 +56,8 @@ class CalculatorViewModel( ) : BaseSEEDViewModel(), CalculatorEvent { // region SEED - private val _state = MutableStateFlow(CalculatorState()) + private val _state = + MutableStateFlow(CalculatorState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -68,51 +69,53 @@ class CalculatorViewModel( // endregion init { - combine( - currencyDataSource.getActiveCurrenciesFlow(), - calculationStorage.getBaseFlow(), - state.map { it.input }.distinctUntilChanged(), - ) { activeCurrencies, base, input -> - Logger.d { - "CalculatorViewModel combined: " + - "base: $base, input: $input, activeCurrencies: ${activeCurrencies.joinToString(",") { it.code }}" + currencyDataSource.getActiveCurrenciesFlow() + .onStart { + _state.update { + copy( + currencyList = currencyDataSource.getActiveCurrencies(), + base = calculationStorage.currentBase, + input = calculationStorage.lastInput, + loading = true + ) + } + updateConversion() + observeBase() + observeInput() } + .onEach { + Logger.d { "CalculatorViewModel currencyList changed: ${it.joinToString(",")}" } + _state.update { copy(currencyList = it) } - _state.update { copy(currencyList = activeCurrencies) } - - analyticsManager.setUserProperty( - UserProperty.CurrencyCount(activeCurrencies.count().toString()) - ) - - if (state.value.base != base) { - currentBaseChanged(base, input) + analyticsManager.setUserProperty(UserProperty.CurrencyCount(it.count().toString())) } + .launchIn(viewModelScope) + } - calculateOutput(input) - calculationStorage.setLastInput(input) - }.launchIn(viewModelScope) - - viewModelScope.launch { - _state.update { - copy( - currencyList = currencyDataSource.getActiveCurrencies(), - base = calculationStorage.getBase(), - input = calculationStorage.getLastInput(), - symbol = currencyDataSource.getCurrencyByCode(calculationStorage.getBase())?.symbol.orEmpty(), - loading = true, - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } + private fun observeBase() = state.map { it.base } + .distinctUntilChanged() + .onEach { + Logger.d { "CalculatorViewModel observeBase $it" } + currentBaseChanged(it, true) } - } + .launchIn(viewModelScope) + + private fun observeInput() = state.map { it.input } + .distinctUntilChanged() + .onEach { + Logger.d { "CalculatorViewModel observeInput $it" } + calculationStorage.lastInput = it + calculateOutput(it) + } + .launchIn(viewModelScope) - private suspend fun updateConversion() { + private fun updateConversion() { _state.update { copy(loading = true) } data.conversion?.let { calculateConversions(it, ConversionState.Cached(it.date)) } ?: viewModelScope.launch { - runCatching { backendApiService.getConversion(calculationStorage.getBase()) } + runCatching { backendApiService.getConversion(calculationStorage.currentBase) } .onFailure(::updateConversionFailed) .onSuccess(::updateConversionSuccess) } @@ -122,8 +125,9 @@ class CalculatorViewModel( conversion.copy(date = nowAsDateString()) .let { data.conversion = it + calculateConversions(it, ConversionState.Online(it.date)) + viewModelScope.launch { - calculateConversions(it, ConversionState.Online(it.date)) conversionDataSource.insertConversion(it) } } @@ -131,7 +135,7 @@ class CalculatorViewModel( private fun updateConversionFailed(t: Throwable) = viewModelScope.launchIgnored { Logger.w(t) { "CalculatorViewModel updateConversionFailed" } conversionDataSource.getConversionByBase( - calculationStorage.getBase() + calculationStorage.currentBase )?.let { calculateConversions(it, ConversionState.Offline(it.date)) } ?: run { @@ -143,25 +147,23 @@ class CalculatorViewModel( } } - private suspend fun calculateConversions( - conversion: Conversion?, - conversionState: ConversionState - ) = _state.update { - copy( - currencyList = _state.value.currencyList.onEach { - it.rate = conversion.calculateRate(it.code, _state.value.output) - .getFormatted(calculationStorage.getPrecision()) - .toStandardDigits() - }, - conversionState = conversionState, - loading = false - ) - } + private fun calculateConversions(conversion: Conversion?, conversionState: ConversionState) = + _state.update { + copy( + currencyList = _state.value.currencyList.onEach { + it.rate = conversion.calculateRate(it.code, _state.value.output) + .getFormatted(calculationStorage.precision) + .toStandardDigits() + }, + conversionState = conversionState, + loading = false + ) + } private fun calculateOutput(input: String) = viewModelScope.launch { val output = data.parser .calculate(input.toSupportedCharacters(), MAXIMUM_FLOATING_POINT) - .mapTo { if (isFinite()) getFormatted(calculationStorage.getPrecision()) else "" } + .mapTo { if (isFinite()) getFormatted(calculationStorage.precision) else "" } _state.update { copy(output = output) } @@ -185,20 +187,23 @@ class CalculatorViewModel( } } - private fun currentBaseChanged(newBase: String, input: String) = viewModelScope.launchIgnored { - Logger.d { "CalculatorViewModel currentBaseChanged $newBase" } - data.conversion = null - _state.update { - copy( - base = newBase, - input = input, - symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() - ) - } + private fun currentBaseChanged(newBase: String, shouldTrack: Boolean = false) = + viewModelScope.launchIgnored { + data.conversion = null + calculationStorage.currentBase = newBase + _state.update { + copy( + base = newBase, + input = input, + symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() + ) + } - analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) - analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) - } + if (shouldTrack) { + analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) + analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) + } + } // region Event override fun onKeyPress(key: String) { @@ -217,10 +222,10 @@ class CalculatorViewModel( } } - override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { + override fun onItemClick(currency: Currency) = with(currency) { Logger.d { "CalculatorViewModel onItemClick ${currency.code}" } - val newInput = currency.rate.toSupportedCharacters().let { + val newInput = rate.toSupportedCharacters().let { if (it.last() == CHAR_DOT) { it.dropLast(1) } else { @@ -229,13 +234,11 @@ class CalculatorViewModel( } _state.update { - copy(input = newInput) + copy( + base = code, + input = newInput + ) } - - @Suppress("MagicNumber") - delay(100) - - calculationStorage.setBase(currency.code) } override fun onItemImageLongClick(currency: Currency) { @@ -247,7 +250,7 @@ class CalculatorViewModel( _effect.emit( CalculatorEffect.ShowConversion( currency.getConversionStringFromBase( - calculationStorage.getBase(), + calculationStorage.currentBase, data.conversion ), currency.code @@ -296,5 +299,11 @@ class CalculatorViewModel( Logger.d { "CalculatorViewModel onSettingsClicked" } _effect.emit(CalculatorEffect.OpenSettings) } + + override fun onBaseChange(base: String) { + Logger.d { "CalculatorViewModel onBaseChange $base" } + currentBaseChanged(base) + calculateOutput(_state.value.input) + } // endregion } diff --git a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt index 2c15b82690..9f27b93a25 100644 --- a/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt +++ b/client/viewmodel/calculator/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/calculator/CalculatorViewModelTest.kt @@ -34,7 +34,6 @@ import io.mockative.every import io.mockative.mock import io.mockative.verify import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onSubscription @@ -48,8 +47,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue internal class CalculatorViewModelTest { @@ -98,25 +95,22 @@ internal class CalculatorViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { currencyDataSource.getActiveCurrenciesFlow() } - .returns(flowOf(currencyList)) - - every { calculationStorage.getBaseFlow() } - .returns(flowOf(currency1.code)) + every { calculationStorage.currentBase } + .returns(currency1.code) - runTest { - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) + every { calculationStorage.lastInput } + .returns("") - coEvery { calculationStorage.getBase() } - .returns(currency1.code) + every { currencyDataSource.getActiveCurrenciesFlow() } + .returns(flowOf(currencyList)) - coEvery { calculationStorage.getLastInput() } - .returns("") + every { calculationStorage.precision } + .returns(3) - coEvery { calculationStorage.getPrecision() } - .returns(3) + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) + runTest { coEvery { currencyDataSource.getActiveCurrencies() } .returns(currencyList) @@ -152,10 +146,10 @@ internal class CalculatorViewModelTest { assertEquals(ConversionState.Online(nowAsDateString()), it.conversionState) assertEquals(currencyList, it.currencyList) assertEquals(shouldShowAds, it.isBannerAdVisible) - assertTrue { it.loading } + assertFalse { it.loading } } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } @@ -163,10 +157,10 @@ internal class CalculatorViewModelTest { fun `init updates the latest base and input`() = runTest { val mock = "mock" - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(currency1.code) - coEvery { calculationStorage.getLastInput() } + every { calculationStorage.lastInput } .returns(mock) viewModel.state.firstOrNull().let { @@ -183,31 +177,6 @@ internal class CalculatorViewModelTest { assertNotNull(viewModel.data.parser) } - @Test - fun `base changes are observed correctly`() = runTest { - coEvery { calculationStorage.getBase() } - .returns(currency1.code) - - coEvery { calculationStorage.getBaseFlow() } - .returns(flowOf(currency1.code)) - - coEvery { backendApiService.getConversion(currency1.code) } - .returns(conversion) - - viewModel.state.firstOrNull().let { - assertNotNull(it) - assertNotNull(viewModel.data.conversion) - assertEquals(currency1.code, viewModel.data.conversion!!.base) - assertEquals(currency1.code, it.base) - - verify { analyticsManager.trackEvent(Event.BaseChange(Param.Base(currency1.code))) } - .wasInvoked() - - verify { analyticsManager.setUserProperty(UserProperty.BaseCurrency(currency1.code)) } - .wasInvoked() - } - } - @Test fun `when api fails and there is conversion in db then conversion rates are calculated`() = runTest { @@ -223,7 +192,7 @@ internal class CalculatorViewModelTest { val result = currencyList.onEach { currency -> currency.rate = conversion.calculateRate(currency.code, it.output) - .getFormatted(calculationStorage.getPrecision()) + .getFormatted(calculationStorage.precision) .toStandardDigits() } @@ -259,29 +228,32 @@ internal class CalculatorViewModelTest { } @Test - fun `when there is few currency app doesn't make API call or search in DB`() = runTest { - every { currencyDataSource.getActiveCurrenciesFlow() } - .returns(flowOf(listOf(currency1))) + fun `when api fails and there is no offline and no enough currency few currency effect emitted`() = + runTest { + coEvery { backendApiService.getConversion(currency1.code) } + .throws(Exception()) - viewModel.effect.onSubscription { - viewModel.event.onKeyPress("1") // trigger api call - }.firstOrNull().let { - assertIs(it) + coEvery { conversionDataSource.getConversionByBase(currency1.code) } + .returns(null) - viewModel.state.value.let { state -> - assertNotNull(state) - assertFalse { state.loading } - assertNull(viewModel.data.conversion) - assertEquals(ConversionState.None, state.conversionState) - } - } + every { currencyDataSource.getActiveCurrenciesFlow() } + .returns(flowOf(listOf(currency1))) - coVerify { conversionDataSource.getConversionByBase(currency1.code) } - .wasNotInvoked() + viewModel.effect.onSubscription { + viewModel.event.onKeyPress("1") // trigger api call + }.firstOrNull().let { + assertIs(it) - coVerify { backendApiService.getConversion(currency1.code) } - .wasNotInvoked() - } + viewModel.state.value.let { state -> + assertNotNull(state) + assertFalse { state.loading } + assertEquals(ConversionState.Error, state.conversionState) + } + } + + coVerify { conversionDataSource.getConversionByBase(currency1.code) } + .wasInvoked() + } @Test fun `when input is too long it should drop the last digit and give TooBigInput effect`() = @@ -369,42 +341,32 @@ internal class CalculatorViewModelTest { var currency = currency1 viewModel.state.onSubscription { viewModel.event.onItemClick(currency1) - delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency1.code, it.base) assertEquals(currency1.rate, it.input) } - coVerify { calculationStorage.setBase(currency1.code) } - .wasInvoked() - // when last digit is . it should be removed currency = currency.copy(rate = "123.") viewModel.state.onSubscription { viewModel.event.onItemClick(currency) - delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency.code, it.base) assertEquals("123", it.input) } - coVerify { calculationStorage.setBase(currency.code) } - .wasInvoked() currency = currency.copy(rate = "123 456.78") viewModel.state.onSubscription { viewModel.event.onItemClick(currency) - delay(100) }.firstOrNull().let { assertNotNull(it) assertEquals(currency.code, it.base) assertEquals("123456.78", it.input) } - coVerify { calculationStorage.setBase(currency.code) } - .wasInvoked() } @Test @@ -447,7 +409,6 @@ internal class CalculatorViewModelTest { val output = "5" viewModel.effect.onSubscription { viewModel.event.onKeyPress(output) - delay(100) viewModel.event.onOutputLongClick() }.firstOrNull().let { assertEquals(CalculatorEffect.CopyToClipboard(output), it) @@ -538,4 +499,28 @@ internal class CalculatorViewModelTest { assertEquals(key, it.input) } } + + @Test + fun onBaseChanged() = runTest { + every { calculationStorage.currentBase } + .returns(currency1.code) + + coEvery { backendApiService.getConversion(currency1.code) } + .returns(conversion) + + viewModel.state.onSubscription { + viewModel.event.onBaseChange(currency1.code) + }.firstOrNull().let { + assertNotNull(it) + assertNotNull(viewModel.data.conversion) + assertEquals(currency1.code, viewModel.data.conversion!!.base) + assertEquals(currency1.code, it.base) + + verify { analyticsManager.trackEvent(Event.BaseChange(Param.Base(currency1.code))) } + .wasInvoked() + + verify { analyticsManager.setUserProperty(UserProperty.BaseCurrency(currency1.code)) } + .wasInvoked() + } + } } diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt index f64d91f436..e6f2d24700 100644 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesSEED.kt @@ -8,8 +8,8 @@ import com.oztechan.ccc.common.core.model.Currency // State data class CurrenciesState( - val isBannerAdVisible: Boolean = false, - val isOnboardingVisible: Boolean = false, + val isBannerAdVisible: Boolean, + val isOnboardingVisible: Boolean, val currencyList: List = listOf(), val loading: Boolean = true, val selectionVisibility: Boolean = false @@ -30,6 +30,7 @@ sealed class CurrenciesEffect : BaseEffect { data object FewCurrency : CurrenciesEffect() data object OpenCalculator : CurrenciesEffect() data object Back : CurrenciesEffect() + data class ChangeBase(val newBase: String) : CurrenciesEffect() } // Data diff --git a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt index 6c01dae5c4..70d2394247 100755 --- a/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt +++ b/client/viewmodel/currencies/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModel.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch @Suppress("TooManyFunctions") class CurrenciesViewModel( @@ -39,7 +38,12 @@ class CurrenciesViewModel( ) : BaseSEEDViewModel(), CurrenciesEvent { // region SEED - private val _state = MutableStateFlow(CurrenciesState()) + private val _state = MutableStateFlow( + CurrenciesState( + isBannerAdVisible = adControlRepository.shouldShowBannerAd(), + isOnboardingVisible = appStorage.firstRun + ) + ) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -51,14 +55,6 @@ class CurrenciesViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - isOnboardingVisible = appStorage.isFirstRun(), - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } - } currencyDataSource.getCurrenciesFlow() .onEach { currencyList -> @@ -84,10 +80,10 @@ class CurrenciesViewModel( private suspend fun verifyListSize() = data.unFilteredList .filter { it.isActive } .whether { it.size < MINIMUM_ACTIVE_CURRENCY } - ?.whetherNot { appStorage.isFirstRun() } + ?.whetherNot { appStorage.firstRun } ?.run { _effect.emit(CurrenciesEffect.FewCurrency) } - private suspend fun verifyCurrentBase() = calculationStorage.getBase().either( + private suspend fun verifyCurrentBase() = calculationStorage.currentBase.either( { isEmpty() }, { base -> state.value.currencyList @@ -97,10 +93,12 @@ class CurrenciesViewModel( )?.mapTo { state.value.currencyList.firstOrNull { it.isActive }?.code.orEmpty() }?.let { newBase -> - calculationStorage.setBase(newBase) + calculationStorage.currentBase = newBase analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) + + _effect.emit(CurrenciesEffect.ChangeBase(newBase)) } private fun filterList(txt: String) = data.unFilteredList @@ -133,7 +131,7 @@ class CurrenciesViewModel( .whether { it < MINIMUM_ACTIVE_CURRENCY } ?.let { _effect.emit(CurrenciesEffect.FewCurrency) } ?: run { - appStorage.setFirstRun(false) + appStorage.firstRun = false _state.update { copy(isOnboardingVisible = false) } filterList("") _effect.emit(CurrenciesEffect.OpenCalculator) diff --git a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt index 8f7b9ad2a4..bc936f029b 100644 --- a/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt +++ b/client/viewmodel/currencies/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/currencies/CurrenciesViewModelTest.kt @@ -14,7 +14,6 @@ import com.oztechan.ccc.client.storage.calculation.CalculationStorage import com.oztechan.ccc.common.core.model.Currency import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery import io.mockative.coVerify import io.mockative.configure import io.mockative.every @@ -90,16 +89,14 @@ internal class CurrenciesViewModelTest { every { currencyDataSource.getCurrenciesFlow() } .returns(currencyListFlow) - runTest { - coEvery { appStorage.isFirstRun() } - .returns(false) + every { appStorage.firstRun } + .returns(false) - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) + every { calculationStorage.currentBase } + .returns(currency1.code) - coEvery { calculationStorage.getBase() } - .returns(currency1.code) - } + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) } // Analytics @@ -149,10 +146,10 @@ internal class CurrenciesViewModelTest { assertFalse { it.loading } } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() - coVerify { appStorage.isFirstRun() } + verify { appStorage.firstRun } .wasInvoked() } @@ -182,7 +179,7 @@ internal class CurrenciesViewModelTest { @Test fun `don't show FewCurrency effect if there is MINIMUM_ACTIVE_CURRENCY and not firstRun`() = runTest { - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("") // in order to get ChangeBase effect, have to have an effect to finish test every { currencyDataSource.getCurrenciesFlow() } @@ -193,19 +190,18 @@ internal class CurrenciesViewModelTest { } ) - viewModel // init - - coVerify { calculationStorage.setBase(currency1.code) } - .wasNotInvoked() + viewModel.effect.firstOrNull().let { + assertIs(it) + } } @Test fun `don't show FewCurrency effect if there is less than MINIMUM_ACTIVE_CURRENCY it is firstRun`() = runTest { - coEvery { appStorage.isFirstRun() } + every { appStorage.firstRun } .returns(true) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("") // in order to get ChangeBase effect, have to have an effect to finish test every { currencyDataSource.getCurrenciesFlow() } @@ -216,10 +212,9 @@ internal class CurrenciesViewModelTest { } ) - viewModel // init - - coVerify { calculationStorage.setBase(currency1.code) } - .wasNotInvoked() + viewModel.effect.firstOrNull().let { + assertIs(it) + } } @Test @@ -238,14 +233,22 @@ internal class CurrenciesViewModelTest { val firstActiveBase = currency1.code // first active currency every { currencyDataSource.getCurrenciesFlow() } - .returns(flowOf(currencyList)) + .returns( + flow { + delay(1.seconds.inWholeMilliseconds) + emit(currencyList) + } + ) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("") - viewModel // init + viewModel.effect.firstOrNull().let { + assertIs(it) + assertEquals(firstActiveBase, it.newBase) + } - coVerify { calculationStorage.setBase(firstActiveBase) } + verify { calculationStorage.currentBase = firstActiveBase } .wasInvoked() } @@ -255,31 +258,41 @@ internal class CurrenciesViewModelTest { currency1 = currency1.copy(isActive = false) // make first item in list not active every { currencyDataSource.getCurrenciesFlow() } - .returns(flowOf(listOf(currency1, currency2, currency3))) + .returns( + flow { + delay(1.seconds.inWholeMilliseconds) + emit(listOf(currency1, currency2, currency3)) + } + ) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns(currency1.code) // not active one - viewModel // init + viewModel.effect.firstOrNull().let { + assertIs(it) + assertEquals(currency2.code, it.newBase) + } - coVerify { calculationStorage.setBase(currency2.code) } + verify { calculationStorage.currentBase = currency2.code } .wasInvoked() } // Event @Test - fun updateAllCurrenciesState() = runTest { - coEvery { appStorage.isFirstRun() } + fun updateAllCurrenciesState() { + every { appStorage.firstRun } .returns(false) - coEvery { calculationStorage.getBase() } + every { calculationStorage.currentBase } .returns("EUR") val mockValue = Random.nextBoolean() viewModel.event.updateAllCurrenciesState(mockValue) - coVerify { currencyDataSource.updateCurrencyStates(mockValue) } - .wasInvoked() + runTest { + coVerify { currencyDataSource.updateCurrencyStates(mockValue) } + .wasInvoked() + } } @Test @@ -392,7 +405,7 @@ internal class CurrenciesViewModelTest { @Test fun onDoneClick() = runTest { - coEvery { appStorage.isFirstRun() } + every { appStorage.firstRun } .returns(true) // where there is single currency @@ -419,7 +432,7 @@ internal class CurrenciesViewModelTest { assertFalse { viewModel.state.value.isOnboardingVisible } - coVerify { appStorage.setFirstRun(false) } + verify { appStorage.firstRun = false } .wasInvoked() } diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt index adc2a944ef..b3ae057a6a 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainSEED.kt @@ -8,8 +8,8 @@ import kotlinx.coroutines.Job // State data class MainState( - var shouldOnboardUser: Boolean? = null, - var appTheme: Int? = null + var shouldOnboardUser: Boolean, + var appTheme: Int ) : BaseState // Effect diff --git a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt index 6dde5b9be5..3aa79de6a7 100644 --- a/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt +++ b/client/viewmodel/main/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModel.kt @@ -11,7 +11,6 @@ import com.oztechan.ccc.client.core.analytics.model.UserProperty import com.oztechan.ccc.client.core.shared.model.AppTheme import com.oztechan.ccc.client.core.shared.util.isNotPassed import com.oztechan.ccc.client.core.viewmodel.BaseSEEDViewModel -import com.oztechan.ccc.client.core.viewmodel.util.launchIgnored import com.oztechan.ccc.client.core.viewmodel.util.update import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository import com.oztechan.ccc.client.repository.appconfig.AppConfigRepository @@ -34,7 +33,12 @@ class MainViewModel( analyticsManager: AnalyticsManager, ) : BaseSEEDViewModel(), MainEvent { // region SEED - private val _state = MutableStateFlow(MainState()) + private val _state = MutableStateFlow( + MainState( + shouldOnboardUser = appStorage.firstRun, + appTheme = appStorage.appTheme + ) + ) override val state: StateFlow = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -46,30 +50,22 @@ class MainViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - appTheme = appStorage.getAppTheme(), - shouldOnboardUser = appStorage.isFirstRun() + with(analyticsManager) { + setUserProperty( + UserProperty.IsPremium( + appStorage.premiumEndDate.isNotPassed().toString() ) - } - with(analyticsManager) { - setUserProperty( - UserProperty.IsPremium( - appStorage.getPremiumEndDate().isNotPassed().toString() - ) - ) - setUserProperty(UserProperty.SessionCount(appStorage.getSessionCount().toString())) - setUserProperty( - UserProperty.AppTheme( - AppTheme.getAnalyticsThemeName( - appStorage.getAppTheme(), - appConfigRepository.getDeviceType() - ) + ) + setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) + setUserProperty( + UserProperty.AppTheme( + AppTheme.getAnalyticsThemeName( + appStorage.appTheme, + appConfigRepository.getDeviceType() ) ) - setUserProperty(UserProperty.DevicePlatform(appConfigRepository.getDeviceType().name)) - } + ) + setUserProperty(UserProperty.DevicePlatform(appConfigRepository.getDeviceType().name)) } } @@ -88,9 +84,9 @@ class MainViewModel( } } - private suspend fun adjustSessionCount() { + private fun adjustSessionCount() { if (data.isNewSession) { - appStorage.setSessionCount(appStorage.getSessionCount() + 1) + appStorage.sessionCount++ data.isNewSession = false } } @@ -109,7 +105,7 @@ class MainViewModel( } } - private suspend fun checkReview() { + private fun checkReview() { if (appConfigRepository.shouldShowAppReview()) { viewModelScope.launch { delay(reviewConfigService.config.appReviewDialogDelay) @@ -125,13 +121,13 @@ class MainViewModel( data.adVisibility = false } - override fun onResume() = viewModelScope.launchIgnored { + override fun onResume() { Logger.d { "MainViewModel onResume" } _state.update { copy( - shouldOnboardUser = appStorage.isFirstRun(), - appTheme = appStorage.getAppTheme() + shouldOnboardUser = appStorage.firstRun, + appTheme = appStorage.appTheme ) } diff --git a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt index 3fb6fc8a11..cc7e8c79b6 100644 --- a/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt +++ b/client/viewmodel/main/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/main/MainViewModelTest.kt @@ -21,8 +21,6 @@ import com.oztechan.ccc.client.repository.appconfig.AppConfigRepository import com.oztechan.ccc.client.storage.app.AppStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.configure import io.mockative.every import io.mockative.mock @@ -86,54 +84,45 @@ internal class MainViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) - every { appConfigRepository.getDeviceType() } - .returns(mockDevice) + every { appStorage.appTheme } + .returns(appThemeValue) - runTest { - coEvery { appStorage.getSessionCount() } - .returns(1L) + every { appStorage.premiumEndDate } + .returns(nowAsLong()) - coEvery { appStorage.getPremiumEndDate() } - .returns(nowAsLong()) + every { appStorage.sessionCount } + .returns(1L) - coEvery { adControlRepository.shouldShowInterstitialAd() } - .returns(false) + every { appConfigRepository.getDeviceType() } + .returns(mockDevice) - coEvery { appStorage.isFirstRun() } - .returns(isFirstRun) + every { adControlRepository.shouldShowInterstitialAd() } + .returns(false) - coEvery { appStorage.getAppTheme() } - .returns(appThemeValue) - } + every { appStorage.firstRun } + .returns(isFirstRun) } // Analytics @Test - fun ifUserPropertiesSetCorrect() = runTest { + fun ifUserPropertiesSetCorrect() { viewModel // init - coVerify { + verify { analyticsManager.setUserProperty( UserProperty.IsPremium( - appStorage.getPremiumEndDate().isNotPassed().toString() - ) - ) - }.wasInvoked() - - coVerify { - analyticsManager.setUserProperty( - UserProperty.SessionCount( - appStorage.getSessionCount().toString() + appStorage.premiumEndDate.isNotPassed().toString() ) ) } .wasInvoked() - - coVerify { + verify { analyticsManager.setUserProperty(UserProperty.SessionCount(appStorage.sessionCount.toString())) } + .wasInvoked() + verify { analyticsManager.setUserProperty( UserProperty.AppTheme( AppTheme.getAnalyticsThemeName( - appStorage.getAppTheme(), + appStorage.appTheme, mockDevice ) ) @@ -153,10 +142,10 @@ internal class MainViewModelTest { assertEquals(appThemeValue, it.appTheme) } - coVerify { appStorage.isFirstRun() } + verify { appStorage.firstRun } .wasInvoked() - coVerify { appStorage.getAppTheme() } + verify { appStorage.appTheme } .wasInvoked() } @@ -179,7 +168,7 @@ internal class MainViewModelTest { } @Test - fun `onResume adjustSessionCount`() = runTest { + fun `onResume adjustSessionCount`() = with(viewModel) { val mockSessionCount = Random.nextLong() every { reviewConfigService.config } @@ -188,7 +177,7 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } @@ -197,26 +186,26 @@ internal class MainViewModelTest { every { appConfigRepository.checkAppUpdate(true) } .returns(false) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) every { appConfigRepository.getMarketLink() } .returns("") - assertTrue { viewModel.data.isNewSession } + assertTrue { data.isNewSession } - viewModel.event.onResume() + event.onResume() - coVerify { appStorage.setSessionCount(mockSessionCount + 1) } + verify { appStorage.sessionCount = mockSessionCount + 1 } .wasInvoked() - assertFalse { viewModel.data.isNewSession } + assertFalse { data.isNewSession } - viewModel.event.onResume() + event.onResume() - coVerify { appStorage.setSessionCount(mockSessionCount + 1) } + verify { appStorage.sessionCount = mockSessionCount + 1 } .wasNotInvoked() - assertFalse { viewModel.data.isNewSession } + assertFalse { data.isNewSession } } @Test @@ -229,19 +218,19 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - coEvery { adControlRepository.shouldShowInterstitialAd() } + every { adControlRepository.shouldShowInterstitialAd() } .returns(true) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.seconds.inWholeMilliseconds) viewModel.effect.onSubscription { @@ -260,39 +249,40 @@ internal class MainViewModelTest { verify { reviewConfigService.config } .wasInvoked() - coVerify { adControlRepository.shouldShowInterstitialAd() } + verify { adControlRepository.shouldShowInterstitialAd() } .wasInvoked() - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasInvoked() } @Test - fun `onResume checkAppUpdate nothing happens when check update returns null`() = runTest { - val mockSessionCount = Random.nextLong() + fun `onResume checkAppUpdate nothing happens when check update returns null`() = + with(viewModel) { + val mockSessionCount = Random.nextLong() - every { reviewConfigService.config } - .returns(ReviewConfig(0, 0L)) + every { reviewConfigService.config } + .returns(ReviewConfig(0, 0L)) - every { adConfigService.config } - .returns(AdConfig(0, 0, 0L, 0L)) + every { adConfigService.config } + .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } - .returns(mockSessionCount) + every { appStorage.sessionCount } + .returns(mockSessionCount) - every { appConfigRepository.checkAppUpdate(false) } - .returns(null) + every { appConfigRepository.checkAppUpdate(false) } + .returns(null) - coEvery { appConfigRepository.shouldShowAppReview() } - .returns(true) + every { appConfigRepository.shouldShowAppReview() } + .returns(true) - viewModel.event.onResume() + event.onResume() - assertFalse { viewModel.data.isAppUpdateShown } + assertFalse { data.isAppUpdateShown } - verify { appConfigRepository.checkAppUpdate(false) } - .wasInvoked() - } + verify { appConfigRepository.checkAppUpdate(false) } + .wasInvoked() + } @Test fun `onResume checkAppUpdate app review should ask when check update returns not null`() = @@ -300,7 +290,7 @@ internal class MainViewModelTest { val mockSessionCount = Random.nextLong() val mockBoolean = Random.nextBoolean() - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { adConfigService.config } @@ -312,7 +302,7 @@ internal class MainViewModelTest { every { reviewConfigService.config } .returns(ReviewConfig(0, 0L)) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) every { appConfigRepository.getMarketLink() } @@ -344,13 +334,13 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) viewModel.effect.onSubscription { @@ -359,7 +349,7 @@ internal class MainViewModelTest { assertIs(it) } - coVerify { appConfigRepository.shouldShowAppReview() } + verify { appConfigRepository.shouldShowAppReview() } .wasInvoked() verify { reviewConfigService.config } @@ -368,7 +358,7 @@ internal class MainViewModelTest { @Test fun `onResume checkReview should do nothing when shouldShowAppReview returns false`() = - runTest { + with(viewModel) { val mockSessionCount = Random.nextLong() every { reviewConfigService.config } @@ -377,18 +367,18 @@ internal class MainViewModelTest { every { adConfigService.config } .returns(AdConfig(0, 0, 0L, 0L)) - coEvery { appStorage.getSessionCount() } + every { appStorage.sessionCount } .returns(mockSessionCount) every { appConfigRepository.checkAppUpdate(false) } .returns(null) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(false) - viewModel.onResume() + onResume() - coVerify { appConfigRepository.shouldShowAppReview() } + verify { appConfigRepository.shouldShowAppReview() } .wasInvoked() } @@ -397,7 +387,7 @@ internal class MainViewModelTest { every { appConfigRepository.checkAppUpdate(false) } .returns(false) - coEvery { appConfigRepository.shouldShowAppReview() } + every { appConfigRepository.shouldShowAppReview() } .returns(true) every { adConfigService.config } @@ -416,10 +406,10 @@ internal class MainViewModelTest { val newAppThemeValue = appThemeValue + 10 val newIsFirstRun = isFirstRun.not() - coEvery { appStorage.getAppTheme() } + every { appStorage.appTheme } .returns(newAppThemeValue) - coEvery { appStorage.isFirstRun() } + every { appStorage.firstRun } .returns(newIsFirstRun) viewModel.state @@ -431,10 +421,10 @@ internal class MainViewModelTest { assertEquals(newAppThemeValue, it.appTheme) } - coVerify { appStorage.isFirstRun() } + verify { appStorage.firstRun } .wasInvoked() - coVerify { appStorage.getAppTheme() } + verify { appStorage.appTheme } .wasInvoked() } } diff --git a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt index 3812352e7e..23b033f183 100644 --- a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt +++ b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModel.kt @@ -44,31 +44,29 @@ class PremiumViewModel( ) = viewModelScope.launchIgnored { Logger.d { "PremiumViewModel onPremiumActivated ${adType?.data?.duration.orEmpty()}" } adType?.let { - appStorage.setPremiumEndDate(it.calculatePremiumEnd(startDate)) + appStorage.premiumEndDate = it.calculatePremiumEnd(startDate) _effect.emit(PremiumEffect.PremiumActivated(it, isRestorePurchase)) } } - override fun onRestorePurchase(oldPurchaseList: List) = - viewModelScope.launchIgnored { - Logger.d { "PremiumViewModel onRestorePurchase" } - val premiumEndDate = appStorage.getPremiumEndDate() - oldPurchaseList - .maxByOrNull { - it.type.calculatePremiumEnd(it.date) - }?.whether( - { type.calculatePremiumEnd(date).isNotPassed() }, - { date > premiumEndDate }, - { PremiumType.getPurchaseIds().any { id -> id == type.data.id } } - )?.run { - onPremiumActivated( - adType = PremiumType.getById(type.data.id), - startDate = this.date, - isRestorePurchase = true - ) - _state.update { copy(loading = false) } - } - } + override fun onRestorePurchase(oldPurchaseList: List) { + Logger.d { "PremiumViewModel onRestorePurchase" } + oldPurchaseList + .maxByOrNull { + it.type.calculatePremiumEnd(it.date) + }?.whether( + { type.calculatePremiumEnd(date).isNotPassed() }, + { date > appStorage.premiumEndDate }, + { PremiumType.getPurchaseIds().any { id -> id == type.data.id } } + )?.run { + onPremiumActivated( + adType = PremiumType.getById(type.data.id), + startDate = this.date, + isRestorePurchase = true + ) + _state.update { copy(loading = false) } + } + } override fun onAddPurchaseMethods(premiumDataList: List) { Logger.d { "PremiumViewModel onAddPurchaseMethods" } diff --git a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt index 49169fb441..d263c3366c 100644 --- a/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt +++ b/client/viewmodel/premium/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumViewModelTest.kt @@ -14,10 +14,10 @@ import com.oztechan.ccc.client.viewmodel.premium.model.PremiumType import com.oztechan.ccc.client.viewmodel.premium.util.calculatePremiumEnd import io.mockative.Mock import io.mockative.classOf -import io.mockative.coEvery -import io.mockative.coVerify import io.mockative.configure +import io.mockative.every import io.mockative.mock +import io.mockative.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onSubscription @@ -72,7 +72,7 @@ internal class PremiumViewModelTest { @Test fun onPremiumActivated() = runTest { viewModel.event.onPremiumActivated(null) - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasNotInvoked() PremiumType.values().forEach { premiumType -> @@ -84,7 +84,7 @@ internal class PremiumViewModelTest { assertEquals(premiumType, it.premiumType) assertFalse { it.isRestorePurchase } - coVerify { appStorage.setPremiumEndDate(premiumType.calculatePremiumEnd(now)) } + verify { appStorage.premiumEndDate = premiumType.calculatePremiumEnd(now) } .wasInvoked() } } @@ -92,7 +92,7 @@ internal class PremiumViewModelTest { @Test fun onRestorePurchase() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(0) val now = nowAsLong() @@ -109,30 +109,34 @@ internal class PremiumViewModelTest { assertTrue { it.isRestorePurchase } assertFalse { viewModel.state.value.loading } - coVerify { appStorage.setPremiumEndDate(it.premiumType.calculatePremiumEnd(now)) } + verify { appStorage.premiumEndDate = it.premiumType.calculatePremiumEnd(now) } .wasInvoked() } // onRestorePurchase shouldn't do anything if all the old purchases out of dated var oldPurchase = OldPurchase(nowAsLong(), PremiumType.MONTH) - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) viewModel.event.onRestorePurchase(listOf(oldPurchase)) - coVerify { appStorage.setPremiumEndDate(oldPurchase.type.calculatePremiumEnd(oldPurchase.date)) } + verify { + appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) + } .wasNotInvoked() // onRestorePurchase shouldn't do anything if the old purchase is already expired oldPurchase = OldPurchase(nowAsLong() - (32.days.inWholeMilliseconds), PremiumType.MONTH) - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(0) viewModel.event.onRestorePurchase(listOf(oldPurchase)) - coVerify { appStorage.setPremiumEndDate(oldPurchase.type.calculatePremiumEnd(oldPurchase.date)) } + verify { + appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) + } .wasNotInvoked() } diff --git a/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts b/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts index 1dc8354b3a..bd79ec9ace 100644 --- a/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts +++ b/client/viewmodel/selectcurrency/client-viewmodel-selectcurrency.gradle.kts @@ -29,10 +29,6 @@ kotlin { implementation(project(currency)) } - Modules.Client.Storage.apply { - implementation(project(calculation)) - } - Modules.Common.Core.apply { implementation(project(model)) } diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt index f407f918a8..468d37c91a 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencySEED.kt @@ -20,6 +20,6 @@ interface SelectCurrencyEvent : BaseEvent { // Effect sealed class SelectCurrencyEffect : BaseEffect { - data object DismissDialog : SelectCurrencyEffect() + data class CurrencyChange(val newBase: String) : SelectCurrencyEffect() data object OpenCurrencies : SelectCurrencyEffect() } diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt index 4067d25acf..f8e5a2f820 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModel.kt @@ -10,7 +10,6 @@ import com.oztechan.ccc.client.core.viewmodel.BaseSEEDViewModel import com.oztechan.ccc.client.core.viewmodel.util.launchIgnored import com.oztechan.ccc.client.core.viewmodel.util.update import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource -import com.oztechan.ccc.client.storage.calculation.CalculationStorage import com.oztechan.ccc.common.core.model.Currency import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -20,8 +19,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class SelectCurrencyViewModel( - currencyDataSource: CurrencyDataSource, - private val calculationStorage: CalculationStorage, + currencyDataSource: CurrencyDataSource ) : BaseSEEDViewModel(), SelectCurrencyEvent { // region SEED private val _state = MutableStateFlow(SelectCurrencyState()) @@ -51,8 +49,7 @@ class SelectCurrencyViewModel( // region Event override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { Logger.d { "SelectCurrencyViewModel onItemClick ${currency.code}" } - calculationStorage.setBase(currency.code) - _effect.emit(SelectCurrencyEffect.DismissDialog) + _effect.emit(SelectCurrencyEffect.CurrencyChange(currency.code)) } override fun onSelectClick() = viewModelScope.launchIgnored { diff --git a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt index 03e94aad88..44356c362b 100644 --- a/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt +++ b/client/viewmodel/selectcurrency/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/di/ClientViewModelSelectCurrencyModule.kt @@ -5,5 +5,5 @@ import com.oztechan.ccc.client.viewmodel.selectcurrency.SelectCurrencyViewModel import org.koin.dsl.module val clientViewModelSelectCurrencyModule = module { - viewModelDefinition { SelectCurrencyViewModel(get(), get()) } + viewModelDefinition { SelectCurrencyViewModel(get()) } } diff --git a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt index 2d01022064..fd18fa17fc 100644 --- a/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt +++ b/client/viewmodel/selectcurrency/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/selectcurrency/SelectCurrencyViewModelTest.kt @@ -6,10 +6,8 @@ package com.oztechan.ccc.client.viewmodel.selectcurrency import co.touchlab.kermit.CommonWriter import co.touchlab.kermit.Logger import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource -import com.oztechan.ccc.client.storage.calculation.CalculationStorage import io.mockative.Mock import io.mockative.classOf -import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.verify @@ -33,15 +31,12 @@ import com.oztechan.ccc.common.core.model.Currency as CurrencyCommon internal class SelectCurrencyViewModelTest { private val subject: SelectCurrencyViewModel by lazy { - SelectCurrencyViewModel(currencyDataSource, calculationStorage) + SelectCurrencyViewModel(currencyDataSource) } @Mock private val currencyDataSource = mock(classOf()) - @Mock - private val calculationStorage = mock(classOf()) - private val currencyDollar = CurrencyCommon("USD", "Dollar", "$", "", true) private val currencyEuro = CurrencyCommon("Eur", "Euro", "", "", true) @@ -102,10 +97,8 @@ internal class SelectCurrencyViewModelTest { subject.effect.onSubscription { subject.event.onItemClick(currencyDollar) }.firstOrNull().let { - assertIs(it) - - coVerify { calculationStorage.setBase(currencyDollar.code) } - .wasInvoked() + assertIs(it) + assertEquals(currencyDollar.code, it.newBase) } } diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt index 3a031065e1..67cad2f71a 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsSEED.kt @@ -9,7 +9,7 @@ import com.oztechan.ccc.client.viewmodel.settings.model.PremiumStatus // State data class SettingsState( - val isBannerAdVisible: Boolean = false, + val isBannerAdVisible: Boolean, val activeCurrencyCount: Int = 0, val activeWatcherCount: Int = 0, val appThemeType: AppTheme = AppTheme.SYSTEM_DEFAULT, diff --git a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt index b6ebf556cf..0095ad5d6e 100644 --- a/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt +++ b/client/viewmodel/settings/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModel.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch @Suppress("TooManyFunctions", "LongParameterList") class SettingsViewModel( @@ -45,7 +44,8 @@ class SettingsViewModel( private val analyticsManager: AnalyticsManager ) : BaseSEEDViewModel(), SettingsEvent { // region SEED - private val _state = MutableStateFlow(SettingsState()) + private val _state = + MutableStateFlow(SettingsState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -57,16 +57,13 @@ class SettingsViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.getAppTheme()), - premiumStatus = appStorage.getPremiumEndDate().toPremiumStatus(), - precision = calculationStorage.getPrecision(), - version = appConfigRepository.getVersion(), - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } + _state.update { + copy( + appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.appTheme), + premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), + precision = calculationStorage.precision, + version = appConfigRepository.getVersion() + ) } currencyDataSource.getActiveCurrenciesFlow() @@ -141,7 +138,7 @@ class SettingsViewModel( override fun onPremiumClick() = viewModelScope.launchIgnored { Logger.d { "SettingsViewModel onPremiumClick" } - if (appStorage.getPremiumEndDate().isPassed()) { + if (appStorage.premiumEndDate.isPassed()) { _effect.emit(SettingsEffect.Premium) } else { _effect.emit(SettingsEffect.AlreadyPremium) @@ -170,18 +167,16 @@ class SettingsViewModel( _effect.emit(SettingsEffect.SelectPrecision) } - override fun onPrecisionSelect(index: Int) = viewModelScope.launchIgnored { + override fun onPrecisionSelect(index: Int) { Logger.d { "SettingsViewModel onPrecisionSelect $index" } - index.indexToNumber().let { - calculationStorage.setPrecision(it) - _state.update { copy(precision = it) } - } + calculationStorage.precision = index.indexToNumber() + _state.update { copy(precision = index.indexToNumber()) } } override fun onThemeChange(theme: AppTheme) = viewModelScope.launchIgnored { Logger.d { "SettingsViewModel onThemeChange $theme" } _state.update { copy(appThemeType = theme) } - appStorage.setAppTheme(theme.themeValue) + appStorage.appTheme = theme.themeValue _effect.emit(SettingsEffect.ChangeTheme(theme.themeValue)) } // endregion diff --git a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt index af709a90be..f0dbb832d2 100644 --- a/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt +++ b/client/viewmodel/settings/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/settings/SettingsViewModelTest.kt @@ -116,31 +116,29 @@ internal class SettingsViewModelTest { @Suppress("OPT_IN_USAGE") Dispatchers.setMain(UnconfinedTestDispatcher()) + every { appStorage.appTheme } + .returns(-1) + + every { appStorage.premiumEndDate } + .returns(0) + + every { calculationStorage.precision } + .returns(mockedPrecision) + every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyList)) every { watcherDataSource.getWatchersFlow() } .returns(flowOf(watcherLists)) + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) + every { appConfigRepository.getDeviceType() } .returns(Device.IOS) every { appConfigRepository.getVersion() } .returns(version) - - runTest { - coEvery { appStorage.getPremiumEndDate() } - .returns(0) - - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) - - coEvery { calculationStorage.getPrecision() } - .returns(mockedPrecision) - - coEvery { appStorage.getAppTheme() } - .returns(-1) - } } // init @@ -157,7 +155,7 @@ internal class SettingsViewModelTest { assertIs(it.premiumStatus) } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } @@ -169,7 +167,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is never set PremiumStatus is NeverActivated`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(0) viewModel.state.firstOrNull().let { @@ -180,7 +178,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is passed PremiumStatus is Expired`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() - 1.days.inWholeMilliseconds) viewModel.state.firstOrNull().let { @@ -191,7 +189,7 @@ internal class SettingsViewModelTest { @Test fun `when premiumEndDate is not passed PremiumStatus is Active`() = runTest { - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.state.firstOrNull().let { @@ -331,10 +329,10 @@ internal class SettingsViewModelTest { assertIs(it) } - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasInvoked() - coEvery { appStorage.getPremiumEndDate() } + every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.days.inWholeMilliseconds) viewModel.effect.onSubscription { @@ -343,7 +341,7 @@ internal class SettingsViewModelTest { assertIs(it) } - coVerify { appStorage.getPremiumEndDate() } + verify { appStorage.premiumEndDate } .wasInvoked() } @@ -399,7 +397,7 @@ internal class SettingsViewModelTest { println("-----") - coVerify { calculationStorage.setPrecision(value.indexToNumber()) } + verify { calculationStorage.precision = value.indexToNumber() } .wasInvoked() } } @@ -416,7 +414,7 @@ internal class SettingsViewModelTest { assertEquals(mockTheme.themeValue, it.themeValue) } - coVerify { appStorage.setAppTheme(mockTheme.themeValue) } + verify { appStorage.appTheme = mockTheme.themeValue } .wasInvoked() } } diff --git a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt index f5799306ed..02845f1d61 100644 --- a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt +++ b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersSEED.kt @@ -7,7 +7,7 @@ import com.oztechan.ccc.client.core.viewmodel.BaseState import com.oztechan.ccc.common.core.model.Watcher data class WatchersState( - val isBannerAdVisible: Boolean = false, + val isBannerAdVisible: Boolean, val watcherList: List = emptyList() ) : BaseState diff --git a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt index 7b8fc7c910..78f3998f34 100644 --- a/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt +++ b/client/viewmodel/watchers/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModel.kt @@ -29,7 +29,8 @@ class WatchersViewModel( private val analyticsManager: AnalyticsManager ) : BaseSEEDViewModel(), WatchersEvent { // region SEED - private val _state = MutableStateFlow(WatchersState()) + private val _state = + MutableStateFlow(WatchersState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) override val state = _state.asStateFlow() private val _effect = MutableSharedFlow() @@ -41,13 +42,6 @@ class WatchersViewModel( // endregion init { - viewModelScope.launch { - _state.update { - copy( - isBannerAdVisible = adControlRepository.shouldShowBannerAd() - ) - } - } watcherDataSource.getWatchersFlow() .onEach { _state.update { copy(watcherList = it) } @@ -76,11 +70,10 @@ class WatchersViewModel( watcherDataSource.updateWatcherBaseById(newBase, watcher.id) } - override fun onTargetChanged(watcher: Watcher, newTarget: String) = - viewModelScope.launchIgnored { - Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } - watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) - } + override fun onTargetChanged(watcher: Watcher, newTarget: String) = viewModelScope.launchIgnored { + Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } + watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) + } override fun onAddClick() = viewModelScope.launchIgnored { Logger.d { "WatcherViewModel onAddClick" } @@ -102,11 +95,10 @@ class WatchersViewModel( watcherDataSource.deleteWatcher(watcher.id) } - override fun onRelationChange(watcher: Watcher, isGreater: Boolean) = - viewModelScope.launchIgnored { - Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } - watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) - } + override fun onRelationChange(watcher: Watcher, isGreater: Boolean) = viewModelScope.launchIgnored { + Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } + watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) + } override fun onRateChange(watcher: Watcher, rate: String): String { Logger.d { "WatcherViewModel onRateChange $watcher $rate" } diff --git a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt index 66369f1fd6..f980545d38 100644 --- a/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt +++ b/client/viewmodel/watchers/src/commonTest/kotlin/com/oztechan/ccc/client/viewmodel/watchers/WatchersViewModelTest.kt @@ -73,10 +73,8 @@ internal class WatchersViewModelTest { every { watcherDataSource.getWatchersFlow() } .returns(flowOf(watcherList)) - runTest { - coEvery { adControlRepository.shouldShowBannerAd() } - .returns(shouldShowAds) - } + every { adControlRepository.shouldShowBannerAd() } + .returns(shouldShowAds) } // init @@ -88,7 +86,7 @@ internal class WatchersViewModelTest { assertEquals(watcherList, it.watcherList) } - coVerify { adControlRepository.shouldShowBannerAd() } + verify { adControlRepository.shouldShowBannerAd() } .wasInvoked() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd0546fd61..89daaeeb3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,6 @@ common-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl common-testAnnotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } common-koinCore = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } common-multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } -common-multiplatformSettingsCoroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } common-kotlinXDateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinXDateTime" } common-ktorLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } common-ktorJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } diff --git a/ios/CCC/UI/Calculator/CalculatorRootView.swift b/ios/CCC/UI/Calculator/CalculatorRootView.swift index 5c91afaf0e..a90d357d79 100644 --- a/ios/CCC/UI/Calculator/CalculatorRootView.swift +++ b/ios/CCC/UI/Calculator/CalculatorRootView.swift @@ -62,7 +62,7 @@ struct CalculatorRootView: View { text: String(\.choose_at_least_two_currency), buttonText: String(\.select), buttonAction: { - navigationStack.push(CurrenciesRootView()) + navigationStack.push(CurrenciesRootView(onBaseChange: { observable.event.onBaseChange(base: $0) })) } ) } @@ -80,7 +80,10 @@ struct CalculatorRootView: View { .sheet( isPresented: $isBarShown, content: { - SelectCurrencyRootView(isBarShown: $isBarShown).environmentObject(navigationStack) + SelectCurrencyRootView( + isBarShown: $isBarShown, + onCurrencySelected: { observable.event.onBaseChange(base: $0) } + ).environmentObject(navigationStack) } ) .onAppear { @@ -105,7 +108,7 @@ struct CalculatorRootView: View { case is CalculatorEffect.OpenBar: isBarShown = true case is CalculatorEffect.OpenSettings: - navigationStack.push(SettingsRootView()) + navigationStack.push(SettingsRootView(onBaseChange: { observable.event.onBaseChange(base: $0) })) case is CalculatorEffect.ShowPasteRequest: isPasteRequestSnackShown.toggle() case let copyToClipboardEffect as CalculatorEffect.CopyToClipboard: diff --git a/ios/CCC/UI/Currencies/CurrenciesRootView.swift b/ios/CCC/UI/Currencies/CurrenciesRootView.swift index f78f7e3bff..71e6e6e156 100644 --- a/ios/CCC/UI/Currencies/CurrenciesRootView.swift +++ b/ios/CCC/UI/Currencies/CurrenciesRootView.swift @@ -24,6 +24,8 @@ struct CurrenciesRootView: View { private let analyticsManager: AnalyticsManager = koin.get() + var onBaseChange: (String) -> Void + var body: some View { CurrenciesView( event: observable.event, @@ -48,6 +50,8 @@ struct CurrenciesRootView: View { navigationStack.push(CalculatorRootView()) case is CurrenciesEffect.Back: navigationStack.pop() + case let changeBaseEffect as CurrenciesEffect.ChangeBase: + onBaseChange(changeBaseEffect.newBase) default: logger.i(message: { "CurrenciesRootView unknown effect" }) } diff --git a/ios/CCC/UI/Main/MainView.swift b/ios/CCC/UI/Main/MainView.swift index 955ad4a2b2..dbac5a7b80 100644 --- a/ios/CCC/UI/Main/MainView.swift +++ b/ios/CCC/UI/Main/MainView.swift @@ -18,13 +18,10 @@ struct MainView: View { transitionType: .default, easing: Animation.easeInOut ) { - switch state.shouldOnboardUser { - case true: + if state.shouldOnboardUser { IntroSlideRootView() - case false: + } else { CalculatorRootView() - default: - EmptyView() } } } diff --git a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift index 209512be66..7523992979 100644 --- a/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift +++ b/ios/CCC/UI/SelectCurrency/SelectCurrencyRootView.swift @@ -24,6 +24,8 @@ struct SelectCurrencyRootView: View { private let analyticsManager: AnalyticsManager = koin.get() + var onCurrencySelected: (String) -> Void + var body: some View { SelectCurrencyView( event: observable.event, @@ -40,10 +42,11 @@ struct SelectCurrencyRootView: View { private func onEffect(effect: SelectCurrencyEffect) { logger.i(message: { "SelectCurrencyRootView onEffect \(effect.description)" }) switch effect { - case is SelectCurrencyEffect.DismissDialog: + case let currencyChangeEffect as SelectCurrencyEffect.CurrencyChange: + onCurrencySelected(currencyChangeEffect.newBase) isBarShown = false case is SelectCurrencyEffect.OpenCurrencies: - navigationStack.push(CurrenciesRootView()) + navigationStack.push(CurrenciesRootView(onBaseChange: onCurrencySelected)) default: logger.i(message: { "SelectCurrencyRootView unknown effect" }) } diff --git a/ios/CCC/UI/Settings/SettingsRootView.swift b/ios/CCC/UI/Settings/SettingsRootView.swift index 29199dd007..42427f48a7 100644 --- a/ios/CCC/UI/Settings/SettingsRootView.swift +++ b/ios/CCC/UI/Settings/SettingsRootView.swift @@ -31,6 +31,8 @@ struct SettingsRootView: View { private let analyticsManager: AnalyticsManager = koin.get() + var onBaseChange: ((String) -> Void) + var body: some View { SettingsView( event: observable.event, @@ -72,7 +74,7 @@ struct SettingsRootView: View { case is SettingsEffect.Back: navigationStack.pop() case is SettingsEffect.OpenCurrencies: - navigationStack.push(CurrenciesRootView()) + navigationStack.push(CurrenciesRootView(onBaseChange: onBaseChange)) case is SettingsEffect.OpenWatchers: navigationStack.push(WatchersRootView()) case is SettingsEffect.FeedBack: diff --git a/ios/CCC/UI/Slides/BugReportSlideRootView.swift b/ios/CCC/UI/Slides/BugReportSlideRootView.swift index e4ba1d9f65..593e24c6e9 100644 --- a/ios/CCC/UI/Slides/BugReportSlideRootView.swift +++ b/ios/CCC/UI/Slides/BugReportSlideRootView.swift @@ -24,7 +24,7 @@ struct BugReportSlideRootView: View { subTitle1: String(\.slide_bug_report_text_1), subTitle2: String(\.slide_bug_report_text_2), buttonText: String(\.got_it), - buttonAction: { navigationStack.push(CurrenciesRootView()) } + buttonAction: { navigationStack.push(CurrenciesRootView(onBaseChange: { _ in })) } ) .onAppear { analyticsManager.trackScreen(screenName: ScreenName.Slider(position: 2)) diff --git a/ios/CCC/UI/Watchers/WatcherItem.swift b/ios/CCC/UI/Watchers/WatcherItem.swift index d938bc3f7f..22324b5fcb 100644 --- a/ios/CCC/UI/Watchers/WatcherItem.swift +++ b/ios/CCC/UI/Watchers/WatcherItem.swift @@ -16,6 +16,9 @@ struct WatcherItem: View { @State private var relationSelection = 0 @State private var amount = "" + @Binding var isBaseBarShown: Bool + @Binding var isTargetBarShown: Bool + let watcher: Provider.Watcher let event: WatchersEvent diff --git a/ios/CCC/UI/Watchers/WatchersRootView.swift b/ios/CCC/UI/Watchers/WatchersRootView.swift index 8dc046a903..a8983c2f84 100644 --- a/ios/CCC/UI/Watchers/WatchersRootView.swift +++ b/ios/CCC/UI/Watchers/WatchersRootView.swift @@ -49,13 +49,29 @@ struct WatchersRootView: View { .sheet( isPresented: $baseBarInfo.isShown, content: { - SelectCurrencyRootView(isBarShown: $baseBarInfo.isShown).environmentObject(navigationStack) + SelectCurrencyRootView( + isBarShown: $baseBarInfo.isShown, + onCurrencySelected: { + observable.event.onBaseChanged( + watcher: baseBarInfo.watcher!, + newBase: $0 + ) + } + ).environmentObject(navigationStack) } ) .sheet( isPresented: $targetBarInfo.isShown, content: { - SelectCurrencyRootView(isBarShown: $targetBarInfo.isShown).environmentObject(navigationStack) + SelectCurrencyRootView( + isBarShown: $targetBarInfo.isShown, + onCurrencySelected: { + observable.event.onTargetChanged( + watcher: targetBarInfo.watcher!, + newTarget: $0 + ) + } + ).environmentObject(navigationStack) } ) .onAppear { diff --git a/ios/CCC/UI/Watchers/WatchersView.swift b/ios/CCC/UI/Watchers/WatchersView.swift index 4fb2b0420d..61bde8024e 100644 --- a/ios/CCC/UI/Watchers/WatchersView.swift +++ b/ios/CCC/UI/Watchers/WatchersView.swift @@ -33,6 +33,8 @@ struct WatchersView: View { Form { List(state.watcherList, id: \.id) { watcher in WatcherItem( + isBaseBarShown: $baseBarInfo.isShown, + isTargetBarShown: $targetBarInfo.isShown, watcher: watcher, event: event )