fetchAndApplyKbcPatch method
η v1: in-app fetch + apply for iOS Path C.
Replaces the manual xcrun devicectl device copy to workflow
with a single in-app call. Hits the Sankofa server's
/api/deploy/check endpoint, downloads the latest matching
envelope, verifies sha-256, persists to
<Documents>/sankofa-deploy/patches/active/patch.skdp, and
applies via loader.
The canonical wiring from a host app:
import 'package:dynamic_modules/dynamic_modules.dart';
final result = await Sankofa.deploy!.fetchAndApplyKbcPatch(
endpoint: 'http://172.20.10.8:8080', // Mac's LAN IP in dev
apiKey: '<test or live SDK key>',
appVersion: '1.0.0',
engineVersion: '3.41.9+sankofa-1',
distinctId: await getOrCreateDistinctId(),
loader: loadModuleFromBytes,
);
if (result.hasUpdate) {
print('patched: ${result.applied?.returnValue}');
}
Throws KbcFetchException on any HTTP / sha-mismatch / apply failure. The exception message names the most likely cause.
Implementation
Future<KbcFetchResult> fetchAndApplyKbcPatch({
required KbcLoaderFn loader,
String? endpoint,
String? apiKey,
String? appVersion,
String? engineVersion,
String? distinctId,
String? platform,
String? currentLabel,
bool persistToDisk = true,
Duration timeout = const Duration(seconds: 30),
}) async {
// Path C is pure Dart — see applyKbcPatchFromBytes above.
// Prefix-call to avoid infinite recursion on the instance method.
final banned = await getBannedKbcLabels();
final resolvedAppVersion = _resolveAppVersion(appVersion);
final resolvedPlatform = platform ?? _effectivePlatform;
KbcFetchResult result;
try {
result = await kbc_fetch.fetchAndApplyKbcPatch(
endpoint: _resolveEndpoint(endpoint),
apiKey: _resolveApiKey(apiKey),
appVersion: resolvedAppVersion,
engineVersion: _resolveEngineVersion(engineVersion),
distinctId: _resolveDistinctId(distinctId),
loader: loader,
platform: resolvedPlatform,
currentLabel: currentLabel,
persistToDisk: persistToDisk,
timeout: timeout,
signingPubkeysB64: _effectiveSigningPubkeys,
bannedLabels: banned.isEmpty ? null : banned,
);
} catch (err, st) {
// Telemetry fire-and-forget. Don't await — preserve the original
// throw timing for the host's UI feedback.
final stack = err is KbcApplyException
? _formatKbcStackTrace(err.causeStackTrace ?? st)
: _formatKbcStackTrace(st);
_reportKbcEvent(
eventType: 'kbc_apply_failed',
bundleLabel: currentLabel,
appVersion: resolvedAppVersion,
platform: resolvedPlatform,
errorMessage: err.toString(),
extra: {
'phase': 'fetch_apply',
'cause_class': err.runtimeType.toString(),
if (err is KbcApplyException && err.cause != null)
'inner_cause': err.cause.toString(),
if (err is KbcApplyException && err.cause != null)
'inner_cause_class': err.cause.runtimeType.toString(),
if (stack != null) 'stack_trace': stack,
},
);
rethrow;
}
if (result.hasUpdate && result.applied != null) {
_reportKbcEvent(
eventType: 'kbc_apply_success',
releaseId: result.releaseId,
bundleLabel: result.label,
appVersion: resolvedAppVersion,
platform: resolvedPlatform,
);
} else if (result.reason == 'label_banned_locally') {
// Surface the skip so dashboards know "this device knows label X
// is bad and is refusing to re-fetch it". Server-side telemetry
// can then prompt the customer to disable the release globally.
_reportKbcEvent(
eventType: 'kbc_apply_skipped_label_banned',
releaseId: result.releaseId,
bundleLabel: result.label,
appVersion: resolvedAppVersion,
platform: resolvedPlatform,
);
}
return result;
}