From ee7da17925a4dfe3fb51012a11b245493a2574b2 Mon Sep 17 00:00:00 2001 From: daichi-matsumoto Date: Fri, 15 Dec 2023 01:08:43 +0900 Subject: [PATCH] add plus subscription --- README-JA.md | 2 +- README.md | 2 +- .../android/fanbox/ui/PixiViewNavHost.kt | 2 + .../fanbox/core/billing/models/ProductId.kt | 1 + .../PurchasePlusSubscriptionUseCase.kt | 48 +++++++++++++++++++ .../core/billing/usecase/VerifyPlusUseCase.kt | 13 +++++ .../fanbox/core/ui/view/SimpleAlertDialog.kt | 5 ++ core/ui/src/main/res/values-ja/strings.xml | 10 ++-- core/ui/src/main/res/values/strings.xml | 8 +++- .../about/billing/BillingPlusDialog.kt | 17 +++++-- .../about/billing/BillingPlusViewModel.kt | 19 ++++---- feature/library/build.gradle.kts | 1 + .../fanbox/feature/library/LibraryNavHost.kt | 3 ++ .../feature/library/LibraryNavigation.kt | 3 ++ .../fanbox/feature/library/LibraryScreen.kt | 3 ++ .../library/home/LibraryHomeNavigation.kt | 3 ++ .../feature/library/home/LibraryHomeScreen.kt | 10 ++++ .../library/home/LibraryHomeViewModel.kt | 23 ++++++++- .../feature/setting/top/SettingTopScreen.kt | 13 ++++- 19 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/PurchasePlusSubscriptionUseCase.kt diff --git a/README-JA.md b/README-JA.md index 1e8d4511..6badcc90 100644 --- a/README-JA.md +++ b/README-JA.md @@ -124,7 +124,7 @@ graph LR 何か不具合を発見したり機能を改善したい場合、機能を新たに開発したい場合は、まず issue を書いてください。その上であなた自身を assign し、開発に取り組んでください。pull request はいつでも歓迎です :smile: -将来的に Pixiv API を用いて新規機能を開発する予定です。APIを使用する場合は `local.properties` に Client ID と Client Secret を追加してください。デフォルトでは空文字が入っています。詳細は `app/build.gradle.kts` を読んでください。 +このアプリは AdMob を用いて収益化しています。GitHub から手動でビルドする際は AdMob App ID を `local.properties` に記述する必要があります。デフォルトではダミーの ID が入っているため、起動時にクラッシュします。もしくは AdMob の当該コードを削除してからアプリをビルドしてください。その他、 `local.properties` には様々な ID が記述されています。詳細は `app/build.gradle.kts` または `PixiViewConfig` をご覧ください。 ## License diff --git a/README.md b/README.md index e670bb32..c1c774e0 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ This app uses Gradle's Convention Plugins to standardize the build logic, and al If you find a bug, want to improve a feature, or want to develop a new feature, please first write an issue. Then assign yourself and work on the development. Pull requests are always welcome :smile: -We plan to develop new features using the Pixiv API in the future. When using the API, add Client ID and Client Secret to `local.properties`. By default, it contains an empty string. Read `app/build.gradle.kts` for details. +This app is monetized using AdMob. When building manually from GitHub, you need to write the AdMob App ID in `local.properties`. By default it contains a dummy ID, which causes it to crash on startup. Alternatively, please delete the AdMob code and build the app. In addition, various IDs are described in `local.properties`. See `appbuild.gradle.kts` or `PixiViewConfig` for details. ## License diff --git a/app/src/main/java/caios/android/fanbox/ui/PixiViewNavHost.kt b/app/src/main/java/caios/android/fanbox/ui/PixiViewNavHost.kt index 0a7990b8..8e156540 100644 --- a/app/src/main/java/caios/android/fanbox/ui/PixiViewNavHost.kt +++ b/app/src/main/java/caios/android/fanbox/ui/PixiViewNavHost.kt @@ -167,6 +167,7 @@ private fun ExpandedNavHost( navigateToPostDetailFromSupported = { subNavController.navigateToPostDetail(it, PostDetailPagingType.Supported) }, navigateToCreatorPosts = { mainNavController.navigateToCreatorTop(it, isPosts = true) }, navigateToCreatorPlans = { mainNavController.navigateToCreatorTop(it) }, + navigateToCancelPlus = { mainNavController.navigateToSimpleAlertDialog(it) }, ) { applyNavGraph(mainNavController, subNavController) } @@ -212,6 +213,7 @@ private fun NavGraphBuilder.applyNavGraph( navigateToSettingTop = { subNavController.navigateToSettingTop() }, navigateToAbout = { subNavController.navigateToAbout() }, navigateToBillingPlus = { mainNavController.navigateToBillingPlus() }, + navigateToCancelPlus = { mainNavController.navigateToSimpleAlertDialog(it) }, ) postDetailScreen( diff --git a/core/billing/src/main/java/caios/android/fanbox/core/billing/models/ProductId.kt b/core/billing/src/main/java/caios/android/fanbox/core/billing/models/ProductId.kt index fb80f0f0..8ca99ffc 100644 --- a/core/billing/src/main/java/caios/android/fanbox/core/billing/models/ProductId.kt +++ b/core/billing/src/main/java/caios/android/fanbox/core/billing/models/ProductId.kt @@ -11,6 +11,7 @@ data class ProductId(val value: String) : Parcelable { object ProductItem { // premium mode val plus = ProductId("plus") + val plusSubscription = ProductId("plus_subscription") // donation val love = ProductId("donate_love") diff --git a/core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/PurchasePlusSubscriptionUseCase.kt b/core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/PurchasePlusSubscriptionUseCase.kt new file mode 100644 index 00000000..1a87aaf7 --- /dev/null +++ b/core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/PurchasePlusSubscriptionUseCase.kt @@ -0,0 +1,48 @@ +package caios.android.fanbox.core.billing.usecase + +import android.app.Activity +import caios.android.fanbox.core.billing.AcknowledgeResult +import caios.android.fanbox.core.billing.BillingClient +import caios.android.fanbox.core.billing.models.ProductDetails +import caios.android.fanbox.core.billing.models.ProductItem +import caios.android.fanbox.core.billing.models.ProductType +import caios.android.fanbox.core.billing.purchaseSingle +import com.android.billingclient.api.Purchase +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class PurchasePlusSubscriptionUseCase( + private val billingClient: BillingClient, + private val mainDispatcher: CoroutineDispatcher, +) { + @Inject + constructor(billingClient: BillingClient) : this( + billingClient = billingClient, + mainDispatcher = Dispatchers.Main, + ) + + suspend fun execute(activity: Activity): PurchaseConsumableResult { + val productDetails = billingClient.queryProductDetails(ProductItem.plusSubscription, ProductType.SUBS) + val purchaseResult = purchase(activity, productDetails) + + acknowledge(purchaseResult.purchase) + + return purchaseResult + } + + private suspend fun purchase( + activity: Activity, + productDetails: ProductDetails, + ): PurchaseConsumableResult = withContext(mainDispatcher) { + val command = purchaseSingle(productDetails, null) + val result = billingClient.launchBillingFlow(activity, command) + + PurchaseConsumableResult(command, productDetails, result.billingPurchase) + } + + private suspend fun acknowledge(purchase: Purchase): AcknowledgeResult { + return billingClient.acknowledgePurchase(purchase) + } +} diff --git a/core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/VerifyPlusUseCase.kt b/core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/VerifyPlusUseCase.kt index 68398e4b..655a7231 100644 --- a/core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/VerifyPlusUseCase.kt +++ b/core/billing/src/main/java/caios/android/fanbox/core/billing/usecase/VerifyPlusUseCase.kt @@ -10,6 +10,10 @@ class VerifyPlusUseCase @Inject constructor( private val billingClient: BillingClient, ) { suspend fun execute(): Purchase? { + return verifyInAppPurchase() ?: verifySubscriptionPurchase() + } + + private suspend fun verifyInAppPurchase(): Purchase? { billingClient.queryPurchaseHistory(ProductType.INAPP) val productDetails = billingClient.queryProductDetails(ProductItem.plus, ProductType.INAPP) @@ -17,4 +21,13 @@ class VerifyPlusUseCase @Inject constructor( return purchases.find { it.products.contains(productDetails.productId.toString()) } } + + private suspend fun verifySubscriptionPurchase(): Purchase? { + billingClient.queryPurchaseHistory(ProductType.SUBS) + + val productDetails = billingClient.queryProductDetails(ProductItem.plusSubscription, ProductType.SUBS) + val purchases = billingClient.queryPurchases(ProductType.SUBS) + + return purchases.find { it.products.contains(productDetails.productId.toString()) } + } } diff --git a/core/ui/src/main/java/caios/android/fanbox/core/ui/view/SimpleAlertDialog.kt b/core/ui/src/main/java/caios/android/fanbox/core/ui/view/SimpleAlertDialog.kt index 72e9ad13..ea0aa27c 100644 --- a/core/ui/src/main/java/caios/android/fanbox/core/ui/view/SimpleAlertDialog.kt +++ b/core/ui/src/main/java/caios/android/fanbox/core/ui/view/SimpleAlertDialog.kt @@ -50,6 +50,11 @@ enum class SimpleAlertContents( negativeTextRes = R.string.common_cancel, isCaution = true, ), + CancelPlus( + titleRes = R.string.billing_plus_cancel_title, + descriptionRes = R.string.billing_plus_cancel_message, + positiveTextRes = R.string.common_ok, + ), } const val SimpleAlertDialogContent = "simpleAlertDialogSongs" diff --git a/core/ui/src/main/res/values-ja/strings.xml b/core/ui/src/main/res/values-ja/strings.xml index e1a7d0a9..0c53d687 100644 --- a/core/ui/src/main/res/values-ja/strings.xml +++ b/core/ui/src/main/res/values-ja/strings.xml @@ -178,7 +178,7 @@ 表示制限コンテンツを無視する 支援者のみが閲覧できる投稿を無視します。 グリッドモード - 投稿一覧画面の閲覧方法を切り替えます。 + 投稿一覧画面の閲覧方法を表形式(一覧表示)に切り替えます。 情報 CAIOS ID アプリバージョン @@ -225,8 +225,8 @@ FANBOX Viewer+ - FANBOX Viewer+ は、コーヒー1杯程度の金額で全ての機能にアクセスできるようになる有料サービスです。 - %1$s で購入 + FANBOX Viewer+ は、コーヒー1杯の金額で全ての機能にアクセスできるようになる開発者への支援プランです。 + %1$s / 月で支援 購入を復元 購入を消費 この機能はFANBOX Viewer+でのみ使用することができます @@ -242,6 +242,8 @@ クリエイターの投稿を一括でダウンロードして、ほかの媒体でも楽しもう! アプリをロック アプリを起動する際にパスワードを要求し、セキュリティを強化! + 支援プラン外の投稿を非表示 + 支援していないプランでの投稿など、見れない投稿を全て非表示にして一覧性を高めよう!(設定でON/OFF可能) ホームウィジェット ホーム画面にウィジェットを追加し、ホームから投稿をチェックできる!(アップデートで提供予定) Material You @@ -252,6 +254,8 @@ 新機能が追加された際にいち早く体験! 最優先サポート いつでも最優先でサポート! + プランの解約 + FANBOX Viewer+ のプランが解約されたため、通常ユーザーに戻りました。\n\n意図していない場合は支払いに失敗している可能性があります。Playストアより、プランの状況をご確認ください。 Oops! Something went wrong diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 70bdfd71..58daa1ae 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -178,7 +178,7 @@ Hide restricted contents Hide content that can only be viewed by supporters. Grid Mode - Change the display mode of the post list. + Switch the viewing method of the post list screen to table format (list display). Information CAIOS ID App version @@ -226,7 +226,7 @@ FANBOX Viewer+ FANBOX Viewer+ is a paid service that allows you to access all features for the price of a cup of coffee. - Buy for %1$s + Buy for %1$s / month Restore purchase Consume Purchase This feature can only be used with FANBOX Viewer+ @@ -242,6 +242,8 @@ Download creators\' posts in bulk and enjoy them on other media! Lock app Require a password when starting the app to increase security! + Hide posts outside the support plan + Hide posts that cannot be seen outside of your support plan to improve visibility! Home widget Add a widget to your home screen and check posts from home! (Scheduled to be provided in an update) Material You @@ -252,6 +254,8 @@ Be the first to experience new features when they are added! Priority support Always give top priority support! + Plan canceled + Your FANBOX Viewer+ plan was canceled, so returned to being a regular user. \n\nIf this is not what you intended, the payment may have failed. Please check the plan status from the Play Store. Oops! Something went wrong diff --git a/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusDialog.kt b/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusDialog.kt index 33c5d58f..1d8cb488 100644 --- a/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusDialog.kt +++ b/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.Widgets import androidx.compose.material.icons.outlined.HelpOutline +import androidx.compose.material.icons.outlined.HideImage import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -47,7 +48,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import caios.android.fanbox.core.billing.models.ProductDetails import caios.android.fanbox.core.ui.AsyncLoadContents import caios.android.fanbox.core.ui.theme.bold import caios.android.fanbox.feature.about.R @@ -76,7 +76,7 @@ internal fun BillingPlusRoute( BillingPlusDialog( modifier = Modifier.fillMaxSize(), purchase = uiState.purchase, - productDetails = uiState.productDetails, + formattedPrice = uiState.formattedPrice, isDeveloperMode = uiState.isDeveloperMode, onClickPurchase = { scope.launch { @@ -104,7 +104,7 @@ internal fun BillingPlusRoute( @Composable private fun BillingPlusDialog( purchase: Purchase?, - productDetails: ProductDetails?, + formattedPrice: String?, isDeveloperMode: Boolean, onClickPurchase: () -> Unit, onClickVerify: () -> Unit, @@ -138,7 +138,7 @@ private fun BillingPlusDialog( .fillMaxWidth(), onClick = { onClickPurchase.invoke() }, ) { - Text(stringResource(R.string.billing_plus_purchase_button, productDetails?.rawProductDetails?.oneTimePurchaseOfferDetails?.formattedPrice ?: "¥300")) + Text(stringResource(R.string.billing_plus_purchase_button, formattedPrice ?: "¥300")) } OutlinedButton( @@ -191,6 +191,13 @@ private fun BillingPlusDialog( icon = Icons.Default.Lock, ) + PlusItem( + modifier = Modifier.fillMaxWidth(), + title = R.string.billing_plus_item_hide_restricted, + description = R.string.billing_plus_item_hide_restricted_description, + icon = Icons.Outlined.HideImage, + ) + PlusItem( modifier = Modifier.fillMaxWidth(), title = R.string.billing_plus_item_widget, @@ -309,7 +316,7 @@ private fun BillingPlusScreenPreview() { .background(MaterialTheme.colorScheme.surface) .fillMaxSize(), purchase = null, - productDetails = null, + formattedPrice = null, isDeveloperMode = true, onClickPurchase = {}, onClickVerify = {}, diff --git a/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusViewModel.kt b/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusViewModel.kt index c07559ba..14ce8ce7 100644 --- a/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusViewModel.kt +++ b/feature/about/src/main/java/caios/android/fanbox/feature/about/billing/BillingPlusViewModel.kt @@ -6,11 +6,10 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import caios.android.fanbox.core.billing.BillingClient -import caios.android.fanbox.core.billing.models.ProductDetails import caios.android.fanbox.core.billing.models.ProductItem import caios.android.fanbox.core.billing.models.ProductType import caios.android.fanbox.core.billing.usecase.ConsumePlusUseCase -import caios.android.fanbox.core.billing.usecase.PurchasePlusUseCase +import caios.android.fanbox.core.billing.usecase.PurchasePlusSubscriptionUseCase import caios.android.fanbox.core.billing.usecase.VerifyPlusUseCase import caios.android.fanbox.core.common.util.ToastUtil import caios.android.fanbox.core.model.ScreenState @@ -32,7 +31,7 @@ import javax.inject.Inject @HiltViewModel class BillingPlusViewModel( private val billingClient: BillingClient, - private val purchasePlusUseCase: PurchasePlusUseCase, + private val purchasePlusSubscriptionUseCase: PurchasePlusSubscriptionUseCase, private val consumePlusUseCase: ConsumePlusUseCase, private val verifyPlusUseCase: VerifyPlusUseCase, private val userDataRepository: UserDataRepository, @@ -47,11 +46,15 @@ class BillingPlusViewModel( viewModelScope.launch { _screenState.value = runCatching { val userData = userDataRepository.userData.firstOrNull() + val productDetail = billingClient.queryProductDetails(ProductItem.plusSubscription, ProductType.SUBS) + + val plusSubscription = productDetail.rawProductDetails.subscriptionOfferDetails?.firstOrNull() + val basePlanPricing = plusSubscription?.pricingPhases?.pricingPhaseList?.firstOrNull() BillingPlusUiState( isPlusMode = userData?.isPlusMode ?: false, isDeveloperMode = userData?.isDeveloperMode ?: false, - productDetails = billingClient.queryProductDetails(ProductItem.plus, ProductType.INAPP), + formattedPrice = basePlanPricing?.formattedPrice, purchase = runCatching { verifyPlusUseCase.execute() }.getOrNull(), ) }.fold( @@ -70,13 +73,13 @@ class BillingPlusViewModel( @Inject constructor( billingClient: BillingClient, - purchasePlusUseCase: PurchasePlusUseCase, + purchasePlusSubscriptionUseCase: PurchasePlusSubscriptionUseCase, consumePlusUseCase: ConsumePlusUseCase, verifyPlusUseCase: VerifyPlusUseCase, userDataRepository: UserDataRepository, ) : this( billingClient = billingClient, - purchasePlusUseCase = purchasePlusUseCase, + purchasePlusSubscriptionUseCase = purchasePlusSubscriptionUseCase, consumePlusUseCase = consumePlusUseCase, verifyPlusUseCase = verifyPlusUseCase, userDataRepository = userDataRepository, @@ -86,7 +89,7 @@ class BillingPlusViewModel( suspend fun purchase(activity: Activity): Boolean { return runCatching { withContext(ioDispatcher) { - purchasePlusUseCase.execute(activity) + purchasePlusSubscriptionUseCase.execute(activity) } }.fold( onSuccess = { @@ -150,6 +153,6 @@ class BillingPlusViewModel( data class BillingPlusUiState( val isPlusMode: Boolean = false, val isDeveloperMode: Boolean = false, - val productDetails: ProductDetails? = null, + val formattedPrice: String? = null, val purchase: Purchase? = null, ) diff --git a/feature/library/build.gradle.kts b/feature/library/build.gradle.kts index c4c08523..6f3830fa 100644 --- a/feature/library/build.gradle.kts +++ b/feature/library/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(project(":core:repository")) implementation(project(":core:datastore")) implementation(project(":core:ui")) + implementation(project(":core:billing")) implementation(libs.bundles.ui.implementation) implementation(libs.pinch.zoom.grid) diff --git a/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavHost.kt b/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavHost.kt index 5f48d11c..30b5bdfb 100644 --- a/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavHost.kt +++ b/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavHost.kt @@ -8,6 +8,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import caios.android.fanbox.core.model.fanbox.id.CreatorId import caios.android.fanbox.core.model.fanbox.id.PostId +import caios.android.fanbox.core.ui.view.SimpleAlertContents import caios.android.fanbox.feature.library.discovery.LibraryDiscoveryRoute import caios.android.fanbox.feature.library.discovery.libraryDiscoveryScreen import caios.android.fanbox.feature.library.home.LibraryHomeRoute @@ -25,6 +26,7 @@ fun LibraryNavHost( navigateToPostDetailFromSupported: (postId: PostId) -> Unit, navigateToCreatorPosts: (creatorId: CreatorId) -> Unit, navigateToCreatorPlans: (creatorId: CreatorId) -> Unit, + navigateToCancelPlus: (SimpleAlertContents) -> Unit, modifier: Modifier = Modifier, navController: NavHostController = rememberNavController(), startDestination: String = LibraryHomeRoute, @@ -41,6 +43,7 @@ fun LibraryNavHost( navigateToPostDetailFromSupported = navigateToPostDetailFromSupported, navigateToCreatorPosts = navigateToCreatorPosts, navigateToCreatorPlans = navigateToCreatorPlans, + navigateToCancelPlus = navigateToCancelPlus, ) libraryDiscoveryScreen( diff --git a/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavigation.kt b/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavigation.kt index b89bc03e..374f6939 100644 --- a/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavigation.kt +++ b/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryNavigation.kt @@ -8,6 +8,7 @@ import androidx.navigation.compose.composable import caios.android.fanbox.core.model.fanbox.id.CreatorId import caios.android.fanbox.core.model.fanbox.id.PostId import caios.android.fanbox.core.ui.animation.NavigateAnimation +import caios.android.fanbox.core.ui.view.SimpleAlertContents const val LibraryRoute = "library" @@ -28,6 +29,7 @@ fun NavGraphBuilder.libraryScreen( navigateToSettingTop: () -> Unit, navigateToAbout: () -> Unit, navigateToBillingPlus: () -> Unit, + navigateToCancelPlus: (SimpleAlertContents) -> Unit, ) { composable( route = LibraryRoute, @@ -70,6 +72,7 @@ fun NavGraphBuilder.libraryScreen( navigateToSettingTop = navigateToSettingTop, navigateToAbout = navigateToAbout, navigateToBillingPlus = navigateToBillingPlus, + navigateToCancelPlus = navigateToCancelPlus, ) } } diff --git a/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryScreen.kt b/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryScreen.kt index 7a55a7c4..54b90cf2 100644 --- a/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryScreen.kt +++ b/feature/library/src/main/java/caios/android/fanbox/feature/library/LibraryScreen.kt @@ -25,6 +25,7 @@ import caios.android.fanbox.core.model.fanbox.id.PostId import caios.android.fanbox.core.ui.AsyncLoadContents import caios.android.fanbox.core.ui.extensition.LocalNavigationType import caios.android.fanbox.core.ui.extensition.PixiViewNavigationType +import caios.android.fanbox.core.ui.view.SimpleAlertContents import caios.android.fanbox.feature.library.component.LibraryBottomBar import caios.android.fanbox.feature.library.component.LibraryDestination import caios.android.fanbox.feature.library.component.LibraryDrawer @@ -50,6 +51,7 @@ fun LibraryScreen( navigateToSettingTop: () -> Unit, navigateToAbout: () -> Unit, navigateToBillingPlus: () -> Unit, + navigateToCancelPlus: (SimpleAlertContents) -> Unit, modifier: Modifier = Modifier, viewModel: LibraryViewModel = hiltViewModel(), ) { @@ -109,6 +111,7 @@ fun LibraryScreen( navigateToPostDetailFromSupported = navigateToPostDetailFromSupported, navigateToCreatorPosts = navigateToCreatorPosts, navigateToCreatorPlans = navigateToCreatorPlans, + navigateToCancelPlus = navigateToCancelPlus, ) AnimatedVisibility(navigationType.type == PixiViewNavigationType.BottomNavigation) { diff --git a/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeNavigation.kt b/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeNavigation.kt index d43d9132..696c120d 100644 --- a/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeNavigation.kt +++ b/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeNavigation.kt @@ -9,6 +9,7 @@ import androidx.navigation.compose.composable import caios.android.fanbox.core.model.fanbox.id.CreatorId import caios.android.fanbox.core.model.fanbox.id.PostId import caios.android.fanbox.core.ui.animation.NavigateAnimation +import caios.android.fanbox.core.ui.view.SimpleAlertContents const val LibraryHomeRoute = "libraryHome" @@ -22,6 +23,7 @@ fun NavGraphBuilder.libraryHomeScreen( navigateToPostDetailFromSupported: (PostId) -> Unit, navigateToCreatorPosts: (CreatorId) -> Unit, navigateToCreatorPlans: (CreatorId) -> Unit, + navigateToCancelPlus: (SimpleAlertContents) -> Unit, ) { composable( route = LibraryHomeRoute, @@ -35,6 +37,7 @@ fun NavGraphBuilder.libraryHomeScreen( navigateToPostDetailFromSupported = navigateToPostDetailFromSupported, navigateToCreatorPosts = navigateToCreatorPosts, navigateToCreatorPlans = navigateToCreatorPlans, + navigateToCancelPlus = navigateToCancelPlus, ) } } diff --git a/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeScreen.kt b/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeScreen.kt index 8b52a440..f52f84b6 100644 --- a/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeScreen.kt +++ b/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -35,10 +36,12 @@ import caios.android.fanbox.core.model.fanbox.id.PostId import caios.android.fanbox.core.ui.LazyPagingItemsLoadContents import caios.android.fanbox.core.ui.extensition.LocalNavigationType import caios.android.fanbox.core.ui.extensition.PixiViewNavigationType +import caios.android.fanbox.core.ui.view.SimpleAlertContents import caios.android.fanbox.feature.library.R import caios.android.fanbox.feature.library.home.items.LibraryHomeIdleSection import caios.android.fanbox.feature.library.home.items.LibrarySupportedIdleSection import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -49,6 +52,7 @@ internal fun LibraryHomeScreen( navigateToPostDetailFromSupported: (PostId) -> Unit, navigateToCreatorPlans: (CreatorId) -> Unit, navigateToCreatorPosts: (CreatorId) -> Unit, + navigateToCancelPlus: (SimpleAlertContents) -> Unit, modifier: Modifier = Modifier, viewModel: LibraryHomeViewModel = hiltViewModel(), ) { @@ -67,6 +71,12 @@ internal fun LibraryHomeScreen( HomeTabs.Home, ) + LaunchedEffect(true) { + viewModel.cancelPlusTrigger.collectLatest { + navigateToCancelPlus.invoke(SimpleAlertContents.CancelPlus) + } + } + Scaffold( modifier = if (navigationType != PixiViewNavigationType.PermanentNavigationDrawer) modifier.nestedScroll(scrollBehavior.nestedScrollConnection) else modifier, topBar = { diff --git a/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeViewModel.kt b/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeViewModel.kt index 7546c176..8851dd38 100644 --- a/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeViewModel.kt +++ b/feature/library/src/main/java/caios/android/fanbox/feature/library/home/LibraryHomeViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData +import caios.android.fanbox.core.billing.usecase.VerifyPlusUseCase import caios.android.fanbox.core.common.PixiViewConfig import caios.android.fanbox.core.common.util.suspendRunCatching import caios.android.fanbox.core.model.UserData @@ -14,10 +15,13 @@ import caios.android.fanbox.core.repository.UserDataRepository import caios.android.fanbox.core.ui.ads.NativeAdsPreLoader import caios.android.fanbox.core.ui.extensition.emptyPaging import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,6 +29,7 @@ import javax.inject.Inject class LibraryHomeViewModel @Inject constructor( private val userDataRepository: UserDataRepository, private val fanboxRepository: FanboxRepository, + private val verifiedPlusUseCase: VerifyPlusUseCase, private val nativeAdsPreLoader: NativeAdsPreLoader, private val pixiViewConfig: PixiViewConfig, ) : ViewModel() { @@ -39,8 +44,12 @@ class LibraryHomeViewModel @Inject constructor( ), ) + private val _cancelPlusTrigger = Channel() + val uiState = _uiState.asStateFlow() + val cancelPlusTrigger = _cancelPlusTrigger.receiveAsFlow() + val adsPreLoader = nativeAdsPreLoader init { @@ -64,7 +73,19 @@ class LibraryHomeViewModel @Inject constructor( } } - adsPreLoader.preloadAd() + viewModelScope.launch { + val userData = userDataRepository.userData.first() + val plusPurchase = verifiedPlusUseCase.execute() + + if (userData.isPlusMode && plusPurchase == null) { + userDataRepository.setPlusMode(false) + _cancelPlusTrigger.send(Unit) + } + } + + viewModelScope.launch { + adsPreLoader.preloadAd() + } } fun postLike(postId: PostId) { diff --git a/feature/setting/src/main/java/caios/android/fanbox/feature/setting/top/SettingTopScreen.kt b/feature/setting/src/main/java/caios/android/fanbox/feature/setting/top/SettingTopScreen.kt index c029f067..90ff2d4a 100644 --- a/feature/setting/src/main/java/caios/android/fanbox/feature/setting/top/SettingTopScreen.kt +++ b/feature/setting/src/main/java/caios/android/fanbox/feature/setting/top/SettingTopScreen.kt @@ -81,8 +81,19 @@ internal fun SettingTopRoute( onClickFollowTabDefaultHome = viewModel::setFollowTabDefaultHome, onClickHideAdultContents = viewModel::setHideAdultContents, onClickOverrideAdultContents = viewModel::setOverrideAdultContents, - onClickHideRestricted = viewModel::setHideRestricted, onClickGridMode = viewModel::setGridMode, + onClickHideRestricted = { + if (it) { + if (uiState.userData.hasPrivilege) { + viewModel.setHideRestricted(true) + } else { + ToastUtil.show(context, R.string.billing_plus_toast_require_plus) + navigateToBillingPlus.invoke() + } + } else { + viewModel.setHideRestricted(false) + } + }, onClickAppLock = { if (it) { if (uiState.userData.hasPrivilege) {