diff --git a/.github/workflows/internal_release.yml b/.github/workflows/internal_release.yml index 1bf49fa874..527bc5668e 100644 --- a/.github/workflows/internal_release.yml +++ b/.github/workflows/internal_release.yml @@ -139,6 +139,7 @@ jobs: REPO: ${{ github.repository }} - name: Create GitHub Release + if: always() #Execute even the generation of changelog has failed id: create_release uses: actions/create-release@latest env: diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index d0b810ad20..83b9d01306 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,14 +20,17 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile +# Fix broken stuff by R8 +-keep class com.ivy.wallet.ui.widget.** { *; } +-keep class com.ivy.wallet.domain.data.** { *; } +-keep class com.ivy.wallet.io.network.** { *; } +-keep class com.ivy.wallet.io.persistence.data.** { *; } +-keep class com.ivy.wallet.io.network.data.** { *; } +-keep class com.ivy.wallet.domain.event.** { *; } + -keepattributes EnclosingMethod -keepattributes InnerClasses -# Widget --keep class com.ivy.wallet.widget.** { *; } -# ------ - - # Firebase Crashlytics -dontwarn org.xmlpull.v1.** -dontnote org.xmlpull.v1.** @@ -136,14 +139,6 @@ # Application classes that will be serialized/deserialized over Gson -keep class com.ivy.wallet.model.** { ; } -# Fix broken stuff by R8 --keep class com.ivy.wallet.domain.data.** { *; } --keep class com.ivy.wallet.ui.widget.** { *; } --keep class com.ivy.wallet.io.network.** { *; } --keep class com.ivy.wallet.io.persistence.data.** { *; } --keep class com.ivy.wallet.io.network.data.** { *; } --keep class com.ivy.wallet.domain.event.** { *; } - # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) -keep class * implements com.google.gson.TypeAdapter diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/IvyComposeTest.kt b/app/src/androidTest/java/com/ivy/wallet/compose/IvyComposeTest.kt index 0b14f6b553..9fa1a19ca3 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/IvyComposeTest.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/IvyComposeTest.kt @@ -1,12 +1,17 @@ package com.ivy.wallet.compose import android.content.Context +import android.content.Context.INPUT_METHOD_SERVICE import android.util.Log +import android.view.inputmethod.InputMethodManager +import androidx.activity.ComponentActivity import androidx.compose.ui.test.IdlingResource import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.platform.app.InstrumentationRegistry import androidx.work.Configuration import androidx.work.impl.utils.SynchronousExecutor @@ -191,4 +196,14 @@ fun ComposeTestRule.clickWithRetry( ) } } +} + +fun AndroidComposeTestRule, A>.hideKeyboard() { + with(this.activity) { + if (currentFocus != null) { + val inputMethodManager: InputMethodManager = + this.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0) + } + } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AccountsTab.kt b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AccountsTab.kt index 6e7437fc4c..1d1f9f5328 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AccountsTab.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AccountsTab.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.ivy.wallet.compose.hideKeyboard import com.ivy.wallet.compose.printTree import com.ivy.wallet.ui.theme.Ivy @@ -65,6 +66,8 @@ class AccountsTab( accountModal.apply { enterTitle(name) + composeTestRule.hideKeyboard() + ivyColorPicker.chooseColor(color = color) if (icon != null) { diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AmountInput.kt b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AmountInput.kt index bf05c9a2d8..81b67f45d1 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AmountInput.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/AmountInput.kt @@ -4,30 +4,46 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.test.ext.junit.rules.ActivityScenarioRule class AmountInput( private val composeTestRule: AndroidComposeTestRule, A> ) { - fun enterNumber(number: String) { + fun enterNumber( + number: String, + onCalculator: Boolean = false, + autoPressNonCalculator: Boolean = true, + ) { composeTestRule.waitForIdle() for (char in number) { when (char) { - in '0'..'9' -> pressNumber(char.toString().toInt()) + in '0'..'9' -> pressNumber( + number = char.toString().toInt(), + onCalculator = onCalculator + ) ',' -> { //do nothing } - '.' -> pressDecimalSeparator() + '.' -> pressDecimalSeparator( + onCalculator = onCalculator + ) } } - clickSet() + if (!onCalculator && autoPressNonCalculator) { + clickSet() + } } - fun pressNumber(number: Int) { - composeTestRule.onNode(hasTestTag("key_$number")) + private fun pressNumber(number: Int, onCalculator: Boolean) { + composeTestRule.onNode( + hasTestTag( + if (onCalculator) "calc_key_$number" else "key_$number" + ) + ) .performClick() } @@ -36,8 +52,54 @@ class AmountInput( .performClick() } - fun pressDecimalSeparator() { - composeTestRule.onNode(hasTestTag("key_decimal_separator")) + fun pressDecimalSeparator( + onCalculator: Boolean + ) { + composeTestRule.onNode( + hasTestTag( + if (onCalculator) "calc_key_decimal_separator" else "key_decimal_separator" + ) + ) + .performClick() + } + + fun pressPlus() { + composeTestRule.onNodeWithTag("key_+") + .performClick() + } + + fun pressMinus() { + composeTestRule.onNodeWithTag("key_-") + .performClick() + } + + fun pressMultiplication() { + composeTestRule.onNodeWithTag("key_*") + .performClick() + } + + fun pressDivision() { + composeTestRule.onNodeWithTag("key_/") + .performClick() + } + + fun pressLeftBracket() { + composeTestRule.onNodeWithTag("key_(") + .performClick() + } + + fun pressRightBracket() { + composeTestRule.onNodeWithTag("key_)") + .performClick() + } + + fun pressCalcEqual() { + composeTestRule.onNodeWithTag("key_=") + .performClick() + } + + fun clickCalcSet() { + composeTestRule.onNodeWithTag("calc_set") .performClick() } @@ -45,4 +107,9 @@ class AmountInput( composeTestRule.onNode(hasText("Enter")) .performClick() } + + fun clickCalculator() { + composeTestRule.onNodeWithTag("btn_calculator") + .performClick() + } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/HomeTab.kt b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/HomeTab.kt index a1fb280da4..b254d8bb67 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/HomeTab.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/HomeTab.kt @@ -4,6 +4,7 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.ivy.wallet.compose.printTree class HomeTab( private val composeTestRule: AndroidComposeTestRule, A> @@ -24,25 +25,46 @@ class HomeTab( account: String? = null, category: String? = null ) { - var matcher = hasTestTag("transaction_card") - .and(hasText(amount)) + var matcher = hasTestTag("type_amount_currency") + .and(hasAnyDescendant(hasText(amount))) if (account != null) { - matcher = matcher.and(hasAnyDescendant(hasText(account))) + matcher = matcher.and( + hasAnySibling( + hasAnyDescendant( + hasText(account) + ) + ) + ) } if (category != null) { - matcher = matcher.and(hasAnyDescendant(hasText(category))) + matcher = matcher.and( + hasAnySibling( + hasAnyDescendant( + hasText(category) + ) + ) + ) } if (title != null) { - matcher = matcher.and(hasText(title)) + matcher = matcher.and( + hasAnySibling( + hasText(title) + ) + ) } - composeTestRule.onNode(matcher) + composeTestRule.printTree( + useUnmergedTree = true + ) + + composeTestRule.onNode( + matcher = matcher, + useUnmergedTree = true + ) .assertIsDisplayed() - .assertHasClickAction() - .performScrollTo() .performClick() } @@ -109,4 +131,14 @@ class HomeTab( composeTestRule.onNodeWithTag("home_greeting_text", useUnmergedTree = true) .assertTextEquals(greeting) } + + fun clickIncomeCard() { + composeTestRule.onNodeWithTag("home_card_income") + .performClick() + } + + fun clickExpenseCard() { + composeTestRule.onNodeWithTag("home_card_expense") + .performClick() + } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/IvyColorPicker.kt b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/IvyColorPicker.kt index 010f4ea57d..3da9ea5db5 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/IvyColorPicker.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/IvyColorPicker.kt @@ -7,15 +7,12 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.test.ext.junit.rules.ActivityScenarioRule -import com.ivy.wallet.compose.printTree class IvyColorPicker( private val composeTestRule: AndroidComposeTestRule, A> ) { fun chooseColor(color: Color) { - composeTestRule.printTree() - composeTestRule.onNode(hasTestTag("color_item_${color.value}")) .performScrollTo() .performClick() diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/PieChartScreen.kt b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/PieChartScreen.kt new file mode 100644 index 0000000000..4fc1a66658 --- /dev/null +++ b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/PieChartScreen.kt @@ -0,0 +1,39 @@ +package com.ivy.wallet.compose.helpers + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.ext.junit.rules.ActivityScenarioRule + +class PieChartScreen( + private val composeTestRule: AndroidComposeTestRule, A> +) { + fun assertTitle(title: String) { + composeTestRule.onNodeWithTag("piechart_title") + .assertTextContains(title) + } + + fun assertTotalAmount( + amountInt: String, + decimalPart: String, + currency: String = "USD" + ) { + val matchText: (String) -> SemanticsMatcher = { text -> + hasTestTag("piechart_total_amount") + .and( + hasAnyDescendant( + hasText(text) + ) + ) + } + + composeTestRule.onNode(matchText(amountInt)) + .assertIsDisplayed() + + composeTestRule.onNode(matchText(decimalPart)) + .assertIsDisplayed() + + composeTestRule.onNode(matchText(currency)) + .assertIsDisplayed() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/EditTransactionScreen.kt b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/TransactionScreen.kt similarity index 86% rename from app/src/androidTest/java/com/ivy/wallet/compose/helpers/EditTransactionScreen.kt rename to app/src/androidTest/java/com/ivy/wallet/compose/helpers/TransactionScreen.kt index c7f0341169..7b2da68e23 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/helpers/EditTransactionScreen.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/helpers/TransactionScreen.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.test.ext.junit.rules.ActivityScenarioRule -class EditTransactionScreen( +class TransactionScreen( private val composeTestRule: AndroidComposeTestRule, A> ) { private val amountInput = AmountInput(composeTestRule) @@ -60,4 +60,14 @@ class EditTransactionScreen( composeTestRule.onNodeWithText("Save") .performClick() } + + fun clickAdd() { + composeTestRule.onNodeWithText("Add") + .performClick() + } + + fun skipCategory() { + composeTestRule.onNodeWithText("Skip") + .performClick() + } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/AccountsTest.kt b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/AccountsTest.kt index 9c0e746450..d673804769 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/AccountsTest.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/AccountsTest.kt @@ -20,7 +20,7 @@ class AccountsTest : IvyComposeTest() { private val transactionFlow = TransactionFlow(composeTestRule) private val homeTab = HomeTab(composeTestRule) private val accountsTab = AccountsTab(composeTestRule) - private val editTransactionScreen = EditTransactionScreen(composeTestRule) + private val editTransactionScreen = TransactionScreen(composeTestRule) private val itemStatisticScreen = ItemStatisticScreen(composeTestRule) private val reorderModal = ReorderModal(composeTestRule) private val deleteConfirmationModal = DeleteConfirmationModal(composeTestRule) diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/CalculatorTest.kt b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/CalculatorTest.kt new file mode 100644 index 0000000000..5aee0aec5e --- /dev/null +++ b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/CalculatorTest.kt @@ -0,0 +1,250 @@ +package com.ivy.wallet.compose.scenario + +import com.ivy.wallet.compose.IvyComposeTest +import com.ivy.wallet.compose.helpers.* +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class CalculatorTest : IvyComposeTest() { + private val onboarding = OnboardingFlow(composeTestRule) + private val homeTab = HomeTab(composeTestRule) + private val mainBottomBar = MainBottomBar(composeTestRule) + private val amountInput = AmountInput(composeTestRule) + private val transactionScreen = TransactionScreen(composeTestRule) + + + @Test + fun calcAmount_viaExtraction() = testWithRetry { + onboarding.quickOnboarding() + mainBottomBar.clickAddFAB() + mainBottomBar.clickAddExpense() + + //--------------------------- + amountInput.clickCalculator() + + amountInput.enterNumber( + number = "21", + onCalculator = true + ) + + amountInput.pressMinus() + + amountInput.enterNumber( + number = "3.52", + onCalculator = true + ) + + amountInput.clickCalcSet() + amountInput.clickSet() + + transactionScreen.skipCategory() + + transactionScreen.editTitle("Calc 1") + + transactionScreen.clickAdd() + + //---------------------------------------- + + homeTab.dismissPrompt() + + //21 - 3.52 = 17.48 + homeTab.assertBalance( + amount = "-17", + amountDecimal = ".48" + ) + + homeTab.clickTransaction( + amount = "17.48", + title = "Calc 1" + ) + } + + @Test + fun setAmount_withAddition() = testWithRetry { + onboarding.quickOnboarding() + mainBottomBar.clickAddFAB() + mainBottomBar.clickAddIncome() + + amountInput.enterNumber( + number = "38.16", + autoPressNonCalculator = false + ) + amountInput.clickCalculator() + + //--------------------------- + + amountInput.pressPlus() + amountInput.enterNumber( + number = "80.74", + onCalculator = true + ) + + amountInput.clickCalcSet() + amountInput.clickSet() + + transactionScreen.skipCategory() + transactionScreen.editTitle("Calc 2") + transactionScreen.clickAdd() + + //---------------------------- + + homeTab.dismissPrompt() + + //38.16 + 80.74 = 118.90 + homeTab.assertBalance( + amount = "118", + amountDecimal = ".90" + ) + + homeTab.clickTransaction( + amount = "118.90", + title = "Calc 2" + ) + } + + @Test + fun calcAmount_viaDivision() = testWithRetry { + onboarding.quickOnboarding() + mainBottomBar.clickAddFAB() + mainBottomBar.clickAddExpense() + + amountInput.clickCalculator() + + //--------------------------- + + amountInput.enterNumber( + number = "72.50", + onCalculator = true + ) + + amountInput.pressDivision() + + amountInput.enterNumber( + number = "3", + onCalculator = true + ) + + amountInput.pressCalcEqual() + + amountInput.clickCalcSet() + amountInput.clickSet() + + transactionScreen.skipCategory() + transactionScreen.editTitle("Calc 3") + + transactionScreen.clickAdd() + //---------------------------------------- + + homeTab.dismissPrompt() + + //72.50 / 3 = 24.17 + homeTab.assertBalance( + amount = "-24", + amountDecimal = ".17" + ) + + homeTab.clickTransaction( + amount = "24.17", + title = "Calc 3" + ) + } + + @Test + fun setAmount_withMultiplication_percentDiscount() = testWithRetry { + onboarding.quickOnboarding() + mainBottomBar.clickAddFAB() + mainBottomBar.clickAddIncome() + + amountInput.enterNumber( + number = "83,000.50", + autoPressNonCalculator = false + ) + amountInput.clickCalculator() + + //--------------------------- + + amountInput.pressMultiplication() + + amountInput.enterNumber( + number = "0.9", + onCalculator = true + ) + + amountInput.clickCalcSet() + amountInput.clickSet() + + transactionScreen.skipCategory() + transactionScreen.editTitle("Calc 4") + transactionScreen.clickAdd() + + //---------------------------------------- + + homeTab.dismissPrompt() + + //83,000.50 * 0.9 = 74,700.45 + homeTab.assertBalance( + amount = "74,700", + amountDecimal = ".45" + ) + + homeTab.clickTransaction( + amount = "74,700.45", + title = "Calc 4" + ) + } + + @Test + fun calcAmount_complexExpression() = testWithRetry { + onboarding.quickOnboarding() + mainBottomBar.clickAddFAB() + mainBottomBar.clickAddExpense() + + amountInput.clickCalculator() + //--------------------------- + + //(523.90+16.7-4+2345.88)*0.9*0.7 + + amountInput.pressLeftBracket() + amountInput.enterNumber("523.90", onCalculator = true) + amountInput.pressPlus() + amountInput.enterNumber("16.7", onCalculator = true) + amountInput.pressMinus() + amountInput.enterNumber("4", onCalculator = true) + amountInput.pressPlus() + amountInput.enterNumber("2345.88", onCalculator = true) + amountInput.pressRightBracket() + amountInput.pressMultiplication() + amountInput.enterNumber("0.9", onCalculator = true) + amountInput.pressMultiplication() + amountInput.enterNumber("0.7", onCalculator = true) + + + //+ 10 = + amountInput.pressCalcEqual() + amountInput.pressPlus() + amountInput.enterNumber("10", onCalculator = true) + amountInput.pressCalcEqual() + + amountInput.clickCalcSet() + amountInput.clickSet() + transactionScreen.skipCategory() + transactionScreen.editTitle("Calc Complex") + transactionScreen.clickAdd() + + //--------------------------------------------------------- + + homeTab.dismissPrompt() + + //(523.90+16.7-4+2345.88)*0.9*0.7 = 1815.9624 ; 1815.9624 + 10; = 1,825.96 + homeTab.assertBalance( + amount = "-1,825", + amountDecimal = ".96" + ) + + homeTab.clickTransaction( + amount = "1,825.96", + title = "Calc Complex" + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt index 9110a3f926..de300cbc56 100644 --- a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt +++ b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/OperationsCoreTest.kt @@ -19,7 +19,7 @@ class OperationsCoreTest : IvyComposeTest() { private val transactionFlow = TransactionFlow(composeTestRule) private val homeTab = HomeTab(composeTestRule) private val accountsTab = AccountsTab(composeTestRule) - private val editTransactionScreen = EditTransactionScreen(composeTestRule) + private val editTransactionScreen = TransactionScreen(composeTestRule) private val itemStatisticScreen = ItemStatisticScreen(composeTestRule) private val deleteConfirmationModal = DeleteConfirmationModal(composeTestRule) diff --git a/app/src/androidTest/java/com/ivy/wallet/compose/scenario/PieChartTest.kt b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/PieChartTest.kt new file mode 100644 index 0000000000..91fe7e9e99 --- /dev/null +++ b/app/src/androidTest/java/com/ivy/wallet/compose/scenario/PieChartTest.kt @@ -0,0 +1,144 @@ +package com.ivy.wallet.compose.scenario + +import com.ivy.wallet.compose.IvyComposeTest +import com.ivy.wallet.compose.helpers.HomeTab +import com.ivy.wallet.compose.helpers.OnboardingFlow +import com.ivy.wallet.compose.helpers.PieChartScreen +import com.ivy.wallet.compose.helpers.TransactionFlow +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class PieChartTest : IvyComposeTest() { + private val onboarding = OnboardingFlow(composeTestRule) + private val homeTab = HomeTab(composeTestRule) + private val transactionFlow = TransactionFlow(composeTestRule) + private val pieChartScreen = PieChartScreen(composeTestRule) + + @Test + fun expensePieChart_realistic() = testWithRetry { + onboarding.quickOnboarding() + + transactionFlow.addExpense( + amount = 50.23 + ) + + transactionFlow.addExpense( + amount = 150.72, + category = "Food & Drinks" + ) + + transactionFlow.addExpense( + amount = 75.0, + category = "Groceries" + ) + + transactionFlow.addExpense( + amount = 5.0, + title = "Bread", + category = "Groceries" + ) + //---------------------------------------------------- + + homeTab.clickExpenseCard() + + pieChartScreen.assertTitle("Expenses") + pieChartScreen.assertTotalAmount( + amountInt = "280", + decimalPart = ".95", + currency = "USD" + ) + } + + @Test + fun expensePieChart_empty() = testWithRetry { + onboarding.quickOnboarding() + + transactionFlow.addIncome( + amount = 23.23 + ) + + //---------------------------------------------------- + + homeTab.clickExpenseCard() + + pieChartScreen.assertTitle("Expenses") + pieChartScreen.assertTotalAmount( + amountInt = "0", + decimalPart = ".00", + currency = "USD" + ) + } + + @Test + fun expensePieChart_oneTrn() = testWithRetry { + onboarding.quickOnboarding() + + transactionFlow.addExpense( + amount = 55.01 + ) + + //---------------------------------------------------- + + homeTab.clickExpenseCard() + + pieChartScreen.assertTitle("Expenses") + pieChartScreen.assertTotalAmount( + amountInt = "55", + decimalPart = ".01", + currency = "USD" + ) + } + + @Test + fun incomePieChart_realistic() = testWithRetry { + onboarding.quickOnboarding() + + //To ensure that the code filters expenses + transactionFlow.addExpense( + amount = 10.0 + ) + + transactionFlow.addIncome( + amount = 7200.0, + title = "Salary", + category = "Groceries" + ) + + transactionFlow.addIncome( + amount = 1.1, + title = "Adjust balance" + ) + + //---------------------------------------------------- + + homeTab.clickIncomeCard() + + pieChartScreen.assertTitle("Income") + pieChartScreen.assertTotalAmount( + amountInt = "7,201", + decimalPart = ".10", + currency = "USD" + ) + } + + @Test + fun incomePieChart_empty() = testWithRetry { + onboarding.quickOnboarding() + + transactionFlow.addExpense( + amount = 23.23 + ) + + //---------------------------------------------------- + + homeTab.clickIncomeCard() + + pieChartScreen.assertTitle("Income") + pieChartScreen.assertTotalAmount( + amountInt = "0", + decimalPart = ".00", + currency = "USD" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt index b647569ac1..1cf898d960 100644 --- a/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt +++ b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/DueTrnsInfoAct.kt @@ -16,8 +16,8 @@ import com.ivy.wallet.domain.pure.exchange.exchangeInBaseCurrency import com.ivy.wallet.domain.pure.transaction.expenses import com.ivy.wallet.domain.pure.transaction.incomes import com.ivy.wallet.domain.pure.transaction.sumTrns -import com.ivy.wallet.utils.timeNowUTC -import java.time.LocalDateTime +import com.ivy.wallet.utils.dateNowUTC +import java.time.LocalDate import javax.inject.Inject class DueTrnsInfoAct @Inject constructor( @@ -29,11 +29,11 @@ class DueTrnsInfoAct @Inject constructor( override suspend fun Input.compose(): suspend () -> Output = suspend { range } then dueTrnsAct then { trns -> - val timeNow = timeNowUTC() + val dateNow = dateNowUTC() trns.filter { - this.dueFilter(it, timeNow) + this.dueFilter(it, dateNow) } - } then { upcomingTrns -> + } then { dueTrns -> //We have due transactions in different currencies val exchangeArg = ExchangeTrnArgument( baseCurrency = baseCurrency, @@ -45,17 +45,17 @@ class DueTrnsInfoAct @Inject constructor( Output( dueIncomeExpense = IncomeExpensePair( income = sumTrns( - incomes(upcomingTrns), + incomes(dueTrns), ::exchangeInBaseCurrency, exchangeArg ), expense = sumTrns( - expenses(upcomingTrns), + expenses(dueTrns), ::exchangeInBaseCurrency, exchangeArg ) ), - dueTrns = upcomingTrns + dueTrns = dueTrns ) } } @@ -63,7 +63,7 @@ class DueTrnsInfoAct @Inject constructor( data class Input( val range: ClosedTimeRange, val baseCurrency: String, - val dueFilter: (Transaction, LocalDateTime) -> Boolean + val dueFilter: (Transaction, LocalDate) -> Boolean ) data class Output( diff --git a/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/OverdueAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/OverdueAct.kt index b1f5075d57..2461d55788 100644 --- a/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/OverdueAct.kt +++ b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/home/OverdueAct.kt @@ -6,6 +6,8 @@ import com.ivy.wallet.domain.data.core.Transaction import com.ivy.wallet.domain.pure.data.ClosedTimeRange import com.ivy.wallet.domain.pure.data.IncomeExpensePair import com.ivy.wallet.domain.pure.transaction.isOverdue +import com.ivy.wallet.utils.beginningOfIvyTime +import java.time.LocalDateTime import javax.inject.Inject class OverdueAct @Inject constructor( @@ -14,7 +16,10 @@ class OverdueAct @Inject constructor( override suspend fun Input.compose(): suspend () -> Output = suspend { DueTrnsInfoAct.Input( - range = range, + range = ClosedTimeRange( + from = beginningOfIvyTime(), + to = toRange + ), baseCurrency = baseCurrency, dueFilter = ::isOverdue ) @@ -26,7 +31,7 @@ class OverdueAct @Inject constructor( } data class Input( - val range: ClosedTimeRange, + val toRange: LocalDateTime, val baseCurrency: String ) diff --git a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/AccountCreator.kt b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/AccountCreator.kt index f4b099f6c3..d744d003bf 100644 --- a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/AccountCreator.kt +++ b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/AccountCreator.kt @@ -5,6 +5,7 @@ import com.ivy.wallet.domain.data.core.Account import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData import com.ivy.wallet.domain.deprecated.sync.item.TransactionSync import com.ivy.wallet.domain.deprecated.sync.uploader.AccountUploader +import com.ivy.wallet.domain.pure.util.nextOrderNum import com.ivy.wallet.io.persistence.dao.AccountDao import com.ivy.wallet.utils.ioThread @@ -33,7 +34,7 @@ class AccountCreator( color = data.color.toArgb(), icon = data.icon, includeInBalance = data.includeBalance, - orderNum = accountDao.findMaxOrderNum() + 1.0, + orderNum = accountDao.findMaxOrderNum().nextOrderNum(), isSynced = false ) accountDao.save(account.toEntity()) diff --git a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/BudgetCreator.kt b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/BudgetCreator.kt index 664e734120..18c52faac1 100644 --- a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/BudgetCreator.kt +++ b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/BudgetCreator.kt @@ -3,6 +3,7 @@ package com.ivy.wallet.domain.deprecated.logic import com.ivy.wallet.domain.data.core.Budget import com.ivy.wallet.domain.deprecated.logic.model.CreateBudgetData import com.ivy.wallet.domain.deprecated.sync.uploader.BudgetUploader +import com.ivy.wallet.domain.pure.util.nextOrderNum import com.ivy.wallet.io.persistence.dao.BudgetDao import com.ivy.wallet.utils.ioThread @@ -29,7 +30,7 @@ class BudgetCreator( amount = data.amount, categoryIdsSerialized = data.categoryIdsSerialized, accountIdsSerialized = data.accountIdsSerialized, - orderId = budgetDao.findMaxOrderNum() + 1, + orderId = budgetDao.findMaxOrderNum().nextOrderNum(), isSynced = false ) diff --git a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/CategoryCreator.kt b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/CategoryCreator.kt index e66343d983..0f356c4da8 100644 --- a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/CategoryCreator.kt +++ b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/CategoryCreator.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.graphics.toArgb import com.ivy.wallet.domain.data.core.Category import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData import com.ivy.wallet.domain.deprecated.sync.uploader.CategoryUploader +import com.ivy.wallet.domain.pure.util.nextOrderNum import com.ivy.wallet.io.persistence.dao.CategoryDao import com.ivy.wallet.utils.ioThread @@ -28,7 +29,7 @@ class CategoryCreator( name = name.trim(), color = data.color.toArgb(), icon = data.icon, - orderNum = categoryDao.findMaxOrderNum() + 1, + orderNum = categoryDao.findMaxOrderNum().nextOrderNum(), isSynced = false ) diff --git a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/LoanCreator.kt b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/LoanCreator.kt index 37719e297c..1d65906833 100644 --- a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/LoanCreator.kt +++ b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/LoanCreator.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.graphics.toArgb import com.ivy.wallet.domain.data.core.Loan import com.ivy.wallet.domain.deprecated.logic.model.CreateLoanData import com.ivy.wallet.domain.deprecated.sync.uploader.LoanUploader +import com.ivy.wallet.domain.pure.util.nextOrderNum import com.ivy.wallet.io.persistence.dao.LoanDao import com.ivy.wallet.utils.ioThread import java.util.* @@ -34,7 +35,7 @@ class LoanCreator( type = data.type, color = data.color.toArgb(), icon = data.icon, - orderNum = dao.findMaxOrderNum() + 1, + orderNum = dao.findMaxOrderNum().nextOrderNum(), isSynced = false, accountId = data.account?.id ) diff --git a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/csv/CSVImporter.kt b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/csv/CSVImporter.kt index 3fdc40ab26..9389b9dff5 100644 --- a/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/csv/CSVImporter.kt +++ b/app/src/main/java/com/ivy/wallet/domain/deprecated/logic/csv/CSVImporter.kt @@ -9,6 +9,7 @@ import com.ivy.wallet.domain.data.core.Transaction import com.ivy.wallet.domain.deprecated.logic.csv.model.CSVRow import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportResult import com.ivy.wallet.domain.deprecated.logic.csv.model.RowMapping +import com.ivy.wallet.domain.pure.util.nextOrderNum import com.ivy.wallet.io.persistence.dao.AccountDao import com.ivy.wallet.io.persistence.dao.CategoryDao import com.ivy.wallet.io.persistence.dao.SettingsDao @@ -442,7 +443,7 @@ class CSVImporter( ), color = colorArgb, icon = icon, - orderNum = orderNum ?: accountDao.findMaxOrderNum() + 1 + orderNum = orderNum ?: accountDao.findMaxOrderNum().nextOrderNum() ) accountDao.save(newAccount.toEntity()) accounts = accountDao.findAll().map { it.toDomain() } @@ -491,7 +492,7 @@ class CSVImporter( name = categoryNameString, color = colorArgb, icon = icon, - orderNum = orderNum ?: categoryDao.findMaxOrderNum() + 1 + orderNum = orderNum ?: categoryDao.findMaxOrderNum().nextOrderNum() ) categoryDao.save(newCategory.toEntity()) categories = categoryDao.findAll().map { it.toDomain() } diff --git a/app/src/main/java/com/ivy/wallet/domain/pure/account/AccountFunctions.kt b/app/src/main/java/com/ivy/wallet/domain/pure/account/AccountFunctions.kt index 8ce07dff3b..755b01446b 100644 --- a/app/src/main/java/com/ivy/wallet/domain/pure/account/AccountFunctions.kt +++ b/app/src/main/java/com/ivy/wallet/domain/pure/account/AccountFunctions.kt @@ -3,4 +3,7 @@ package com.ivy.wallet.domain.pure.account import com.ivy.wallet.domain.data.core.Account fun filterExcluded(accounts: List): List = - accounts.filter { it.includeInBalance } \ No newline at end of file + accounts.filter { it.includeInBalance } + +fun accountCurrency(account: Account, baseCurrency: String): String = + account.currency ?: baseCurrency \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/domain/pure/exchange/ExchangeTrns.kt b/app/src/main/java/com/ivy/wallet/domain/pure/exchange/ExchangeTrns.kt index e2788c4551..0e18a19eb5 100644 --- a/app/src/main/java/com/ivy/wallet/domain/pure/exchange/ExchangeTrns.kt +++ b/app/src/main/java/com/ivy/wallet/domain/pure/exchange/ExchangeTrns.kt @@ -6,6 +6,7 @@ import com.ivy.fp.Pure import com.ivy.fp.SideEffect import com.ivy.wallet.domain.data.core.Account import com.ivy.wallet.domain.data.core.Transaction +import com.ivy.wallet.domain.pure.account.accountCurrency import com.ivy.wallet.domain.pure.transaction.trnCurrency import java.math.BigDecimal import java.util.* @@ -25,7 +26,9 @@ suspend fun exchangeInBaseCurrency( transaction: Transaction, arg: ExchangeTrnArgument ): BigDecimal { - val fromCurrency = arg.getAccount(transaction.accountId)?.currency.toOption() + val fromCurrency = arg.getAccount(transaction.accountId)?.let { + accountCurrency(it, arg.baseCurrency) + }.toOption() return exchangeInCurrency( transaction = transaction, diff --git a/app/src/main/java/com/ivy/wallet/domain/pure/transaction/TrnFunctions.kt b/app/src/main/java/com/ivy/wallet/domain/pure/transaction/TrnFunctions.kt index 83605bc487..b8b4a694f0 100644 --- a/app/src/main/java/com/ivy/wallet/domain/pure/transaction/TrnFunctions.kt +++ b/app/src/main/java/com/ivy/wallet/domain/pure/transaction/TrnFunctions.kt @@ -6,7 +6,8 @@ import com.ivy.fp.Pure import com.ivy.wallet.domain.data.TransactionType import com.ivy.wallet.domain.data.core.Account import com.ivy.wallet.domain.data.core.Transaction -import java.time.LocalDateTime +import com.ivy.wallet.domain.pure.account.accountCurrency +import java.time.LocalDate @Pure fun expenses(transactions: List): List { @@ -24,23 +25,24 @@ fun transfers(transactions: List): List { } @Pure -fun isUpcoming(transaction: Transaction, timeNowUTC: LocalDateTime): Boolean = - timeNowUTC.isBefore(transaction.dueDate) - -@Pure -fun isOverdue(transaction: Transaction, timeNowUTC: LocalDateTime): Boolean = - timeNowUTC.isAfter(transaction.dueDate) +fun isUpcoming(transaction: Transaction, dateNow: LocalDate): Boolean { + val dueDate = transaction.dueDate?.toLocalDate() ?: return false + return dateNow.isBefore(dueDate) || dateNow.isEqual(dueDate) +} @Pure -fun trnCurrency( - transaction: Transaction, - accounts: List -): Option = accounts.find { it.id == transaction.accountId }?.currency.toOption() +fun isOverdue(transaction: Transaction, dateNow: LocalDate): Boolean { + val dueDate = transaction.dueDate?.toLocalDate() ?: return false + return dateNow.isAfter(dueDate) +} @Pure fun trnCurrency( transaction: Transaction, accounts: List, baseCurrency: String -): Option = - ((accounts.find { it.id == transaction.accountId }?.currency) ?: baseCurrency).toOption() \ No newline at end of file +): Option { + val account = accounts.find { it.id == transaction.accountId } + ?: return baseCurrency.toOption() + return accountCurrency(account, baseCurrency).toOption() +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/domain/pure/util/IvyDomainUtils.kt b/app/src/main/java/com/ivy/wallet/domain/pure/util/IvyDomainUtils.kt new file mode 100644 index 0000000000..2b1eb487e9 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/domain/pure/util/IvyDomainUtils.kt @@ -0,0 +1,3 @@ +package com.ivy.wallet.domain.pure.util + +fun Double?.nextOrderNum(): Double = this?.plus(1) ?: 0.0 \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/io/network/data/CategoryDTO.kt b/app/src/main/java/com/ivy/wallet/io/network/data/CategoryDTO.kt index a013b9de29..7446f8184e 100644 --- a/app/src/main/java/com/ivy/wallet/io/network/data/CategoryDTO.kt +++ b/app/src/main/java/com/ivy/wallet/io/network/data/CategoryDTO.kt @@ -19,6 +19,7 @@ data class CategoryDTO( icon = icon, orderNum = orderNum, isSynced = true, - isDeleted = false + isDeleted = false, + id = id ) } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/io/persistence/dao/AccountDao.kt b/app/src/main/java/com/ivy/wallet/io/persistence/dao/AccountDao.kt index 4145ca297d..8dee90628c 100644 --- a/app/src/main/java/com/ivy/wallet/io/persistence/dao/AccountDao.kt +++ b/app/src/main/java/com/ivy/wallet/io/persistence/dao/AccountDao.kt @@ -37,5 +37,5 @@ interface AccountDao { suspend fun findMinOrderNum(): Double @Query("SELECT MAX(orderNum) FROM accounts") - suspend fun findMaxOrderNum(): Double + suspend fun findMaxOrderNum(): Double? } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/io/persistence/dao/BudgetDao.kt b/app/src/main/java/com/ivy/wallet/io/persistence/dao/BudgetDao.kt index ecae068e50..b04d7e2ae6 100644 --- a/app/src/main/java/com/ivy/wallet/io/persistence/dao/BudgetDao.kt +++ b/app/src/main/java/com/ivy/wallet/io/persistence/dao/BudgetDao.kt @@ -34,5 +34,5 @@ interface BudgetDao { suspend fun deleteAll() @Query("SELECT MAX(orderId) FROM budgets") - suspend fun findMaxOrderNum(): Double + suspend fun findMaxOrderNum(): Double? } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/io/persistence/dao/CategoryDao.kt b/app/src/main/java/com/ivy/wallet/io/persistence/dao/CategoryDao.kt index db01b463a4..5055935921 100644 --- a/app/src/main/java/com/ivy/wallet/io/persistence/dao/CategoryDao.kt +++ b/app/src/main/java/com/ivy/wallet/io/persistence/dao/CategoryDao.kt @@ -34,5 +34,5 @@ interface CategoryDao { suspend fun deleteAll() @Query("SELECT MAX(orderNum) FROM categories") - suspend fun findMaxOrderNum(): Double + suspend fun findMaxOrderNum(): Double? } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/io/persistence/dao/LoanDao.kt b/app/src/main/java/com/ivy/wallet/io/persistence/dao/LoanDao.kt index e0fda32466..e4cfd76991 100644 --- a/app/src/main/java/com/ivy/wallet/io/persistence/dao/LoanDao.kt +++ b/app/src/main/java/com/ivy/wallet/io/persistence/dao/LoanDao.kt @@ -34,5 +34,5 @@ interface LoanDao { suspend fun deleteAll() @Query("SELECT MAX(orderNum) FROM loans") - suspend fun findMaxOrderNum(): Double + suspend fun findMaxOrderNum(): Double? } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyComposeApp.kt b/app/src/main/java/com/ivy/wallet/ui/IvyComposeApp.kt index 179a63a434..0cb46087ac 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyComposeApp.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyComposeApp.kt @@ -1,5 +1,6 @@ package com.ivy.wallet.ui +import android.view.View import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -8,6 +9,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import com.ivy.design.IvyContext import com.ivy.design.api.IvyDesign import com.ivy.design.api.NavigationRoot @@ -17,12 +20,17 @@ import com.ivy.design.l0_system.Theme import com.ivy.design.l0_system.UI import com.ivy.design.navigation.Navigation import com.ivy.design.utils.IvyPreview +import com.ivy.wallet.IvyAndroidApp @Composable -fun ivyWalletCtx(): IvyWalletCtx { - return ivyContext() as IvyWalletCtx -} +fun ivyWalletCtx(): IvyWalletCtx = ivyContext() as IvyWalletCtx + +@Composable +fun rootView(): View = LocalView.current + +@Composable +fun rootActivity(): RootActivity = LocalContext.current as RootActivity fun appDesign(context: IvyWalletCtx): IvyDesign = object : IvyWalletDesign() { override fun context(): IvyContext = context @@ -52,6 +60,7 @@ fun IvyWalletPreview( theme: Theme = Theme.LIGHT, Content: @Composable BoxWithConstraintsScope.() -> Unit ) { + IvyAndroidApp.appContext = rootView().context IvyPreview( theme = theme, design = appDesign(IvyWalletCtx()), 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 632c72f631..d8eed6d4aa 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.lifecycle.viewmodel.compose.viewModel @@ -531,9 +530,4 @@ class RootActivity : AppCompatActivity() { val addTransactionWidget = ComponentName(this, widget) appWidgetManager.requestPinAppWidget(addTransactionWidget, null, null) } -} - -@Composable -fun rootActivity(): RootActivity { - return LocalContext.current as RootActivity } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/budget/BudgetViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/budget/BudgetViewModel.kt index 820b2749ec..ddb8a84589 100644 --- a/app/src/main/java/com/ivy/wallet/ui/budget/BudgetViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/budget/BudgetViewModel.kt @@ -145,7 +145,7 @@ class BudgetViewModel @Inject constructor( ExchangeAct.Input( data = ExchangeData( baseCurrency = baseCurrencyCode, - fromCurrency = trnCurrency(it, accounts) + fromCurrency = trnCurrency(it, accounts, baseCurrencyCode) ), amount = it.amount ) diff --git a/app/src/main/java/com/ivy/wallet/ui/csvimport/ImportViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/csvimport/ImportViewModel.kt index 5cec1d883a..4d278ea971 100644 --- a/app/src/main/java/com/ivy/wallet/ui/csvimport/ImportViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/csvimport/ImportViewModel.kt @@ -16,10 +16,7 @@ import com.ivy.wallet.domain.deprecated.logic.zip.ExportZipLogic import com.ivy.wallet.ui.Import import com.ivy.wallet.ui.IvyWalletCtx import com.ivy.wallet.ui.onboarding.viewmodel.OnboardingViewModel -import com.ivy.wallet.utils.TestIdlingResource -import com.ivy.wallet.utils.asLiveData -import com.ivy.wallet.utils.ioThread -import com.ivy.wallet.utils.uiThread +import com.ivy.wallet.utils.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import timber.log.Timber @@ -79,7 +76,7 @@ class ImportViewModel @Inject constructor( _importStep.value = ImportStep.LOADING - _importResult.value = if (hasCSVExtension(fileUri)) + _importResult.value = if (hasCSVExtension(context, fileUri)) restoreCSVFile(fileUri = fileUri, importType = importType) else { exportZipLogic.import( @@ -196,10 +193,11 @@ class ImportViewModel @Inject constructor( _importStep.value = ImportStep.IMPORT_FROM } - private fun hasCSVExtension(fileUri: Uri): Boolean { - var ex = fileUri.toString() - ex = ex.substring(ex.lastIndexOf(".")) - - return ex.equals(".csv", ignoreCase = true) + private suspend fun hasCSVExtension( + context: Context, + fileUri: Uri + ): Boolean = ioThread { + val fileName = context.getFileName(fileUri) + fileName?.endsWith(suffix = ".csv", ignoreCase = true) ?: false } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt b/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt index 6059431725..45705eb179 100644 --- a/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/edit/EditTransactionScreen.kt @@ -22,6 +22,7 @@ import com.google.accompanist.insets.statusBarsPadding import com.ivy.design.api.navigation import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style +import com.ivy.design.utils.hideKeyboard import com.ivy.wallet.R import com.ivy.wallet.domain.data.CustomExchangeRateState import com.ivy.wallet.domain.data.TransactionType @@ -29,11 +30,8 @@ import com.ivy.wallet.domain.data.core.Account import com.ivy.wallet.domain.data.core.Category import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData -import com.ivy.wallet.ui.EditPlanned -import com.ivy.wallet.ui.EditTransaction -import com.ivy.wallet.ui.IvyWalletPreview +import com.ivy.wallet.ui.* import com.ivy.wallet.ui.edit.core.* -import com.ivy.wallet.ui.ivyWalletCtx import com.ivy.wallet.ui.loan.data.EditTransactionDisplayLoan import com.ivy.wallet.ui.theme.components.AddPrimaryAttributeButton import com.ivy.wallet.ui.theme.components.ChangeTransactionTypeModal @@ -77,6 +75,8 @@ fun BoxWithConstraintsScope.EditTransactionScreen(screen: EditTransaction) { viewModel.start(screen) } + val view = rootView() + UI( screen = screen, transactionType = transactionType, @@ -112,7 +112,10 @@ fun BoxWithConstraintsScope.EditTransactionScreen(screen: EditTransaction) { onCreateCategory = viewModel::createCategory, onEditCategory = viewModel::editCategory, onPayPlannedPayment = viewModel::onPayPlannedPayment, - onSave = viewModel::save, + onSave = { + view.hideKeyboard() + viewModel.save() + }, onSetHasChanges = viewModel::setHasChanges, onDelete = viewModel::delete, onCreateAccount = viewModel::createAccount, diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt index baf16d0f45..d4ed55d3f5 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt @@ -271,7 +271,8 @@ private fun IncomeExpenses( textColor = White, label = stringResource(R.string.income), currency = currency, - amount = monthlyIncome + amount = monthlyIncome, + testTag = "home_card_income" ) { nav.navigateTo( PieChartStatistic( @@ -289,7 +290,8 @@ private fun IncomeExpenses( textColor = UI.colors.pure, label = stringResource(R.string.expenses), currency = currency, - amount = monthlyExpenses.absoluteValue + amount = monthlyExpenses.absoluteValue, + testTag = "home_card_expense" ) { nav.navigateTo( PieChartStatistic( @@ -311,6 +313,7 @@ private fun RowScope.HeaderCard( label: String, currency: String, amount: Double, + testTag: String, onClick: () -> Unit ) { Column( @@ -321,6 +324,7 @@ private fun RowScope.HeaderCard( } .clip(UI.shapes.r4) .background(backgroundGradient.asHorizontalBrush()) + .testTag(testTag) .clickable( onClick = onClick ) diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt index a609de10e7..c92762e17f 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt @@ -159,7 +159,7 @@ class HomeViewModel @Inject constructor( updateState { val result = overdueAct( OverdueAct.Input( - range = timeRange, + toRange = timeRange.to, baseCurrency = baseCurrency ) ) diff --git a/app/src/main/java/com/ivy/wallet/ui/reports/ReportViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/reports/ReportViewModel.kt index f19bee7620..b7b9caa6c1 100644 --- a/app/src/main/java/com/ivy/wallet/ui/reports/ReportViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/reports/ReportViewModel.kt @@ -244,7 +244,7 @@ class ReportViewModel @Inject constructor( ExchangeAct.Input( data = ExchangeData( baseCurrency = baseCurrency, - fromCurrency = trnCurrency(it, accounts), + fromCurrency = trnCurrency(it, accounts, baseCurrency), ), amount = it.amount ) @@ -328,7 +328,7 @@ class ReportViewModel @Inject constructor( ExchangeAct.Input( data = ExchangeData( baseCurrency = baseCurrency, - fromCurrency = trnCurrency(trn, accounts), + fromCurrency = trnCurrency(trn, accounts, baseCurrency), ), amount = trn.amount ) @@ -346,7 +346,7 @@ class ReportViewModel @Inject constructor( ExchangeAct.Input( data = ExchangeData( baseCurrency = baseCurrency, - fromCurrency = trnCurrency(trn, accounts), + fromCurrency = trnCurrency(trn, accounts, baseCurrency), ), amount = trn.amount ) 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 819225a939..4afe00dd58 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 @@ -20,6 +20,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel @@ -29,6 +30,8 @@ import com.google.accompanist.insets.statusBarsPadding import com.ivy.design.api.navigation import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style +import com.ivy.design.l1_buildingBlocks.IconScale +import com.ivy.design.l1_buildingBlocks.IvyIconScaled import com.ivy.wallet.BuildConfig import com.ivy.wallet.Constants import com.ivy.wallet.Constants.URL_IVY_CONTRIBUTORS @@ -39,7 +42,6 @@ import com.ivy.wallet.domain.data.core.User import com.ivy.wallet.ui.* import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.components.IvyButton -import com.ivy.wallet.ui.theme.components.IvyIcon import com.ivy.wallet.ui.theme.components.IvySwitch import com.ivy.wallet.ui.theme.components.IvyToolbar import com.ivy.wallet.ui.theme.modal.* @@ -122,7 +124,7 @@ private fun BoxWithConstraintsScope.UI( showNotifications: Boolean = true, hideCurrentBalance: Boolean = false, progressState: Boolean = false, - treatTransfersAsIncomeExpense :Boolean = false, + treatTransfersAsIncomeExpense: Boolean = false, nameLocalAccount: String?, startDateOfMonth: Int = 1, @@ -228,8 +230,9 @@ private fun BoxWithConstraintsScope.UI( Spacer(Modifier.height(12.dp)) SettingsDefaultButton( - icon = R.drawable.ic_export_csv, + icon = R.drawable.ic_vue_security_shield, text = stringResource(R.string.backup_data), + iconPadding = 6.dp ) { onBackupData() } @@ -415,9 +418,11 @@ private fun BoxWithConstraintsScope.UI( DeleteModal( title = stringResource(R.string.delete_all_user_data_question), - description = stringResource(R.string.delete_all_user_data_warning, user?.email ?: stringResource( - R.string.your_account) - ), + description = stringResource( + R.string.delete_all_user_data_warning, user?.email ?: stringResource( + R.string.your_account + ) + ), visible = deleteAllDataModalVisible, dismiss = { deleteAllDataModalVisible = false }, onDelete = { @@ -427,9 +432,11 @@ private fun BoxWithConstraintsScope.UI( ) DeleteModal( - title = stringResource(R.string.confirm_all_userd_data_deletion, user?.email ?: stringResource( - R.string.all_of_your_data) - ), + title = stringResource( + R.string.confirm_all_userd_data_deletion, user?.email ?: stringResource( + R.string.all_of_your_data + ) + ), description = stringResource(R.string.final_deletion_warning), visible = deleteAllDataModalFinalVisible, dismiss = { deleteAllDataModalFinalVisible = false }, @@ -453,14 +460,13 @@ private fun StartDateOfMonth( SettingsButtonRow( onClick = onClick ) { - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(12.dp)) - IvyIcon( - modifier = Modifier - .size(48.dp) - .padding(all = 4.dp), + IvyIconScaled( icon = R.drawable.ic_custom_calendar_m, - tint = UI.colors.pureInverse + tint = UI.colors.pureInverse, + iconScale = IconScale.M, + padding = 0.dp ) Spacer(Modifier.width(8.dp)) @@ -496,6 +502,7 @@ private fun IvyTelegram() { icon = R.drawable.ic_telegram_24dp, text = stringResource(R.string.ivy_telegram), backgroundGradient = Gradient.solid(Blue), + iconPadding = 8.dp ) { rootActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE) } @@ -541,12 +548,12 @@ private fun RequestFeature( @Composable private fun ContactSupport() { - val ivyActivity = LocalContext.current as RootActivity + val rootActivity = rootActivity() SettingsDefaultButton( icon = R.drawable.ic_support, text = stringResource(R.string.contact_support), ) { - ivyActivity.contactSupport() + rootActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE) } } @@ -554,8 +561,9 @@ private fun ContactSupport() { private fun ProjectContributors() { val nav = navigation() SettingsDefaultButton( - icon = R.drawable.ic_custom_people_m, + icon = R.drawable.ic_vue_people_people, text = stringResource(R.string.project_contributors), + iconPadding = 6.dp ) { nav.navigateTo( IvyWebView(url = URL_IVY_CONTRIBUTORS) @@ -576,11 +584,13 @@ private fun AppSwitch( onSetLockApp(!lockApp) } ) { - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(12.dp)) - IvyIcon( + IvyIconScaled( icon = icon, - tint = UI.colors.pureInverse + tint = UI.colors.pureInverse, + iconScale = IconScale.M, + padding = 0.dp ) Spacer(Modifier.width(8.dp)) @@ -588,7 +598,8 @@ private fun AppSwitch( Column( Modifier .weight(1f) - .padding(top = 20.dp, bottom = 20.dp, end = 8.dp)) { + .padding(top = 20.dp, bottom = 20.dp, end = 8.dp) + ) { Text( text = text, style = UI.typo.b2.style( @@ -739,10 +750,12 @@ private fun AccountCardUser( Row( verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(24.dp)) + Spacer(Modifier.width(20.dp)) - IvyIcon( - icon = R.drawable.ic_email + IvyIconScaled( + icon = R.drawable.ic_email, + iconScale = IconScale.S, + padding = 0.dp ) Spacer(Modifier.width(12.dp)) @@ -765,11 +778,13 @@ private fun AccountCardUser( Row( verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(24.dp)) + Spacer(Modifier.width(20.dp)) - IvyIcon( + IvyIconScaled( icon = R.drawable.ic_data_synced, - tint = Orange + tint = Orange, + iconScale = IconScale.S, + padding = 0.dp ) Spacer(Modifier.width(12.dp)) @@ -791,11 +806,13 @@ private fun AccountCardUser( Row( verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(24.dp)) + Spacer(Modifier.width(20.dp)) - IvyIcon( + IvyIconScaled( icon = R.drawable.ic_data_synced, - tint = Green + tint = Green, + iconScale = IconScale.S, + padding = 0.dp ) Spacer(Modifier.width(12.dp)) @@ -848,7 +865,10 @@ private fun AccountCardLocalAccount( ) { Spacer(Modifier.width(20.dp)) - IvyIcon(icon = R.drawable.ic_local_account) + IvyIconScaled( + icon = R.drawable.ic_local_account, + iconScale = IconScale.M + ) Spacer(Modifier.width(12.dp)) @@ -886,8 +906,9 @@ private fun ExportCSV( onExportToCSV: () -> Unit ) { SettingsDefaultButton( - icon = R.drawable.ic_export_csv, + icon = R.drawable.ic_vue_pc_printer, text = stringResource(R.string.export_to_csv), + iconPadding = 6.dp ) { onExportToCSV() } @@ -951,6 +972,7 @@ private fun SettingsPrimaryButton( hasShadow: Boolean = false, backgroundGradient: Gradient = Gradient.solid(UI.colors.medium), textColor: Color = White, + iconPadding: Dp = 0.dp, onClick: () -> Unit ) { SettingsButtonRow( @@ -958,11 +980,13 @@ private fun SettingsPrimaryButton( backgroundGradient = backgroundGradient, onClick = onClick ) { - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(12.dp)) - IvyIcon( + IvyIconScaled( icon = icon, - tint = textColor + tint = textColor, + iconScale = IconScale.M, + padding = iconPadding ) Spacer(Modifier.width(8.dp)) @@ -1022,8 +1046,9 @@ private fun AccountCardButton( ) { Spacer(Modifier.width(12.dp)) - IvyIcon( - icon = icon + IvyIconScaled( + icon = icon, + iconScale = IconScale.M ) Spacer(Modifier.width(4.dp)) @@ -1058,9 +1083,13 @@ private fun CurrencyButton( }, verticalAlignment = Alignment.CenterVertically ) { - Spacer(Modifier.width(20.dp)) + Spacer(Modifier.width(12.dp)) - IvyIcon(icon = R.drawable.ic_currency) + IvyIconScaled( + icon = R.drawable.ic_currency, + iconScale = IconScale.M, + padding = 0.dp + ) Spacer(Modifier.width(8.dp)) @@ -1085,7 +1114,10 @@ private fun CurrencyButton( Spacer(Modifier.height(4.dp)) - IvyIcon(icon = R.drawable.ic_arrow_right) + IvyIconScaled( + icon = R.drawable.ic_arrow_right, + iconScale = IconScale.M + ) Spacer(Modifier.width(24.dp)) } @@ -1112,13 +1144,15 @@ private fun SettingsSectionDivider( private fun SettingsDefaultButton( @DrawableRes icon: Int, text: String, + iconPadding: Dp = 0.dp, onClick: () -> Unit ) { SettingsPrimaryButton( icon = icon, text = text, backgroundGradient = Gradient.solid(UI.colors.medium), - textColor = UI.colors.pureInverse + textColor = UI.colors.pureInverse, + iconPadding = iconPadding ) { onClick() } diff --git a/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticScreen.kt b/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticScreen.kt index 2801de6c31..cef77bbaf0 100644 --- a/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -118,7 +119,9 @@ private fun BoxWithConstraintsScope.UI( Spacer(Modifier.height(20.dp)) Text( - modifier = Modifier.padding(start = 32.dp), + modifier = Modifier + .padding(start = 32.dp) + .testTag("piechart_title"), text = if (state.transactionType == TransactionType.EXPENSE) stringResource(R.string.expenses) else stringResource( R.string.income ), @@ -130,6 +133,7 @@ private fun BoxWithConstraintsScope.UI( BalanceRow( modifier = Modifier .padding(start = 32.dp, end = 16.dp) + .testTag("piechart_total_amount") .alpha(percentExpanded), currency = state.baseCurrency, balance = state.totalAmount, diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/components/BalanceRow.kt b/app/src/main/java/com/ivy/wallet/ui/theme/components/BalanceRow.kt index 7a7cbedd2c..616d27b42d 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/components/BalanceRow.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/components/BalanceRow.kt @@ -22,7 +22,6 @@ import com.ivy.wallet.utils.decimalPartFormatted import com.ivy.wallet.utils.shortenAmount import com.ivy.wallet.utils.shouldShortAmount import java.text.DecimalFormat -import kotlin.math.truncate @Composable fun BalanceRowMedium( @@ -121,10 +120,12 @@ fun BalanceRow( Spacer(Modifier.width(spacerCurrency)) } + val balancePrecise = balance.toBigDecimal() + val integerPartFormatted = if (shortAmount) { shortenAmount(balance) } else { - DecimalFormat("###,###").format(truncate(balance)) + DecimalFormat("###,###").format(balancePrecise.toInt()) } Text( text = when { diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/components/ItemIcon.kt b/app/src/main/java/com/ivy/wallet/ui/theme/components/ItemIcon.kt index 4674998ffd..d1c6f0598f 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/components/ItemIcon.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/components/ItemIcon.kt @@ -3,14 +3,20 @@ package com.ivy.wallet.ui.theme.components import android.content.Context import androidx.annotation.DrawableRes import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.ivy.design.l0_system.UI +import com.ivy.design.utils.thenWhen import com.ivy.wallet.ui.IvyWalletComponentPreview import com.ivy.wallet.utils.toLowerCaseLocal @@ -23,7 +29,8 @@ fun ItemIconL( Default: (@Composable () -> Unit)? = null ) { ItemIcon( - modifier = modifier, + modifier = modifier + .size(64.dp), size = "l", iconName = iconName, tint = tint, @@ -61,7 +68,8 @@ fun ItemIconM( Default: (@Composable () -> Unit)? = null ) { ItemIcon( - modifier = modifier, + modifier = modifier + .size(48.dp), size = "m", iconName = iconName, tint = tint, @@ -99,7 +107,8 @@ fun ItemIconS( Default: (@Composable () -> Unit)? = null ) { ItemIcon( - modifier = modifier, + modifier = modifier + .size(32.dp), size = "s", iconName = iconName, tint = tint, @@ -116,18 +125,41 @@ private fun ItemIcon( Default: (@Composable () -> Unit)? = null ) { val context = LocalContext.current - val iconId = getCustomIconId( + val iconInfo = getCustomIconId( context = context, iconName = iconName, size = size ) - if (iconId != null) { + if (iconInfo != null) { Image( - modifier = modifier, - painter = painterResource(id = iconId), + modifier = modifier + .thenWhen { + if (!iconInfo.newFormat) { + //do nothing for the old format of icons + return@thenWhen this + } + + when (iconInfo.style) { + IconStyle.L -> + //64.dp - 48.dp = 16.dp / 4 = 4.dp + this.padding(all = 4.dp) + IconStyle.M -> + //48.dp - 32.dp = 16.dp / 4 = 4.dp + this.padding(all = 4.dp) + IconStyle.S -> + //32.dp - 24.dp = 8.dp / 4 = 2.dp + //2.dp is too small padding + this.padding(all = 4.dp) + IconStyle.UNKNOWN -> this + } + }, + painter = painterResource(id = iconInfo.iconId), colorFilter = ColorFilter.tint(tint), - contentDescription = "item icon" + alignment = Alignment.Center, + contentScale = if (iconInfo.newFormat) + ContentScale.Fit else ContentScale.None, + contentDescription = iconName ?: "item icon" ) } else { Default?.invoke() @@ -145,7 +177,7 @@ fun getCustomIconIdS( context = context, iconName = iconName, size = "s" - ) ?: defaultIcon + )?.iconId ?: defaultIcon } @DrawableRes @@ -159,7 +191,7 @@ fun getCustomIconIdM( context = context, iconName = iconName, size = "m" - ) ?: defaultIcon + )?.iconId ?: defaultIcon } @DrawableRes @@ -173,26 +205,91 @@ fun getCustomIconIdL( context = context, iconName = iconName, size = "l" - ) ?: defaultIcon + )?.iconId ?: defaultIcon } -@DrawableRes fun getCustomIconId( context: Context, iconName: String?, size: String, -): Int? { +): IconInfo? { + val iconStyle = when (size) { + "l" -> IconStyle.L + "m" -> IconStyle.M + "s" -> IconStyle.S + else -> IconStyle.UNKNOWN + } + return iconName?.let { try { val iconNameNormalized = iconName .replace(" ", "") .trim() .toLowerCaseLocal() - context.resources.getIdentifier( + + val itemId = context.resources.getIdentifier( "ic_custom_${iconNameNormalized}_${size}", "drawable", context.packageName ).takeIf { it != 0 } + + itemId?.let { nonNullId -> + IconInfo( + iconId = nonNullId, + style = iconStyle, + newFormat = false + ) + } ?: fallbackToNewIconFormat( + context = context, + iconName = iconName, + iconStyle = iconStyle + ) + } catch (e: Exception) { + fallbackToNewIconFormat( + context = context, + iconName = iconName, + iconStyle = iconStyle + ) + } + } +} + +data class IconInfo( + @DrawableRes + val iconId: Int, + val style: IconStyle, + val newFormat: Boolean +) + +enum class IconStyle { + L, M, S, UNKNOWN +} + +fun fallbackToNewIconFormat( + iconStyle: IconStyle, + context: Context, + iconName: String?, +): IconInfo? { + return iconName?.let { + try { + val iconNameNormalized = iconName + .replace(" ", "") + .trim() + .toLowerCaseLocal() + + val iconId = context.resources.getIdentifier( + iconNameNormalized, + "drawable", + context.packageName + ).takeIf { it != 0 } + + iconId?.let { nonNullId -> + IconInfo( + iconId = nonNullId, + style = iconStyle, + newFormat = true + ) + } } catch (e: Exception) { null } diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/components/IvyDescriptionTextField.kt b/app/src/main/java/com/ivy/wallet/ui/theme/components/IvyDescriptionTextField.kt index e0a61cc8b4..17cce29ccd 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/components/IvyDescriptionTextField.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/components/IvyDescriptionTextField.kt @@ -63,7 +63,7 @@ fun IvyDescriptionTextField( modifier = textModifier, value = value, onValueChange = onValueChanged, - textStyle = UI.typo.b2.style( + textStyle = UI.typo.nB2.style( color = textColor, fontWeight = fontWeight, textAlign = TextAlign.Start diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChooseIconModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChooseIconModal.kt index df9544e299..f89467cf38 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChooseIconModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/ChooseIconModal.kt @@ -18,6 +18,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.design.l0_system.UI +import com.ivy.design.l1_buildingBlocks.DividerW +import com.ivy.design.l1_buildingBlocks.IvyText +import com.ivy.design.l1_buildingBlocks.SpacerHor +import com.ivy.design.l1_buildingBlocks.SpacerVer import com.ivy.wallet.R import com.ivy.wallet.ui.IvyWalletPreview import com.ivy.wallet.ui.theme.Ivy @@ -28,6 +32,7 @@ import com.ivy.wallet.utils.onScreenStart import com.ivy.wallet.utils.thenIf import java.util.* +private const val ICON_PICKER_ICONS_PER_ROW = 5 @Composable fun BoxWithConstraintsScope.ChooseIconModal( @@ -72,7 +77,7 @@ fun BoxWithConstraintsScope.ChooseIconModal( ModalTitle(text = stringResource(R.string.choose_icon)) - Spacer(Modifier.height(32.dp)) + Spacer(Modifier.height(4.dp)) } icons(selectedIcon = selectedIcon, color = color) { @@ -92,37 +97,121 @@ private fun LazyListScope.icons( onIconSelected: (String) -> Unit ) { - val icons = listOf( - "account", "category", "cash", "bank", "revolut", - "clothes2", "clothes", "family", "star", - "education", "fitness", "loan", "orderfood", "orderfood2", - "pet", "restaurant", "selfdevelopment", "work", "vehicle", - "atom", "bills", "birthday", "calculator", "camera", - "chemistry", "coffee", "connect", "dna", "doctor", - "document", "drink", "farmacy", "fingerprint", "fishfood", - "food2", "fooddrink", "furniture", "gambling", "game", - "gears", "gift", "groceries", "hairdresser", "health", - "hike", "house", "insurance", "label", "leaf", - "location", "makeup", "music", "notice", "people", - "plant", "programming", "relationship", "rocket", "safe", - "sail", "server", "shopping2", "shopping", "sports", - "stats", "tools", "transport", "travel", "trees", - "zeus", "calendar", "crown", "diamond", "palette" -// "ada", "btc", "eth", "xrp", "doge" + val icons = ivyIcons() + + iconsR( + icons = icons, + iconsPerRow = ICON_PICKER_ICONS_PER_ROW, + selectedIcon = selectedIcon, + color = color, + onIconSelected = onIconSelected ) +} + +private tailrec fun LazyListScope.iconsR( + icons: List, + rowAcc: List = emptyList(), + + iconsPerRow: Int, + selectedIcon: String?, + color: Color, + + onIconSelected: (String) -> Unit +) { + if (icons.isNotEmpty()) { + //recurse + + when (val currentItem = icons.first()) { + is IconPickerSection -> { + addIconsRowIfNotEmpty( + rowAcc = rowAcc, + selectedIcon = selectedIcon, + color = color, + onIconSelected = onIconSelected + ) + + item { + Section(title = currentItem.title) + } + + //RECURSE + iconsR( + icons = icons.drop(1), + rowAcc = emptyList(), - val rowsCount = icons.size / 5 + 1 + iconsPerRow = iconsPerRow, + selectedIcon = selectedIcon, + color = color, + onIconSelected = onIconSelected - for (row in 0 until rowsCount) { - val toIndex = (row * 5) + 5 - val rowIcons = icons.subList( - fromIndex = row * 5, - toIndex = if (toIndex < icons.size - 1) toIndex else icons.size + ) + } + is String -> { + //icon + + if (rowAcc.size == iconsPerRow) { + //recurse and reset acc + + addIconsRowIfNotEmpty( + rowAcc = rowAcc, + selectedIcon = selectedIcon, + color = color, + onIconSelected = onIconSelected + ) + + //RECURSE + iconsR( + icons = icons.drop(1), + rowAcc = emptyList(), + + iconsPerRow = iconsPerRow, + selectedIcon = selectedIcon, + color = color, + onIconSelected = onIconSelected + + ) + } else { + //recurse by filling acc + + //RECURSE + iconsR( + icons = icons.drop(1), + rowAcc = rowAcc + currentItem, + + iconsPerRow = iconsPerRow, + selectedIcon = selectedIcon, + color = color, + onIconSelected = onIconSelected + + ) + } + } + } + } else { + //end recursion + addIconsRowIfNotEmpty( + rowAcc = rowAcc, + selectedIcon = selectedIcon, + color = color, + onIconSelected = onIconSelected ) + } +} + +private fun LazyListScope.addIconsRowIfNotEmpty( + rowAcc: List, + selectedIcon: String?, + color: Color, + + onIconSelected: (String) -> Unit +) { + if (rowAcc.isNotEmpty()) { item { IconsRow( - icons = rowIcons, selectedIcon = selectedIcon, color = color + icons = rowAcc, + selectedIcon = selectedIcon, + color = color ) { onIconSelected(it) } @@ -190,6 +279,30 @@ private fun Icon( ) } +@Composable +private fun Section( + title: String +) { + SpacerVer(height = 20.dp) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + DividerW() + + SpacerHor(width = 16.dp) + + IvyText(text = title, typo = UI.typo.b1) + + SpacerHor(width = 16.dp) + + DividerW() + } + + SpacerVer(height = 20.dp) +} + @Preview @Composable private fun ChooseIconModal() { @@ -203,4 +316,411 @@ private fun ChooseIconModal() { } } -} \ No newline at end of file +} + +data class IconPickerSection(val title: String) + +fun ivyIcons(): List = listOf( + IconPickerSection("Ivy"), + "account", + "category", + "cash", + "bank", + "revolut", + "clothes2", + "clothes", + "family", + "star", + "education", + "fitness", + "loan", + "orderfood", + "orderfood2", + "pet", + "restaurant", + "selfdevelopment", + "work", + "vehicle", + "atom", + "bills", + "birthday", + "calculator", + "camera", + "chemistry", + "coffee", + "connect", + "dna", + "doctor", + "document", + "drink", + "farmacy", + "fingerprint", + "fishfood", + "food2", + "fooddrink", + "furniture", + "gambling", + "game", + "gears", + "gift", + "groceries", + "hairdresser", + "health", + "hike", + "house", + "insurance", + "label", + "leaf", + "location", + "makeup", + "music", + "notice", + "people", + "plant", + "programming", + "relationship", + "rocket", + "safe", + "sail", + "server", + "shopping2", + "shopping", + "sports", + "stats", + "tools", + "transport", + "travel", + "trees", + "zeus", + "calendar", + "crown", + "diamond", + "palette", + IconPickerSection("Brands"), + "ic_vue_brands_triangle", + "ic_vue_brands_trello", + "ic_vue_brands_html5", + "ic_vue_brands_spotify", + "ic_vue_brands_bootsrap", + "ic_vue_brands_dribbble", + "ic_vue_brands_google_play", + "ic_vue_brands_dropbox", + "ic_vue_brands_js", + "ic_vue_brands_drive", + "ic_vue_brands_paypal", + "ic_vue_brands_be", + "ic_vue_brands_figma", + "ic_vue_brands_messenger", + "ic_vue_brands_facebook", + "ic_vue_brands_framer", + "ic_vue_brands_whatsapp", + "ic_vue_brands_html3", + "ic_vue_brands_zoom", + "ic_vue_brands_ok", + "ic_vue_brands_twitch", + "ic_vue_brands_youtube", + "ic_vue_brands_apple", + "ic_vue_brands_android", + "ic_vue_brands_slack", + "ic_vue_brands_vuesax", + "ic_vue_brands_blogger", + "ic_vue_brands_photoshop", + "ic_vue_brands_python", + "ic_vue_brands_google", + "ic_vue_brands_xd", + "ic_vue_brands_illustrator", + "ic_vue_brands_xiaomi", + "ic_vue_brands_windows", + "ic_vue_brands_snapchat", + "ic_vue_brands_ui8", + IconPickerSection("Building"), + "ic_vue_building_building1", + "ic_vue_building_buildings", + "ic_vue_building_hospital", + "ic_vue_building_building", + "ic_vue_building_bank", + "ic_vue_building_house", + "ic_vue_building_courthouse", + IconPickerSection("Chart"), + "ic_vue_chart_diagram", + "ic_vue_chart_graph", + "ic_vue_chart_status_up", + "ic_vue_chart_chart", + "ic_vue_chart_trend_up", + IconPickerSection("Crypto"), + "ic_vue_crypto_dent", + "ic_vue_crypto_icon", + "ic_vue_crypto_decred", + "ic_vue_crypto_ocean_protocol", + "ic_vue_crypto_hedera_hashgraph", + "ic_vue_crypto_binance_usd", + "ic_vue_crypto_maker", + "ic_vue_crypto_xrp", + "ic_vue_crypto_harmony", + "ic_vue_crypto_theta", + "ic_vue_crypto_celsius_", + "ic_vue_crypto_vibe", + "ic_vue_crypto_augur", + "ic_vue_crypto_graph", + "ic_vue_crypto_monero", + "ic_vue_crypto_aave", + "ic_vue_crypto_dai", + "ic_vue_crypto_litecoin", + "ic_vue_crypto_tether", + "ic_vue_crypto_thorchain", + "ic_vue_crypto_nexo", + "ic_vue_crypto_chainlink", + "ic_vue_crypto_ethereum_classic", + "ic_vue_crypto_usd_coin", + "ic_vue_crypto_nem", + "ic_vue_crypto_eos", + "ic_vue_crypto_emercoin", + "ic_vue_crypto_dash", + "ic_vue_crypto_ontology", + "ic_vue_crypto_ftx_token", + "ic_vue_crypto_educare", + "ic_vue_crypto_solana", + "ic_vue_crypto_ethereum", + "ic_vue_crypto_velas", + "ic_vue_crypto_hex", + "ic_vue_crypto_polkadot", + "ic_vue_crypto_huobi_token", + "ic_vue_crypto_polyswarm", + "ic_vue_crypto_ankr", + "ic_vue_crypto_enjin_coin", + "ic_vue_crypto_polygon", + "ic_vue_crypto_wing", + "ic_vue_crypto_nebulas", + "ic_vue_crypto_iost", + "ic_vue_crypto_binance_coin", + "ic_vue_crypto_kyber_network", + "ic_vue_crypto_trontron", + "ic_vue_crypto_stellar", + "ic_vue_crypto_avalanche", + "ic_vue_crypto_wanchain", + "ic_vue_crypto_cardano", + "ic_vue_crypto_okb", + "ic_vue_crypto_stacks", + "ic_vue_crypto_siacoin", + "ic_vue_crypto_autonio", + "ic_vue_crypto_civic", + "ic_vue_crypto_zel", + "ic_vue_crypto_quant", + "ic_vue_crypto_tenx", + "ic_vue_crypto_celo", + "ic_vue_crypto_bitcoin", + IconPickerSection("Delivery"), + "ic_vue_delivery_package", + "ic_vue_delivery_receive", + "ic_vue_delivery_box1", + "ic_vue_delivery_box", + "ic_vue_delivery_truck", + IconPickerSection("Design"), + "ic_vue_design_bezier", + "ic_vue_design_brush", + "ic_vue_design_color_swatch", + "ic_vue_design_scissors", + "ic_vue_design_magicpen", + "ic_vue_design_roller", + "ic_vue_design_tool_pen", + IconPickerSection("Dev"), + "ic_vue_dev_code", + "ic_vue_dev_hierarchy", + "ic_vue_dev_relation", + "ic_vue_dev_arrow", + "ic_vue_dev_data", + "ic_vue_dev_hashtag", + IconPickerSection("Education"), + "ic_vue_edu_planer", + "ic_vue_edu_briefcase", + "ic_vue_edu_award", + "ic_vue_edu_glass", + "ic_vue_edu_graduate_cap", + "ic_vue_edu_calculator", + "ic_vue_edu_note", + "ic_vue_edu_magazine", + "ic_vue_edu_pen", + "ic_vue_edu_telescope", + "ic_vue_edu_book", + "ic_vue_edu_ruler_pen", + "ic_vue_edu_todo", + "ic_vue_edu_omega", + "ic_vue_edu_bookmark", + IconPickerSection("Files"), + "ic_vue_files_folder_favorite", + "ic_vue_files_folder", + "ic_vue_files_folder_cloud", + IconPickerSection("Location"), + "ic_vue_location_map1", + "ic_vue_location_map", + "ic_vue_location_location", + "ic_vue_location_global", + "ic_vue_location_global_search", + "ic_vue_location_routing", + "ic_vue_location_discover", + "ic_vue_location_radar", + "ic_vue_location_global_edit", + IconPickerSection("Main"), + "ic_vue_main_cake", + "ic_vue_main_reserve", + "ic_vue_main_archive", + "ic_vue_main_signpost", + "ic_vue_main_coffee", + "ic_vue_main_sport", + "ic_vue_main_notification", + "ic_vue_main_lamp_charge", + "ic_vue_main_home", + "ic_vue_main_judge", + "ic_vue_main_timer", + "ic_vue_main_lamp", + "ic_vue_main_battery_charging", + "ic_vue_main_calendar", + "ic_vue_main_home_wifi", + "ic_vue_main_tree", + "ic_vue_main_battery_half", + "ic_vue_main_send", + "ic_vue_main_glass", + "ic_vue_main_emoji_normal", + "ic_vue_main_share", + "ic_vue_main_trash", + "ic_vue_main_milk", + "ic_vue_main_lifebuoy", + "ic_vue_main_broom", + "ic_vue_main_gift", + "ic_vue_main_clock", + "ic_vue_main_emoji_happy", + "ic_vue_main_home_safe", + "ic_vue_main_crown", + "ic_vue_main_cup", + "ic_vue_main_emoji_sad", + "ic_vue_main_pet", + "ic_vue_main_flash", + IconPickerSection("Media"), + "ic_vue_media_microphone", + "ic_vue_media_music", + "ic_vue_media_voice", + "ic_vue_media_image", + "ic_vue_media_scissors", + "ic_vue_media_mountains", + "ic_vue_media_film", + "ic_vue_media_photocamera", + "ic_vue_media_film_play", + "ic_vue_media_camera", + "ic_vue_media_screenmirroring", + "ic_vue_media_speaker", + "ic_vue_media_play", + "ic_vue_media_subtitle", + "ic_vue_media_setting", + IconPickerSection("Messages"), + "ic_vue_messages_msg_favorite", + "ic_vue_messages_direct", + "ic_vue_messages_msg_notification", + "ic_vue_messages_device_msg", + "ic_vue_messages_edit", + "ic_vue_messages_msgs", + "ic_vue_messages_msg_text", + "ic_vue_messages_letter", + "ic_vue_messages_msg", + "ic_vue_messages_msg_search", + IconPickerSection("Money"), + "ic_vue_money_bitcoin_refresh", + "ic_vue_money_dollar", + "ic_vue_money_archive", + "ic_vue_money_coins", + "ic_vue_money_discount", + "ic_vue_money_recive", + "ic_vue_money_card_send", + "ic_vue_money_buy_crypto", + "ic_vue_money_card_bitcoin", + "ic_vue_money_buy_bitcoin", + "ic_vue_money_ticket_star", + "ic_vue_money_wallet", + "ic_vue_money_send", + "ic_vue_money_ticket_discount", + "ic_vue_money_wallet_cards", + "ic_vue_money_receipt_empty", + "ic_vue_money_percentage", + "ic_vue_money_math", + "ic_vue_money_security_card", + "ic_vue_money_wallet_money", + "ic_vue_money_ticket", + "ic_vue_money_card_receive", + "ic_vue_money_wallet_empty", + "ic_vue_money_transfer", + "ic_vue_money_card_coin", + "ic_vue_money_receipt_items", + "ic_vue_money_tag", + "ic_vue_money_receipt_discount", + "ic_vue_money_card", + IconPickerSection("PC"), + "ic_vue_pc_charging", + "ic_vue_pc_watch", + "ic_vue_pc_headphone", + "ic_vue_pc_gameboy", + "ic_vue_pc_phone_call", + "ic_vue_pc_setting", + "ic_vue_pc_monitor", + "ic_vue_pc_cpu", + "ic_vue_pc_printer", + "ic_vue_pc_bluetooth", + "ic_vue_pc_wifi", + "ic_vue_pc_game", + "ic_vue_pc_speaker", + "ic_vue_pc_phone", + IconPickerSection("People"), + "ic_vue_people_2persons", + "ic_vue_people_person_tag", + "ic_vue_people_person_search", + "ic_vue_people_people", + "ic_vue_people_person", + IconPickerSection("Security"), + "ic_vue_security_eye", + "ic_vue_security_shield_security", + "ic_vue_security_key", + "ic_vue_security_alarm", + "ic_vue_security_lock", + "ic_vue_security_password", + "ic_vue_security_radar", + "ic_vue_security_shield_person", + "ic_vue_security_shield", + IconPickerSection("Shop"), + "ic_vue_shop_cart", + "ic_vue_shop_bag", + "ic_vue_shop_barcode", + "ic_vue_shop_bag1", + "ic_vue_shop_shop", + IconPickerSection("Support"), + "ic_vue_support_star", + "ic_vue_support_medal", + "ic_vue_support_dislike", + "ic_vue_support_like_dislike", + "ic_vue_support_smileys", + "ic_vue_support_heart", + "ic_vue_support_like", + IconPickerSection("Transport"), +// "ic_vue_transport_car_wash", //TODO: Fix car_wash icon missing + "ic_vue_transport_bus", + "ic_vue_transport_airplane", + "ic_vue_transport_train", + "ic_vue_transport_ship", + "ic_vue_transport_gas", + "ic_vue_transport_car", + IconPickerSection("Type"), + "ic_vue_type_link2", + "ic_vue_type_text", + "ic_vue_type_paperclip", + "ic_vue_type_textalign_left", + "ic_vue_type_translate", + "ic_vue_type_textalign_right", + "ic_vue_type_link", + "ic_vue_type_textalign_center", + "ic_vue_type_textalign_justifycenter", + IconPickerSection("Weather"), + "ic_vue_weather_wind", + "ic_vue_weather_cloud", + "ic_vue_weather_flash", + "ic_vue_weather_moon", + "ic_vue_weather_drop", + "ic_vue_weather_cold", + "ic_vue_weather_sun", +) diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt index 3121e28367..984d6b31f9 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt @@ -72,6 +72,7 @@ fun BoxWithConstraintsScope.AmountModal( onClick = { calculatorModalVisible = true }) + .testTag("btn_calculator") .padding(all = 4.dp), icon = R.drawable.ic_custom_calculator_m, tint = UI.colors.pureInverse @@ -178,6 +179,7 @@ fun AmountInput( var firstInput by remember { mutableStateOf(true) } AmountKeyboard( + forCalculator = false, onNumberPressed = { if (firstInput) { setAmount(it) @@ -243,6 +245,7 @@ private fun formatNumber(number: String): String? { @Composable fun AmountKeyboard( + forCalculator: Boolean, ZeroRow: (@Composable RowScope.() -> Unit)? = null, FirstRowExtra: (@Composable RowScope.() -> Unit)? = null, SecondRowExtra: (@Composable RowScope.() -> Unit)? = null, @@ -271,6 +274,7 @@ fun AmountKeyboard( horizontalArrangement = Arrangement.Center ) { CircleNumberButton( + forCalculator = forCalculator, value = "7", onNumberPressed = onNumberPressed ) @@ -278,6 +282,7 @@ fun AmountKeyboard( Spacer(Modifier.width(16.dp)) CircleNumberButton( + forCalculator = forCalculator, value = "8", onNumberPressed = onNumberPressed ) @@ -285,6 +290,7 @@ fun AmountKeyboard( Spacer(Modifier.width(16.dp)) CircleNumberButton( + forCalculator = forCalculator, value = "9", onNumberPressed = onNumberPressed ) @@ -304,6 +310,7 @@ fun AmountKeyboard( horizontalArrangement = Arrangement.Center ) { CircleNumberButton( + forCalculator = forCalculator, value = "4", onNumberPressed = onNumberPressed ) @@ -311,6 +318,7 @@ fun AmountKeyboard( Spacer(Modifier.width(16.dp)) CircleNumberButton( + forCalculator = forCalculator, value = "5", onNumberPressed = onNumberPressed ) @@ -318,6 +326,7 @@ fun AmountKeyboard( Spacer(Modifier.width(16.dp)) CircleNumberButton( + forCalculator = forCalculator, value = "6", onNumberPressed = onNumberPressed ) @@ -337,6 +346,7 @@ fun AmountKeyboard( horizontalArrangement = Arrangement.Center ) { CircleNumberButton( + forCalculator = forCalculator, value = "1", onNumberPressed = onNumberPressed ) @@ -344,6 +354,7 @@ fun AmountKeyboard( Spacer(Modifier.width(16.dp)) CircleNumberButton( + forCalculator = forCalculator, value = "2", onNumberPressed = onNumberPressed ) @@ -351,6 +362,7 @@ fun AmountKeyboard( Spacer(Modifier.width(16.dp)) CircleNumberButton( + forCalculator = forCalculator, value = "3", onNumberPressed = onNumberPressed ) @@ -371,7 +383,8 @@ fun AmountKeyboard( ) { KeypadCircleButton( text = localDecimalSeparator(), - testTag = "key_decimal_separator" + testTag = if (forCalculator) + "calc_key_decimal_separator" else "key_decimal_separator" ) { onDecimalPoint() } @@ -379,6 +392,7 @@ fun AmountKeyboard( Spacer(Modifier.width(16.dp)) CircleNumberButton( + forCalculator = forCalculator, value = "0", onNumberPressed = onNumberPressed ) @@ -403,12 +417,14 @@ fun AmountKeyboard( @Composable fun CircleNumberButton( + forCalculator: Boolean, value: String, onNumberPressed: (String) -> Unit, ) { KeypadCircleButton( text = value, - testTag = "key_${value}", + testTag = if (forCalculator) + "calc_key_${value}" else "key_${value}", onClick = { onNumberPressed(value) } diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt index e6c3cd4a14..32751c898f 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -41,7 +42,9 @@ fun BoxWithConstraintsScope.CalculatorModal( visible = visible, dismiss = dismiss, PrimaryAction = { - ModalSet { + ModalSet( + modifier = Modifier.testTag("calc_set") + ) { val result = calculate(expression) if (result != null) { onCalculation(result) @@ -72,6 +75,7 @@ fun BoxWithConstraintsScope.CalculatorModal( Spacer(Modifier.height(32.dp)) AmountKeyboard( + forCalculator = true, ZeroRow = { KeypadCircleButton( text = "C", diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/DescriptionModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/DescriptionModal.kt index 21f0af2bcd..fd7f6cedbd 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/DescriptionModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/DescriptionModal.kt @@ -18,8 +18,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style +import com.ivy.design.utils.hideKeyboard import com.ivy.wallet.R import com.ivy.wallet.ui.IvyWalletPreview +import com.ivy.wallet.ui.rootView import com.ivy.wallet.ui.theme.components.IvyDescriptionTextField import com.ivy.wallet.ui.theme.modal.IvyModal import com.ivy.wallet.ui.theme.modal.ModalDynamicPrimaryAction @@ -40,6 +42,7 @@ fun BoxWithConstraintsScope.DescriptionModal( var descTextFieldValue by remember(description) { mutableStateOf(selectEndTextFieldValue(description)) } + val view = rootView() IvyModal( id = id, @@ -51,9 +54,11 @@ fun BoxWithConstraintsScope.DescriptionModal( initialChanged = description != descTextFieldValue.text, onSave = { onDescriptionChanged(descTextFieldValue.text) + view.hideKeyboard() }, onDelete = { onDescriptionChanged(null) + view.hideKeyboard() }, dismiss = dismiss ) diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/transaction/TransactionCard.kt b/app/src/main/java/com/ivy/wallet/ui/theme/transaction/TransactionCard.kt index cd8f598ef9..cb1af72260 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/transaction/TransactionCard.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/transaction/TransactionCard.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.unit.dp import com.ivy.design.api.navigation import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style +import com.ivy.design.l1_buildingBlocks.IvyText +import com.ivy.design.l1_buildingBlocks.SpacerHor import com.ivy.wallet.R import com.ivy.wallet.domain.data.TransactionType import com.ivy.wallet.domain.data.core.Account @@ -34,7 +36,6 @@ import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.components.ItemIconSDefaultIcon import com.ivy.wallet.ui.theme.components.IvyButton import com.ivy.wallet.ui.theme.components.IvyIcon -import com.ivy.wallet.ui.theme.components.getCustomIconIdS import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 import com.ivy.wallet.utils.* import java.time.LocalDateTime @@ -85,7 +86,10 @@ fun LazyItemScope.TransactionCard( Text( modifier = Modifier.padding(horizontal = 24.dp), - text = stringResource(R.string.due_on, transaction.dueDate.formatNicely()).uppercase(), + text = stringResource( + R.string.due_on, + transaction.dueDate.formatNicely() + ).uppercase(), style = UI.typo.nC.style( color = if (transaction.dueDate.isAfter(timeNowUTC())) Orange else UI.colors.gray, @@ -112,14 +116,15 @@ fun LazyItemScope.TransactionCard( } - if (transaction.description.isNotNullOrBlank()){ + if (transaction.description.isNotNullOrBlank()) { Spacer( Modifier.height( if (transaction.title.isNotNullOrBlank()) 4.dp else 8.dp ) ) - Text(text = transaction.description!!, + Text( + text = transaction.description!!, modifier = Modifier.padding(horizontal = 24.dp), style = UI.typo.nC.style( color = UI.colors.gray, @@ -201,21 +206,11 @@ private fun TransactionHeaderRow( transaction.categoryId ?.let { targetId -> categories.find { it.id == targetId } } if (category != null) { - IvyButton( + TransactionBadge( text = category.name, - backgroundGradient = Gradient.solid(category.color.toComposeColor()), - hasGlow = false, - iconTint = findContrastTextColor(category.color.toComposeColor()), - iconStart = getCustomIconIdS( - iconName = category.icon, - defaultIcon = R.drawable.ic_custom_category_s - ), - textStyle = UI.typo.c.style( - color = findContrastTextColor(category.color.toComposeColor()), - fontWeight = FontWeight.ExtraBold - ), - padding = 8.dp, - iconEdgePadding = 10.dp + backgroundColor = category.color.toComposeColor(), + icon = category.icon, + defaultIcon = R.drawable.ic_custom_category_s ) { nav.navigateTo( ItemStatistic( @@ -229,22 +224,11 @@ private fun TransactionHeaderRow( } val account = accounts.find { it.id == transaction.accountId } - //TODO: Rework that by using dedicated component for "Account" - IvyButton( - backgroundGradient = Gradient.solid(UI.colors.pure), - hasGlow = false, - iconTint = UI.colors.pureInverse, + TransactionBadge( text = account?.name ?: stringResource(R.string.deleted), - iconStart = getCustomIconIdS( - iconName = account?.icon, - defaultIcon = R.drawable.ic_custom_account_s - ), - textStyle = UI.typo.c.style( - color = UI.colors.pureInverse, - fontWeight = FontWeight.ExtraBold - ), - padding = 8.dp, - iconEdgePadding = 10.dp + backgroundColor = UI.colors.pure, + icon = account?.icon, + defaultIcon = R.drawable.ic_custom_account_s ) { account?.let { nav.navigateTo( @@ -259,6 +243,50 @@ private fun TransactionHeaderRow( } } +@Composable +private fun TransactionBadge( + text: String, + backgroundColor: Color, + icon: String?, + @DrawableRes + defaultIcon: Int, + + onClick: () -> Unit +) { + Row( + modifier = Modifier + .clip(UI.shapes.rFull) + .background(backgroundColor, UI.shapes.rFull) + .clickable { + onClick() + } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SpacerHor(width = 8.dp) + + val contrastColor = findContrastTextColor(backgroundColor) + + ItemIconSDefaultIcon( + iconName = icon, + defaultIcon = defaultIcon, + tint = contrastColor + ) + + SpacerHor(width = 4.dp) + + IvyText( + text = text, + typo = UI.typo.c.style( + color = contrastColor, + fontWeight = FontWeight.ExtraBold + ) + ) + + SpacerHor(width = 20.dp) + } +} + @Composable private fun TransferHeader( accounts: List, @@ -329,6 +357,7 @@ fun TypeAmountCurrency( ) { Row( + modifier = Modifier.testTag("type_amount_currency"), verticalAlignment = Alignment.CenterVertically ) { Spacer(Modifier.width(24.dp)) diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/wallet/AmountCurrency.kt b/app/src/main/java/com/ivy/wallet/ui/theme/wallet/AmountCurrency.kt index 9ebd444314..9a34133580 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/wallet/AmountCurrency.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/wallet/AmountCurrency.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.ivy.design.l0_system.UI @@ -76,6 +77,7 @@ fun AmountCurrencyB1( val shortAmount = shortenBigNumbers && shouldShortAmount(amount) Text( + modifier = Modifier.testTag("amount_currency_b1"), text = if (shortAmount) shortenAmount(amount) else amount.format(currency), style = UI.typo.nB1.style( fontWeight = amountFontWeight, diff --git a/app/src/main/java/com/ivy/wallet/utils/FileUtil.kt b/app/src/main/java/com/ivy/wallet/utils/FileUtil.kt index 1cfe4ce093..0243acd8d5 100644 --- a/app/src/main/java/com/ivy/wallet/utils/FileUtil.kt +++ b/app/src/main/java/com/ivy/wallet/utils/FileUtil.kt @@ -1,8 +1,10 @@ package com.ivy.wallet.utils +import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Environment +import android.provider.OpenableColumns import java.io.* import java.nio.charset.Charset @@ -81,4 +83,16 @@ private fun readFileContent( } return sb.toString() } -} \ No newline at end of file +} + +fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { + ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) + else -> uri.path?.let(::File)?.name +} + +private fun Context.getContentFileName(uri: Uri): String? = runCatching { + contentResolver.query(uri, null, null, null, null)?.use { cursor -> + cursor.moveToFirst() + return@use cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString) + } +}.getOrNull() \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/utils/UIExt.kt b/app/src/main/java/com/ivy/wallet/utils/UIExt.kt index 3f88171496..0ec41dd627 100644 --- a/app/src/main/java/com/ivy/wallet/utils/UIExt.kt +++ b/app/src/main/java/com/ivy/wallet/utils/UIExt.kt @@ -151,9 +151,13 @@ fun colorLerp(start: Color, end: Color, fraction: Float): Color { } fun hideKeyboard(view: View) { - val imm: InputMethodManager = - view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.windowToken, 0) + try { + + val imm: InputMethodManager = + view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + } catch (ignore: Exception) { + } } /* diff --git a/app/src/main/res/drawable/ic_vue_transport_car_wash.xml b/app/src/main/res/drawable/ic_vue_transport_car_wash.xml deleted file mode 100644 index 22b3435888..0000000000 --- a/app/src/main/res/drawable/ic_vue_transport_car_wash.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - diff --git a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt index 23d674e7a0..030f9e8b9c 100644 --- a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt @@ -22,8 +22,8 @@ import org.gradle.kotlin.dsl.project object Project { //Version - const val versionName = "3.1.2-fast" - const val versionCode = 104 + const val versionName = "4.1.1" + const val versionCode = 107 //Compile SDK & Build Tools const val compileSdkVersion = 31 diff --git a/docs/Ivy-Dao.md b/docs/Ivy-Dao.md index ffc309015a..a566064416 100644 --- a/docs/Ivy-Dao.md +++ b/docs/Ivy-Dao.md @@ -16,18 +16,18 @@ graph TD; contribs -- Develop --> product contribs -- Design --> product contribs -- Promote --> product - contribs -- Vote --> dao_proposals + contribs -- Vote with IVY --> dao_proposals product -- Acquire --> users users -- Reviews --> product - users -- Donate --> dao + users -- Donate crypto --> dao dao -- Store Donations --> dao_dev_fund - dao -- Manage --> dao_proposals + dao -- Smart Contract --> dao_proposals dao_dev_fund -- Bounty --> dao_proposals dao_proposals -- If passed voting --> tickets - tickets -- Earn --> contribs + tickets -- Earn: Bounty + IVY --> contribs ``` \ No newline at end of file diff --git a/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/Dividers.kt b/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/Dividers.kt index 7a3702a559..7215d42118 100644 --- a/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/Dividers.kt +++ b/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/Dividers.kt @@ -67,6 +67,38 @@ fun DividerV( ) } +@Composable +fun RowScope.DividerW( + weight: Float = 1f, + height: Dp = 1.dp, + color: Color = UI.colors.gray, + shape: Shape = UI.shapes.rFull +) { + Divider( + modifier = Modifier + .weight(weight) + .height(1.dp), + color = color, + shape = shape + ) +} + +@Composable +fun ColumnScope.DividerW( + weight: Float = 1f, + width: Dp = 1.dp, + color: Color = UI.colors.gray, + shape: Shape = UI.shapes.rFull +) { + Divider( + modifier = Modifier + .weight(weight) + .width(width), + color = color, + shape = shape + ) +} + @Composable fun Divider( modifier: Modifier = Modifier, diff --git a/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/IvyIcon.kt b/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/IvyIcon.kt index 2e3b10ce27..51998f315e 100644 --- a/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/IvyIcon.kt +++ b/ivy-design/src/main/java/com/ivy/design/l1_buildingBlocks/IvyIcon.kt @@ -1,12 +1,21 @@ package com.ivy.design.l1_buildingBlocks import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.ivy.design.l0_system.UI +import com.ivy.design.utils.thenWhen @Composable fun IvyIcon( @@ -21,4 +30,42 @@ fun IvyIcon( contentDescription = contentDescription, tint = tint ) +} + +@Composable +fun IvyIconScaled( + modifier: Modifier = Modifier, + @DrawableRes icon: Int, + tint: Color = UI.colors.pureInverse, + iconScale: IconScale, + padding: Dp = when (iconScale) { + IconScale.S -> 4.dp + IconScale.M -> 4.dp + IconScale.L -> 4.dp + }, + contentDescription: String = "icon" +) { + Image( + modifier = modifier + .thenWhen { + when (iconScale) { + IconScale.L -> + this.size(64.dp) + IconScale.M -> + this.size(48.dp) + IconScale.S -> + this.size(32.dp) + } + } + .padding(all = padding), + painter = painterResource(id = icon), + colorFilter = ColorFilter.tint(tint), + alignment = Alignment.Center, + contentScale = ContentScale.Fit, + contentDescription = contentDescription + ) +} + +enum class IconScale { + S, M, L } \ No newline at end of file