Skip to content

feat: exposer le commitment Apple (iOS 26.4+) dans le bridge Flutter - #137

Merged
kherembourg merged 4 commits into
feat/sdk-v6-migrationfrom
feat/v6-commitment-bridge
Jul 20, 2026
Merged

feat: exposer le commitment Apple (iOS 26.4+) dans le bridge Flutter#137
kherembourg merged 4 commits into
feat/sdk-v6-migrationfrom
feat/v6-commitment-bridge

Conversation

@kherembourg

Copy link
Copy Markdown
Collaborator

Contexte

Le SDK iOS natif Purchasely v6 (pod 6.0.0-rc.3) expose le support Apple « monthly subscription with 12-month commitment » (facturation par échéances, iOS 26.4+) via une API publique. Les bridges cross-platform ne l'exposaient pas encore. Cette PR comble le trou de parité côté Flutter uniquement.

Fonctionnalité Apple-only : sur Android et les autres plateformes, les champs restent vides / null (parsing null-safe garanti).

Contrat iOS vérifié

API présente et confirmée sur le pod épinglé Purchasely 6.0.0-rc.3 — vérifié à la fois sur le tag git et sur le binaire réellement distribué en cache CocoaPods (arm64-apple-ios.swiftinterface, checksum ffb024…48 = celui du Podfile.lock) :

  • PLYPlan.commitmentInfo: [PLYCommitmentInfo]
  • PLYSubscription.commitmentProgress: PLYCommitmentProgress?
  • PLYCommitmentInfo (billingPlanType, billingPrice, billingPeriod, totalPrice, totalPeriod, totalDuration)
  • PLYCommitmentProgress (billingPeriodNumber, totalBillingPeriods, commitmentExpiresDate, commitmentPrice)
  • PLYBillingPlanType (unspecified / upFront / monthly), PLYOffering.billingPlanType, setDynamicOffering(…, billingPlanType:)

Fichiers modifiés

Dart

  • purchasely/lib/src/ply_models.dart — enum PLYBillingPlanType + helper plyBillingPlanTypeFromWire (tolère string ET int legacy) + extension .wire ; classes PLYCommitmentInfo / PLYCommitmentProgress (avec fromJson) ; champ commitmentInfo sur PLYPlan.
  • purchasely/lib/src/ply_transformers.dartplyCommitmentInfoFromMap / plyCommitmentProgressFromMap ; plyPlanFromMap peuple commitmentInfo.
  • purchasely/lib/purchasely_flutter.dart — champ commitmentProgress sur PLYSubscription (peuplé dans userSubscriptions + userSubscriptionsHistory) ; PLYDynamicOffering gagne billingPlanType (propagé à setDynamicOffering, reparsé depuis getDynamicOfferings).

iOS (bridge Swift, calqué sur les patterns existants)

  • purchasely/ios/purchasely_flutter/Classes/PLYPlan+ToMap.swift — clé commitmentInfo (via commitmentInfoMaps réutilisable) + extension PLYBillingPlanType.wireValue.
  • purchasely/ios/purchasely_flutter/Classes/PLYSubscription+ToMap.swift — clé commitmentProgress (date en ISO 8601).
  • purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swiftcommitmentInfo dans le plan du payload purchase de l'interceptor ; setDynamicOffering passe billingPlanType au natif ; getDynamicOfferings sérialise billingPlanType.

Docs

  • MIGRATION-v6.md — nouvelle section « Apple commitment plans (iOS 26.4+) ».

Android : aucun changement. Vérifié que le plugin Kotlin lit ses arguments par clé et ignore les clés extra → parsing null-safe côté Dart.

Forme exacte des payloads

Plan (clé commitmentInfo, tableau) :

{"billingPlanType":"monthly","billingPrice":9.99,"billingPeriod":"P1M","totalPrice":119.88,"totalPeriod":"P1Y","totalDuration":12}

Subscription (clé commitmentProgress) :

{"billingPeriodNumber":3,"totalBillingPeriods":12,"commitmentExpiresDate":"2027-01-15T00:00:00+0000","commitmentPrice":9.99}

Dynamic offering : clé billingPlanType = "up_front" / "monthly" / "unspecified" (string).

Convention wire billingPlanType

Le wire de billingPlanType est une string snake_case ("up_front" / "monthly" / "unspecified"), alignée sur la forme interne du SDK natif iOS. C'est intentionnel et non uniformisé cross-bridge (chaque bridge garde sa convention wire) ; le parsing Dart tolère aussi l'int rawValue par robustesse.

Résultats

  • flutter test : 308 tests, 0 échec (dont 10 nouveaux dans test/commitment_test.dart).
  • flutter analyze : No issues found.
  • dart format --set-exit-if-changed . : clean.
  • Build iOS local : flutter build ios --simulator --no-codesign → exit 0, Runner.app construit. Compilé contre le pod Purchasely 6.0.0-rc.3 ; les 3 fichiers Swift modifiés produisent bien leurs .o (arm64 + x86_64). Preuve que le mapping Swift compile contre l'API du pod.

Note CI iOS

La CI (ci.yml) couvre bien iOS : jobs ios-unit-tests (xcodebuild test RunnerTests), build-ios (flutter build ios --debug --simulator --no-codesign), build-ios-swiftpm (build SwiftPM) et validate-podspec (pod lib lint), tous gated par ci-success.

Attention : ci.yml (et e2e-ios.yml) ne se déclenchent que sur les PR ciblant main / master / develop. Cette PR cible feat/sdk-v6-migration, donc la CI ne tournera PAS automatiquement dessus. La couverture iOS s'appliquera lorsque feat/sdk-v6-migration sera mergée vers main. (Signalé, non modifié.)

🤖 Generated with Claude Code

kherembourg and others added 3 commits July 20, 2026 16:34
Add PLYCommitmentInfo, PLYCommitmentProgress and the PLYBillingPlanType
enum (Apple-only, iOS 26.4+ monthly-commitment billing). Wire them into
the native map transformers:

- PLYPlan gains commitmentInfo (populated by plyPlanFromMap wherever a
  plan is marshaled, incl. the purchase interceptor payload).
- PLYSubscription gains commitmentProgress (userSubscriptions /
  userSubscriptionsHistory).
- PLYDynamicOffering gains billingPlanType, propagated to
  setDynamicOffering and parsed back from getDynamicOfferings.

Parsing is null/empty-safe so Android and other platforms (which never
send these keys) yield an empty list / null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Marshal PLYPlan.commitmentInfo (billingPlanType/billingPrice/billingPeriod/
totalPrice/totalPeriod/totalDuration) into the plan dictionary — via
PLYPlan.toMap (allProducts, planWithIdentifier, outcome.plan) and the
purchase interceptor payload — and PLYSubscription.commitmentProgress
(billingPeriodNumber/totalBillingPeriods/commitmentExpiresDate ISO 8601/
commitmentPrice) into the subscription dictionary. Propagate billingPlanType
through setDynamicOffering and getDynamicOfferings. Verified against the
pinned Purchasely 6.0.0-rc.3 pod's public API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds Flutter bridge support for Apple's "monthly subscription with N-month commitment" (installment billing) feature introduced in iOS 26.4+, matching the API already exposed by the native Purchasely iOS SDK v6. The implementation is additive and null-safe: Android and non-Apple platforms receive no extra keys and existing Dart parsing defaults to empty lists / null.

  • Dart layer: new PLYBillingPlanType enum, PLYCommitmentInfo / PLYCommitmentProgress models, wire helpers, and fields on PLYPlan, PLYSubscription, and PLYDynamicOffering — all with fallback defaults so existing call sites are unaffected.
  • Swift bridge: PLYPlan+ToMap and PLYSubscription+ToMap serialize the new native fields using established if let / guard patterns; SwiftPurchaselyFlutterPlugin threads billingPlanType through setDynamicOffering and getDynamicOfferings.
  • Tests: 10 new unit tests cover the full parsing round-trip including the Android / empty-list path.

Confidence Score: 4/5

Safe to merge after the const [] fix — all other changes are well-structured and tested.

The plyCommitmentInfoFromMap function returns the immutable const [] for plans with no commitment info, while returning a mutable .toList() otherwise. PLYPlan.commitmentInfo is declared as a mutable list field, so plan.commitmentInfo.add(x) would throw UnsupportedError at runtime for Android plans or pre-iOS 26.4 devices — the common case — while silently succeeding for iOS commitment plans. Everything else is clean and follows existing conventions.

purchasely/lib/src/ply_transformers.dart (the const [] return) and purchasely/lib/src/ply_models.dart (the public leak of plyBillingPlanTypeFromWire).

Important Files Changed

Filename Overview
purchasely/lib/src/ply_transformers.dart Adds plyCommitmentInfoFromMap and plyCommitmentProgressFromMap parsers; attaches commitment info to plan via cascade. Returns immutable const [] for the empty case while the non-empty path returns a mutable list — inconsistent contract on the public commitmentInfo field.
purchasely/lib/src/ply_models.dart Adds PLYBillingPlanType enum, PLYCommitmentInfo, PLYCommitmentProgress classes, and wire-mapping helpers. Internal parsing function plyBillingPlanTypeFromWire is unintentionally part of the public API because ply_models.dart is re-exported from the barrel.
purchasely/lib/purchasely_flutter.dart Adds commitmentProgress to PLYSubscription and billingPlanType to PLYDynamicOffering; threads both through userSubscriptions, userSubscriptionsHistory, setDynamicOffering, and getDynamicOfferings. Changes are additive and backward-compatible.
purchasely/ios/purchasely_flutter/Classes/PLYPlan+ToMap.swift Serializes commitmentInfo array to wire format; adds PLYBillingPlanType.wireValue extension. Logic is consistent with existing toMap patterns and correctly guards the empty-array case.
purchasely/ios/purchasely_flutter/Classes/PLYSubscription+ToMap.swift Adds optional commitmentProgress serialization using the existing dateFormat instance. Follows established if-let guard pattern; date format is consistent with other date fields.
purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swift Passes billingPlanType to setDynamicOffering native call; serializes billingPlanType in getDynamicOfferings; injects commitmentInfo into the purchase interceptor plan map. All changes look correct and consistent.
purchasely/test/commitment_test.dart 10 new unit tests covering wire mapping, plan parsing, interceptor payload, progress parsing, and dynamic-offering serialization. Good coverage of the null/empty (Android) path.

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
purchasely/lib/src/ply_transformers.dart:42-48
**`const []` vs mutable list inconsistency in `commitmentInfo`**

`plyCommitmentInfoFromMap` returns the immutable `const []` sentinel for the "no data" case, but a fresh mutable `.toList()` otherwise. `PLYPlan.commitmentInfo` is declared as a mutable `List<PLYCommitmentInfo>`, so any code that calls `plan.commitmentInfo.add(…)` will throw `UnsupportedError` exactly when no commitment info was received (Android / pre-iOS 26.4), and succeed silently when data was present — an inconsistent, hard-to-diagnose runtime trap. Replace `const []` with a regular `[]` to keep the mutability contract uniform.

```suggestion
  if (raw is! List) return [];
```

### Issue 2 of 3
purchasely/lib/src/ply_models.dart:117-140
**`plyBillingPlanTypeFromWire` leaked into the public API**

`ply_models.dart` is explicitly `export`-ed from the barrel (`purchasely_flutter.dart` line 25), making `plyBillingPlanTypeFromWire` — an internal wire-parsing helper — part of the package's public surface. The analogous helpers (`plyPlanTypeFromWire`, etc.) live in `ply_transformers.dart`, which is *not* exported. Moving `plyBillingPlanTypeFromWire` to `ply_transformers.dart` would keep the public API clean without losing any functionality.

### Issue 3 of 3
purchasely/lib/src/ply_models.dart:231-234
**`_toDouble` / `_toInt` duplicated across files**

`ply_transformers.dart` already defines a private `_toDouble` (lines 103–109). The same helper (plus a companion `_toInt`) is now re-defined here. Since both files are file-private they won't conflict, but any future divergence in edge-case handling will be silently inconsistent. Consider extracting a shared internal helper, or at minimum ensure the two implementations stay in sync.

Reviews (1): Last reviewed commit: "docs(migration): document Apple commitme..." | Re-trigger Greptile

Comment thread purchasely/lib/src/ply_transformers.dart
Comment thread purchasely/lib/src/ply_models.dart
Comment thread purchasely/lib/src/ply_models.dart Outdated
- fix: plyCommitmentInfoFromMap returns a mutable [] (was const []) so
  PLYPlan.commitmentInfo.add() never throws on the Android/empty path (P1)
- move plyBillingPlanTypeFromWire + commitment fromJson parsing into
  ply_transformers.dart (not exported) — keeps it out of the public API (P2)
- single _toDouble/_toInt in ply_transformers.dart, drop the duplicates in
  ply_models.dart; models are now pure data classes like PLYPlan (P2)

flutter test 308 pass, flutter analyze clean, dart format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kherembourg

Copy link
Copy Markdown
Collaborator Author

All 3 findings addressed in a9350ab (net −7 lines; models are now pure data classes like PLYPlan):

# Severity Finding Fix
1 P1 const [] vs mutable list in commitmentInfo plyCommitmentInfoFromMap returns a mutable []; .add() no longer throws on the Android/empty path
2 P2 plyBillingPlanTypeFromWire leaked into public API Moved to ply_transformers.dart (not exported); commitment fromJson parsing moved there too (no circular import)
3 P2 _toDouble/_toInt duplicated across files Single _toDouble/_toInt in ply_transformers.dart; duplicates dropped from ply_models.dart

flutter test 308 pass · flutter analyze clean · dart format clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Cette PR ajoute la parité Flutter pour la fonctionnalité iOS Purchasely v6 “monthly subscription with N‑month commitment” (iOS 26.4+), en exposant les informations de commitment côté Dart et en les sérialisant dans le bridge iOS (Swift), tout en gardant un comportement null-safe sur Android / autres plateformes.

Changes:

  • Ajout des modèles Dart PLYBillingPlanType, PLYCommitmentInfo et PLYCommitmentProgress, et propagation dans PLYPlan / PLYSubscription / PLYDynamicOffering.
  • Ajout du parsing/mapping côté Dart (plyCommitmentInfoFromMap, plyCommitmentProgressFromMap, plyBillingPlanTypeFromWire) et du wiring via userSubscriptions* et setDynamicOffering / getDynamicOfferings.
  • Ajout de la sérialisation iOS (Swift) pour commitmentInfo (plans), commitmentProgress (subscriptions) et billingPlanType (dynamic offerings), plus documentation de migration et tests unitaires.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
purchasely/test/commitment_test.dart Ajoute des tests unitaires couvrant le mapping wire et le parsing des nouveaux champs commitment.
purchasely/lib/src/ply_transformers.dart Ajoute les parseurs commitment (plan/progress) et le mapping tolérant billingPlanType depuis le wire.
purchasely/lib/src/ply_models.dart Introduit les nouveaux modèles/enum commitment et le champ commitmentInfo sur PLYPlan.
purchasely/lib/purchasely_flutter.dart Expose commitmentProgress sur PLYSubscription et billingPlanType sur PLYDynamicOffering, et les connecte aux APIs bridge.
purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swift Propage billingPlanType dans setDynamicOffering, sérialise billingPlanType dans getDynamicOfferings, et inclut commitmentInfo dans le payload interceptor purchase.
purchasely/ios/purchasely_flutter/Classes/PLYSubscription+ToMap.swift Ajoute la sérialisation commitmentProgress côté iOS.
purchasely/ios/purchasely_flutter/Classes/PLYPlan+ToMap.swift Ajoute commitmentInfo au mapping plan iOS + helper réutilisable commitmentInfoMaps + PLYBillingPlanType.wireValue.
MIGRATION-v6.md Documente l’API Flutter v6 pour les Apple commitment plans et donne un exemple d’usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kherembourg
kherembourg merged commit 5a4bf0d into feat/sdk-v6-migration Jul 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants