Skip to content
This repository has been archived by the owner on Oct 6, 2024. It is now read-only.

Commit

Permalink
add plus subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
matsumo0922 committed Dec 14, 2023
1 parent e20d133 commit ee7da17
Show file tree
Hide file tree
Showing 19 changed files with 164 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README-JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/caios/android/fanbox/ui/PixiViewNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -212,6 +213,7 @@ private fun NavGraphBuilder.applyNavGraph(
navigateToSettingTop = { subNavController.navigateToSettingTop() },
navigateToAbout = { subNavController.navigateToAbout() },
navigateToBillingPlus = { mainNavController.navigateToBillingPlus() },
navigateToCancelPlus = { mainNavController.navigateToSimpleAlertDialog(it) },
)

postDetailScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,24 @@ 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)
val purchases = billingClient.queryPurchases(ProductType.INAPP)

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()) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions core/ui/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
<string name="setting_top_general_hide_restricted_contents">表示制限コンテンツを無視する</string>
<string name="setting_top_general_hide_restricted_contents_description">支援者のみが閲覧できる投稿を無視します。</string>
<string name="setting_top_general_grid_mode">グリッドモード</string>
<string name="setting_top_general_grid_mode_description">投稿一覧画面の閲覧方法を切り替えます。</string>
<string name="setting_top_general_grid_mode_description">投稿一覧画面の閲覧方法を表形式(一覧表示)に切り替えます。</string>
<string name="setting_top_information">情報</string>
<string name="setting_top_information_id">CAIOS ID</string>
<string name="setting_top_information_version">アプリバージョン</string>
Expand Down Expand Up @@ -225,8 +225,8 @@

<!-- Billing -->
<string name="billing_plus_title">FANBOX Viewer+</string>
<string name="billing_plus_description">FANBOX Viewer+ は、コーヒー1杯程度の金額で全ての機能にアクセスできるようになる有料サービスです。</string>
<string name="billing_plus_purchase_button">%1$s で購入</string>
<string name="billing_plus_description">FANBOX Viewer+ は、コーヒー1杯の金額で全ての機能にアクセスできるようになる開発者への支援プランです。</string>
<string name="billing_plus_purchase_button">%1$s / 月で支援</string>
<string name="billing_plus_verify_button">購入を復元</string>
<string name="billing_plus_consume_button">購入を消費</string>
<string name="billing_plus_toast_require_plus">この機能はFANBOX Viewer+でのみ使用することができます</string>
Expand All @@ -242,6 +242,8 @@
<string name="billing_plus_item_download_description">クリエイターの投稿を一括でダウンロードして、ほかの媒体でも楽しもう!</string>
<string name="billing_plus_item_lock">アプリをロック</string>
<string name="billing_plus_item_lock_description">アプリを起動する際にパスワードを要求し、セキュリティを強化!</string>
<string name="billing_plus_item_hide_restricted">支援プラン外の投稿を非表示</string>
<string name="billing_plus_item_hide_restricted_description">支援していないプランでの投稿など、見れない投稿を全て非表示にして一覧性を高めよう!(設定でON/OFF可能)</string>
<string name="billing_plus_item_widget">ホームウィジェット</string>
<string name="billing_plus_item_widget_description">ホーム画面にウィジェットを追加し、ホームから投稿をチェックできる!(アップデートで提供予定)</string>
<string name="billing_plus_item_material_you">Material You</string>
Expand All @@ -252,6 +254,8 @@
<string name="billing_plus_item_feature_description">新機能が追加された際にいち早く体験!</string>
<string name="billing_plus_item_support">最優先サポート</string>
<string name="billing_plus_item_support_description">いつでも最優先でサポート!</string>
<string name="billing_plus_cancel_title">プランの解約</string>
<string name="billing_plus_cancel_message">FANBOX Viewer+ のプランが解約されたため、通常ユーザーに戻りました。\n\n意図していない場合は支払いに失敗している可能性があります。Playストアより、プランの状況をご確認ください。</string>

<!-- Report -->
<string name="report_crush_title">Oops! Something went wrong</string>
Expand Down
8 changes: 6 additions & 2 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
<string name="setting_top_general_hide_restricted_contents">Hide restricted contents</string>
<string name="setting_top_general_hide_restricted_contents_description">Hide content that can only be viewed by supporters.</string>
<string name="setting_top_general_grid_mode">Grid Mode</string>
<string name="setting_top_general_grid_mode_description">Change the display mode of the post list.</string>
<string name="setting_top_general_grid_mode_description">Switch the viewing method of the post list screen to table format (list display).</string>
<string name="setting_top_information">Information</string>
<string name="setting_top_information_id">CAIOS ID</string>
<string name="setting_top_information_version">App version</string>
Expand Down Expand Up @@ -226,7 +226,7 @@
<!-- Billing -->
<string name="billing_plus_title">FANBOX Viewer+</string>
<string name="billing_plus_description">FANBOX Viewer+ is a paid service that allows you to access all features for the price of a cup of coffee.</string>
<string name="billing_plus_purchase_button">Buy for %1$s</string>
<string name="billing_plus_purchase_button">Buy for %1$s / month</string>
<string name="billing_plus_verify_button">Restore purchase</string>
<string name="billing_plus_consume_button">Consume Purchase</string>
<string name="billing_plus_toast_require_plus">This feature can only be used with FANBOX Viewer+</string>
Expand All @@ -242,6 +242,8 @@
<string name="billing_plus_item_download_description">Download creators\' posts in bulk and enjoy them on other media!</string>
<string name="billing_plus_item_lock">Lock app</string>
<string name="billing_plus_item_lock_description">Require a password when starting the app to increase security!</string>
<string name="billing_plus_item_hide_restricted">Hide posts outside the support plan</string>
<string name="billing_plus_item_hide_restricted_description">Hide posts that cannot be seen outside of your support plan to improve visibility!</string>
<string name="billing_plus_item_widget">Home widget</string>
<string name="billing_plus_item_widget_description">Add a widget to your home screen and check posts from home! (Scheduled to be provided in an update)</string>
<string name="billing_plus_item_material_you">Material You</string>
Expand All @@ -252,6 +254,8 @@
<string name="billing_plus_item_feature_description">Be the first to experience new features when they are added!</string>
<string name="billing_plus_item_support">Priority support</string>
<string name="billing_plus_item_support_description">Always give top priority support!</string>
<string name="billing_plus_cancel_title">Plan canceled</string>
<string name="billing_plus_cancel_message">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.</string>

<!-- Report -->
<string name="report_crush_title">Oops! Something went wrong</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -104,7 +104,7 @@ internal fun BillingPlusRoute(
@Composable
private fun BillingPlusDialog(
purchase: Purchase?,
productDetails: ProductDetails?,
formattedPrice: String?,
isDeveloperMode: Boolean,
onClickPurchase: () -> Unit,
onClickVerify: () -> Unit,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -309,7 +316,7 @@ private fun BillingPlusScreenPreview() {
.background(MaterialTheme.colorScheme.surface)
.fillMaxSize(),
purchase = null,
productDetails = null,
formattedPrice = null,
isDeveloperMode = true,
onClickPurchase = {},
onClickVerify = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -86,7 +89,7 @@ class BillingPlusViewModel(
suspend fun purchase(activity: Activity): Boolean {
return runCatching {
withContext(ioDispatcher) {
purchasePlusUseCase.execute(activity)
purchasePlusSubscriptionUseCase.execute(activity)
}
}.fold(
onSuccess = {
Expand Down Expand Up @@ -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,
)
1 change: 1 addition & 0 deletions feature/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit ee7da17

Please sign in to comment.