Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <base64-blob> <comment>` 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!
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ class GameHubViewModel(
}
)
}

AnalyticsManager.logCustomEventWithParams("did_play_game", null)
viewModelScope.launch {
delay(800L)
inAppReviewService.showInAppReviewDialogIfNeeded()
}
}
}
}
Expand Down
79 changes: 79 additions & 0 deletions app/src/test/java/com/brainwallet/wallet/BRPeerManagerTest.kt
Original file line number Diff line number Diff line change
@@ -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<Throwable>()
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<Throwable>()
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()) }
}
}