Skip to content

Track free trial start for Stripe purchases#452

Merged
yusuftor merged 9 commits intodevelopfrom
fix/stripe-free-trial-tracking
Mar 10, 2026
Merged

Track free trial start for Stripe purchases#452
yusuftor merged 9 commits intodevelopfrom
fix/stripe-free-trial-tracking

Conversation

@yusuftor
Copy link
Collaborator

@yusuftor yusuftor commented Mar 10, 2026

Summary

  • Fires FreeTrialStart event when a Stripe purchase completes with a trial-eligible product, so the delegate receives .freeTrialStart for Stripe purchases (previously only fired for App Store purchases)
  • Fixes notification scheduling to additionally check paywallInfo.isFreeTrialAvailable, so trial notifications only fire when the user is actually eligible for a trial (not just when the product has trialPeriodDays > 0)
  • Both product.trialPeriodDays > 0 (product has a trial configured) and isFreeTrialAvailable (user is eligible) must be true

Test plan

  • Verify .freeTrialStart delegate event fires after a Stripe purchase with a free trial when user is eligible
  • Verify .freeTrialStart does NOT fire when isFreeTrialAvailable is false (user already had the entitlement)
  • Verify trial notifications are scheduled only when both trialPeriodDays > 0 and isFreeTrialAvailable are true
  • Verify trial notifications are NOT scheduled when the product has trialPeriodDays > 0 but user is ineligible

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

Greptile Summary

This PR extends free trial tracking to Stripe purchases by firing the FreeTrialStart event when a Stripe redemption completes with a trial-eligible product, and tightens the notification scheduling guard so both trialPeriodDays > 0 (product-level) and isFreeTrialAvailable (user-level eligibility) must be true before any trial-related side effects fire.

Key changes:

  • afterRedeem() in WebEntitlementRedeemer: Added a nested isFreeTrialAvailable guard inside the existing trialPeriodDays > 0 check; both must be true to call superwall.track(FreeTrialStart(...)) and schedule local notifications. Uses the injected superwall parameter (not Superwall.shared) and redemptionProduct.identifier directly, correctly addressing the dependency-injection and identifier-mismatch concerns raised previously.
  • handleStripeCheckoutAbandon: Co-bundled fix passes product: product instead of product: nil to the abandon Transaction event — the product was already embedded in the state enum but was missing from the top-level field, which would have caused analytics to drop product information on Stripe checkout abandons.
  • Tests: Four cases are now fully covered — fires when eligible, suppressed when trialPeriodDays == 0, suppressed when isFreeTrialAvailable == false, and suppressed when trialPeriodDays == 0 even if isFreeTrialAvailable == true. The isFreeTrialAvailable: true setter was also retroactively added to the existing positive-path test to prevent false-positives after the new guard was introduced.
  • RevenueCat dependency: Bumped from 5.60.05.61.0 (minor version, no API surface changes expected).

Confidence Score: 4/5

  • Safe to merge; logic is correct and well-tested, with one minor documentation gap around the co-bundled abandon fix.
  • All previously flagged issues (Superwall.shared misuse, variable shadowing, product identifier mismatch) have been addressed. Four test cases provide comprehensive branch coverage of the new dual-guard condition. The only notable gap is that the handleStripeCheckoutAbandon product: nil → product: product fix is not reflected in CHANGELOG.md, which is a documentation concern rather than a functional one.
  • Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift — specifically the co-bundled abandon fix on line 253 which lacks a changelog entry

Important Files Changed

Filename Overview
Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift Adds FreeTrialStart event tracking for Stripe purchases (gated by both trialPeriodDays > 0 and isFreeTrialAvailable), and fixes abandon transaction event to include product info instead of nil
Tests/SuperwallKitTests/Web/WebEntitlementRedeemerTests.swift Adds comprehensive test coverage for all four cases: event fires when eligible, and event does NOT fire when trialPeriodDays==0, isFreeTrialAvailable==false, or both conditions unmet
CHANGELOG.md Updated entry to reflect freeTrial_start tracking; the co-bundled fix to abandon transaction product (nil → product) in handleStripeCheckoutAbandon is not explicitly mentioned
Examples/Advanced/Advanced.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved RevenueCat dependency bumped from 5.60.0 to 5.61.0 — routine minor version update

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[afterRedeem called] --> B{codeResult == .success?}
    B -- No --> Z[Skip trial logic]
    B -- Yes --> C{redemptionInfo.paywallInfo?.product != nil?}
    C -- No --> Z
    C -- Yes --> D{redemptionProduct.trialPeriodDays > 0?}
    D -- No --> Z
    D -- Yes --> E{superwall.paywallViewController != nil?}
    E -- No --> Z
    E -- Yes --> F[Fetch paywallInfo from VC]
    F --> G{paywallInfo.isFreeTrialAvailable?}
    G -- No --> Z
    G -- Yes --> H[Create StoreProduct.blank with redemptionProduct.identifier]
    H --> I[await superwall.track FreeTrialStart event]
    I --> J[Filter localNotifications for .trialStarted]
    J --> K[scheduleNotifications]
Loading

Comments Outside Diff (2)

  1. Tests/SuperwallKitTests/Web/WebEntitlementRedeemerTests.swift, line 1253-1261 (link)

    Missing assertion for freeTrialStart delegate event

    The core new behaviour introduced in this PR — dispatching a .freeTrialStart event to the delegate via superwall.track(InternalSuperwallEvent.FreeTrialStart(...)) — is never verified by any test. MockSuperwallDelegate already records all dispatched events in eventsReceived, so checking for the event is straightforward.

    Positive test (testRedeem_withFreeTrial_schedulesNotification): should additionally assert that .freeTrialStart was received:

    // Verify freeTrialStart event was dispatched
    #expect(mockDelegate.eventsReceived.contains {
      if case .freeTrialStart = $0 { return true }
      return false
    })

    Negative tests (testRedeem_withTrialDaysButNotEligible_doesNotScheduleNotification at line ~1611, and testRedeem_withEligibleButNoTrialDays_doesNotScheduleNotification at line ~1785): should assert the event was not fired to confirm the guard conditions are actually preventing the delegate call, not just the notification scheduling:

    // Verify freeTrialStart was NOT dispatched
    #expect(!mockDelegate.eventsReceived.contains {
      if case .freeTrialStart = $0 { return true }
      return false
    })

    Without these assertions, the tests only validate that NotificationSchedulerMock wasn't called — but the actual track(FreeTrialStart(...)) call could fire (or silently fail) without any test catching it.

  2. Sources/SuperwallKit/Web/WebEntitlementRedeemer.swift, line 250-253 (link)

    Abandon fix not reflected in CHANGELOG

    The change from product: nil to product: product on this line is a meaningful bug fix — previously the Transaction(.abandon) event sent to analytics was missing product information despite already embedding the product in the state enum. This fix is bundled into the PR but isn't mentioned in CHANGELOG.md (the entry only references trial eligibility and freeTrial_start). Worth adding a separate bullet so consumers can understand the full scope of the change.

Last reviewed commit: 02acd97

…duling

- Fire `FreeTrialStart` event when a Stripe purchase completes and
  `isFreeTrialAvailable` is true, so the delegate receives
  `.freeTrialStart` for Stripe purchases (previously only fired for
  App Store purchases)
- Fix notification scheduling to check `paywallInfo.isFreeTrialAvailable`
  instead of `product.trialPeriodDays > 0`, so notifications respect
  the actual trial eligibility that was sent to Stripe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
yusuftor and others added 2 commits March 10, 2026 17:12
isFreeTrialAvailable is paywall-level, so also verify the specific
purchased product has trialPeriodDays > 0 to avoid false positives
when another product on the paywall has a trial.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
yusuftor and others added 2 commits March 10, 2026 17:18
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
yusuftor and others added 4 commits March 10, 2026 17:33
Fixes mismatch where productIds.first could refer to a different
product than what the user actually purchased on a multi-product paywall.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix existing trial notification test to set isFreeTrialAvailable = true
- Add test: trialPeriodDays > 0 but isFreeTrialAvailable = false → no notification
- Add test: isFreeTrialAvailable = true but trialPeriodDays = 0 → no notification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies that the freeTrialStart event is dispatched to the delegate
in the positive case and not dispatched in the three negative cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yusuftor yusuftor merged commit 1e3383c into develop Mar 10, 2026
4 checks passed
@yusuftor yusuftor deleted the fix/stripe-free-trial-tracking branch March 10, 2026 17:07
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.

1 participant