diff --git a/.circleci/config.yml b/.circleci/config.yml index 1ad5baa3..c074e195 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,60 @@ version: 2.1 orbs: - android: circleci/android@3.0.2 + android: circleci/android@3.1.0 + ruby: circleci/ruby@2.5.3 + +# Define reusable commands +commands: + setup_environment: + description: "Sets up the basic environment for all jobs (checkout, credentials, submodules)" + steps: + - checkout + - run: + name: "Setup environment..." + command: "echo Setting up environment for brainwallet-android!" + - run: + name: "Create google-services.json from env" + command: | + mkdir -p app + echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json + ls -la app/google-services.json + - run: + name: "create service-data.json from env" + command: | + mkdir -p app/src/main/assets + echo "$SERVICE_DATA_JSON" | base64 --decode > app/src/main/assets/service-data.json + ls -la app/src/main/assets/service-data.json + - run: + name: "Create keystore files from env" + command: | + echo "$DEBUG_STORE_FILE" | base64 --decode > debug.keystore + echo "$RELEASE_STORE_FILE" | base64 --decode > release.keystore + ls -la *.keystore + - run: + name: "Create local.properties file" + command: | + cat > local.properties \<< EOL + #adjust with your location + DEBUG_STORE_FILE=${CIRCLE_WORKING_DIRECTORY}/debug.keystore + DEBUG_STORE_PASSWORD=${DEBUG_STORE_PASSWORD} + DEBUG_KEY_ALIAS=${DEBUG_KEY_ALIAS} + DEBUG_KEY_PASSWORD=${DEBUG_KEY_PASSWORD} + + #adjust with your location + RELEASE_STORE_FILE=${CIRCLE_WORKING_DIRECTORY}/release.keystore + RELEASE_STORE_PASSWORD=${RELEASE_STORE_PASSWORD} + RELEASE_KEY_ALIAS=${RELEASE_KEY_ALIAS} + RELEASE_KEY_PASSWORD=${RELEASE_KEY_PASSWORD} + + # screengrab paperkey + SCREENGRAB_PAPERKEY=${SCREENGRAB_PAPERKEY} + EOL + ls && cat local.properties + - run: + name: "Initialize submodule" + command: "git submodule init && git submodule update --init --recursive" + - android/restore_gradle_cache # Define a job to be invoked later in a workflow. # See: https://circleci.com/docs/2.0/configuration-reference/#jobs @@ -14,23 +67,7 @@ jobs: resource_class: large tag: 2024.07.1-ndk steps: - - checkout - - run: - name: "brainwallet-android unit test setup..." - command: "echo Building tests for brainwallet-android! && ls" - - run: - name: "Default for gradle.properties" - command: "echo \"RELEASE_STORE_FILE=/\nRELEASE_STORE_PASSWORD=\nRELEASE_KEY_ALIAS=\nRELEASE_KEY_PASSWORD=\nandroid.useAndroidX=true\nandroid.enableJetifier=true\" >> gradle.properties && ls && cat gradle.properties" - - run: - name: "Initialize submodule" - command: "git submodule init && git submodule update --init --recursive" - - run: - name: "Export google-services.json to env" - command: echo 'export $GOOGLE_SERVICES_JSON="$GOOGLE_SERVICES_JSON"' >> $BASH_ENV - - run: - name: "decode $GOOGLE_SERVICES_JSON to google-services.json" - command: echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json - - android/restore_gradle_cache + - setup_environment - run: name: "Execute Unit Test" command: ./gradlew testBrainwalletDebugUnitTest @@ -46,6 +83,57 @@ jobs: - store_artifacts: path: ~/test-results/junit + screengrab: + executor: + name: android/android_machine + resource_class: large + tag: default + steps: + - setup_environment + - ruby/install: + version: '3.2.0' + - ruby/install-deps + - run: + name: "Install fastlane" + command: | + gem install bundler + bundle install + - android/create_avd: + avd_name: bw_avd + install: true + system_image: system-images;android-34;google_apis;x86_64 + additional_args: '-d "pixel_7_pro"' + - android/start_emulator: + avd_name: bw_avd + no_window: true + memory: 4096 + delay_adb: true + run_logcat: true + # additional_args: "-skin 1080x1920 -memory 2048" + post_emulator_launch_assemble_command: | + # bundle exec fastlane android build_and_screengrab + echo "skipping assemble command, as we only need the emulator running for screengrab" + - run: + name: "Wait for emulator to boot and set PIN" + command: | + echo "Waiting for emulator to boot..." + timeout 300 sh -c 'until adb shell getprop sys.boot_completed | grep -m 1 "1"; do sleep 5; done' + echo "Emulator is ready!" + adb shell svc power stayon true + adb shell locksettings set-pin 123456 + adb shell input keyevent 82 + sleep 5 + - run: + name: "Run Screengrab" + command: | + bundle exec fastlane android build_and_screengrab + - run: + name: "List fastlane android contents" + command: ls -R fastlane/android/ + - android/save_gradle_cache + - store_artifacts: + path: fastlane/metadata/android/ + destination: android-screenshots # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows @@ -53,3 +141,18 @@ workflows: test-and-build: jobs: - unit-test +# - screengrab: +# requires: +# - unit-test + + # Daily scheduled screengrab workflow + # daily-screengrab: + # triggers: + # - schedule: + # cron: "0 0 * * *" # Run at midnight UTC every day + # filters: + # branches: + # only: + # - develop # Adjust to your main branch name + # jobs: + # - screengrab diff --git a/Gemfile.lock b/Gemfile.lock index f322ed56..03d4f21e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,6 +214,8 @@ GEM PLATFORMS arm64-darwin-23 arm64-darwin-24 + x86-linux + x86_64-linux DEPENDENCIES fastlane diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6da0c265..94d8b60a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 34 - versionCode = 202505261 - versionName = "v4.5.4" + versionCode = 202506031 + versionName = "v4.5.5" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") diff --git a/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt b/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt index 30060516..56e381e6 100644 --- a/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt +++ b/app/src/androidTest/kotlin/flow/RecoverWalletScreenGrabsTest.kt @@ -1,33 +1,46 @@ package flow import android.Manifest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onChild import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performTextInput import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import androidx.test.uiautomator.UiDevice -import com.brainwallet.BuildConfig import com.brainwallet.R +import com.brainwallet.test.BuildConfig import com.brainwallet.ui.BrainwalletActivity +import org.junit.After +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import timber.log.Timber import tools.fastlane.screengrab.FalconScreenshotStrategy import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.cleanstatusbar.BluetoothState +import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar +import tools.fastlane.screengrab.cleanstatusbar.IconVisibility +import tools.fastlane.screengrab.cleanstatusbar.MobileDataType import tools.fastlane.screengrab.locale.LocaleTestRule /** * TODO: revisit this, since breaking with new navigation + * + * this will require pixel 7 pro */ @RunWith(JUnit4::class) @LargeTest @@ -47,23 +60,47 @@ class RecoverWalletScreenGrabsTest { @get:Rule val composeTestRule = createAndroidComposeRule() + private val uiDevice + get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + @Before + fun setUp() { + CleanStatusBar() + .setBluetoothState(BluetoothState.DISCONNECTED) + .setMobileNetworkDataType(MobileDataType.LTE) + .setWifiVisibility(IconVisibility.HIDE) + .setShowNotifications(false) + .setClock("0900") + .setBatteryLevel(100) + .enable() + } + + @After + fun tearDown() { + CleanStatusBar.disable() + } + + + @OptIn(ExperimentalTestApi::class) @Test fun onRecoverFlowSuccess() { + composeTestRule.activityRule.scenario.onActivity { Screengrab.setDefaultScreenshotStrategy(FalconScreenshotStrategy(it)) } - val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + waitUntilReady() - Screengrab.screenshot("1_intro_screen") + uiDevice.waitForIdle(60_000) + composeTestRule.waitForIdle() - onView(withId(R.id.button_recover_wallet)).perform(click()) + Screengrab.screenshot("1_welcome_screen") - Screengrab.screenshot("2_recover_screen") + composeTestRule.onNode(hasTestTag("buttonRestore")).performClick() - onView(withId(R.id.send_button)).perform(click()) //actually next button + uiDevice.waitForIdle() - Screengrab.screenshot("3_input_paperkey_screen") + Screengrab.screenshot("2_input_words_screen") //seed words input val editTextTags = (0..11).map { index -> "textFieldSeedWord$index" } @@ -71,123 +108,159 @@ class RecoverWalletScreenGrabsTest { val paperKey = BuildConfig.SCREENGRAB_PAPERKEY editTextTags.zip(paperKey).forEachIndexed { index, (textFieldTag, value) -> - device.waitForIdle(100) + uiDevice.waitForIdle() val textForTyping = value.dropLast(1) composeTestRule.onNodeWithTag(textFieldTag).onChild().performTextInput(textForTyping) composeTestRule.onNodeWithText(value).performClick() - } - composeTestRule.onNodeWithTag("buttonRestore").performClick() + Screengrab.screenshot("3_input_words_screen_2") - device.waitForIdle(300) + composeTestRule.onNodeWithTag("buttonRestore").performScrollTo() + composeTestRule.onNodeWithTag("buttonRestore").assertExists() + composeTestRule.onNodeWithTag("buttonRestore").performClick() - Screengrab.screenshot("4_input_paperkey_screen_2") + uiDevice.waitForIdle() - Screengrab.screenshot("5_setpin_screen") + composeTestRule.waitUntilAtLeastOneExists(hasTestTag("keypad1")) - repeat(6) { - onView(withId(R.id.num1)).perform(click()) + repeat(4) { + uiDevice.waitForIdle() + composeTestRule.onNodeWithTag("keypad1").assertExists() + composeTestRule.onNodeWithTag("keypad1").performClick() } - Screengrab.screenshot("6_setpin_confirm_screen") + Screengrab.screenshot("4_set_passcode_screen") - repeat(6) { - onView(withId(R.id.num1)).perform(click()) - } + composeTestRule.waitUntilAtLeastOneExists(hasTestTag("keypad1")) - device.waitForIdle(3000) + repeat(4) { + uiDevice.waitForIdle() + composeTestRule.onNodeWithTag("keypad1").assertExists() + composeTestRule.onNodeWithTag("keypad1").performClick() + } - Screengrab.screenshot("7_main_screen") + Screengrab.screenshot("5_set_passcode_confirm_screen") - onView(withId(R.id.menuBut)).perform(click()) + uiDevice.waitForIdle() - Screengrab.screenshot("8_menu_bottom_sheet") + Thread.sleep(1000) - onView(withText(R.string.MenuButton_security)).perform(click()) + Screengrab.screenshot("6_main_screen") - Screengrab.screenshot("9_security_center_screen") + //setting drawer + onView(withId(R.id.menuBut)).perform(click()) - onView(withId(R.id.close_button)).perform(click()) + Screengrab.screenshot("7_setting_drawer_open") - onView(withId(R.id.menuBut)).perform(click()) + composeTestRule.onNodeWithTag("settingSecurity").assertExists() + composeTestRule.onNodeWithTag("settingSecurity").performClick() - onView(withText(R.string.MenuButton_settings)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("10_settings_screen") + Screengrab.screenshot("8_setting_drawer_open_security") - onView(withText(R.string.settings_show_seed)).perform(click()) + composeTestRule.waitForIdle() - onView(withId(R.id.show_seed_button)).perform(click()) + composeTestRule.onNodeWithTag("settingLanguage").assertExists() + composeTestRule.onNodeWithTag("settingLanguage").performClick() - Screengrab.screenshot("11_settings_seed_phrase") + composeTestRule.waitForIdle() - device.pressBack() + Screengrab.screenshot("9_setting_drawer_open_language") - onView(withText(R.string.Settings_wipe)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("12_settings_recover_another_wallet") + composeTestRule.onNodeWithTag("settingCurrency").assertExists() + composeTestRule.onNodeWithTag("settingCurrency").performClick() - onView(withId(R.id.close_button)).perform(click()) + composeTestRule.waitForIdle() - onView(withText(R.string.Settings_languages)).perform(click()) + Screengrab.screenshot("10_setting_drawer_open_currency") - Screengrab.screenshot("13_settings_language") + composeTestRule.waitForIdle() - device.pressBack() + composeTestRule.onNodeWithTag("settingGames").assertExists() + composeTestRule.onNodeWithTag("settingGames").performClick() - onView(withText(R.string.Settings_currency)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("14_settings_currency") + Screengrab.screenshot("11_setting_drawer_open_games") - device.pressBack() + composeTestRule.waitForIdle() - onView(withText(R.string.Settings_sync)).perform(click()) + composeTestRule.onNodeWithTag("lazyColumnSetting").performScrollToNode(hasTestTag("settingBlockchain")) + composeTestRule.onNodeWithTag("settingBlockchain").assertExists() + composeTestRule.onNodeWithTag("settingBlockchain").performClick() - Screengrab.screenshot("15_settings_sync") + composeTestRule.waitForIdle() - device.pressBack() + Screengrab.screenshot("12_setting_drawer_open_blockchain") - onView(withText(R.string.Settings_shareData)).perform(click()) + composeTestRule.waitForIdle() - Screengrab.screenshot("16_settings_share_anonymous_data") + composeTestRule.onNodeWithTag("lazyColumnSetting").performScrollToNode(hasTestTag("settingLock")) + composeTestRule.onNodeWithTag("settingLock").assertExists() + composeTestRule.onNodeWithTag("settingLock").performClick() - device.pressBack() + composeTestRule.waitForIdle() - onView(withText(R.string.Settings_about)).perform(click()) + Screengrab.screenshot("13_setting_drawer_lock") - Screengrab.screenshot("17_settings_about") + repeat(4) { + uiDevice.waitForIdle() + composeTestRule.onNodeWithTag("keypad1").assertExists() + composeTestRule.onNodeWithTag("keypad1").performClick() + } - device.pressBack() + uiDevice.waitForIdle() + Thread.sleep(1000) - device.pressBack() + //tx send ui onView(withId(R.id.nav_send)).perform(click()) onView(withId(R.id.amount_edit)).perform(click()) - Screengrab.screenshot("18_transaction_send") + Screengrab.screenshot("14_transaction_send") onView(withId(R.id.close_button)).perform(click()) - onView(withId(R.id.nav_receive)).perform(click()) - - Screengrab.screenshot("19_transaction_receive") - onView(withId(R.id.close_button)).perform(click()) - - onView(withId(R.id.nav_buy)).perform(click()) - - Screengrab.screenshot("20_transaction_buy") + //tx buy/receive ui + onView(withId(R.id.nav_receive)).perform(click()) - onView(withId(R.id.nav_history)).perform(click()) + Screengrab.screenshot("15_transaction_buy_receive") - onView(withId(R.id.menuBut)).perform(click()) + composeTestRule.onNodeWithTag("buttonClose").performClick() - onView(withText(R.string.MenuButton_lock)).perform(click()) + uiDevice.waitForIdle() - Screengrab.screenshot("21_lock_screen") + } + private fun waitUntilReady() { + var attempts = 0 + val maxAttempts = 5 + + while (attempts < maxAttempts) { + try { + composeTestRule.waitUntil(timeoutMillis = 60_000) { + try { + composeTestRule.onRoot().fetchSemanticsNode() + true + } catch (e: Exception) { + Timber.e("[waitForComposeHierarchy] Error1 : ${e.message}", e) + false + } + } + return + } catch (e: Exception) { + Timber.e("[waitForComposeHierarchy] Error2 : ${e.message}", e) + attempts++ + if (attempts >= maxAttempts) throw e + Thread.sleep(1000) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/BrainwalletApp.kt b/app/src/main/java/com/brainwallet/BrainwalletApp.kt index 237bf5a1..03fd029e 100644 --- a/app/src/main/java/com/brainwallet/BrainwalletApp.kt +++ b/app/src/main/java/com/brainwallet/BrainwalletApp.kt @@ -31,7 +31,7 @@ import java.util.Timer import java.util.TimerTask import java.util.concurrent.atomic.AtomicInteger -class BrainwalletApp : Application() { +open class BrainwalletApp : Application() { override fun onCreate() { super.onCreate() @@ -73,7 +73,7 @@ class BrainwalletApp : Application() { appsFlyerLib.start(this) } - protected fun initializeModule() { + protected open fun initializeModule() { startKoin { androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.ERROR) androidContext(this@BrainwalletApp) diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt index 962602f7..b5983c3c 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -13,6 +13,7 @@ import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.tools.util.Utils interface LtcRepository { suspend fun fetchRates(): List @@ -77,7 +78,14 @@ interface LtcRepository { remoteApiSource.getBuyQuote(params) override suspend fun fetchMoonpaySignedUrl(params: Map): String { - return remoteApiSource.getMoonpaySignedUrl(params) + val agentString = Utils.getAgentString(context, "android/HttpURLConnection") + val finalParams = params + mapOf( + "defaultCurrencyCode" to "ltc", + "externalTransactionId" to agentString, + "currencyCode" to "ltc", + "themeId" to "main-v1.0.0", + ) + return remoteApiSource.getMoonpaySignedUrl(finalParams) .signedUrl.toUri() .buildUpon() .apply { diff --git a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt index d38eaeb9..26e1b5dc 100644 --- a/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt +++ b/app/src/main/java/com/brainwallet/navigation/LegacyNavigation.kt @@ -9,6 +9,7 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.core.net.toUri import com.brainwallet.BuildConfig import com.brainwallet.R +import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.source.RemoteApiSource import com.brainwallet.di.getKoinInstance import com.brainwallet.presenter.activities.BreadActivity @@ -66,14 +67,14 @@ object LegacyNavigation { context.startActivity(it) } + @JvmOverloads @JvmStatic fun showMoonPayWidget( context: Context, params: Map = mapOf(), isDarkMode: Boolean = true, ) { - val remoteApiSource: RemoteApiSource = getKoinInstance() - + val ltcRepository: LtcRepository = getKoinInstance() val progressDialog = ProgressDialog(context).apply { setMessage(context.getString(R.string.loading)) setCancelable(false) @@ -83,17 +84,14 @@ object LegacyNavigation { CoroutineScope(Dispatchers.Main).launch { try { val result = withContext(Dispatchers.IO) { - remoteApiSource.getMoonpaySignedUrl( + ltcRepository.fetchMoonpaySignedUrl( params = params.toMutableMap().apply { - put("defaultCurrencyCode", "ltc") - put("currencyCode", "ltc") - put("themeId", "main-v1.0.0") put("theme", if (isDarkMode) "dark" else "light") } ) } - val widgetUri = result.signedUrl.toUri().buildUpon() + val widgetUri = result.toUri().buildUpon() .apply { if (BuildConfig.DEBUG) { authority("buy-sandbox.moonpay.com")//replace base url from buy.moonpay.com diff --git a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java index b210395c..2fd05ab0 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java @@ -421,7 +421,7 @@ private void initializeViews() { bottomNav = findViewById(R.id.bottomNav); bottomNav.getMenu().clear(); - bottomNav.inflateMenu(isInUsa() ? R.menu.bottom_nav_menu_us : R.menu.bottom_nav_menu); + bottomNav.inflateMenu(R.menu.bottom_nav_menu); balanceTxtV = findViewById(R.id.balanceTxtV); primaryPrice = findViewById(R.id.primary_price); @@ -447,11 +447,6 @@ public void onGlobalLayout() { balanceTxtV.append(":"); } - private boolean isInUsa() { - TelephonyManager telManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - return "us".equals(telManager.getSimCountryIso()); - } - @Override public void onBalanceChanged(final long balance) { updateUI(); diff --git a/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java b/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java index 7c15df6d..6640916f 100644 --- a/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java +++ b/app/src/main/java/com/brainwallet/tools/sqlite/CurrencyDataSource.java @@ -89,6 +89,26 @@ public void putCurrencies(Collection currencyEntities) { } } + public List getCurrenciesForBuy() { + List supportedFiatCodes = Arrays.asList( + "AUD", + "BRL", + "CAD", + "CHF", + "EUR", + "GBP", + "IDR", + "MXN", + "NGN", + "TRY", + "USD", + "ZAR" + ); + return getAllCurrencies(true).stream() + .filter(currencyEntity -> supportedFiatCodes.contains(currencyEntity.code)) + .collect(Collectors.toList()); + } + public List getAllCurrencies(Boolean shouldBeFiltered) { List currencies = new ArrayList<>(); diff --git a/app/src/main/java/com/brainwallet/tools/util/Utils.java b/app/src/main/java/com/brainwallet/tools/util/Utils.java index fb03d0c3..21b556dd 100644 --- a/app/src/main/java/com/brainwallet/tools/util/Utils.java +++ b/app/src/main/java/com/brainwallet/tools/util/Utils.java @@ -171,6 +171,9 @@ public static void hideKeyboard(Context app) { public static String getAgentString(Context app, String cfnetwork) { int versionNumber = 0; + String deviceCode = Build.MANUFACTURER + "-|-" + Build.MODEL; + + if (app != null) { try { PackageInfo pInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0); @@ -179,7 +182,7 @@ public static String getAgentString(Context app, String cfnetwork) { Timber.e(e); } } - return String.format(Locale.ENGLISH, "%s/%d %s Android/%s", "Brainwallet", versionNumber, cfnetwork, Build.VERSION.RELEASE); + return String.format(Locale.ENGLISH, "%s/%d %s Android/%s Device/%s", "Brainwallet", versionNumber, cfnetwork, Build.VERSION.RELEASE, deviceCode); } public static String reverseHex(String hex) { diff --git a/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt b/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt index 4fac2149..a0607cbc 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/PasscodeKeypad.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp @Composable @@ -34,7 +35,7 @@ fun PasscodeKeypad( repeat(9) { index -> val number = index + 1 CircleButton( - modifier = modifierCircleButton, + modifier = modifierCircleButton.testTag("keypad$number"), onClick = { onEvent.invoke(PasscodeKeypadEvent.OnPressed(number)) }, diff --git a/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt b/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt index 040457fd..202f21f8 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/SeedWordItem.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -22,7 +21,6 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -32,18 +30,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.PopupProperties -import com.brainwallet.ui.screens.welcome.WelcomeScreen import com.brainwallet.ui.theme.BrainwalletTheme import com.brainwallet.ui.theme.chilli @@ -56,10 +48,14 @@ fun SeedWordItem( ) { SeedWordItemBox(modifier = modifier) { Text( - modifier = Modifier.padding(vertical = 12.dp), + modifier = Modifier + .padding(vertical = 12.dp) + .weight(1f), text = label, style = MaterialTheme.typography.bodyMedium, - color = if (isError) chilli else BrainwalletTheme.colors.content + color = if (isError) chilli else BrainwalletTheme.colors.content, + overflow = TextOverflow.Ellipsis, + maxLines = 1 ) trailingIcon?.let { icon -> Spacer(modifier = Modifier.width(8.dp)) diff --git a/app/src/main/java/com/brainwallet/ui/composable/SeedWordsLayout.kt b/app/src/main/java/com/brainwallet/ui/composable/SeedWordsLayout.kt new file mode 100644 index 00000000..a8f3d98c --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/SeedWordsLayout.kt @@ -0,0 +1,27 @@ +package com.brainwallet.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun SeedWordsLayout( + modifier: Modifier = Modifier, + content: LazyGridScope.() -> Unit +) { + LazyVerticalGrid( + modifier = modifier + .height(220.dp), + columns = GridCells.Fixed(3), //fixed 3 + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt b/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt index b3559014..d96cda42 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/composable/HomeSettingDrawerSheet.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.coroutineScope @@ -76,6 +77,7 @@ fun HomeSettingDrawerSheet( ) { LazyColumn( modifier = Modifier + .testTag("lazyColumnSetting") .weight(1f) .padding(top = headerPadding.dp) .wrapContentHeight(align = Alignment.Top) @@ -83,6 +85,7 @@ fun HomeSettingDrawerSheet( item { SecurityDetail( modifier = Modifier + .testTag("settingSecurity") .fillMaxSize() .wrapContentHeight(), shareAnalyticsDataEnabled = state.shareAnalyticsDataEnabled, @@ -94,6 +97,7 @@ fun HomeSettingDrawerSheet( item { LanguageDetail( modifier = Modifier + .testTag("settingLanguage") .fillMaxSize() .wrapContentHeight(), selectedLanguage = state.selectedLanguage, @@ -110,6 +114,7 @@ fun HomeSettingDrawerSheet( item { CurrencyDetail( modifier = Modifier + .testTag("settingCurrency") .fillMaxSize() .wrapContentHeight(), selectedCurrency = state.selectedCurrency, @@ -125,6 +130,7 @@ fun HomeSettingDrawerSheet( item { GamesDetail( modifier = Modifier + .testTag("settingGames") .fillMaxSize() .wrapContentHeight() ) @@ -132,6 +138,7 @@ fun HomeSettingDrawerSheet( item { LitecoinBlockchainDetail( modifier = Modifier + .testTag("settingBlockchain") .fillMaxSize() .wrapContentHeight(), selectedCurrency = state.selectedCurrency, @@ -144,6 +151,7 @@ fun HomeSettingDrawerSheet( } item { SettingRowItem( + modifier = Modifier.testTag("settingSupport"), title = stringResource(R.string.settings_title_support), description = "brainwallet.co/support.html", onClick = { @@ -155,6 +163,7 @@ fun HomeSettingDrawerSheet( } item { SettingRowItem( + modifier = Modifier.testTag("settingSocialMedia"), title = stringResource(R.string.settings_title_social_media), description = "linktr.ee/brainwallet", onClick = { @@ -166,7 +175,9 @@ fun HomeSettingDrawerSheet( } item { // Lock / Unlock - LockSettingRowItem { + LockSettingRowItem( + modifier = Modifier.testTag("settingLock"), + ) { viewModel.onEvent(SettingsEvent.OnToggleLock) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt index 8f0ab2eb..871b466f 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/composable/settingsrows/LockSettingRowItem.kt @@ -4,14 +4,17 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.brainwallet.R @Composable fun LockSettingRowItem( + modifier: Modifier = Modifier, onClick: () -> Unit, ) { SettingRowItem( + modifier = modifier, title = stringResource(R.string.settings_title_lock), onClick = onClick ) { diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt index 291015e2..8292904c 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialog.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType @@ -152,7 +153,10 @@ fun ReceiveDialog( } }, actions = { - IconButton(onClick = onDismissRequest) { + IconButton( + modifier = Modifier.testTag("buttonClose"), + onClick = onDismissRequest + ) { Icon( Icons.Default.Close, contentDescription = stringResource(R.string.AccessibilityLabels_close), diff --git a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt index 80ab1984..f6b2ba8a 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/home/receive/ReceiveDialogViewModel.kt @@ -50,7 +50,7 @@ class ReceiveDialogViewModel( address = address, qrBitmap = QRUtils.generateQR(event.context, "litecoin:${address}"), fiatCurrencies = CurrencyDataSource.getInstance(event.context) - .getAllCurrencies(true), + .getCurrenciesForBuy(), ) } @@ -154,9 +154,6 @@ class ReceiveDialogViewModel( "baseCurrencyAmount" to currentState.fiatAmount.toString(), "language" to appSetting.value.languageCode, "walletAddress" to currentState.address, - "defaultCurrencyCode" to "ltc", - "currencyCode" to "ltc", - "themeId" to "main-v1.0.0", "theme" to if (appSetting.value.isDarkMode) "dark" else "light" ) ) diff --git a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt index f4d8ba3a..845a9073 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/welcome/WelcomeScreen.kt @@ -3,6 +3,7 @@ package com.brainwallet.ui.screens.welcome import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,10 +11,12 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -26,6 +29,8 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -60,7 +65,8 @@ fun WelcomeScreen( val configuration = LocalConfiguration.current val screenHeight = configuration.screenHeightDp - var mainBoxFactor = 0.5 + val density = LocalDensity.current.density + val mainBoxFactor = if (density > 2) 0.5 else 0.4 val thirdOfScreenHeight = (screenHeight * mainBoxFactor).toInt() LaunchedEffect(Unit) { @@ -72,11 +78,9 @@ fun WelcomeScreen( val buttonFontSize = 24 val thinButtonFontSize = 22 val toggleButtonSize = 45 - val leadTrailPadding = 18 - val halfLeadTrailPadding = leadTrailPadding / 2 - val doubleLeadTrailPadding = leadTrailPadding * 2 + val leadTrailPadding = 8 val rowPadding = 8 - val versionPadding = 12 + val versionPadding = 8 val activeRowHeight = 58 val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.welcomeemoji20250212)) @@ -85,16 +89,12 @@ fun WelcomeScreen( iterations = LottieConstants.IterateForever ) - Column( + Box( modifier = Modifier .fillMaxSize() - .background(BrainwalletTheme.colors.surface), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + .background(BrainwalletTheme.colors.surface) + .verticalScroll(rememberScrollState()), ) { - - Spacer(modifier = Modifier.weight(0.2f)) - Image( painterResource(R.drawable.brainwallet_logotype_white), contentDescription = "brainwallet_logotype_white", @@ -103,123 +103,132 @@ fun WelcomeScreen( BrainwalletTheme.colors.content, ), modifier = Modifier + .align(Alignment.TopCenter) .fillMaxWidth() - .padding(doubleLeadTrailPadding.dp) + .padding(horizontal = 55.dp) + .padding(vertical = 30.dp) ) // Animation Placeholder - Card( + LottieAnimation( modifier = Modifier + .offset(y = 120.dp) .fillMaxWidth() - .height(thirdOfScreenHeight.dp) .padding(leadTrailPadding.dp) - ) { - - LottieAnimation( - modifier = Modifier.background(BrainwalletTheme.colors.surface), - composition = composition, - contentScale = ContentScale.Fit, - progress = { progress } - ) - } - // TODO: implement later, for now just comment this - Row( - modifier = Modifier - .fillMaxWidth() - .height(activeRowHeight.dp) - .padding(horizontal = leadTrailPadding.dp) - .padding(vertical = rowPadding.dp), - horizontalArrangement = Arrangement.SpaceEvenly + .background( + BrainwalletTheme.colors.surface, + BrainwalletTheme.shapes.large + ) + .height(thirdOfScreenHeight.dp) + .clip(BrainwalletTheme.shapes.large), + composition = composition, + contentScale = ContentScale.FillWidth, + alignment = Alignment.Center, + progress = { progress } + ) + Column( + modifier = Modifier.align(Alignment.BottomCenter), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - - BrainwalletButton( + Row( modifier = Modifier - .weight(1f) - .fillMaxWidth(), - onClick = { - viewModel.onEvent(WelcomeEvent.OnLanguageSelectorButtonClick) - } + .fillMaxWidth() + .height(activeRowHeight.dp) + .padding(horizontal = leadTrailPadding.dp) + .padding(vertical = rowPadding.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { - Text( - text = state.selectedLanguage.title, - fontSize = 14.sp, - color = BrainwalletTheme.colors.content + + BrainwalletButton( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + onClick = { + viewModel.onEvent(WelcomeEvent.OnLanguageSelectorButtonClick) + } + ) { + Text( + text = state.selectedLanguage.title, + fontSize = 14.sp, + color = BrainwalletTheme.colors.content + ) + } + + Spacer(modifier = Modifier.weight(0.1f)) + + DarkModeToggleButton( + modifier = Modifier + .width(toggleButtonSize.dp) + .aspectRatio(1f), + checked = state.darkMode, + onCheckedChange = { + viewModel.onEvent(WelcomeEvent.OnToggleDarkMode) + } ) - } - Spacer(modifier = Modifier.weight(0.1f)) + Spacer(modifier = Modifier.weight(0.1f)) - DarkModeToggleButton( - modifier = Modifier - .width(toggleButtonSize.dp) - .aspectRatio(1f), - checked = state.darkMode, - onCheckedChange = { - viewModel.onEvent(WelcomeEvent.OnToggleDarkMode) + BrainwalletButton( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + onClick = { viewModel.onEvent(WelcomeEvent.OnFiatButtonClick) } + ) { + Text( + text = state.selectedCurrency.name, + fontSize = 14.sp, + color = BrainwalletTheme.colors.content + ) } - ) - - Spacer(modifier = Modifier.weight(0.1f)) - BrainwalletButton( + } + // Ready Button + BorderedLargeButton( + onClick = { + onNavigate.invoke(UiEffect.Navigate(Route.Ready)) + }, + shape = RoundedCornerShape(50), modifier = Modifier - .weight(1f) - .fillMaxWidth(), - onClick = { viewModel.onEvent(WelcomeEvent.OnFiatButtonClick) } + .padding(horizontal = leadTrailPadding.dp) + .height(activeRowHeight.dp) + ) { Text( - text = state.selectedCurrency.name, - fontSize = 14.sp, - color = BrainwalletTheme.colors.content + text = stringResource(R.string.ready), + fontSize = buttonFontSize.sp, + fontWeight = FontWeight.SemiBold, ) } - } - // Ready Button - BorderedLargeButton( - onClick = { - onNavigate.invoke(UiEffect.Navigate(Route.Ready)) - }, - shape = RoundedCornerShape(50), - modifier = Modifier - .padding(horizontal = leadTrailPadding.dp) - .padding(vertical = rowPadding.dp) - .height(activeRowHeight.dp) - - ) { - Text( - text = stringResource(R.string.ready), - fontSize = buttonFontSize.sp, - fontWeight = FontWeight.SemiBold, - ) - } + // Restore Button + BorderedLargeButton( + onClick = { + onNavigate.invoke(UiEffect.Navigate(Route.InputWords())) + }, + shape = RoundedCornerShape(50), + modifier = Modifier + .testTag("buttonRestore") + .padding(horizontal = leadTrailPadding.dp) + .height(activeRowHeight.dp) + .clip(RoundedCornerShape(50)) + ) { + Text( + text = stringResource(R.string.restore), + fontSize = thinButtonFontSize.sp, + fontWeight = FontWeight.Thin, + ) + } - // Restore Button - BorderedLargeButton( - onClick = { - onNavigate.invoke(UiEffect.Navigate(Route.InputWords())) - }, - shape = RoundedCornerShape(50), - modifier = Modifier - .padding(horizontal = leadTrailPadding.dp) - .padding(vertical = rowPadding.dp) - .height(activeRowHeight.dp) - .clip(RoundedCornerShape(50)) - ) { Text( - text = stringResource(R.string.restore), - fontSize = thinButtonFontSize.sp, - fontWeight = FontWeight.Thin, + modifier = Modifier.padding(vertical = versionPadding.dp), + text = BRConstants.APP_VERSION_NAME_CODE, + fontSize = 13.sp, + color = BrainwalletTheme.colors.content ) } - - Text( modifier = Modifier - .padding(vertical = versionPadding.dp), - text = BRConstants.APP_VERSION_NAME_CODE, - fontSize = 13.sp, - color = BrainwalletTheme.colors.content - ) } //language selector diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt index 171186ab..214982a7 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedproveit/YourSeedProveItScreen.kt @@ -9,27 +9,24 @@ import android.content.ClipData import android.content.ClipDescription import android.media.MediaPlayer import android.view.View -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.draganddrop.dragAndDropSource import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -60,6 +57,7 @@ import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar import com.brainwallet.ui.composable.LargeButton import com.brainwallet.ui.composable.SeedWordItem +import com.brainwallet.ui.composable.SeedWordsLayout import org.koin.compose.koinInject @Composable @@ -72,7 +70,7 @@ fun YourSeedProveItScreen( val context = LocalContext.current /// Layout values - val columnPadding = 18 + val columnPadding = 12 val horizontalVerticalSpacing = 8 val spacerHeight = 48 val maxItemsPerRow = 3 @@ -104,23 +102,12 @@ fun YourSeedProveItScreen( } }) }, - floatingActionButton = { - if (state.orderCorrected.not()) { - FloatingActionButton( - onClick = { - viewModel.onEvent(YourSeedProveItEvent.OnClear) - } - ) { - Icon(Icons.Default.Clear, contentDescription = null) - } - } - } ) { paddingValues -> Column( modifier = Modifier + .fillMaxSize() .padding(paddingValues) .padding(columnPadding.dp) - .fillMaxSize() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), @@ -138,17 +125,10 @@ fun YourSeedProveItScreen( ) ) - Spacer(modifier = Modifier.height(spacerHeight.dp)) - - FlowRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - maxItemsInEachRow = maxItemsPerRow - ) { - state.correctSeedWords.values.forEachIndexed { index, (expectedWord, actualWord) -> + Spacer(modifier = Modifier.weight(0.1f)) + SeedWordsLayout { + itemsIndexed(items = state.correctSeedWords.values.toList()) { index: Int, (expectedWord, actualWord): SeedWordItem -> val label = if (expectedWord != actualWord && actualWord.isEmpty()) { "${index + 1}" } else { @@ -158,7 +138,6 @@ fun YourSeedProveItScreen( SeedWordItem( modifier = Modifier .fillMaxWidth() - .weight(1f) .dragAndDropTarget( shouldStartDragAndDrop = { event -> event @@ -195,17 +174,20 @@ fun YourSeedProveItScreen( Spacer(modifier = Modifier.weight(1f)) - AnimatedVisibility(visible = state.orderCorrected.not()) { - FlowRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - maxItemsInEachRow = maxItemsPerRow - ) { - state.shuffledSeedWords.forEachIndexed { index, word -> + SeedWordsLayout { + itemsIndexed(items = state.shuffledSeedWords) { index, word -> + + val isWordUsedCorrectly = + state.correctSeedWords.values.any { (expectedWord, actualWord) -> + expectedWord == word && actualWord == word + } + + if (isWordUsedCorrectly) { + Box(modifier = Modifier.fillMaxWidth()) + } else { SeedWordItem( modifier = Modifier + .fillMaxWidth() .dragAndDropSource { detectTapGestures( onLongPress = { @@ -230,20 +212,25 @@ fun YourSeedProveItScreen( } ) } + + } } - AnimatedVisibility(visible = state.orderCorrected) { - LargeButton( - onClick = { + + LargeButton( + onClick = { + if (state.orderCorrected) { onNavigate.invoke(UiEffect.Navigate(Route.TopUp)) - }, - ) { - Text( - text = stringResource(R.string.game_and_sync), - style = MaterialTheme.typography.labelLarge - ) - } + } else { + viewModel.onEvent(YourSeedProveItEvent.OnClear) + } + }, + ) { + Text( + text = stringResource(if (state.orderCorrected) R.string.game_and_sync else R.string.reset_start_over).uppercase(), + style = MaterialTheme.typography.labelLarge + ) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt index c541f588..cd7b1f76 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/yourseedwords/YourSeedWordsScreen.kt @@ -5,12 +5,11 @@ package com.brainwallet.ui.screens.yourseedwords import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -36,6 +35,7 @@ import com.brainwallet.ui.composable.BrainwalletScaffold import com.brainwallet.ui.composable.BrainwalletTopAppBar import com.brainwallet.ui.composable.LargeButton import com.brainwallet.ui.composable.SeedWordItem +import com.brainwallet.ui.composable.SeedWordsLayout import org.koin.compose.koinInject @Composable @@ -45,12 +45,20 @@ fun YourSeedWordsScreen( viewModel: YourSeedWordsViewModel = koinInject() ) { /// Layout values - val columnPadding = 16 + val columnPadding = 12 val horizontalVerticalSpacing = 8 - val spacerHeight = 48 - val maxItemsPerRow = 3 - val leadingCopyPadding = 16 - val detailLineHeight = 28 + val spacerHeight = 36 + val leadingCopyPadding = 8 + val detailLineHeight = 24 + + LaunchedEffect(Unit) { + viewModel.uiEffect.collect { effect -> + when (effect) { + is UiEffect.Navigate -> onNavigate.invoke(effect) + else -> Unit + } + } + } LaunchedEffect(Unit) { viewModel.uiEffect.collect { effect -> @@ -78,9 +86,9 @@ fun YourSeedWordsScreen( ) { paddingValues -> Column( modifier = Modifier + .fillMaxSize() .padding(paddingValues) .padding(columnPadding.dp) - .fillMaxSize() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), @@ -102,16 +110,10 @@ fun YourSeedWordsScreen( ) ) - Spacer(modifier = Modifier.height(spacerHeight.dp)) + Spacer(modifier = Modifier.weight(0.1f)) - FlowRow( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - verticalArrangement = Arrangement.spacedBy(horizontalVerticalSpacing.dp), - maxItemsInEachRow = maxItemsPerRow - ) { - seedWords.forEachIndexed { index, word -> + SeedWordsLayout(modifier = Modifier.weight(1f)) { + itemsIndexed(items = seedWords) { index, word -> SeedWordItem( modifier = Modifier .fillMaxWidth() @@ -121,14 +123,14 @@ fun YourSeedWordsScreen( } } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(0.1f)) Text( text = stringResource(R.string.blockchain_litecoin), style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center) ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(0.2f)) LargeButton( onClick = { diff --git a/app/src/main/res/layout/activity_bread.xml b/app/src/main/res/layout/activity_bread.xml index 7ad85ca9..e17e37f7 100644 --- a/app/src/main/res/layout/activity_bread.xml +++ b/app/src/main/res/layout/activity_bread.xml @@ -146,7 +146,7 @@ android:layout_height="64dp" android:layout_alignParentBottom="true" app:labelVisibilityMode="labeled" - tools:menu="@menu/bottom_nav_menu_us" /> + tools:menu="@menu/bottom_nav_menu" /> diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index dc0df45e..e5323f61 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -1,28 +1,22 @@ - - + + - - - - - - + android:title="@string/bottom_nav_item_buy_receive_title" /> \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu_us.xml b/app/src/main/res/menu/bottom_nav_menu_us.xml deleted file mode 100644 index 187ac237..00000000 --- a/app/src/main/res/menu/bottom_nav_menu_us.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7c7deb0..12fca16d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -855,4 +855,5 @@ Buy / Receive Custom + Reset / Start Over diff --git a/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt b/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt index 99874e08..4dbbde76 100644 --- a/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt +++ b/app/src/screengrab/kotlin/com/brainwallet/BrainwalletScreengrabApp.kt @@ -1,20 +1,30 @@ package com.brainwallet import com.brainwallet.data.source.RemoteConfigSource -import com.brainwallet.di.Module -import com.brainwallet.util.cryptography.KeyStoreKeyGenerator -import com.brainwallet.util.cryptography.KeyStoreManager +import com.brainwallet.di.appModule +import com.brainwallet.di.dataModule +import com.brainwallet.di.viewModelModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.GlobalContext.startKoin +import org.koin.core.logger.Level import timber.log.Timber class BrainwalletScreengrabApp : BrainwalletApp() { override fun initializeModule() { Timber.d("Timber: initializeModule Screengrab") - module = Module( - remoteConfigSource = MockRemoteConfigSource() - ) - module.remoteConfigSource.initialize() - keyStoreManager = KeyStoreManager(this, KeyStoreKeyGenerator.Impl()) +// module = Module( +// remoteConfigSource = MockRemoteConfigSource() +// ) +// module.remoteConfigSource.initialize() +// keyStoreManager = KeyStoreManager(this, KeyStoreKeyGenerator.Impl()) + + startKoin { + androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.ERROR) + androidContext(this@BrainwalletScreengrabApp) + modules(dataModule, viewModelModule, appModule) + } } } diff --git a/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt b/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt index 37b7e46a..6be7752e 100644 --- a/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt +++ b/app/src/test/java/com/brainwallet/currency/CurrencyTests.kt @@ -50,6 +50,13 @@ class CurrencyTests { assertEquals(currencyDataSource?.getAllCurrencies(true)?.count(), 0) } + @Test + fun `invoke CurrencyDataSource instance and Brainwallet filtered Fiats for Buy, should return the correct number of supported currencies (moonpay)`() { + //The actual number of BW currencies is 16. The 0 is a placeholder and needs to be replaced with a db query. + mockCursorDataFromDatabase() + assertEquals(currencyDataSource?.currenciesForBuy?.count(), 0) + } + @Test fun `invoke CurrencyDataSource instance and open database, then should validate database was not null`() { val database = currencyDataSource?.openDatabase() diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5552e997..a0bcf944 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -16,6 +16,10 @@ default_platform(:android) platform :android do + before_all do + setup_circle_ci + end + desc "Runs all the unit tests" lane :test do gradle(task: "test") @@ -25,7 +29,7 @@ platform :android do lane :build_and_screengrab do adb(command: "shell pm clear ltd.grunt.brainwallet.screengrab") rescue nil gradle( - tasks: ['clean', 'assembleScreengrabDebug', 'assembleScreengrabAndroidTest'] + tasks: ['assembleScreengrabDebug', 'assembleScreengrabAndroidTest'] ) build_dir = "#{ENV['PWD']}/app/build/" UI.message "Build directory: #{build_dir}"