diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b0abe117..d748086e 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,9 +2,9 @@ name: Android CI CD on: push: - branches: [ "main", "develop", "release" ] + branches: [ "main", "develop", "release", "feature/*" ] pull_request: - branches: [ "main", "develop", "release" ] + branches: [ "main", "develop", "release", "feature/*" ] jobs: build: @@ -60,31 +60,33 @@ jobs: - name: Build clean run: ./gradlew clean - - name: Run Android lint - run: ./gradlew lint + # - name: Run Android lint + # run: ./gradlew lint - name: Build assemble debug apk run: ./gradlew assembleDebug --stacktrace - - name: Build assemble release aab - run: ./gradlew bundleRelease --stacktrace + # - name: Build assemble release aab + # run: ./gradlew bundleRelease --stacktrace - name: Upload Debug APK - if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/release') uses: actions/upload-artifact@v4 with: name: debug path: ./app/build/outputs/apk/debug/app-debug.apk - - name: Upload Release APK - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 - with: - name: release - path: ./app/build/outputs/bundle/release/app-release.aab + # - name: Upload Release APK + # if: github.event_name == 'push' && github.ref == 'refs/heads/main' + # uses: actions/upload-artifact@v4 + # with: + # name: release + # path: ./app/build/outputs/bundle/release/app-release.aab - name: Upload apk to Firebase App Distribution - if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/release') + if: github.event_name == 'pull_request' && ( + github.ref == 'refs/heads/develop' || + startsWith(github.ref, 'refs/heads/feature/') + ) uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{ secrets.FIREBASE_APP_ID }} @@ -92,6 +94,17 @@ jobs: groups: testers file: app/build/outputs/apk/debug/app-debug.apk + - name: Notify Slack about new distribution + if: github.event_name == 'push' + run: | + curl -X POST -H 'Content-type: application/json' \ + --data "{ + \"text\": \"πŸ”₯ Firebase Distribution μ—…λ‘œλ“œ μ™„λ£Œ! \nπŸ”§ 브랜치: ${GITHUB_REF##*/}\nπŸ”— 컀밋: ${GITHUB_SHA}\" + }" \ + "$SLACK_WEBHOOK_URL_FIREBASE_DISTRIBUTION" + env: + SLACK_WEBHOOK_URL_FIREBASE_DISTRIBUTION: ${{ secrets.SLACK_WEBHOOK_URL_FIREBASE_DISTRIBUTION }} + - name: Publish to Play Store if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: r0adkll/upload-google-play@v1 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 45ac2e69..18756fe7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -78,9 +78,6 @@ dependencies { implementation(libs.timber) implementation(libs.billing) - debugImplementation(libs.bundles.flipper) - releaseImplementation(libs.flipper.noop) - implementation(libs.hilt.android) kapt(libs.hilt.compiler) } diff --git a/app/src/main/java/com/nextroom/nextroom/NextRoomApplication.kt b/app/src/main/java/com/nextroom/nextroom/NextRoomApplication.kt index 2a56218a..e6387b85 100644 --- a/app/src/main/java/com/nextroom/nextroom/NextRoomApplication.kt +++ b/app/src/main/java/com/nextroom/nextroom/NextRoomApplication.kt @@ -1,13 +1,6 @@ package com.nextroom.nextroom import android.app.Application -import com.facebook.flipper.android.AndroidFlipperClient -import com.facebook.flipper.android.utils.FlipperUtils -import com.facebook.flipper.plugins.inspector.DescriptorMapping -import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin -import com.facebook.flipper.plugins.network.NetworkFlipperPlugin -import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin -import com.facebook.soloader.SoLoader import dagger.hilt.android.HiltAndroidApp import timber.log.Timber import javax.inject.Inject @@ -15,32 +8,13 @@ import javax.inject.Inject @HiltAndroidApp class NextRoomApplication : Application() { @Inject - lateinit var flipperNetworkPlugin: NetworkFlipperPlugin + lateinit var flavorExtraFunction: FlavorExtraFunction override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) - } - - setDebugTool() - } - - private fun setDebugTool() { - SoLoader.init(this, false) - - if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) { - val client = AndroidFlipperClient.getInstance(this) - client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults())) - client.addPlugin( - InspectorFlipperPlugin( - applicationContext, - DescriptorMapping.withDefaults(), - ), - ) - client.addPlugin(SharedPreferencesFlipperPlugin(this, "app-settings.json")) - client.addPlugin(flipperNetworkPlugin) - client.start() + flavorExtraFunction.initializeFlipper() } } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index a4e558f5..37efe54b 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -46,6 +46,12 @@ android { buildFeatures { buildConfig = true } + sourceSets.getByName("debug") { + java.setSrcDirs(listOf("src/debug/java")) + } + sourceSets.getByName("release") { + java.setSrcDirs(listOf("src/release/java")) + } } dependencies { diff --git a/data/src/debug/java/com/nextroom/nextroom/FlavorExtraFunction.kt b/data/src/debug/java/com/nextroom/nextroom/FlavorExtraFunction.kt new file mode 100644 index 00000000..bd9ff609 --- /dev/null +++ b/data/src/debug/java/com/nextroom/nextroom/FlavorExtraFunction.kt @@ -0,0 +1,45 @@ +package com.nextroom.nextroom + +import android.content.Context +import com.facebook.flipper.android.AndroidFlipperClient +import com.facebook.flipper.android.utils.FlipperUtils +import com.facebook.flipper.plugins.inspector.DescriptorMapping +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin +import com.facebook.soloader.SoLoader +import com.nextroom.nextroom.data.BuildConfig +import okhttp3.Interceptor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FlavorExtraFunction @Inject constructor( + private val context: Context, +) { + private lateinit var flipperNetworkPlugin: NetworkFlipperPlugin + + fun initializeFlipper() { + SoLoader.init(context, false) + + if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(context)) { + flipperNetworkPlugin = NetworkFlipperPlugin() + val client = AndroidFlipperClient.getInstance(context) + client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())) + client.addPlugin( + InspectorFlipperPlugin( + context, + DescriptorMapping.withDefaults(), + ), + ) + client.addPlugin(SharedPreferencesFlipperPlugin(context, "app-settings.json")) + client.addPlugin(flipperNetworkPlugin) + client.start() + } + } + + fun getFlipperInterceptor(): Interceptor { + return FlipperOkhttpInterceptor(flipperNetworkPlugin, true) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/nextroom/nextroom/data/di/NetworkModule.kt b/data/src/main/java/com/nextroom/nextroom/data/di/NetworkModule.kt index a7088530..192c2b63 100644 --- a/data/src/main/java/com/nextroom/nextroom/data/di/NetworkModule.kt +++ b/data/src/main/java/com/nextroom/nextroom/data/di/NetworkModule.kt @@ -1,7 +1,7 @@ package com.nextroom.nextroom.data.di -import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor -import com.facebook.flipper.plugins.network.NetworkFlipperPlugin +import android.content.Context +import com.nextroom.nextroom.FlavorExtraFunction import com.nextroom.nextroom.data.BuildConfig import com.nextroom.nextroom.data.datasource.AuthDataSource import com.nextroom.nextroom.data.datasource.TokenDataSource @@ -12,7 +12,9 @@ import com.nextroom.nextroom.data.network.ResultCallAdapterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -76,7 +78,7 @@ object NetworkModule { fun provideAuthOkHttpClient( authInterceptor: AuthInterceptor, authAuthenticator: AuthAuthenticator, - flipperPlugin: NetworkFlipperPlugin, + flipperInterceptor: Interceptor?, ): OkHttpClient { val loggingInterceptor = HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) { @@ -91,7 +93,11 @@ object NetworkModule { .authenticator(authAuthenticator) .addInterceptor(authInterceptor) .addInterceptor(loggingInterceptor) - .addInterceptor(FlipperOkhttpInterceptor(flipperPlugin, true)) + .apply { + flipperInterceptor?.let { + addInterceptor(it) + } + } .build() } @@ -99,7 +105,7 @@ object NetworkModule { @Provides @Named("defaultOkHttpClient") fun provideDefaultOkHttpClient( - flipperPlugin: NetworkFlipperPlugin, + flipperInterceptor: Interceptor?, ): OkHttpClient { val loggingInterceptor = HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) { @@ -112,7 +118,11 @@ object NetworkModule { .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .addInterceptor(loggingInterceptor) - .addNetworkInterceptor(FlipperOkhttpInterceptor(flipperPlugin)) + .apply { + flipperInterceptor?.let { + addInterceptor(it) + } + } .build() } @@ -134,7 +144,17 @@ object NetworkModule { @Singleton @Provides - fun provideNetworkFlipperPlugin(): NetworkFlipperPlugin { - return NetworkFlipperPlugin() + fun provideFlipperInterceptor( + flavorExtraFunction: FlavorExtraFunction, + ): Interceptor? { + return flavorExtraFunction.getFlipperInterceptor() + } + + @Singleton + @Provides + fun provideFlavorExtraFunction( + @ApplicationContext context: Context, + ): FlavorExtraFunction { + return FlavorExtraFunction(context) } } diff --git a/data/src/release/java/com/nextroom/nextroom/FlavorExtraFunction.kt b/data/src/release/java/com/nextroom/nextroom/FlavorExtraFunction.kt new file mode 100644 index 00000000..b6e379f8 --- /dev/null +++ b/data/src/release/java/com/nextroom/nextroom/FlavorExtraFunction.kt @@ -0,0 +1,17 @@ +package com.nextroom.nextroom + +import android.content.Context +import okhttp3.Interceptor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FlavorExtraFunction @Inject constructor( + private val context: Context, +) { + fun initializeFlipper() { + // do nothing + } + + fun getFlipperInterceptor(): Interceptor? = null +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a22b49c..4eafa388 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,8 +3,8 @@ minSdk = "24" review = "2.0.1" reviewKtx = "2.0.1" targetSdk = "34" -versionCode = "61" -versionName = "1.4.6" +versionCode = "65" +versionName = "1.4.7" compileSdk = "34" targetJvm = "17" kotlin = "1.9.10" diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/SubscriptionPromotionBottomSheet.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/SubscriptionPromotionBottomSheet.kt index 6f182d34..eabd1f4e 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/SubscriptionPromotionBottomSheet.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/SubscriptionPromotionBottomSheet.kt @@ -17,10 +17,12 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R +import com.nextroom.nextroom.presentation.common.NROneButtonDialog import com.nextroom.nextroom.presentation.databinding.BottomSheetSubscriptionPromotionBinding import com.nextroom.nextroom.presentation.databinding.ItemBenefitBinding import com.nextroom.nextroom.presentation.extension.dpToPx import com.nextroom.nextroom.presentation.extension.repeatOnStarted +import com.nextroom.nextroom.presentation.extension.safeNavigate import com.nextroom.nextroom.presentation.extension.snackbar import com.nextroom.nextroom.presentation.extension.toast import com.nextroom.nextroom.presentation.ui.billing.BillingEvent @@ -95,11 +97,14 @@ class SubscriptionPromotionBottomSheet : BottomSheetDialogFragment() { binding.acbSubscribe.setOnClickListener { viewModel.container.stateFlow.value.plan.plans.firstOrNull()?.let { binding.pbLoading.isVisible = true - billingViewModel.buyPlans( - productId = it.subscriptionProductId, - tag = "", - upDowngrade = false, - ) + try { + billingViewModel.buyPlans( + productId = it.subscriptionProductId, + upDowngrade = false, + ) + } catch (e: Exception) { + showErrorDialog(e.message ?: "") + } } } binding.ivClose.setOnClickListener { @@ -167,6 +172,18 @@ class SubscriptionPromotionBottomSheet : BottomSheetDialogFragment() { } } + private fun showErrorDialog(errorText: String) { + NavGraphDirections + .moveToNrOneButtonDialog( + NROneButtonDialog.NROneButtonArgument( + title = getString(R.string.dialog_noti), + message = getString(R.string.error_something), + btnText = getString(R.string.text_confirm), + errorText = errorText + ) + ).also { findNavController().safeNavigate(it) } + } + data class Benefit( val title: String, val desc: String, diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingEvent.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingEvent.kt index 50b8b7f2..9416eaa3 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingEvent.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingEvent.kt @@ -2,5 +2,5 @@ package com.nextroom.nextroom.presentation.ui.billing sealed interface BillingEvent { data object PurchaseAcknowledged : BillingEvent - data class PurchaseFailed(val purchaseState: Int) : BillingEvent + data class PurchaseFailed(val errorMessage: String = "", val purchaseState: Int? = null) : BillingEvent } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingViewModel.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingViewModel.kt index 0b3bf003..7427888d 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingViewModel.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/billing/BillingViewModel.kt @@ -114,63 +114,6 @@ class BillingViewModel ).build() } - /** - * Calculates the lowest priced offer amongst all eligible offers. - * In this implementation the lowest price of all offers' pricing phases is returned. - * It's possible the logic can be implemented differently. - * For example, the lowest average price in terms of month could be returned instead. - * - * @param offerDetails List of of eligible offers and base plans. - * - * @return the offer id token of the lowest priced offer. - * - */ - private fun leastPricedOfferToken( - offerDetails: List, - ): String { - var offerToken = String() - var leastPricedOffer: ProductDetails.SubscriptionOfferDetails - var lowestPrice = Int.MAX_VALUE - - if (offerDetails.isNotEmpty()) { - for (offer in offerDetails) { - for (price in offer.pricingPhases.pricingPhaseList) { - if (price.priceAmountMicros < lowestPrice) { - lowestPrice = price.priceAmountMicros.toInt() - leastPricedOffer = offer - offerToken = leastPricedOffer.offerToken - } - } - } - } - return offerToken - - TODO("Replace this with least average priced offer implementation") - } - - /** - * Retrieves all eligible base plans and offers using tags from ProductDetails. - * - * @param offerDetails offerDetails from a ProductDetails returned by the library. - * @param tag string representing tags associated with offers and base plans. - * - * @return the eligible offers and base plans in a list. - * - */ - private fun retrieveEligibleOffers( - offerDetails: MutableList, - tag: String, - ): - List { - val eligibleOffers = emptyList().toMutableList() - offerDetails.forEach { offerDetail -> - if (offerDetail.offerTags.contains(tag)) { - eligibleOffers.add(offerDetail) - } - } - return eligibleOffers - } - // 이미 ꡬ독 쀑인 μƒν’ˆμ΄ μžˆλŠ”μ§€ 체크 private fun purchaseForProduct(purchases: List?, product: String) = purchases?.firstOrNull { it.products.first() == product } @@ -182,11 +125,10 @@ class BillingViewModel /** * μš”κΈˆμ œ ꡬ맀 * - * @param tag: μš”κΈˆμ œμ™€ κ΄€λ ¨λœ νƒœκ·Έλ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ¬Έμžμ—΄ * @param productId: ꡬ맀 ν•˜λ €λŠ” μƒν’ˆμ˜ id * @param upDowngrade: ꡬ맀가 μ—…κ·Έλ ˆμ΄λ“œ λ˜λŠ” λ‹€μš΄κ·Έλ ˆμ΄λ“œμΈμ§€, μš”κΈˆμ œλ₯Ό μ „ν™˜ν•˜λ €λŠ” κ²½μš°μ— true */ - fun buyPlans(tag: String, productId: String, upDowngrade: Boolean) { + fun buyPlans(productId: String, upDowngrade: Boolean) { val isProductOnDevice = deviceHasGooglePlaySubscription(purchases.value, productId) if (isProductOnDevice) { Timber.d("The user already owns this item: $productId") @@ -196,19 +138,11 @@ class BillingViewModel when (productId) { Constants.MEMBERSHIP_PRODUCT -> membershipProductWithProductDetails.value else -> null - }?.also { productDetails -> - productDetails.subscriptionOfferDetails?.let { offerDetailsList -> - retrieveEligibleOffers( - offerDetails = offerDetailsList, - tag = tag, - ) - }.let { offers -> - offers?.let { leastPricedOfferToken(it) }.toString() - }.also { offerToken -> - launchFlow(upDowngrade, offerToken, productDetails) - } + }?.let { productDetails -> + val offerToken = requireNotNull(productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken) + launchFlow(upDowngrade, offerToken, productDetails) } ?: run { - Timber.e("Could not find product details.") + throw Exception("Could not find product details. productId: $productId") } } diff --git a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/purchase/PurchaseFragment.kt b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/purchase/PurchaseFragment.kt index cecdb978..f319c39f 100644 --- a/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/purchase/PurchaseFragment.kt +++ b/presentation/src/main/java/com/nextroom/nextroom/presentation/ui/purchase/PurchaseFragment.kt @@ -10,12 +10,12 @@ import androidx.navigation.fragment.findNavController import com.nextroom.nextroom.presentation.NavGraphDirections import com.nextroom.nextroom.presentation.R import com.nextroom.nextroom.presentation.base.BaseFragment +import com.nextroom.nextroom.presentation.common.NROneButtonDialog import com.nextroom.nextroom.presentation.databinding.FragmentPurchaseBinding import com.nextroom.nextroom.presentation.extension.repeatOnStarted import com.nextroom.nextroom.presentation.extension.safeNavigate import com.nextroom.nextroom.presentation.extension.snackbar import com.nextroom.nextroom.presentation.extension.strikeThrow -import com.nextroom.nextroom.presentation.extension.toast import com.nextroom.nextroom.presentation.ui.billing.BillingEvent import com.nextroom.nextroom.presentation.ui.billing.BillingViewModel import dagger.hilt.android.AndroidEntryPoint @@ -48,11 +48,14 @@ class PurchaseFragment : BaseFragment(FragmentPurchaseB binding.btnSubscribe.setOnClickListener { (viewModel.uiState.value as? PurchaseViewModel.UiState.Loaded)?.let { loaded -> binding.pbLoading.isVisible = true // TODO JH: κ°œμ„  - billingViewModel.buyPlans( - productId = loaded.subscriptionProductId, - tag = "", - upDowngrade = false, - ) + try { + billingViewModel.buyPlans( + productId = loaded.subscriptionProductId, + upDowngrade = false, + ) + } catch (e: Exception) { + showErrorDialog(errorText = e.message ?: "") + } } } } @@ -90,12 +93,9 @@ class PurchaseFragment : BaseFragment(FragmentPurchaseB } is BillingEvent.PurchaseFailed -> { - toast( - getString( - R.string.purchase_error_message, - event.purchaseState, - ), - ) + val errorText = event.errorMessage + "\n" + + event.purchaseState?.let { getString(R.string.text_error_code, it) } + showErrorDialog(errorText) binding.pbLoading.isVisible = false // TODO JH: κ°œμ„  } } @@ -122,4 +122,16 @@ class PurchaseFragment : BaseFragment(FragmentPurchaseB } } } + + private fun showErrorDialog(errorText: String) { + NavGraphDirections + .moveToNrOneButtonDialog( + NROneButtonDialog.NROneButtonArgument( + title = getString(R.string.dialog_noti), + message = getString(R.string.error_something), + btnText = getString(R.string.text_confirm), + errorText = errorText + ) + ).also { findNavController().safeNavigate(it) } + } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 5309fc76..a0ce21fd 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -229,4 +229,5 @@ 에 κ²Œμž„μ΄ μ’…λ£Œλ©λ‹ˆλ‹€. μ‹œκ°„ μˆ˜μ • λΆ„ + μ—λŸ¬ μ½”λ“œ: %d \ No newline at end of file