diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..7fc2e2519f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,98 @@ +name: Build + +on: + schedule: + - cron: '0 0 * * 0' # Runs every Sunday at midnight UTC + workflow_dispatch: # Allows manual trigger + +env: + BASE_URL_BACKEND: ${{ secrets.BASE_URL_BACKEND }} + BASE_URL_API: ${{ secrets.BASE_URL_API }} + BASE_URL_API_PREMIUM: ${{ secrets.BASE_URL_API_PREMIUM }} + API_KEY_PREMIUM: ${{ secrets.API_KEY_PREMIUM }} + ANDROID_KEY_STORE_PATH: ${{ secrets.ANDROID_KEY_STORE_PATH }} + ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + RELEASE_ADVERTISEMENT_ID_GOOGLE: ${{ secrets.RELEASE_ADVERTISEMENT_ID_GOOGLE }} + DEBUG_ADVERTISEMENT_ID_GOOGLE: ${{ secrets.DEBUG_ADVERTISEMENT_ID_GOOGLE }} + RELEASE_ADVERTISEMENT_ID_HUAWEI: ${{ secrets.RELEASE_ADVERTISEMENT_ID_HUAWEI }} + DEBUG_ADVERTISEMENT_ID_HUAWEI: ${{ secrets.DEBUG_ADVERTISEMENT_ID_HUAWEI }} + GOOGLE_BANNER_AD_UNIT_ID_CALCULATOR_RELEASE: ${{ secrets.GOOGLE_BANNER_AD_UNIT_ID_CALCULATOR_RELEASE }} + GOOGLE_BANNER_AD_UNIT_ID_SETTINGS_RELEASE: ${{ secrets.GOOGLE_BANNER_AD_UNIT_ID_SETTINGS_RELEASE }} + GOOGLE_BANNER_AD_UNIT_ID_CURRENCIES_RELEASE: ${{ secrets.GOOGLE_BANNER_AD_UNIT_ID_CURRENCIES_RELEASE }} + GOOGLE_INTERSTITIAL_AD_ID_RELEASE: ${{ secrets.GOOGLE_INTERSTITIAL_AD_ID_RELEASE }} + GOOGLE_REWARDED_AD_UNIT_ID_RELEASE: ${{ secrets.GOOGLE_REWARDED_AD_UNIT_ID_RELEASE }} + GOOGLE_BANNER_AD_UNIT_ID_CALCULATOR_DEBUG: ${{ secrets.GOOGLE_BANNER_AD_UNIT_ID_CALCULATOR_DEBUG }} + GOOGLE_BANNER_AD_UNIT_ID_SETTINGS_DEBUG: ${{ secrets.GOOGLE_BANNER_AD_UNIT_ID_SETTINGS_DEBUG }} + GOOGLE_BANNER_AD_UNIT_ID_CURRENCIES_DEBUG: ${{ secrets.GOOGLE_BANNER_AD_UNIT_ID_CURRENCIES_DEBUG }} + GOOGLE_INTERSTITIAL_AD_ID_DEBUG: ${{ secrets.GOOGLE_INTERSTITIAL_AD_ID_DEBUG }} + GOOGLE_REWARDED_AD_UNIT_ID_DEBUG: ${{ secrets.GOOGLE_REWARDED_AD_UNIT_ID_DEBUG }} + HUAWEI_BANNER_AD_UNIT_ID_CALCULATOR_RELEASE: ${{ secrets.HUAWEI_BANNER_AD_UNIT_ID_CALCULATOR_RELEASE }} + HUAWEI_BANNER_AD_UNIT_ID_SETTINGS_RELEASE: ${{ secrets.HUAWEI_BANNER_AD_UNIT_ID_SETTINGS_RELEASE }} + HUAWEI_BANNER_AD_UNIT_ID_CURRENCIES_RELEASE: ${{ secrets.HUAWEI_BANNER_AD_UNIT_ID_CURRENCIES_RELEASE }} + HUAWEI_INTERSTITIAL_AD_ID_RELEASE: ${{ secrets.HUAWEI_INTERSTITIAL_AD_ID_RELEASE }} + HUAWEI_REWARDED_AD_UNIT_ID_RELEASE: ${{ secrets.HUAWEI_REWARDED_AD_UNIT_ID_RELEASE }} + HUAWEI_BANNER_AD_UNIT_ID_CALCULATOR_DEBUG: ${{ secrets.HUAWEI_BANNER_AD_UNIT_ID_CALCULATOR_DEBUG }} + HUAWEI_BANNER_AD_UNIT_ID_SETTINGS_DEBUG: ${{ secrets.HUAWEI_BANNER_AD_UNIT_ID_SETTINGS_DEBUG }} + HUAWEI_BANNER_AD_UNIT_ID_CURRENCIES_DEBUG: ${{ secrets.HUAWEI_BANNER_AD_UNIT_ID_CURRENCIES_DEBUG }} + HUAWEI_INTERSTITIAL_AD_ID_DEBUG: ${{ secrets.HUAWEI_INTERSTITIAL_AD_ID_DEBUG }} + HUAWEI_REWARDED_AD_UNIT_ID_DEBUG: ${{ secrets.HUAWEI_REWARDED_AD_UNIT_ID_DEBUG }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }} + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + IOS_RELEASE_FIREBASE_APP_ID: ${{ secrets.IOS_RELEASE_FIREBASE_APP_ID }} + IOS_DEBUG_FIREBASE_APP_ID: ${{ secrets.IOS_DEBUG_FIREBASE_APP_ID }} + FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} + GIT_AUTHORIZATION: ${{ secrets.GIT_AUTHORIZATION }} + SECRET_PASSWORD: ${{ secrets.SECRET_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.SECRET_PASSWORD }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 5 + FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 5 + CI: true + +jobs: + + Build: + runs-on: macos-14 + outputs: + status: ${{ steps.status.outputs.status }} + steps: + + - name: Setup Gradle Repo + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e + + - name: Adding secret files + uses: ./.github/actions/add-secret-files + with: + ANDROID_RELEASE_KEYSTORE_ASC: ${{ secrets.ANDROID_RELEASE_KEYSTORE_ASC }} + SECRET_PASSWORD: ${{ secrets.SECRET_PASSWORD }} + GOOGLE_SERVICES_JSON_ASC: ${{ secrets.GOOGLE_SERVICES_JSON_ASC }} + AG_CONNECT_SERVICES_JSON_ASC: ${{ secrets.AG_CONNECT_SERVICES_JSON_ASC }} + GOOGLE_SERVICE_INFO_PLIST_ASC_RELEASE: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_ASC_RELEASE }} + GOOGLE_SERVICE_INFO_PLIST_ASC_DEBUG: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_ASC_DEBUG }} + IOS_XCCONFIG_ASC_RELEASE: ${{ secrets.IOS_XCCONFIG_ASC_RELEASE }} + IOS_XCCONFIG_ASC_DEBUG: ${{ secrets.IOS_XCCONFIG_ASC_DEBUG }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + + - name: Build + run: ./gradlew build + + - name: Set Job Status + id: status + run: echo "status=success" >> $GITHUB_OUTPUT + + Notify: + runs-on: ubuntu-22.04 + needs: [ Build ] + if: always() + steps: + + - name: Notify slack fail + if: false == (needs.Build.outputs.status == 'success') + uses: voxmedia/github-action-slack-notify-build@v1.6.0 + with: + channel: ccc-github + status: FAILED + color: danger diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3b47568b1..3836996911 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,10 +5,12 @@ on: branches: - develop pull_request: + schedule: + - cron: '0 0 * * *' # At 00:00 every day concurrency: - group: ${{ github.ref == 'refs/heads/develop' && 'develop' || github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/develop' }} + group: ${{ github.ref == 'refs/heads/develop' && 'develop' || github.ref }} # Only run one job at a time for the same branch + cancel-in-progress: ${{ github.ref != 'refs/heads/develop' }} # Only cancel the same branch but not develop env: BASE_URL_BACKEND: ${{ secrets.BASE_URL_BACKEND }} @@ -46,7 +48,8 @@ env: APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }} APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} - IOS_GOOGLE_FIREBASE_APP_ID: ${{ secrets.IOS_GOOGLE_FIREBASE_APP_ID }} + IOS_RELEASE_FIREBASE_APP_ID: ${{ secrets.IOS_RELEASE_FIREBASE_APP_ID }} + IOS_DEBUG_FIREBASE_APP_ID: ${{ secrets.IOS_DEBUG_FIREBASE_APP_ID }} FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} GIT_AUTHORIZATION: ${{ secrets.GIT_AUTHORIZATION }} SECRET_PASSWORD: ${{ secrets.SECRET_PASSWORD }} @@ -55,6 +58,8 @@ env: FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 5 FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 5 CI: true + BUILD_TYPE: ${{ github.event_name == 'schedule' && 'Release' || 'Debug' }} + BUILD_TYPE_LOWERCASE: ${{ github.event_name == 'schedule' && 'release' || 'debug' }} jobs: @@ -65,7 +70,7 @@ jobs: steps: - name: Setup Gradle Repo - uses: Oztechan/Global/actions/setup-gradle-repo@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e - name: Adding secret files uses: ./.github/actions/add-secret-files @@ -80,19 +85,17 @@ jobs: IOS_XCCONFIG_ASC_DEBUG: ${{ secrets.IOS_XCCONFIG_ASC_DEBUG }} GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} - - name: Assemble - run: ./gradlew assemble + - name: Assemble ${{ env.BUILD_TYPE }} + run: ./gradlew assemble${{ env.BUILD_TYPE }} - - name: Upload Android Artifacts - uses: actions/upload-artifact@v4.3.3 - if: github.event_name == 'push' + - name: Upload Android ${{ env.BUILD_TYPE }} Artifacts + uses: actions/upload-artifact@v4.3.6 + if: github.event_name == 'push' || github.event_name == 'schedule' with: name: androidArtifacts path: | - android/app/build/outputs/apk/google/debug/app-google-debug.apk - android/app/build/outputs/apk/huawei/debug/app-huawei-debug.apk - android/app/build/outputs/apk/google/release/app-google-release.apk - android/app/build/outputs/apk/huawei/release/app-huawei-release.apk + android/app/build/outputs/apk/google/${{ env.BUILD_TYPE_LOWERCASE }}/app-google-${{ env.BUILD_TYPE_LOWERCASE }}.apk + android/app/build/outputs/apk/huawei/${{ env.BUILD_TYPE_LOWERCASE }}/app-huawei-${{ env.BUILD_TYPE_LOWERCASE }}.apk - name: Cancel other jobs if this fails if: failure() @@ -105,7 +108,7 @@ jobs: DistributeAndroid: runs-on: ubuntu-22.04 needs: [ GradleBuild ] - if: github.event_name == 'push' + if: github.event_name == 'push' || github.event_name == 'schedule' outputs: status: ${{ steps.status.outputs.status }} steps: @@ -113,44 +116,28 @@ jobs: - name: Clone Repo # Needed for reading commit message for Firebase App Distribution uses: actions/checkout@v4.1.7 - - name: Download Android Artifacts - uses: actions/download-artifact@v4.1.7 + - name: Download Android ${{ env.BUILD_TYPE }} Artifacts + uses: actions/download-artifact@v4.1.8 with: name: androidArtifacts - - name: Firebase App Distribution Google Debug - uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 - with: - appId: ${{secrets.ANDROID_GOOGLE_DEBUG_FIREBASE_APP_ID}} - token: ${{secrets.FIREBASE_CLI_TOKEN}} - groups: QA - file: google/debug/app-google-debug.apk - - - name: Firebase App Distribution Google Release - uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 - with: - appId: ${{secrets.ANDROID_GOOGLE_RELEASE_FIREBASE_APP_ID}} - token: ${{secrets.FIREBASE_CLI_TOKEN}} - groups: QA - file: google/release/app-google-release.apk - - - name: Firebase App Distribution Huawei Debug + - name: Firebase App Distribution Google ${{ env.BUILD_TYPE }} uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 with: - appId: ${{secrets.ANDROID_HUAWEI_DEBUG_FIREBASE_APP_ID}} + appId: ${{ github.event_name == 'schedule' && secrets.ANDROID_GOOGLE_RELEASE_FIREBASE_APP_ID || secrets.ANDROID_GOOGLE_DEBUG_FIREBASE_APP_ID }} token: ${{secrets.FIREBASE_CLI_TOKEN}} groups: QA - file: huawei/debug/app-huawei-debug.apk + file: google/${{ env.BUILD_TYPE_LOWERCASE }}/app-google-${{ env.BUILD_TYPE_LOWERCASE }}.apk - - name: Firebase App Distribution Huawei Release + - name: Firebase App Distribution Huawei ${{ env.BUILD_TYPE }} uses: wzieba/Firebase-Distribution-Github-Action@v1.7.0 with: - appId: ${{secrets.ANDROID_HUAWEI_RELEASE_FIREBASE_APP_ID}} + appId: ${{ github.event_name == 'schedule' && secrets.ANDROID_HUAWEI_RELEASE_FIREBASE_APP_ID || secrets.ANDROID_HUAWEI_DEBUG_FIREBASE_APP_ID }} token: ${{secrets.FIREBASE_CLI_TOKEN}} groups: QA - file: huawei/release/app-huawei-release.apk + file: huawei/${{ env.BUILD_TYPE_LOWERCASE }}/app-huawei-${{ env.BUILD_TYPE_LOWERCASE }}.apk - - name: Delete Android Artifacts + - name: Delete Android ${{ env.BUILD_TYPE }} Artifacts uses: geekyeggo/delete-artifact@v5.0.0 with: name: androidArtifacts @@ -166,7 +153,7 @@ jobs: steps: - name: Setup Gradle Repo - uses: Oztechan/Global/actions/setup-gradle-repo@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e - name: Adding secret files uses: ./.github/actions/add-secret-files @@ -181,13 +168,20 @@ jobs: IOS_XCCONFIG_ASC_DEBUG: ${{ secrets.IOS_XCCONFIG_ASC_DEBUG }} GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} - - name: Build + - name: Build ${{ env.BUILD_TYPE }} working-directory: ios - run: fastlane build + run: fastlane build${{ env.BUILD_TYPE }} - - name: Upload iOS Artifacts - uses: actions/upload-artifact@v4.3.3 + - name: Upload iOS Debug Artifacts + uses: actions/upload-artifact@v4.3.6 if: github.event_name == 'push' + with: + name: iOSArtifacts + path: ios/CCC_I.ipa + + - name: Upload iOS Artifacts + uses: actions/upload-artifact@v4.3.6 + if: github.event_name == 'schedule' with: name: iOSArtifacts path: | @@ -205,27 +199,27 @@ jobs: DistributeIOS: runs-on: ubuntu-22.04 needs: [ XCodeBuild ] - if: github.event_name == 'push' + if: github.event_name == 'push' || github.event_name == 'schedule' outputs: status: ${{ steps.status.outputs.status }} steps: - name: Clone Repo uses: actions/checkout@v4.1.7 - - name: Download iOS IPA - uses: actions/download-artifact@v4.1.7 + - name: Download iOS ${{ env.BUILD_TYPE }} Artifacts + uses: actions/download-artifact@v4.1.8 with: name: iOSArtifacts path: ios # was necessary to use chown to fix permission issues in linux machines - - name: Distribute + - name: Distribute ${{ env.BUILD_TYPE }} working-directory: ios run: | sudo chown -R $(whoami) /var/lib/gems/3.0.0 - fastlane distribute + fastlane distribute${{ env.BUILD_TYPE }} - - name: Delete iOS IPA + - name: Delete iOS ${{ env.BUILD_TYPE }} Artifacts uses: geekyeggo/delete-artifact@v5.0.0 with: name: iOSArtifacts @@ -234,18 +228,17 @@ jobs: id: status run: echo "status=success" >> $GITHUB_OUTPUT - Check: + Test: runs-on: macos-14 - if: github.ref != 'refs/heads/develop' outputs: status: ${{ steps.status.outputs.status }} steps: - name: Setup Gradle Repo - uses: Oztechan/Global/actions/setup-gradle-repo@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e - - name: Run Check - run: ./gradlew check + - name: Test + run: ./gradlew test - name: Cancel other jobs if this fails if: failure() @@ -262,7 +255,7 @@ jobs: steps: - name: Setup Gradle Repo - uses: Oztechan/Global/actions/setup-gradle-repo@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e - name: Generate Coverage run: ./gradlew koverXmlReport @@ -305,10 +298,10 @@ jobs: steps: - name: Setup Gradle Repo - uses: Oztechan/Global/actions/setup-gradle-repo@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e - - name: Detekt - run: ./gradlew detektAll + - name: Detekt & Lint + run: ./gradlew detektAll lint - name: SwiftLint uses: norio-nomura/action-swiftlint@3.2.1 @@ -327,18 +320,18 @@ jobs: Notify: runs-on: ubuntu-22.04 - needs: [ GradleBuild, XCodeBuild, Check, Coverage, CodeAnalysis, DistributeAndroid, DistributeIOS ] + needs: [ GradleBuild, XCodeBuild, Test, Coverage, CodeAnalysis, DistributeAndroid, DistributeIOS ] if: always() steps: - name: Notify slack fail if: false == (needs.GradleBuild.outputs.status == 'success') || false == (needs.XCodeBuild.outputs.status == 'success') || - (false == (needs.Check.outputs.status == 'success') && github.ref != 'refs/heads/develop')|| + false == (needs.Test.outputs.status == 'success') || false == (needs.Coverage.outputs.status == 'success') || false == (needs.CodeAnalysis.outputs.status == 'success') || - (false == (needs.DistributeAndroid.outputs.status == 'success') && github.event_name == 'push') || - (false == (needs.DistributeIOS.outputs.status == 'success') && github.event_name == 'push') + (false == (needs.DistributeAndroid.outputs.status == 'success') && (github.event_name == 'push' || github.event_name == 'schedule')) || + (false == (needs.DistributeIOS.outputs.status == 'success') && (github.event_name == 'push' || github.event_name == 'schedule')) uses: voxmedia/github-action-slack-notify-build@v1.6.0 with: channel: ccc-github diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index da597ea75e..b4ac5b16aa 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@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/.github/workflows/reusable-project.yml@007659c3464bb29eeaaed0abfd2822af806dfe1e with: project_id: 2 secrets: inherit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92fea6009c..c49bf04150 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,7 +8,7 @@ on: jobs: PublishRelease: - uses: Oztechan/Global/.github/workflows/reusable-publish.yml@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/.github/workflows/reusable-publish.yml@007659c3464bb29eeaaed0abfd2822af806dfe1e with: slack_channel: "ccc-github" secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0451b1482d..d3c5be2331 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,8 @@ env: APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }} APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} - IOS_GOOGLE_FIREBASE_APP_ID: ${{ secrets.IOS_GOOGLE_FIREBASE_APP_ID }} + IOS_RELEASE_FIREBASE_APP_ID: ${{ secrets.IOS_RELEASE_FIREBASE_APP_ID }} + IOS_DEBUG_FIREBASE_APP_ID: ${{ secrets.IOS_DEBUG_FIREBASE_APP_ID }} FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} GIT_AUTHORIZATION: ${{ secrets.GIT_AUTHORIZATION }} SECRET_PASSWORD: ${{ secrets.SECRET_PASSWORD }} @@ -60,7 +61,7 @@ jobs: steps: - name: Setup Gradle Repo - uses: Oztechan/Global/actions/setup-gradle-repo@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e - name: Adding secret files uses: ./.github/actions/add-secret-files @@ -79,19 +80,19 @@ jobs: run: ./gradlew :android:app:bundleRelease :backend:app:jar --parallel - name: Upload Google App Bundle - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.6 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.3 + uses: actions/upload-artifact@v4.3.6 with: name: huaweiBundle path: android/app/build/outputs/bundle/huaweiRelease/app-huawei-release.aab - name: Upload Backend Jar - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.6 with: name: backendJar path: backend/app/build/libs/app-*.jar @@ -111,7 +112,7 @@ jobs: uses: actions/checkout@v4.1.7 - name: Download App Bundle - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4.1.8 with: name: googleBundle @@ -154,7 +155,7 @@ jobs: steps: - name: Download App Bundle - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4.1.8 with: name: huaweiBundle @@ -185,13 +186,13 @@ jobs: steps: - name: Download Backend Jar - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4.1.8 with: name: backendJar path: artifact - name: Deploy to Server - uses: easingthemes/ssh-deploy@v5.0.3 + uses: easingthemes/ssh-deploy@v5.1.1 env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} @@ -214,7 +215,7 @@ jobs: status: ${{ steps.status.outputs.status }} steps: - name: Setup Gradle Repo - uses: Oztechan/Global/actions/setup-gradle-repo@d5c2d633506a792e53d7a273d90dde429df3bba7 + uses: Oztechan/Global/actions/setup-gradle-repo@007659c3464bb29eeaaed0abfd2822af806dfe1e - name: Adding secret files uses: ./.github/actions/add-secret-files diff --git a/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt b/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt index 9728a5294c..aaf48d7387 100644 --- a/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt +++ b/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt @@ -10,30 +10,34 @@ import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.ConsumeResponseListener +import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.ProductDetailsResponseListener import com.android.billingclient.api.Purchase -import com.android.billingclient.api.PurchaseHistoryRecord -import com.android.billingclient.api.PurchaseHistoryResponseListener +import com.android.billingclient.api.PurchasesResponseListener import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.QueryProductDetailsParams -import com.android.billingclient.api.QueryPurchaseHistoryParams +import com.android.billingclient.api.QueryPurchasesParams import com.github.submob.scopemob.whether import com.oztechan.ccc.android.core.billing.mapper.toProductDetailsModel -import com.oztechan.ccc.android.core.billing.mapper.toPurchaseHistoryRecordModel +import com.oztechan.ccc.android.core.billing.mapper.toPurchaseModel import com.oztechan.ccc.android.core.billing.util.launchWithLifeCycle import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow // Billing will not work on debug builds // .debug suffix needs to be removed in app-level build.gradle and google-services.json +@Suppress("TooManyFunctions") internal class BillingManagerImpl(private val context: Context) : BillingManager, AcknowledgePurchaseResponseListener, PurchasesUpdatedListener, BillingClientStateListener, - PurchaseHistoryResponseListener, - ProductDetailsResponseListener { + PurchasesResponseListener, + ProductDetailsResponseListener, + ConsumeResponseListener { private lateinit var billingClient: BillingClient private lateinit var lifecycleOwner: LifecycleOwner @@ -62,7 +66,10 @@ internal class BillingManagerImpl(private val context: Context) : billingClient = BillingClient .newBuilder(context.applicationContext) .setListener(this) - .enablePendingPurchases() + .enablePendingPurchases( + PendingPurchasesParams.newBuilder().enableOneTimeProducts().enablePrepaidPlans() + .build() + ) .build() billingClient.startConnection(this) @@ -78,13 +85,9 @@ internal class BillingManagerImpl(private val context: Context) : productDetailList .firstOrNull { it.productId == skuId } ?.let { - val offerToken = - it.subscriptionOfferDetails?.get(productDetailList.indexOf(it))?.offerToken.orEmpty() - val productDetailsParamsList = listOf( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(it) - .setOfferToken(offerToken) .build() ) val billingFlowParams = @@ -103,6 +106,11 @@ internal class BillingManagerImpl(private val context: Context) : } } + override fun consumePurchase(token: String) { + Logger.v { "BillingManagerImpl consumePurchase" } + billingClient.consumeAsync(ConsumeParams.newBuilder().setPurchaseToken(token).build(), this) + } + override fun onAcknowledgePurchaseResponse(billingResult: BillingResult) { Logger.v { "BillingManagerImpl onAcknowledgePurchaseResponse ${billingResult.responseCode}" } if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { @@ -142,11 +150,11 @@ internal class BillingManagerImpl(private val context: Context) : override fun onBillingSetupFinished(billingResult: BillingResult) { Logger.v { "BillingManagerImpl onBillingSetupFinished ${billingResult.responseCode}" } - val queryPurchaseHistoryParams = QueryPurchaseHistoryParams.newBuilder() + val queryPurchasesParams = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) .build() - billingClient.queryPurchaseHistoryAsync(queryPurchaseHistoryParams, this) + billingClient.queryPurchasesAsync(queryPurchasesParams, this) billingClient.whether( { it.isReady }, @@ -190,18 +198,21 @@ internal class BillingManagerImpl(private val context: Context) : } } - override fun onPurchaseHistoryResponse( + override fun onQueryPurchasesResponse( billingResult: BillingResult, - purchaseHistoryList: MutableList? + purchasesResponse: MutableList ) { - Logger.v { "BillingManagerImpl onPurchaseHistoryResponse ${billingResult.responseCode}" } - - purchaseHistoryList - ?.map { it.toPurchaseHistoryRecordModel() } - ?.let { - lifecycleOwner.launchWithLifeCycle { - _effect.emit(BillingEffect.RestorePurchase(it)) + Logger.v { "BillingManagerImpl onQueryPurchasesResponse ${billingResult.responseCode}" } + lifecycleOwner.launchWithLifeCycle { + purchasesResponse + .map { it.toPurchaseModel() } + .let { + _effect.emit(BillingEffect.RestoreOrConsumePurchase(it)) } - } + } + } + + override fun onConsumeResponse(billingResult: BillingResult, token: String) { + Logger.v { "BillingManagerImpl onConsumeResponse ${billingResult.responseCode}, token:$token" } } } diff --git a/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/mapper/PurchaseHistoryRecordMapper.kt b/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/mapper/PurchaseHistoryRecordMapper.kt deleted file mode 100644 index 643e921c23..0000000000 --- a/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/mapper/PurchaseHistoryRecordMapper.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.oztechan.ccc.android.core.billing.mapper - -import com.android.billingclient.api.PurchaseHistoryRecord as PurchaseHistoryRecordIAPModel -import com.oztechan.ccc.android.core.billing.model.PurchaseHistoryRecord as toPurchaseHistoryRecordModel - -internal fun PurchaseHistoryRecordIAPModel.toPurchaseHistoryRecordModel() = toPurchaseHistoryRecordModel( - products, - purchaseTime -) diff --git a/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/mapper/PurchaseMapper.kt b/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/mapper/PurchaseMapper.kt new file mode 100644 index 0000000000..5775401a17 --- /dev/null +++ b/android/core/billing/src/google/kotlin/com/oztechan/ccc/android/core/billing/mapper/PurchaseMapper.kt @@ -0,0 +1,10 @@ +package com.oztechan.ccc.android.core.billing.mapper + +import com.android.billingclient.api.Purchase +import com.oztechan.ccc.android.core.billing.model.Purchase as PurchaseModel + +internal fun Purchase.toPurchaseModel() = PurchaseModel( + products, + purchaseTime, + purchaseToken +) diff --git a/android/core/billing/src/huawei/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt b/android/core/billing/src/huawei/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt index 669fa728ba..1282cc7582 100644 --- a/android/core/billing/src/huawei/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt +++ b/android/core/billing/src/huawei/kotlin/com/oztechan/ccc/android/core/billing/BillingManagerImpl.kt @@ -31,4 +31,8 @@ internal class BillingManagerImpl(private val context: Context) : BillingManager override fun acknowledgePurchase() { Logger.v { "BillingManagerImpl acknowledgePurchase" } } + + override fun consumePurchase(token: String) { + Logger.v { "BillingManagerImpl consumePurchase" } + } } diff --git a/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingEffect.kt b/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingEffect.kt index 47501c1b4f..e965e468f6 100644 --- a/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingEffect.kt +++ b/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingEffect.kt @@ -1,14 +1,14 @@ package com.oztechan.ccc.android.core.billing import com.oztechan.ccc.android.core.billing.model.ProductDetails -import com.oztechan.ccc.android.core.billing.model.PurchaseHistoryRecord +import com.oztechan.ccc.android.core.billing.model.Purchase sealed class BillingEffect { data object SuccessfulPurchase : BillingEffect() data object BillingUnavailable : BillingEffect() - data class RestorePurchase( - val purchaseHistoryRecordRecordList: List + data class RestoreOrConsumePurchase( + val purchaseList: List ) : BillingEffect() data class AddPurchaseMethods( diff --git a/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingManager.kt b/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingManager.kt index 368dbee52a..b01b797dc1 100644 --- a/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingManager.kt +++ b/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/BillingManager.kt @@ -17,4 +17,6 @@ interface BillingManager { fun launchBillingFlow(activity: Activity, skuId: String) fun acknowledgePurchase() + + fun consumePurchase(token: String) } diff --git a/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/model/Purchase.kt b/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/model/Purchase.kt new file mode 100644 index 0000000000..f45e0026b5 --- /dev/null +++ b/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/model/Purchase.kt @@ -0,0 +1,7 @@ +package com.oztechan.ccc.android.core.billing.model + +data class Purchase( + var products: List, + var purchaseTime: Long, + var purchaseToken: String +) diff --git a/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/model/PurchaseHistoryRecord.kt b/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/model/PurchaseHistoryRecord.kt deleted file mode 100644 index ad8334e562..0000000000 --- a/android/core/billing/src/main/kotlin/com/oztechan/ccc/android/core/billing/model/PurchaseHistoryRecord.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.oztechan.ccc.android.core.billing.model - -data class PurchaseHistoryRecord( - var ids: List, - var date: Long -) diff --git a/android/ui/mobile/android-ui-mobile.gradle.kts b/android/ui/mobile/android-ui-mobile.gradle.kts index 192bee483d..58fff12edd 100644 --- a/android/ui/mobile/android-ui-mobile.gradle.kts +++ b/android/ui/mobile/android-ui-mobile.gradle.kts @@ -5,7 +5,7 @@ plugins { libs.plugins.apply { alias(androidLibrary) alias(kotlinAndroid) - alias(safeArgs) // todo can be removed once compose migration done + alias(safeArgsKotlin) // todo can be removed once compose migration done alias(jetbrainsCompose) alias(kotlinPluginCompose) } @@ -68,7 +68,7 @@ dependencies { } android.google.apply { - DeviceFlavour.GOOGLE.implementation(playCore) + DeviceFlavour.GOOGLE.implementation(playCoreReview) } } diff --git a/android/ui/mobile/src/main/ic_launcher-playstore.png b/android/ui/mobile/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000..dd96003a26 Binary files /dev/null and b/android/ui/mobile/src/main/ic_launcher-playstore.png differ 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 f67eb5b55a..2ca511c744 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 @@ -40,10 +40,10 @@ class CalculatorFragment : BaseVBFragment() { private val analyticsManager: AnalyticsManager by inject() private val adManager: AdManager by inject() - private val calculatorViewModel: CalculatorViewModel by viewModel() + private val viewModel: CalculatorViewModel by viewModel() private val calculatorAdapter: CalculatorAdapter by lazy { - CalculatorAdapter(calculatorViewModel.event) + CalculatorAdapter(viewModel.event) } override fun getViewBinding() = FragmentCalculatorBinding.inflate(layoutInflater) @@ -81,7 +81,7 @@ class CalculatorFragment : BaseVBFragment() { R.id.calculatorFragment )?.observe(viewLifecycleOwner) { Logger.i { "CalculatorFragment observeNavigationResults $it" } - calculatorViewModel.event.onBaseChange(it) + viewModel.event.onBaseChange(it) } private fun FragmentCalculatorBinding.initViews() { @@ -92,17 +92,17 @@ class CalculatorFragment : BaseVBFragment() { } else { getString(R.string.banner_ad_unit_id_calculator_release) }, - shouldShowAd = calculatorViewModel.state.value.isBannerAdVisible + shouldShowAd = viewModel.state.value.isBannerAdVisible ) recyclerViewMain.adapter = calculatorAdapter } @SuppressLint("SetTextI18n") - private fun FragmentCalculatorBinding.observeStates() = calculatorViewModel.state + private fun FragmentCalculatorBinding.observeStates() = viewModel.state .flowWithLifecycle(lifecycle) .onEach { with(it) { - calculatorAdapter.submitList(currencyList.toValidList(calculatorViewModel.state.value.base)) + calculatorAdapter.submitList(currencyList.toValidList(viewModel.state.value.base)) txtInput.text = input with(layoutBar) { @@ -117,7 +117,7 @@ class CalculatorFragment : BaseVBFragment() { } }.launchIn(viewLifecycleOwner.lifecycleScope) - private fun observeEffects() = calculatorViewModel.effect + private fun observeEffects() = viewModel.effect .flowWithLifecycle(lifecycle) .onEach { viewEffect -> Logger.i { "CalculatorFragment observeEffects ${viewEffect::class.simpleName}" } @@ -150,7 +150,7 @@ class CalculatorFragment : BaseVBFragment() { text = R.string.text_paste_request, actionText = R.string.text_paste ) { - calculatorViewModel.onPasteToInput(it.context.getFromClipBoard()) + viewModel.onPasteToInput(it.context.getFromClipBoard()) } } @@ -162,7 +162,7 @@ class CalculatorFragment : BaseVBFragment() { } }.launchIn(viewLifecycleOwner.lifecycleScope) - private fun FragmentCalculatorBinding.setListeners() = with(calculatorViewModel.event) { + private fun FragmentCalculatorBinding.setListeners() = with(viewModel.event) { btnSettings.setOnClickListener { onSettingsClicked() } layoutBar.root.setOnClickListener { onBarClick() } @@ -203,7 +203,7 @@ class CalculatorFragment : BaseVBFragment() { } private fun Button.setKeyboardListener() = setOnClickListener { - calculatorViewModel.event.onKeyPress(text.toString()) + viewModel.event.onKeyPress(text.toString()) } companion object { 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 e7752f6bd7..66602b4fee 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 @@ -39,10 +39,10 @@ class CurrenciesFragment : BaseVBFragment() { private val analyticsManager: AnalyticsManager by inject() private val adManager: AdManager by inject() - private val currenciesViewModel: CurrenciesViewModel by viewModel() + private val viewModel: CurrenciesViewModel by viewModel() private val currenciesAdapter: CurrenciesAdapter by lazy { - CurrenciesAdapter(currenciesViewModel.event) + CurrenciesAdapter(viewModel.event) } override fun getViewBinding() = FragmentCurrenciesBinding.inflate(layoutInflater) @@ -71,7 +71,7 @@ class CurrenciesFragment : BaseVBFragment() { } else { getString(R.string.banner_ad_unit_id_currencies_release) }, - shouldShowAd = currenciesViewModel.state.value.isBannerAdVisible + shouldShowAd = viewModel.state.value.isBannerAdVisible ) setSpanByOrientation(resources.configuration.orientation) @@ -82,7 +82,7 @@ class CurrenciesFragment : BaseVBFragment() { } } - private fun FragmentCurrenciesBinding.observeStates() = currenciesViewModel.state + private fun FragmentCurrenciesBinding.observeStates() = viewModel.state .flowWithLifecycle(lifecycle) .onEach { with(it) { @@ -113,7 +113,7 @@ class CurrenciesFragment : BaseVBFragment() { } }.launchIn(viewLifecycleOwner.lifecycleScope) - private fun observeEffects() = currenciesViewModel.effect + private fun observeEffects() = viewModel.effect .flowWithLifecycle(lifecycle) .onEach { viewEffect -> Logger.i { "CurrenciesFragment observeEffects ${viewEffect::class.simpleName}" } @@ -140,7 +140,7 @@ class CurrenciesFragment : BaseVBFragment() { } }.launchIn(viewLifecycleOwner.lifecycleScope) - private fun FragmentCurrenciesBinding.setListeners() = with(currenciesViewModel.event) { + private fun FragmentCurrenciesBinding.setListeners() = with(viewModel.event) { btnDone.setOnClickListener { onDoneClick() } with(layoutCurrenciesToolbar) { @@ -152,7 +152,7 @@ class CurrenciesFragment : BaseVBFragment() { override fun onQueryTextSubmit(query: String) = false override fun onQueryTextChange(newText: String): Boolean { Logger.i { "CurrenciesFragment onQueryTextChange $newText" } - currenciesViewModel.event.onQueryChange(newText) + viewModel.event.onQueryChange(newText) return true } }) 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 9282e2c1a1..44068df877 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 @@ -33,7 +33,7 @@ class MainActivity : BaseActivity() { override var containerId: Int = R.id.content private val adManager: AdManager by inject() - private val mainViewModel: MainViewModel by viewModel() + private val viewModel: MainViewModel by viewModel() init { // use dark mode default for old devices @@ -52,7 +52,7 @@ class MainActivity : BaseActivity() { observeEffects() } - private fun observeStates() = mainViewModel.state + private fun observeStates() = viewModel.state .flowWithLifecycle(lifecycle) .onEach { with(it) { @@ -65,7 +65,7 @@ class MainActivity : BaseActivity() { } }.launchIn(lifecycleScope) - private fun observeEffects() = mainViewModel.effect + private fun observeEffects() = viewModel.effect .flowWithLifecycle(lifecycle) .onEach { viewEffect -> Logger.i { "MainActivity observeEffects ${viewEffect::class.simpleName}" } @@ -105,12 +105,12 @@ class MainActivity : BaseActivity() { override fun onResume() { super.onResume() Logger.i { "MainActivity onResume" } - mainViewModel.event.onResume() + viewModel.event.onResume() } override fun onPause() { Logger.i { "MainActivity onPause" } - mainViewModel.event.onPause() + viewModel.event.onPause() super.onPause() } diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/premium/PremiumBottomSheet.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/premium/PremiumBottomSheet.kt index 93118934d8..fd003dc6af 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/premium/PremiumBottomSheet.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/premium/PremiumBottomSheet.kt @@ -37,10 +37,10 @@ class PremiumBottomSheet : BaseVBBottomSheetDialogFragment Logger.i { "PremiumBottomSheet observeEffects ${viewEffect::class.simpleName}" } @@ -112,6 +112,7 @@ class PremiumBottomSheet : BaseVBBottomSheetDialogFragment billingManager.consumePurchase(viewEffect.token) } }.launchIn(viewLifecycleOwner.lifecycleScope) @@ -121,19 +122,19 @@ class PremiumBottomSheet : BaseVBBottomSheetDialogFragment restartActivity() - is BillingEffect.RestorePurchase -> premiumViewModel.event.onRestorePurchase( - viewEffect.purchaseHistoryRecordRecordList.toOldPurchaseList() + is BillingEffect.RestoreOrConsumePurchase -> viewModel.event.onRestoreOrConsumePurchase( + viewEffect.purchaseList.toOldPurchaseList() ) - is BillingEffect.AddPurchaseMethods -> premiumViewModel.event.onAddPurchaseMethods( + is BillingEffect.AddPurchaseMethods -> viewModel.event.onAddPurchaseMethods( viewEffect.productDetailsList.toPremiumDataList() ) - is BillingEffect.UpdatePremiumEndDate -> premiumViewModel.onPremiumActivated( + is BillingEffect.UpdatePremiumEndDate -> viewModel.onPremiumActivated( PremiumType.getById(viewEffect.id) ) - BillingEffect.BillingUnavailable -> premiumViewModel.event.onPremiumActivationFailed() + BillingEffect.BillingUnavailable -> viewModel.event.onPremiumActivationFailed() } }.launchIn(viewLifecycleOwner.lifecycleScope) @@ -147,10 +148,10 @@ class PremiumBottomSheet : BaseVBBottomSheetDialogFragment() { private val analyticsManager: AnalyticsManager by inject() - private val selectCurrencyViewModel: SelectCurrencyViewModel by viewModel() + private val viewModel: SelectCurrencyViewModel by viewModel() private val selectCurrencyAdapter: SelectCurrencyAdapter by lazy { - SelectCurrencyAdapter(selectCurrencyViewModel.event) + SelectCurrencyAdapter(viewModel.event) } override fun getViewBinding() = BottomSheetSelectCurrencyBinding.inflate(layoutInflater) @@ -60,7 +60,7 @@ class SelectCurrencyBottomSheet : recyclerViewSelectCurrency.adapter = selectCurrencyAdapter } - private fun BottomSheetSelectCurrencyBinding.observeStates() = selectCurrencyViewModel.state + private fun BottomSheetSelectCurrencyBinding.observeStates() = viewModel.state .flowWithLifecycle(lifecycle) .onEach { with(it) { @@ -81,7 +81,7 @@ class SelectCurrencyBottomSheet : } }.launchIn(viewLifecycleOwner.lifecycleScope) - private fun observeEffects() = selectCurrencyViewModel.effect + private fun observeEffects() = viewModel.effect .flowWithLifecycle(lifecycle) .onEach { viewEffect -> Logger.i { "SelectCurrencyBottomSheet observeEffects ${viewEffect::class.simpleName}" } @@ -103,6 +103,6 @@ class SelectCurrencyBottomSheet : }.launchIn(viewLifecycleOwner.lifecycleScope) private fun BottomSheetSelectCurrencyBinding.setListeners() = btnSelect.setOnClickListener { - selectCurrencyViewModel.event.onSelectClick() + viewModel.event.onSelectClick() } } diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/settings/SettingsFragment.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/settings/SettingsFragment.kt index de6aee79e3..4c59d85078 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/settings/SettingsFragment.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/content/settings/SettingsFragment.kt @@ -45,7 +45,7 @@ class SettingsFragment : BaseVBFragment() { private val analyticsManager: AnalyticsManager by inject() private val adManager: AdManager by inject() - private val settingsViewModel: SettingsViewModel by viewModel() + private val viewModel: SettingsViewModel by viewModel() override fun getViewBinding() = FragmentSettingsBinding.inflate(layoutInflater) @@ -73,7 +73,7 @@ class SettingsFragment : BaseVBFragment() { } else { getString(R.string.banner_ad_unit_id_settings_release) }, - shouldShowAd = settingsViewModel.state.value.isBannerAdVisible + shouldShowAd = viewModel.state.value.isBannerAdVisible ) with(itemCurrencies) { @@ -146,7 +146,7 @@ class SettingsFragment : BaseVBFragment() { } } - private fun FragmentSettingsBinding.observeStates() = settingsViewModel.state + private fun FragmentSettingsBinding.observeStates() = viewModel.state .flowWithLifecycle(lifecycle) .onEach { with(it) { @@ -182,7 +182,7 @@ class SettingsFragment : BaseVBFragment() { }.launchIn(viewLifecycleOwner.lifecycleScope) @Suppress("ComplexMethod") - private fun observeEffects() = settingsViewModel.effect + private fun observeEffects() = viewModel.effect .flowWithLifecycle(lifecycle) .onEach { viewEffect -> Logger.i { "SettingsFragment observeEffects ${viewEffect::class.simpleName}" } @@ -241,7 +241,7 @@ class SettingsFragment : BaseVBFragment() { } }.launchIn(viewLifecycleOwner.lifecycleScope) - private fun FragmentSettingsBinding.setListeners() = with(settingsViewModel.event) { + private fun FragmentSettingsBinding.setListeners() = with(viewModel.event) { backButton.setOnClickListener { onBackClick() } itemCurrencies.root.setOnClickListener { onCurrenciesClick() } @@ -267,9 +267,9 @@ class SettingsFragment : BaseVBFragment() { activity?.showSingleChoiceDialog( getString(R.string.title_dialog_choose_theme), AppTheme.values().map { it.themeName }.toTypedArray(), - settingsViewModel.state.value.appThemeType.ordinal + viewModel.state.value.appThemeType.ordinal ) { index -> - AppTheme.getThemeByOrdinal(index)?.let { settingsViewModel.event.onThemeChange(it) } + AppTheme.getThemeByOrdinal(index)?.let { viewModel.event.onThemeChange(it) } } } @@ -281,9 +281,9 @@ class SettingsFragment : BaseVBFragment() { it ) }.toTypedArray(), - settingsViewModel.state.value.precision.numberToIndex() + viewModel.state.value.precision.numberToIndex() ) { - settingsViewModel.event.onPrecisionSelect(it) + viewModel.event.onPrecisionSelect(it) } private fun share(marketLink: String) = Intent(Intent.ACTION_SEND).apply { diff --git a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtil.kt b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtil.kt index b5c81b8dff..3362f30ba9 100644 --- a/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtil.kt +++ b/android/ui/mobile/src/main/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtil.kt @@ -1,7 +1,7 @@ package com.oztechan.ccc.android.ui.mobile.util import com.oztechan.ccc.android.core.billing.model.ProductDetails -import com.oztechan.ccc.android.core.billing.model.PurchaseHistoryRecord +import com.oztechan.ccc.android.core.billing.model.Purchase import com.oztechan.ccc.client.viewmodel.premium.model.OldPurchase import com.oztechan.ccc.client.viewmodel.premium.model.PremiumData import com.oztechan.ccc.client.viewmodel.premium.model.PremiumType @@ -10,9 +10,9 @@ internal fun List.toPremiumDataList(): List = map { PremiumData(it.price, it.description, it.id) } -internal fun List.toOldPurchaseList(): List = - mapNotNull { purchaseHistoryRecord -> - PremiumType.getById(purchaseHistoryRecord.ids.firstOrNull())?.let { - OldPurchase(purchaseHistoryRecord.date, it) +internal fun List.toOldPurchaseList(): List = + mapNotNull { purchase -> + PremiumType.getById(purchase.products.firstOrNull())?.let { + OldPurchase(purchase.purchaseTime, it, purchase.purchaseToken) } } diff --git a/android/ui/mobile/src/main/res/drawable/ic_launcher_foreground.xml b/android/ui/mobile/src/main/res/drawable/ic_launcher_foreground.xml index b20f991c16..77f3e85a87 100644 --- a/android/ui/mobile/src/main/res/drawable/ic_launcher_foreground.xml +++ b/android/ui/mobile/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,68 +1,75 @@ - - - - + + android:pathData="M 299.7 414.5 C 251.2 403.5 218.5 358.2 223.8 309.9 C 226.5 286.2 236.4 266.3 253.8 249.8 C 290.8 214.7 347.6 214.5 385.6 249.3 C 397.6 260.3 407.8 276.7 412.5 292.5 C 416.1 304.6 417 325.5 414.5 338 C 406.9 376.1 377.8 406.1 340.7 414.1 C 329.9 416.4 308.9 416.6 299.7 414.5 M 272 384.1 C 270.8 381.8 272.8 379.6 275.3 380.5 C 277.2 381.2 277.4 383 276 384.8 C 274.5 386.5 273.1 386.2 272 384.1 M 346.3 373.4 C 346 372.6 346.3 371.3 347 370.4 C 348 369.2 351.4 369 365.5 369 C 375 369 383.5 369.2 384.4 369.6 C 385.2 369.9 386 371.2 386 372.6 C 386 375 386 375 366.5 375 C 350 375 346.9 374.7 346.3 373.4 M 254.4 366.5 C 253.1 363.3 255 363 274.6 363 C 293.6 363 294.1 363 293.5 365 C 293 366.8 291.9 367 274 367.5 C 257.9 367.9 254.9 367.7 254.4 366.5 M 346.3 359.3 C 345.9 358.3 346.3 357.1 347.5 356.3 C 350.3 354.2 384.6 354.8 385.9 356.9 C 388.2 360.6 386.6 361 366.5 361 C 349.3 361 346.9 360.8 346.3 359.3 M 272.5 350.3 C 271.6 349.1 271.9 346.1 273 345.4 C 274.6 344.4 277.3 347.5 276.6 349.4 C 275.9 351 273.4 351.5 272.5 350.3 M 272 294.8 C 271.4 294.1 271 290.1 271 285.8 C 271 278 271 278 262.7 277.7 C 255.2 277.5 254.5 277.3 254.5 275.5 C 254.5 273.6 255.2 273.4 262.7 273.2 C 271 272.9 271 272.9 271 265.6 C 271 257.7 272.4 254.4 275.2 255.5 C 276.5 256 276.9 257.7 277.2 264.3 C 277.5 272.5 277.5 272.5 285.4 272.7 C 293.3 273 295.3 273.9 294.3 276.6 C 293.9 277.6 291.4 278 285.4 278 C 277 278 277 278 276.5 286.5 C 276.1 292.8 275.6 295.1 274.5 295.5 C 273.6 295.7 272.5 295.4 272 294.8 M 352 289.8 C 350.3 287.8 350.7 287.1 356.5 281 C 362 275.2 362 275.2 356.4 269.6 C 351.5 264.8 350.9 263.8 351.9 262 C 352.5 260.9 353.4 260 353.9 260 C 354.4 260 357.4 262.5 360.5 265.6 C 366.1 271.2 366.1 271.2 371.8 265.6 C 375.9 261.5 378.1 260.1 379.3 260.5 C 382.1 261.6 381.1 264.3 376 269.5 C 373.2 272.2 371 274.8 371 275.2 C 371 275.7 373.4 278.6 376.4 281.6 C 381 286.3 381.7 287.5 380.9 289.1 C 380.3 290.1 379.4 291 378.8 291 C 378.3 291 375.1 288.4 371.8 285.3 C 365.9 279.6 365.9 279.6 359.9 285.3 C 356.6 288.4 353.7 291 353.5 291 C 353.2 291 352.5 290.4 352 289.8" /> - + + android:pathData="M 272 384.1 C 270.8 381.8 272.8 379.6 275.3 380.5 C 277.2 381.2 277.4 383 276 384.8 C 274.5 386.5 273.1 386.2 272 384.1" /> + android:pathData="M 346.3 373.4 C 346 372.6 346.3 371.3 347 370.4 C 348 369.2 351.4 369 365.5 369 C 375 369 383.5 369.2 384.4 369.6 C 385.2 369.9 386 371.2 386 372.6 C 386 375 386 375 366.5 375 C 350 375 346.9 374.7 346.3 373.4" /> + android:pathData="M 254.4 366.5 C 253.1 363.3 255 363 274.6 363 C 293.6 363 294.1 363 293.5 365 C 293 366.8 291.9 367 274 367.5 C 257.9 367.9 254.9 367.7 254.4 366.5" /> + android:pathData="M 167 360.4 C 139 359.9 139 359.9 139 355.1 C 139 350.5 139.1 350.2 141.9 349.6 C 143.6 349.3 146.1 348.2 147.6 347.3 C 150.8 345.2 154.9 336.7 154.9 332.2 C 155 329 155 329 147 329 C 139 329 139 329 139 323.5 C 139 318 139 318 146.9 318 C 154.9 318 154.9 318 152.4 312.7 C 147.5 302.1 150.2 289.8 158.4 285.3 C 167.8 280.1 179.4 280.2 191.2 285.8 C 197 288.5 197 288.5 194.5 292.6 C 192 296.8 192 296.8 184.7 294.2 C 176.2 291.3 168.8 291.5 164 294.9 C 160 297.8 159.4 301.5 162 308.6 C 163.2 311.8 164.4 315.2 164.6 316.2 C 165.1 317.6 166.7 317.9 175.3 318.2 C 185.5 318.5 185.5 318.5 185.8 323.7 C 186.1 329 186.1 329 176.1 329 C 166.2 329 166.2 329 165.6 335.1 C 165.2 338.7 163.9 343 162.5 345.6 C 160.1 350 160.1 350 178.5 350 C 197 350 197 350 197 355 C 197 357.7 196.5 360.2 196 360.5 C 195.4 360.7 182.4 360.7 167 360.4" /> + android:pathData="M 346.3 359.3 C 345.9 358.3 346.3 357.1 347.5 356.3 C 350.3 354.2 384.6 354.8 385.9 356.9 C 388.2 360.6 386.6 361 366.5 361 C 349.3 361 346.9 360.8 346.3 359.3" /> + android:pathData="M 272.5 350.3 C 271.6 349.1 271.9 346.1 273 345.4 C 274.6 344.4 277.3 347.5 276.6 349.4 C 275.9 351 273.4 351.5 272.5 350.3" /> + android:pathData="M 272 294.8 C 271.4 294.1 271 290.1 271 285.8 C 271 278 271 278 262.7 277.7 C 255.2 277.5 254.5 277.3 254.5 275.5 C 254.5 273.6 255.2 273.4 262.7 273.2 C 271 272.9 271 272.9 271 265.6 C 271 257.7 272.4 254.4 275.2 255.5 C 276.5 256 276.9 257.7 277.2 264.3 C 277.5 272.5 277.5 272.5 285.4 272.7 C 293.3 273 295.3 273.9 294.3 276.6 C 293.9 277.6 291.4 278 285.4 278 C 277 278 277 278 276.5 286.5 C 276.1 292.8 275.6 295.1 274.5 295.5 C 273.6 295.7 272.5 295.4 272 294.8" /> + android:pathData="M 352 289.8 C 350.3 287.8 350.7 287.1 356.5 281 C 362 275.2 362 275.2 356.4 269.6 C 351.5 264.8 350.9 263.8 351.9 262 C 352.5 260.9 353.4 260 353.9 260 C 354.4 260 357.4 262.5 360.5 265.6 C 366.1 271.2 366.1 271.2 371.8 265.6 C 375.9 261.5 378.1 260.1 379.3 260.5 C 382.1 261.6 381.1 264.3 376 269.5 C 373.2 272.2 371 274.8 371 275.2 C 371 275.7 373.4 278.6 376.4 281.6 C 381 286.3 381.7 287.5 380.9 289.1 C 380.3 290.1 379.4 291 378.8 291 C 378.3 291 375.1 288.4 371.8 285.3 C 365.9 279.6 365.9 279.6 359.9 285.3 C 356.6 288.4 353.7 291 353.5 291 C 353.2 291 352.5 290.4 352 289.8" /> + android:pathData="M 160 210.1 C 160 207.6 159.2 206.9 153.5 204 C 146.8 200.8 141.8 195.8 140 190.8 C 139.1 188.1 139.3 187.8 143.9 185.5 C 148.8 183 148.8 183 149.3 185.4 C 151.1 192.5 161.3 197.9 170.6 196.6 C 184.1 194.7 190.5 184 182.5 176.5 C 178.8 172.9 173.8 171.4 163.8 170.5 C 155.5 169.8 151.1 168 145.8 163.2 C 137.4 155.6 136.6 145.2 143.8 137.6 C 146.6 134.6 153.7 130.7 158.2 129.6 C 160.4 129 161 128.4 161 125.9 C 161 123 161 123 166.5 123 C 171.8 123 172 123 172 125.8 C 172 128.4 172.3 128.8 176.1 129.4 C 182.2 130.3 189.9 134.8 193.1 139.4 C 194.7 141.5 196 143.7 196 144.4 C 196 145 193.9 146.2 191.4 147.1 C 186.9 148.7 186.8 148.7 185.8 146.6 C 184.4 143.4 179.2 140.2 173.3 138.9 C 160.5 135.9 146.5 143.6 149.9 151.8 C 151.9 156.6 156.7 158.9 167.1 160 C 183 161.7 193.1 168.2 196 178.5 C 198.1 186 195.9 192.9 189.3 199.3 C 185.6 202.9 176.9 207 172.9 207 C 171.4 207 171 207.6 171 210 C 171 212.9 170.9 213 165.5 213 C 160.1 213 160 212.9 160 210.1" /> + android:pathData="M 319.6 205.6 C 310 202.8 300.9 194.8 296.1 185.2 C 293.9 181 293.9 181 287.4 181 C 280.8 181 280.8 181 281.1 176.2 C 281.5 171.5 281.5 171.5 286.8 171.1 C 291.9 170.9 292.1 170.7 291.8 168.1 C 291.5 165.6 291.1 165.4 286.1 165.1 C 280.8 164.8 280.8 164.8 281.1 160.1 C 281.5 155.5 281.5 155.5 287.6 155.2 C 293.8 154.9 293.8 154.9 296.4 149.5 C 303.4 135.1 321.5 126.2 338 129 C 344.9 130.1 354 134.7 358.7 139.3 C 361 141.5 361 141.5 357 145.4 C 353 149.3 353 149.3 350.2 146.8 C 338.6 135.9 319 137.7 309.2 150.3 C 308 151.9 307 153.6 307 154.1 C 307 154.6 310.6 155 315 155 C 323 155 323 155 323 159.9 C 323 164.9 323 164.9 313.2 165.2 C 303.5 165.5 303.5 165.5 303.1 168.2 C 302.8 171 302.8 171 312.9 171 C 323 171 323 171 323 176 C 323 181 323 181 315 181 C 309.5 181 307 181.3 307 182.1 C 307 182.8 309.1 185.4 311.8 188 C 317.4 193.3 323.8 196 331.5 196 C 338.1 196 342.2 194.5 348.3 190.2 C 353.7 186.5 352.6 186.2 359.7 192.7 C 361.2 194 360.1 195.4 353.8 200.2 C 345.8 206.4 330.2 208.8 319.6 205.6" /> - \ No newline at end of file + diff --git a/android/ui/mobile/src/main/res/drawable/ic_launcher_monochrome.xml b/android/ui/mobile/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000000..e50738ba3e --- /dev/null +++ b/android/ui/mobile/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android/ui/mobile/src/main/res/drawable/ic_splash.xml b/android/ui/mobile/src/main/res/drawable/ic_splash.xml index 427f5f7ee9..fbe3cb5f45 100755 --- a/android/ui/mobile/src/main/res/drawable/ic_splash.xml +++ b/android/ui/mobile/src/main/res/drawable/ic_splash.xml @@ -4,7 +4,7 @@ diff --git a/android/ui/mobile/src/main/res/layout/layout_slide_intro.xml b/android/ui/mobile/src/main/res/layout/layout_slide_intro.xml index 1bd10ff456..a791856d6a 100644 --- a/android/ui/mobile/src/main/res/layout/layout_slide_intro.xml +++ b/android/ui/mobile/src/main/res/layout/layout_slide_intro.xml @@ -15,7 +15,7 @@ android:layout_width="@dimen/height_width_app_logo" android:layout_height="@dimen/height_width_app_logo" android:layout_marginTop="@dimen/margin_top_slide_items" - android:src="@drawable/ic_launcher_foreground" + android:src="@drawable/ic_app_logo" tools:ignore="ContentDescription" /> + \ No newline at end of file diff --git a/android/ui/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/ui/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index ac94b34f54..f30783b210 100644 --- a/android/ui/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/android/ui/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher.png b/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index b38eb92d33..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000..65822ba756 Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 67b6a5ca72..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..47c8faebf7 Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher.png b/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index d6cb875646..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000..68c61d832a Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index d213769412..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..6317c12670 Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 6ff6c66b66..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000..1527c5f06c Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index e1edce8b72..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..e1fb08e47e Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index f62977ad32..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..3d9ab4131e Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 317bed966d..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..37383bdfcc Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index f4b0b2c108..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000..8ac6351604 Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 66b7dcba1e..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000..7c8bea60d3 Binary files /dev/null and b/android/ui/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/ui/mobile/src/main/res/mipmap/ic_launcher.png b/android/ui/mobile/src/main/res/mipmap/ic_launcher.png deleted file mode 100644 index f62977ad32..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap/ic_launcher.png and /dev/null differ diff --git a/android/ui/mobile/src/main/res/mipmap/ic_launcher_round.png b/android/ui/mobile/src/main/res/mipmap/ic_launcher_round.png deleted file mode 100644 index 317bed966d..0000000000 Binary files a/android/ui/mobile/src/main/res/mipmap/ic_launcher_round.png and /dev/null differ diff --git a/android/ui/mobile/src/test/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtilTest.kt b/android/ui/mobile/src/test/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtilTest.kt index 4726006ef4..82be65cb91 100644 --- a/android/ui/mobile/src/test/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtilTest.kt +++ b/android/ui/mobile/src/test/kotlin/com/oztechan/ccc/android/ui/mobile/util/PurchaseUtilTest.kt @@ -1,7 +1,7 @@ package com.oztechan.ccc.android.ui.mobile.util import com.oztechan.ccc.android.core.billing.model.ProductDetails -import com.oztechan.ccc.android.core.billing.model.PurchaseHistoryRecord +import com.oztechan.ccc.android.core.billing.model.Purchase import com.oztechan.ccc.client.viewmodel.premium.model.PremiumType import kotlin.test.Test import kotlin.test.assertEquals @@ -24,17 +24,18 @@ internal class PurchaseUtilTest { @Test fun `toOldPurchaseList maps correctly`() { - val purchaseHistoryRecordLists = listOf( - PurchaseHistoryRecord(listOf("1", "2"), 123L), - PurchaseHistoryRecord(listOf("9", "8"), 987L) + val purchaseLists = listOf( + Purchase(listOf("1", "2"), 123L, "token1"), + Purchase(listOf("9", "8"), 987L, "token2") ) - val oldPurchaseList = purchaseHistoryRecordLists.toOldPurchaseList() + val oldPurchaseList = purchaseLists.toOldPurchaseList() - purchaseHistoryRecordLists.zip(oldPurchaseList) { first, second -> - assertEquals(first.date, second.date) - val type = PremiumType.getById(first.ids.firstOrNull()) + purchaseLists.zip(oldPurchaseList) { first, second -> + assertEquals(first.purchaseTime, second.date) + val type = PremiumType.getById(first.products.firstOrNull()) assertEquals(type, second.type) + assertEquals(first.purchaseToken, second.purchaseToken) } } } 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 b655b763ca..995ef35d4c 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 @@ -7,17 +7,11 @@ import com.oztechan.ccc.client.core.shared.util.getRateFromCode import com.oztechan.ccc.client.core.shared.util.isNotPassed import com.oztechan.ccc.client.core.shared.util.nowAsDateString import com.oztechan.ccc.client.core.viewmodel.BaseEffect -import com.oztechan.ccc.client.core.viewmodel.BaseSEEDViewModel -import com.oztechan.ccc.client.core.viewmodel.util.launchIgnored +import com.oztechan.ccc.client.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource import com.oztechan.ccc.client.service.backend.BackendApiService import com.oztechan.ccc.client.storage.app.AppStorage import com.oztechan.ccc.client.storage.calculation.CalculationStorage -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class WidgetViewModel( @@ -25,28 +19,18 @@ class WidgetViewModel( private val backendApiService: BackendApiService, private val currencyDataSource: CurrencyDataSource, private val appStorage: AppStorage -) : BaseSEEDViewModel(), WidgetEvent { - - // region SEED - private val _state = MutableStateFlow( - WidgetState( - currentBase = calculationStorage.currentBase, - isPremium = appStorage.premiumEndDate.isNotPassed() - ) - ) - override val state = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as WidgetEvent - - override val data = WidgetData() - // endregion +) : SEEDViewModel( + initialState = WidgetState( + currentBase = calculationStorage.currentBase, + isPremium = appStorage.premiumEndDate.isNotPassed() + ), + initialData = WidgetData() +), + WidgetEvent { private fun refreshWidgetData() { - _state.update { - it.copy( + setState { + copy( currencyList = listOf(), lastUpdate = "", currentBase = calculationStorage.currentBase, @@ -54,7 +38,7 @@ class WidgetViewModel( ) } - if (_state.value.isPremium) { + if (state.value.isPremium) { getFreshWidgetData() } } @@ -72,8 +56,8 @@ class WidgetViewModel( } .take(MAXIMUM_NUMBER_OF_CURRENCY) .let { currencyList -> - _state.update { - it.copy( + setState { + copy( currencyList = currencyList, lastUpdate = nowAsDateString() ) @@ -81,46 +65,48 @@ class WidgetViewModel( } } - private suspend fun updateBase(isToNext: Boolean) { - val activeCurrencies = currencyDataSource.getActiveCurrencies() - - val newBaseIndex = activeCurrencies - .map { it.code } - .indexOf(calculationStorage.currentBase) - .let { - if (isToNext) { - it + 1 - } else { - it - 1 + private fun updateBase(isToNext: Boolean) { + viewModelScope.launch { + val activeCurrencies = currencyDataSource.getActiveCurrencies() + + val newBaseIndex = activeCurrencies + .map { it.code } + .indexOf(calculationStorage.currentBase) + .let { + if (isToNext) { + it + 1 + } else { + it - 1 + } + }.let { + (it + activeCurrencies.size) % activeCurrencies.size // it handles index -1 and index size +1 } - }.let { - (it + activeCurrencies.size) % activeCurrencies.size // it handles index -1 and index size +1 - } - calculationStorage.currentBase = activeCurrencies[newBaseIndex].code + calculationStorage.currentBase = activeCurrencies[newBaseIndex].code + + refreshWidgetData() + } } // region Event - override fun onPreviousClick() = viewModelScope.launchIgnored { + override fun onPreviousClick() { Logger.d { "WidgetViewModel onPreviousClick" } updateBase(false) - refreshWidgetData() } - override fun onNextClick() = viewModelScope.launchIgnored { + override fun onNextClick() { Logger.d { "WidgetViewModel onNextClick" } updateBase(true) - refreshWidgetData() } - override fun onRefreshClick() = viewModelScope.launchIgnored { + override fun onRefreshClick() { Logger.d { "WidgetViewModel onRefreshClick" } refreshWidgetData() } - override fun onOpenAppClick() = viewModelScope.launchIgnored { + override fun onOpenAppClick() { Logger.d { "WidgetViewModel onOpenAppClick" } - _effect.emit(WidgetEffect.OpenApp) + sendEffect { WidgetEffect.OpenApp } } // endregion } diff --git a/buildSrc/src/main/kotlin/ProjectSettings.kt b/buildSrc/src/main/kotlin/ProjectSettings.kt index d22fb2d6f7..c8e508613b 100644 --- a/buildSrc/src/main/kotlin/ProjectSettings.kt +++ b/buildSrc/src/main/kotlin/ProjectSettings.kt @@ -23,7 +23,7 @@ object ProjectSettings { const val MIN_SDK_VERSION = 21 const val TARGET_SDK_VERSION = 33 - val JAVA_VERSION = JavaVersion.VERSION_17 + val JAVA_VERSION = JavaVersion.VERSION_21 @Suppress("TooGenericExceptionCaught", "SwallowedException") fun getVersionCode(project: Project) = try { diff --git a/client/core/res/client-core-res.gradle.kts b/client/core/res/client-core-res.gradle.kts index 8f07949500..2ce5f5bfd8 100644 --- a/client/core/res/client-core-res.gradle.kts +++ b/client/core/res/client-core-res.gradle.kts @@ -1,5 +1,3 @@ -import io.gitlab.arturbosch.detekt.Detekt - plugins { libs.plugins.apply { alias(kotlinMultiplatform) @@ -43,19 +41,9 @@ android { targetCompatibility = JAVA_VERSION } } - - // Todo https://github.com/icerockdev/moko-resources/issues/510 - sourceSets { - getByName("main").java.srcDirs("build/generated/moko/androidMain/src") - } } multiplatformResources { resourcesPackage.set(Modules.Client.Core.res.packageName) resourcesClassName.set(Modules.Client.Core.res.frameworkName) } - -// todo https://github.com/icerockdev/moko-resources/issues/421 -tasks.withType { - dependsOn("generateMRcommonMain") -} diff --git a/client/core/res/src/commonMain/moko-resources/images/ic_app_logo@2x.png b/client/core/res/src/commonMain/moko-resources/images/ic_app_logo@2x.png old mode 100755 new mode 100644 index d0971428a7..b959454eda Binary files a/client/core/res/src/commonMain/moko-resources/images/ic_app_logo@2x.png and b/client/core/res/src/commonMain/moko-resources/images/ic_app_logo@2x.png differ diff --git a/client/core/res/src/commonMain/moko-resources/images/ic_dialog_and_snackbar@2x.png b/client/core/res/src/commonMain/moko-resources/images/ic_dialog_and_snackbar@2x.png old mode 100755 new mode 100644 index 8b29025aac..1c4581b7b1 Binary files a/client/core/res/src/commonMain/moko-resources/images/ic_dialog_and_snackbar@2x.png and b/client/core/res/src/commonMain/moko-resources/images/ic_dialog_and_snackbar@2x.png differ diff --git a/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/BaseSEEDViewModel.kt b/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/BaseSEEDViewModel.kt deleted file mode 100644 index 2fbb212366..0000000000 --- a/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/BaseSEEDViewModel.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2021 Mustafa Ozhan. All rights reserved. - */ - -package com.oztechan.ccc.client.core.viewmodel - -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow - -abstract class BaseSEEDViewModel< - State : BaseState, - Effect : BaseEffect, - Event : BaseEvent, - Data : BaseData - > : BaseViewModel() { - // region SEED - abstract val state: StateFlow? - abstract val effect: SharedFlow? - abstract val event: Event? - abstract val data: Data? - // endregion -} diff --git a/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/SEEDViewModel.kt b/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/SEEDViewModel.kt new file mode 100644 index 0000000000..50b7da7e72 --- /dev/null +++ b/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/SEEDViewModel.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 Mustafa Ozhan. All rights reserved. + */ + +package com.oztechan.ccc.client.core.viewmodel + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +abstract class SEEDViewModel< + State : BaseState, + Effect : BaseEffect, + Event : BaseEvent, + Data : BaseData + >( + initialState: State, + initialData: Data? = null +) : BaseViewModel() { + // region SEED + private val _state: MutableStateFlow = MutableStateFlow(initialState) + val state = _state.asStateFlow() + + private val _effect: MutableSharedFlow = MutableSharedFlow() + val effect = _effect.asSharedFlow() + + @Suppress("UNCHECKED_CAST") + val event: Event by lazy { this as Event } + + lateinit var data: Data + // endregion + + init { + if (initialData != null) { + this.data = initialData + } + } + + protected fun setState(newState: State.() -> State) { + _state.value = state.value.newState() + } + + protected suspend fun setEffect(newEffect: () -> Effect) { + _effect.emit(newEffect()) + } + + protected fun sendEffect(newEffect: () -> Effect) { + viewModelScope.launch { + _effect.emit(newEffect()) + } + } +} diff --git a/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/util/CoroutineUtil.kt b/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/util/CoroutineUtil.kt deleted file mode 100644 index ab04514cdd..0000000000 --- a/client/core/viewmodel/src/commonMain/kotlin/com/oztechan/ccc/client/core/viewmodel/util/CoroutineUtil.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.oztechan.ccc.client.core.viewmodel.util - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -fun CoroutineScope.launchIgnored(function: suspend () -> Unit) { - launch { - function() - } -} - -inline fun MutableStateFlow.update(function: T.() -> T) { - update { function(value) } -} 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 bc68094e14..9266a87327 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 @@ -16,9 +16,7 @@ import com.oztechan.ccc.client.core.shared.util.getFormatted import com.oztechan.ccc.client.core.shared.util.nowAsDateString import com.oztechan.ccc.client.core.shared.util.toStandardDigits import com.oztechan.ccc.client.core.shared.util.toSupportedCharacters -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.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository import com.oztechan.ccc.client.service.backend.BackendApiService @@ -34,10 +32,6 @@ 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.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -53,27 +47,19 @@ class CalculatorViewModel( private val conversionDataSource: ConversionDataSource, adControlRepository: AdControlRepository, private val analyticsManager: AnalyticsManager -) : BaseSEEDViewModel(), +) : SEEDViewModel( + initialState = CalculatorState(isBannerAdVisible = adControlRepository.shouldShowBannerAd()), + initialData = CalculatorData() +), CalculatorEvent { - // region SEED - private val _state = - MutableStateFlow(CalculatorState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) - override val state = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as CalculatorEvent - - override val data = CalculatorData() - // endregion init { currencyDataSource.getActiveCurrenciesFlow() .onStart { - _state.update { + val activeCurrencies = currencyDataSource.getActiveCurrencies() + setState { copy( - currencyList = currencyDataSource.getActiveCurrencies(), + currencyList = activeCurrencies, base = calculationStorage.currentBase, input = calculationStorage.lastInput, loading = true @@ -85,7 +71,7 @@ class CalculatorViewModel( } .onEach { Logger.d { "CalculatorViewModel currencyList changed: ${it.joinToString(",")}" } - _state.update { copy(currencyList = it) } + setState { copy(currencyList = it) } analyticsManager.setUserProperty(UserProperty.CurrencyCount(it.count().toString())) } @@ -110,7 +96,7 @@ class CalculatorViewModel( .launchIn(viewModelScope) private fun updateConversion() { - _state.update { copy(loading = true) } + setState { copy(loading = true) } data.conversion?.let { calculateConversions(it, ConversionState.Cached(it.date)) @@ -121,18 +107,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) = viewModelScope.launch { + conversion.copy(date = nowAsDateString()).let { + data.conversion = it + calculateConversions(it, ConversionState.Online(it.date)) + conversionDataSource.insertConversion(it) + } + } - private fun updateConversionFailed(t: Throwable) = viewModelScope.launchIgnored { + private fun updateConversionFailed(t: Throwable) = viewModelScope.launch { Logger.w(t) { "CalculatorViewModel updateConversionFailed" } conversionDataSource.getConversionByBase( calculationStorage.currentBase @@ -141,17 +124,17 @@ class CalculatorViewModel( } ?: run { Logger.w { "No offline conversion found in the DB" } - _effect.emit(CalculatorEffect.Error) + sendEffect { CalculatorEffect.Error } calculateConversions(null, ConversionState.Error) } } private fun calculateConversions(conversion: Conversion?, conversionState: ConversionState) = - _state.update { + setState { copy( - currencyList = _state.value.currencyList.onEach { - it.rate = conversion.calculateRate(it.code, _state.value.output) + currencyList = state.value.currencyList.onEach { + it.rate = conversion.calculateRate(it.code, state.value.output) .getFormatted(calculationStorage.precision) .toStandardDigits() }, @@ -160,27 +143,27 @@ class CalculatorViewModel( ) } - private fun calculateOutput(input: String) = viewModelScope.launch { + private fun calculateOutput(input: String) { val output = data.parser .calculate(input.toSupportedCharacters(), MAXIMUM_FLOATING_POINT) .mapTo { if (it.isFinite()) it.getFormatted(calculationStorage.precision) else "" } - _state.update { copy(output = output) } + setState { copy(output = output) } when { input.length > MAXIMUM_INPUT -> { - _effect.emit(CalculatorEffect.TooBigInput) - _state.update { copy(input = input.dropLast(1)) } + sendEffect { CalculatorEffect.TooBigInput } + setState { copy(input = input.dropLast(1)) } } output.length > MAXIMUM_OUTPUT -> { - _effect.emit(CalculatorEffect.TooBigOutput) - _state.update { copy(input = input.dropLast(1)) } + sendEffect { CalculatorEffect.TooBigOutput } + setState { copy(input = input.dropLast(1)) } } state.value.currencyList.size < MINIMUM_ACTIVE_CURRENCY -> { - _effect.emit(CalculatorEffect.FewCurrency) - _state.update { copy(loading = false) } + sendEffect { CalculatorEffect.FewCurrency } + setState { copy(loading = false) } } else -> updateConversion() @@ -188,14 +171,15 @@ class CalculatorViewModel( } private fun currentBaseChanged(newBase: String, shouldTrack: Boolean = false) = - viewModelScope.launchIgnored { + viewModelScope.launch { data.conversion = null calculationStorage.currentBase = newBase - _state.update { + val symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() + setState { copy( base = newBase, input = input, - symbol = currencyDataSource.getCurrencyByCode(newBase)?.symbol.orEmpty() + symbol = symbol ) } @@ -210,15 +194,15 @@ class CalculatorViewModel( Logger.d { "CalculatorViewModel onKeyPress $key" } when (key) { - KEY_AC -> _state.update { copy(input = "") } + KEY_AC -> setState { copy(input = "") } KEY_DEL -> state.value.input .whetherNot { it.isEmpty() } ?.apply { - _state.update { copy(input = substring(0, length - 1)) } + setState { copy(input = substring(0, length - 1)) } } - else -> _state.update { copy(input = state.value.input + key) } + else -> setState { copy(input = state.value.input + key) } } } @@ -233,7 +217,7 @@ class CalculatorViewModel( } } - _state.update { + setState { copy( base = code, input = newInput @@ -246,40 +230,32 @@ class CalculatorViewModel( analyticsManager.trackEvent(Event.ShowConversion(Param.Base(currency.code))) - viewModelScope.launch { - _effect.emit( - CalculatorEffect.ShowConversion( - currency.getConversionStringFromBase( - calculationStorage.currentBase, - data.conversion - ), - currency.code - ) + sendEffect { + CalculatorEffect.ShowConversion( + currency.getConversionStringFromBase( + calculationStorage.currentBase, + data.conversion + ), + currency.code ) } } override fun onItemAmountLongClick(amount: String) { Logger.d { "CalculatorViewModel onItemAmountLongClick $amount" } - analyticsManager.trackEvent(Event.CopyClipboard) - - viewModelScope.launch { - _effect.emit(CalculatorEffect.CopyToClipboard(amount)) - } + sendEffect { CalculatorEffect.CopyToClipboard(amount) } } - override fun onOutputLongClick() = viewModelScope.launchIgnored { + override fun onOutputLongClick() { Logger.d { "CalculatorViewModel onOutputLongClick" } - analyticsManager.trackEvent(Event.CopyClipboard) - - _effect.emit(CalculatorEffect.CopyToClipboard(state.value.output)) + sendEffect { CalculatorEffect.CopyToClipboard(state.value.output) } } - override fun onInputLongClick() = viewModelScope.launchIgnored { + override fun onInputLongClick() { Logger.d { "CalculatorViewModel onInputLongClick" } - _effect.emit(CalculatorEffect.ShowPasteRequest) + sendEffect { CalculatorEffect.ShowPasteRequest } } override fun onPasteToInput(text: String) { @@ -287,23 +263,23 @@ class CalculatorViewModel( analyticsManager.trackEvent(Event.PasteFromClipboard) - _state.update { copy(input = text.toSupportedCharacters()) } + setState { copy(input = text.toSupportedCharacters()) } } - override fun onBarClick() = viewModelScope.launchIgnored { + override fun onBarClick() { Logger.d { "CalculatorViewModel onBarClick" } - _effect.emit(CalculatorEffect.OpenBar) + sendEffect { CalculatorEffect.OpenBar } } - override fun onSettingsClicked() = viewModelScope.launchIgnored { + override fun onSettingsClicked() { Logger.d { "CalculatorViewModel onSettingsClicked" } - _effect.emit(CalculatorEffect.OpenSettings) + sendEffect { CalculatorEffect.OpenSettings } } override fun onBaseChange(base: String) { Logger.d { "CalculatorViewModel onBaseChange $base" } currentBaseChanged(base) - calculateOutput(_state.value.input) + 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 5e20b7042e..4b61b21134 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 @@ -202,6 +202,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onKeyPress("1") // trigger api call }.firstOrNull().let { + assertNotNull(it) assertIs(it) viewModel.state.value.let { state -> @@ -229,6 +230,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onKeyPress("1") // trigger api call }.firstOrNull().let { + assertNotNull(it) assertIs(it) viewModel.state.value.let { state -> @@ -248,6 +250,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onKeyPress(fortyFiveDigitNumber) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertFalse { viewModel.state.value.loading } assertEquals(fortyFiveDigitNumber.dropLast(1), viewModel.state.value.input) @@ -261,6 +264,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onKeyPress(nineteenDigitNumber) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertFalse { viewModel.state.value.loading } assertEquals(nineteenDigitNumber.dropLast(1), viewModel.state.value.input) @@ -309,6 +313,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onBarClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -318,6 +323,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onSettingsClicked() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -360,6 +366,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onItemImageLongClick(currency1) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals( currency1.getConversionStringFromBase( @@ -379,6 +386,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onItemAmountLongClick(currency1.rate) }.firstOrNull().let { + assertNotNull(it) assertEquals( CalculatorEffect.CopyToClipboard(currency1.rate), it @@ -395,6 +403,7 @@ internal class CalculatorViewModelTest { viewModel.event.onKeyPress(output) viewModel.event.onOutputLongClick() }.firstOrNull().let { + assertNotNull(it) assertEquals(CalculatorEffect.CopyToClipboard(output), it) verify { analyticsManager.trackEvent(Event.CopyClipboard) } @@ -406,6 +415,7 @@ internal class CalculatorViewModelTest { viewModel.effect.onSubscription { viewModel.event.onInputLongClick() }.firstOrNull().let { + assertNotNull(it) assertEquals(CalculatorEffect.ShowPasteRequest, it) } } 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 75a9eff767..ae3582a72d 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 @@ -13,20 +13,15 @@ import com.oztechan.ccc.client.core.analytics.model.Event import com.oztechan.ccc.client.core.analytics.model.Param import com.oztechan.ccc.client.core.analytics.model.UserProperty import com.oztechan.ccc.client.core.shared.constants.MINIMUM_ACTIVE_CURRENCY -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.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository import com.oztechan.ccc.client.storage.app.AppStorage 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 -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( @@ -35,24 +30,14 @@ class CurrenciesViewModel( private val currencyDataSource: CurrencyDataSource, adControlRepository: AdControlRepository, private val analyticsManager: AnalyticsManager -) : BaseSEEDViewModel(), +) : SEEDViewModel( + initialState = CurrenciesState( + isBannerAdVisible = adControlRepository.shouldShowBannerAd(), + isOnboardingVisible = appStorage.firstRun + ), + initialData = CurrenciesData() +), CurrenciesEvent { - // region SEED - private val _state = MutableStateFlow( - CurrenciesState( - isBannerAdVisible = adControlRepository.shouldShowBannerAd(), - isOnboardingVisible = appStorage.firstRun - ) - ) - override val state = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as CurrenciesEvent - - override val data = CurrenciesData() - // endregion init { currencyDataSource.getCurrenciesFlow() @@ -77,13 +62,13 @@ class CurrenciesViewModel( filterList("") } - private suspend fun verifyListSize() = data.unFilteredList + private fun verifyListSize() = data.unFilteredList .filter { it.isActive } .whether { it.size < MINIMUM_ACTIVE_CURRENCY } ?.whetherNot { appStorage.firstRun } - ?.run { _effect.emit(CurrenciesEffect.FewCurrency) } + ?.run { sendEffect { CurrenciesEffect.FewCurrency } } - private suspend fun verifyCurrentBase() = calculationStorage.currentBase.either( + private fun verifyCurrentBase() = calculationStorage.currentBase.either( { it.isEmpty() }, { base -> state.value.currencyList @@ -98,7 +83,7 @@ class CurrenciesViewModel( analyticsManager.trackEvent(Event.BaseChange(Param.Base(newBase))) analyticsManager.setUserProperty(UserProperty.BaseCurrency(newBase)) - _effect.emit(CurrenciesEffect.ChangeBase(newBase)) + sendEffect { CurrenciesEffect.ChangeBase(newBase) } } private fun filterList(txt: String) = data.unFilteredList @@ -108,47 +93,51 @@ class CurrenciesViewModel( symbol.contains(txt, true) }.toMutableList() .let { - _state.update { copy(currencyList = it, loading = false) } + setState { copy(currencyList = it, loading = false) } }.run { data.query = txt } // region Event - override fun updateAllCurrenciesState(state: Boolean) = viewModelScope.launchIgnored { + override fun updateAllCurrenciesState(state: Boolean) { Logger.d { "CurrenciesViewModel updateAllCurrenciesState $state" } - currencyDataSource.updateCurrencyStates(state) + viewModelScope.launch { + currencyDataSource.updateCurrencyStates(state) + } } - override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { + override fun onItemClick(currency: Currency) { Logger.d { "CurrenciesViewModel onItemClick ${currency.code}" } - currencyDataSource.updateCurrencyStateByCode(currency.code, !currency.isActive) + viewModelScope.launch { + currencyDataSource.updateCurrencyStateByCode(currency.code, !currency.isActive) + } } - override fun onDoneClick() = viewModelScope.launchIgnored { + override fun onDoneClick() { Logger.d { "CurrenciesViewModel onDoneClick" } data.unFilteredList .filter { it.isActive }.size .whether { it < MINIMUM_ACTIVE_CURRENCY } - ?.let { _effect.emit(CurrenciesEffect.FewCurrency) } + ?.let { sendEffect { CurrenciesEffect.FewCurrency } } ?: run { appStorage.firstRun = false - _state.update { copy(isOnboardingVisible = false) } + setState { copy(isOnboardingVisible = false) } filterList("") - _effect.emit(CurrenciesEffect.OpenCalculator) + sendEffect { CurrenciesEffect.OpenCalculator } } } - override fun onItemLongClick() = _state.value.selectionVisibility.let { + override fun onItemLongClick() = state.value.selectionVisibility.let { Logger.d { "CurrenciesViewModel onItemLongClick" } - _state.update { copy(selectionVisibility = !it) } + setState { copy(selectionVisibility = !it) } } - override fun onCloseClick() = viewModelScope.launchIgnored { + override fun onCloseClick() { Logger.d { "CurrenciesViewModel onCloseClick" } - if (_state.value.selectionVisibility) { - _state.update { copy(selectionVisibility = false) } + if (state.value.selectionVisibility) { + setState { copy(selectionVisibility = false) } } else { - _effect.emit(CurrenciesEffect.Back) + sendEffect { CurrenciesEffect.Back } }.run { filterList("") } 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 84f42b6260..4d57198231 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 @@ -160,6 +160,7 @@ internal class CurrenciesViewModelTest { ) viewModel.effect.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -179,6 +180,7 @@ internal class CurrenciesViewModelTest { ) viewModel.effect.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -201,6 +203,7 @@ internal class CurrenciesViewModelTest { ) viewModel.effect.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -232,6 +235,7 @@ internal class CurrenciesViewModelTest { .returns("") viewModel.effect.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(firstActiveBase, it.newBase) } @@ -256,6 +260,7 @@ internal class CurrenciesViewModelTest { .returns(currency1.code) // not active one viewModel.effect.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(currency2.code, it.newBase) } @@ -372,6 +377,7 @@ internal class CurrenciesViewModelTest { viewModel.effect.onSubscription { viewModel.onCloseClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals("", viewModel.data.query) } @@ -401,6 +407,7 @@ internal class CurrenciesViewModelTest { viewModel.effect.onSubscription { viewModel.onDoneClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertTrue { viewModel.data.query.isEmpty() } assertTrue { viewModel.state.value.isOnboardingVisible } 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..1445f10aef 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 @@ -10,17 +10,11 @@ import com.oztechan.ccc.client.core.analytics.AnalyticsManager 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.update +import com.oztechan.ccc.client.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository import com.oztechan.ccc.client.repository.appconfig.AppConfigRepository import com.oztechan.ccc.client.storage.app.AppStorage import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -31,23 +25,14 @@ class MainViewModel( private val adConfigService: AdConfigService, private val adControlRepository: AdControlRepository, analyticsManager: AnalyticsManager, -) : BaseSEEDViewModel(), MainEvent { - // region SEED - private val _state = MutableStateFlow( - MainState( - shouldOnboardUser = appStorage.firstRun, - appTheme = appStorage.appTheme - ) - ) - override val state: StateFlow = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as MainEvent - - override val data = MainData() - // endregion +) : SEEDViewModel( + initialState = MainState( + shouldOnboardUser = appStorage.firstRun, + appTheme = appStorage.appTheme + ), + initialData = MainData() +), + MainEvent { init { with(analyticsManager) { @@ -77,7 +62,7 @@ class MainViewModel( while (isActive && adControlRepository.shouldShowInterstitialAd()) { if (data.adVisibility) { - _effect.emit(MainEffect.ShowInterstitialAd) + setEffect { MainEffect.ShowInterstitialAd } } delay(adConfigService.config.interstitialAdPeriod) } @@ -93,15 +78,13 @@ class MainViewModel( private fun checkAppUpdate() { appConfigRepository.checkAppUpdate(data.isAppUpdateShown)?.let { isCancelable -> - viewModelScope.launch { - _effect.emit( - MainEffect.AppUpdateEffect( - isCancelable, - appConfigRepository.getMarketLink() - ) + sendEffect { + MainEffect.AppUpdateEffect( + isCancelable, + appConfigRepository.getMarketLink() ) - data.isAppUpdateShown = true } + data.isAppUpdateShown = true } } @@ -109,7 +92,7 @@ class MainViewModel( if (appConfigRepository.shouldShowAppReview()) { viewModelScope.launch { delay(reviewConfigService.config.appReviewDialogDelay) - _effect.emit(MainEffect.RequestReview) + setEffect { MainEffect.RequestReview } } } } @@ -124,7 +107,7 @@ class MainViewModel( override fun onResume() { Logger.d { "MainViewModel onResume" } - _state.update { + setState { copy( 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 43c221b000..e544ee2dbc 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 @@ -293,6 +293,7 @@ internal class MainViewModelTest { viewModel.effect.onSubscription { viewModel.onResume() }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(mockBoolean, it.isCancelable) assertTrue { viewModel.data.isAppUpdateShown } @@ -326,6 +327,7 @@ internal class MainViewModelTest { viewModel.effect.onSubscription { viewModel.onResume() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } diff --git a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumSEED.kt b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumSEED.kt index 0c9bfaadd8..acd07cc9a9 100644 --- a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumSEED.kt +++ b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/PremiumSEED.kt @@ -22,7 +22,7 @@ interface PremiumEvent : BaseEvent { isRestorePurchase: Boolean = false ) - fun onRestorePurchase(oldPurchaseList: List) + fun onRestoreOrConsumePurchase(oldPurchaseList: List) fun onAddPurchaseMethods(premiumDataList: List) fun onPremiumItemClick(type: PremiumType) fun onPremiumActivationFailed() @@ -38,4 +38,6 @@ sealed class PremiumEffect : BaseEffect { val premiumType: PremiumType, val isRestorePurchase: Boolean ) : PremiumEffect() + + data class ConsumePurchase(val token: String) : PremiumEffect() } 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 e39c128b4b..f1b8f96807 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 @@ -7,65 +7,62 @@ package com.oztechan.ccc.client.viewmodel.premium import co.touchlab.kermit.Logger import com.github.submob.scopemob.whether import com.oztechan.ccc.client.core.shared.util.isNotPassed +import com.oztechan.ccc.client.core.shared.util.isPassed import com.oztechan.ccc.client.core.viewmodel.BaseData -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.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.storage.app.AppStorage import com.oztechan.ccc.client.viewmodel.premium.model.OldPurchase import com.oztechan.ccc.client.viewmodel.premium.model.PremiumData import com.oztechan.ccc.client.viewmodel.premium.model.PremiumType import com.oztechan.ccc.client.viewmodel.premium.util.calculatePremiumEnd -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow class PremiumViewModel( private val appStorage: AppStorage -) : BaseSEEDViewModel(), PremiumEvent { - // region SEED - private val _state = MutableStateFlow(PremiumState()) - override val state = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as PremiumEvent - - override val data: BaseData? = null - // endregion +) : SEEDViewModel( + initialState = PremiumState() +), + PremiumEvent { // region Event override fun onPremiumActivated( adType: PremiumType?, startDate: Long, isRestorePurchase: Boolean - ) = viewModelScope.launchIgnored { + ) { Logger.d { "PremiumViewModel onPremiumActivated ${adType?.data?.duration.orEmpty()}" } adType?.let { appStorage.premiumEndDate = it.calculatePremiumEnd(startDate) - _effect.emit(PremiumEffect.PremiumActivated(it, isRestorePurchase)) + sendEffect { PremiumEffect.PremiumActivated(it, isRestorePurchase) } } } - override fun onRestorePurchase(oldPurchaseList: List) { + override fun onRestoreOrConsumePurchase(oldPurchaseList: List) { Logger.d { "PremiumViewModel onRestorePurchase" } - oldPurchaseList - .maxByOrNull { - it.type.calculatePremiumEnd(it.date) - }?.whether( - { it.type.calculatePremiumEnd(it.date).isNotPassed() }, - { it.date > appStorage.premiumEndDate }, - { PremiumType.getPurchaseIds().any { id -> id == it.type.data.id } } - )?.run { - onPremiumActivated( - adType = PremiumType.getById(type.data.id), - startDate = this.date, - isRestorePurchase = true - ) - _state.update { copy(loading = false) } - } + + // Consume old purchases + oldPurchaseList.filter { + it.type.calculatePremiumEnd(it.date).isPassed() + }.forEach { + sendEffect { PremiumEffect.ConsumePurchase(it.purchaseToken) } + } + + // Restore purchase if not already activated + oldPurchaseList.filter { + it.type.calculatePremiumEnd(it.date).isNotPassed() + }.maxByOrNull { + it.type.calculatePremiumEnd(it.date) + }?.whether( + { it.date > appStorage.premiumEndDate }, + { PremiumType.getPurchaseIds().any { id -> id == it.type.data.id } } + )?.let { + onPremiumActivated( + adType = PremiumType.getById(it.type.data.id), + startDate = it.date, + isRestorePurchase = true + ) + } + + setState { copy(loading = false) } } override fun onAddPurchaseMethods(premiumDataList: List) { @@ -80,23 +77,23 @@ class PremiumViewModel( tempList.add(it) } tempList.sortBy { it.ordinal } - _state.update { copy(premiumTypes = tempList) } + setState { copy(premiumTypes = tempList) } }.also { - _state.update { copy(loading = false) } // in case list is empty, loading will be false + setState { copy(loading = false) } // in case list is empty, loading will be false } } - override fun onPremiumItemClick(type: PremiumType) = viewModelScope.launchIgnored { + override fun onPremiumItemClick(type: PremiumType) { Logger.d { "PremiumViewModel onPremiumItemClick ${type.data.duration}" } - _state.update { + setState { copy(loading = type != PremiumType.VIDEO) } - _effect.emit(PremiumEffect.LaunchActivatePremiumFlow(type)) + sendEffect { PremiumEffect.LaunchActivatePremiumFlow(type) } } override fun onPremiumActivationFailed() { Logger.d { "PremiumViewModel onPremiumActivationFailed" } - _state.update { copy(loading = false) } + setState { copy(loading = false) } } // endregion } diff --git a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/model/OldPurchase.kt b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/model/OldPurchase.kt index 4faaf34958..5ac0a5b866 100644 --- a/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/model/OldPurchase.kt +++ b/client/viewmodel/premium/src/commonMain/kotlin/com/oztechan/ccc/client/viewmodel/premium/model/OldPurchase.kt @@ -2,5 +2,6 @@ package com.oztechan.ccc.client.viewmodel.premium.model data class OldPurchase( val date: Long, - val type: PremiumType + val type: PremiumType, + val purchaseToken: String ) 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 c4dd0fb5b4..e9914c12c5 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 @@ -27,10 +27,10 @@ import kotlinx.coroutines.test.setMain import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull -import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds @@ -64,7 +64,12 @@ internal class PremiumViewModelTest { // SEED @Test fun `init updates data correctly`() { - assertNull(viewModel.data) + assertFailsWith { + viewModel.data + }.message.let { + assertNotNull(it) + assertEquals("lateinit property data has not been initialized", it) + } } // Event @@ -78,6 +83,7 @@ internal class PremiumViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumActivated(premiumType, now) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(premiumType, it.premiumType) assertFalse { it.isRestorePurchase } @@ -88,46 +94,61 @@ internal class PremiumViewModelTest { } @Test - fun onRestorePurchase() = runTest { + fun onRestoreOrConsumePurchase() = runTest { every { appStorage.premiumEndDate } .returns(0) val now = nowAsLong() + // onRestoreOrConsumePurchase should activate the premium with the product which has farthest end date + // if there is a valid old purchase viewModel.effect.onSubscription { - viewModel.event.onRestorePurchase( + viewModel.event.onRestoreOrConsumePurchase( listOf( - OldPurchase(now, PremiumType.MONTH), - OldPurchase(now, PremiumType.YEAR) + OldPurchase(now, PremiumType.MONTH, ""), + OldPurchase(now, PremiumType.YEAR, "") ) ) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertTrue { it.isRestorePurchase } assertFalse { viewModel.state.value.loading } - verify { appStorage.premiumEndDate = it.premiumType.calculatePremiumEnd(now) } + verify { appStorage.premiumEndDate = PremiumType.YEAR.calculatePremiumEnd(now) } + verify(VerifyMode.not) { + appStorage.premiumEndDate = PremiumType.MONTH.calculatePremiumEnd(now) + } } - // onRestorePurchase shouldn't do anything if all the old purchases out of dated - var oldPurchase = OldPurchase(nowAsLong(), PremiumType.MONTH) + // onRestoreOrConsumePurchase shouldn't do anything + // if all the old purchases doesn't have later end date than the current premium end date even if they are valid + var oldPurchase = OldPurchase(nowAsLong(), PremiumType.MONTH, "") every { appStorage.premiumEndDate } .returns(nowAsLong() + 1.seconds.inWholeMilliseconds) - viewModel.event.onRestorePurchase(listOf(oldPurchase)) + viewModel.event.onRestoreOrConsumePurchase(listOf(oldPurchase)) verify(VerifyMode.not) { appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) } - // onRestorePurchase shouldn't do anything if the old purchase is already expired - oldPurchase = OldPurchase(nowAsLong() - (32.days.inWholeMilliseconds), PremiumType.MONTH) + // onRestoreOrConsumePurchase should consume product if the old purchase is already expired + val token = "token" + oldPurchase = + OldPurchase(nowAsLong() - (32.days.inWholeMilliseconds), PremiumType.MONTH, token) every { appStorage.premiumEndDate } .returns(0) - viewModel.event.onRestorePurchase(listOf(oldPurchase)) + viewModel.effect.onSubscription { + viewModel.event.onRestoreOrConsumePurchase(listOf(oldPurchase)) + }.firstOrNull().let { + assertNotNull(it) + assertIs(it) + assertTrue { it.token == token } + } verify(VerifyMode.not) { appStorage.premiumEndDate = oldPurchase.type.calculatePremiumEnd(oldPurchase.date) @@ -173,6 +194,7 @@ internal class PremiumViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumItemClick(PremiumType.VIDEO) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(PremiumType.VIDEO, it.premiumType) assertFalse { viewModel.state.value.loading } @@ -181,6 +203,7 @@ internal class PremiumViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumItemClick(PremiumType.MONTH) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(PremiumType.MONTH, it.premiumType) assertTrue { viewModel.state.value.loading } @@ -189,6 +212,7 @@ internal class PremiumViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumItemClick(PremiumType.QUARTER) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(PremiumType.QUARTER, it.premiumType) assertTrue { viewModel.state.value.loading } @@ -197,6 +221,7 @@ internal class PremiumViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumItemClick(PremiumType.HALF_YEAR) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(PremiumType.HALF_YEAR, it.premiumType) assertTrue { viewModel.state.value.loading } @@ -205,6 +230,7 @@ internal class PremiumViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumItemClick(PremiumType.YEAR) }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(PremiumType.YEAR, it.premiumType) assertTrue { viewModel.state.value.loading } 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..5a335bdfd2 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 @@ -6,37 +6,23 @@ package com.oztechan.ccc.client.viewmodel.selectcurrency import co.touchlab.kermit.Logger import com.oztechan.ccc.client.core.shared.constants.MINIMUM_ACTIVE_CURRENCY import com.oztechan.ccc.client.core.viewmodel.BaseData -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.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource import com.oztechan.ccc.common.core.model.Currency -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class SelectCurrencyViewModel( currencyDataSource: CurrencyDataSource -) : BaseSEEDViewModel(), SelectCurrencyEvent { - // region SEED - private val _state = MutableStateFlow(SelectCurrencyState()) - override val state = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as SelectCurrencyEvent - - override val data: BaseData? = null - // endregion +) : SEEDViewModel( + initialState = SelectCurrencyState() +), + SelectCurrencyEvent { init { currencyDataSource.getActiveCurrenciesFlow() .onEach { - _state.update { + setState { copy( currencyList = it, loading = false, @@ -47,14 +33,14 @@ class SelectCurrencyViewModel( } // region Event - override fun onItemClick(currency: Currency) = viewModelScope.launchIgnored { + override fun onItemClick(currency: Currency) { Logger.d { "SelectCurrencyViewModel onItemClick ${currency.code}" } - _effect.emit(SelectCurrencyEffect.CurrencyChange(currency.code)) + sendEffect { SelectCurrencyEffect.CurrencyChange(currency.code) } } - override fun onSelectClick() = viewModelScope.launchIgnored { + override fun onSelectClick() { Logger.d { "SelectCurrencyViewModel onSelectClick" } - _effect.emit(SelectCurrencyEffect.OpenCurrencies) + sendEffect { SelectCurrencyEffect.OpenCurrencies } } // endregion } 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 3ff8528c27..532f1946c7 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 @@ -20,16 +20,16 @@ import kotlinx.coroutines.test.setMain import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull -import kotlin.test.assertNull import kotlin.test.assertTrue import com.oztechan.ccc.common.core.model.Currency as CurrencyCommon internal class SelectCurrencyViewModelTest { - private val subject: SelectCurrencyViewModel by lazy { + private val viewModel: SelectCurrencyViewModel by lazy { SelectCurrencyViewModel(currencyDataSource) } @@ -55,7 +55,12 @@ internal class SelectCurrencyViewModelTest { // SEED @Test fun `init updates data correctly`() { - assertNull(subject.data) + assertFailsWith { + viewModel.data + }.message.let { + assertNotNull(it) + assertEquals("lateinit property data has not been initialized", it) + } } // init @@ -64,7 +69,7 @@ internal class SelectCurrencyViewModelTest { every { currencyDataSource.getActiveCurrenciesFlow() } .returns(flowOf(currencyListNotEnough)) - subject.state.firstOrNull().let { + viewModel.state.firstOrNull().let { assertNotNull(it) assertFalse { it.loading } assertFalse { it.enoughCurrency } @@ -77,7 +82,7 @@ internal class SelectCurrencyViewModelTest { @Test fun `init updates the states with enough currency`() { runTest { - subject.state.firstOrNull().let { + viewModel.state.firstOrNull().let { assertNotNull(it) assertFalse { it.loading } assertTrue { it.enoughCurrency } @@ -90,8 +95,8 @@ internal class SelectCurrencyViewModelTest { @Test fun onItemClick() = runTest { - subject.effect.onSubscription { - subject.event.onItemClick(currencyDollar) + viewModel.effect.onSubscription { + viewModel.event.onItemClick(currencyDollar) }.firstOrNull().let { assertNotNull(it) assertIs(it) @@ -101,9 +106,10 @@ internal class SelectCurrencyViewModelTest { @Test fun onSelectClick() = runTest { - subject.effect.onSubscription { - subject.event.onSelectClick() + viewModel.effect.onSubscription { + viewModel.event.onSelectClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } 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 3830157b17..3fa4c4b0da 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 @@ -9,9 +9,7 @@ import com.oztechan.ccc.client.core.analytics.model.Event import com.oztechan.ccc.client.core.shared.model.AppTheme import com.oztechan.ccc.client.core.shared.util.isPassed import com.oztechan.ccc.client.core.shared.util.toDateString -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.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource import com.oztechan.ccc.client.datasource.watcher.WatcherDataSource import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository @@ -24,12 +22,9 @@ import com.oztechan.ccc.client.viewmodel.settings.model.PremiumStatus import com.oztechan.ccc.client.viewmodel.settings.util.indexToNumber 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.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch @Suppress("TooManyFunctions", "LongParameterList") class SettingsViewModel( @@ -39,25 +34,17 @@ class SettingsViewModel( private val currencyDataSource: CurrencyDataSource, private val conversionDataSource: ConversionDataSource, watcherDataSource: WatcherDataSource, - adControlRepository: AdControlRepository, + private val adControlRepository: AdControlRepository, private val appConfigRepository: AppConfigRepository, private val analyticsManager: AnalyticsManager -) : BaseSEEDViewModel(), SettingsEvent { - // region SEED - private val _state = - MutableStateFlow(SettingsState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) - override val state = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as SettingsEvent - - override val data = SettingsData() - // endregion +) : SEEDViewModel( + initialState = SettingsState(isBannerAdVisible = adControlRepository.shouldShowBannerAd()), + initialData = SettingsData() +), + SettingsEvent { init { - _state.update { + setState { copy( appThemeType = AppTheme.getThemeByValueOrDefault(appStorage.appTheme), premiumStatus = appStorage.premiumEndDate.toPremiumStatus(), @@ -68,12 +55,12 @@ class SettingsViewModel( currencyDataSource.getActiveCurrenciesFlow() .onEach { - _state.update { copy(activeCurrencyCount = it.size) } + setState { copy(activeCurrencyCount = it.size) } }.launchIn(viewModelScope) watcherDataSource.getWatchersFlow() .onEach { - _state.update { copy(activeWatcherCount = it.size) } + setState { copy(activeWatcherCount = it.size) } }.launchIn(viewModelScope) } @@ -83,106 +70,110 @@ class SettingsViewModel( else -> PremiumStatus.Active(toDateString()) } - private suspend fun synchroniseConversions() { - _effect.emit(SettingsEffect.Synchronising) + private fun synchroniseConversions() { + viewModelScope.launch { + sendEffect { SettingsEffect.Synchronising } - currencyDataSource.getActiveCurrencies() - .forEach { (name) -> - runCatching { backendApiService.getConversion(name) } - .onFailure { error -> Logger.w(error) { error.message.toString() } } - .onSuccess { conversionDataSource.insertConversion(it) } + currencyDataSource.getActiveCurrencies() + .forEach { (name) -> + runCatching { backendApiService.getConversion(name) } + .onFailure { error -> Logger.w(error) { error.message.toString() } } + .onSuccess { conversionDataSource.insertConversion(it) } - delay(SYNC_DELAY) - } + delay(SYNC_DELAY) + } - _effect.emit(SettingsEffect.Synchronised) + sendEffect { SettingsEffect.Synchronised } - data.synced = true + data.synced = true + } } // region Event - override fun onBackClick() = viewModelScope.launchIgnored { + override fun onBackClick() { Logger.d { "SettingsViewModel onBackClick" } - _effect.emit(SettingsEffect.Back) + sendEffect { SettingsEffect.Back } } - override fun onCurrenciesClick() = viewModelScope.launchIgnored { + override fun onCurrenciesClick() { Logger.d { "SettingsViewModel onCurrenciesClick" } - _effect.emit(SettingsEffect.OpenCurrencies) + sendEffect { SettingsEffect.OpenCurrencies } } - override fun onWatchersClick() = viewModelScope.launchIgnored { + override fun onWatchersClick() { Logger.d { "SettingsViewModel onWatchersClick" } - _effect.emit(SettingsEffect.OpenWatchers) + sendEffect { SettingsEffect.OpenWatchers } } - override fun onFeedBackClick() = viewModelScope.launchIgnored { + override fun onFeedBackClick() { Logger.d { "SettingsViewModel onFeedBackClick" } - _effect.emit(SettingsEffect.FeedBack) + sendEffect { SettingsEffect.FeedBack } } - override fun onShareClick() = viewModelScope.launchIgnored { + override fun onShareClick() { Logger.d { "SettingsViewModel onShareClick" } - _effect.emit(SettingsEffect.Share(appConfigRepository.getMarketLink())) + sendEffect { SettingsEffect.Share(appConfigRepository.getMarketLink()) } } - override fun onSupportUsClick() = viewModelScope.launchIgnored { + override fun onSupportUsClick() { Logger.d { "SettingsViewModel onSupportUsClick" } - _effect.emit(SettingsEffect.SupportUs(appConfigRepository.getMarketLink())) + sendEffect { SettingsEffect.SupportUs(appConfigRepository.getMarketLink()) } } - override fun onPrivacySettingsClick() = viewModelScope.launchIgnored { + override fun onPrivacySettingsClick() { Logger.d { "SettingsViewModel onPrivacySettingsClick" } - _effect.emit(SettingsEffect.PrivacySettings) + sendEffect { SettingsEffect.PrivacySettings } } - override fun onOnGitHubClick() = viewModelScope.launchIgnored { + override fun onOnGitHubClick() { Logger.d { "SettingsViewModel onOnGitHubClick" } - _effect.emit(SettingsEffect.OnGitHub) + sendEffect { SettingsEffect.OnGitHub } } - override fun onPremiumClick() = viewModelScope.launchIgnored { + override fun onPremiumClick() { Logger.d { "SettingsViewModel onPremiumClick" } - if (appStorage.premiumEndDate.isPassed()) { - _effect.emit(SettingsEffect.Premium) - } else { - _effect.emit(SettingsEffect.AlreadyPremium) + sendEffect { + if (appStorage.premiumEndDate.isPassed()) { + SettingsEffect.Premium + } else { + SettingsEffect.AlreadyPremium + } } } - override fun onThemeClick() = viewModelScope.launchIgnored { + override fun onThemeClick() { Logger.d { "SettingsViewModel onThemeClick" } - _effect.emit(SettingsEffect.ThemeDialog) + sendEffect { SettingsEffect.ThemeDialog } } - override fun onSyncClick() = viewModelScope.launchIgnored { + override fun onSyncClick() { Logger.d { "SettingsViewModel onSyncClick" } analyticsManager.trackEvent(Event.OfflineSync) if (data.synced) { - _effect.emit(SettingsEffect.OnlyOneTimeSync) + sendEffect { SettingsEffect.OnlyOneTimeSync } } else { synchroniseConversions() } } - override fun onPrecisionClick() = viewModelScope.launchIgnored { + override fun onPrecisionClick() { Logger.d { "SettingsViewModel onPrecisionClick" } - _effect.emit(SettingsEffect.SelectPrecision) + sendEffect { SettingsEffect.SelectPrecision } } override fun onPrecisionSelect(index: Int) { Logger.d { "SettingsViewModel onPrecisionSelect $index" } calculationStorage.precision = index.indexToNumber() - _state.update { copy(precision = index.indexToNumber()) } + setState { copy(precision = index.indexToNumber()) } } - override fun onThemeChange(theme: AppTheme) = viewModelScope.launchIgnored { + override fun onThemeChange(theme: AppTheme) { Logger.d { "SettingsViewModel onThemeChange $theme" } - _state.update { copy(appThemeType = theme) } + setState { copy(appThemeType = theme) } appStorage.appTheme = theme.themeValue - _effect.emit(SettingsEffect.ChangeTheme(theme.themeValue)) + sendEffect { 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 8f255e7c45..228d6d7a9b 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 @@ -202,6 +202,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onSyncClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } @@ -223,6 +224,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onSyncClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } @@ -235,6 +237,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onBackClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -244,6 +247,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onCurrenciesClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -253,6 +257,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onWatchersClick() }.firstOrNull().let { + assertNotNull(it) assertEquals(SettingsEffect.OpenWatchers, it) } } @@ -262,6 +267,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onFeedBackClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -276,6 +282,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onShareClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(link, it.marketLink) } @@ -291,6 +298,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onSupportUsClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) assertEquals(link, it.marketLink) } @@ -301,6 +309,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onOnGitHubClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -310,6 +319,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPrivacySettingsClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -319,6 +329,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } @@ -330,6 +341,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPremiumClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } @@ -341,6 +353,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onThemeClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -353,12 +366,14 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onSyncClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } viewModel.effect.onSubscription { viewModel.event.onSyncClick() }.firstOrNull().let { + assertNotNull(it) assertTrue { viewModel.data.synced } assertIs(it) } @@ -371,6 +386,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.event.onPrecisionClick() }.firstOrNull().let { + assertNotNull(it) assertIs(it) } } @@ -398,6 +414,7 @@ internal class SettingsViewModelTest { viewModel.effect.onSubscription { viewModel.onThemeChange(mockTheme) }.firstOrNull().let { + assertNotNull(it) assertEquals(mockTheme, viewModel.state.value.appThemeType) assertIs(it) assertEquals(mockTheme.themeValue, it.themeValue) 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..2815cb0d7f 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 @@ -5,19 +5,13 @@ import com.oztechan.ccc.client.core.analytics.AnalyticsManager import com.oztechan.ccc.client.core.analytics.model.UserProperty import com.oztechan.ccc.client.core.shared.util.toStandardDigits import com.oztechan.ccc.client.core.shared.util.toSupportedCharacters -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.core.viewmodel.SEEDViewModel import com.oztechan.ccc.client.datasource.currency.CurrencyDataSource import com.oztechan.ccc.client.datasource.watcher.WatcherDataSource import com.oztechan.ccc.client.repository.adcontrol.AdControlRepository import com.oztechan.ccc.client.viewmodel.watchers.WatchersData.Companion.MAXIMUM_INPUT import com.oztechan.ccc.client.viewmodel.watchers.WatchersData.Companion.MAXIMUM_NUMBER_OF_WATCHER import com.oztechan.ccc.common.core.model.Watcher -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -27,92 +21,94 @@ class WatchersViewModel( private val watcherDataSource: WatcherDataSource, adControlRepository: AdControlRepository, private val analyticsManager: AnalyticsManager -) : BaseSEEDViewModel(), WatchersEvent { - // region SEED - private val _state = - MutableStateFlow(WatchersState(isBannerAdVisible = adControlRepository.shouldShowBannerAd())) - override val state = _state.asStateFlow() - - private val _effect = MutableSharedFlow() - override val effect = _effect.asSharedFlow() - - override val event = this as WatchersEvent - - override val data = WatchersData() - // endregion +) : SEEDViewModel( + initialState = WatchersState(isBannerAdVisible = adControlRepository.shouldShowBannerAd()), + initialData = WatchersData() +), + WatchersEvent { init { watcherDataSource.getWatchersFlow() .onEach { - _state.update { copy(watcherList = it) } + setState { copy(watcherList = it) } analyticsManager.setUserProperty(UserProperty.WatcherCount(it.count().toString())) }.launchIn(viewModelScope) } // region Event - override fun onBackClick() = viewModelScope.launchIgnored { + override fun onBackClick() { Logger.d { "WatcherViewModel onBackClick" } - _effect.emit(WatchersEffect.Back) + sendEffect { WatchersEffect.Back } } - override fun onBaseClick(watcher: Watcher) = viewModelScope.launchIgnored { + override fun onBaseClick(watcher: Watcher) { Logger.d { "WatcherViewModel onBaseClick $watcher" } - _effect.emit(WatchersEffect.SelectBase(watcher)) + sendEffect { WatchersEffect.SelectBase(watcher) } } - override fun onTargetClick(watcher: Watcher) = viewModelScope.launchIgnored { + override fun onTargetClick(watcher: Watcher) { Logger.d { "WatcherViewModel onTargetClick $watcher" } - _effect.emit(WatchersEffect.SelectTarget(watcher)) + sendEffect { WatchersEffect.SelectTarget(watcher) } } - override fun onBaseChanged(watcher: Watcher, newBase: String) = viewModelScope.launchIgnored { + override fun onBaseChanged(watcher: Watcher, newBase: String) { Logger.d { "WatcherViewModel onBaseChanged $watcher $newBase" } - watcherDataSource.updateWatcherBaseById(newBase, watcher.id) + viewModelScope.launch { + watcherDataSource.updateWatcherBaseById(newBase, watcher.id) + } } - override fun onTargetChanged(watcher: Watcher, newTarget: String) = viewModelScope.launchIgnored { + override fun onTargetChanged(watcher: Watcher, newTarget: String) { Logger.d { "WatcherViewModel onTargetChanged $watcher $newTarget" } - watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) + viewModelScope.launch { + watcherDataSource.updateWatcherTargetById(newTarget, watcher.id) + } } - override fun onAddClick() = viewModelScope.launchIgnored { + override fun onAddClick() { Logger.d { "WatcherViewModel onAddClick" } - if (watcherDataSource.getWatchers().size >= MAXIMUM_NUMBER_OF_WATCHER) { - _effect.emit(WatchersEffect.MaximumNumberOfWatchers) - } else { - currencyDataSource.getActiveCurrencies().let { list -> - watcherDataSource.addWatcher( - base = list.firstOrNull()?.code.orEmpty(), - target = list.lastOrNull()?.code.orEmpty() - ) + viewModelScope.launch { + if (watcherDataSource.getWatchers().size >= MAXIMUM_NUMBER_OF_WATCHER) { + sendEffect { WatchersEffect.MaximumNumberOfWatchers } + } else { + currencyDataSource.getActiveCurrencies().let { list -> + watcherDataSource.addWatcher( + base = list.firstOrNull()?.code.orEmpty(), + target = list.lastOrNull()?.code.orEmpty() + ) + } } } } - override fun onDeleteClick(watcher: Watcher) = viewModelScope.launchIgnored { + override fun onDeleteClick(watcher: Watcher) { Logger.d { "WatcherViewModel onDeleteClick $watcher" } - watcherDataSource.deleteWatcher(watcher.id) + viewModelScope.launch { + watcherDataSource.deleteWatcher(watcher.id) + } } - override fun onRelationChange(watcher: Watcher, isGreater: Boolean) = viewModelScope.launchIgnored { + override fun onRelationChange(watcher: Watcher, isGreater: Boolean) { Logger.d { "WatcherViewModel onRelationChange $watcher $isGreater" } - watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) + viewModelScope.launch { + watcherDataSource.updateWatcherRelationById(isGreater, watcher.id) + } } override fun onRateChange(watcher: Watcher, rate: String): String { Logger.d { "WatcherViewModel onRateChange $watcher $rate" } return if (rate.length > MAXIMUM_INPUT) { - viewModelScope.launch { _effect.emit(WatchersEffect.TooBigInput) } + sendEffect { WatchersEffect.TooBigInput } rate.dropLast(1) } else { rate.toSupportedCharacters().toStandardDigits().toDoubleOrNull()?.let { viewModelScope.launch { watcherDataSource.updateWatcherRateById(it, watcher.id) } - } ?: viewModelScope.launch { - _effect.emit(WatchersEffect.InvalidInput) + } ?: sendEffect { + WatchersEffect.InvalidInput } rate } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe9f6374e2..d02d944de2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -kotlin = "2.0.0" -androidGradlePlugin = "8.5.0" +kotlin = "2.0.10" +androidGradlePlugin = "8.5.2" jetbrainsCompose = "1.6.11" glance = "1.1.0" -mokkery = "2.1.1" -activityCompose = "1.9.0" +mokkery = "2.2.0" +activityCompose = "1.9.1" navigationCompose = "2.7.0-alpha07" detekt = "1.23.6" androidDesugaring = "2.0.4" @@ -23,22 +23,22 @@ googleServices = "4.4.2" firebasePer = "21.0.1" firebasePerPlugin = "1.4.2" firebaseCrashlyticsPlugin = "3.0.2" -googleAds = "23.2.0" -googleUmp = "2.2.0" -huaweiAds = "3.4.72.301" +googleAds = "23.3.0" +googleUmp = "3.0.0" +huaweiAds = "3.4.73.301" huaweiOsm = "1.3.35" navigation = "2.7.7" -playCore = "1.10.3" +playCoreReview = "2.0.1" kotlinXDateTime = "0.6.0" coroutines = "1.8.1" -billing = "6.2.1" +billing = "7.0.0" leakCanary = "2.14" sqlDelight = "1.5.5" -lifecycle = "2.8.2" +lifecycle = "2.8.4" mokoResources = "0.24.1" buildKonfig = "0.15.1" splashScreen = "1.0.1" -kover = "0.8.2" +kover = "0.8.3" rootBeer = "0.1.0" anrWatchDog = "1.4.0" kermit = "2.0.4" @@ -90,7 +90,7 @@ android-anrWatchDog = { module = "com.github.anrwatchdog:anrwatchdog", version.r android-google-billing = { module = "com.android.billingclient:billing", version.ref = "billing" } android-google-googleAds = { module = "com.google.android.gms:play-services-ads", version.ref = "googleAds" } android-google-ump = { module = "com.google.android.ump:user-messaging-platform", version.ref = "googleUmp" } -android-google-playCore = { module = "com.google.android.play:core", version.ref = "playCore" } +android-google-playCoreReview = { module = "com.google.android.play:review", version.ref = "playCoreReview" } # ANDROID HUAWEI android-huawei-huaweiAds = { module = "com.huawei.hms:ads-prime", version.ref = "huaweiAds" } @@ -118,7 +118,7 @@ kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } androidApplication = { id = "com.android.application", version.ref = "androidGradlePlugin" } androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugin" } -safeArgs = { id = "androidx.navigation.safeargs", version.ref = "navigation" } +safeArgsKotlin = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" } buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildKonfig" } firebaseCrashlyticsPlugin = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" } googleServices = { id = "com.google.gms.google-services", version.ref = "googleServices" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f7a6eb33e..dedd5d1e69 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.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/ios/CCC.xcodeproj/project.pbxproj b/ios/CCC.xcodeproj/project.pbxproj index ecb0a4c853..d6dad9b858 100644 --- a/ios/CCC.xcodeproj/project.pbxproj +++ b/ios/CCC.xcodeproj/project.pbxproj @@ -739,7 +739,7 @@ "$(inherited)", "-lsqlite3", ); - PRODUCT_BUNDLE_IDENTIFIER = com.oztechan.ccc; + PRODUCT_BUNDLE_IDENTIFIER = com.oztechan.ccc.debug; PRODUCT_NAME = "$(TARGET_NAME)_I"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/100.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/100.png deleted file mode 100644 index 8fe7745305..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/100.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/1024.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/1024.png deleted file mode 100644 index 9c30db7696..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/1024.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/114.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/114.png deleted file mode 100644 index 480befe160..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/114.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/120.png deleted file mode 100644 index a09628844d..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/120.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/144.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/144.png deleted file mode 100644 index 8868b660ba..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/144.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/152.png deleted file mode 100644 index 22ace376ee..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/152.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/167.png deleted file mode 100644 index bf09043bdc..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/167.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/180.png deleted file mode 100644 index 37046b0203..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/180.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/20.png deleted file mode 100644 index da5da0c525..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/20.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/29.png deleted file mode 100644 index 86506571b5..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/29.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/40.png deleted file mode 100644 index 4ce75d795f..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/40.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/50.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/50.png deleted file mode 100644 index b094a839c2..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/50.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/57.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/57.png deleted file mode 100644 index 0e2f0f9c8d..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/57.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index 05e39fd1f3..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/58.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/60.png deleted file mode 100644 index 02519f1822..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/60.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/72.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/72.png deleted file mode 100644 index 566ba2ad93..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/72.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/76.png deleted file mode 100644 index b5325730b6..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/76.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index f1783c4f39..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/80.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/87.png deleted file mode 100644 index fa31931761..0000000000 Binary files a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/87.png and /dev/null differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000..a77fe82159 Binary files /dev/null and b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 03f35aa2fc..7140563583 100644 --- a/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/CCC/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,153 +1,9 @@ { "images": [ { - "filename": "40.png", - "idiom": "iphone", - "scale": "2x", - "size": "20x20" - }, - { - "filename": "60.png", - "idiom": "iphone", - "scale": "3x", - "size": "20x20" - }, - { - "filename": "29.png", - "idiom": "iphone", - "scale": "1x", - "size": "29x29" - }, - { - "filename": "58.png", - "idiom": "iphone", - "scale": "2x", - "size": "29x29" - }, - { - "filename": "87.png", - "idiom": "iphone", - "scale": "3x", - "size": "29x29" - }, - { - "filename": "80.png", - "idiom": "iphone", - "scale": "2x", - "size": "40x40" - }, - { - "filename": "120.png", - "idiom": "iphone", - "scale": "3x", - "size": "40x40" - }, - { - "filename": "57.png", - "idiom": "iphone", - "scale": "1x", - "size": "57x57" - }, - { - "filename": "114.png", - "idiom": "iphone", - "scale": "2x", - "size": "57x57" - }, - { - "filename": "120.png", - "idiom": "iphone", - "scale": "2x", - "size": "60x60" - }, - { - "filename": "180.png", - "idiom": "iphone", - "scale": "3x", - "size": "60x60" - }, - { - "filename": "20.png", - "idiom": "ipad", - "scale": "1x", - "size": "20x20" - }, - { - "filename": "40.png", - "idiom": "ipad", - "scale": "2x", - "size": "20x20" - }, - { - "filename": "29.png", - "idiom": "ipad", - "scale": "1x", - "size": "29x29" - }, - { - "filename": "58.png", - "idiom": "ipad", - "scale": "2x", - "size": "29x29" - }, - { - "filename": "40.png", - "idiom": "ipad", - "scale": "1x", - "size": "40x40" - }, - { - "filename": "80.png", - "idiom": "ipad", - "scale": "2x", - "size": "40x40" - }, - { - "filename": "50.png", - "idiom": "ipad", - "scale": "1x", - "size": "50x50" - }, - { - "filename": "100.png", - "idiom": "ipad", - "scale": "2x", - "size": "50x50" - }, - { - "filename": "72.png", - "idiom": "ipad", - "scale": "1x", - "size": "72x72" - }, - { - "filename": "144.png", - "idiom": "ipad", - "scale": "2x", - "size": "72x72" - }, - { - "filename": "76.png", - "idiom": "ipad", - "scale": "1x", - "size": "76x76" - }, - { - "filename": "152.png", - "idiom": "ipad", - "scale": "2x", - "size": "76x76" - }, - { - "filename": "167.png", - "idiom": "ipad", - "scale": "2x", - "size": "83.5x83.5" - }, - { - "filename": "1024.png", - "idiom": "ios-marketing", - "scale": "1x", + "filename": "AppIcon.png", + "idiom": "universal", + "platform": "ios", "size": "1024x1024" } ], diff --git a/ios/CCC/Resources/Assets.xcassets/launchImage.imageset/launchImage.png b/ios/CCC/Resources/Assets.xcassets/launchImage.imageset/launchImage.png index 2d3ac2753f..ee40782d97 100644 Binary files a/ios/CCC/Resources/Assets.xcassets/launchImage.imageset/launchImage.png and b/ios/CCC/Resources/Assets.xcassets/launchImage.imageset/launchImage.png differ diff --git a/ios/CCC/Util/ObservableSEEDViewModel.swift b/ios/CCC/Util/ObservableSEEDViewModel.swift index c2b43a6319..3156798ea9 100644 --- a/ios/CCC/Util/ObservableSEEDViewModel.swift +++ b/ios/CCC/Util/ObservableSEEDViewModel.swift @@ -14,7 +14,7 @@ final class ObservableSEEDViewModel< Effect: BaseEffect, Event: BaseEvent, Data: BaseData, - ViewModel: BaseSEEDViewModel + ViewModel: SEEDViewModel >: ObservableObject { let viewModel: ViewModel = koin.get() @@ -30,8 +30,8 @@ final class ObservableSEEDViewModel< logger.d(message: { "ObservableSEED \(ViewModel.description()) init" }) // swiftlint:disable:next force_cast - self.state = viewModel.state!.value as! State - self.event = viewModel.event! + self.state = viewModel.state.value as! State + self.event = viewModel.event } deinit { @@ -41,18 +41,15 @@ final class ObservableSEEDViewModel< func startObserving() { logger.d(message: { "ObservableSEED \(ViewModel.description()) startObserving" }) - if viewModel.state != nil { - stateClosable = CoroutineUtilKt.observeWithCloseable(viewModel.state!, onChange: { - // swiftlint:disable:next force_cast - self.state = $0 as! State - }) - } - if viewModel.effect != nil { - effectClosable = CoroutineUtilKt.observeWithCloseable(viewModel.effect!, onChange: { - // swiftlint:disable:next force_cast - self.effect.send($0 as! Effect) - }) - } + stateClosable = CoroutineUtilKt.observeWithCloseable(viewModel.state, onChange: { + // swiftlint:disable:next force_cast + self.state = $0 as! State + }) + + effectClosable = CoroutineUtilKt.observeWithCloseable(viewModel.effect, onChange: { + // swiftlint:disable:next force_cast + self.effect.send($0 as! Effect) + }) } func stopObserving() { diff --git a/ios/Gemfile b/ios/Gemfile index ce182ff6d4..62bdc35ff8 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,4 +1,4 @@ source "https://rubygems.org" -gem "fastlane", "2.221.1" +gem "fastlane", "2.222.0" gem "fastlane-plugin-firebase_app_distribution", "0.9.1" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 4c675f2cf9..4b1a23c0ba 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -10,20 +10,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.947.0) - aws-sdk-core (3.199.0) + aws-partitions (1.965.0) + aws-sdk-core (3.201.5) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.87.0) - aws-sdk-core (~> 3, >= 3.199.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.154.0) - aws-sdk-core (~> 3, >= 3.199.0) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.158.0) + aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,7 +38,7 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.110.0) + excon (0.111.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -60,7 +60,7 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) @@ -68,7 +68,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.221.1) + fastlane (2.222.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -133,7 +133,7 @@ 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.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -154,14 +154,14 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) json (2.7.2) jwt (2.8.2) base64 - mini_magick (4.13.1) + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) @@ -171,14 +171,14 @@ GEM optparse (0.5.0) os (1.1.4) plist (3.7.1) - public_suffix (6.0.0) + public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.9) + rexml (3.3.5) strscan rouge (2.0.7) ruby2_keywords (0.0.5) @@ -204,13 +204,13 @@ GEM uber (0.1.0) unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.2, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) @@ -222,7 +222,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - fastlane (= 2.221.1) + fastlane (= 2.222.0) fastlane-plugin-firebase_app_distribution (= 0.9.1) BUNDLED WITH diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile index 32f70b7c70..f3f00f8fb7 100644 --- a/ios/fastlane/Appfile +++ b/ios/fastlane/Appfile @@ -1,4 +1,4 @@ -app_identifier("com.oztechan.ccc") # The bundle identifier of your app +app_identifier("com.oztechan.ccc", "com.oztechan.ccc.debug") # The bundle identifier of your app apple_id("development.oztechan@gmail.com") # Your Apple email address itc_team_id("124231627") # App Store Connect Team ID diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index c9f86876aa..51fdba086b 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -42,15 +42,38 @@ def get_method_from_match_type(match_type) return method end +def get_identifier_from_configuration(configuration) + identifier = "" + + if configuration == "Release" + identifier = "com.oztechan.ccc" + else + identifier = "com.oztechan.ccc.debug" + end + + return identifier +end + platform :ios do desc "iOS Lanes" - lane :build do + lane :buildRelease do + install_certificate_and_profile(match_type: "adhoc") + add_test_devices + build_project( + match_type: "adhoc", + profile: "match AdHoc com.oztechan.ccc", + configuration: "Release" + ) + end + + lane :buildDebug do install_certificate_and_profile(match_type: "adhoc") add_test_devices build_project( match_type: "adhoc", - profile: "match AdHoc com.oztechan.ccc" + profile: "match AdHoc com.oztechan.ccc.debug", + configuration: "Debug" ) end @@ -59,15 +82,16 @@ platform :ios do add_test_devices build_project( match_type: "appstore", - profile: "match AppStore com.oztechan.ccc" + profile: "match AppStore com.oztechan.ccc", + configuration: "Release" ) pilot(skip_waiting_for_build_processing: true) upload_crashlytics_symbols end - lane :distribute do + lane :distributeRelease do firebase_app_distribution( - app: ENV["IOS_GOOGLE_FIREBASE_APP_ID"], + app: ENV["IOS_RELEASE_FIREBASE_APP_ID"], groups: "QA", release_notes: changelog_from_git_commits(commits_count: 1), firebase_cli_token: ENV["FIREBASE_CLI_TOKEN"], @@ -76,6 +100,16 @@ platform :ios do upload_crashlytics_symbols end + lane :distributeDebug do + firebase_app_distribution( + app: ENV["IOS_DEBUG_FIREBASE_APP_ID"], + groups: "QA", + release_notes: changelog_from_git_commits(commits_count: 1), + firebase_cli_token: ENV["FIREBASE_CLI_TOKEN"], + ipa_path: "../ios/CCC_I.ipa" + ) + end + # Sub lines lane :install_certificate_and_profile do |options| api_key = app_store_connect_api_key( @@ -90,6 +124,7 @@ platform :ios do ensure_temp_keychain(keychain_name, keychain_password) match( + app_identifier: ["com.oztechan.ccc", "com.oztechan.ccc.debug"], type: options[:match_type], readonly: is_ci, git_basic_authorization: Base64.strict_encode64(ENV["GIT_AUTHORIZATION"]), @@ -110,24 +145,22 @@ platform :ios do lane :build_project do |options| match_type = options[:match_type] method = get_method_from_match_type(match_type) + identifier = get_identifier_from_configuration(options[:configuration]) update_code_signing_settings(use_automatic_signing: false) update_project_provisioning( - profile: ENV["sigh_com.oztechan.ccc_" + match_type + "_profile-path"] + profile: ENV["sigh_" + identifier + "_" + match_type + "_profile-path"] ) xcodes(select_for_current_build_only: true) gym( workspace: "CCC.xcworkspace", scheme: "CCC", - configuration: "Release", + configuration: options[:configuration], xcargs: "-allowProvisioningUpdates CODE_SIGN_STYLE=Manual", codesigning_identity: "Apple Distribution: Mustafa Ozhan (Q5WB95G58X)", export_method: method, export_options: { - signingStyle: "manual", - provisioningProfiles: { - "com.oztechan.ccc": options[:profile] - } + signingStyle: "manual" }, ) end diff --git a/submodule/basemob b/submodule/basemob index 34eb65f466..0e49a1bc58 160000 --- a/submodule/basemob +++ b/submodule/basemob @@ -1 +1 @@ -Subproject commit 34eb65f466d9072bd78063954446961b59e4c194 +Subproject commit 0e49a1bc58e4fa711706a97e1efd9a4aaa81af89 diff --git a/submodule/logmob b/submodule/logmob index 9b30a8c6e4..744a46a3de 160000 --- a/submodule/logmob +++ b/submodule/logmob @@ -1 +1 @@ -Subproject commit 9b30a8c6e4bd35d2e90bc1b114c1f9867e5971c5 +Subproject commit 744a46a3deeced69b06ad763499fb5d58347bfe3 diff --git a/submodule/parsermob b/submodule/parsermob index 1ad0858866..afd024cccc 160000 --- a/submodule/parsermob +++ b/submodule/parsermob @@ -1 +1 @@ -Subproject commit 1ad0858866fc26dd37cf2174854533881ec777e9 +Subproject commit afd024ccccb75d7d8bd6b367791034b16f631e81 diff --git a/submodule/scopemob b/submodule/scopemob index cefe5c7d18..3591e8bd7a 160000 --- a/submodule/scopemob +++ b/submodule/scopemob @@ -1 +1 @@ -Subproject commit cefe5c7d180225f90fd2a1d1018a44c8159b55e2 +Subproject commit 3591e8bd7a2529dfe56763c387ff310364f48511