feat: exposer le commitment Apple (iOS 26.4+) dans le bridge Flutter - #137
Conversation
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>
|
| 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. |
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
- 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>
|
All 3 findings addressed in
|
There was a problem hiding this comment.
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,PLYCommitmentInfoetPLYCommitmentProgress, et propagation dansPLYPlan/PLYSubscription/PLYDynamicOffering. - Ajout du parsing/mapping côté Dart (
plyCommitmentInfoFromMap,plyCommitmentProgressFromMap,plyBillingPlanTypeFromWire) et du wiring viauserSubscriptions*etsetDynamicOffering/getDynamicOfferings. - Ajout de la sérialisation iOS (Swift) pour
commitmentInfo(plans),commitmentProgress(subscriptions) etbillingPlanType(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.
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, checksumffb024…48= celui duPodfile.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— enumPLYBillingPlanType+ helperplyBillingPlanTypeFromWire(tolère string ET int legacy) + extension.wire; classesPLYCommitmentInfo/PLYCommitmentProgress(avecfromJson) ; champcommitmentInfosurPLYPlan.purchasely/lib/src/ply_transformers.dart—plyCommitmentInfoFromMap/plyCommitmentProgressFromMap;plyPlanFromMappeuplecommitmentInfo.purchasely/lib/purchasely_flutter.dart— champcommitmentProgresssurPLYSubscription(peuplé dansuserSubscriptions+userSubscriptionsHistory) ;PLYDynamicOfferinggagnebillingPlanType(propagé àsetDynamicOffering, reparsé depuisgetDynamicOfferings).iOS (bridge Swift, calqué sur les patterns existants)
purchasely/ios/purchasely_flutter/Classes/PLYPlan+ToMap.swift— clécommitmentInfo(viacommitmentInfoMapsréutilisable) + extensionPLYBillingPlanType.wireValue.purchasely/ios/purchasely_flutter/Classes/PLYSubscription+ToMap.swift— clécommitmentProgress(date en ISO 8601).purchasely/ios/purchasely_flutter/Classes/SwiftPurchaselyFlutterPlugin.swift—commitmentInfodans le plan du payload purchase de l'interceptor ;setDynamicOfferingpassebillingPlanTypeau natif ;getDynamicOfferingssérialisebillingPlanType.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
billingPlanTypeest 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 danstest/commitment_test.dart).flutter analyze: No issues found.dart format --set-exit-if-changed .: clean.flutter build ios --simulator --no-codesign→ exit 0,Runner.appconstruit. Compilé contre le podPurchasely 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 : jobsios-unit-tests(xcodebuild test RunnerTests),build-ios(flutter build ios --debug --simulator --no-codesign),build-ios-swiftpm(build SwiftPM) etvalidate-podspec(pod lib lint), tous gated parci-success.Attention :
ci.yml(ete2e-ios.yml) ne se déclenchent que sur les PR ciblantmain/master/develop. Cette PR ciblefeat/sdk-v6-migration, donc la CI ne tournera PAS automatiquement dessus. La couverture iOS s'appliquera lorsquefeat/sdk-v6-migrationsera mergée versmain. (Signalé, non modifié.)🤖 Generated with Claude Code