parseWebhookEventData function
Pure parser exported for tests so the SSE-frame → WebhookEvent
path can be validated without spinning up a real HTTP listener.
Implementation
WebhookEvent? parseWebhookEventData(String raw) {
if (raw.isEmpty) return null;
Map<String, dynamic>? decoded;
try {
final value = jsonDecode(raw);
if (value is Map<String, dynamic>) decoded = value;
} catch (_) {
return null;
}
if (decoded == null) return null;
if (!decoded.containsKey('id') ||
!decoded.containsKey('type') ||
!decoded.containsKey('occurredAt') ||
!decoded.containsKey('receivedAt')) {
return null;
}
// `purchaseToken` is required for every event type *except*
// TestNotification — Apple ASN v2 / Google RTDN test payloads carry
// no transaction, so kit emits the event with `purchaseToken` unset.
// Hard-requiring the field surfaced valid test webhooks as
// MALFORMED_EVENT in Flutter and never reached listeners.
if (decoded['type'] != 'TestNotification' &&
!decoded.containsKey('purchaseToken')) {
return null;
}
// The wire format kit currently emits uses GraphQL enum identifiers
// (PascalCase, e.g. `AppleAppStoreServerNotificationsV2`). The
// generated Dart `fromJson` factories only accept the kebab-case
// wire form (`apple-app-store-server-notifications-v2`). Normalize
// each enum field here so consumers don't have to know about the
// representational difference. PR #123 (https://github.com/hyodotdev/openiap/pull/123) review caught this drift.
return _decodeWithFallback(decoded);
}