article / Mobile

Debugging StoreKit 2 JWS verification in Flutter

A practical path from an opaque subscription failure to a testable verification boundary.

Why does StoreKit verification fail after a valid purchase?

A successful purchase and a trustworthy entitlement are separate events. Treat the signed transaction as untrusted input until its signature, bundle, environment, and dates have passed verification.

Where should the verification boundary live?

Put parsing and policy behind one typed boundary. The Flutter layer should receive a small result such as verified, expired, environment mismatch, or malformed instead of a raw platform exception.

dart
sealed class EntitlementResult {}
final class Verified extends EntitlementResult {
  Verified(this.productId, this.expiresAt);
  final String productId;
  final DateTime expiresAt;
}
final class Rejected extends EntitlementResult {
  Rejected(this.reason);
  final VerificationReason reason;
}

How do you debug the failure without guessing?

Capture the environment, product identifier, transaction identifier suffix, verification stage, and normalized failure reason. Do not log the full signed payload or account identifiers.

Reproduce each boundary independently

Use fixture payloads for date and claim checks, platform tests for native decoding, and one end-to-end sandbox flow. That keeps a sandbox outage from blocking every useful test.

What is the production takeaway?

Subscription code is security and revenue code. Make the trust boundary explicit, return typed outcomes, and log enough context to explain a rejection without exposing the payload.