diff --git a/.circleci/config.yml b/.circleci/config.yml index 6d4ff714..ca18843c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,13 +67,15 @@ jobs: unit-test: executor: name: android/android_docker - resource_class: large tag: 2024.07.1-ndk + resource_class: large + environment: + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2" steps: - setup_environment - run: name: "Execute Unit Tests" - command: ./gradlew testBrainwalletDebugUnitTest --no-daemon --max-workers=2 -Dorg.gradle.jvmargs="-Xmx8g" -Dkotlin.compiler.execution.strategy=in-process + command: ./gradlew testBrainwalletDebugUnitTest --no-daemon --max-workers=2 - android/save_gradle_cache - run: name: Save test results diff --git a/.github/workflows/auto-translate.yml b/.github/workflows/auto-translate.yml index 88d71d72..c80bb89e 100644 --- a/.github/workflows/auto-translate.yml +++ b/.github/workflows/auto-translate.yml @@ -1,5 +1,4 @@ -name: Auto-Translate strings.xml - +name: 🇧🇷 🇳🇱 🇹🇭 Auto-Translate strings.xml on: push: branches: @@ -51,7 +50,8 @@ jobs: git checkout -b "$BRANCH" git add app/src/main/res/values-*/strings.xml git commit -m "chore(i18n): auto-translate missing strings [skip ci] - Languages updated: ar, zh-rTW, zh-rCN, fr, de, fa, pa, pl, in, es, sv, uk, ru, tr, ja, ko, hi, it, pt-rBR + + Languages updated: ar, de, es, fa, fr, hi, in, it, ja, ko, nl, pa, pl, pt-rBR, ru, sv, th, tr, uk, zh-rCN, zh-rTW Triggered by: ${{ github.sha }} " git push origin "$BRANCH" @@ -63,7 +63,7 @@ jobs: gh pr create \ --title "🌍 Auto-translations: missing strings updated" \ --body "$(cat /tmp/pr_body.md)" \ - --base main \ + --base develop \ --head "$BRANCH" \ --label "translations" \ --reviewer "${{ github.actor }}" diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml new file mode 100644 index 00000000..bb601962 --- /dev/null +++ b/.github/workflows/notify.yml @@ -0,0 +1,27 @@ +name: Notify Mirror on PR + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + trigger-mirror: + runs-on: ubuntu-latest + steps: + - name: Trigger mirror workflow + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.MIRROR_REPO_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'gruntsoftware', + repo: 'brainwallet-android', + workflow_id: 'sync.yml', + ref: 'main', + inputs: { + source: '${{ github.event.repository.name }}', + pr_number: '${{ github.event.pull_request.number }}', + pr_title: '${{ github.event.pull_request.title }}', + branch: '${{ github.head_ref }}' + } + }) \ No newline at end of file diff --git a/.github/workflows/pr-summary-copilot.yml b/.github/workflows/pr-summary-copilot.yml index bb4892fa..f631db1a 100644 --- a/.github/workflows/pr-summary-copilot.yml +++ b/.github/workflows/pr-summary-copilot.yml @@ -2,7 +2,7 @@ name: 🤖 Copilot PR Summary on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, reopened] branches: - develop - main diff --git a/.gitignore b/.gitignore index 554f0f3b..b3531703 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ app/src/.DS_Store /app/src/main/assets/service-data.json /app/src/main/assets/canary-file.json service-data.json -canary-file.json \ No newline at end of file +canary-file.json + +# Claude +.claude/ \ No newline at end of file diff --git a/README.md b/README.md index 1d01cf45..c92a651c 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,46 @@ The open source code of Brainwallet Android **develop**: [![CircleCI](https://dl.circleci.com/status-badge/img/gh/gruntsoftware/android/tree/develop.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/gruntsoftware/android/tree/develop) -## Release Notes -### v4.8.3 + # 🚀 Brainwallet Android — Release Notes +### v4.9.0 + +### ✨ New Features + +#### 📋 Copy Transaction Details to Clipboard +Transaction details can now be copied directly to the clipboard from the transaction detail view. A new `BRClipboardManager.putDetailsClipboard()` method handles the copy action with full analytics tracking, making it easy to share or reference transaction information outside the app. + +#### 📊 Analytics Tracking for Key Interactions +Firebase analytics events are now fired for core user interactions — fiat/LTC toggle, balance visibility toggle, and transaction detail copy — giving the team better insight into how users engage with the wallet's main screens. + +--- + +### 🎨 UI / Design System + +#### 🧱 Unified `bentoSurface` Design System +A new `bentoSurface()` modifier in `BentoModifiers.kt` consolidates gradient and border styling across the entire bento UI. Previously hardcoded values like `1.dp` borders are now driven by shared constants (`bentoBorderWidth`, `bentoCornerRadius`, `bentoCornerRadius`, `bentoSpacer`, `transactionActionHt`, `transactionDetailHt`), making the visual system consistent and easy to maintain going forward. + +#### 🃏 Bento Section Refactors +- **GameHub** — migrated from `Card` to `Box` with `bentoSurface()`, with a `clickable` modifier added +- **Favourites** — refactored to `bentoSurface()` with improved `Spacer` layout +- **LTC Picker** — label repositioned to top, spacing and item heights adjusted +- **Transactions** — `TransactionFilter`, `TransactionsBentoScreen`, `ConfirmationStatus`, `CopyTransactionWidget`, `ExportTransactionsWidget`, `NoTxRow`, `TransactionDetail`, `TransactionFilterWidget`, `TransactionRow` all updated to unified styling +- **Tutorials & Bottom Nav Bar** — updated to use unified theme system + +--- + +### 🔧 Technical Changes +- `MainScreen`, `MainScreenEvent`, and `MainViewModel` updated for new event handling and state management +- `NoWifiBalanceAlertScreen` and `ReceiveDialog` border widths replaced with `bentoBorderWidth` constant +- `strings.xml` updated with new/updated string resources +- `gradle/libs.versions.toml` and `app/build.gradle.kts` dependencies updated +- `detekt-app-baseline.xml` baseline updated +- Version bumped: **v4.8.4 (202506314) → v4.9.0 (202506315)** +- Release/v4.7.2 202506296 by @kcw-grunt in https://github.com/gruntsoftware/android/pull/134 +- Release/v4.8.0 by @kcw-grunt in https://github.com/gruntsoftware/android/pull/145 + +**Full Changelog**: https://github.com/gruntsoftware/android/compare/v4.7.2...v4.9.0 +### v4.8.3 ## ✨ New UI — Favourites & Tutorials Bento Sections **PR [#191](https://github.com/gruntsoftware/android/pull/191)** diff --git a/android-build-logic b/android-build-logic index 23d87315..124541f9 160000 --- a/android-build-logic +++ b/android-build-logic @@ -1 +1 @@ -Subproject commit 23d873150461e257a082378b6558c2e0761b480d +Subproject commit 124541f95776c2674fa867fe4eb066f6b22f7624 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6da6667..5be208d8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,5 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties +import org.gradle.kotlin.dsl.androidTestImplementation import org.gradle.kotlin.dsl.grunt plugins { @@ -34,8 +35,8 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 35 - versionCode = 202506315 - versionName = "v4.9.0" + versionCode = 202506323 + versionName = "v4.9.1" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -179,9 +180,18 @@ android { packaging { resources { pickFirsts.add("protobuf.meta") + excludes += setOf( + "META-INF/LICENSE.md", + "META-INF/LICENSE-notice.md", + "META-INF/NOTICE.md", + "META-INF/NOTICE", + "META-INF/LICENSE" + ) } } - + testFixtures { + enable = true + } //TODO: rename output apk/bundle } @@ -190,6 +200,8 @@ dependencies { implementation(project(":iap")) implementation(project(":core")) implementation("androidx.webkit:webkit:1.9.0") + testImplementation(testFixtures(project(":app"))) + androidTestImplementation(testFixtures(project(":app"))) implementation(grunt.androidx.core.ktx) implementation(grunt.app.startup) implementation(libs.androidx.appcompat) @@ -218,7 +230,6 @@ dependencies { implementation(libs.bundles.google.play.review) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.coroutines.android) - implementation (libs.airbnb.lottie.compose) implementation(platform(grunt.koin.bom)) implementation(grunt.bundles.koin) implementation(platform(grunt.koin.annotation.bom)) @@ -229,6 +240,7 @@ dependencies { implementation(libs.androidx.runtime) implementation(libs.androidx.foundation) implementation(libs.androidx.animation) + implementation(libs.play.services.games) ksp(grunt.koin.annotation.compiler) implementation(platform(libs.squareup.okhttp.bom)) implementation(libs.bundles.squareup.okhttp) @@ -259,8 +271,14 @@ dependencies { androidTestImplementation(libs.bundles.android.test) androidTestImplementation(libs.fastlane.screengrab) androidTestImplementation(libs.slf4j.android) + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") + androidTestImplementation("io.mockk:mockk-android:1.13.8") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") } tasks.withType { jvmArgs("-XX:+EnableDynamicAgentLoading") } + diff --git a/app/src/androidTest/java/com/brainwallet/ui/screens/send/SendInstrumentationTest.kt b/app/src/androidTest/java/com/brainwallet/ui/screens/send/SendInstrumentationTest.kt new file mode 100644 index 00000000..7e95d950 --- /dev/null +++ b/app/src/androidTest/java/com/brainwallet/ui/screens/send/SendInstrumentationTest.kt @@ -0,0 +1,164 @@ +package com.brainwallet.ui.screens.send + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.brainwallet.presenter.entities.TransactionItem +import com.brainwallet.tools.security.PostAuth +import com.brainwallet.wallet.BRWalletManager +import io.mockk.mockk +import junit.framework.TestCase.assertFalse +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.assertNotNull +import org.junit.runner.RunWith +import kotlin.test.DefaultAsserter.assertTrue + +/** + * Instrumentation tests that exercise [BWSender] against the REAL + * [com.brainwallet.wallet.BRWalletManager] JNI layer. These are the only way to catch regressions in + * the native boundary — MockK cannot stub native methods on the JVM. + * + * Run on an emulator/device where libcore.so and libbreadwallet.so are loaded. + * + * NOTE: These tests assume a wallet has been initialised by the test runner or + * a prior setup step. They `assumeTrue(isCreated)` so they are safely skipped + * on fresh installs rather than failing spuriously. + */ +@LargeTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class SendInstrumentationTest { + + private lateinit var context: Context + private lateinit var walletManager: BRWalletManager + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + walletManager = BRWalletManager.getInstance() + } + + @Before + fun loadNativeLibrary() { + System.loadLibrary("core-lib") + } + + // ------------------------------------------------------------------------- + // Native address validation — the regression that crashed production + // ------------------------------------------------------------------------- + + @Test + fun validateAddress_acceptsKnownGoodLitecoinAddress() { + // Litecoin P2PKH sample address (starts with L). + val addr = "LcWHwzZitnqQsXwi6UScRjtjDFYjR7t24t" + assertTrue( + "Native validator must accept canonical L-prefix addresses", + BRWalletManager.getInstance().validateAddress(addr) + ) + } + + @Test + fun validateAddress_rejectsEmptyString() { + assertFalse(BRWalletManager.getInstance().validateAddress("")) + } + + @Test + fun validateAddress_rejectsGarbage() { + assertFalse(BRWalletManager.getInstance().validateAddress("not-a-real-address")) + } + + @Test + fun validateAddress_rejectsBitcoinAddress() { + // Bitcoin address should be rejected by a Litecoin wallet. + assertFalse( + BRWalletManager.getInstance().validateAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa") + ) + } + + // ------------------------------------------------------------------------- + // BWSender through the real JNI layer + // ------------------------------------------------------------------------- + + @Test + fun bwSender_withRealWalletManager_handlesNullTransactionGracefully() = runTest { + assumeTrue("Skipping — wallet not created on this device", walletManager.isCreated()) + + val postAuth = mockk(relaxed = true) + val sender = BWSender( + context = context, + getWalletManager = { walletManager }, + getPostAuth = { postAuth } + ) + + // Send to an invalid address so the native method returns null rather + // than throwing — this is the real-world failure mode that previously + // locked the Send button. + val tx = TransactionItem( + "LhK2kQwiaAvhjWY799cZvMyYwnQAcxkarr", + "LOpsAddrInvalid", + null, + /* sendAmount = */ + 1L, + /* opsFee = */ + 0L, + null, + false, + "" + ) + + val result = sender.prepareTransaction(tx) + + // Either AmountTooSmall or Unknown is acceptable here — what matters is + // that isSending is reset and the app is not permanently locked. + assertNotNull(result) + assertFalse( + "isSending must not leak through real-JNI error paths", + sender.isSending + ) + } + + @Test + fun bwSender_reentrancyGuard_holdsAgainstRealWallet() = runTest { + assumeTrue(walletManager.isCreated()) + + val postAuth = mockk(relaxed = true) + val sender = BWSender( + context = context, + getWalletManager = { walletManager }, + getPostAuth = { postAuth } + ) + + sender.isSending = true + val result = sender.prepareTransaction(buildDummyTx()) + + assert(result is BWSendResult.Error.AlreadySending) + } + + // ------------------------------------------------------------------------- + // Balance read — driven through getBalance which is used by SendViewModel + // to decide isAmountBelowBalance. A zero/stale read is the "Send stays + // disabled" production symptom. + // ------------------------------------------------------------------------- + + @Test + fun getBalance_returnsNonNegativeValue() { + val balance = BRWalletManager.getInstance().getBalance(context) + assertTrue("Balance must never go negative", balance >= 0) + } + + private fun buildDummyTx() = TransactionItem( + "LhK2kQwiaAvhjWY799cZvMyYwnQAcxkarr", + "LOpsAddr", + null, + 100_000L, + 500L, + null, + false, + "" + ) +} diff --git a/app/src/androidTest/java/com/brainwallet/wallet/FakeWalletManager.kt b/app/src/androidTest/java/com/brainwallet/wallet/FakeWalletManager.kt new file mode 100644 index 00000000..975f94e0 --- /dev/null +++ b/app/src/androidTest/java/com/brainwallet/wallet/FakeWalletManager.kt @@ -0,0 +1,9 @@ +package com.brainwallet.wallet + +class FakeWalletManager( + private val created: Boolean = true, + private val addressValid: Boolean = true +) : WalletManager { + override fun isCreated() = created + override fun validateAddress(address: String) = addressValid +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e929fb90..64aeb9f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -110,18 +110,6 @@ android:exported="true" android:launchMode="singleTask" android:screenOrientation="portrait" /> - - - - - - - - - - - - >(persistentListOf()) override val transactionItems: StateFlow> = _transactionItems.asStateFlow() override suspend fun refresh() = withContext(Dispatchers.IO) { - val items = BRWalletManager.getInstance().getTransactions() + val items = transactionSource.getTransactions() ?.toImmutableList() ?: persistentListOf() Timber.d("TxRepositoryImpl refresh: count=${items.size}") _transactionItems.update { items } diff --git a/app/src/main/java/com/brainwallet/data/source/TransactionSource.kt b/app/src/main/java/com/brainwallet/data/source/TransactionSource.kt new file mode 100644 index 00000000..e0a8ee42 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/source/TransactionSource.kt @@ -0,0 +1,7 @@ +package com.brainwallet.data.source + +import com.brainwallet.presenter.entities.TxItem + +interface TransactionSource { + fun getTransactions(): Array? +} diff --git a/app/src/main/java/com/brainwallet/exceptions/PaymentRequestExpiredException.kt b/app/src/main/java/com/brainwallet/exceptions/PaymentRequestExpiredException.kt deleted file mode 100644 index 625127cd..00000000 --- a/app/src/main/java/com/brainwallet/exceptions/PaymentRequestExpiredException.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.brainwallet.exceptions - -import java.security.GeneralSecurityException - -class PaymentRequestExpiredException : GeneralSecurityException { - private constructor() : super() - - constructor(msg: String?) : super("The request is expired!") - - private constructor(msg: String, cause: Throwable) : super(msg, cause) - - private constructor(cause: Throwable) : super(cause) - - companion object { - val TAG: String = PaymentRequestExpiredException::class.java.name - } -} 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 79c86137..39ee3517 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java @@ -234,34 +234,9 @@ private void setListeners() { public boolean handleNavigationItemSelected(int menuItemId) { if (mSelectedBottomNavItem == menuItemId) return true; mSelectedBottomNavItem = menuItemId; + //TODO: Removal as Compose Send workflow is live and in Production + // Will remove as cruft as a backlog item - //TODO: revisit - // we are using compose, that's why commented, will remove it after fully migrated to compose - if (menuItemId == R.id.nav_history) { - ExtensionKt.replaceFragment(BreadActivity.this, new HistoryFragment(), false, R.id.fragment_container); - } else if (menuItemId == R.id.nav_send) { - if (BRAnimator.isClickAllowed()) { - BRAnimator.showSendFragment(BreadActivity.this, null); - } - mSelectedBottomNavItem = 0; - } else if (menuItemId == R.id.nav_receive) { - if (BRAnimator.isClickAllowed()) { -// BRAnimator.showReceiveFragment(BreadActivity.this, true); - //todo - ReceiveDialogFragment.show(getSupportFragmentManager()); - } - mSelectedBottomNavItem = 0; - } -// else if (menuItemId == R.id.nav_buy) { -// if (BRAnimator.isClickAllowed()) { -//// BRAnimator.showMoonpayFragment(BreadActivity.this); -// } -// -// -// -// -// mSelectedBottomNavItem = 0; -// } return true; } diff --git a/app/src/main/java/com/brainwallet/presenter/activities/LoginActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/LoginActivity.java deleted file mode 100644 index 450c1321..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/LoginActivity.java +++ /dev/null @@ -1,297 +0,0 @@ -package com.brainwallet.presenter.activities; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.os.Handler; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import com.brainwallet.R; -import com.brainwallet.navigation.Route; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.presenter.customviews.BRKeyboard; -import com.brainwallet.data.model.CurrencyEntity; -import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.tools.animation.SpringAnimator; -import com.brainwallet.tools.manager.AnalyticsManager; -import com.brainwallet.tools.manager.BRSharedPrefs; -import com.brainwallet.tools.security.AuthManager; -import com.brainwallet.tools.security.BRKeyStore; -import com.brainwallet.tools.sqlite.CurrencyDataSource; -import com.brainwallet.tools.threads.BRExecutor; -import com.brainwallet.constants.BWConstants; -import com.brainwallet.tools.util.BRCurrency; -import com.brainwallet.ui.BrainwalletActivity; -import com.brainwallet.wallet.BRWalletManager; - -import java.math.BigDecimal; - -import timber.log.Timber; - -@Deprecated -public class LoginActivity extends BRActivity { - private BRKeyboard keyboard; - private LinearLayout pinLayout; - private View dot1; - private View dot2; - private View dot3; - private View dot4; - private View dot5; - private View dot6; - private StringBuilder pin = new StringBuilder(); - private int pinLimit = 6; - private static LoginActivity app; - - private ImageView unlockedImage; - private TextView unlockedText; - private TextView enterPinLabel; - private TextView versionText; - private ViewGroup ltcPriceConstraintLayout; - - private TextView ltcPriceTextView; - private TextView ltcPriceDescTextView; - - public static boolean appVisible = false; - private boolean inputAllowed = true; - - public static LoginActivity getApp() { - return app; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_pin); - View parentLayout = findViewById(android.R.id.content); - String pin = BRKeyStore.getPinCode(this); - if (pin.isEmpty() || (pin.length() != 4 && pin.length() != 2)) { - //using setpasscode compose - Intent intent = BrainwalletActivity.createIntent(this, new Route.SetPasscode()); - intent.putExtra("noPin", true); - startActivity(intent); - if (!LoginActivity.this.isDestroyed()) finish(); - return; - } - if (BRKeyStore.getPinCode(this).length() == 4) pinLimit = 4; - - keyboard = findViewById(R.id.brkeyboard); - pinLayout = findViewById(R.id.pinLayout); - versionText = findViewById(R.id.version_text); - - ltcPriceTextView = findViewById(R.id.ltcPriceTextView); - ltcPriceDescTextView = findViewById(R.id.ltcPriceDescTextView); - - unlockedImage = findViewById(R.id.unlocked_image); - unlockedText = findViewById(R.id.unlocked_text); - enterPinLabel = findViewById(R.id.enter_pin_label); - ltcPriceConstraintLayout = findViewById(R.id.ltcPriceConstraintLayout); - - dot1 = findViewById(R.id.dot1); - dot2 = findViewById(R.id.dot2); - dot3 = findViewById(R.id.dot3); - dot4 = findViewById(R.id.dot4); - - keyboard.addOnInsertListener(key -> handleClick(key)); - keyboard.setBRButtonTextColor(R.color.white); - keyboard.setShowDot(false); - - keyboard.setCustomButtonBackgroundColor(10, getColor(android.R.color.transparent)); - keyboard.setDeleteImage(getDrawable(R.drawable.ic_delete_white)); - versionText.setText(BWConstants.APP_VERSION_NAME_CODE); - -// DEV: Keeping for reference when REFACTORING -// findViewById(R.id.scanQRCodeImgBut).setOnClickListener(v -> { -// if (!BRAnimator.isClickAllowed()) return; -// try { -// // Check if the camera permission is granted -// if (ContextCompat.checkSelfPermission(app, -// Manifest.permission.CAMERA) -// != PackageManager.PERMISSION_GRANTED) { -// if (ActivityCompat.shouldShowRequestPermissionRationale(app, -// Manifest.permission.CAMERA)) { -// BRDialog.showCustomDialog(app, getString(R.string.Send_cameraUnavailabeTitle_android), -// getString(R.string.Send_cameraUnavailabeMessage_android), getString(R.string.AccessibilityLabels_close), null, brDialogView -> brDialogView.dismiss(), null, null, 0); -// } else { -// ActivityCompat.requestPermissions(app, -// new String[]{Manifest.permission.CAMERA}, -// BWConstants.CAMERA_REQUEST_ID); -// } -// } else { -// Intent intent = new Intent(app, ScanQRActivity.class); -// app.startActivityForResult(intent, SCANNER_REQUEST); -// app.overridePendingTransition(R.anim.fade_up, 0); -// } -// } catch (Exception e) { -// Timber.e(e); -// } -// }); - - setCurrentLtcPrice(); - } - - private void setCurrentLtcPrice() { - String iso = BRSharedPrefs.getIsoSymbol(this); - - String formattedCurrency = null; - CurrencyEntity currency = CurrencyDataSource.getInstance(this).getCurrencyByIso(iso); - if (currency != null) { - final BigDecimal roundedPriceAmount = new BigDecimal(currency.rate).multiply(new BigDecimal(100)) - .divide(new BigDecimal(100), 2, BWConstants.ROUNDING_MODE); - formattedCurrency = BRCurrency.getFormattedCurrencyString(this, iso, roundedPriceAmount); - } else { - Timber.w("The currency related to %s is NULL", iso); - } - - if (formattedCurrency != null) { - ltcPriceTextView.setText(getString(R.string.Login_ltcPrice, formattedCurrency)); - ltcPriceDescTextView.setText(getString(R.string.Login_currentLtcPrice, iso)); - } - } - - @Override - protected void onResume() { - super.onResume(); - updateDots(); - - appVisible = true; - app = this; - inputAllowed = true; - if (!BRWalletManager.getInstance().isCreated()) { - BRExecutor.getInstance().forBackgroundTasks().execute(() -> BRWalletManager.getInstance().initWallet(LoginActivity.this)); - } - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } - - private void handleClick(String key) { - if (!inputAllowed) { - Timber.d("timber: handleClick: input not allowed"); - return; - } - if (key == null) { - Timber.d("timber: handleClick: key is null! "); - return; - } - - if (key.isEmpty()) { - handleDeleteClick(); - } else if (Character.isDigit(key.charAt(0))) { - handleDigitClick(Integer.parseInt(key.substring(0, 1))); - } else { - Timber.d("timber: handleClick: oops: %s", key); - } - } - - - private void handleDigitClick(Integer dig) { - if (pin.length() < pinLimit) - pin.append(dig); - updateDots(); - } - - private void handleDeleteClick() { - if (pin.length() > 0) - pin.deleteCharAt(pin.length() - 1); - updateDots(); - } - - @Override - public void onBackPressed() { - if (getFragmentManager().getBackStackEntryCount() > 0) { - super.onBackPressed(); - } else { - finishAffinity(); - } - } - - private void unlockWallet() { - pin = new StringBuilder(); - ltcPriceConstraintLayout.animate().translationY(-600).setInterpolator(new AccelerateInterpolator()); - pinLayout.animate().translationY(-2000).setInterpolator(new AccelerateInterpolator()); - enterPinLabel.animate().translationY(-1800).setInterpolator(new AccelerateInterpolator()); - keyboard.animate().translationY(2000).setInterpolator(new AccelerateInterpolator()); - unlockedImage.animate().alpha(1f).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - new Handler().postDelayed(() -> { - Intent intent = new Intent(LoginActivity.this, BreadActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.fade_up, R.anim.fade_down); - if (!LoginActivity.this.isDestroyed()) { - LoginActivity.this.finish(); - - } - }, 400); - } - }); - unlockedText.animate().alpha(1f); - } - - private void showFailedToUnlock() { - SpringAnimator.failShakeAnimation(LoginActivity.this, pinLayout); - pin = new StringBuilder(); - new Handler().postDelayed(() -> { - inputAllowed = true; - updateDots(); - }, 1000); - } - - private void updateDots() { - AuthManager.getInstance().updateDots(this, pinLimit, pin.toString(), dot1, dot2, dot3, dot4, R.drawable.ic_pin_dot_white, - () -> { - inputAllowed = false; - if (AuthManager.getInstance().checkAuth(pin.toString(), LoginActivity.this)) { - AuthManager.getInstance().authSuccess(LoginActivity.this); - unlockWallet(); - AnalyticsManager.logCustomEvent(BWConstants._20200217_DUWB); - - } else { - showFailedToUnlock(); - } - }); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - switch (requestCode) { - case BWConstants.CAMERA_REQUEST_ID: { - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - BRAnimator.openScanner(this, BWConstants.SCANNER_REQUEST); - // permission was granted, yay! Do the - // contacts-related task you need to do. - } else { - Timber.i("timber: onRequestPermissionsResult: permission isn't granted for: %s", requestCode); - // permission denied, boo! Disable the - // functionality that depends on this permission. - } - return; - } - - // other 'case' lines to check for other - // permissions this app might request - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/PaperKeyActivity.kt b/app/src/main/java/com/brainwallet/presenter/activities/PaperKeyActivity.kt deleted file mode 100644 index d327d199..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/PaperKeyActivity.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.brainwallet.presenter.activities - -import android.os.Bundle -import androidx.compose.ui.platform.ViewCompositionStrategy -import com.brainwallet.databinding.ActivityPaperKeyBinding -import com.brainwallet.presenter.activities.util.BRActivity -import com.brainwallet.tools.security.PostAuth -import com.brainwallet.ui.theme.setContentWithTheme - -@Deprecated(message = "move to compose") -class PaperKeyActivity : BRActivity() { - - private lateinit var binding: ActivityPaperKeyBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityPaperKeyBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) - - val phrase = intent.getStringExtra("phrase") - if (phrase == null || phrase[phrase.length - 1] == '\u0000') { - finish() - return - } - val seedWords = phrase.split(" ") - - if (seedWords.size != 12) { - finish() - return - } - - binding.composeView.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContentWithTheme { -// YourSeedWordsScreen( -// seedWords = seedWords, -// onEvent = { action -> -// when (action) { -// YourSeedWordsEvent.OnBackClick -> finish() -// YourSeedWordsEvent.OnSavedItClick -> onSavedItClick() -// } -// } -// ) - } - } - } - - private fun onSavedItClick() { - PostAuth.getInstance().onPhraseProveAuth(this, false) - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/PaperKeyProveActivity.kt b/app/src/main/java/com/brainwallet/presenter/activities/PaperKeyProveActivity.kt deleted file mode 100644 index b20ce2fe..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/PaperKeyProveActivity.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.brainwallet.presenter.activities - -import android.os.Bundle -import androidx.compose.ui.platform.ViewCompositionStrategy -import com.brainwallet.databinding.ActivityPaperKeyProveBinding -import com.brainwallet.presenter.activities.util.BRActivity -import com.brainwallet.ui.theme.setContentWithTheme - -@Deprecated(message = "move to compose") -class PaperKeyProveActivity : BRActivity() { - - private lateinit var binding: ActivityPaperKeyProveBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = ActivityPaperKeyProveBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) - - val phrase = intent.getStringExtra("phrase") - if (phrase == null || phrase[phrase.length - 1] == '\u0000') { - finish() - return - } - val seedWords = phrase.split(" ") - - if (seedWords.size != 12) { - finish() - return - } - - binding.composeView.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContentWithTheme { -// YourSeedProveItScreen( -// seedWords = seedWords, -// onEvent = { event -> -// when (event) { -// YourSeedProveItEvent.OnBackClick -> finish() -// YourSeedProveItEvent.OnGameAndSync -> { -// BRSharedPrefs.putPhraseWroteDown(this@PaperKeyProveActivity, true) -// BRAnimator.startBreadActivity( -// this@PaperKeyProveActivity, -// false -// ) -// finishAffinity() -// } -// -// else -> Unit -// } -// } -// ) - } - } - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/UpdatePinActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/UpdatePinActivity.java deleted file mode 100644 index 4233e92c..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/UpdatePinActivity.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.brainwallet.presenter.activities; - -import static com.brainwallet.constants.BWConstants.BW_PIN_LENGTH; - -import android.os.Bundle; -import android.os.Handler; -import android.view.View; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.brainwallet.R; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.presenter.customviews.BRKeyboard; -import com.brainwallet.presenter.interfaces.BROnSignalCompletion; -import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.tools.animation.SpringAnimator; -import com.brainwallet.tools.security.AuthManager; -import com.brainwallet.tools.security.BRKeyStore; - -import timber.log.Timber; - -public class UpdatePinActivity extends BRActivity { - private BRKeyboard keyboard; - private View dot1; - private View dot2; - private View dot3; - private View dot4; - private StringBuilder pin = new StringBuilder(); - private int pinLimit = 4; - // private boolean allowInserting = true; - private TextView title; - private TextView description; - int mode = ENTER_PIN; - public static final int ENTER_PIN = 1; - public static final int ENTER_NEW_PIN = 2; - public static final int RE_ENTER_NEW_PIN = 3; - - private ImageButton faq; - private LinearLayout pinLayout; - private String curNewPin = ""; - public static boolean appVisible = false; - private static UpdatePinActivity app; - - public static UpdatePinActivity getApp() { - return app; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_pin_template); - - keyboard = (BRKeyboard) findViewById(R.id.brkeyboard); - title = (TextView) findViewById(R.id.title); - description = (TextView) findViewById(R.id.description); - pinLayout = (LinearLayout) findViewById(R.id.pinLayout); - if (BRKeyStore.getPinCode(this).length() == BW_PIN_LENGTH) pinLimit = BW_PIN_LENGTH; - setMode(ENTER_PIN); - title.setText(getString(R.string.UpdatePin_updateTitle)); - dot1 = findViewById(R.id.dot1); - dot2 = findViewById(R.id.dot2); - dot3 = findViewById(R.id.dot3); - dot4 = findViewById(R.id.dot4); - - //TODO: all views are using the layout of this button. Views should be refactored without it - // Hiding until layouts are built. - faq = (ImageButton) findViewById(R.id.faq_button); - - keyboard.addOnInsertListener(new BRKeyboard.OnInsertListener() { - @Override - public void onClick(String key) { - handleClick(key); - } - }); - keyboard.setShowDot(false); - - } - - @Override - protected void onResume() { - super.onResume(); - updateDots(); - appVisible = true; - app = this; - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } - - private void handleClick(String key) { - if (key == null) { - Timber.d("timber: handleClick: key is null! "); - return; - } - - if (key.isEmpty()) { - handleDeleteClick(); - } else if (Character.isDigit(key.charAt(0))) { - handleDigitClick(Integer.parseInt(key.substring(0, 1))); - } else { - Timber.d("timber: handleClick: oops: %s", key); - } - } - - - private void handleDigitClick(Integer dig) { - if (pin.length() < pinLimit) - pin.append(dig); - updateDots(); - } - - private void handleDeleteClick() { - if (pin.length() > 0) - pin.deleteCharAt(pin.length() - 1); - updateDots(); - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right); - } - - private void updateDots() { - - AuthManager.getInstance().updateDots(this, pinLimit, pin.toString(), dot1, dot2, dot3, dot4, R.drawable.ic_pin_dot_gray, new AuthManager.OnPinSuccess() { - @Override - public void onSuccess() { - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - goNext(); - } - }, 100); - } - }); - } - - private void goNext() { - switch (mode) { - case ENTER_PIN: - if (AuthManager.getInstance().checkAuth(pin.toString(), this)) { - setMode(ENTER_NEW_PIN); - pinLimit = 4; - } else { - SpringAnimator.failShakeAnimation(this, pinLayout); - } - pin = new StringBuilder(); - updateDots(); - break; - case ENTER_NEW_PIN: - setMode(RE_ENTER_NEW_PIN); - curNewPin = pin.toString(); - pin = new StringBuilder(); - updateDots(); - break; - - case RE_ENTER_NEW_PIN: - if (curNewPin.equalsIgnoreCase(pin.toString())) { - AuthManager.getInstance().setPinCode(pin.toString(), this); - BRAnimator.showBreadSignal(this, getString(R.string.Alerts_pinSet), getString(R.string.UpdatePin_caption), R.drawable.ic_check_mark_white, new BROnSignalCompletion() { - @Override - public void onComplete() { - BRAnimator.startBreadActivity(UpdatePinActivity.this, false); - } - }); - } else { - SpringAnimator.failShakeAnimation(this, pinLayout); - setMode(ENTER_NEW_PIN); - pinLimit = BRKeyStore.getPinCode(this).length(); - } - pin = new StringBuilder(); - updateDots(); - break; - } - } - - private void setMode(int mode) { - String text = ""; - this.mode = mode; - switch (mode) { - case ENTER_PIN: - text = getString(R.string.UpdatePin_enterCurrent); - break; - case ENTER_NEW_PIN: - text = getString(R.string.UpdatePin_enterNew); - break; - case RE_ENTER_NEW_PIN: - text = getString(R.string.UpdatePin_reEnterNew); - break; - } - description.setText(text); - SpringAnimator.springView(description); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/AboutActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/AboutActivity.java deleted file mode 100644 index ccdbc1c2..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/AboutActivity.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.brainwallet.presenter.activities.settings; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import com.brainwallet.R; -import com.brainwallet.presenter.activities.util.ActivityUTILS; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.constants.BWConstants; - -public class AboutActivity extends BRActivity { - private static final String TAG = AboutActivity.class.getName(); - private TextView policyText; - private TextView versionText; - - private ImageView instagramShare; - private ImageView twitterShare; - private ImageView blogShare; - private static AboutActivity app; - - private ImageButton closeButton; - - public static AboutActivity getApp() { - return app; - } - - public static boolean appVisible = false; - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_about); - - versionText = (TextView) findViewById(R.id.info_text); - policyText = (TextView) findViewById(R.id.policy_text); - instagramShare = (ImageView) findViewById(R.id.instagram_share_button); - twitterShare = (ImageView) findViewById(R.id.twitter_share_button); - blogShare = (ImageView) findViewById(R.id.blog_share_button); - versionText.setText(BWConstants.APP_VERSION_NAME_CODE); - - versionText.setText(BWConstants.APP_VERSION_NAME_CODE); - - instagramShare.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(BWConstants.INSTAGRAM_LINK)); - startActivity(browserIntent); - app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); - } - }); - - twitterShare.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(BWConstants.TWITTER_LINK)); - startActivity(browserIntent); - app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); - } - }); - blogShare.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(BWConstants.WEB_LINK)); - startActivity(browserIntent); - app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); - } - }); - policyText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(BWConstants.TOS_LINK)); - startActivity(browserIntent); - app.overridePendingTransition(R.anim.enter_from_bottom, R.anim.empty_300); - } - }); - - closeButton = (ImageButton) findViewById(R.id.close_button); - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } - }); - - } - - @Override - protected void onResume() { - super.onResume(); - appVisible = true; - app = this; - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } - - @Override - public void onBackPressed() { - if (ActivityUTILS.isLast(this)) { - BRAnimator.startBreadActivity(this, false); - } else { - super.onBackPressed(); - } - overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right); - } - -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/AdvancedActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/AdvancedActivity.java deleted file mode 100644 index 3ad39f07..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/AdvancedActivity.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.brainwallet.presenter.activities.settings; - -import static com.brainwallet.R.layout.settings_list_item; -import static com.brainwallet.R.layout.settings_list_section; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.brainwallet.R; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.presenter.entities.BRSettingsItem; - -import java.util.ArrayList; -import java.util.List; - -public class AdvancedActivity extends BRActivity { - private static final String TAG = AdvancedActivity.class.getName(); - private ListView listView; - public List items; - public static boolean appVisible = false; - private static AdvancedActivity app; - - public static AdvancedActivity getApp() { - return app; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_advanced); - - listView = (ListView) findViewById(R.id.settings_list); - - } - - - public class SettingsListAdapter extends ArrayAdapter { - - private List items; - private Context mContext; - - public SettingsListAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List items) { - super(context, resource); - this.items = items; - this.mContext = context; - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - - View v; - BRSettingsItem item = items.get(position); - LayoutInflater inflater = ((Activity) mContext).getLayoutInflater(); - - if (item.isSection) { - v = inflater.inflate(settings_list_section, parent, false); - } else { - v = inflater.inflate(settings_list_item, parent, false); - TextView addon = (TextView) v.findViewById(R.id.item_addon); - addon.setText(item.addonText); - v.setOnClickListener(item.listener); - } - - TextView title = (TextView) v.findViewById(R.id.item_title); - title.setText(item.title); - return v; - - } - - @Override - public int getCount() { - return items == null ? 0 : items.size(); - } - - @Override - public int getItemViewType(int position) { - return super.getItemViewType(position); - } - } - - @Override - protected void onResume() { - super.onResume(); - appVisible = true; - app = this; - if (items == null) - items = new ArrayList<>(); - items.clear(); - - populateItems(); - - listView.setAdapter(new SettingsListAdapter(this, R.layout.settings_list_item, items)); - } - - - @Override - public void onBackPressed() { - super.onBackPressed(); - overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right); - } - - private void populateItems() { - - items.add(new BRSettingsItem("", "", null, true)); - - items.add(new BRSettingsItem(getString(R.string.NodeSelector_title), "", new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(AdvancedActivity.this, NodesActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.empty_300); - - } - }, false)); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/DisplayCurrencyActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/DisplayCurrencyActivity.java deleted file mode 100644 index fb09f353..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/DisplayCurrencyActivity.java +++ /dev/null @@ -1,239 +0,0 @@ -package com.brainwallet.presenter.activities.settings; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Point; -import android.os.Bundle; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import com.brainwallet.R; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.data.model.CurrencyEntity; -import com.brainwallet.tools.manager.BRSharedPrefs; -import com.brainwallet.tools.sqlite.CurrencyDataSource; -import com.brainwallet.constants.BWConstants; -import com.brainwallet.tools.util.BRCurrency; - -import java.math.BigDecimal; -import java.util.Currency; -import java.util.Objects; - -import timber.log.Timber; - - -public class DisplayCurrencyActivity extends BRActivity { - private TextView exchangeText; - private ListView listView; - private CurrencyListAdapter adapter; - - public static boolean appVisible = false; - public static boolean shouldBeBrainwalletFilteredFiat = true; - private static DisplayCurrencyActivity app; - private Button leftButton; - private Button rightButton; - - private ImageButton closeButton; - - public static DisplayCurrencyActivity getApp() { - return app; - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_display_currency); - - //TODO: all views are using the layout of this button. Views should be refactored without it - // Hiding until layouts are built. - ImageButton faq = findViewById(R.id.faq_button); - - exchangeText = findViewById(R.id.exchange_text); - listView = findViewById(R.id.currency_list_view); - adapter = new CurrencyListAdapter(this); - adapter.addAll(CurrencyDataSource.getInstance(this).getAllCurrencies(shouldBeBrainwalletFilteredFiat)); - leftButton = findViewById(R.id.left_button); - rightButton = findViewById(R.id.right_button); - leftButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setButton(true); - } - }); - - rightButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setButton(false); - } - }); - - int unit = BRSharedPrefs.getCurrencyUnit(this); - if (unit == BWConstants.CURRENT_UNIT_LITES) { - setButton(true); - } else { - setButton(false); - } - updateExchangeRate(); - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, - int position, long id) { - TextView currencyItemText = (TextView) view.findViewById(R.id.currency_item_text); - final String selectedCurrency = currencyItemText.getText().toString(); - String iso = selectedCurrency.substring(0, 3); - BRSharedPrefs.putIso(DisplayCurrencyActivity.this, iso); - BRSharedPrefs.putCurrencyListPosition(DisplayCurrencyActivity.this, position); - - updateExchangeRate(); - } - }); - listView.setAdapter(adapter); - adapter.notifyDataSetChanged(); - - - closeButton = (ImageButton) findViewById(R.id.close_button); - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } - }); - } - - private void updateExchangeRate() { - //set the rate from the last saved - String iso = BRSharedPrefs.getIsoSymbol(this); - CurrencyEntity entity = CurrencyDataSource.getInstance(this).getCurrencyByIso(iso); - if (entity != null) { - String finalExchangeRate = BRCurrency.getFormattedCurrencyString(DisplayCurrencyActivity.this, BRSharedPrefs.getIsoSymbol(this), new BigDecimal(entity.rate)); - boolean bits = BRSharedPrefs.getCurrencyUnit(this) == BWConstants.CURRENT_UNIT_LITES; - exchangeText.setText(BRCurrency.getFormattedCurrencyString(this, "LTC", new BigDecimal(bits ? 1000 : 1)) + " = " + finalExchangeRate); - } - adapter.notifyDataSetChanged(); - } - - private void setButton(boolean left) { - if (left) { - BRSharedPrefs.putCurrencyUnit(this, BWConstants.CURRENT_UNIT_LITES); - leftButton.setTextColor(getColor(R.color.grape)); - leftButton.setBackground(getDrawable(R.drawable.b_half_left_blue)); - rightButton.setTextColor(getColor(R.color.cheddar)); - rightButton.setBackground(getDrawable(R.drawable.b_half_right_blue_stroke)); - } else { - BRSharedPrefs.putCurrencyUnit(this, BWConstants.CURRENT_UNIT_LITECOINS); - leftButton.setTextColor(getColor(R.color.cheddar)); - leftButton.setBackground(getDrawable(R.drawable.b_half_left_blue_stroke)); - rightButton.setTextColor(getColor(R.color.grape)); - rightButton.setBackground(getDrawable(R.drawable.b_half_right_blue)); - } - updateExchangeRate(); - } - - @Override - protected void onResume() { - super.onResume(); - appVisible = true; - app = this; - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right); - } - - public class CurrencyListAdapter extends ArrayAdapter { - public final String TAG = CurrencyListAdapter.class.getName(); - - private final Context mContext; - private final int layoutResourceId; - private TextView textViewItem; - private final Point displayParameters = new Point(); - - public CurrencyListAdapter(Context mContext) { - super(mContext, R.layout.currency_list_item); - - this.layoutResourceId = R.layout.currency_list_item; - this.mContext = mContext; - ((Activity) mContext).getWindowManager().getDefaultDisplay().getSize(displayParameters); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - -// final int tmp = BRSharedPrefs.getCurrencyListPosition(mContext); - - if (convertView == null) { - // inflate the layout - LayoutInflater inflater = ((Activity) mContext).getLayoutInflater(); - convertView = inflater.inflate(layoutResourceId, parent, false); - } - // get the TextView and then set the text (item name) and tag (item ID) values - textViewItem = convertView.findViewById(R.id.currency_item_text); - String isoCached = BRSharedPrefs.getIsoSymbol(mContext); - String iso = getItem(position).code; - Currency c = null; - try { - c = Currency.getInstance(iso); - } catch (IllegalArgumentException ignored) { - } - textViewItem.setText(c == null ? iso : String.format("%s (%s)", iso, c.getSymbol())); - ImageView checkMark = convertView.findViewById(R.id.currency_checkmark); - - Timber.d("timber: isoCached: %s", isoCached); - Timber.d("timber: item iso: %s", iso); - - if (Objects.equals(iso, isoCached)) { - checkMark.setVisibility(View.VISIBLE); - } else { - checkMark.setVisibility(View.GONE); - } - normalizeTextView(); - return convertView; - } - - @Override - public int getItemViewType(int position) { - return IGNORE_ITEM_VIEW_TYPE; - } - - private boolean isTextSizeAcceptable(TextView textView) { - textView.measure(0, 0); - int textWidth = textView.getMeasuredWidth(); - int checkMarkWidth = 76 + 20; - return (textWidth <= (displayParameters.x - checkMarkWidth)); - } - - private boolean normalizeTextView() { - int count = 0; - while (!isTextSizeAcceptable(textViewItem)) { - count++; - float textSize = textViewItem.getTextSize(); - textViewItem.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize - 2); - this.notifyDataSetChanged(); - } - return (count > 0); - } - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/SecurityCenterActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/SecurityCenterActivity.java deleted file mode 100644 index 80de11af..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/SecurityCenterActivity.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.brainwallet.presenter.activities.settings; - -import static com.brainwallet.constants.BWConstants.BW_PIN_LENGTH; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.brainwallet.R; -import com.brainwallet.navigation.Route; -import com.brainwallet.ui.BrainwalletActivity; -import com.brainwallet.presenter.activities.intro.WriteDownActivity; -import com.brainwallet.presenter.activities.util.ActivityUTILS; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.presenter.entities.BRSecurityCenterItem; -import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.tools.manager.BRSharedPrefs; -import com.brainwallet.tools.security.BRKeyStore; - -import java.util.ArrayList; -import java.util.List; - -public class SecurityCenterActivity extends BRActivity { - - public ListView mListView; - public RelativeLayout layout; - public List itemList; - public static boolean appVisible = false; - private static SecurityCenterActivity app; - private ImageButton close; - - public static SecurityCenterActivity getApp() { - return app; - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_security_center); - - itemList = new ArrayList<>(); - mListView = (ListView) findViewById(R.id.menu_listview); - mListView.addFooterView(new View(this), null, true); - mListView.addHeaderView(new View(this), null, true); - mListView.setVerticalScrollBarEnabled(false); - mListView.setClickable(false); - close = (ImageButton) findViewById(R.id.close_button); - close.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (!BRAnimator.isClickAllowed()) return; - onBackPressed(); - } - }); - - updateList(); - - ImageButton faq = (ImageButton) findViewById(R.id.faq_button); - //TODO: all views are using the layout of this button. Views should be refactored without it - // Hiding until layouts are built. - - } - - @Override - protected void onResume() { - super.onResume(); - updateList(); - appVisible = true; - app = this; - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - if (ActivityUTILS.isLast(this)) { - BRAnimator.startBreadActivity(this, false); - } else { - super.onBackPressed(); - } - overridePendingTransition(R.anim.fade_up, R.anim.exit_to_bottom); - } - - public RelativeLayout getMainLayout() { - return layout; - } - - public class SecurityCenterListAdapter extends ArrayAdapter { - - private List items; - private Context mContext; - private int defaultLayoutResource = R.layout.security_center_list_item; - - public SecurityCenterListAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List items) { - super(context, resource); - this.items = items; - this.mContext = context; - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - if (convertView == null) { - // inflate the layout - LayoutInflater inflater = ((Activity) mContext).getLayoutInflater(); - convertView = inflater.inflate(defaultLayoutResource, parent, false); - } - TextView title = (TextView) convertView.findViewById(R.id.item_title); - TextView text = (TextView) convertView.findViewById(R.id.item_text); - ImageView checkMark = (ImageView) convertView.findViewById(R.id.check_mark); - - title.setText(items.get(position).title); - text.setText(items.get(position).text); - checkMark.setImageResource(items.get(position).checkMarkResId); - convertView.setOnClickListener(items.get(position).listener); - return convertView; - } - - @Override - public int getCount() { - return items == null ? 0 : items.size(); - } - } - - @Override - public void onPause() { - super.onPause(); - appVisible = false; - } - - private void updateList() { - boolean isPinSet = BRKeyStore.getPinCode(this).length() == BW_PIN_LENGTH; - itemList.clear(); - itemList.add(new BRSecurityCenterItem(getString(R.string.SecurityCenter_pinTitle), getString(R.string.SecurityCenter_pinDescription), - isPinSet ? R.drawable.ic_check_mark_blue : R.drawable.ic_check_mark_grey, new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = BrainwalletActivity.createIntent( - SecurityCenterActivity.this, - new Route.UnLock(true) - ); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left); - } - })); - - boolean isPaperKeySet = BRSharedPrefs.getPhraseWroteDown(this); - itemList.add(new BRSecurityCenterItem(getString(R.string.SecurityCenter_paperKeyTitle), getString(R.string.SecurityCenter_paperKeyDescription), - isPaperKeySet ? R.drawable.ic_check_mark_blue : R.drawable.ic_check_mark_grey, new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(SecurityCenterActivity.this, WriteDownActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_bottom, R.anim.fade_down); - } - })); - - mListView.setAdapter(new SecurityCenterListAdapter(this, R.layout.menu_list_item, itemList)); - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/SettingsActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/SettingsActivity.java deleted file mode 100644 index 7f33f632..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/SettingsActivity.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.brainwallet.presenter.activities.settings; - -import static com.brainwallet.R.layout.settings_list_item; -import static com.brainwallet.R.layout.settings_list_section; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageButton; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.brainwallet.BrainwalletApp; -import com.brainwallet.R; -import com.brainwallet.data.repository.SettingRepository; -import com.brainwallet.navigation.LegacyNavigation; -import com.brainwallet.navigation.Route; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.presenter.entities.BRSettingsItem; -import com.brainwallet.presenter.language.ChangeLanguageBottomSheet; -import com.brainwallet.tools.animation.BRAnimator; -import com.brainwallet.tools.manager.BRSharedPrefs; - -import org.koin.java.KoinJavaComponent; - -import java.util.ArrayList; -import java.util.List; - -public class SettingsActivity extends BRActivity { - private ListView listView; - public List items; - public static boolean appVisible = false; - private ImageButton closeButton; - private static SettingsActivity app; - - public static SettingsActivity getApp() { - return app; - } - - private SettingRepository settingRepository = (SettingRepository) KoinJavaComponent.inject(SettingRepository.class).getValue(); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - - listView = findViewById(R.id.settings_list); - - closeButton = (ImageButton) findViewById(R.id.close_button); - closeButton.setOnClickListener(v -> finish()); - } - - public class SettingsListAdapter extends ArrayAdapter { - - private List items; - private Context mContext; - - public SettingsListAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List items) { - super(context, resource); - this.items = items; - this.mContext = context; - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - - View v; - BRSettingsItem item = items.get(position); - LayoutInflater inflater = ((Activity) mContext).getLayoutInflater(); - - if (item.isSection) { - v = inflater.inflate(settings_list_section, parent, false); - } else { - v = inflater.inflate(settings_list_item, parent, false); - TextView addon = (TextView) v.findViewById(R.id.item_addon); - addon.setText(item.addonText); - v.setOnClickListener(item.listener); - } - - TextView title = (TextView) v.findViewById(R.id.item_title); - title.setText(item.title); - return v; - - } - - @Override - public int getCount() { - return items == null ? 0 : items.size(); - } - - @Override - public int getItemViewType(int position) { - return super.getItemViewType(position); - } - } - - @Override - protected void onResume() { - super.onResume(); - appVisible = true; - app = this; - if (items == null) - items = new ArrayList<>(); - items.clear(); - - populateItems(); - - listView.setAdapter(new SettingsListAdapter(this, R.layout.settings_list_item, items)); - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - overridePendingTransition(R.anim.fade_up, R.anim.exit_to_bottom); - } - - private void populateItems() { - - /*Wallet Title*/ - items.add(new BRSettingsItem(getString(R.string.Settings_wallet), "", null, true)); - - /*Show Seed Phrase*/ - items.add(new BRSettingsItem(getString(R.string.settings_show_seed), "", v -> { - BRAnimator.showBalanceSeedFragment(this); - }, false)); - - /*Manage Title*/ - items.add(new BRSettingsItem(getString(R.string.Settings_manage), "", null, true)); - - //toggle dark mode - boolean isDarkMode = settingRepository.isDarkMode(); - - items.add(new BRSettingsItem(getString(R.string.toggle_dark_mode), getString(isDarkMode ? androidx.appcompat.R.string.abc_capital_on : androidx.appcompat.R.string.abc_capital_off), v -> { - settingRepository.toggleDarkMode(!isDarkMode); - LegacyNavigation.openComposeScreen(SettingsActivity.this); - }, false)); - - /*Languages*/ - items.add(new BRSettingsItem(getString(R.string.Settings_languages), null, v -> { - ChangeLanguageBottomSheet fragment = new ChangeLanguageBottomSheet(); - fragment.show(getSupportFragmentManager(), fragment.getTag()); - }, false)); - - /*Display Currency*/ - items.add(new BRSettingsItem(getString(R.string.Settings_currency), BRSharedPrefs.getIsoSymbol(this), v -> { - Intent intent = new Intent(SettingsActivity.this, DisplayCurrencyActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left); - }, false)); - - /*Sync Blockchain*/ - items.add(new BRSettingsItem(getString(R.string.Settings_sync), "", v -> { - Intent intent = new Intent(SettingsActivity.this, SyncBlockchainActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left); - }, false)); - - /*SPACER*/ - items.add(new BRSettingsItem("", "", null, true)); - - /*Share Anonymous Data*/ - items.add(new BRSettingsItem(getString(R.string.Settings_shareData), "", v -> { - Intent intent = new Intent(SettingsActivity.this, ShareDataActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left); - }, false)); - - /*About*/ - items.add(new BRSettingsItem(getString(R.string.Settings_about), "", v -> { - Intent intent = new Intent(SettingsActivity.this, AboutActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left); - }, false)); - - /*SPACER*/ - items.add(new BRSettingsItem("", "", null, true)); - - /*Advanced Settings*/ - items.add(new BRSettingsItem(getString(R.string.Settings_advancedTitle), "", v -> { - Intent intent = new Intent(SettingsActivity.this, AdvancedActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left); - }, false)); - - /*SPACER*/ - items.add(new BRSettingsItem("", "", null, true)); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/ShareDataActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/ShareDataActivity.java deleted file mode 100644 index e4ec6f5f..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/ShareDataActivity.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.brainwallet.presenter.activities.settings; - -import android.os.Bundle; -import android.widget.CompoundButton; -import android.widget.ToggleButton; - -import com.brainwallet.R; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.tools.manager.BRSharedPrefs; - -public class ShareDataActivity extends BRActivity { - private static final String TAG = ShareDataActivity.class.getName(); - private ToggleButton toggleButton; - public static boolean appVisible = false; - private static ShareDataActivity app; - - public static ShareDataActivity getApp() { - return app; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_share_data); - - toggleButton = (ToggleButton) findViewById(R.id.toggleButton); - toggleButton.setChecked(BRSharedPrefs.getShareData(this)); - toggleButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - BRSharedPrefs.putShareData(ShareDataActivity.this, isChecked); - } - }); - - } - - @Override - protected void onResume() { - super.onResume(); - appVisible = true; - app = this; - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/SpendLimitActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/SpendLimitActivity.java deleted file mode 100644 index 6a85a967..00000000 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/SpendLimitActivity.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.brainwallet.presenter.activities.settings; - -import static com.brainwallet.constants.BWConstants.ONE_BITCOIN; - -import android.app.Activity; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import com.brainwallet.R; -import com.brainwallet.presenter.activities.util.BRActivity; -import com.brainwallet.tools.manager.BRSharedPrefs; -import com.brainwallet.tools.security.AuthManager; -import com.brainwallet.tools.security.BRKeyStore; -import com.brainwallet.tools.util.BRExchange; -import com.brainwallet.wallet.BRWalletManager; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -import timber.log.Timber; - - -public class SpendLimitActivity extends BRActivity { - public static boolean appVisible = false; - private static SpendLimitActivity app; - private ListView listView; - private LimitAdaptor adapter; - - private ImageButton closeButton; - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - public static SpendLimitActivity getApp() { - return app; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_spend_limit); - - //TODO: all views are using the layout of this button. Views should be refactored without it - // Hiding until layouts are built. - ImageButton faq = findViewById(R.id.faq_button); - - closeButton = (ImageButton) findViewById(R.id.close_button); - listView = findViewById(R.id.limit_list); - listView.setFooterDividersEnabled(true); - adapter = new LimitAdaptor(this); - List items = new ArrayList<>(); - items.add(getAmountByStep(0).intValue()); - items.add(getAmountByStep(1).intValue()); - items.add(getAmountByStep(2).intValue()); - items.add(getAmountByStep(3).intValue()); - items.add(getAmountByStep(4).intValue()); - - adapter.addAll(items); - - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, - int position, long id) { - Timber.d("timber: onItemClick: %s", position); - int limit = adapter.getItem(position); - BRKeyStore.putSpendLimit(limit, app); - - AuthManager.getInstance().setTotalLimit(app, BRWalletManager.getInstance().getTotalSent() - + BRKeyStore.getSpendLimit(app)); - adapter.notifyDataSetChanged(); - } - }); - listView.setAdapter(adapter); - adapter.notifyDataSetChanged(); - - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } - }); - } - - //satoshis - private BigDecimal getAmountByStep(int step) { - BigDecimal result; - switch (step) { - case 0: - result = new BigDecimal(0);// 0 always require - break; - case 1: - result = new BigDecimal(ONE_BITCOIN / 100);// 0.01 BTC - break; - case 2: - result = new BigDecimal(ONE_BITCOIN / 10);// 0.1 BTC - break; - case 3: - result = new BigDecimal(ONE_BITCOIN);// 1 BTC - break; - case 4: - result = new BigDecimal(ONE_BITCOIN * 10);// 10 BTC - break; - - default: - result = new BigDecimal(ONE_BITCOIN);// 1 BTC Default - break; - } - return result; - } - - private int getStepFromLimit(long limit) { - switch ((int) limit) { - - case 0: - return 0; - case ONE_BITCOIN / 100: - return 1; - case ONE_BITCOIN / 10: - return 2; - case ONE_BITCOIN: - return 3; - case ONE_BITCOIN * 10: - return 4; - default: - return 2; //1 BTC Default - } - } - - @Override - protected void onResume() { - super.onResume(); - appVisible = true; - app = this; - } - - @Override - protected void onPause() { - super.onPause(); - appVisible = false; - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right); - } - - public class LimitAdaptor extends ArrayAdapter { - - private final Context mContext; - private final int layoutResourceId; - - public LimitAdaptor(Context mContext) { - super(mContext, R.layout.currency_list_item); - this.layoutResourceId = R.layout.currency_list_item; - this.mContext = mContext; - } - - @Override - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - final long limit = BRKeyStore.getSpendLimit(app); - if (convertView == null) { - // inflate the layout - LayoutInflater inflater = ((Activity) mContext).getLayoutInflater(); - convertView = inflater.inflate(layoutResourceId, parent, false); - } - // get the TextView and then set the text (item name) and tag (item ID) values - TextView textViewItem = convertView.findViewById(R.id.currency_item_text); - Integer item = getItem(position); - BigDecimal curAmount = BRExchange.getAmountFromLitoshis(app, BRSharedPrefs.getIsoSymbol(app), new BigDecimal(item)); - BigDecimal btcAmount = BRExchange.getLitecoinForLitoshis(app, new BigDecimal(item)); - String text = String.format(item == 0 ? app.getString(R.string.TouchIdSpendingLimit) : "%s (%s)", curAmount, btcAmount); - textViewItem.setText(text); - ImageView checkMark = convertView.findViewById(R.id.currency_checkmark); - - if (position == getStepFromLimit(limit)) { - checkMark.setVisibility(View.VISIBLE); - } else { - checkMark.setVisibility(View.GONE); - } - return convertView; - } - - @Override - public int getItemViewType(int position) { - return IGNORE_ITEM_VIEW_TYPE; - } - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/activities/settings/TestActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/settings/TestActivity.java index 455b0931..b15e73e0 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/settings/TestActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/settings/TestActivity.java @@ -17,7 +17,7 @@ protected void onSaveInstanceState(Bundle outState) { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_about); + setContentView(R.layout.activity_bread); } diff --git a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentMenu.java b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentMenu.java index 31143495..db164fc9 100644 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentMenu.java +++ b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentMenu.java @@ -21,17 +21,12 @@ import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.browser.customtabs.CustomTabsIntent; import androidx.constraintlayout.widget.ConstraintLayout; import com.brainwallet.R; -import com.brainwallet.presenter.activities.settings.SecurityCenterActivity; -import com.brainwallet.presenter.activities.settings.SettingsActivity; import com.brainwallet.presenter.entities.BRMenuItem; import com.brainwallet.tools.animation.BRAnimator; import com.brainwallet.tools.animation.SlideDetector; -import com.brainwallet.tools.manager.AnalyticsManager; -import com.brainwallet.constants.BWConstants; import com.brainwallet.data.source.RemoteConfigSource; import org.json.JSONObject; @@ -64,32 +59,6 @@ public View onCreateView(LayoutInflater inflater, itemList = new ArrayList<>(); - /* Security Center */ - itemList.add(new BRMenuItem(getString(R.string.MenuButton_security), R.drawable.ic_shield, v -> { - Intent intent = new Intent(getActivity(), SecurityCenterActivity.class); - launchActivity(intent); - })); - - /* Customer Support */ - itemList.add(new BRMenuItem(getString(R.string.MenuButton_support), R.drawable.faq_question_black, v -> { - CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); - CustomTabsIntent customTabsIntent = builder.build(); - customTabsIntent.launchUrl(getContext(), Uri.parse(BWConstants.WEB_LINK)); - AnalyticsManager.logCustomEvent(BWConstants._20201118_DTGS); - })); - - /* Settings */ - itemList.add(new BRMenuItem(getString(R.string.MenuButton_settings), R.drawable.ic_settings, v -> { - Intent intent = new Intent(getActivity(), SettingsActivity.class); - launchActivity(intent); - })); - - /* Lock Wallet */ - itemList.add(new BRMenuItem(getString(R.string.MenuButton_lock), R.drawable.ic_lock, v -> { - closeMenu(); - final Activity from = getActivity(); - BRAnimator.startBreadActivity(from, true); - })); /** * remote config example here @@ -182,4 +151,4 @@ private void closeMenu() { getActivity().getFragmentManager().popBackStack(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentMoonpay.kt b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentMoonpay.kt deleted file mode 100644 index 87edd946..00000000 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentMoonpay.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.brainwallet.presenter.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.webkit.WebSettings -import android.webkit.WebView -import android.webkit.WebViewClient -import android.widget.ImageButton -import android.widget.TextView -import androidx.fragment.app.Fragment -import com.brainwallet.R -import com.brainwallet.tools.manager.BRClipboardManager -import com.brainwallet.tools.manager.BRSharedPrefs - -class FragmentMoonpay : Fragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_moonpay_temp, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val webView = view.findViewById(R.id.web_view) - val buttonClose = view.findViewById(R.id.close_button) - val copyAddressButton = view.findViewById(R.id.copy_button) - var receiveAddressLabel = view.findViewById(R.id.receive_address) - val addressText = BRSharedPrefs.getReceiveAddress(context) - - // buttonClose - receiveAddressLabel.setText(addressText) - receiveAddressLabel.setTextColor(receiveAddressLabel.getResources().getColor(R.color.midnight, null)) - webView.setInitialScale(80) - webView.settings.apply { - javaScriptEnabled = true - domStorageEnabled = true - cacheMode = WebSettings.LOAD_DEFAULT - setSupportZoom(true) - useWideViewPort = true - loadWithOverviewMode = true - } - - webView.webViewClient = WebViewClient() - webView.loadUrl("https://www.brainwallet.co/mobile-top-up.html") - - buttonClose.setOnClickListener { - parentFragmentManager.popBackStack() - } - - receiveAddressLabel.setOnClickListener { - BRClipboardManager.putClipboard(context, receiveAddressLabel.getText().toString()) - } - - copyAddressButton.setOnClickListener { - BRClipboardManager.putClipboard(context, receiveAddressLabel.getText().toString()) - } - } -} diff --git a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt b/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt deleted file mode 100644 index 8a98028a..00000000 --- a/app/src/main/java/com/brainwallet/presenter/fragments/FragmentSend.kt +++ /dev/null @@ -1,806 +0,0 @@ -package com.brainwallet.presenter.fragments - -import android.app.Activity -import android.content.res.Configuration -import android.os.Bundle -import android.os.Handler -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewTreeObserver.OnGlobalLayoutListener -import android.view.animation.OvershootInterpolator -import android.view.inputmethod.EditorInfo -import android.widget.Button -import android.widget.EditText -import android.widget.ImageButton -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.fragment.app.Fragment -import androidx.transition.AutoTransition -import androidx.transition.Transition -import androidx.transition.TransitionManager -import com.brainwallet.R -import com.brainwallet.presenter.customviews.BRKeyboard -import com.brainwallet.presenter.entities.ServiceItems -import com.brainwallet.presenter.entities.TransactionItem -import com.brainwallet.tools.animation.BRAnimator -import com.brainwallet.tools.animation.BRDialog -import com.brainwallet.tools.animation.SlideDetector -import com.brainwallet.tools.animation.SpringAnimator -import com.brainwallet.tools.manager.AnalyticsManager -import com.brainwallet.tools.manager.BRClipboardManager -import com.brainwallet.tools.manager.BRSharedPrefs -import com.brainwallet.tools.manager.FeeManager -import com.brainwallet.tools.security.BRSender -import com.brainwallet.tools.security.BitcoinUrlHandler -import com.brainwallet.tools.threads.BRExecutor -import com.brainwallet.tools.util.BRCurrency -import com.brainwallet.tools.util.BRExchange -import com.brainwallet.tools.util.Utils -import com.brainwallet.constants.BWConstants -import com.brainwallet.wallet.BRWalletManager -import timber.log.Timber -import java.math.BigDecimal -import java.math.RoundingMode -import java.util.regex.Pattern - -// TODO: make sure remove unused after refactor network fee move to HomeSettingDrawerSheet -class FragmentSend : Fragment() { - private lateinit var signalLayout: LinearLayout - private lateinit var keyboardLayout: LinearLayout - private lateinit var scanButton: Button - private lateinit var pasteButton: Button - private lateinit var sendButton: Button - private lateinit var isoCurrencySymbolButton: Button - private lateinit var commentEdit: EditText - private lateinit var addressEdit: EditText - private lateinit var amountEdit: EditText - private lateinit var isoCurrencySymbolText: TextView - private lateinit var balanceText: TextView - private lateinit var feeText: TextView - private lateinit var feeDescription: TextView - private lateinit var warningText: TextView - private var amountLabelOn = true - private var ignoreCleanup = false - private var feeButtonsShown = false - private var currentBalance: Long = 0 - private var keyboardIndex = 0 - private lateinit var keyboard: BRKeyboard - private lateinit var closeButton: ImageButton - private lateinit var amountLayout: ConstraintLayout - private var selectedIsoCurrencySymbol: String? = null - private lateinit var backgroundLayout: ScrollView - private lateinit var amountBuilder: StringBuilder - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View? { - val rootView = inflater.inflate(R.layout.fragment_send, container, false) - backgroundLayout = rootView.findViewById(R.id.background_layout) - signalLayout = rootView.findViewById(R.id.signal_layout) as LinearLayout - keyboard = rootView.findViewById(R.id.keyboard) as BRKeyboard - keyboard.setBRButtonBackgroundResId(R.drawable.keyboard_white_button) - keyboard.setBRKeyboardColor(R.color.white) - isoCurrencySymbolText = rootView.findViewById(R.id.iso_text) as TextView - addressEdit = rootView.findViewById(R.id.address_edit) as EditText - scanButton = rootView.findViewById(R.id.scan) as Button - pasteButton = rootView.findViewById(R.id.paste_button) as Button - - sendButton = rootView.findViewById(R.id.send_button) as Button - commentEdit = rootView.findViewById(R.id.comment_edit) as EditText - amountEdit = rootView.findViewById(R.id.amount_edit) as EditText - balanceText = rootView.findViewById(R.id.balance_text) as TextView - feeText = rootView.findViewById(R.id.fee_text) as TextView - isoCurrencySymbolButton = rootView.findViewById(R.id.iso_button) as Button - keyboardLayout = rootView.findViewById(R.id.keyboard_layout) as LinearLayout - amountLayout = rootView.findViewById(R.id.amount_layout) as ConstraintLayout -// feeLayout = rootView.findViewById(R.id.fee_buttons_layout) as BRLinearLayoutWithCaret -// feeDescription = rootView.findViewById(R.id.fee_description) as TextView -// warningText = rootView.findViewById(R.id.warning_text) as TextView - closeButton = rootView.findViewById(R.id.close_button) as ImageButton - selectedIsoCurrencySymbol = - if (BRSharedPrefs.getLTCViewingPreference(context)) { - "LTC" - } else { - BRSharedPrefs.getIsoSymbol( - context - ) - } - amountBuilder = StringBuilder(0) - setListeners() - - // / Setup Currency Button that switches between LTC and the preferred fiat (e.g.; "USD") - isoCurrencySymbolText.text = getString(R.string.Send_amountLabel) - isoCurrencySymbolText.textSize = 18f - isoCurrencySymbolText.setTextColor(requireContext().getColor(R.color.cheddar)) - isoCurrencySymbolText.requestLayout() - - // / Setup Fees Description - feeText.text = "" - - signalLayout.setOnTouchListener(SlideDetector(signalLayout) { animateClose() }) - AnalyticsManager.logCustomEvent(BWConstants._20191105_VSC) -// setupFeesSelector(rootView) -// showFeeSelectionButtons(feeButtonsShown) -// edit.setOnClickListener { -// feeButtonsShown = !feeButtonsShown -// showFeeSelectionButtons(feeButtonsShown) -// } - keyboardIndex = signalLayout.indexOfChild(keyboardLayout) - // TODO: all views are using the layout of this button. Views should be refactored without it - // Hiding until layouts are built. - showKeyboard(false) - signalLayout.layoutTransition = BRAnimator.getDefaultTransition() - - updateText() - - // update fee - BRWalletManager.getInstance().setFeePerKb(FeeManager.getInstance().currentFeeValue) - - return rootView - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - } - -// private fun setupFeesSelector(rootView: View) { -// val feesSegment = rootView.findViewById(R.id.fees_segment) -// feesSegment.setOnCheckedChangeListener { _, checkedTypeId -> onFeeTypeSelected(checkedTypeId) } -// onFeeTypeSelected(R.id.regular_fee_but) -// } - -// private fun onFeeTypeSelected(checkedTypeId: Int) { -// val feeManager = FeeManager.getInstance() -// when (checkedTypeId) { -// R.id.regular_fee_but -> { -// feeManager.setFeeType(FeeManager.REGULAR) -// BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.regular) -// setFeeInformation(R.string.FeeSelector_regularTime, 0, 0, View.GONE) -// } -// R.id.economy_fee_but -> { -// feeManager.setFeeType(FeeManager.ECONOMY) -// BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.economy) -// setFeeInformation( -// R.string.FeeSelector_economyTime, -// R.string.FeeSelector_economyWarning, -// R.color.chili, -// View.VISIBLE, -// ) -// } -// R.id.luxury_fee_but -> { -// feeManager.setFeeType(FeeManager.LUXURY) -// BRWalletManager.getInstance().setFeePerKb(feeManager.currentFeeOptions.luxury) -// setFeeInformation( -// R.string.FeeSelector_luxuryTime, -// R.string.FeeSelector_luxuryMessage, -// R.color.cheddar, -// View.VISIBLE, -// ) -// } -// else -> { -// } -// } -// updateText() -// } - -// private fun setFeeInformation( -// @StringRes deliveryTime: Int, -// @StringRes warningStringId: Int, -// @ColorRes warningColorId: Int, -// visibility: Int, -// ) { -// feeDescription.text = -// getString(R.string.FeeSelector_estimatedDeliver, getString(deliveryTime)) -// if (warningStringId != 0) { -// warningText.setText(warningStringId) -// } -// if (warningColorId != 0) { -// warningText.setTextColor(resources.getColor(warningColorId, null)) -// } -// warningText.visibility = visibility -// } - - private fun setListeners() { - amountEdit.setOnClickListener { - showKeyboard(true) - if (amountLabelOn) { // only first time - amountLabelOn = false - amountEdit.hint = "0" - amountEdit.textSize = 24f - balanceText.visibility = View.VISIBLE - feeText.visibility = View.VISIBLE -// edit.visibility = View.VISIBLE - isoCurrencySymbolText.text = - BRCurrency.getSymbolByIso(activity, selectedIsoCurrencySymbol) - isoCurrencySymbolText.textSize = 28f - val scaleX = amountEdit.scaleX - amountEdit.scaleX = 0f - val tr = AutoTransition() - tr.interpolator = OvershootInterpolator() - tr.addListener( - object : Transition.TransitionListener { - override fun onTransitionStart(transition: Transition) {} - - override fun onTransitionEnd(transition: Transition) { - amountEdit.requestLayout() - amountEdit.animate().setDuration(100).scaleX(scaleX) - } - - override fun onTransitionCancel(transition: Transition) {} - - override fun onTransitionPause(transition: Transition) {} - - override fun onTransitionResume(transition: Transition) {} - }, - ) - val set = ConstraintSet() - set.clone(amountLayout) - TransitionManager.beginDelayedTransition(amountLayout, tr) - val px4 = Utils.getPixelsFromDps(context, 4) - set.connect( - balanceText.id, - ConstraintSet.TOP, - isoCurrencySymbolText.id, - ConstraintSet.BOTTOM, - px4, - ) - set.connect( - feeText.id, - ConstraintSet.TOP, - balanceText.id, - ConstraintSet.BOTTOM, - px4, - ) - set.connect( - feeText.id, - ConstraintSet.BOTTOM, - ConstraintSet.PARENT_ID, - ConstraintSet.BOTTOM, - px4, - ) - set.connect( - isoCurrencySymbolText.id, - ConstraintSet.TOP, - ConstraintSet.PARENT_ID, - ConstraintSet.TOP, - px4, - ) - set.connect( - isoCurrencySymbolText.id, - ConstraintSet.BOTTOM, - -1, - ConstraintSet.TOP, - -1 - ) - set.applyTo(amountLayout) - } - } - - // needed to fix the overlap bug - commentEdit.setOnKeyListener( - - View.OnKeyListener { v, keyCode, event -> - - if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { - amountLayout.requestLayout() - return@OnKeyListener true - } - false - }, - ) - pasteButton.setOnClickListener( - View.OnClickListener { - if (!BRAnimator.isClickAllowed()) return@OnClickListener - val bitcoinUrl = BRClipboardManager.getClipboard(activity) - if (Utils.isNullOrEmpty(bitcoinUrl) || !isInputValid(bitcoinUrl)) { - showClipboardError() - return@OnClickListener - } - val obj = BitcoinUrlHandler.getRequestFromString(bitcoinUrl) - if (obj?.address == null) { - showClipboardError() - return@OnClickListener - } - val address = obj.address - val wm = BRWalletManager.getInstance() - if (BRWalletManager.validateAddress(address)) { - val app: Activity? = activity - if (app == null) { - Timber.e("timber:paste onClick: app is null") - return@OnClickListener - } - BRExecutor.getInstance().forLightWeightBackgroundTasks().execute { - if (wm.addressContainedInWallet(address)) { - app.runOnUiThread( - Runnable { - BRDialog.showCustomDialog( - requireActivity(), - "", - resources.getString(R.string.Send_containsAddress), - resources.getString(R.string.AccessibilityLabels_close), - null, - { brDialogView -> brDialogView.dismiss() }, - null, - null, - 0, - ) - BRClipboardManager.putClipboard(activity, "") - }, - ) - } else if (wm.addressIsUsed(address)) { - app.runOnUiThread( - Runnable { - BRDialog.showCustomDialog( - requireActivity(), - getString(R.string.Send_UsedAddress_firstLine), - getString(R.string.Send_UsedAddress_secondLIne), - "Ignore", - "Cancel", - { brDialogView -> - brDialogView.dismiss() - addressEdit.setText(address) - }, - { brDialogView -> brDialogView.dismiss() }, - null, - 0, - ) - }, - ) - } else { - app.runOnUiThread(Runnable { addressEdit.setText(address) }) - } - } - } else { - showClipboardError() - } - }, - ) - isoCurrencySymbolButton.setOnClickListener { - selectedIsoCurrencySymbol = - if (selectedIsoCurrencySymbol.equals( - BRSharedPrefs.getIsoSymbol(context), - ignoreCase = true - ) - ) { - "LTC" - } else { - BRSharedPrefs.getIsoSymbol(context) - } - updateText() - } - scanButton.setOnClickListener( - View.OnClickListener { - if (!BRAnimator.isClickAllowed()) return@OnClickListener - saveMetaData() - BRAnimator.openScanner(activity, BWConstants.SCANNER_REQUEST) - }, - ) - - sendButton.setOnClickListener( - View.OnClickListener { - if (!BRAnimator.isClickAllowed()) { - return@OnClickListener - } - var allFilled = true - val sendAddress = addressEdit.text.toString() - val amountStr = amountBuilder.toString() - val iso = selectedIsoCurrencySymbol - val comment = commentEdit.text.toString() - - // get amount in satoshis from any isos - val bigAmount = BigDecimal(if (Utils.isNullOrEmpty(amountStr)) "0" else amountStr) - val litoshiAmount = BRExchange.getLitoshisFromAmount(activity, iso, bigAmount) - if (sendAddress.isEmpty() || !BRWalletManager.validateAddress(sendAddress)) { - allFilled = false - SpringAnimator.failShakeAnimation(activity, addressEdit) - } - if (amountStr.isEmpty()) { - allFilled = false - SpringAnimator.failShakeAnimation(activity, amountEdit) - } - if (litoshiAmount.toLong() > BRWalletManager.getInstance().getBalance(activity)) { - SpringAnimator.failShakeAnimation(activity, balanceText) - SpringAnimator.failShakeAnimation(activity, feeText) - } - if (allFilled) { - BRSender.getInstance().sendTransaction( - context, - TransactionItem( - sendAddress, - Utils.fetchServiceItem(context, ServiceItems.WALLETOPS), - null, - litoshiAmount.toLong(), - Utils.tieredOpsFee(context, litoshiAmount.toLong()), - null, - false, - comment - ), - ) - AnalyticsManager.logCustomEvent(BWConstants._20191105_DSL) - BRSharedPrefs.incrementSendTransactionCount(context) - } - }, - ) - backgroundLayout.setOnClickListener( - View.OnClickListener { - if (!BRAnimator.isClickAllowed()) return@OnClickListener - animateClose() - }, - ) - closeButton.setOnClickListener { - animateClose() - } - addressEdit.setOnEditorActionListener { _, actionId, event -> - showKeyboard(false) - if (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER || actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_NEXT) { - Utils.hideKeyboard(activity) - Handler().postDelayed({ showKeyboard(true) }, 500) - } - false - } - keyboard.addOnInsertListener { key -> handleClick(key) } - } - - private fun showKeyboard(b: Boolean) { - val curIndex = keyboardIndex - if (!b) { - signalLayout.removeView(keyboardLayout) - } else { - Utils.hideKeyboard(activity) - if (signalLayout.indexOfChild(keyboardLayout) == -1) { - signalLayout.addView( - keyboardLayout, - curIndex, - ) - } else { - signalLayout.removeView(keyboardLayout) - } - } - } - - private fun showClipboardError() { - BRDialog.showCustomDialog( - requireActivity(), - getString(R.string.Send_emptyPasteboard), - resources.getString(R.string.Send_invalidAddressTitle), - getString(R.string.AccessibilityLabels_close), - null, - { brDialogView -> brDialogView.dismiss() }, - null, - null, - 0, - ) - BRClipboardManager.putClipboard(activity, "") - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - val observer = signalLayout.viewTreeObserver - observer.addOnGlobalLayoutListener( - object : OnGlobalLayoutListener { - override fun onGlobalLayout() { - if (observer.isAlive) { - observer.removeOnGlobalLayoutListener(this) - } - BRAnimator.animateBackgroundDim(backgroundLayout, false) - BRAnimator.animateSignalSlide(signalLayout, false) { - val bundle = arguments - if (bundle?.getString("url") != null) setUrl(bundle.getString("url")) - } - } - }, - ) - } - - override fun onStop() { - super.onStop() - } - - override fun onResume() { - super.onResume() - loadMetaData() - } - - override fun onPause() { - super.onPause() - Utils.hideKeyboard(activity) - if (!ignoreCleanup) { - savedIsoCurrencySymbol = null - savedAmount = null - savedMemo = null - } - } - - private fun handleClick(key: String?) { - if (key == null) { - Timber.d("timber: handleClick: key is null! ") - return - } - when { - key.isEmpty() -> { - handleDeleteClick() - } - - Character.isDigit(key[0]) -> { - handleDigitClick(key.substring(0, 1).toInt()) - } - - key[0] == '.' -> { - handleSeparatorClick() - } - } - } - - private fun handleDigitClick(dig: Int) { - val currentAmount = amountBuilder.toString() - val iso = selectedIsoCurrencySymbol - if (BigDecimal(currentAmount + dig.toString()).toDouble() - <= BRExchange.getMaxAmount(activity, iso).toDouble() - ) { - // do not insert 0 if the balance is 0 now - if (currentAmount.equals("0", ignoreCase = true)) amountBuilder = StringBuilder("") - if (currentAmount.contains(".") && currentAmount.length - currentAmount.indexOf(".") > - BRCurrency.getMaxDecimalPlaces( - iso, - ) - ) { - return - } - amountBuilder.append(dig) - updateText() - } - } - - private fun handleSeparatorClick() { - val currentAmount = amountBuilder.toString() - if (currentAmount.contains(".") || BRCurrency.getMaxDecimalPlaces(selectedIsoCurrencySymbol) == 0) return - amountBuilder.append(".") - updateText() - } - - private fun handleDeleteClick() { - val currentAmount = amountBuilder.toString() - if (currentAmount.isNotEmpty()) { - amountBuilder.deleteCharAt(currentAmount.length - 1) - updateText() - } - } - - private fun updateText() { - if (activity == null) return - var tempDoubleAmountValue = 0.0 - if (amountBuilder.toString() != "" && amountBuilder.toString() != ".") { - tempDoubleAmountValue = amountBuilder.toString().toDouble() - } - val scaleValue = 4 - setAmount() - - // Fetch/Set Current ISOSymbol - val selectedISOSymbol = selectedIsoCurrencySymbol - val currencySymbol = BRCurrency.getSymbolByIso(activity, selectedIsoCurrencySymbol) - if (!amountLabelOn) isoCurrencySymbolText.text = currencySymbol - isoCurrencySymbolButton.text = String.format( - "%s(%s)", - BRCurrency.getCurrencyName(activity, selectedIsoCurrencySymbol), - currencySymbol - ) - - // Balance depending on ISOSymbol - currentBalance = BRWalletManager.getInstance().getBalance(activity) - val balanceForISOSymbol = BRExchange.getAmountFromLitoshis( - activity, - selectedISOSymbol, - BigDecimal(currentBalance) - ) - val formattedBalance = - BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, balanceForISOSymbol) - - // Current amount depending on ISOSymbol - val currentAmountInLitoshis = - if (selectedIsoCurrencySymbol.equals("LTC", ignoreCase = true)) { - BRExchange.convertltcsToLitoshis(tempDoubleAmountValue).toLong() - } else { - BRExchange.getLitoshisFromAmount( - activity, - selectedIsoCurrencySymbol, - BigDecimal(tempDoubleAmountValue) - ).toLong() - } - Timber.d("timber: updateText: currentAmountInLitoshis %d", currentAmountInLitoshis) - - // Service Fee depending on ISOSymbol - var serviceFee = Utils.tieredOpsFee(activity, currentAmountInLitoshis) - val serviceFeeForISOSymbol = - BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(serviceFee)) - .setScale(scaleValue, RoundingMode.HALF_UP) - val formattedServiceFee = BRCurrency.getFormattedCurrencyString( - activity, - selectedISOSymbol, - serviceFeeForISOSymbol - ) - - val totalAmountToCalculateFees = currentAmountInLitoshis + serviceFee - - // Network Fee depending on ISOSymbol - var networkFee = if (totalAmountToCalculateFees > 0) { - BRWalletManager.getInstance().feeForTransactionAmount(totalAmountToCalculateFees) - } else { - 0 - } // Amount is zero so network fee is also zero - val networkFeeForISOSymbol = - BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(networkFee)) - .setScale(scaleValue, RoundingMode.HALF_UP) - val formattedNetworkFee = BRCurrency.getFormattedCurrencyString( - activity, - selectedISOSymbol, - networkFeeForISOSymbol - ) - - // Total Fees depending on ISOSymbol - val totalFees = networkFee + serviceFee - val totalFeeForISOSymbol = - BRExchange.getAmountFromLitoshis(activity, selectedISOSymbol, BigDecimal(totalFees)) - .setScale(scaleValue, RoundingMode.HALF_UP) - val formattedTotalFees = - BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, totalFeeForISOSymbol) - - // Update UI with alert red when over balance - if (BigDecimal(currentAmountInLitoshis).toDouble() > currentBalance.toDouble()) { - balanceText.setTextColor(requireContext().getColor(R.color.chili)) - feeText.setTextColor(requireContext().getColor(R.color.chili)) - amountEdit.setTextColor(requireContext().getColor(R.color.chili)) - if (!amountLabelOn) isoCurrencySymbolText.setTextColor(requireContext().getColor(R.color.chili)) - } else { - balanceText.setTextColor(requireContext().getColor(R.color.cheddar)) - feeText.setTextColor(requireContext().getColor(R.color.cheddar)) - amountEdit.setTextColor(requireContext().getColor(R.color.cheddar)) - if (!amountLabelOn) isoCurrencySymbolText.setTextColor(requireContext().getColor(R.color.cheddar)) - } - - balanceText.text = getString(R.string.Send_balance, formattedBalance) - feeText.text = String.format( - "(%s + %s): %s + %s = %s", - getString(R.string.Network_feeLabel), - getString(R.string.Fees_Service), - formattedNetworkFee, - formattedServiceFee, - formattedTotalFees - ) - amountLayout.requestLayout() - } - - fun setUrl(url: String?) { - val obj = BitcoinUrlHandler.getRequestFromString(url) ?: return - if (obj.address != null) { - addressEdit.setText(obj.address.trim { it <= ' ' }) - } - if (obj.message != null) { - commentEdit.setText(obj.message) - } - if (obj.amount != null) { - val iso = selectedIsoCurrencySymbol - val satoshiAmount = BigDecimal(obj.amount).multiply(BigDecimal(100000000)) - amountBuilder = - StringBuilder( - BRExchange.getAmountFromLitoshis(activity, iso, satoshiAmount).toPlainString(), - ) - updateText() - } - } - -// private fun showFeeSelectionButtons(b: Boolean) { -// if (!b) { -// signalLayout.removeView(feeLayout) -// } else { -// signalLayout.addView(feeLayout, signalLayout.indexOfChild(amountLayout) + 1) -// } -// } - - private fun setAmount() { - val tmpAmount = amountBuilder.toString() - var divider = tmpAmount.length - if (tmpAmount.contains(".")) { - divider = tmpAmount.indexOf(".") - } - val newAmount = StringBuilder() - for (i in tmpAmount.indices) { - newAmount.append(tmpAmount[i]) - if (divider > 3 && divider - 1 != i && divider > i && (divider - i - 1) % 3 == 0) { - newAmount.append(",") - } - } - - amountEdit.setText(newAmount.toString()) - } - - private fun isInputValid(input: String): Boolean { - return Pattern.matches("[a-zA-Z0-9]*", input) - } - - // from the link above - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - // Checks whether a hardware keyboard is available - if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) { - Timber.d("timber: onConfigurationChanged: hidden") - showKeyboard(true) - } else if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { - Timber.d("timber: onConfigurationChanged: shown") - showKeyboard(false) - } - } - - private fun saveMetaData() { - if (commentEdit.text.toString().isNotEmpty()) savedMemo = commentEdit.text.toString() - if (amountBuilder.toString().isNotEmpty()) savedAmount = amountBuilder.toString() - savedIsoCurrencySymbol = selectedIsoCurrencySymbol - ignoreCleanup = true - } - - private fun loadMetaData() { - ignoreCleanup = false - if (!Utils.isNullOrEmpty(savedMemo)) commentEdit.setText(savedMemo) - if (!Utils.isNullOrEmpty(savedIsoCurrencySymbol)) { - selectedIsoCurrencySymbol = - savedIsoCurrencySymbol - } - if (!Utils.isNullOrEmpty(savedAmount)) { - amountBuilder = StringBuilder(savedAmount!!) - Handler().postDelayed({ - amountEdit.performClick() - updateText() - }, 500) - } - } - - private fun animateClose() { - BRAnimator.animateBackgroundDim(backgroundLayout, true) - BRAnimator.animateSignalSlide(signalLayout, true) { close() } - } - - private fun close() { - if (activity != null && activity?.isFinishing != true) { - activity?.onBackPressed() - } - } - - companion object { - private var savedMemo: String? = null - private var savedIsoCurrencySymbol: String? = null - private var savedAmount: String? = null - } -} - -// /DEV WIP - -// val approximateNetworkFee = BRCurrency.getFormattedCurrencyString(activity, selectedISOSymbol, feeForISOSymbol) - -// -// var currentAmountValue = 0L -// val amountString = amountEdit.getText().toString() -// if (amountString != "") { -// currentAmountValue = amountString.toDouble().toLong() -// if (selectedIsoCurrencySymbol.equals("LTC")) { -// currentAmountValue = BRExchange.getAmountFromLitoshis( -// activity, -// selectedISOSymbol, -// BigDecimal(currentAmountValue) -// ).toLong() -// } else { -// currentAmountValue = BRExchange.getLitoshisFromAmount( -// activity, -// selectedISOSymbol, -// BigDecimal(currentAmountValue) -// ).toLong() -// } -// diff --git a/app/src/main/java/com/brainwallet/tools/animation/BRAnimator.java b/app/src/main/java/com/brainwallet/tools/animation/BRAnimator.java index 14cd1ca2..0006047b 100644 --- a/app/src/main/java/com/brainwallet/tools/animation/BRAnimator.java +++ b/app/src/main/java/com/brainwallet/tools/animation/BRAnimator.java @@ -27,7 +27,6 @@ import androidx.fragment.app.FragmentManager; import com.brainwallet.navigation.LegacyNavigation; -import com.brainwallet.presenter.fragments.FragmentMoonpay; import com.brainwallet.tools.manager.AnalyticsManager; import com.brainwallet.tools.threads.BRExecutor; import com.brainwallet.constants.BWConstants; @@ -38,7 +37,6 @@ import com.brainwallet.presenter.entities.TxItem; import com.brainwallet.presenter.fragments.FragmentBalanceSeedReminder; import com.brainwallet.presenter.fragments.FragmentReceive; -import com.brainwallet.presenter.fragments.FragmentSend; import com.brainwallet.presenter.fragments.FragmentSignal; import com.brainwallet.presenter.fragments.FragmentTransactionDetails; import com.brainwallet.presenter.interfaces.BROnSignalCompletion; @@ -107,31 +105,6 @@ public static void showBalanceSeedFragment(@NonNull FragmentActivity app) { } finally { } } - public static void showSendFragment(FragmentActivity app, final String bitcoinUrl) { - if (app == null) { - Timber.i("timber: showSendFragment: app is null"); - return; - } - androidx.fragment.app.FragmentManager fragmentManager = app.getSupportFragmentManager(); - FragmentSend fragmentSend = (FragmentSend) fragmentManager.findFragmentByTag(FragmentSend.class.getName()); - if (fragmentSend != null && fragmentSend.isAdded()) { - fragmentSend.setUrl(bitcoinUrl); - return; - } - try { - fragmentSend = new FragmentSend(); - if (bitcoinUrl != null && !bitcoinUrl.isEmpty()) { - Bundle bundle = new Bundle(); - bundle.putString("url", bitcoinUrl); - fragmentSend.setArguments(bundle); - } - fragmentManager.beginTransaction() - .setCustomAnimations(0, 0, 0, R.animator.plain_300) - .add(android.R.id.content, fragmentSend, FragmentSend.class.getName()) - .addToBackStack(FragmentSend.class.getName()).commit(); - } finally { - } - } public static void showTransactionPager(FragmentActivity app, List items, int position) { if (app == null) { @@ -239,27 +212,6 @@ public static void showReceiveFragment(Activity app, boolean isReceive) { } - public static void showMoonpayFragment(FragmentActivity app) { - - if (app == null) { - Timber.i("timber: showSendFragment: app is null"); - return; - } - androidx.fragment.app.FragmentManager fragmentManager = app.getSupportFragmentManager(); - FragmentMoonpay fragmentMoonpay = (FragmentMoonpay) fragmentManager.findFragmentByTag(FragmentMoonpay.class.getName()); - if (fragmentMoonpay != null && fragmentMoonpay.isAdded()) { - return; - } - try { - fragmentMoonpay = new FragmentMoonpay(); - fragmentManager.beginTransaction() - .setCustomAnimations(0, 0, 0, R.animator.plain_300) - .add(android.R.id.content, fragmentMoonpay, FragmentMoonpay.class.getName()) - .addToBackStack(FragmentMoonpay.class.getName()).commit(); - } finally { - } - } - public static boolean isClickAllowed() { if (clickAllowed) { clickAllowed = false; diff --git a/app/src/main/java/com/brainwallet/tools/manager/AnalyticsManager.java b/app/src/main/java/com/brainwallet/tools/manager/AnalyticsManager.java index 9b05afc0..a4af2c72 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/AnalyticsManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/AnalyticsManager.java @@ -23,8 +23,8 @@ public static Object logCustomEvent(@BWConstants.Event String customEvent) { return null; } - public static Object logQuickEvent(String quickEvent, Bundle params) { - instance.logEvent(quickEvent, params); + public static Object logCustomAdHocEvent(String adHocEvent) { + instance.logEvent(adHocEvent, null); return null; } diff --git a/app/src/main/java/com/brainwallet/tools/manager/BRClipboardManager.java b/app/src/main/java/com/brainwallet/tools/manager/BRClipboardManager.java index 4db8c025..0fa9c3fb 100644 --- a/app/src/main/java/com/brainwallet/tools/manager/BRClipboardManager.java +++ b/app/src/main/java/com/brainwallet/tools/manager/BRClipboardManager.java @@ -38,10 +38,11 @@ public static void putDetailsClipboard(Context context, String text) { .newPlainText("Transaction Details: ", text); clipboard.setPrimaryClip(clip); AnalyticsManager.logCustomEventWithParams("tx_details_copied", null); + } catch (Exception e) { Timber.e(e); } - } + } @SuppressLint("NewApi") public static String getClipboard(Context context) { diff --git a/app/src/main/java/com/brainwallet/tools/security/BitcoinUrlHandler.java b/app/src/main/java/com/brainwallet/tools/security/BitcoinUrlHandler.java index 15923bf5..9587651d 100644 --- a/app/src/main/java/com/brainwallet/tools/security/BitcoinUrlHandler.java +++ b/app/src/main/java/com/brainwallet/tools/security/BitcoinUrlHandler.java @@ -1,20 +1,11 @@ package com.brainwallet.tools.security; - -import android.media.metrics.Event; - import androidx.fragment.app.FragmentActivity; - -import com.brainwallet.constants.BWConstants; -import com.brainwallet.tools.animation.BRAnimator; import com.brainwallet.tools.animation.BRDialog; import com.brainwallet.tools.threads.PaymentProtocolTask; -import com.brainwallet.tools.util.Utils; import com.brainwallet.R; import com.brainwallet.presenter.customviews.BRDialogView; -import com.brainwallet.presenter.entities.ServiceItems; import com.brainwallet.presenter.entities.PaymentRequestWrapper; import com.brainwallet.presenter.entities.RequestObject; -import com.brainwallet.presenter.entities.TransactionItem; import com.brainwallet.util.EventBus; import com.brainwallet.wallet.BRWalletManager; @@ -101,7 +92,7 @@ public static RequestObject getRequestFromString(String str) { String host = uri.getHost(); if (host != null) { String addrs = host.trim(); - if (BRWalletManager.validateAddress(addrs)) { + if (BRWalletManager.getInstance().validateAddress(addrs)) { obj.address = addrs; } } diff --git a/app/src/main/java/com/brainwallet/tools/security/LitecoinURIHandler.java b/app/src/main/java/com/brainwallet/tools/security/LitecoinURIHandler.java new file mode 100644 index 00000000..ce540d6c --- /dev/null +++ b/app/src/main/java/com/brainwallet/tools/security/LitecoinURIHandler.java @@ -0,0 +1,110 @@ +package com.brainwallet.tools.security; + +import com.brainwallet.R; +import com.brainwallet.presenter.customviews.BRDialogView; +import com.brainwallet.presenter.entities.PaymentRequestWrapper; +import com.brainwallet.presenter.entities.RequestObject; +import com.brainwallet.tools.animation.BRDialog; +import com.brainwallet.tools.threads.PaymentProtocolTask; +import com.brainwallet.util.EventBus; +import com.brainwallet.wallet.BRWalletManager; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.URI; +import java.net.URLDecoder; +import timber.log.Timber; + +import androidx.fragment.app.FragmentActivity; + +public class LitecoinURIHandler { + + public static RequestObject getRequestFromString(String str) { + if (str == null || str.isEmpty()) return null; + RequestObject obj = new RequestObject(); + + String tmp = str.trim().replaceAll("\n", "").replaceAll(" ", "%20"); + + if (!tmp.startsWith("litecoin://")) { + if (!tmp.startsWith("litecoin:")) + tmp = "litecoin://".concat(tmp); + else + tmp = tmp.replace("litecoin:", "litecoin://"); + } + URI uri; + try { + uri = URI.create(tmp); + } catch (IllegalArgumentException ex) { + Timber.e(ex, "getRequestFromString: "); + return null; + } + + String host = uri.getHost(); + if (host != null) { + String addrs = host.trim(); + if (BRWalletManager.getInstance().validateAddress(addrs)) { + obj.address = addrs; + } + } + String query = uri.getQuery(); + if (query == null) return obj; + String[] params = query.split("&"); + for (String s : params) { + String[] keyValue = s.split("=", 2); + if (keyValue.length != 2) + continue; + if (keyValue[0].trim().equals("amount")) { + try { + BigDecimal bigDecimal = new BigDecimal(keyValue[1].trim()); + obj.amount = bigDecimal.multiply(new BigDecimal("100000000")).toString(); + } catch (NumberFormatException e) { + Timber.e(e); + } + } else if (keyValue[0].trim().equals("label")) { + obj.label = keyValue[1].trim(); + } else if (keyValue[0].trim().equals("message")) { + obj.message = keyValue[1].trim(); + } else if (keyValue[0].trim().startsWith("req")) { + obj.req = keyValue[1].trim(); + } else if (keyValue[0].trim().startsWith("r")) { + obj.r = keyValue[1].trim(); + } + } + return obj; + } + + public static boolean isValidLitecoinURI(String url) { + RequestObject requestObject = getRequestFromString(url); + // return true if the request is valid url and has param: r or param: address + // return true if it is a valid bitcoinPrivKey + return (requestObject != null && (requestObject.r != null || requestObject.address != null) + || BRWalletManager.getInstance().isValidBitcoinBIP38Key(url) + || BRWalletManager.getInstance().isValidBitcoinPrivateKey(url)); + } + + public static boolean isValidLitecoinUrl(String url) { + if (isValidLitecoinURI(url)) { + return BRWalletManager.getInstance().validateAddress(url); + } + return false; + } + + private static boolean tryLitecoinURL(final String url, final FragmentActivity app) { + RequestObject requestObject = getRequestFromString(url); + if (requestObject == null || requestObject.address == null || requestObject.address.isEmpty()) + return false; + + String amount = requestObject.amount; + + if (amount == null || amount.isEmpty() || new BigDecimal(amount).doubleValue() == 0) { + app.runOnUiThread(new Runnable() { + @Override + public void run() { + EventBus.INSTANCE.postQRCodeScanned(requestObject.address); + } + }); + } + return true; + } + + +} diff --git a/app/src/main/java/com/brainwallet/tools/threads/PaymentProtocolTask.java b/app/src/main/java/com/brainwallet/tools/threads/PaymentProtocolTask.java index 525515a2..f35c9db0 100644 --- a/app/src/main/java/com/brainwallet/tools/threads/PaymentProtocolTask.java +++ b/app/src/main/java/com/brainwallet/tools/threads/PaymentProtocolTask.java @@ -121,7 +121,7 @@ public void onClick(BRDialogView brDialogView) { StringBuilder allAddresses = new StringBuilder(); for (String s : paymentRequest.addresses) { allAddresses.append(s).append(", "); - if (!BRWalletManager.validateAddress(s)) { + if (!BRWalletManager.getInstance().validateAddress(s)) { if (app != null) BRDialog.showCustomDialog(app, app.getString(R.string.Alert_error), app.getString(R.string.Send_invalidAddressTitle) + ": " + s, app.getString(R.string.Button_ok), null, new BRDialogView.BROnClickListener() { @Override diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/balancebento/BalanceBentoScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/balancebento/BalanceBentoScreen.kt index e65c8cb8..123ad7eb 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/balancebento/BalanceBentoScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/balancebento/BalanceBentoScreen.kt @@ -11,8 +11,6 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -25,7 +23,6 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text @@ -61,9 +58,7 @@ import com.brainwallet.ui.screens.main.MainScreenState import com.brainwallet.ui.screens.main.MainViewModel import com.brainwallet.ui.theme.DesignTheme import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.balanceBackgroundGradient -import com.brainwallet.ui.theme.bentoDarkBorderGradient -import com.brainwallet.ui.theme.bentoLightBorderGradient +import com.brainwallet.ui.theme.balanceGameBentoSurface import com.brainwallet.ui.theme.blurWhen import kotlinx.collections.immutable.ImmutableList import org.koin.compose.viewmodel.koinViewModel @@ -169,15 +164,7 @@ fun BalanceBentoScreen( modifier = Modifier .fillMaxWidth() .fillMaxHeight() - .background( - brush = balanceBackgroundGradient, - shape = RoundedCornerShape(16.dp) - ) - .border( - width = 1.5.dp, - brush = if (state.darkMode) bentoDarkBorderGradient else bentoLightBorderGradient, - shape = RoundedCornerShape(16.dp) - ) + .balanceGameBentoSurface(isDarkMode = true) ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/favouritesbento/FavouritesBentoScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/favouritesbento/FavouritesBentoScreen.kt index 1fee8896..d77da2e0 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/favouritesbento/FavouritesBentoScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/favouritesbento/FavouritesBentoScreen.kt @@ -35,11 +35,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.airbnb.lottie.model.content.CircleShape import com.brainwallet.constants.favouriteSize import com.brainwallet.ui.theme.DesignTheme import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface +import com.brainwallet.ui.theme.mainBentoSurface import org.koin.compose.viewmodel.koinViewModel @Composable @@ -58,7 +57,7 @@ fun FavouritesBentoScreen( Box( modifier = Modifier .fillMaxSize() - .bentoSurface(isDarkMode) + .mainBentoSurface(isDarkMode) ) { Column( modifier = Modifier diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/FallinScene.kt b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/FallinScene.kt new file mode 100644 index 00000000..62f862f5 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/FallinScene.kt @@ -0,0 +1,132 @@ +package com.brainwallet.ui.bentosections.gamehubbento + +import androidx.compose.animation.core.withInfiniteAnimationFrameMillis +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.sp +import kotlin.random.Random + +public data class EmojiDot( + val emoji: String, + var x: Float, + var y: Float, + var velX: Float, + var velY: Float +) + +public val emojiList = listOf( + "😬", "🇮🇹", "😂", "😭", "❤️", "🤣", "🔥", + "😍", "🥺", "🥰", "🙏", "✨", "👀", "👠", + "🍑", "🌴", "🍌", "🧄", "📻", "🏖️", "🎨", + "💍", "🍋", "📡", "🌙", "🎭", "🦋", "🌸" +) + +@Composable +fun FallinScene( + modifier: Modifier = Modifier, + dotQuantity: Int = 12, +) { + val textMeasurer = rememberTextMeasurer() + val density = LocalDensity.current + val dots = remember { mutableStateListOf() } + var lastTime = remember { 0L } + var spawnTimer = remember { 0f } + + LaunchedEffect(Unit) { + while (true) { + withInfiniteAnimationFrameMillis { frameTimeMs -> + val delta = if (lastTime == 0L) { + 0.6f + } else { + (frameTimeMs - lastTime) / 1000f + } + lastTime = frameTimeMs + + // spawn + spawnTimer += delta + if (spawnTimer >= 1f) { + spawnTimer = 0f + if (dots.size < dotQuantity) { + dots.add( + EmojiDot( + emoji = emojiList.random(), + x = Random.nextFloat(), + y = Random.nextFloat(), + velX = Random.nextFloat() * 100f - 50f, + velY = Random.nextFloat() * 60f - 30f + ) + ) + } else { + dots.clear() + dots.add( + EmojiDot( + emoji = emojiList.random(), + x = Random.nextFloat(), + y = Random.nextFloat(), + velX = Random.nextFloat() * 100f - 50f, + velY = Random.nextFloat() * 60f - 30f + ) + ) + } + } + + // update physics + val gravity = 1000f + val restitution = 0.8f + val friction = 0.04f + + dots.replaceAll { dot -> + var newVelY = dot.velY + gravity * delta + var newVelX = dot.velX * (1f - friction) + var newX = dot.x + newVelX * delta / 1000f + var newY = dot.y + newVelY * delta / 1000f + + if (newY > 1f) { + newY = 1f + newVelY = -newVelY * restitution + } + if (newY < 0f) { + newY = 0f + newVelY = -newVelY * restitution + } + if (newX > 1f) { + newX = 1f + newVelX = -newVelX * restitution + } + if (newX < 0f) { + newX = 0f + newVelX = -newVelX * restitution + } + + dot.copy(x = newX, y = newY, velX = newVelX, velY = newVelY) + } + } + } + } + + Canvas( + modifier = modifier + .fillMaxSize() + ) { + dots.forEach { dot -> + val px = dot.x * size.width + val py = dot.y * size.height + drawText( + textMeasurer = textMeasurer, + text = dot.emoji, + topLeft = Offset(px - 20f, py - 20f), + style = TextStyle(fontSize = 28.sp), + ) + } + } +} diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoPagerScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoPagerScreen.kt index f964f463..7790e4f7 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoPagerScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoPagerScreen.kt @@ -32,7 +32,7 @@ fun GameHubBentoPagerScreen( LaunchedEffect(pagerState.pageCount) { var forward = true while (true) { - delay(5_000) + delay(15_000) if (!pagerState.isScrollInProgress) { val nextPage = if (forward) { (pagerState.currentPage + 1).also { diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoRiceScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoRiceScreen.kt index 6af4a2ff..51340c42 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoRiceScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoRiceScreen.kt @@ -1,6 +1,5 @@ package com.brainwallet.ui.bentosections.gamehubbento import androidx.compose.foundation.Image -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,6 +20,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -30,9 +31,9 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.brainwallet.R +import com.brainwallet.constants.bentoCornerRadius import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoDarkBorderGradient -import com.brainwallet.ui.theme.gameTaglineGradient +import com.brainwallet.ui.theme.balanceGameBentoSurface @Composable fun GameHubBentoRiceScreen( @@ -42,7 +43,7 @@ fun GameHubBentoRiceScreen( val hubPageCount = 3 val pagerState = rememberPagerState(pageCount = { hubPageCount }) var resizedTaglineFontSize by remember { mutableStateOf(16.sp) } - val riceBackground = R.drawable.rice_partner_03 + val riceBackground = R.drawable.rice_partner_04 Card( modifier = modifier.fillMaxSize(), shape = RoundedCornerShape(16.dp), @@ -53,16 +54,13 @@ fun GameHubBentoRiceScreen( modifier = Modifier .fillMaxWidth() .weight(1f) - .border( - width = 0.7.dp, - brush = bentoDarkBorderGradient, - shape = RoundedCornerShape(17.dp) - ) + .balanceGameBentoSurface(isDarkMode = true) ) { Image( painter = painterResource(riceBackground), contentDescription = "game_hub_background", - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() + .clip(RoundedCornerShape(bentoCornerRadius)), contentScale = ContentScale.Crop ) Column( @@ -80,10 +78,10 @@ fun GameHubBentoRiceScreen( } }, style = TextStyle( - brush = gameTaglineGradient, fontFamily = IBMPlexSans, fontWeight = FontWeight.SemiBold, - fontSize = resizedTaglineFontSize + fontSize = resizedTaglineFontSize, + color = Color.Black ), maxLines = 2 ) diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoScreen.kt index 9a18b777..8e4cf431 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoScreen.kt @@ -37,7 +37,7 @@ import com.brainwallet.R import com.brainwallet.constants.bentoCornerRadius import com.brainwallet.ui.theme.BoldenVan import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface +import com.brainwallet.ui.theme.balanceGameBentoSurface import com.brainwallet.ui.theme.gameTaglineGradient import com.brainwallet.ui.theme.gameTitleGradient @@ -54,7 +54,7 @@ fun GameHubBentoScreen( Box( modifier = Modifier .fillMaxSize() - .bentoSurface(isDarkMode = true) + .balanceGameBentoSurface(isDarkMode = true) .clickable(onClick = onClick) ) { Image( @@ -65,6 +65,14 @@ fun GameHubBentoScreen( contentScale = ContentScale.Crop, ) val textWidthRatio = 0.85f + + FallinScene( + modifier = Modifier + .fillMaxWidth(textWidthRatio) + .fillMaxHeight(1f) + .align(Alignment.CenterEnd), + dotQuantity = 14 + ) Column( modifier = Modifier .padding(start = 16.dp) diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoUnagiScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoUnagiScreen.kt index e8d30c25..c659c532 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoUnagiScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/gamehubbento/GameHubBentoUnagiScreen.kt @@ -1,6 +1,5 @@ package com.brainwallet.ui.bentosections.gamehubbento import androidx.compose.foundation.Image -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,6 +20,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -31,8 +31,9 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.brainwallet.R +import com.brainwallet.constants.bentoCornerRadius import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoDarkBorderGradient +import com.brainwallet.ui.theme.balanceGameBentoSurface import com.brainwallet.ui.theme.gameTaglineGradient @Composable @@ -42,34 +43,26 @@ fun GameHubBentoUnagiScreen( ) { val hubPageCount = 3 val pagerState = rememberPagerState(pageCount = { hubPageCount }) - var resizedTaglineFontSize by remember { mutableStateOf(14.sp) } - val unagiBackground = R.drawable.unagi_social_01 + var resizedTaglineFontSize by remember { mutableStateOf(19.sp) } + val unagiBackground = R.drawable.unagi_social_02 Card( modifier = modifier.fillMaxSize(), shape = RoundedCornerShape(18.dp), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - onClick = { - when (unagiBackground) { - R.drawable.unagi_social_01 -> onClick() - else -> Unit - } - } + onClick = onClick ) { Box( modifier = Modifier .fillMaxWidth() - .weight(1f) - .border( - width = 0.7.dp, - brush = bentoDarkBorderGradient, - shape = RoundedCornerShape(18.dp) - ) + .balanceGameBentoSurface(isDarkMode = true) + ) { Image( painter = painterResource(unagiBackground), contentDescription = "game_hub_background", - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() + .clip(RoundedCornerShape(bentoCornerRadius)), contentScale = ContentScale.Crop ) Column( diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt index fe9a3642..1d443c5a 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.R import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext @@ -38,7 +37,7 @@ import com.brainwallet.ui.composable.VerticalWheelPicker import com.brainwallet.ui.composable.WheelPickerFocusVertical import com.brainwallet.ui.composable.rememberWheelPickerState import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface +import com.brainwallet.ui.theme.mainBentoSurface import kotlinx.coroutines.delay import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged @@ -94,7 +93,7 @@ fun LTCPickerBentoScreen( Box( modifier = Modifier .fillMaxSize() - .bentoSurface(state.darkMode) + .mainBentoSurface(state.darkMode) ) { Column( modifier = Modifier diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/CopyTransactionWidget.kt b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/CopyTransactionWidget.kt index 47a5221a..86fb86c2 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/CopyTransactionWidget.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/CopyTransactionWidget.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.brainwallet.ui.bentosections.transactionbento.TransactionBentoViewModel import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface import org.koin.compose.viewmodel.koinViewModel import com.brainwallet.R +import com.brainwallet.ui.theme.mainBentoSurface @Composable fun CopyTransactionsWidget( @@ -38,7 +38,7 @@ fun CopyTransactionsWidget( Box( modifier = Modifier .fillMaxSize() - .bentoSurface(isDarkMode) + .mainBentoSurface(isDarkMode) ) { Row( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/ExportTransactionsWidget.kt b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/ExportTransactionsWidget.kt index b7099c3d..28577ea3 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/ExportTransactionsWidget.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/ExportTransactionsWidget.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.unit.sp import com.brainwallet.constants.transactionActionHt import com.brainwallet.ui.bentosections.transactionbento.TransactionBentoViewModel import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface import org.koin.compose.viewmodel.koinViewModel import com.brainwallet.R +import com.brainwallet.ui.theme.mainBentoSurface @Composable fun ExportedTransactionsWidget( @@ -38,7 +38,7 @@ fun ExportedTransactionsWidget( Box( modifier = Modifier .height(transactionActionHt) - .bentoSurface(isDarkMode) + .mainBentoSurface(isDarkMode) ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/NoTxRow.kt b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/NoTxRow.kt index 63b34ae9..ae5466cb 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/NoTxRow.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/NoTxRow.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -31,6 +32,7 @@ import com.brainwallet.ui.theme.bentoDarkBorderGradient import com.brainwallet.ui.theme.bentoDarkSurfaceGradient import com.brainwallet.ui.theme.bentoLightBorderGradient import com.brainwallet.ui.theme.bentoLightSurfaceGradient +import com.brainwallet.ui.theme.colorMainScreenBackground3 @Composable fun NoTxRow( @@ -89,22 +91,42 @@ fun NoTxRow( maxLines = 1 ) } - + Spacer(modifier = Modifier.weight(0.3f)) Column( - horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth(0.6f) + .padding(vertical = 6.dp), + horizontalAlignment = Alignment.End, ) { - Image( - painter = if (isDarkMode) { - painterResource(R.drawable.powered_by_moonpay_wht) - } else { - painterResource(R.drawable.powered_by_moonpay_blk) - }, - contentDescription = "moonpay_logo", + Box( modifier = Modifier - .fillMaxSize() - .padding(4.dp), - contentScale = ContentScale.Fit - ) + .background( + color = if (isDarkMode) { + Color.White.copy(alpha = 0.1f) + } else { + colorMainScreenBackground3.copy(alpha = 0.05f) + }, + shape = RoundedCornerShape(10.dp) + ) + .border( + width = 0.5.dp, + color = Color.White.copy(alpha = 0.1f), + shape = RoundedCornerShape(10.dp) + ) + ) { + Image( + painter = if (isDarkMode) { + painterResource(R.drawable.powered_by_moonpay_wht) + } else { + painterResource(R.drawable.powered_by_moonpay_blk) + }, + contentDescription = "moonpay_logo", + modifier = Modifier + .fillMaxSize() + .padding(1.dp), + contentScale = ContentScale.Fit + ) + } } } } diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionDetail.kt b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionDetail.kt index 540eb3de..038a17e9 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionDetail.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionDetail.kt @@ -31,7 +31,7 @@ import com.brainwallet.tools.util.BRCurrency import com.brainwallet.tools.util.BRExchange import com.brainwallet.tools.util.BRExchange.ONE_LITECOIN_OF_LITOSHIS import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface +import com.brainwallet.ui.theme.mainBentoSurface import timber.log.Timber import java.math.BigDecimal import java.text.SimpleDateFormat @@ -109,7 +109,7 @@ fun TransactionDetail( modifier = Modifier .fillMaxWidth() .height(sectionHeight) - .bentoSurface(isDarkMode) + .mainBentoSurface(isDarkMode) ) { Column { Row( diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionFilterWidget.kt b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionFilterWidget.kt index 4aeb7223..4aa4e490 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionFilterWidget.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionFilterWidget.kt @@ -29,12 +29,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.brainwallet.ui.bentosections.transactionbento.TransactionBentoViewModel import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface import org.koin.compose.viewmodel.koinViewModel import com.brainwallet.R import com.brainwallet.presenter.entities.TxItem import com.brainwallet.ui.bentosections.transactionbento.TransactionFilterState import com.brainwallet.ui.theme.DesignTheme +import com.brainwallet.ui.theme.mainBentoSurface import kotlinx.collections.immutable.ImmutableList @Composable @@ -60,7 +60,7 @@ fun TransactionFilterWidget( Box( modifier = Modifier .fillMaxSize() - .bentoSurface(isDarkMode) + .mainBentoSurface(isDarkMode) ) { Row( modifier = Modifier.fillMaxHeight(), diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionRow.kt b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionRow.kt index d8fdc2b0..f4409d5b 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionRow.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/transactionbento/subscreens/TransactionRow.kt @@ -33,7 +33,7 @@ import com.brainwallet.tools.util.Utils import com.brainwallet.ui.bentosections.transactionbento.TransactionBentoViewModel import com.brainwallet.ui.theme.DesignTheme import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface +import com.brainwallet.ui.theme.mainBentoSurface import com.brainwallet.wallet.BRPeerManager import org.koin.compose.viewmodel.koinViewModel import timber.log.Timber @@ -125,7 +125,7 @@ fun TransactionRow( Box( modifier = Modifier .height(transactionRowHt) - .bentoSurface(isDarkMode) + .mainBentoSurface(isDarkMode) ) { Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/tutorials/TutorialsBentoScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/tutorials/TutorialsBentoScreen.kt index da2fd879..dbc4eba3 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/tutorials/TutorialsBentoScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/tutorials/TutorialsBentoScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,10 +23,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.brainwallet.ui.theme.IBMPlexSans -import com.brainwallet.ui.theme.bentoSurface +import com.brainwallet.ui.theme.mainBentoSurface import org.koin.compose.viewmodel.koinViewModel @Composable @@ -41,13 +43,13 @@ fun TutorialsBentoScreen( Box( modifier = Modifier .fillMaxSize() - .bentoSurface(isDarkMode) + .mainBentoSurface(isDarkMode) ) { Column( modifier = Modifier .padding(10.dp) .fillMaxHeight(), - verticalArrangement = Arrangement.Top, + verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start ) { Text( @@ -67,20 +69,29 @@ fun TutorialsBentoScreen( .padding(horizontal = 6.dp, vertical = 2.dp) ) Spacer(modifier = Modifier.weight(1f)) - Text( - text = stringResource(com.brainwallet.R.string.coming_soon), - modifier = Modifier - .fillMaxSize() - .padding(30.dp) - .align(Alignment.CenterHorizontally), - style = TextStyle( - fontFamily = IBMPlexSans, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - textAlign = TextAlign.Center, - color = if (isDarkMode) Color.White else Color.Black - ) - ) } + + Text( + text = stringResource(com.brainwallet.R.string.coming_soon), + modifier = Modifier + .fillMaxSize() + .padding(30.dp) + .wrapContentSize(Alignment.Center), + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = if (isDarkMode) Color.White else Color.Black, + textAlign = TextAlign.Center + ), + ) + } +} + +@PreviewLightDark +@Composable +private fun TutorialsBentoScreenPreview() { + Box(modifier = Modifier) { + TutorialsBentoScreen() } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt index 728eb65b..5c0afdc6 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt @@ -4,6 +4,7 @@ import android.net.Uri import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -15,23 +16,28 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Scaffold import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,63 +45,57 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.brainwallet.ui.theme.mainScreenLightSurfaceGradient -import androidx.compose.ui.unit.dp -import com.brainwallet.navigation.OnNavigate -import com.brainwallet.navigation.Route -import com.brainwallet.navigation.UiEffect -import com.brainwallet.ui.composable.BentoBottomNavBar -import com.brainwallet.ui.screens.main.SettingsButton -import com.brainwallet.ui.screens.main.ThemeButton -import kotlinx.coroutines.launch -import org.koin.compose.viewmodel.koinViewModel -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.brainwallet.constants.BWConstants import com.brainwallet.constants.BWConstants.EXPAND_DURATION import com.brainwallet.constants.BWConstants.FADE_IN_DURATION import com.brainwallet.constants.BWConstants.FADE_OUT_DURATION -import com.brainwallet.data.model.AppSetting -import com.brainwallet.ui.bentosections.balancebento.BalanceBentoScreen -import com.brainwallet.ui.bentosections.ltcpickerbento.LTCPickerBentoScreen import com.brainwallet.constants.balanceGameBentoHt +import com.brainwallet.constants.bentoSpacer import com.brainwallet.constants.gameHubHt -import com.brainwallet.constants.statusBarPadding import com.brainwallet.constants.topNavButtonSize import com.brainwallet.constants.topNavStartEndPadding import com.brainwallet.constants.transactionRowHt +import com.brainwallet.data.model.AppSetting +import com.brainwallet.navigation.OnNavigate +import com.brainwallet.navigation.Route +import com.brainwallet.navigation.UiEffect +import com.brainwallet.tools.animation.BRAnimator +import com.brainwallet.tools.manager.AnalyticsManager +import com.brainwallet.ui.bentosections.balancebento.BalanceBentoScreen +import com.brainwallet.ui.bentosections.buyreceivebento.receive.ReceiveDialog +import com.brainwallet.ui.bentosections.favouritesbento.FavouritesBentoScreen +import com.brainwallet.ui.bentosections.gamehubbento.GameHubBentoPagerScreen +import com.brainwallet.ui.bentosections.ltcpickerbento.LTCPickerBentoScreen import com.brainwallet.ui.bentosections.transactionbento.TransactionsBentoScreen +import com.brainwallet.ui.bentosections.tutorials.TutorialsBentoScreen +import com.brainwallet.ui.composable.BentoBottomNavBar import com.brainwallet.ui.screens.main.MainScreenEvent import com.brainwallet.ui.screens.main.MainViewModel -import com.brainwallet.ui.bentosections.buyreceivebento.receive.ReceiveDialog +import com.brainwallet.ui.screens.main.SettingsButton +import com.brainwallet.ui.screens.main.ThemeButton +import com.brainwallet.ui.screens.send.SendScreen import com.brainwallet.ui.screens.settings.settingsrows.HomeSettingDrawerSheet import com.brainwallet.ui.theme.BrainwalletAppTheme +import com.brainwallet.ui.theme.bentoClearGradient +import com.brainwallet.ui.theme.bentoModalDarkGradient +import com.brainwallet.ui.theme.blurAnimatedWith import com.brainwallet.ui.theme.mainScreenDarkSurfaceGradient +import com.brainwallet.ui.theme.mainScreenLightSurfaceGradient import com.brainwallet.util.EventBus import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import androidx.compose.animation.core.animateFloatAsState -import com.brainwallet.ui.theme.blurAnimatedWith -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.imePadding -import androidx.compose.material3.BottomSheetDefaults -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LifecycleEventEffect -import com.brainwallet.constants.BWConstants -import com.brainwallet.constants.bentoSpacer -import com.brainwallet.tools.animation.BRAnimator -import com.brainwallet.ui.bentosections.favouritesbento.FavouritesBentoScreen -import com.brainwallet.ui.bentosections.gamehubbento.GameHubBentoPagerScreen -import com.brainwallet.ui.bentosections.tutorials.TutorialsBentoScreen -import com.brainwallet.ui.screens.send.SendScreen -import com.brainwallet.ui.theme.bentoClearGradient -import com.brainwallet.ui.theme.bentoModalDarkGradient +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel +import timber.log.Timber @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -161,10 +161,11 @@ fun MainScreen( Scaffold( modifier = modifier .background( - if (isDarkMode) mainScreenDarkSurfaceGradient else mainScreenLightSurfaceGradient - ) - .padding( - top = statusBarPadding + if (isDarkMode) { + mainScreenDarkSurfaceGradient + } else { + mainScreenLightSurfaceGradient + } ) .blurAnimatedWith(blurRadiusWhen), containerColor = Color.Transparent, @@ -197,7 +198,6 @@ fun MainScreen( isSheetOpen = true } }, - ) } @@ -205,26 +205,26 @@ fun MainScreen( BoxWithConstraints( modifier = Modifier .fillMaxSize() - .navigationBarsPadding() + .padding(padding) ) { - val topPadding = padding.calculateTopPadding() - val bottomPadding = padding.calculateBottomPadding() - bentoSpacer + val topNavRowHt = topNavButtonSize + (topNavStartEndPadding * 2) val availableHeight = maxHeight - + topNavRowHt - (bentoSpacer * 5) - - topPadding - balanceGameBentoHt - transactionRowHt - - gameHubHt - - bottomPadding + gameHubHt val transactionsDetailHeight = availableHeight + gameHubHt + balanceGameBentoHt - val ltcPickerBentoHeight = (availableHeight * 0.75f) - (bentoSpacer / 2) - val favoritesBentoHeight = (availableHeight * 0.25f) - (bentoSpacer / 2) + val ltcPickerBentoHeight = (availableHeight * 0.7f) - (bentoSpacer / 2) + val favoritesBentoHeight = (availableHeight * 0.3f) - (bentoSpacer / 2) val transactionBentoHeight by animateDpAsState( targetValue = if (showTransactionDetail) transactionsDetailHeight else transactionRowHt, animationSpec = tween(EXPAND_DURATION), label = "transactionHeight" ) - + Timber.d( + "bento layout maxHeight=$maxHeight " + ) Column { Row( modifier = Modifier @@ -272,7 +272,8 @@ fun MainScreen( } item(span = { GridItemSpan(2) }) { TransactionsBentoScreen( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .height(transactionBentoHeight), transactions = state.transactionItems.toImmutableList(), toggleState = state.filterState, @@ -287,7 +288,6 @@ fun MainScreen( } ) } - item(span = { GridItemSpan(1) }) { AnimatedVisibility( visible = !showTransactionDetail, @@ -324,10 +324,13 @@ fun MainScreen( Box(modifier = Modifier.height(gameHubHt)) { GameHubBentoPagerScreen(onClick = { page -> when (page) { - 0 -> Unit + 0 -> { + AnalyticsManager.logCustomEvent("user_did_tap_fallinmoji_no_op") + } 1 -> { modalContentRoute = Route.BuyReceive isSheetOpen = true + AnalyticsManager.logCustomEvent("user_did_tap_mp_in_gh") } 2 -> { val builder = CustomTabsIntent.Builder() @@ -336,6 +339,8 @@ fun MainScreen( context, Uri.parse(BWConstants.LINKTREE_URL) ) + + AnalyticsManager.logCustomEvent("user_did_tap_linktree") } } }) diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt index fa499e7c..e0dc6ba0 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainViewModel.kt @@ -1,5 +1,9 @@ package com.brainwallet.ui.screens.main import android.app.Application +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.core.content.ContextCompat.getSystemService import androidx.lifecycle.viewModelScope import com.brainwallet.R import com.brainwallet.constants.BWConstants @@ -11,7 +15,6 @@ import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.repository.SettingRepository import com.brainwallet.data.repository.TxRepository import com.brainwallet.tools.manager.AnalyticsManager -import com.brainwallet.tools.manager.BRClipboardManager import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.sqlite.TransactionDataSource import com.brainwallet.tools.util.BRExchange.ONE_LITECOIN_OF_LITOSHIS @@ -356,14 +359,12 @@ class MainViewModel( (txItem.received - txItem.sent < 0) }.toImmutableList() } - Timber.d("timber: filtered state: $nextFilter and transactions: ${filteredTransactions.size}") - AnalyticsManager.logCustomEventWithParams("did_toggle_transaction_filter", null) - it.copy( filterState = nextFilter, transactionItems = filteredTransactions ) } + AnalyticsManager.logCustomAdHocEvent("did_toggle_txn_filter") } is MainScreenEvent.OnExportTransactions -> { // TODO: Implement @@ -436,7 +437,15 @@ class MainViewModel( Blockchain browser URL: $txIDBrowserURL """.trimIndent() - BRClipboardManager.putDetailsClipboard(app, transactionDetailsString) + try { + val clipboard = app.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData + .newPlainText("Transaction Details: ", transactionDetailsString) + clipboard.setPrimaryClip(clip) + AnalyticsManager.logCustomEventWithParams("txn_details_copied", null) + } catch (e: java.lang.Exception) { + Timber.e(e) + } } } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/send/BWSender.kt b/app/src/main/java/com/brainwallet/ui/screens/send/BWSender.kt index ed11842d..565329e2 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/send/BWSender.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/send/BWSender.kt @@ -4,6 +4,7 @@ import android.content.Context import com.brainwallet.presenter.entities.TransactionItem import com.brainwallet.tools.security.PostAuth import com.brainwallet.wallet.BRWalletManager +import com.brainwallet.wallet.WalletOperations import kotlinx.coroutines.Dispatchers import org.koin.core.annotation.Single import timber.log.Timber @@ -12,7 +13,7 @@ import kotlinx.coroutines.withContext @Single class BWSender( private val context: Context, - private val getWalletManager: () -> BRWalletManager = { BRWalletManager.getInstance() }, + private val getWalletManager: () -> WalletOperations = { BRWalletManager.getInstance() }, private val getPostAuth: () -> PostAuth = { PostAuth.getInstance() } ) { diff --git a/app/src/main/java/com/brainwallet/ui/screens/send/PreSend.kt b/app/src/main/java/com/brainwallet/ui/screens/send/PreSend.kt index e10a4537..79ae8174 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/send/PreSend.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/send/PreSend.kt @@ -1,4 +1,5 @@ package com.brainwallet.ui.screens.send +import android.R.attr.value import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/com/brainwallet/ui/screens/send/PreSendAddressRow.kt b/app/src/main/java/com/brainwallet/ui/screens/send/PreSendAddressRow.kt index 02c564c8..af07a568 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/send/PreSendAddressRow.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/send/PreSendAddressRow.kt @@ -19,11 +19,11 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle @@ -58,6 +58,10 @@ fun PreSendAddressRow( ) val context = LocalContext.current + LaunchedEffect(value) { + onEvent(SendEvent.OnRecipientAddressChanged(value)) + } + Box( modifier = Modifier .height(sectionHeight) @@ -81,12 +85,7 @@ fun PreSendAddressRow( modifier = Modifier.fillMaxWidth(0.65F) ) { TextField( - modifier = Modifier.height(sectionHeight) - .onFocusChanged { focusState -> - if (focusState.hasFocus) { - onEvent(SendEvent.OnFieldFocused) - } - }, + modifier = Modifier.height(sectionHeight), value = value, onValueChange = { onEvent(SendEvent.OnRecipientAddressChanged(it)) diff --git a/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt index 398bd1e4..de7c2209 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt @@ -43,6 +43,11 @@ class SendViewModel( private val bwSender: BWSender, private val txRepository: TxRepository, private val settingRepository: SettingRepository, + private val isWalletCreated: () -> Boolean = { BRWalletManager.getInstance().isCreated() }, + private val validateAddress: (String) -> Boolean = { BRWalletManager.getInstance().validateAddress(it) }, + private val getBalance: () -> Long = { BRWalletManager.getInstance().getBalance(app) }, + private val getCurrentFee: () -> Long = { FeeManager.getInstance().currentFeeValue }, + private val getOpsFee: (Long) -> Long = { Utils.tieredOpsFee(app, it) }, ) : BrainwalletViewModel() { private val _state = MutableStateFlow(SendState()) @@ -85,7 +90,9 @@ class SendViewModel( txRepository.refresh() val address = _state.value.recipientLTCAddress if (address.isNotBlank()) { - val isValid = BRWalletManager.validateAddress(address) + val isValid = validateAddress(address) + Timber.d("OnTapPasteLTCAddress SendViewModel: isValid : $isValid") + _state.update { it.copy( isLTCAddressValid = isValid, @@ -106,7 +113,9 @@ class SendViewModel( } is SendEvent.OnTapPasteLTCAddress -> { val litecoinUrl = BRClipboardManager.getClipboard(app) - val isAddressValid = BRWalletManager.validateAddress(litecoinUrl) + Timber.d("timber: OnTapPasteLTCAddress: $litecoinUrl") + + val isAddressValid = validateAddress(litecoinUrl) if (isAddressValid) { _state.update { it.copy( @@ -161,8 +170,8 @@ class SendViewModel( } } is SendEvent.OnRecipientAddressChanged -> { - val isAddressValid = if (BRWalletManager.getInstance().isCreated()) { - BRWalletManager.validateAddress(event.address) + val isAddressValid = if (isWalletCreated()) { + validateAddress(event.address) } else { null } @@ -209,9 +218,9 @@ class SendViewModel( ?.toLong() ?: 0L - val currentBalance = BRWalletManager.getInstance().getBalance(app) - val networkFee = FeeManager.getInstance().currentFeeValue - val opsFee = Utils.tieredOpsFee(app, litoshiAmount) + val currentBalance = getBalance() + val networkFee = getCurrentFee() + val opsFee = getOpsFee(litoshiAmount) // isAmountValid is false if rate was unavailable in fiat mode val isAmountValid = amountInLTC != null && @@ -222,8 +231,7 @@ class SendViewModel( it.copy( amountString = event.amountInLTCString, isAmountBelowBalance = isAmountValid, - isReadyToSend = isAmountValid && - BRWalletManager.validateAddress(it.recipientLTCAddress), + isReadyToSend = isAmountValid && it.isLTCAddressValid, networkFees = BigDecimal(networkFee.toDouble()), serviceFees = BigDecimal(opsFee), amountInLitoshi = BigDecimal(litoshiAmount) @@ -240,7 +248,7 @@ class SendViewModel( Utils.fetchServiceItem(app, ServiceItems.WALLETOPS), null, amountInLitoshi, - Utils.tieredOpsFee(app, amountInLitoshi), + getOpsFee(amountInLitoshi), null, false, _state.value.userMemorandum @@ -306,10 +314,14 @@ class SendViewModel( _state.update { it.copy(userMemorandum = event.memo) } } is SendEvent.OnFieldFocused -> { + Timber.d( + "timber: is ${_state.value.isAmountBelowBalance}" + + "\ntimber: isLTC${_state.value.isLTCAddressValid}" + ) + _state.update { it.copy( - isReadyToSend = it.isAmountBelowBalance && - BRWalletManager.validateAddress(it.recipientLTCAddress), + isReadyToSend = (it.isAmountBelowBalance && it.isLTCAddressValid) ) } } @@ -351,7 +363,7 @@ class SendViewModel( // Utils.fetchServiceItem(context, ServiceItems.WALLETOPS), // null, // litoshiAmount.toLong(), -// Utils.tieredOpsFee(context, litoshiAmount.toLong()), +// getOpsFee(litoshiAmount.toLong()), // null, // false, // comment diff --git a/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/HomeSettingDrawerSheet.kt b/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/HomeSettingDrawerSheet.kt index 7bcca907..5bfd38a0 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/HomeSettingDrawerSheet.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/settings/settingsrows/HomeSettingDrawerSheet.kt @@ -3,6 +3,7 @@ package com.brainwallet.ui.screens.settings.settingsrows import android.content.Context import android.net.Uri import android.util.AttributeSet +import android.view.View import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -234,13 +235,22 @@ class HomeSettingDrawerComposeView @JvmOverloads constructor( } } - fun observeBus( - onEach: (EventBus.Event.Message) -> Unit - ) { + fun observeBus(onEach: (EventBus.Event.Message) -> Unit) { + val lifecycleOwner = findViewTreeLifecycleOwner() ?: run { + // View not yet attached — wire up once it is + addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + removeOnAttachStateChangeListener(this) + observeBus(onEach) // retry once attached + } + override fun onViewDetachedFromWindow(v: View) {} + }) + return + } EventBus.events .filter { it is EventBus.Event.Message } .map { it as EventBus.Event.Message } .onEach { onEach.invoke(it) } - .launchIn(findViewTreeLifecycleOwner()!!.lifecycle.coroutineScope) + .launchIn(lifecycleOwner.lifecycle.coroutineScope) } } 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 6d67583d..66c4da7b 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 @@ -24,10 +24,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -35,16 +41,11 @@ 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.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.LottieConstants -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition -import com.brainwallet.BuildConfig import com.brainwallet.R import com.brainwallet.navigation.OnNavigate import com.brainwallet.navigation.Route @@ -55,7 +56,12 @@ import com.brainwallet.ui.composable.DarkModeToggleButton import com.brainwallet.ui.composable.bottomsheet.FiatSelectorBottomSheet import com.brainwallet.ui.composable.bottomsheet.LanguageSelectorBottomSheet import com.brainwallet.constants.BWConstants +import com.brainwallet.constants.bentoCornerRadius +import com.brainwallet.ui.bentosections.gamehubbento.FallinScene +import com.brainwallet.ui.theme.BoldenVan import com.brainwallet.ui.theme.DesignTheme +import com.brainwallet.ui.theme.gameTitleGradient +import kotlinx.coroutines.delay import org.koin.compose.koinInject @Composable @@ -86,11 +92,19 @@ fun WelcomeScreen( val versionPadding = 8 val activeRowHeight = 58 - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.welcomeemoji20250212)) - val progress by animateLottieCompositionAsState( - composition = composition, - iterations = LottieConstants.IterateForever - ) + val gameHubBackground = R.drawable.game_hub_bk + + val fullText = "FALLINMOJI" + var displayedText by remember { mutableStateOf("") } + + LaunchedEffect(Unit) { + fullText.forEachIndexed { index, _ -> + delay(120L) // ms per character + displayedText = fullText.substring(0, index + 1) + } + delay(5000L) + displayedText = "" + } Box( modifier = Modifier @@ -116,24 +130,60 @@ fun WelcomeScreen( .padding(horizontal = 55.dp) ) - // Animation Placeholder - // Shows only during Prod - if (!BuildConfig.DEBUG) { - LottieAnimation( + Box( + modifier = Modifier + .fillMaxWidth() + .height(thirdOfScreenHeight.dp) + ) { + Image( + painter = painterResource(gameHubBackground), + contentDescription = "game_hub_background", + modifier = Modifier.fillMaxWidth() + .padding(leadTrailPadding.dp) + .height(thirdOfScreenHeight.dp) + .clip(RoundedCornerShape(bentoCornerRadius)), + contentScale = ContentScale.Crop, + ) + FallinScene( modifier = Modifier .fillMaxWidth() .padding(leadTrailPadding.dp) - .background( - DesignTheme.colors.surface, - DesignTheme.shapes.large - ) .height(thirdOfScreenHeight.dp) .clip(DesignTheme.shapes.large), - composition = composition, - contentScale = ContentScale.FillWidth, - alignment = Alignment.Center, - progress = { progress } + dotQuantity = 24 ) + + Box( + modifier = Modifier + .fillMaxWidth().padding(top = 48.dp) + .height(65.dp), + Alignment.Center + ) { + Text( + text = displayedText, + style = TextStyle( + fontFamily = BoldenVan, + fontWeight = FontWeight.Normal, + fontSize = 60.sp, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.5f), + offset = Offset(x = 4f, y = 4f), + blurRadius = 4f + ) + ), + maxLines = 1, + ) + Text( + text = displayedText, + style = TextStyle( + brush = gameTitleGradient, + fontFamily = BoldenVan, + fontWeight = FontWeight.Normal, + fontSize = 60.sp + ), + maxLines = 1, + ) + } } } diff --git a/app/src/main/java/com/brainwallet/ui/theme/BentoModifiers.kt b/app/src/main/java/com/brainwallet/ui/theme/BentoModifiers.kt index 3ad2fb21..af357819 100644 --- a/app/src/main/java/com/brainwallet/ui/theme/BentoModifiers.kt +++ b/app/src/main/java/com/brainwallet/ui/theme/BentoModifiers.kt @@ -7,10 +7,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.brainwallet.constants.bentoBorderWidth import com.brainwallet.constants.bentoCornerRadius +import com.brainwallet.constants.mainBentoBorderWidth @Suppress("ModifierComposable") @Composable -fun Modifier.bentoSurface( +fun Modifier.mainBentoSurface( isDarkMode: Boolean ): Modifier = this .background( @@ -18,7 +19,22 @@ fun Modifier.bentoSurface( shape = RoundedCornerShape(bentoCornerRadius) ) .border( - width = bentoBorderWidth, + width = mainBentoBorderWidth, brush = if (isDarkMode) bentoDarkBorderGradient else bentoLightBorderGradient, shape = RoundedCornerShape(bentoCornerRadius) ) + +@Suppress("ModifierComposable") +@Composable +fun Modifier.balanceGameBentoSurface( + isDarkMode: Boolean +): Modifier = this + .background( + brush = if (isDarkMode) balanceBackgroundGradient else bentoLightSurfaceGradient, + shape = RoundedCornerShape(bentoCornerRadius) + ) + .border( + width = bentoBorderWidth, + brush = if (isDarkMode) bentoBalanceGameDarkBorderGradient else bentoLightBorderGradient, + shape = RoundedCornerShape(bentoCornerRadius) + ) diff --git a/app/src/main/java/com/brainwallet/ui/theme/BrainwalletGameState.kt b/app/src/main/java/com/brainwallet/ui/theme/BrainwalletGameState.kt new file mode 100644 index 00000000..ae76e821 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/theme/BrainwalletGameState.kt @@ -0,0 +1,7 @@ +package com.brainwallet.ui.theme + +data class BrainwalletGameState( + val isInGameHubBento: Boolean = false, + val isInWelcomeScreen: Boolean = false, + val isInMainGameHub: Boolean = false, +) diff --git a/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt b/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt index 3aa2d7b9..6581354b 100644 --- a/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt +++ b/app/src/main/java/com/brainwallet/ui/theme/Gradient.kt @@ -19,6 +19,8 @@ val colorMainScreenBackground3 = Color(0xFF491FA3) val colorMainScreenBackground4 = Color(0xFF000000) val colorMainScreenBackground5 = Color(0xFF000000) +val colorMainScreenBackground6 = Color(0xFF5827E2) + /** * A reusable vertical gradient brush for text and backgrounds.33 */ @@ -54,7 +56,7 @@ val gameHubBackgroundGradient: Brush tileMode = TileMode.Clamp ) -val balanceBackgroundGradient: Brush +val balanceBackgroundGradientOld: Brush @Composable get() = Brush.verticalGradient( @@ -64,6 +66,17 @@ val balanceBackgroundGradient: Brush endY = Float.POSITIVE_INFINITY, tileMode = TileMode.Clamp ) + +val balanceBackgroundGradient: Brush + + @Composable + get() = Brush.verticalGradient( + 0.0f to colorMainScreenBackground2, + 1.0f to colorMainScreenBackground3, + startY = 0f, + endY = Float.POSITIVE_INFINITY, + tileMode = TileMode.Clamp + ) val bentoDarkSurfaceGradient: Brush @Composable get() = Brush.verticalGradient( @@ -129,21 +142,33 @@ val bentoClearGradient: Brush start = Offset(Float.MIN_VALUE, Float.POSITIVE_INFINITY), end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY), ) -val bentoDarkBorderGradient: Brush +val bentoBalanceGameDarkBorderGradient: Brush @Composable get() = Brush.linearGradient( colors = listOf( Color(0xFFFFFFFF).copy(alpha = 0.2f), Color(0xFF9074FF).copy(alpha = 0.8f), - Color(0xFF020148).copy(alpha = 1f), - Color(0xFF2B193B).copy(alpha = 1f), + Color(0xFF020148).copy(alpha = 0.8f), + Color(0xFF2B193B).copy(alpha = 0.8f), Color(0xFF6944BE).copy(alpha = 0.2f) ), start = Offset.Zero, end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY), ) +val bentoDarkBorderGradient: Brush + + @Composable + get() = Brush.linearGradient( + colors = listOf( + Color(0xFFFFFFFF).copy(alpha = 0.2f), + Color(0xFFFFFFFF).copy(alpha = 0.2f) + ), + start = Offset.Zero, + end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY), + ) + val bentoModalDarkGradient: Brush @Composable diff --git a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java index 496141b9..4d0bc438 100644 --- a/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java +++ b/app/src/main/java/com/brainwallet/wallet/BRWalletManager.java @@ -60,7 +60,9 @@ import kotlin.Suppress; import timber.log.Timber; -public class BRWalletManager { +public class BRWalletManager implements WalletOperations, + WalletManager, + com.brainwallet.data.source.TransactionSource { private static BRWalletManager instance; public List balanceListeners = new ArrayList<>(); private boolean isInitiatingWallet; @@ -569,7 +571,7 @@ public void setFeePerKb(long fee) { public native TxItem[] getTransactions(); - public static native boolean validateAddress(String address); + public native boolean validateAddress(String address); public native boolean addressContainedInWallet(String address); diff --git a/app/src/main/java/com/brainwallet/wallet/WalletManager.kt b/app/src/main/java/com/brainwallet/wallet/WalletManager.kt new file mode 100644 index 00000000..96c6cb29 --- /dev/null +++ b/app/src/main/java/com/brainwallet/wallet/WalletManager.kt @@ -0,0 +1,6 @@ +package com.brainwallet.wallet + +interface WalletManager { + fun isCreated(): Boolean + fun validateAddress(address: String): Boolean +} diff --git a/app/src/main/java/com/brainwallet/wallet/WalletOperations.kt b/app/src/main/java/com/brainwallet/wallet/WalletOperations.kt new file mode 100644 index 00000000..1279a3d3 --- /dev/null +++ b/app/src/main/java/com/brainwallet/wallet/WalletOperations.kt @@ -0,0 +1,7 @@ +package com.brainwallet.wallet + +interface WalletOperations { + fun tryTransactionWithOps(sendAddress: String, sendAmount: Long, opsAddress: String, opsFeeAmount: Long): ByteArray? + fun getMinOutputAmount(): Long + fun getMinOutputAmountRequested(): Long +} diff --git a/app/src/main/res/drawable/rice_partner_04.jpg b/app/src/main/res/drawable/rice_partner_04.jpg new file mode 100644 index 00000000..b0098582 Binary files /dev/null and b/app/src/main/res/drawable/rice_partner_04.jpg differ diff --git a/app/src/main/res/drawable/unagi_social_02.jpg b/app/src/main/res/drawable/unagi_social_02.jpg new file mode 100644 index 00000000..a7d7429a Binary files /dev/null and b/app/src/main/res/drawable/unagi_social_02.jpg differ diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml deleted file mode 100644 index bd8a0e6b..00000000 --- a/app/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_advanced.xml b/app/src/main/res/layout/activity_advanced.xml deleted file mode 100644 index 22fc60a1..00000000 --- a/app/src/main/res/layout/activity_advanced.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/activity_security_center.xml b/app/src/main/res/layout/activity_security_center.xml deleted file mode 100644 index 5ddef0ff..00000000 --- a/app/src/main/res/layout/activity_security_center.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml deleted file mode 100644 index f6f8ad0f..00000000 --- a/app/src/main/res/layout/activity_settings.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_share_data.xml b/app/src/main/res/layout/activity_share_data.xml deleted file mode 100644 index cf571e30..00000000 --- a/app/src/main/res/layout/activity_share_data.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index f0bdec88..20b0b0f6 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -3,7 +3,6 @@ موقع إلكتروني سياسة الخصوصية عن - Twitter يغلق خطأ تم إبطال صلاحية بيانات Brainwallet المشفرة الخاصة بك مؤخرًا بسبب تعطيل شاشة قفل Android. @@ -260,10 +259,8 @@ "التاريخ: " "المبلغ: " "الرسوم: " - "معرف المعاملة: " لا شيء بعد. أضف رصيداً الآن! سجل المعاملات - معاملات أدخل رمز المرور المستقبل: إرسال Litecoin @@ -278,4 +275,5 @@ أدخل PIN سري جداً البرامج التعليمية - قريباً جداً!أضف رصيداً في 5 دقائق مع شريكناانقر واتبعنا!تصدير جميع المعاملاتنسخ تفاصيل المعاملةالكلمستقبلمرسلسعر LTC + قريباً جداً!أضف رصيداً في 5 دقائق مع شريكناانقر واتبعنا!تصدير جميع المعاملاتنسخ تفاصيل المعاملةالكلمستقبلمرسلسعر LTC + اشترك للحصول على التحديثات والأخبار والمسابقات!"يرجى قبول إذن إشعاراتنا " diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d3abde95..fd09e77e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -5,8 +5,6 @@ Datenschutzerklärung Info - - Twitter Schließen Fehler @@ -393,10 +391,8 @@ "Datum: " "Betrag: " "Gebühren: " - "TX ID: " Noch nichts. Jetzt aufladen! TRANSAKTIONSVERLAUF - Txns Passcode eingeben Empfänger: Litecoin senden @@ -411,4 +407,5 @@ PIN eingeben Top Secret Tutorials - Kommt bald!Laden Sie in 5 Minuten mit unserem Partner aufKlicken Sie und folgen Sie uns!Alle Transaktionen exportierenTransaktionsdetails kopierenAlleEmpfangenGesendetLTC-Preis + Kommt bald!Laden Sie in 5 Minuten mit unserem Partner aufKlicken Sie und folgen Sie uns!Alle Transaktionen exportierenTransaktionsdetails kopierenAlleEmpfangenGesendetLTC-Preis + Melden Sie sich für Updates, Nachrichten und Gewinnspiele an!"Bitte akzeptieren Sie die Berechtigung für unsere Benachrichtigungen " diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3542dbdc..6c8e10ef 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -6,8 +6,6 @@ Política de privacidad Acerca de nosotros - - Twitter Cerrar @@ -468,10 +466,8 @@ "Fecha: " "Monto: " "Comisiones: " - "ID de TX: " Nada aún. ¡Recarga ahora! HISTORIAL DE TRANSACCIONES - txns Ingresa el código de acceso Destinatario: Enviar Litecoin @@ -487,4 +483,5 @@ Top Secret Tutoriales ¡Próximamente! -¡Recarga en 5 minutos con nuestro socio!¡Haz clic y síguenos!Exportar todas las transaccionesCopiar detalles de la transacciónTodasRecibidasEnviadasPrecio LTC +¡Recarga en 5 minutos con nuestro socio!¡Haz clic y síguenos!Exportar todas las transaccionesCopiar detalles de la transacciónTodasRecibidasEnviadasPrecio LTC + ¡Suscríbete a actualizaciones, noticias y concursos!"Por favor, acepta el permiso para nuestras notificaciones " diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 496422fc..22667eeb 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -11,8 +11,6 @@ درباره - - توییتر بستن خطا @@ -494,12 +492,10 @@ تاریخ تعادل - 1 LTC = %1$s - ارزش فعلی LTC در %1$s + ارزش فعلی LTC در %1$s زبان‌ها - ۱: %s ۲: %s ۳: %s ۴: %s \\n۵: %s ۶: %s ۷: %s ۸: %s%s \\n۹: %s ۱۰: %s ۱۱: %s ۱۲: %s %s - نمایش + نمایش نمایش عبارت بازیابی من ترجیح همگام‌سازی: %1$s مثبت‌های کاذب حریم خصوصی کم (حدود ۴۵ دقیقه) @@ -508,10 +504,7 @@ خدمات هزینه‌ها: - عمومی - اخبار لایت کوین - بروزرسانی کیف پول مغزی - لغو + لغو باشه لطفاً اجازه اعلان را صادر کنید اطلاعات مجوز @@ -567,12 +560,7 @@ همگام‌سازی مدت همگام‌سازی: >۲۰ دقیقه بازی %1$d: - %s - %s - %s - %s - پین - عبارت بازیابی + عبارت بازیابی اشتراک‌گذاری داده‌های تحلیلی بروزرسانی پین نمایش @@ -612,11 +600,8 @@ "تاریخ: " "مقدار: " "کارمزد: " - "شناسه تراکنش: " - هنوز چیزی نیست. همین الان شارژ کنید! - تاریخچه تراکنش‌ها - تراکنش‌ها - رمز عبور را وارد کنید + تاریخچه تراکنش‌ها + رمز عبور را وارد کنید گیرنده: ارسال Litecoin تایید جزئیات ارسال: @@ -630,4 +615,5 @@ PIN را وارد کنید اسرار تاپ آموزش‌ها -به زودی!شارژ کنید در ۵ دقیقه با پارتنر ماکلیک کنید و ما را دنبال کنید!صادر کردن تمام تراکنش‌هاکپی جزئیات تراکنشهمهدریافت شدهارسال شدهقیمت LTC +به زودی!شارژ کنید در ۵ دقیقه با پارتنر ماکلیک کنید و ما را دنبال کنید!صادر کردن تمام تراکنش‌هاکپی جزئیات تراکنشهمهدریافت شدهارسال شدهقیمت LTC + برای دریافت به‌روزرسانی‌ها، اخبار و مسابقات ثبت‌نام کنید!"لطفاً اجازه برای اعلان‌های ما را بپذیرید " diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 65e1e1fe..c7b42246 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,8 +10,6 @@ À propos de - - Twitter Fermer @@ -621,10 +619,8 @@ "Date : " "Montant : " "Frais : " - "ID TX : " Rien pour l\'instant. Rechargez maintenant ! HISTORIQUE DES TRANSACTIONS - txns Entrez le code d\'accès Destinataire : Envoyer Litecoin @@ -640,4 +636,5 @@ Top Secret Tutoriels À bientôt ! -Rechargez en 5 minutes avec notre partenaireCliquez et suivez-nous !Exporter toutes les transactionsCopier les détails de la transactionTousReçusEnvoyésPrix LTC +Rechargez en 5 minutes avec notre partenaireCliquez et suivez-nous !Exporter toutes les transactionsCopier les détails de la transactionTousReçusEnvoyésPrix LTC + Inscrivez-vous pour recevoir les mises à jour, actualités et concours !"Veuillez accepter la permission pour nos notifications " diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8967911f..e0f9205b 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -3,7 +3,6 @@ वेबसाइट गोपनीयता नीति के बारे में - ट्विटर बंद करना गलती आपका ब्रेनवॉलेट एन्क्रिप्टेड डेटा हाल ही में अमान्य कर दिया गया था क्योंकि आपकी एंड्रॉइड लॉक स्क्रीन अक्षम कर दी गई थी। @@ -260,11 +259,9 @@ "तारीख: " "राशि: " "शुल्क: " - "TX ID: " - अभी कुछ नहीं। अभी टॉप अप करें! + अभी कुछ नहीं। अभी टॉप अप करें! लेनदेन इतिहास - ट्रांज़ैक्शन - पासकोड दर्ज करें + पासकोड दर्ज करें प्राप्तकर्ता: Litecoin भेजें भेजने के विवरण की पुष्टि करें: @@ -279,4 +276,5 @@ शीर्ष गोपनीय ट्यूटोरियल जल्द ही आ रहा है! -हमारे पार्टनर के साथ 5 मिनट में टॉप अप करेंक्लिक करें और हमें फॉलो करें!सभी लेनदेन निर्यात करेंलेनदेन विवरण कॉपी करेंसभीप्राप्तभेजा गयाLTC मूल्य +हमारे पार्टनर के साथ 5 मिनट में टॉप अप करेंक्लिक करें और हमें फॉलो करें!सभी लेनदेन निर्यात करेंलेनदेन विवरण कॉपी करेंसभीप्राप्तभेजा गयाLTC मूल्य + अपडेट, समाचार और प्रतियोगिताओं के लिए साइन अप करें!"कृपया हमारी सूचनाओं के लिए अनुमति स्वीकार करें " diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index f8345857..96cced96 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -10,8 +10,6 @@ Tentang - - Twitter Menutup @@ -622,10 +620,8 @@ "Tanggal: " "Jumlah: " "Biaya: " - "ID TX: " Belum ada. Top up sekarang! RIWAYAT TRANSAKSI - txns Masukkan kode akses Penerima: Kirim Litecoin @@ -641,4 +637,5 @@ Top Secret Tutorial Segera hadir! -Top up dalam 5 menit dengan mitra kamiKlik dan ikuti kami!Ekspor Semua TransaksiSalin detail transaksiSemuaDiterimaDikirimHarga LTC +Top up dalam 5 menit dengan mitra kamiKlik dan ikuti kami!Ekspor Semua TransaksiSalin detail transaksiSemuaDiterimaDikirimHarga LTC + Daftar untuk mendapatkan update, berita, dan kontes!"Mohon izinkan notifikasi dari kami " diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f86ceba8..f879dda4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -10,8 +10,6 @@ Chi siamo - - Twitter Chiudi @@ -621,10 +619,8 @@ "Data: " "Importo: " "Commissioni: " - "ID TX: " Niente ancora. Ricarica ora! CRONOLOGIA TRANSAZIONI - txns Inserisci passcode Destinatario: Invia Litecoin @@ -640,4 +636,5 @@ Top Secret Tutorial Presto disponibile! -Ricarica in 5 minuti con il nostro partnerClicca e seguici!Esporta tutte le transazioniCopia dettagli transazioneTuttoRicevutoInviatoPrezzo LTC +Ricarica in 5 minuti con il nostro partnerClicca e seguici!Esporta tutte le transazioniCopia dettagli transazioneTuttoRicevutoInviatoPrezzo LTC + Iscriviti per ricevere aggiornamenti, notizie e concorsi!"Accetta l'autorizzazione per le nostre notifiche " diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7d75c2ae..bdd4d57c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -10,8 +10,6 @@ 概要 - - Twitter 閉じる @@ -621,10 +619,8 @@ "日付: " "金額: " "手数料: " - "TX ID: " まだありません。今すぐチャージ! 取引履歴 - 取引 パスコードを入力 受取人: Litecoinを送信 @@ -640,4 +636,5 @@ トップシークレット チュートリアル 近日公開予定! -パートナーでわずか5分でチャージできますクリックしてフォローしてください!すべてのトランザクションをエクスポートトランザクションの詳細をコピーすべて受け取り送信LTC 価格 +パートナーでわずか5分でチャージできますクリックしてフォローしてください!すべてのトランザクションをエクスポートトランザクションの詳細をコピーすべて受け取り送信LTC 価格 + アップデート、ニュース、コンテストの最新情報を受け取ろう!"通知の許可をお願いします " diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 91d3ae75..3f5e053e 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -10,8 +10,6 @@ 관련 사항 - - 트위터 닫기 @@ -621,10 +619,8 @@ "날짜: " "금액: " "수수료: " - "TX ID: " 아직 없습니다. 지금 충전하세요! 거래 내역 - 거래 암호를 입력하세요 받는 사람: Litecoin 전송 @@ -640,4 +636,5 @@ Top Secret 튜토리얼 곧 출시됩니다! -파트너와 함께 5분 안에 충전하세요클릭하고 우리를 팔로우하세요!모든 거래 내역 내보내기거래 상세정보 복사전체받음보냄LTC 가격 +파트너와 함께 5분 안에 충전하세요클릭하고 우리를 팔로우하세요!모든 거래 내역 내보내기거래 상세정보 복사전체받음보냄LTC 가격 + 업데이트, 뉴스 및 이벤트 소식을 받아보세요!"알림 권한을 허용해 주세요 " diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 00000000..6df60f8f --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,290 @@ + + + + Website + Privacybeleid + Over + Sluiten + Fout + Uw Brainwallet-versleutelde gegevens zijn onlangs ongeldig gemaakt omdat uw Android-vergrendelingsscherm is uitgeschakeld. + Android KeyStore-fout + PIN ingesteld + Verzenden mislukt + Verzendbevestiging + Geld verzonden! + Vingerafdruk herkend + Annuleren + Nee + OK + Verzenden + Ja + Bedrag om te verzenden: + Netwerk + Naar + Totale kosten: + Litecoin weergave-eenheid + Wisselkoers + Brainwallet kan niet worden geverifieerd vanwege een bug in uw Android-versie. [Tik hier voor meer informatie.] + Vingerafdruk niet herkend. Probeer het opnieuw. + Saldo van privésleutel controleren... + %1$s van deze privésleutel naar uw portemonnee verzenden? Het Litecoin-netwerk ontvangt een vergoeding van %2$s. + Deze privésleutel is leeg. + Geen geldige privésleutel + Fout bij ondertekening van transactie + Deze privésleutel is met een wachtwoord beveiligd. + Negeren + WAARSCHUWING + Portemonnee vergrendelen + Beveiligingscentrum + Instellingen + Klantenondersteuning + Nieuwe portemonnee maken + Menu + Overschakelen naar automatische modus + Verbonden + Voer Node IP-adres en poort in (optioneel) + Node invoeren + Schakel naar handmatige modus + Huidige primaire Node + Niet verbonden + Node-verbindingsstatus + Litecoin Nodes + Ongeldige betalingsverzoek + Niet-ondersteund of beschadigd document + verzoek verlopen + Litecoin-betalingen kunnen niet minder zijn dan %1$s. + Litecoin transactie-outputs kunnen niet minder zijn dan %1$s. + niet-vertrouwd certificaat + Uw portemonnee is mogelijk niet gesynchroniseerd. Dit kan vaak worden opgelost door de blockchain opnieuw in te scannen. + Transactie afgewezen + Brainwallet is bijgewerkt naar een 6-cijferige PIN. Tik hier om bij te werken. + PIN bijwerken + Schakel meldingen in om speciale berichten van Brainwallet in de toekomst te ontvangen. + Pushmeldingen + Meldingen + Gekopieerd naar klembord. + E-mail + Delen + Ontvangen + Gereed + Portemonnee herstellen + Herstel uw Brainwallet met uw papieren sleutel. + De papieren sleutel die u hebt ingevoerd, is ongeldig. Controleer elk woord zorgvuldig en probeer het opnieuw. + Volgende + Synchroniseren + Synchroniseren met blockchain? + Als een transactie als voltooid wordt weergegeven op het Litecoin-netwerk, maar niet in uw Brainwallet. + U krijgt herhaaldelijk een foutmelding dat uw transactie is afgewezen. + Synchronisatie starten + U kunt geen geld verzenden tijdens het synchroniseren met de blockchain. + Wanneer synchroniseren? + Schakel alle beveiligingsfuncties in voor maximale bescherming. + De enige manier om uw Litecoin te benaderen als u uw telefoon verliest of vervangt. + Papieren sleutel + Beschermt je Brainwallet tegen ongeautoriseerde gebruikers. + 4-Cijferige PIN + Beveiligingscentrum + Bedrag + Saldo: %1$s + Sta cameratoegang toe in \"Instellingen\" > \"Apps\" > \"Brainwallet\" > \"Machtigingen\" + Brainwallet mag niet op de camera zugreifen + Het doeladdres is je eigen adres. Je kunt niet naar jezelf versturen. + Notitie + Klembord is leeg + Ongeldig adres + Verzenden is uitgeschakeld tijdens een volledige scan. + Plakken + Kon betalingsverzoek niet laden + Scannen + Verzenden + Verzenden + Voer een Litecoin-adres in + Litecoin-adressen zijn bedoeld voor eenmalig gebruik. + Hergebruik vermindert de privacy voor jou en de ontvanger en kan tot verlies leiden als de ontvanger het adres niet rechtstreeks beheert. + Over + Geavanceerde instellingen + Weergavemunt + Beheren + Anonieme gegevens delen + Blockchain synchroniseren + Portemonnee + Help Brainwallet verbeteren door je anonieme gegevens met ons te delen. Dit omvat geen financiële informatie. We respecteren je financiële privacy. + Gegevens delen? + Anonieme gegevens delen? + Je papieren sleutel is de enige manier om je Brainwallet te herstellen als je telefoon verloren gaat, gestolen wordt, kapot gaat of wordt bijgewerkt.\n\nWe laten je een lijst met woorden zien die je op een stuk papier moet schrijven en veilig moet bewaren. + Papieren sleutel opschrijven + Synchroniseren + Passcode altijd vereisen + Je wordt gevraagd je 6-cijferige PIN in te voeren om transacties boven je uitgavenlimiet te verzenden, en elke 48 uur sinds je je 6-cijferige PIN voor het laatst hebt ingevoerd. + Vingerafdruk autorisatie uitgavenlimiet + Beschikbaar om uit te geven + Voltooid + Eindsaldo: %1$s + ONGELDIG + Startsaldo: %1$s + Memo + Uw transacties verschijnen hier. + bij %1$s + %1$s ontvangen + %1$s verzonden + Status + Transactiedetails + naar %1$s + Litecoin Transactie-ID + Ontvangen op dit adres + Verzonden naar dit adres + Mijn adres + PIN opnieuw instellen + Voer PIN in + Aanraaksensor + Ontgrendel uw Android-apparaat om door te gaan. + Bekijk op blockchain + Verificatie vereist + Portefeuille ontgrendeld + Bent u uw zaadzin of PIN vergeten? + Onthoud deze PIN. Als u deze vergeet, kunt u niet meer bij uw Litecoin. + Uw PIN wordt gebruikt om uw Brainwallet te ontgrendelen en geld te verzenden. + PIN instellen + PIN opnieuw invoeren + Voer uw huidige PIN in. + Voer uw nieuwe PIN in. + Voer uw nieuwe PIN opnieuw in. + PIN bijwerken + Kopiëren + Bijwerken... + Weet u zeker dat u deze portefeuille wilt verwijderen? + Portefeuille wissen? + Wissen + Geschiedenis + Saldo + Huidige LTC-waarde in %1$s + Talen + Weergeven + Mijn zaadzin weergeven + Sync-voorkeur: %1$s valse positieven + Lage privacy (~45 minuten) + Semi-privé (~60 minuten) + Anoniem (~77 minuten) + Service + Kosten: + Annuleren + Ok + Verleen meldingspermissie + Permissie-info + Meldingspermissie verleend + Terug + Je seed-woorden + "Alleen voor jou. \n Het is de <b xmlns:tools="http://schemas.android.com/tools">privésleutel</b> waarmee je kunt verzenden." + Blockchain: Litecoin + Ik heb het op papier of metaal opgeslagen + Je hebt het opgeslagen, toch? + Bewijs het!\nSleep de woorden in de juiste volgorde. + Spel & Sync + Klaar om te beginnen? + Dit is alleen voor jou. + Stel de app-code in, open je wachtwoordmanager of pak een pen om deze en je nieuwe seedfrase op te schrijven. + We kennen het niet en we hebben geen kopie! + App-code instellen + Kies een code om je Brainwallet te ontgrendelen + Niet je telefoonsluitcode! + Maak het anders. Maak het cool. + Je hebt je sleutels opgeslagen. + Voltooi het volgende niveau. + Tik op een woord en sleep het op zijn plaats + Bijvullen? + Krijg Litecoin in 5 minuten + Krijg wat Litecoin! + Misschien later. + Herstel je macht + Je kunt terugkomen naar waar je bent begonnen + Herstel mijn Brainwallet + Wissen + Gok niet.\n\nHet zou je \n5.444.517.950.000.000.000.000.000.000.000.000.000.000.000.000.000 pogingen kosten + Bevestigen + Je bent het toch niet vergeten? Voer het opnieuw in. Of ga terug om opnieuw te beginnen. + Herstellen met Seed Phrase + Onjuiste Passcode, probeer het alstublieft opnieuw! + down-left-arrow + toggle-dark-mode + Beveiliging + Taal + Valuta + Games + Ondersteuning + Social Media + Vergrendelen + Ontgrendelen + Thema + App-versie: + Metagegevens synchroniseren: + Synchroniseren + Synchronisatieduur: >20 minuten + Game %1$d: + Seed phrase + Analysegegevens delen + PIN bijwerken + Weergeven + LTC kopen + Aangedreven door Moonpay + Laden... + Netwerkgebeur (per kb): Een hogere waarde betekent dat de transactie sneller wordt voltooid + Hoog + Gemiddeld + Laag + Bedrag ligt onder minimaal limiet (%f) + Bedrag overschrijdt maximaal limiet (%f) + Litecoin kopen + Zal worden gestort op uw LTC-adres: + Kopen met Moonpay + Nieuw adres + Kopen / Ontvangen + Aangepast + Opnieuw instellen / Opnieuw beginnen + Transactiegegevens exporteren + BEN JIJ GOED GENOEG OM #1 TE ZIJN? + Papieren vliegtuigpictogram voor de verzendactie + Twee pijlen die kopen en ontvangen van Litecoin vertegenwoordigen + gaming controller + circulaire pijl tegen de klok in roterend + Game Hub + Geschiedenis + per %1$s + MIJN SALDO + Synchronisatie zonder internet niet mogelijk! + Maak alstublieft opnieuw verbinding om de synchronisatie te voltooien. + Portefeuille tx-aantal: + eenvoudige thuistekening + Start + "Memo: " + "Blok: " + "Datum: " + "Bedrag: " + "Kosten: " + Nog niets. Vul nu aan! + TRANSACTIEGESCHIEDENIS + Voer passcode in + Ontvanger: + Litecoin verzenden + Bevestig verzendgegevens: + Bewerken + Netwerkkosten + Servicekosten + Bedrag in fiatvaluta + Saldo: + Wisselkoers: + Voer een geldig LTC-adres en bedrag in om door te gaan + Voer PIN in + Top Secret + Tutorials + Komt binnenkort! + Vul in 5 minuten bij met onze partner + Klik en volg ons! + Meld je aan voor updates, nieuws en wedstrijden! + "Accepteer alstublieft toestemming voor onze meldingen" + Alle transacties exporteren + Transactiegegevens kopiëren + Alles + Ontvangen + Verzonden + LTC Prijs + diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index c5ae58b8..ca45b535 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -11,8 +11,6 @@ متعلق - - ٹوئٹر بند کرو @@ -517,12 +515,10 @@ تاریخ توازن - 1 ایل ٹی سی = %1$s - موجودہ ایل ٹی سی قیمت %1$s میں + موجودہ ایل ٹی سی قیمت %1$s میں زبانیں - 1: %s 2: %s 3: %s 4: %s \\n5: %s 6: %s 7: %s 8: %s%s \\n9: %s 10: %s 11: %s 12: %s %s - شو + شو میری سیڈ فریس دکھاؤ ترجیح ہم آہنگی: %1$s غلط مثبتیاں کم پرائیویسی (تقریباً 45 منٹ) @@ -531,10 +527,7 @@ سروس فیس: - جنرل - لائٹ کوائن خبریں - برین والٹ اپڈیٹ - منسوخ کریں۔ + منسوخ کریں۔ ٹھیک ہے۔ براہ کرم اطلاع کی اجازت دیں۔ اجازت کی معلومات @@ -592,12 +585,7 @@ مزامنت ہم آہنگی کا دورانیہ: >20 منٹ کھیل %1$d: - %s - %s - %s - %s - پن - بیج عبارت + بیج عبارت تجزیاتی ڈیٹا شئیر کریں۔ پن کو اپ ڈیٹ کریں شوو @@ -637,8 +625,7 @@ "تاریخ: " "رقم: " "فیس: " - "TX ID: " - ہالے کجھ نئیں۔ ہُنے ٹاپ اَپ کرو! + ہالے کجھ نئیں۔ ہُنے ٹاپ اَپ کرو! ٹرانزیکشن تاریخ - ٹرانزیکشنز -ਪਾਸਕੋਡ ਦਰਜ ਕਰੋਪ੍ਰਾਪਤਕਾਰ:Litecoin ਭੇਜੋਭੇਜਣ ਦਾ ਵੇਰਵਾ ਪੁਸ਼ਟੀ ਕਰੋ:ਸੰਪਾਦਿਤ ਕਰੋਨੈਟਵਰਕ ਫੀਸਸੇਵਾ ਫੀਸFiat ਵਿੱਚ ਰਕਮਬਕਾਇਆ:ਵਿਨਿਮਯ ਦਰ:ਜਾਰੀ ਰੱਖਣ ਲਈ ਇੱਕ ਵੈਧ LTC ਪਤਾ ਅਤੇ ਰਕਮ ਦਰਜ ਕਰੋPIN ਦਰਜ ਕਰੋਸਰਵੋਚ ਗੁਪਤਟਿਊਟੋਰਿਅਲਜਲਦੀ ਆ ਰਿਹਾ ਹੈ!ਆਪਣੇ ਭਾਈ ਦੇ ਨਾਲ 5 ਮਿੰਟ ਵਿੱਚ ਟপ ਅਪ ਕਰੋਕਲਿੱਕ ਕਰੋ ਅਤੇ ਸਾਡੀ ਪਾਲਣਾ ਕਰੋ!ਸਾਰੀਆਂ ਲੈਣ-ਦੇਣ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋਲੈਣ-ਦੇਣ ਦੇ ਵੇਰਵਿਆਂ ਨੂੰ ਕਾਪੀ ਕਰੋਸਭਪ੍ਰਾਪਤ ਕੀਤਾਭੇਜਿਆ ਗਿਆLTC ਕੀਮਤ + ਪਾਸਕੋਡ ਦਰਜ ਕਰੋਪ੍ਰਾਪਤਕਾਰ:Litecoin ਭੇਜੋਭੇਜਣ ਦਾ ਵੇਰਵਾ ਪੁਸ਼ਟੀ ਕਰੋ:ਸੰਪਾਦਿਤ ਕਰੋਨੈਟਵਰਕ ਫੀਸਸੇਵਾ ਫੀਸFiat ਵਿੱਚ ਰਕਮਬਕਾਇਆ:ਵਿਨਿਮਯ ਦਰ:ਜਾਰੀ ਰੱਖਣ ਲਈ ਇੱਕ ਵੈਧ LTC ਪਤਾ ਅਤੇ ਰਕਮ ਦਰਜ ਕਰੋPIN ਦਰਜ ਕਰੋਸਰਵੋਚ ਗੁਪਤਟਿਊਟੋਰਿਅਲਜਲਦੀ ਆ ਰਿਹਾ ਹੈ!ਆਪਣੇ ਭਾਈ ਦੇ ਨਾਲ 5 ਮਿੰਟ ਵਿੱਚ ਟপ ਅਪ ਕਰੋਕਲਿੱਕ ਕਰੋ ਅਤੇ ਸਾਡੀ ਪਾਲਣਾ ਕਰੋ!ਸਾਰੀਆਂ ਲੈਣ-ਦੇਣ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋਲੈਣ-ਦੇਣ ਦੇ ਵੇਰਵਿਆਂ ਨੂੰ ਕਾਪੀ ਕਰੋਸਭਪ੍ਰਾਪਤ ਕੀਤਾਭੇਜਿਆ ਗਿਆLTC ਕੀਮਤ + ਅਪਡੇਟ, ਖ਼ਬਰਾਂ ਅਤੇ ਮੁਕਾਬਲਿਆਂ ਲਈ ਸਾਈਨ ਅਪ ਕਰੋ!"ਕਿਰਪਾ ਕਰਕੇ ਸਾਡੀਆਂ ਸੂਚਨਾਵਾਂ ਲਈ ਅਨੁਮਤੀ ਦਿਓ " diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index aaf6bccc..6c994eb8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -11,8 +11,6 @@ O - - Twitter Zamknij @@ -515,12 +513,10 @@ Historia Równowaga - 1 LTC = %1$s - Aktualna wartość LTC w %1$s + Aktualna wartość LTC w %1$s Języki - 1: %s 2: %s 3: %s 4: %s \\n5: %s 6: %s 7: %s 8: %s%s \\n9: %s 10: %s 11: %s 12: %s %s - Pokaż + Pokaż Pokaż moje nasiona Preferencje synchronizacji: %1$s fałszywe alarmy Niski poziom prywatności (~45 minut) @@ -529,10 +525,7 @@ Usługa Opłaty: - Ogólne - Wiadomości Litecoin - Aktualizacja Brainwallet - Anuluj + Anuluj Ok Prosimy o zezwolenie na powiadomienie Informacje o zezwoleniu @@ -587,12 +580,7 @@ Synchronizacja Czas trwania synchronizacji: >20 minut Gra %1$d: - %s - %s - %s - %s - PIN - Fraza seed + Fraza seed Udostępnianie danych analitycznych Aktualizacja kodu PIN Pokaż @@ -632,8 +620,7 @@ "Data: " "Kwota: " "Opłaty: " - "ID TX: " - Nic jeszcze. Doładuj teraz! + Nic jeszcze. Doładuj teraz! HISTORIA TRANSAKCJI - transakcje -Wprowadź kod dostępuOdbiorca:Wyślij LitecoinPotwierdź szczegóły przesyłki:EdytujOpłaty siecioweOpłaty za usługęKwota w walucie fiducjarnejSaldo:Kurs wymiany:Wpisz prawidłowy adres LTC i kwotę, aby kontynuowaćWprowadź PINTop SecretPoradnikiWkrótce!Doładuj w 5 minut z naszym partneremKliknij i obserwuj nas!Eksportuj wszystkie transakcjeSkopiuj szczegóły transakcjiWszystkieOtrzymaneWysłaneCena LTC + Wprowadź kod dostępuOdbiorca:Wyślij LitecoinPotwierdź szczegóły przesyłki:EdytujOpłaty siecioweOpłaty za usługęKwota w walucie fiducjarnejSaldo:Kurs wymiany:Wpisz prawidłowy adres LTC i kwotę, aby kontynuowaćWprowadź PINTop SecretPoradnikiWkrótce!Doładuj w 5 minut z naszym partneremKliknij i obserwuj nas!Eksportuj wszystkie transakcjeSkopiuj szczegóły transakcjiWszystkieOtrzymaneWysłaneCena LTC + Zarejestruj się, aby otrzymywać aktualizacje, wiadomości i informacje o konkursach!"Proszę wyraź zgodę na nasze powiadomienia " diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 65a24938..0f7d67ae 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -260,11 +260,9 @@ "Data: " "Valor: " "Taxas: " - "ID da TX: " - Nada ainda. Recarregue agora! + Nada ainda. Recarregue agora! HISTÓRICO DE TRANSAÇÕES - txns - Digite a senha + Digite a senha Destinatário: Enviar Litecoin Confirme os detalhes do envio: @@ -279,4 +277,5 @@ Super Secreto Tutoriais Em breve! -Recarregue em 5 minutos com nosso parceiroClique e nos siga!Exportar Todas as TransaçõesCopiar detalhes da transaçãoTodasRecebidoEnviadoPreço do LTC +Recarregue em 5 minutos com nosso parceiroClique e nos siga!Exportar Todas as TransaçõesCopiar detalhes da transaçãoTodasRecebidoEnviadoPreço do LTC + Cadastre-se para receber atualizações, notícias e promoções!"Por favor, aceite a permissão para nossas notificações " diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0b599283..2efac42b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -10,8 +10,6 @@ Sobre - - Twitter Fechar @@ -620,9 +618,7 @@ "Data: " "Valor: " "Taxas: " - "ID TX: " Nada ainda. Faça um depósito agora! HISTÓRICO DE TRANSAÇÕES - txns diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8eda2a72..1dab2949 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -10,8 +10,6 @@ О нас - - Twitter Закрыть @@ -621,10 +619,8 @@ "Дата: " "Сумма: " "Комиссия: " - "ID транзакции: " Пока ничего. Пополните сейчас! ИСТОРИЯ ТРАНЗАКЦИЙ - транзакции Введите пароль Получатель: Отправить Litecoin @@ -640,4 +636,5 @@ Top Secret Обучающие материалы Скоро! -Пополните счет за 5 минут с нашим партнеромНажмите и подпишитесь на нас!Экспортировать все транзакцииКопировать детали транзакцииВсеПолученоОтправленоЦена LTC +Пополните счет за 5 минут с нашим партнеромНажмите и подпишитесь на нас!Экспортировать все транзакцииКопировать детали транзакцииВсеПолученоОтправленоЦена LTC + Подпишитесь на обновления, новости и конкурсы!"Пожалуйста, разрешите отправку уведомлений " diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 1c402703..4bfc361a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -3,7 +3,6 @@ Webbplats Sekretesspolicy Om - Kvittra Nära Fel Din Brainwallet-krypterade data ogiltigförklarades nyligen eftersom din Android-låsskärm var inaktiverad. @@ -260,11 +259,9 @@ "Datum: " "Belopp: " "Avgifter: " - "TX ID: " - Inget ännu. Fyll på nu! + Inget ännu. Fyll på nu! TRANSAKTIONSHISTORIK - txns -Ange lösenord + Ange lösenord Mottagare: Skicka Litecoin Bekräfta skicka-detaljer: @@ -279,4 +276,5 @@ Top Secret Guider Kommer snart! -Fylla på på 5 minuter med vår partnerKlicka och följ oss!Exportera alla transaktionerKopiera transaktionsdetaljerAllaMottagetSkickatLTC-pris +Fylla på på 5 minuter med vår partnerKlicka och följ oss!Registrera dig för uppdateringar, nyheter och tävlingar!"Vänligen acceptera behörighet för våra meddelanden " + Exportera alla transaktionerKopiera transaktionsdetaljerAllaMottagetSkickatLTC-pris diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml new file mode 100644 index 00000000..89c54299 --- /dev/null +++ b/app/src/main/res/values-th/strings.xml @@ -0,0 +1,289 @@ + + + ส่ง + สร้างกระเป๋าเงินใหม่ + เว็บไซต์ + นโยบายความเป็นส่วนตัว + เกี่ยวกับ + ปิด + ข้อผิดพลาด + ข้อมูลเข้ารหัส Brainwallet ของคุณถูกยกเลิกเมื่อเร็ว ๆ นี้เนื่องจากปิดใช้งานหน้าจอล็อกของ Android + ข้อผิดพลาด Android KeyStore + ตั้ง PIN เรียบร้อย + การส่งล้มเหลว + ยืนยันการส่ง + ส่งเงินเรียบร้อย! + จำได้ลายนิ้วมือ + ยกเลิก + ไม่ + ตกลง + ใช่ + จำนวนที่จะส่ง: + เครือข่าย + ถึง + ต้นทุนรวม: + หน่วยแสดงผล Litecoin + อัตราแลกเปลี่ยน + Brainwallet ไม่สามารถยืนยันตัวตนได้เนื่องจากข้อบug ในเวอร์ชัน Android ของคุณ [โปรดแตะที่นี่เพื่อข้อมูลเพิ่มเติม] + ไม่รู้จักลายนิ้วมือ โปรดลองอีกครั้ง + กำลังตรวจสอบยอดคงเหลือของคีย์ส่วนตัว... + ส่ง %1$s จากคีย์ส่วนตัวนี้เข้าไปในกระเป๋าเงินของคุณ? เครือข่าย Litecoin จะได้รับค่าธรรมเนียม %2$s + คีย์ส่วนตัวนี้ว่างเปล่า + ไม่ใช่คีย์ส่วนตัวที่ถูกต้อง + ข้อผิดพลาดในการลงนามในธุรกรรม + คีย์ส่วนตัวนี้ได้รับการป้องกันด้วยรหัสผ่าน + ละเว้น + คำเตือน + ล็อกกระเป๋าเงิน + ศูนย์ความปลอดภัย + การตั้งค่า + การสนับสนุนลูกค้า + เมนู + สลับไปโหมดอัตโนมัติ + เชื่อมต่อแล้ว + ป้อนที่อยู่ IP ของโหนดและพอร์ต (ไม่บังคับ) + ป้อนโหนด + 切换到手动模式 + โหนดหลักปัจจุบัน + ไม่ได้เชื่อมต่อ + สถานะการเชื่อมต่อโหนด + Litecoin Nodes + คำขอชำระเงินไม่ถูกต้อง + เอกสารที่ไม่รองรับหรือเสียหาย + คำขอหมดอายุ + การชำระเงิน Litecoin ไม่สามารถน้อยกว่า %1$s + เอาต์พุตธุรกรรม Litecoin ไม่สามารถน้อยกว่า %1$s + ใบรับรองที่ไม่น่าเชื่อถือ + วอลเล็ตของคุณอาจไม่ตรงกัน สามารถแก้ไขได้บ่อยครั้งโดยการสแกนบล็อกเชนใหม่ + ธุรกรรมถูกปฏิเสธ + Brainwallet ได้อัปเกรดเป็นการใช้ PIN 6 หลัก แตะที่นี่เพื่ออัปเกรด + อัปเกรด PIN + เปิดการแจ้งเตือนเพื่อรับข้อความพิเศษจาก Brainwallet ในอนาคต + Push Notifications + การแจ้งเตือน + คัดลอกไปยังคลิปบอร์ดแล้ว + อีเมล + แชร์ + รับเงิน + เสร็จสิ้น + กู้คืนวอลเล็ต + กู้คืน Brainwallet ของคุณด้วยกุญแจกระดาษ + กุญแจกระดาษที่คุณป้อนไม่ถูกต้อง โปรดตรวจสอบแต่ละคำและลองอีกครั้ง + ถัดไป + ซิงค์ + ซิงค์กับบล็อกเชน + หากธุรกรรมแสดงว่าเสร็จสิ้นบนเครือข่าย Litecoin แต่ไม่ใช่ใน Brainwallet + คุณได้รับข้อผิดพลาดซ้ำๆ ที่บอกว่าธุรกรรมของคุณถูกปฏิเสธ + เริ่มซิงค์ + คุณจะไม่สามารถส่งเงินได้ขณะซิงค์กับบล็อกเชน + เมื่อใดจึงต้องซิงค์ + เปิดใช้งานฟีเจอร์ความปลอดภัยทั้งหมดเพื่อการป้องกันสูงสุด + วิธีเดียวในการเข้าถึง Litecoin ของคุณหากคุณสูญหายหรืออัปเกรดโทรศัพท์ของคุณ + Paper Key + ป้องกัน Brainwallet ของคุณจากผู้ใช้ที่ไม่ได้รับอนุญาต + PIN 4 หลัก + ศูนย์ความปลอดภัย + จำนวนเงิน + ยอดคงเหลือ: %1$s + อนุญาตการเข้าถึงกล้องใน "Settings" > "Apps" > "Brainwallet" > "Permissions" + Brainwallet ไม่ได้รับอนุญาตให้เข้าถึงกล้อง + ที่อยู่ปลายทางเป็นที่อยู่ของคุณเอง คุณไม่สามารถส่งเงินให้ตัวเองได้ + หมายเหตุ + ไม่มีข้อมูลในคลิปบอร์ด + ที่อยู่ไม่ถูกต้อง + ฟังก์ชันการส่งเงินถูกปิดใช้งานระหว่างการสแกนแบบเต็ม + วาง + ไม่สามารถโหลดคำขอการชำระเงิน + สแกน + ส่ง + ส่ง + ป้อนที่อยู่ Litecoin + ที่อยู่ Litecoin มีไว้สำหรับใช้เพียงครั้งเดียวเท่านั้น + การใช้ซ้ำจะลดความเป็นส่วนตัวสำหรับคุณและผู้รับ และอาจส่งผลให้สูญเสียเงินหากผู้รับไม่ควบคุมที่อยู่โดยตรง + เกี่ยวกับ + การตั้งค่าขั้นสูง + สกุลเงินแสดงผล + จัดการ + แบ่งปันข้อมูลที่ไม่ระบุตัวตน + ซิงค์บล็อกเชน + กระเป๋าเงิน + ช่วยปรับปรุง Brainwallet โดยแบ่งปันข้อมูลที่ไม่ระบุตัวตนกับเรา ซึ่งไม่รวมข้อมูลทางการเงินใดๆ เราเคารพความเป็นส่วนตัวทางการเงินของคุณ + แบ่งปันข้อมูลหรือไม่? + แบ่งปันข้อมูลที่ไม่ระบุตัวตนหรือไม่? + กระดาษคีย์ของคุณเป็นวิธีเดียวในการคืนค่า Brainwallet ของคุณหากโทรศัพท์ของคุณหายไป ถูกขโมย เสียหาย หรืออัปเกรด\n\nเราจะแสดงรายการคำที่คุณต้องจดลงบนกระดาษและเก็บไว้อย่างปลอดภัย + จดกระดาษคีย์ + กำลังซิงค์ + ต้องใช้รหัส PIN เสมอ + คุณจะถูกขอให้ป้อน PIN 6 หลักเพื่อส่งธุรกรรมใดๆ ที่เกินขีดจำกัดการใช้จ่ายของคุณ และทุกๆ 48 ชั่วโมงนับจากครั้งสุดท้ายที่คุณป้อน PIN 6 หลัก + ขีดจำกัดการใช้จ่ายการอนุญาตลายนิ้วมือ + พร้อมใช้งาน + สมบูรณ์ + ยอดคงเหลือขั้นสุดท้าย: %1$s + ไม่ถูกต้อง + ยอดคงเหลือเริ่มต้น: %1$s + หมายเหตุ + รายการธุรกรรมของคุณจะปรากฏที่นี่ + จาก %1$s + ได้รับ %1$s + ส่ง %1$s + สถานะ + รายละเอียดธุรกรรม + ส่งไปยัง %1$s + รหัสธุรกรรม Litecoin + ได้รับที่ที่อยู่นี้ + ส่งไปยังที่อยู่นี้ + ที่อยู่ของฉัน + รีเซ็ต PIN + ป้อน PIN + เซ็นเซอร์สัมผัส + โปรดปลดล็อกอุปกรณ์ Android ของคุณเพื่อดำเนินการต่อ + ดูบน blockchain + จำเป็นต้องยืนยันตัวตน + ปลดล็อกกระเป๋าเงินแล้ว + ลืม Seed Phrase หรือ PIN หรือไม่ + จำไว้ว่า PIN นี้ หากคุณลืมมันคุณจะไม่สามารถเข้าถึง Litecoin ของคุณได้ + PIN ของคุณจะใช้ในการปลดล็อก Brainwallet และส่งเงิน + ตั้งค่า PIN + ป้อน PIN อีกครั้ง + ป้อน PIN ปัจจุบันของคุณ + ป้อน PIN ใหม่ของคุณ + ป้อน PIN ใหม่ของคุณอีกครั้ง + อัปเดต PIN + คัดลอก + กำลังอัปเดต... + คุณแน่ใจหรือว่าต้องการลบกระเป๋าเงินนี้ + ลบกระเป๋าเงิน? + ลบ + ประวัติ + ยอดคงเหลือ + มูลค่า LTC ปัจจุบันในหน่วย %1$s + ภาษา + แสดง + แสดง Seed Phrase ของฉัน + การตั้งค่าการซิงค์: %1$s false positives + ความเป็นส่วนตัวต่ำ (~45 นาที) + กึ่งส่วนตัว (~60 นาที) + ไม่เปิดเผยตัวตน (~77 นาที) + บริการ + ค่าธรรมเนียม: + ยกเลิก + ตกลง + กรุณาอนุญาตการแจ้งเตือน + ข้อมูลการอนุญาต + อนุญาตการแจ้งเตือนแล้ว + กลับ + คำ seed ของคุณ + "สำหรับคุณเท่านั้น \n นี่คือ <b xmlns:tools="http://schemas.android.com/tools">กุญแจส่วนตัว</b> ที่ให้คุณส่งเงิน" + Blockchain: Litecoin + ฉันบันทึกไว้บนกระดาษหรือโลหะ + คุณบันทึกไว้แล้วใช่ไหม + พิสูจน์สิ!\nลากคำต่าง ๆ ตามลำดับที่ถูกต้อง + เกม & ซิงค์ + พร้อมเริ่มต้นหรือยัง + นี่คือสำหรับคุณคนเดียว + ตั้งค่า PIN ของแอป เปิดตัวจัดการรหัสผ่านของคุณ หรือเตรียมปากกาเพื่อบันทึก PIN และ seed phrase ใหม่ของคุณ + เราไม่รู้และไม่มีสำเนา! + ตั้งค่า PIN แอป + เลือก PIN เพื่อปลดล็อก Brainwallet ของคุณ + ไม่ใช่รหัสล็อคโทรศัพท์! + ทำให้แตกต่าง ทำให้เจ๋ง + คุณบันทึกกุญแจของคุณแล้ว + พิชิตระดับถัดไป + แตะคำและลากไปที่ตำแหน่ง + เติมเงินหรือ + รับ Litecoin ใน 5 นาที + รับ Litecoin! + อาจจะทีหลัง + คืนกำลังของคุณ + คุณสามารถกลับไปยังจุดเริ่มต้นได้ + คืน Brainwallet ของฉัน + ล้าง + อย่าเดาสุ่ม\n\nจะใช้เวลา \n5,444,517,950,000,000,000,000,000,000,000,000,000,000,000,000,000 ครั้ง + ยืนยัน + คุณไม่ลืมใช่ไหม ป้อนอีกครั้ง หรือกลับไปเริ่มใหม่ + คืน ด้วย Seed Phrase + รหัสผ่านไม่ถูกต้อง โปรดลองอีกครั้ง! + down-left-arrow + toggle-dark-mode + ความปลอดภัย + ภาษา + สกุลเงิน + เกม + การสนับสนุน + โซเชียลมีเดีย + ล็อค + ปลดล็อค + ธีม + เวอร์ชันแอป: + ซิงค์เมตาดาต้า: + ซิงค์ + ระยะเวลาซิงค์: >20 นาที + เกม %1$d: + Seed phrase + แชร์ข้อมูลการวิเคราะห์ + อัปเดต PIN + แสดง + ซื้อ LTC + ขับเคลื่อนโดย Moonpay + กำลังโหลด... + ค่าธรรมเนียมเครือข่าย (ต่อ kb): ค่าที่สูงขึ้นหมายความว่าธุรกรรมจะเสร็จสิ้นเร็วขึ้น + สูง + ปานกลาง + ต่ำ + จำนวนเงินต่ำกว่าขีดจำกัดขั้นต่ำ (%f) + จำนวนเงินเกินขีดจำกัดสูงสุด (%f) + ซื้อ Litecoin + จะถูกฝากเข้าที่อยู่ LTC ของคุณ: + ซื้อด้วย Moonpay + ที่อยู่ใหม่ + ซื้อ / รับ + กำหนดเอง + รีเซ็ต / เริ่มใหม่ + ส่งออกข้อมูลธุรกรรม + คุณเก่งพอที่จะเป็น #1 หรือไม่? + ไอคอนเครื่องบินกระดาษแทนการกระทำส่ง + ลูกศรสองลูกแสดงการซื้อและรับ Litecoin + ตัวควบคุมเกมส์ + ลูกศรวงกลมหมุนทวนเข็มนาฬิกา + Game Hub + ประวัติ + ณ วันที่ %1$s + ยอดคงเหลือของฉัน + ไม่สามารถซิงค์ได้หากไม่มีอินเทอร์เน็ต! + โปรดเชื่อมต่ออีกครั้งเพื่อทำการซิงค์ + จำนวนธุรกรรมกระเป๋า: + รูปบ้านอย่างง่าย + หน้าแรก + "หมายเหตุ: " + "บล็อก: " + "วันที่: " + "จำนวน: " + "ค่าธรรมเนียม: " + ยังไม่มีรายการ เติมเงินตอนนี้! + ประวัติการทำธุรกรรม + ป้อนรหัสผ่าน + ผู้รับ: + ส่ง Litecoin + ยืนยันรายละเอียดการส่ง: + แก้ไข + ค่าธรรมเนียมเครือข่าย + ค่าธรรมเนียมบริการ + จำนวนเงินตรา + ยอดคงเหลือ: + อัตราแลกเปลี่ยน: + ป้อนที่อยู่ LTC และจำนวนที่ถูกต้องเพื่อดำเนินการต่อ + ป้อน PIN + ลับสุด + บทช่วยสอน + เร็วๆ นี้! + เติมเงินในเวลา 5 นาทีกับพาร์ตเนอร์ของเรา + คลิกและติดตามเราได้เลย! + ลงทะเบียนเพื่อรับข้อมูลข่าวสาร การแข่งขัน และอื่นๆ! + "โปรดยอมรับสิทธิ์สำหรับการแจ้งเตือนของเรา " + ส่งออกรายการทั้งหมด + คัดลอกรายละเอียดรายการ + ทั้งหมด + รับได้ + ส่งแล้ว + ราคา LTC + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index bc5c3a0b..786a136b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -10,8 +10,6 @@ hakkında - - Twitter Kapat @@ -643,11 +641,9 @@ "Tarih: " "Tutar: " "Ücretler: " - "TX ID: " Henüz yok. Hemen yükle! İŞLEM GEÇMİŞİ - işlemler -Şifre girin + Şifre girin Alıcı: Litecoin gönder Gönderme detaylarını onayla: @@ -662,4 +658,5 @@ Top Secret Eğitimler Çok yakında! -Ortağımız ile 5 dakikada bakiye yükleyinTıklayın ve bizi takip edin!Tüm İşlemleri Dışa Aktarİşlem detaylarını kopyalaTümüAlınanGönderilenLTC Fiyatı +Ortağımız ile 5 dakikada bakiye yükleyinTıklayın ve bizi takip edin!Güncellemeler, yenilikler ve yarışmalar için kaydolun!"Lütfen bildirimlerimiz için izin verin " + Tüm İşlemleri Dışa Aktarİşlem detaylarını kopyalaTümüAlınanGönderilenLTC Fiyatı diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index be29b8da..49320450 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -11,8 +11,6 @@ Про - - Twitter Закрити @@ -626,8 +624,7 @@ "Дата: " "Сума: " "Комісія: " - "ID транзакції: " Поки нічого. Поповніть зараз! ІСТОРІЯ ТРАНЗАКЦІЙ - транзакції -Введіть парольОдержувач:Відправити LitecoinПідтвердіть деталі відправки:РедагуватиКомісія мережіКомісія сервісуСума в готівціБаланс:Курс обміну:Введіть коректну адресу LTC та суму, щоб продовжитиВведіть PINТоп СекретНавчальні матеріалиСкоро!Поповніть рахунок за 5 хвилин через нашого партнераКлікніть і слідкуйте за нами!Експортувати всі транзакціїКопіювати деталі транзакціїУсіОтриманоВідправленоЦіна LTC + Введіть парольОдержувач:Відправити LitecoinПідтвердіть деталі відправки:РедагуватиКомісія мережіКомісія сервісуСума в готівціБаланс:Курс обміну:Введіть коректну адресу LTC та суму, щоб продовжитиВведіть PINТоп СекретНавчальні матеріалиСкоро!Поповніть рахунок за 5 хвилин через нашого партнераКлікніть і слідкуйте за нами!Підпишіться на оновлення, новини та конкурси!"Будь ласка, дозвольте доступ до сповіщень" + Експортувати всі транзакціїКопіювати деталі транзакціїУсіОтриманоВідправленоЦіна LTC diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7ff53774..01008b52 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -10,8 +10,6 @@ 关于 - - Twitter 关闭 @@ -621,10 +619,8 @@ "日期: " "金额: " "手续费: " - "交易 ID: " 暂无记录。立即充值! 交易记录 - 交易 输入密码 收款人: 发送 Litecoin @@ -640,4 +636,5 @@ 最高机密 教程 敬请期待! -通过我们的合作伙伴5分钟内充值点击关注我们!导出所有交易复制交易详情全部已接收已发送LTC 价格 +通过我们的合作伙伴5分钟内充值点击关注我们!注册以获取更新、新闻和竞赛信息!"请允许我们的通知权限" + 导出所有交易复制交易详情全部已接收已发送LTC 价格 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 568da6c6..2aba814c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -9,8 +9,6 @@ 關於 - - Twitter 關閉 @@ -621,10 +619,8 @@ "日期: " "金額: " "手續費: " - "交易 ID: " 尚無紀錄。立即儲值! 交易紀錄 - 交易 輸入密碼 收款人: 發送 Litecoin @@ -640,4 +636,5 @@ 絕密 教學 即將推出! -透過我們的合作夥伴在 5 分鐘內充值點擊並追蹤我們!匯出所有交易複製交易詳情全部已接收已發送LTC 價格 +透過我們的合作夥伴在 5 分鐘內充值點擊並追蹤我們!註冊以接收更新、新功能和競賽資訊!"請允許我們傳送通知權限" + 匯出所有交易複製交易詳情全部已接收已傳送LTC 價格 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d86471e..b13abc11 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,8 +12,6 @@ Instagram About - - Twitter Close @@ -477,5 +475,7 @@ Received Sent LTC Price + Sign up to updates, news and contests! + "Please accept permission for our notifications " diff --git a/app/src/test/java/com/brainwallet/data/repository/TxRepositoryImplIntegrationTest.kt b/app/src/test/java/com/brainwallet/data/repository/TxRepositoryImplIntegrationTest.kt new file mode 100644 index 00000000..e8509774 --- /dev/null +++ b/app/src/test/java/com/brainwallet/data/repository/TxRepositoryImplIntegrationTest.kt @@ -0,0 +1,279 @@ +package com.brainwallet.data.repository + +import android.app.Application +import app.cash.turbine.test +import com.brainwallet.data.source.TransactionSource +import com.brainwallet.presenter.entities.TxItem +import com.brainwallet.util.MainDispatcherRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +/** + * Integration tests for the receive-transaction flow. + * + * TxRepositoryImpl is the single source of truth for transaction state. It pulls + * raw TxItems from BRWalletManager, exposes them as an immutable StateFlow, and + * provides filtered views for sent and received transactions. Tests cover the full + * refresh lifecycle, filtering correctness, and StateFlow emission behaviour. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class TxRepositoryImplIntegrationTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private lateinit var app: Application + private lateinit var transactionSource: TransactionSource + private lateinit var repository: TxRepositoryImpl + + @Before + fun setUp() { + app = mockk(relaxed = true) + transactionSource = mockk(relaxed = true) + repository = TxRepositoryImpl(app, transactionSource) + } + + @After + fun tearDown() { + unmockkAll() + } + + private fun makeTxItem( + sent: Long = 0L, + received: Long = 0L, + timeStamp: Long = 1_000_000L, + txReversed: String = "abc123", + ) = TxItem( + timeStamp, + 800_000, + byteArrayOf(0x01), + txReversed, + sent, + received, + 1_000L, + arrayOf("LReceiver"), + arrayOf("LSender"), + 0L, + 250, + longArrayOf(sent + received), + true, + ) + + // ── initial state ───────────────────────────────────────────────────────── + + @Test + fun `transactionItems StateFlow starts empty before any refresh`() { + assertTrue(repository.transactionItems.value.isEmpty()) + } + + // ── refresh: happy path ─────────────────────────────────────────────────── + + @Test + fun `refresh populates transactionItems with all returned transactions`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 50_000L), + makeTxItem(sent = 30_000L), + ) + + repository.refresh() + + assertEquals(2, repository.transactionItems.value.size) + } + + @Test + fun `refresh with a single received transaction stores it correctly`() = runTest { + val expectedReceived = 75_000L + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = expectedReceived) + ) + + repository.refresh() + + assertEquals(1, repository.transactionItems.value.size) + assertEquals(expectedReceived, repository.transactionItems.value[0].received) + } + + @Test + fun `refresh with a single sent transaction stores it correctly`() = runTest { + val expectedSent = 40_000L + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(sent = expectedSent) + ) + + repository.refresh() + + assertEquals(1, repository.transactionItems.value.size) + assertEquals(expectedSent, repository.transactionItems.value[0].sent) + } + + // ── refresh: edge cases ─────────────────────────────────────────────────── + + @Test + fun `refresh with null from wallet produces empty list`() = runTest { + every { transactionSource.getTransactions() } returns null + + repository.refresh() + + assertTrue(repository.transactionItems.value.isEmpty()) + } + + @Test + fun `refresh with empty array produces empty list`() = runTest { + every { transactionSource.getTransactions() } returns emptyArray() + + repository.refresh() + + assertTrue(repository.transactionItems.value.isEmpty()) + } + + @Test + fun `subsequent refresh replaces previous transaction list`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 10_000L) + ) + repository.refresh() + assertEquals(1, repository.transactionItems.value.size) + + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 20_000L), + makeTxItem(received = 30_000L), + ) + repository.refresh() + + assertEquals(2, repository.transactionItems.value.size) + } + + @Test + fun `refresh after populated state with null collapses list to empty`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf(makeTxItem(received = 10_000L)) + repository.refresh() + + every { transactionSource.getTransactions() } returns null + repository.refresh() + + assertTrue(repository.transactionItems.value.isEmpty()) + } + + // ── receive filter ──────────────────────────────────────────────────────── + + @Test + fun `allReceiveTransactions returns only items with received greater than zero`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 50_000L), + makeTxItem(sent = 30_000L), + ) + repository.refresh() + + val result = repository.allReceiveTransactions() + + assertEquals(1, result.size) + assertEquals(50_000L, result[0].received) + } + + @Test + fun `allReceiveTransactions returns all items when all have received amounts`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 10_000L), + makeTxItem(received = 20_000L), + makeTxItem(received = 30_000L), + ) + repository.refresh() + + val result = repository.allReceiveTransactions() + + assertEquals(3, result.size) + } + + @Test + fun `allReceiveTransactions returns empty when no incoming transactions exist`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(sent = 20_000L), + makeTxItem(sent = 10_000L), + ) + repository.refresh() + + assertTrue(repository.allReceiveTransactions().isEmpty()) + } + + // ── sent filter ─────────────────────────────────────────────────────────── + + @Test + fun `allSentTransactions returns only items with sent greater than zero`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 50_000L), + makeTxItem(sent = 30_000L), + ) + repository.refresh() + + val result = repository.allSentTransactions() + + assertEquals(1, result.size) + assertEquals(30_000L, result[0].sent) + } + + @Test + fun `allSentTransactions returns all items when all have sent amounts`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(sent = 10_000L), + makeTxItem(sent = 20_000L), + ) + repository.refresh() + + val result = repository.allSentTransactions() + + assertEquals(2, result.size) + } + + @Test + fun `allSentTransactions returns empty when no outgoing transactions exist`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 20_000L) + ) + repository.refresh() + + assertTrue(repository.allSentTransactions().isEmpty()) + } + + // ── StateFlow emissions ─────────────────────────────────────────────────── + + @Test + fun `transactionItems flow emits empty then updated list after refresh`() = runTest { + every { transactionSource.getTransactions() } returns arrayOf(makeTxItem(received = 10_000L)) + + repository.transactionItems.test { + assertEquals(0, awaitItem().size) // initial empty emission + repository.refresh() + assertEquals(1, awaitItem().size) // post-refresh emission + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `transactionItems flow emits twice on two refreshes with different data`() = runTest { + repository.transactionItems.test { + awaitItem() // initial empty + + every { transactionSource.getTransactions() } returns arrayOf(makeTxItem(received = 5_000L)) + repository.refresh() + assertEquals(1, awaitItem().size) + + every { transactionSource.getTransactions() } returns arrayOf( + makeTxItem(received = 5_000L), + makeTxItem(sent = 3_000L), + ) + repository.refresh() + assertEquals(2, awaitItem().size) + + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/app/src/test/java/com/brainwallet/localisation/StringsLocalisationCoverageTest.kt b/app/src/test/java/com/brainwallet/localisation/StringsLocalisationCoverageTest.kt new file mode 100644 index 00000000..3dfd8064 --- /dev/null +++ b/app/src/test/java/com/brainwallet/localisation/StringsLocalisationCoverageTest.kt @@ -0,0 +1,205 @@ +package com.brainwallet.localisation + +import org.junit.Test +import org.w3c.dom.Element +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory +import kotlin.math.roundToInt +import kotlin.test.DefaultAsserter.assertTrue + +/** + * Informational test that compares strings.xml coverage across all locale resource folders. + * + * For each values-XX folder found under app/src/main/res it reports: + * - How many of the English (values/) keys are present + * - The coverage percentage + * - Which keys are missing (printed at the end for each locale) + * + * This test NEVER fails — it is purely diagnostic. Run it and read the console output. + * + * Usage: + * ./gradlew :app:testDebugUnitTest --tests "*.StringsLocalisationCoverageTest" + */ +class StringsLocalisationCoverageTest { + + companion object { + private val RES_DIR: File by lazy { + val candidates = listOf( + File("src/main/res"), + File("app/src/main/res"), + ) + candidates.firstOrNull { it.isDirectory } + ?: error( + "Cannot locate app/src/main/res. " + + "Searched: ${candidates.map { it.absolutePath }}" + ) + } + + private val EXCLUDED_DIRS = setOf( + "values-land", + "values-night", + "values-w600dp", + "values-w1240dp", + "values-h700dp" + ) + } + + // ────────────────────────────────────────────────────────────────────── + // Main test entry point + // ────────────────────────────────────────────────────────────────────── + + @Test + fun `print localisation coverage report`() { + val englishKeys = parseStringKeys(File(RES_DIR, "values/strings.xml")) + check(englishKeys.isNotEmpty()) { + "English strings.xml is empty or missing — check path ${RES_DIR.absolutePath}/values/strings.xml" + } + + val localeResults = discoverLocaleFiles() + .map { (locale, file) -> + val localeKeys = parseStringKeys(file) + val present = englishKeys.intersect(localeKeys).size + val missing = englishKeys - localeKeys + val extra = localeKeys - englishKeys // keys the locale has but English doesn't + LocaleReport( + locale = locale, + file = file, + total = englishKeys.size, + present = present, + missing = missing.sorted(), + extra = extra.sorted(), + ) + } + .sortedByDescending { it.coveragePct } + + printReport(englishKeys, localeResults) + + localeResults.forEach { assertTrue("${it.locale} coverage below 80%", it.coveragePct >= 80.0) } + } + + // ────────────────────────────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────────────────────────────── + + /** Parse all keys from a strings.xml file. */ + private fun parseStringKeys(file: File): Set { + if (!file.exists()) return emptySet() + return try { + val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file) + val nodes = doc.getElementsByTagName("string") + (0 until nodes.length) + .map { nodes.item(it) as Element } + .mapNotNull { it.getAttribute("name").takeIf { n -> n.isNotBlank() } } + .toSet() + } catch (e: Exception) { + System.err.println(" ⚠ Could not parse ${file.path}: ${e.message}") + emptySet() + } + } + + /** + * Find every values-XX/strings.xml under RES_DIR. + * Returns pairs of (locale-suffix, file), e.g. ("ar", File("…/values-ar/strings.xml")). + */ + private fun discoverLocaleFiles(): List> = + RES_DIR.listFiles() + .orEmpty() + .filter { it.isDirectory && it.name.startsWith("values-") && it.name !in EXCLUDED_DIRS } + .mapNotNull { dir -> + val locale = dir.name.removePrefix("values-") + val stringsFile = File(dir, "strings.xml") + if (stringsFile.exists()) locale to stringsFile else null + } + .sortedBy { it.first } + + /** Pretty-print the full coverage report to stdout. */ + private fun printReport(englishKeys: Set, results: List) { + val divider = "─".repeat(72) + + println() + println("╔══════════════════════════════════════════════════════════════════════╗") + println("║ BRAINWALLET · Strings Localisation Coverage Report ║") + println("╚══════════════════════════════════════════════════════════════════════╝") + println() + println(" English baseline: ${englishKeys.size} keys") + println(" Locales found: ${results.size}") + println() + println(" %-12s %6s / %-6s %7s %s".format("Locale", "Keys", "Total", "Coverage", "Status")) + println(" $divider") + + results.forEach { r -> + val bar = progressBar(r.coveragePct, width = 20) + val status = when { + r.file.length() == 0L -> "⚠ EMPTY FILE" + r.present == 0 -> "✗ NO MATCHING KEYS" + r.coveragePct >= 95 -> "✓ Complete" + r.coveragePct >= 75 -> "△ Partial" + else -> "✗ Incomplete" + } + println( + " %-12s %6d / %-6d %6.1f%% [%s] %s".format( + r.locale, + r.present, + r.total, + r.coveragePct, + bar, + status + ) + ) + } + + println() + println(" $divider") + println() + + // Detail section — missing keys per locale + val withMissing = results.filter { it.missing.isNotEmpty() } + if (withMissing.isEmpty()) { + println(" 🎉 All locales have 100% key coverage!") + } else { + println(" Missing keys by locale") + println(" (keys present in English but absent from the locale file)") + println() + withMissing.forEach { r -> + println(" ┌── ${r.locale} (${r.missing.size} missing, ${r.coveragePct.roundToInt()}% covered)") + r.missing.forEach { key -> println(" │ $key") } + println() + } + } + + // Extra keys section (locale has keys not in English) + val withExtra = results.filter { it.extra.isNotEmpty() } + if (withExtra.isNotEmpty()) { + println() + println(" Keys present in locale but NOT in English (stale / untranslated additions)") + println() + withExtra.forEach { r -> + println(" ┌── ${r.locale} (${r.extra.size} extra)") + r.extra.forEach { key -> println(" │ $key") } + println() + } + } + + println() + } + + private fun progressBar(pct: Double, width: Int): String { + val filled = ((pct / 100.0) * width).roundToInt().coerceIn(0, width) + return "█".repeat(filled) + "░".repeat(width - filled) + } + + // ────────────────────────────────────────────────────────────────────── + // Data + // ────────────────────────────────────────────────────────────────────── + + private data class LocaleReport( + val locale: String, + val file: File, + val total: Int, + val present: Int, + val missing: List, + val extra: List, + ) { + val coveragePct: Double get() = if (total == 0) 0.0 else (present.toDouble() / total) * 100.0 + } +} diff --git a/app/src/test/java/com/brainwallet/tools/util/LocaleHelperTest.kt b/app/src/test/java/com/brainwallet/tools/util/LocaleHelperTest.kt index b102b988..b33454a8 100644 --- a/app/src/test/java/com/brainwallet/tools/util/LocaleHelperTest.kt +++ b/app/src/test/java/com/brainwallet/tools/util/LocaleHelperTest.kt @@ -56,7 +56,7 @@ class LocaleHelperTest { fun `check all language codes given the subjective business logic which prioritizes localization popularity`() { Language.entries .also { languages -> - assertEquals(20, languages.size) + assertEquals(22, languages.size) } .map { it.code } .also { @@ -64,8 +64,8 @@ class LocaleHelperTest { listOf( "en", "es", "in", "ar", "uk", "ru", "pt", "hi", "de", "fa", - "pa", "pl", "ko", "fr", "zh-TW", - "tr", "ja", "zh-CN", "it", "sv" + "pa", "pl", "ko", "nl", "th", "fr", + "zh-TW", "tr", "ja", "zh-CN", "it", "sv" ), it ) diff --git a/app/src/test/java/com/brainwallet/ui/screens/send/BWSenderIntegrationTest.kt b/app/src/test/java/com/brainwallet/ui/screens/send/BWSenderIntegrationTest.kt new file mode 100644 index 00000000..d60a7e5c --- /dev/null +++ b/app/src/test/java/com/brainwallet/ui/screens/send/BWSenderIntegrationTest.kt @@ -0,0 +1,260 @@ +package com.brainwallet.ui.screens.send + +import android.content.Context +import com.brainwallet.presenter.entities.TransactionItem +import com.brainwallet.tools.security.PostAuth +import com.brainwallet.util.MainDispatcherRule +import com.brainwallet.wallet.WalletOperations +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.Runs +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +/** + * Integration tests for the send-transaction flow. + * + * BWSender is the primary orchestrator: it validates guard conditions, builds a + * serialized transaction via BRWalletManager, and hands it off to PostAuth for + * broadcast. Tests cover the full happy path and every documented error result. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class BWSenderIntegrationTest { + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private lateinit var context: Context + private lateinit var walletManager: WalletOperations + private lateinit var postAuth: PostAuth + private lateinit var sender: BWSender + + private val validTxBytes = byteArrayOf(0x01, 0x02, 0x03) + private val sendAmount = 100_000L + private val minOutput = 10_000L + + @Before + fun setUp() { + context = mockk(relaxed = true) + walletManager = mockk(relaxed = true) + postAuth = mockk(relaxed = true) + + every { + walletManager.tryTransactionWithOps( + any(), any(), any(), any() + ) + } returns validTxBytes + every { walletManager.getMinOutputAmount() } returns minOutput + every { walletManager.getMinOutputAmountRequested() } returns minOutput + every { postAuth.setTransactionItem(any()) } just Runs + every { postAuth.onPublishTxAuth(any(), any()) } just Runs + + sender = BWSender( + context = context, + getWalletManager = { walletManager }, + getPostAuth = { postAuth }, + ) + } + + @After + fun tearDown() { + unmockkAll() + } + + private fun makeTransactionItem( + address: String? = "LSfkWtX2rzXrTeoFmFozj1mcXodTqg4GdH", + amount: Long = sendAmount, + isAmountRequested: Boolean = false, + ) = TransactionItem( + address, + "ops_address", + null, + amount, + 100L, + null, + isAmountRequested + ) + + // ── success path ────────────────────────────────────────────────────────── + + @Test + fun `prepareTransaction returns Success when all conditions are met`() = runTest { + val result = sender.prepareTransaction(makeTransactionItem()) + + assertEquals(BWSendResult.Success, result) + } + + @Test + fun `prepareTransaction invokes onPublishTxAuth exactly once on success`() = runTest { + sender.prepareTransaction(makeTransactionItem()) + + verify(exactly = 1) { postAuth.onPublishTxAuth(context, false) } + } + + @Test + fun `prepareTransaction resets isSending to false after success`() = runTest { + sender.prepareTransaction(makeTransactionItem()) + + assertFalse(sender.isSending) + } + + // ── guard: already sending ──────────────────────────────────────────────── + + @Test + fun `prepareTransaction returns AlreadySending when isSending is true`() = runTest { + sender.isSending = true + + val result = sender.prepareTransaction(makeTransactionItem()) + + assertEquals(BWSendResult.Error.AlreadySending, result) + } + + @Test + fun `prepareTransaction resets isSending after AlreadySending guard triggers`() = runTest { + sender.isSending = true + sender.prepareTransaction(makeTransactionItem()) + + assertFalse(sender.isSending) + } + + // ── guard: timed out ────────────────────────────────────────────────────── + + @Test + fun `prepareTransaction returns TimedOut when timedOut is true`() = runTest { + sender.timedOut = true + + val result = sender.prepareTransaction(makeTransactionItem()) + + assertEquals(BWSendResult.Error.TimedOut, result) + } + + @Test + fun `prepareTransaction resets timedOut flag after TimedOut result`() = runTest { + sender.timedOut = true + sender.prepareTransaction(makeTransactionItem()) + + assertFalse(sender.timedOut) + } + + // ── malformed transaction item ──────────────────────────────────────────── + + @Test + fun `prepareTransaction returns Unknown when sendAddress is null`() = runTest { + val result = sender.prepareTransaction(makeTransactionItem(address = null)) + + assertTrue(result is BWSendResult.Error.Unknown) + } + + // ── wallet manager failures ─────────────────────────────────────────────── + + @Test + fun `prepareTransaction returns Unknown when tryTransactionWithOps returns null`() = runTest { + every { + walletManager.tryTransactionWithOps( + any(), + any(), + any(), + any() + ) + } returns null + + val result = sender.prepareTransaction(makeTransactionItem()) + + assertTrue(result is BWSendResult.Error.Unknown) + } + + @Test + fun `prepareTransaction returns Unknown when walletManager throws`() = runTest { + every { + walletManager.tryTransactionWithOps( + any(), + any(), + any(), + any() + ) + } throws RuntimeException("native crash") + + val result = sender.prepareTransaction(makeTransactionItem()) + + assertTrue(result is BWSendResult.Error.Unknown) + } + + @Test + fun `prepareTransaction resets isSending after walletManager exception`() = runTest { + every { + walletManager.tryTransactionWithOps( + any(), + any(), + any(), + any() + ) + } throws RuntimeException("crash") + + sender.prepareTransaction(makeTransactionItem()) + + assertFalse(sender.isSending) + } + + // ── amount too small ────────────────────────────────────────────────────── + + @Test + fun `prepareTransaction returns AmountTooSmall when sendAmount is below minOutput`() = runTest { + every { walletManager.getMinOutputAmount() } returns 500_000L + + val result = sender.prepareTransaction(makeTransactionItem(amount = 1_000L)) + + assertTrue(result is BWSendResult.Error.AmountTooSmall) + assertEquals(500_000L, (result as BWSendResult.Error.AmountTooSmall).minAmount) + } + + @Test + fun `prepareTransaction uses getMinOutputAmountRequested when isAmountRequested is true`() = runTest { + every { walletManager.getMinOutputAmountRequested() } returns 200_000L + every { walletManager.getMinOutputAmount() } returns 50_000L + + val result = sender.prepareTransaction( + makeTransactionItem(amount = 100_000L, isAmountRequested = true) + ) + + assertTrue(result is BWSendResult.Error.AmountTooSmall) + assertEquals( + 200_000L, + (result as BWSendResult.Error.AmountTooSmall).minAmount + ) + } + + @Test + fun `prepareTransaction uses getMinOutputAmount when isAmountRequested is false`() = runTest { + every { walletManager.getMinOutputAmount() } returns 150_000L + every { walletManager.getMinOutputAmountRequested() } returns 50_000L + + val result = sender.prepareTransaction( + makeTransactionItem(amount = 100_000L, isAmountRequested = false) + ) + + assertTrue(result is BWSendResult.Error.AmountTooSmall) + assertEquals( + 150_000L, + (result as BWSendResult.Error.AmountTooSmall).minAmount + ) + } + + @Test + fun `prepareTransaction succeeds when sendAmount equals minOutput exactly`() = runTest { + every { walletManager.getMinOutputAmount() } returns sendAmount + + val result = sender.prepareTransaction(makeTransactionItem(amount = sendAmount)) + + assertEquals(BWSendResult.Success, result) + } +} diff --git a/app/src/test/java/com/brainwallet/ui/screens/send/BWSenderTest.kt b/app/src/test/java/com/brainwallet/ui/screens/send/BWSenderTest.kt new file mode 100644 index 00000000..a43a0e1e --- /dev/null +++ b/app/src/test/java/com/brainwallet/ui/screens/send/BWSenderTest.kt @@ -0,0 +1,233 @@ +package com.brainwallet.ui.screens.send + +import android.content.Context +import com.brainwallet.presenter.entities.TransactionItem +import com.brainwallet.tools.security.PostAuth +import com.brainwallet.wallet.FakeWalletManager +import com.brainwallet.wallet.WalletOperations +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * Unit tests for [BWSender]. These avoid JNI entirely by injecting a mock + * [WalletOperations] and a mock [PostAuth] through the constructor. + * + * The critical production failure mode this suite guards against: + * - `isSending` being left true after a failure, permanently locking Send + * - `timedOut` never being reset on the finally path + * - a null return from tryTransactionWithOps crashing the coroutine + */ +@OptIn(ExperimentalCoroutinesApi::class) +class BWSenderTest { + + private val testDispatcher = StandardTestDispatcher() + + private lateinit var context: Context + private lateinit var walletManager: WalletOperations + private lateinit var postAuth: PostAuth + private lateinit var sender: BWSender + + private val validSerializedTx = ByteArray(32) { (it + 1).toByte() } + + private fun buildTx( + sendAddress: String? = "LValidAddr", + sendAmount: Long = 100_000L, + opsAddress: String? = "LOpsAddr", + opsFee: Long = 500L, + isAmountRequested: Boolean = false + ): TransactionItem = TransactionItem( + sendAddress, + opsAddress, + null, + sendAmount, + opsFee, + null, + isAmountRequested, + "" + ) + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + context = mockk(relaxed = true) + val fakeWallet = FakeWalletManager(created = true) + walletManager = mockk(relaxed = true) { + every { + tryTransactionWithOps(any(), any(), any(), any()) + } returns validSerializedTx + every { getMinOutputAmount() } returns 1_000L + every { getMinOutputAmountRequested() } returns 500L + } + postAuth = mockk(relaxed = true) + + sender = BWSender( + context = context, + getWalletManager = { walletManager }, + getPostAuth = { postAuth } + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + // ------------------------------------------------------------------------- + // Happy path + // ------------------------------------------------------------------------- + + @Test + fun `prepareTransaction returns Success and calls PostAuth`() = runTest { + val result = sender.prepareTransaction(buildTx()) + + assertEquals(BWSendResult.Success, result) + verify(exactly = 1) { postAuth.setTransactionItem(any()) } + verify(exactly = 1) { postAuth.onPublishTxAuth(context, false) } + } + + @Test + fun `prepareTransaction mutates serializedTx on the item`() = runTest { + val tx = buildTx() + sender.prepareTransaction(tx) + assertTrue( + "BWSender must populate serializedTx before calling PostAuth", + tx.serializedTx?.contentEquals(validSerializedTx) == true + ) + } + + // ------------------------------------------------------------------------- + // REGRESSION: isSending must always be reset in finally + // ------------------------------------------------------------------------- + + @Test + fun `isSending is cleared after successful send`() = runTest { + sender.prepareTransaction(buildTx()) + advanceUntilIdle() + assertFalse( + "isSending must be cleared or subsequent sends are permanently blocked", + sender.isSending + ) + } + + @Test + fun `isSending is cleared after null transaction returns Unknown error`() = runTest { + every { + walletManager.tryTransactionWithOps(any(), any(), any(), any()) + } returns null + + val result = sender.prepareTransaction(buildTx()) + + assertTrue(result is BWSendResult.Error.Unknown) + assertFalse(sender.isSending) + } + + @Test + fun `isSending is cleared after native throws`() = runTest { + every { + walletManager.tryTransactionWithOps(any(), any(), any(), any()) + } throws RuntimeException("native boom") + + val result = sender.prepareTransaction(buildTx()) + + assertTrue(result is BWSendResult.Error.Unknown) + assertFalse("finally block must clear isSending on exception", sender.isSending) + } + + @Test + fun `timedOut flag is cleared in finally`() = runTest { + sender.timedOut = true + // Next attempt should short-circuit to TimedOut but still reset the flag. + val result = sender.prepareTransaction(buildTx()) + + assertEquals(BWSendResult.Error.TimedOut, result) + assertFalse("timedOut must not leak into subsequent attempts", sender.timedOut) + + // And a follow-up send should now succeed. + val second = sender.prepareTransaction(buildTx()) + assertEquals(BWSendResult.Success, second) + } + + // ------------------------------------------------------------------------- + // REGRESSION: AlreadySending reentrancy guard + // ------------------------------------------------------------------------- + + @Test + fun `second concurrent prepareTransaction returns AlreadySending`() = runTest { + // Block the first call inside tryTransactionWithOps by making it slow. + every { + walletManager.tryTransactionWithOps( + any(), + any(), + any(), + any() + ) + } answers { + Thread.sleep(200) + validSerializedTx + } + + val first = async { sender.prepareTransaction(buildTx()) } + // Give the first call a chance to flip isSending = true. + advanceUntilIdle() + val second = sender.prepareTransaction(buildTx()) + + // Second call must be rejected without touching the wallet again. + assertEquals(BWSendResult.Error.AlreadySending, second) + first.await() + } + + // ------------------------------------------------------------------------- + // Validation of inputs + // ------------------------------------------------------------------------- + + @Test + fun `null sendAddress returns Unknown error`() = runTest { + val result = sender.prepareTransaction(buildTx(sendAddress = null)) + assertTrue(result is BWSendResult.Error.Unknown) + verify(exactly = 0) { postAuth.setTransactionItem(any()) } + } + + @Test + fun `amount below minOutput returns AmountTooSmall`() = runTest { + every { walletManager.getMinOutputAmount() } returns 10_000L + + val result = sender.prepareTransaction(buildTx(sendAmount = 500L)) + + assertTrue(result is BWSendResult.Error.AmountTooSmall) + assertEquals(10_000L, (result as BWSendResult.Error.AmountTooSmall).minAmount) + coVerify(exactly = 0) { postAuth.onPublishTxAuth(any(), any()) } + } + + @Test + fun `isAmountRequested uses minOutputAmountRequested`() = runTest { + every { walletManager.getMinOutputAmount() } returns 10_000L + every { walletManager.getMinOutputAmountRequested() } returns 100L + + // 500 is below the regular minOutput but above the requested one. + val result = sender.prepareTransaction( + buildTx(sendAmount = 500L, isAmountRequested = true) + ) + + assertEquals(BWSendResult.Success, result) + verify(exactly = 1) { walletManager.getMinOutputAmountRequested() } + verify(exactly = 0) { walletManager.getMinOutputAmount() } + } +} diff --git a/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt b/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt new file mode 100644 index 00000000..25e0eda1 --- /dev/null +++ b/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt @@ -0,0 +1,481 @@ +package com.brainwallet.ui.screens.send + +import android.app.Application +import com.brainwallet.data.model.AppSetting +import com.brainwallet.data.model.CurrencyEntity +import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.data.repository.TxRepository +import com.brainwallet.presenter.entities.TransactionItem +import com.brainwallet.tools.manager.AnalyticsManager +import com.brainwallet.tools.security.BRKeyStore +import com.brainwallet.tools.util.Utils +import com.brainwallet.util.EventBus +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.math.BigDecimal + +/** + * Regression tests for the Send flow. These cover the state transitions that, + * if broken, would leave the Send button disabled or cause a crash before a + * transaction could be submitted — matching the production incident. + * + * JNI-backed methods (BRWalletManager.validateAddress, getBalance, FeeManager, + * Utils.tieredOpsFee) are injected as lambdas so these tests never trigger the + * native linker. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class SendViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + private lateinit var app: Application + private lateinit var bwSender: BWSender + private lateinit var txRepository: TxRepository + private lateinit var settingRepository: SettingRepository + + private val settingsFlow = MutableStateFlow( + AppSetting( + isDarkMode = false, + currency = CurrencyEntity("USD", "US Dollar", 100f, "$") + ) + ) + + /** + * Build a ViewModel with sensible test defaults. Individual tests override + * whatever they need to exercise specific branches. + */ + + private fun TestScope.buildViewModel( + getBalance: () -> Long = { 1_000_000_000L }, // 10 LTC in litoshis + validateAddress: (String) -> Boolean = { it.startsWith("L") }, + getCurrentFee: () -> Long = { 2_000L }, + getOpsFee: (Long) -> Long = { 500L }, + isWalletCreated: () -> Boolean = { true } + ): SendViewModel = SendViewModel( + app = app, + bwSender = bwSender, + txRepository = txRepository, + settingRepository = settingRepository, + isWalletCreated = isWalletCreated, + validateAddress = validateAddress, + getBalance = getBalance, + getCurrentFee = getCurrentFee, + getOpsFee = getOpsFee, + ).also { advanceUntilIdle() } + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + + app = mockk(relaxed = true) { + every { resources.getString(any()) } returns "error" + every { getString(any()) } returns "error" + } + bwSender = mockk(relaxed = true) + txRepository = mockk(relaxed = true) + settingRepository = mockk(relaxed = true) { + every { currentSettings } returns settingsFlow + } + + mockkStatic(BRKeyStore::class) + every { BRKeyStore.getPinCode(any()) } returns "1234" + + // Stub the asset/Firebase boundary that OnSend hits. + mockkStatic(Utils::class) + every { Utils.fetchServiceItem(any(), any()) } returns "LOpsAddr" + every { Utils.tieredOpsFee(any(), any()) } returns 500L + + mockkStatic(AnalyticsManager::class) + every { AnalyticsManager.logCustomEvent(any()) } returns Unit + every { AnalyticsManager.logCustomEventWithParams(any(), any()) } + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkAll() + } + + // ------------------------------------------------------------------------- + // Initial state & settings propagation + // ------------------------------------------------------------------------- + + @Test + fun `initial state is not ready to send`() = runTest { + val vm = buildViewModel() + val s = vm.state.value + assertFalse("fresh VM must not have Send enabled", s.isReadyToSend) + assertFalse(s.isLTCAddressValid) + assertFalse(s.isAmountBelowBalance) + assertFalse(s.brainwalletIsPublishing) + } + + @Test + fun `settings update propagates currency and darkMode`() = runTest { + val vm = buildViewModel() + val newCurrency = CurrencyEntity("GBP", "Pound", 80f, "£") + + settingsFlow.value = AppSetting(isDarkMode = true, currency = newCurrency) + advanceUntilIdle() + + assertTrue(vm.state.value.darkMode) + assertEquals("GBP", vm.state.value.selectedCurrency.code) + } + + // ------------------------------------------------------------------------- + // REGRESSION: OnToggleFiatOrLTC null / negative rate handling + // ------------------------------------------------------------------------- + + @Test + fun `toggle does not crash when rate is negative sentinel`() = runTest { + // The default CurrencyEntity in SendState has rate = -1f. If the user + // toggles before rates load, the VM must not crash. + settingsFlow.value = AppSetting( + currency = CurrencyEntity("USD", "US Dollar", -1f, "$") + ) + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnAmountChanged("0.5")) + advanceUntilIdle() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) + advanceUntilIdle() + + // Expectation: no crash. The amount string should either stay put or + // clear — but the VM must remain functional. + assertFalse(vm.state.value.brainwalletIsPublishing) + } + + @Test + fun `toggle LTC to fiat multiplies by rate with 2dp rounding`() = runTest { + settingsFlow.value = AppSetting( + currency = CurrencyEntity("USD", "US Dollar", 100f, "$") + ) + val vm = buildViewModel() + // Start in LTC view, enter 0.5 LTC. + vm.onEvent(SendEvent.OnAmountChanged("0.5")) + advanceUntilIdle() + + vm.onEvent(SendEvent.OnToggleFiatOrLTC) + advanceUntilIdle() + + assertTrue("should now view fiat", vm.state.value.userViewsFiat) + // 0.5 * 100 = 50.00 + assertEquals(BigDecimal("50.00"), BigDecimal(vm.state.value.amountString)) + } + + @Test + fun `toggle fiat to LTC divides by rate with 8dp precision`() = runTest { + settingsFlow.value = AppSetting( + currency = CurrencyEntity("USD", "US Dollar", 100f, "$") + ) + val vm = buildViewModel() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat view + vm.onEvent(SendEvent.OnAmountChanged("50")) + advanceUntilIdle() + + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → back to LTC + advanceUntilIdle() + + assertFalse(vm.state.value.userViewsFiat) + // 50 / 100 = 0.50000000 + assertEquals(BigDecimal("0.50000000"), BigDecimal(vm.state.value.amountString)) + } + + // ------------------------------------------------------------------------- + // REGRESSION: Amount validation drives isReadyToSend + // ------------------------------------------------------------------------- + + @Test + fun `valid address alone does not enable send`() = runTest { + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) + advanceUntilIdle() + + assertTrue(vm.state.value.isLTCAddressValid) + assertFalse( + "Send must stay disabled until an amount is entered too", + vm.state.value.isReadyToSend + ) + } + + @Test + fun `valid address plus valid amount enables send`() = runTest { + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) + vm.onEvent(SendEvent.OnAmountChanged("0.1")) + advanceUntilIdle() + + // Note: this asserts the intended contract. It will currently fail + // unless the getBalance/getCurrentFee/getOpsFee lambdas are wired into + // the ViewModel — which is exactly the point of this regression test. + assertTrue(vm.state.value.isReadyToSend) + } + + @Test + fun `amount exceeding balance disables send`() = runTest { + val vm = buildViewModel(getBalance = { 1000L }) // 1000 litoshis ~ tiny + + vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) + vm.onEvent(SendEvent.OnAmountChanged("100")) // 100 LTC — way over + advanceUntilIdle() + + assertFalse(vm.state.value.isAmountBelowBalance) + assertFalse(vm.state.value.isReadyToSend) + } + + @Test + fun `invalid address disables send even with valid amount`() = runTest { + val vm = buildViewModel(validateAddress = { false }) + + vm.onEvent(SendEvent.OnRecipientAddressChanged("not-a-real-addr")) + vm.onEvent(SendEvent.OnAmountChanged("0.1")) + advanceUntilIdle() + + assertFalse(vm.state.value.isLTCAddressValid) + assertFalse(vm.state.value.isReadyToSend) + } + + @Test + fun `zero amount does not enable send`() = runTest { + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) + vm.onEvent(SendEvent.OnAmountChanged("0")) + advanceUntilIdle() + + assertFalse( + "Zero-amount transactions must never be sendable", + vm.state.value.isReadyToSend + ) + } + + @Test + fun `empty amount string does not enable send`() = runTest { + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) + vm.onEvent(SendEvent.OnAmountChanged("")) + advanceUntilIdle() + + assertFalse(vm.state.value.isReadyToSend) + } + + // ------------------------------------------------------------------------- + // REGRESSION: Send button lock-up (isSending never clears) + // ------------------------------------------------------------------------- + + @Test + fun `send success clears publishing flag`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Success + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + assertFalse( + "brainwalletIsPublishing must be cleared after success", + vm.state.value.brainwalletIsPublishing + ) + } + + @Test + fun `send AlreadySending clears publishing flag`() = runTest { + // This is the core regression: if AlreadySending leaves the VM in + // brainwalletIsPublishing = true, the Send button never re-enables + // and the user is permanently blocked. + coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Error.AlreadySending + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + assertFalse(vm.state.value.brainwalletIsPublishing) + } + + @Test + fun `send TimedOut clears publishing flag and surfaces error`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Error.TimedOut + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + assertFalse(vm.state.value.brainwalletIsPublishing) + assertTrue(vm.state.value.errorResultString.isNotEmpty()) + } + + @Test + fun `send Unknown error clears publishing flag and surfaces error`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns + BWSendResult.Error.Unknown(RuntimeException("boom")) + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + assertFalse(vm.state.value.brainwalletIsPublishing) + assertTrue(vm.state.value.errorResultString.isNotEmpty()) + } + + @Test + fun `send AmountTooSmall surfaces error and clears publishing`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns + BWSendResult.Error.AmountTooSmall(minAmount = 10_000L) + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + assertFalse(vm.state.value.brainwalletIsPublishing) + assertTrue(vm.state.value.errorResultString.isNotEmpty()) + } + + @Test + fun `onSend calls bwSender exactly once`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Success + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + coVerify(exactly = 1) { bwSender.prepareTransaction(any()) } + } + + // ------------------------------------------------------------------------- + // Passcode flow + // ------------------------------------------------------------------------- + + @Test + fun `correct passcode triggers send`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Success + every { BRKeyStore.getPinCode(any()) } returns "1234" + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnPasscodeDigitAdded(1)) + vm.onEvent(SendEvent.OnPasscodeDigitAdded(2)) + vm.onEvent(SendEvent.OnPasscodeDigitAdded(3)) + vm.onEvent(SendEvent.OnPasscodeDigitAdded(4)) + advanceUntilIdle() + + coVerify(exactly = 1) { bwSender.prepareTransaction(any()) } + } + + @Test + fun `incorrect passcode does not trigger send and flags auth failure`() = runTest { + every { BRKeyStore.getPinCode(any()) } returns "9999" + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnPasscodeDigitAdded(1)) + vm.onEvent(SendEvent.OnPasscodeDigitAdded(2)) + vm.onEvent(SendEvent.OnPasscodeDigitAdded(3)) + vm.onEvent(SendEvent.OnPasscodeDigitAdded(4)) + advanceUntilIdle() + + assertFalse(vm.state.value.isPasscodeAuthenticated) + coVerify(exactly = 0) { bwSender.prepareTransaction(any()) } + } + + @Test + fun `passcode caps at four digits`() = runTest { + val vm = buildViewModel() + repeat(6) { vm.onEvent(SendEvent.OnPasscodeDigitAdded(1)) } + advanceUntilIdle() + + assertEquals(4, vm.state.value.passcode.size) + } + + @Test + fun `passcode delete removes last digit`() = runTest { + val vm = buildViewModel() + vm.onEvent(SendEvent.OnPasscodeDigitAdded(1)) + vm.onEvent(SendEvent.OnPasscodeDigitAdded(2)) + vm.onEvent(SendEvent.OnPasscodeDigitDeleted) + advanceUntilIdle() + + assertEquals(listOf(1), vm.state.value.passcode) + } + + // ------------------------------------------------------------------------- + // QR scan / EventBus wiring + // ------------------------------------------------------------------------- + + @Test + fun `QR scan event populates recipient address`() = runTest { + val vm = buildViewModel() + + EventBus.emit(EventBus.Event.QRCodeScanned(url = "LQRScannedAddr")) + advanceUntilIdle() + + assertEquals("LQRScannedAddr", vm.state.value.recipientLTCAddress) + } + + @Test + fun `QR scan with null url sets empty string not crash`() = runTest { + val vm = buildViewModel() + + EventBus.emit(EventBus.Event.QRCodeScanned(url = null)) + advanceUntilIdle() + + assertEquals("", vm.state.value.recipientLTCAddress) + } + + // ------------------------------------------------------------------------- + // Memo / Edit + // ------------------------------------------------------------------------- + + @Test + fun `memo update stores value`() = runTest { + val vm = buildViewModel() + vm.onEvent(SendEvent.OnUserMemorandumChanged("coffee")) + advanceUntilIdle() + + assertEquals("coffee", vm.state.value.userMemorandum) + } + + @Test + fun `edit send clears amount`() = runTest { + val vm = buildViewModel() + vm.onEvent(SendEvent.OnAmountChanged("0.1")) + advanceUntilIdle() + vm.onEvent(SendEvent.OnEditSend) + advanceUntilIdle() + + assertEquals("", vm.state.value.amountString) + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private fun dummyTransactionItem() = TransactionItem( + "LValidAddr", + "LOpsAddr", + null, + 100_000L, + 500L, + null, + false, + "" + ) +} diff --git a/app/src/test/java/com/brainwallet/wallet/FakeWalletManager.kt b/app/src/test/java/com/brainwallet/wallet/FakeWalletManager.kt new file mode 100644 index 00000000..975f94e0 --- /dev/null +++ b/app/src/test/java/com/brainwallet/wallet/FakeWalletManager.kt @@ -0,0 +1,9 @@ +package com.brainwallet.wallet + +class FakeWalletManager( + private val created: Boolean = true, + private val addressValid: Boolean = true +) : WalletManager { + override fun isCreated() = created + override fun validateAddress(address: String) = addressValid +} diff --git a/app/src/test/kotlin/com/brainwallet/ui/bentosections/gamehubbento/FallinSceneTest.kt b/app/src/test/kotlin/com/brainwallet/ui/bentosections/gamehubbento/FallinSceneTest.kt new file mode 100644 index 00000000..62a411b8 --- /dev/null +++ b/app/src/test/kotlin/com/brainwallet/ui/bentosections/gamehubbento/FallinSceneTest.kt @@ -0,0 +1,179 @@ +package com.brainwallet.ui.bentosections.gamehubbento + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class FallinSceneTest { + + // ── helpers ────────────────────────────────────────────────────────────── + + private fun makeDot( + emoji: String = "😂", + x: Float = 0.5f, + y: Float = 0.5f, + velX: Float = 0f, + velY: Float = 0f + ) = EmojiDot(emoji, x, y, velX, velY) + + private fun stepPhysics( + dot: EmojiDot, + delta: Float, + gravity: Float = 1000f, + restitution: Float = 0.8f, + friction: Float = 0.04f + ): EmojiDot { + var newVelY = dot.velY + gravity * delta + var newVelX = dot.velX * (1f - friction) + var newX = dot.x + newVelX * delta / 1000f + var newY = dot.y + newVelY * delta / 1000f + + if (newY > 1f) { + newY = 1f + newVelY = -newVelY * restitution + } + if (newY < 0f) { + newY = 0f + newVelY = -newVelY * restitution + } + if (newX > 1f) { + newX = 1f + newVelX = -newVelX * restitution + } + if (newX < 0f) { + newX = 0f + newVelX = -newVelX * restitution + } + + return dot.copy(x = newX, y = newY, velX = newVelX, velY = newVelY) + } + + // ── EmojiDot data class ────────────────────────────────────────────────── + + @Test + fun `EmojiDot copy preserves all fields`() { + val dot = makeDot(emoji = "🔥", x = 0.1f, y = 0.2f, velX = 10f, velY = -5f) + val copy = dot.copy(velY = 99f) + assertEquals("🔥", copy.emoji) + assertEquals(0.1f, copy.x, 0.001f) + assertEquals(0.2f, copy.y, 0.001f) + assertEquals(10f, copy.velX, 0.001f) + assertEquals(99f, copy.velY, 0.001f) + } + + @Test + fun `EmojiDot equality works correctly`() { + val a = makeDot(x = 0.3f, y = 0.4f) + val b = makeDot(x = 0.3f, y = 0.4f) + assertEquals(a, b) + } + + // ── gravity ────────────────────────────────────────────────────────────── + + @Test + fun `gravity increases velY each frame`() { + val dot = makeDot(velY = 0f) + val stepped = stepPhysics(dot, delta = 0.016f) + assertTrue("velY should increase due to gravity", stepped.velY > dot.velY) + } + + @Test + fun `gravity accumulates over multiple frames`() { + var dot = makeDot(velY = 0f) + repeat(10) { dot = stepPhysics(dot, delta = 0.016f) } + assertTrue("velY should be significantly positive after 10 frames", dot.velY > 0.1f) + } + + @Test + fun `dot falls downward with zero initial velocity`() { + val dot = makeDot(y = 0.5f, velY = 0f) + val stepped = stepPhysics(dot, delta = 0.016f) + assertTrue("y should increase (fall down)", stepped.y > dot.y) + } + + // ── floor bounce ───────────────────────────────────────────────────────── + + @Test + fun `dot stays within bounds after many frames`() { + var dot = makeDot(x = 0.5f, y = 0.5f, velX = 300f, velY = -200f) + repeat(100) { dot = stepPhysics(dot, delta = 0.016f) } + assertTrue("x should be within 0..1", dot.x in 0f..1f) + assertTrue("y should be within 0..1", dot.y in 0f..1f) + } + + // ── friction ───────────────────────────────────────────────────────────── + + @Test + fun `friction reduces velX each frame`() { + val dot = makeDot(velX = 100f) + val stepped = stepPhysics(dot, delta = 0.016f, friction = 0.04f) + assertTrue("velX should decrease due to friction", stepped.velX < dot.velX) + } + + @Test + fun `friction does not reverse velX direction`() { + val dot = makeDot(velX = 10f) + val stepped = stepPhysics(dot, delta = 0.016f, friction = 0.04f) + assertTrue("velX should still be positive", stepped.velX > 0f) + } + + @Test + fun `friction accumulates to near zero over many frames`() { + var dot = makeDot(velX = 100f, velY = 0f, y = 1f) + // pin to floor to isolate friction + repeat(500) { + dot = stepPhysics(dot, delta = 0.016f, gravity = 0f, friction = 0.04f) + } + assertTrue("velX should approach zero after many frames", kotlin.math.abs(dot.velX) < 1f) + } + + // ── spawn logic ────────────────────────────────────────────────────────── + + @Test + fun `emojiList is not empty`() { + assertTrue(emojiList.isNotEmpty()) + } + + @Test + fun `emojiList contains expected emojis`() { + assertTrue(emojiList.contains("😂")) + assertTrue(emojiList.contains("🔥")) + assertTrue(emojiList.contains("❤️")) + } + + @Test + fun `emojiList has no duplicates`() { + assertEquals(emojiList.size, emojiList.toSet().size) + } + + @Test + fun `new dot starts with normalized coordinates`() { + val dot = makeDot(x = kotlin.random.Random.nextFloat(), y = kotlin.random.Random.nextFloat()) + assertTrue("x should be in 0..1", dot.x in 0f..1f) + assertTrue("y should be in 0..1", dot.y in 0f..1f) + } + + // ── physics values ─────────────────────────────────────────────────────── + + @Test + fun `restitution of 1 preserves speed after bounce`() { + val dot = makeDot(y = 0.99f, velY = 100f) + val stepped = stepPhysics(dot, delta = 0.016f, gravity = 0f, restitution = 1f) + assertEquals(100f, kotlin.math.abs(stepped.velY), 1f) + } + + @Test + fun `zero gravity means no vertical acceleration`() { + val dot = makeDot(velY = 0f) + val stepped = stepPhysics(dot, delta = 0.016f, gravity = 0f) + assertEquals(0f, stepped.velY, 0.001f) + } + + @Test + fun `zero delta means no position change`() { + val dot = makeDot(x = 0.5f, y = 0.5f, velX = 100f, velY = 100f) + val stepped = stepPhysics(dot, delta = 0f) + assertEquals(dot.x, stepped.x, 0.001f) + assertEquals(dot.y, stepped.y, 0.001f) + } +} diff --git a/config/detekt-app-baseline.xml b/config/detekt-app-baseline.xml index aee518e6..96fed956 100644 --- a/config/detekt-app-baseline.xml +++ b/config/detekt-app-baseline.xml @@ -371,7 +371,6 @@ Indentation:AppModule.kt$AppModule$ Indentation:CurrencyDetail.kt$ Indentation:FiatSelectorBottomSheet.kt$ - Indentation:FragmentMoonpay.kt$FragmentMoonpay$ Indentation:FragmentSend.kt$FragmentSend$ Indentation:HomeSettingDrawerSheet.kt$ Indentation:MoonPayWidgeLauncher.kt$MoonPayWidgetLauncherViewModel$ @@ -530,7 +529,6 @@ NoSemicolons:BRConstantsTest.kt$BRConstantsTest$; NoSemicolons:CountryLanguageAdapter.kt$CountryLanguageAdapter$; NoSemicolons:FragmentBalanceSeedReminder.kt$FragmentBalanceSeedReminder$; - NoSemicolons:FragmentMoonpay.kt$FragmentMoonpay$; NoSemicolons:FragmentSend.kt$FragmentSend$; NoSemicolons:HistoryFragment.kt$HistoryFragment$; NoSemicolons:TransactionDataItem.kt$TransactionDataItem$; @@ -544,7 +542,6 @@ NoUnusedImports:FiatSelectorBottomSheet.kt$com.brainwallet.ui.composable.bottomsheet.FiatSelectorBottomSheet.kt NoUnusedImports:Font.kt$com.brainwallet.ui.theme.Font.kt NoUnusedImports:FragmentBalanceSeedReminder.kt$com.brainwallet.presenter.fragments.FragmentBalanceSeedReminder.kt - NoUnusedImports:FragmentMoonpay.kt$com.brainwallet.presenter.fragments.FragmentMoonpay.kt NoUnusedImports:GoogleServicesTests.kt$com.brainwallet.tools.util.GoogleServicesTests.kt NoUnusedImports:LegacyNavigation.kt$com.brainwallet.navigation.LegacyNavigation.kt NoUnusedImports:LitecoinBlockchainDetail.kt$com.brainwallet.ui.screens.main.composable.settingsrows.LitecoinBlockchainDetail.kt @@ -560,7 +557,6 @@ NoUnusedImports:UnLockScreenBody.kt$com.brainwallet.ui.screens.unlock.components.UnLockScreenBody.kt NoUnusedImports:WelcomeScreen.kt$com.brainwallet.ui.screens.welcome.WelcomeScreen.kt NoWildcardImports:FragmentBalanceSeedReminder.kt$import android.widget.* - ParameterListWrapping:FragmentMoonpay.kt$FragmentMoonpay$( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ) ParameterListWrapping:HomeSettingDrawerSheet.kt$HomeSettingDrawerComposeView$( context: Context, attrs: AttributeSet? = null ) ParameterListWrapping:KeyStoreKeyGeneratorImpl.kt$KeyStoreKeyGeneratorImpl$( builder: KeyGenParameterSpec.Builder, authTimeout: Int? ) ParameterListWrapping:SeedWordItem.kt$( modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7bd6feb..dd78680f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ espresso-core = "3.7.0" runtime = "1.10.6" foundation = "1.10.6" animation = "1.10.6" +play-services-games = "24.0.0" [libraries] androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } @@ -105,6 +106,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co androidx-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } androidx-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" } +play-services-games = { group = "com.google.android.gms", name = "play-services-games", version.ref = "play-services-games" } [bundles] androidx-lifecycle = ["androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", "androidx-lifecycle-viewmodel-compose"] diff --git a/scripts/translate_strings.py b/scripts/translate_strings.py index d716c569..1c555798 100755 --- a/scripts/translate_strings.py +++ b/scripts/translate_strings.py @@ -5,6 +5,9 @@ compares them to the source (values/strings.xml), translates any missing strings via the Anthropic API, and writes the updated files back to disk. Also writes /tmp/pr_body.md summarising what was changed for the PR. + +Updates: + - Added formatting update """ import os @@ -25,24 +28,26 @@ # Map Android locale folder suffix → human language name for the prompt LOCALE_MAP = { "values-ar": "Arabic", - "values-zh-rTW": "Traditional Chinese (Taiwan)", - "values-zh-rCN": "Simplified Chinese (Mainland China)", - "values-fr": "French", "values-de": "German", + "values-es": "Spanish", "values-fa": "Farsi (Persian)", - "values-pa": "Punjabi", - "values-pl": "Polish", + "values-fr": "French", + "values-hi": "Hindi", "values-in": "Indonesian", - "values-es": "Spanish", - "values-sv": "Swedish", - "values-uk": "Ukrainian", - "values-ru": "Russian", - "values-tr": "Turkish", + "values-it": "Italian", "values-ja": "Japanese", "values-ko": "Korean", - "values-hi": "Hindi", - "values-it": "Italian", + "values-nl": "Dutch", + "values-pa": "Punjabi", + "values-pl": "Polish", "values-pt-rBR": "Brazilian Portuguese", + "values-ru": "Russian", + "values-sv": "Swedish", + "values-th": "Thai", + "values-tr": "Turkish", + "values-uk": "Ukrainian", + "values-zh-rCN": "Simplified Chinese (Mainland China)", + "values-zh-rTW": "Traditional Chinese (Taiwan)", } # RTL languages that need android:textDirection="rtl" awareness (informational) @@ -79,15 +84,37 @@ def load_or_create_tree(path: Path): return etree.ElementTree(root) +def normalise_indentation(tree): + """ + Ensure every child element of sits on its own line with + 4-space indentation. This fixes both existing elements that lack + tail whitespace and newly-appended elements added by set_translated(). + """ + root = tree.getroot() + # Root's own text (before the first child) should be a newline + indent + root.text = "\n " + children = list(root) + for i, child in enumerate(children): + # Each element's tail is the whitespace that follows its closing tag. + # For all but the last child that means newline + indent; for the last + # child it means newline only (so sits on its own line). + if i < len(children) - 1: + child.tail = "\n " + else: + child.tail = "\n" + + def write_tree(tree, path: Path): + normalise_indentation(tree) path.parent.mkdir(parents=True, exist_ok=True) tree.write( str(path), encoding="utf-8", xml_declaration=True, - pretty_print=True, + pretty_print=False, # we handle whitespace ourselves ) + def set_translated(tree, name: str, translated_text: str): """Insert or update a element in the tree.""" root = tree.getroot() @@ -98,6 +125,7 @@ def set_translated(tree, name: str, translated_text: str): el = etree.SubElement(root, "string") el.set("name", name) el.text = translated_text + # tail is set later in normalise_indentation(); no need to set it here # ── Translation via Anthropic ─────────────────────────────────────────────────