diff --git a/README.md b/README.md index a6ba2f12..4980363f 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,33 @@ For the full, up-to-date changelog see [GitHub Releases](https://github.com/grun --- +### **v4.12.0** [PR [#270](https://github.com/gruntsoftware/android/pull/270)] +--- +#### โœจ In-App Review Activated +Google Play's in-app review prompt is now actually wired up after sitting dormant โ€” `InAppReviewService` is registered in Koin and `showInAppReviewDialogIfNeeded()` is called from five placements: after a successful send, the game hub exit flow, the balance-visibility toggle, and two tutorial pages (#260). A follow-up fix decouples the prompt from the social-share flow so it fires on every game exit, not just when the player taps Twitter/Instagram share (#263). + +#### ๐Ÿ› Bug Fixes +- **Native `SIGSEGV` crash in `BRPeerManager`** โ€” several accessors (`BRPeerManagerEstimatedBlockHeight`, `LastBlockHeight`, `LastBlockTimestamp`, `SyncProgress`) dereferenced `lastBlock` without a null check, crashing the process if a checkpoint block was ever missing from the persisted block set. `core`'s `BRPeerManager.c` now guards every access and reports the recovery back to the app via a new `BRPeerManagerSetIntegrityWarningCallback`, surfaced through `BRPeerManager.onIntegrityWarning()` to Crashlytics instead of crashing (#269). +- **RSA public key parsing in `getEncryptedAgentString`** โ€” the provisioned key is an OpenSSH `ssh-rsa ` line, not base64-encoded PEM; the old PEM-stripping logic corrupted the payload by merging the `ssh-rsa` prefix and comment into it, failing on every call. Now parses the OpenSSH wire format (RFC 4253) directly (#265). +- **MoonPay signed-url `ipAddress` race** โ€” the Buy button in `ReceiveDialog` could reach MoonPay's signed-url request before the async ipify lookup resolved, sending an empty `ipAddress`. The lookup now happens fresh inside `fetchMoonpaySignedUrl` itself, awaited right before use (#264). + +#### ๐ŸŽฎ Fallinmoji v1.6.2 +"How To Play" screen update and a bento corner-radius fix, plus a bw-gdlib bump to pick it up (#268). + +#### ๐Ÿ“Š Analytics Cleanup +Removed excess/duplicate events (`app_launched`, `home_open`, a double-firing `did_skip_top_up`) and stopped logging `did_request_rating`/`user_completed_rating` as Firebase events โ€” the Play Core API gives no guarantee a review request actually shows the dialog, so these overstated what happened. Replaced with local `Timber.d` traces (#266). + +#### ๐Ÿงช Test Coverage +Added coverage for `InAppReviewService` (gating + Play Core success/failure branches), `UtilsAgentString`'s new OpenSSH parsing path, MoonPay ip-fetch success/failure, and `BRPeerManager.onIntegrityWarning`'s Crashlytics reporting. + +#### ๐Ÿ”ง Chores +- Bumped `targetSdk` to 36 (#267) +- Version bumped: **v4.11.0 (202506346) โ†’ v4.12.0 (202506350)** + +**Full Changelog**: https://github.com/gruntsoftware/android/compare/v4.11.0...v4.12.0 + +--- + ### **v4.11.0** [PR [#259](https://github.com/gruntsoftware/android/pull/259)] --- #### ๐ŸŽฎ Fallinmoji v1.6.0 โ€” Ready, Set, GO! diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0fb4c031..90243649 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,8 +31,8 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 36 - versionCode = 202506349 - versionName = "v4.11.1" + versionCode = 202506350 + versionName = "v4.12.0" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/com/brainwallet/ui/screens/gamehub/GameHubViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/gamehub/GameHubViewModel.kt index bb3bb62d..e6e7f20a 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/gamehub/GameHubViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/gamehub/GameHubViewModel.kt @@ -129,6 +129,12 @@ class GameHubViewModel( } ) } + + AnalyticsManager.logCustomEventWithParams("did_play_game", null) + viewModelScope.launch { + delay(800L) + inAppReviewService.showInAppReviewDialogIfNeeded() + } } } } diff --git a/app/src/test/java/com/brainwallet/wallet/BRPeerManagerTest.kt b/app/src/test/java/com/brainwallet/wallet/BRPeerManagerTest.kt new file mode 100644 index 00000000..200b7249 --- /dev/null +++ b/app/src/test/java/com/brainwallet/wallet/BRPeerManagerTest.kt @@ -0,0 +1,79 @@ +package com.brainwallet.wallet + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.unmockkStatic +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Covers [BRPeerManager.onIntegrityWarning], the Java-side half of the native integrity-warning + * path added alongside the NULL-`lastBlock` guards in core's `BRPeerManager.c` + * (`BRPeerManagerSetIntegrityWarningCallback`). The native guard itself lives in the `core` + * submodule and isn't covered by this JVM suite. + */ +class BRPeerManagerTest { + + private val mockCrashlytics: FirebaseCrashlytics = mockk(relaxed = true) + + @Before + fun setUp() { + mockkStatic(FirebaseCrashlytics::class) + every { FirebaseCrashlytics.getInstance() } returns mockCrashlytics + } + + @After + fun tearDown() { + unmockkStatic(FirebaseCrashlytics::class) + } + + @Test + fun `onIntegrityWarning reports a RuntimeException to Crashlytics`() { + BRPeerManager.onIntegrityWarning("BRPeerManagerLastBlockHeight: lastBlock is NULL") + + val captured = slot() + verify(exactly = 1) { mockCrashlytics.recordException(capture(captured)) } + assertTrue(captured.captured is RuntimeException) + } + + @Test + fun `onIntegrityWarning includes the native warning text verbatim in the recorded exception message`() { + val warning = "BRPeerManagerRescan: checkpoint block missing from block set, lastBlock left unchanged" + + BRPeerManager.onIntegrityWarning(warning) + + val captured = slot() + verify { mockCrashlytics.recordException(capture(captured)) } + assertEquals("BRPeerManager native integrity warning: $warning", captured.captured.message) + } + + @Test + fun `onIntegrityWarning records once per call for each distinct native warning site`() { + // Mirrors the four call sites added in core's BRPeerManager.c + listOf( + "BRPeerManagerRescan: checkpoint block missing from block set, lastBlock left unchanged", + "BRPeerManagerEstimatedBlockHeight: lastBlock is NULL", + "BRPeerManagerLastBlockHeight: lastBlock is NULL", + "BRPeerManagerLastBlockTimestamp: lastBlock is NULL", + ).forEach { BRPeerManager.onIntegrityWarning(it) } + + verify(exactly = 4) { mockCrashlytics.recordException(any()) } + } + + @Test + fun `onIntegrityWarning does not throw when warning is null`() { + // Defensive: the native side always passes a short static string, but the JNI bridge + // (PeerManager.c's integrityWarning()) technically permits a null jstring โ€” this must + // never crash the calling native thread. + BRPeerManager.onIntegrityWarning(null) + + verify(exactly = 1) { mockCrashlytics.recordException(any()) } + } +}