diff --git a/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt b/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt index 62053d7d75..7d69127f8a 100644 --- a/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt +++ b/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt @@ -16,11 +16,13 @@ class IvyBilling( private const val LIFETIME_V1 = "ivy_wallet_lifetime_v1" + const val DONATE_2 = "donate_2" const val DONATE_5 = "donate_5" const val DONATE_10 = "donate_10" const val DONATE_15 = "donate_15" const val DONATE_25 = "donate_25" const val DONATE_50 = "donate_50" + const val DONATE_100 = "donate_100" val SUBSCRIPTIONS = listOf( MONTHLY_V1, @@ -29,11 +31,13 @@ class IvyBilling( ) val ONE_TIME_PLANS = listOf( + DONATE_2, DONATE_5, DONATE_10, DONATE_15, DONATE_25, - DONATE_50 + DONATE_50, + DONATE_100 ) } diff --git a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt index 2f50d7dd7e..a8054b073d 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt @@ -55,6 +55,7 @@ import com.ivy.wallet.ui.budget.BudgetScreen import com.ivy.wallet.ui.category.CategoriesScreen import com.ivy.wallet.ui.charts.ChartsScreen import com.ivy.wallet.ui.csvimport.ImportCSVScreen +import com.ivy.wallet.ui.donate.DonateScreen import com.ivy.wallet.ui.edit.EditTransactionScreen import com.ivy.wallet.ui.experiment.images.ImagesScreen import com.ivy.wallet.ui.loan.LoansScreen @@ -212,6 +213,7 @@ class RootActivity : AppCompatActivity() { is IvyWebView -> WebViewScreen(screen = screen) is ImagesScreen -> ImagesScreen(screen = screen) is ExperimentalScreen -> ExperimentalScreen(screen = screen) + is DonateScreen -> DonateScreen(screen = screen) null -> { } } diff --git a/app/src/main/java/com/ivy/wallet/ui/donate/DonateEvent.kt b/app/src/main/java/com/ivy/wallet/ui/donate/DonateEvent.kt index 5021a70509..a15cb407c5 100644 --- a/app/src/main/java/com/ivy/wallet/ui/donate/DonateEvent.kt +++ b/app/src/main/java/com/ivy/wallet/ui/donate/DonateEvent.kt @@ -1,10 +1,13 @@ package com.ivy.wallet.ui.donate -import com.ivy.wallet.android.billing.Plan import com.ivy.wallet.ui.RootActivity +import com.ivy.wallet.ui.donate.data.DonateOption sealed class DonateEvent { data class Load(val activity: RootActivity) : DonateEvent() - data class Buy(val plan: Plan) : DonateEvent() + data class Donate( + val activity: RootActivity, + val option: DonateOption + ) : DonateEvent() } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/donate/DonateScreen.kt b/app/src/main/java/com/ivy/wallet/ui/donate/DonateScreen.kt index 2596e07018..d11e4d1936 100644 --- a/app/src/main/java/com/ivy/wallet/ui/donate/DonateScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/donate/DonateScreen.kt @@ -1,2 +1,291 @@ package com.ivy.wallet.ui.donate +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.insets.navigationBarsPadding +import com.ivy.design.l0_system.Black +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.White +import com.ivy.design.l0_system.style +import com.ivy.design.l1_buildingBlocks.ColumnRoot +import com.ivy.design.l1_buildingBlocks.IvyText +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer +import com.ivy.design.l1_buildingBlocks.data.Background +import com.ivy.design.l2_components.IconButton +import com.ivy.design.utils.padding +import com.ivy.frp.view.FRP +import com.ivy.frp.view.navigation.Screen +import com.ivy.frp.view.navigation.navigation +import com.ivy.wallet.R +import com.ivy.wallet.ui.IvyWalletPreview +import com.ivy.wallet.ui.RootActivity +import com.ivy.wallet.ui.donate.data.DonateOption +import com.ivy.wallet.ui.rootActivity +import com.ivy.wallet.ui.theme.Gradient +import com.ivy.wallet.ui.theme.components.IvyButton + +object DonateScreen : Screen + +@Composable +fun BoxWithConstraintsScope.DonateScreen(screen: DonateScreen) { + FRP( + initialEvent = DonateEvent.Load(rootActivity()) + ) { state, onEvent -> + UI(state, onEvent) + } +} + +@Composable +private fun BoxWithConstraintsScope.UI( + state: DonateState, + onEvent: (DonateEvent) -> Unit +) { + var donateOption by remember { + mutableStateOf(DonateOption.DONATE_5) + } + + Column { + Image( + modifier = Modifier.fillMaxWidth(), + painter = painterResource(id = R.drawable.donate_illustration), + contentDescription = "rocket illustration", + contentScale = ContentScale.FillWidth + ) + + ScreenContent() + } + + ColumnRoot { + SpacerVer(height = 16.dp) + + val nav = navigation() + IconButton( + modifier = Modifier.padding(start = 16.dp), + icon = R.drawable.ic_back_android, + background = Background.Outlined( + width = 2.dp, + color = White, + shape = CircleShape, + padding = padding(all = 12.dp) + ) + ) { + nav.back() + } + + SpacerVer(height = 16.dp) + + IvyText( + modifier = Modifier.padding(start = 24.dp), + text = "Donate", + typo = UI.typo.h2.style( + fontWeight = FontWeight.Bold, + color = White + ) + ) + + SpacerVer(height = 4.dp) + + DonateOptionPicker(option = donateOption) { + donateOption = it + } + } + + val context = LocalView.current.context + DonateButton { + onEvent(DonateEvent.Donate(context as RootActivity, donateOption)) + } +} + +@Composable +private fun DonateOptionPicker( + option: DonateOption, + onSelect: (DonateOption) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + SpacerHor(width = 16.dp) + + if (option != DonateOption.DONATE_2) + OptionPickerButton( + icon = R.drawable.ic_donate_minus, + contentDescription = "btn_minus" + ) { + val newOption = when (option) { + DonateOption.DONATE_2 -> DonateOption.DONATE_2 + DonateOption.DONATE_5 -> DonateOption.DONATE_2 + DonateOption.DONATE_10 -> DonateOption.DONATE_5 + DonateOption.DONATE_15 -> DonateOption.DONATE_10 + DonateOption.DONATE_25 -> DonateOption.DONATE_15 + DonateOption.DONATE_50 -> DonateOption.DONATE_25 + DonateOption.DONATE_100 -> DonateOption.DONATE_50 + } + onSelect(newOption) + } + + SpacerHor(width = 12.dp) + + IvyText( + text = "$${ + when (option) { + DonateOption.DONATE_2 -> 2 + DonateOption.DONATE_5 -> 5 + DonateOption.DONATE_10 -> 10 + DonateOption.DONATE_15 -> 15 + DonateOption.DONATE_25 -> 25 + DonateOption.DONATE_50 -> 50 + DonateOption.DONATE_100 -> 100 + } + }", + typo = UI.typo.nH1.style( + fontWeight = FontWeight.Bold, + color = White + ) + ) + + SpacerHor(width = 12.dp) + + if (option != DonateOption.DONATE_100) + OptionPickerButton( + icon = R.drawable.ic_donate_plus, + contentDescription = "btn_plus" + ) { + val newOption = when (option) { + DonateOption.DONATE_2 -> DonateOption.DONATE_5 + DonateOption.DONATE_5 -> DonateOption.DONATE_10 + DonateOption.DONATE_10 -> DonateOption.DONATE_15 + DonateOption.DONATE_15 -> DonateOption.DONATE_25 + DonateOption.DONATE_25 -> DonateOption.DONATE_50 + DonateOption.DONATE_50 -> DonateOption.DONATE_100 + DonateOption.DONATE_100 -> DonateOption.DONATE_100 + } + onSelect(newOption) + } + } +} + +@Composable +private fun OptionPickerButton( + @DrawableRes icon: Int, + contentDescription: String, + onClick: () -> Unit +) { + Image( + modifier = Modifier + .clip(UI.shapes.r4) + .background(Black) + .clickable { onClick() } + .padding(horizontal = 8.dp, vertical = 4.dp), + painter = painterResource(icon), + contentDescription = contentDescription, + ) +} + +@Composable +private fun ScreenContent() { + LazyColumn { + item { + SpacerVer(height = 32.dp) + + IvyText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + text = "It seems like you enjoy free and open-source software. We too!", + typo = UI.typo.b1.style( + color = UI.colors.pureInverse, + fontWeight = FontWeight.Bold + ) + ) + } + + item { + SpacerVer(height = 12.dp) + + IvyText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + text = "BIG THANKS to all Ivy contributors who made Ivy Wallet possible! That's why we opened a donations channel to sustain and improve our small project.", + typo = UI.typo.b2.style( + color = UI.colors.gray, + fontWeight = FontWeight.Medium + ) + ) + } + + item { + SpacerVer(height = 24.dp) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .background(UI.colors.medium, UI.shapes.r4) + .padding(horizontal = 24.dp, vertical = 16.dp), + text = "If you want to support us feel free to donate whatever amount you're comfortable with - it all helps! (local taxes may apply)".uppercase(), + style = UI.typo.c.style( + fontWeight = FontWeight.Bold, + color = UI.colors.red1Inverse + ) + ) + } + + item { + SpacerVer(height = 120.dp) //scroll hack + } + } +} + +@Composable +private fun BoxWithConstraintsScope.DonateButton( + onClick: () -> Unit +) { + IvyButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .navigationBarsPadding( + bottom = true, start = false, end = false + ) + .padding(horizontal = 20.dp) + .padding(bottom = 16.dp), + iconStart = R.drawable.ic_donate_crown, + wrapContentMode = false, + iconTint = UI.colors.pure, + iconEdgePadding = 16.dp, + text = "Donate", + backgroundGradient = Gradient.solid(UI.colors.pureInverse), + textStyle = UI.typo.b1.style( + fontWeight = FontWeight.Bold, + color = UI.colors.pure + ) + ) { + onClick() + } +} + +@Preview +@Composable +private fun Preview() { + IvyWalletPreview { + UI(state = DonateState.Success, onEvent = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/donate/DonateState.kt b/app/src/main/java/com/ivy/wallet/ui/donate/DonateState.kt index 76791a615d..2f135cc90e 100644 --- a/app/src/main/java/com/ivy/wallet/ui/donate/DonateState.kt +++ b/app/src/main/java/com/ivy/wallet/ui/donate/DonateState.kt @@ -1,17 +1,9 @@ package com.ivy.wallet.ui.donate -import com.ivy.wallet.android.billing.Plan - sealed class DonateState { object Loading : DonateState() - data class Success( - val donate5: Pair, - val donate10: Pair, - val donate15: Pair, - val donate25: Pair, - val donate50: Pair - ) + object Success : DonateState() data class Error( val errMsg: String diff --git a/app/src/main/java/com/ivy/wallet/ui/donate/DonateViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/donate/DonateViewModel.kt index 9e4d1722e5..1b0931b7d9 100644 --- a/app/src/main/java/com/ivy/wallet/ui/donate/DonateViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/donate/DonateViewModel.kt @@ -1,8 +1,11 @@ package com.ivy.wallet.ui.donate import androidx.lifecycle.viewModelScope +import com.ivy.frp.then import com.ivy.frp.viewmodel.FRPViewModel import com.ivy.wallet.android.billing.IvyBilling +import com.ivy.wallet.android.billing.Plan +import com.ivy.wallet.ui.donate.data.DonateOption import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -14,9 +17,11 @@ class DonateViewModel @Inject constructor( ) : FRPViewModel() { override val _state: MutableStateFlow = MutableStateFlow(DonateState.Loading) + val plans = mutableListOf() + override suspend fun handleEvent(event: DonateEvent): suspend () -> DonateState = when (event) { - is DonateEvent.Buy -> TODO() - is DonateEvent.Load -> TODO() + is DonateEvent.Load -> load(event) + is DonateEvent.Donate -> donate(event) } private fun load(event: DonateEvent.Load) = suspend { @@ -24,17 +29,9 @@ class DonateViewModel @Inject constructor( activity = event.activity, onReady = { viewModelScope.launch { - val plans = ivyBilling.fetchOneTimePlans() - .mapNotNull { - when (it.sku) { - IvyBilling.DONATE_5 -> "Donate 5" to "Show support" - IvyBilling.DONATE_10 -> "Donate 10" to "Give us hope!" - IvyBilling.DONATE_15 -> "Donate 15" to "" - IvyBilling.DONATE_25 -> "Donate 25" to "Pay our servers for 1 month." - IvyBilling.DONATE_50 -> "Donate 50" to "Pay our accountant for 1 month." - else -> null - } - } + plans.clear() + plans.addAll(ivyBilling.fetchOneTimePlans()) + updateStateNonBlocking { DonateState.Success } } }, onError = { code, msg -> @@ -46,4 +43,27 @@ class DonateViewModel @Inject constructor( ) stateVal() } + + private fun donate(event: DonateEvent.Donate) = suspend { + when (event.option) { + DonateOption.DONATE_2 -> IvyBilling.DONATE_2 + DonateOption.DONATE_5 -> IvyBilling.DONATE_5 + DonateOption.DONATE_10 -> IvyBilling.DONATE_10 + DonateOption.DONATE_15 -> IvyBilling.DONATE_15 + DonateOption.DONATE_25 -> IvyBilling.DONATE_25 + DonateOption.DONATE_50 -> IvyBilling.DONATE_50 + DonateOption.DONATE_100 -> IvyBilling.DONATE_100 + } + } then { targetSku -> + plans.find { it.sku == targetSku } + } then { plan -> + if (plan != null) { + ivyBilling.buy( + activity = event.activity, + skuToBuy = plan.skuDetails, + oldSubscriptionPurchaseToken = null + ) + } + stateVal() + } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/donate/data/DonateOption.kt b/app/src/main/java/com/ivy/wallet/ui/donate/data/DonateOption.kt index 1fc5581395..06bb421daa 100644 --- a/app/src/main/java/com/ivy/wallet/ui/donate/data/DonateOption.kt +++ b/app/src/main/java/com/ivy/wallet/ui/donate/data/DonateOption.kt @@ -1,9 +1,5 @@ package com.ivy.wallet.ui.donate.data -import com.ivy.wallet.android.billing.Plan - -data class DonateOption( - val title: String, - val desc: String, - val plan: Plan -) \ No newline at end of file +enum class DonateOption { + DONATE_2, DONATE_5, DONATE_10, DONATE_15, DONATE_25, DONATE_50, DONATE_100 +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt index 412bac75d1..3ccb7c33c1 100644 --- a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt @@ -43,6 +43,7 @@ import com.ivy.wallet.domain.data.AuthProviderType import com.ivy.wallet.domain.data.IvyCurrency import com.ivy.wallet.domain.data.core.User import com.ivy.wallet.ui.* +import com.ivy.wallet.ui.donate.DonateScreen import com.ivy.wallet.ui.settings.experimental.ExperimentalScreen import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.components.IvyButton @@ -347,6 +348,18 @@ private fun BoxWithConstraintsScope.UI( ) { ivyActivity.shareIvyWallet() } + + Spacer(Modifier.height(12.dp)) + + val nav = navigation() + SettingsPrimaryButton( + icon = R.drawable.ic_donate_crown, + text = "Donate", + iconPadding = 8.dp, + backgroundGradient = Gradient.solid(Orange2) + ) { + nav.navigateTo(DonateScreen) + } } item { diff --git a/app/src/main/res/drawable-nodpi/donate_illustration.png b/app/src/main/res/drawable-nodpi/donate_illustration.png new file mode 100644 index 0000000000..b89131be8c Binary files /dev/null and b/app/src/main/res/drawable-nodpi/donate_illustration.png differ diff --git a/app/src/main/res/drawable/ic_back_android.xml b/app/src/main/res/drawable/ic_back_android.xml new file mode 100644 index 0000000000..a8129acd52 --- /dev/null +++ b/app/src/main/res/drawable/ic_back_android.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_donate_crown.xml b/app/src/main/res/drawable/ic_donate_crown.xml new file mode 100644 index 0000000000..5ce35184d8 --- /dev/null +++ b/app/src/main/res/drawable/ic_donate_crown.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_donate_minus.xml b/app/src/main/res/drawable/ic_donate_minus.xml new file mode 100644 index 0000000000..7cf76b0b6b --- /dev/null +++ b/app/src/main/res/drawable/ic_donate_minus.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_donate_plus.xml b/app/src/main/res/drawable/ic_donate_plus.xml new file mode 100644 index 0000000000..46c62e582f --- /dev/null +++ b/app/src/main/res/drawable/ic_donate_plus.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + diff --git a/ivy-design/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt b/ivy-design/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt index df2279e011..54265736d6 100644 --- a/ivy-design/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt +++ b/ivy-design/src/main/java/com/ivy/design/api/systems/IvyWalletDesign.kt @@ -130,6 +130,7 @@ abstract class IvyWalletDesign : IvyDesign { override val red = Red override val red1 = RedLight + override val red1Inverse = RedDark override val isLight = true } @@ -151,6 +152,7 @@ abstract class IvyWalletDesign : IvyDesign { override val red = Red override val red1 = RedDark + override val red1Inverse = RedLight override val isLight = false } diff --git a/ivy-design/src/main/java/com/ivy/design/l0_system/IvyColors.kt b/ivy-design/src/main/java/com/ivy/design/l0_system/IvyColors.kt index 19fdf8eddc..d783d6dcb8 100644 --- a/ivy-design/src/main/java/com/ivy/design/l0_system/IvyColors.kt +++ b/ivy-design/src/main/java/com/ivy/design/l0_system/IvyColors.kt @@ -21,6 +21,7 @@ interface IvyColors { val red: Color val red1: Color + val red1Inverse: Color val isLight: Boolean } \ No newline at end of file diff --git a/ivy-design/src/main/java/com/ivy/design/l5_concept/SettingsScreen.kt b/ivy-design/src/main/java/com/ivy/design/l5_concept/SettingsScreen.kt deleted file mode 100644 index 784cec38c4..0000000000 --- a/ivy-design/src/main/java/com/ivy/design/l5_concept/SettingsScreen.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.ivy.design.l5_concept - -//Upgraded Ivy Wallet's settings screen \ No newline at end of file