Complete Usage Patterns Matrix
The tk_ota_push SDK and shorebird.yaml permit developers to combine trigger mechanisms (native auto-update, silent Gatekeeper, critical forced Gatekeeper, manual checks), restart behaviors, Wi-Fi constraints, distribution tracks (stable / staging), and security policies (TLS pinning / APK anti-repackaging).
Comparison Table of 8 Integration Patterns
| # | Integration Pattern | shorebird.yaml (auto_update) | enableGatekeeper | forceUpdate | requireWifi | Channel (track) | Pinning & Shield | Patch Application |
|---|---|---|---|---|---|---|---|---|
| 1 | Silent Eco-Data with Gatekeeper (Production Standard) | false | true | false | false | stable | Active | Next Cold Start |
| 2 | Silent Wi-Fi Only & Battery >= 30% | false | true | false | true | stable | Active | Deferred (workmanager) then Cold Start |
| 3 | Critical Forced Update (Blocking Modal) | false | true | true | Ignored | stable | Active | Immediate download + restart prompt |
| 4 | Autonomous Native Rust Auto-Update (Zero Dart Code) | true | (N/A) | (N/A) | No | stable | RSA signature (strict) | Next Cold Start |
| 5 | 100% Manual Control ("Check for Updates" Button) | false | false | (N/A) | No | stable | Optional | On demand |
| 6 | Dynamic Staging Track Switch (Internal QA Menu) | false | true / false | false | false | staging | Optional | Immediate on next boot |
| 7 | High Security: TLS Pinning + Anti-Repackaging + Anti-Root | false | true | false / true | false | stable | Strict (HttpOverrides + APK SHA-256) | Blocked if device/network untrusted |
| 8 | Without Gatekeeper (enableGatekeeper: false) with SDK Policies | false | false | false / true | true / false | stable | Active | Always queries server |
Code Examples for Each Combination
Pattern 1: Silent Eco-Data with Gatekeeper (Recommended for Production)
In shorebird.yaml: auto_update: false. The OTA server is only queried when all 3 RemoteConfigGatekeeper conditions are met. The patch is downloaded silently in the background and applied on the next cold start without disrupting user sessions.
final ota = TkOtaPush.init(
appReleaseVersion: '1.0.0+3',
enableGatekeeper: true,
requireWifiForSilentUpdate: false,
minBatteryPercentageForSilentUpdate: 20,
);
final outcome = await ota.syncWithRemoteConfig(
const OtaRemoteConfig(
targetReleaseVersion: '1.0.0+3',
patchAvailable: true,
latestPatchNumber: 2,
forceUpdate: false,
),
);
// outcome.executionMode == OtaUpdateExecutionMode.silentBackgroundUpdate
// outcome.restartRequired == false (applied on next Cold Start)
Pattern 2: Silent Wi-Fi Only & Background Task Scheduler
Ideal for user bases on constrained cellular data plans, deferring downloads until the device connects to unmetered Wi-Fi with at least 30% battery.
final scheduler = InMemoryOtaBackgroundScheduler(); // Or custom workmanager implementation
final ota = TkOtaPush.init(
appReleaseVersion: '1.0.0+3',
enableGatekeeper: true,
requireWifiForSilentUpdate: true,
minBatteryPercentageForSilentUpdate: 30,
environmentMonitor: const DefaultOtaEnvironmentMonitor(
defaultConnectivity: OtaConnectivityType.mobile,
defaultBatteryPercentage: 80,
),
backgroundScheduler: scheduler,
);
final outcome = await ota.syncWithRemoteConfig(
const OtaRemoteConfig(
targetReleaseVersion: '1.0.0+3',
patchAvailable: true,
latestPatchNumber: 2,
forceUpdate: false,
),
);
// outcome.executionMode == OtaUpdateExecutionMode.deferredToBackgroundTask
// outcome.networkCallPerformed == false
Pattern 3: Critical Forced Update with Blocking Modal & Restart
When forceUpdate: true arrives in the Remote Config payload, the SDK bypasses Wi-Fi restrictions, fires onCriticalUpdateRequired, and sets outcome.restartRequired = true upon download completion.
final ota = TkOtaPush.init(
appReleaseVersion: '1.0.0+3',
enableGatekeeper: true,
requireWifiForSilentUpdate: true, // Automatically ignored when forceUpdate == true
onCriticalUpdateRequired: (config, triggerImmediateDownload) async {
// Show non-dismissible modal (barrierDismissible: false)
final success = await triggerImmediateDownload();
if (success) {
// Prompt user to immediately restart application
}
},
);
final outcome = await ota.syncWithRemoteConfig(
const OtaRemoteConfig(
targetReleaseVersion: '1.0.0+3',
patchAvailable: true,
latestPatchNumber: 3,
forceUpdate: true,
releaseNotes: 'Mandatory critical security hotfix.',
),
);
// outcome.executionMode == OtaUpdateExecutionMode.criticalForcedUpdate
// outcome.restartRequired == true
Pattern 4: Autonomous Native Rust Auto-Update (auto_update: true, without Gatekeeper)
When operating without a Remote Config provider and wishing for the C++/Rust engine to poll for updates on every application launch:
# shorebird.yaml
app_id: "com.myapp.mobile"
base_url: "https://ota.myapp.internal"
channel: "stable"
auto_update: true
patch_verification: strict
patch_public_key: MIIBCgKCAQEA...
In main.dart, you can still leverage TkOtaPush for rollback recovery and support code retrieval:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final ota = TkOtaPush.init(
appReleaseVersion: '1.0.0+1',
onRollbackDetected: (event) async {
// Clean local cache following native engine rollback
},
);
await ota.verifyStartupAndHandleRollback();
runApp(const MyApp());
}
Pattern 5: 100% Manual Control Without Remote Config
Triggers an update check only when the user clicks "Check for Updates" in app settings (auto_update: false in shorebird.yaml):
Future<void> onManualUpdateButtonPressed(BuildContext context) async {
final ota = TkOtaPush.instance;
if (!ota.isAvailable) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('OTA engine not available in debug mode.')),
);
return;
}
final status = await ota.checkForUpdate();
if (status == UpdateStatus.outdated) {
await ota.update();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Update downloaded. Please restart the app.')),
);
}
} else if (status == UpdateStatus.restartRequired) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('A patch is already staged. Please restart the app.')),
);
}
}
Pattern 6: Staging Track vs Stable Switching (QA Mode)
Enables internal QA testers running production binaries (stable) to switch to staging on-the-fly to validate canary patches before public promotion:
final ota = TkOtaPush.instance;
// Enable staging channel
ota.enableQaMode();
assert(ota.track == UpdateTrack.staging);
// Download patch published with --channel staging
final downloaded = await ota.checkAndUpdate();
// Revert to stable channel
ota.disableQaMode();
assert(ota.track == UpdateTrack.stable);
Pattern 7: TLS Certificate Pinning, APK Anti-Repackaging & Root Detection
Combines TLS pinning on the Dart HTTP stack (HttpOverrides.global) with OtaSecurityPolicy in TkOtaPush.init:
import 'dart:io';
import 'package:tk_ota_push/tk_ota_push.dart';
void initHighSecurityOta() {
const pinnedCertHash = '42d889f8f4b6bf9d3abb767d3b5f215e0009e4d26cb7b958497e8b291e1b96dc';
// 1. Enforce TLS certificate pinning on HttpClient
HttpOverrides.global = OtaPinnedHttpOverrides(
OtaCertificatePinner(
pinnedSha256Fingerprints: {pinnedCertHash},
enforcedHosts: {'ota.saramoney.internal'},
),
);
// 2. Initialize TkOtaPush with APK verification and root/jailbreak detection
TkOtaPush.init(
appReleaseVersion: '1.0.0+3',
securityPolicy: const OtaSecurityPolicy(
blockRootedOrJailbrokenDevices: true,
expectedApkSignaturesSha256: {
'6b6929c8ff345cf008496328d7bff081a8ccfb89498bf9916ed668be3c3dedcf',
},
pinnedServerCertSha256Fingerprints: {pinnedCertHash},
),
);
}
Pattern 8: Without Gatekeeper (enableGatekeeper: false) with Shield and Scheduler
Retains the mobile security shield, rollback tracking, and battery checks while bypassing the 3 Gatekeeper version/patch conditions (GatekeeperVerdict.bypassedGatekeeperDisabled):
final ota = TkOtaPush.init(
appReleaseVersion: '1.0.0+3',
enableGatekeeper: false, // Bypasses version/patch number conditions
requireWifiForSilentUpdate: true,
);
final outcome = await ota.syncWithRemoteConfig(
const OtaRemoteConfig(
targetReleaseVersion: '',
patchAvailable: false,
latestPatchNumber: 0,
forceUpdate: false,
),
);
// outcome.gatekeeperEvaluation?.verdict == GatekeeperVerdict.bypassedGatekeeperDisabled