Remote config, update walls and maintenance mode for Android. Decided at the edge, verified on device.
Not on Maven Central yet. Until dev.ripstop:ripstop is published, include the
repo as a composite build:
// settings.gradle.kts
includeBuild("../ripstop-kotlin")// build.gradle.kts
dependencies {
implementation("dev.ripstop:ripstop")
}Gradle substitutes the coordinates with the local build. Copying src/ in as a
source module works too. It is one module with a single ~100 KB dependency.
// Off the main thread, because init performs the first check.
val ripstop = Ripstop.init(
apiKey = "rs_pub_your_key",
appVersion = BuildConfig.VERSION_NAME,
storage = SharedPrefsStorage(context), // four lines, below
)
when (val decision = ripstop.check()) {
is RsDecision.ForceUpdate -> showBlockingWall(decision)
is RsDecision.SoftUpdate -> showDismissibleSheet(decision)
is RsDecision.Maintenance -> showMaintenance(decision)
RsDecision.None -> Unit // carry on
}The when is exhaustive: if the protocol grows a decision, your app stops
compiling instead of silently showing nobody anything.
The Swift and Flutter SDKs read the installed version themselves. Doing it on
Android means PackageManager.getPackageInfo(context.packageName, 0), and this
library takes no Context. That is the same reason it ships no SharedPreferences
storage, and what keeps it usable from plain JVM code and from tests.
You lose nothing by passing it. BuildConfig.VERSION_NAME is generated by the
Android Gradle plugin from your module's versionName, so it is the same string
PackageManager would return. Nobody types it, and it cannot drift from the
build it ships in. What you must not pass is a literal.
Pass the version only: 4.1.0, not 4.1.0 (312). versionCode is not part of
a semantic version, and the rules are compared as semantic versions. Anything
unreadable is treated as no opinion, so a mistake here lets the app run rather
than walling it.
The library does not depend on Android, so it cannot ship a SharedPreferences implementation, and in exchange it stays usable from plain JVM code and from tests. Wiring one up is four lines:
class SharedPrefsStorage(context: Context) : RipstopStorage {
private val prefs = context.getSharedPreferences("ripstop", Context.MODE_PRIVATE)
override fun read(key: String) = prefs.getString(key, null)
override fun write(key: String, value: String) { prefs.edit().putString(key, value).apply() }
override fun remove(key: String) { prefs.edit().remove(key).apply() }
}val enabled = ripstop.values["checkout_enabled"] as? Boolean ?: true| Situation | What happens |
|---|---|
| No network | The last signed payload drives the decision |
| No network, no cache | RsDecision.None, so your app runs unrestricted |
| Server returns 500, or times out | Cache, then None |
| Signature doesn't verify | Treated as a failure. A forged payload can never block your app |
| Cache edited on device | Re-verified on read, so it grants nothing |
| Maintenance on, then network lost | The wall stays, until a fresh signed payload clears it |
The JVM has had Ed25519 in the standard library since Java 15, but Android does
not expose it below API 33. An SDK built on JCA would silently stop verifying
signatures on most of the installed base. That is the worst kind of failure,
because it looks like it works. This uses net.i2p.crypto:eddsa (~100 KB, pure
Java).
Every Ripstop SDK runs the same vectors.json: version ordering, evaluation
order, snooze accounting, the fail-open state machine. ./gradlew test runs it
here, alongside tests that sign with a real key pair.
Full docs: ripstop.dev/docs/kotlin
MIT
