Skip to content

Commit 4e94847

Browse files
authored
docs(references): add Lottie animations guide (#10)
* docs(references): add Lottie animations guide * fix(docs): address Lottie review feedback
1 parent b590870 commit 4e94847

8 files changed

Lines changed: 182 additions & 4 deletions

File tree

.github/workflows/validate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
- name: Check plugin folder links
7676
run: |
7777
for f in \
78-
purchasely/skills/integrate/SKILL.md \
78+
purchasely/skills/purchasely-integrate/SKILL.md \
7979
purchasely/references/android/initialization.md \
8080
purchasely/commands/integrate.md \
8181
purchasely/agents/sdk-expert.md \

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project are documented here. The format is based on
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- `references/concepts/lottie-animations.md` — covers Purchasely Screen Lottie setup with the iOS `PLYLottieBridge`, Android `PLYLottieInterface` / `Purchasely.lottieView`, cross-platform host-project notes, and troubleshooting for missing bridges, file size, and Console template availability.
10+
711
## [1.1.0] — 2026-05-25
812

913
### Changed

purchasely/agents/sdk-expert.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Use these signatures when answering exact-code questions. If a platform is not l
6565
14. **Privacy consent**: SDK 5.4.0+ exposes `revokeDataProcessingConsent(...)`. Do not claim legal-basis/revocation APIs are unavailable; load `privacy-settings.md`.
6666
15. **Cordova signatures**: Cordova uses positional callbacks. Verify `references/cordova/integration.md` before writing Cordova code.
6767
16. **Campaigns are SDK-managed, not app-coded**: A Console **Campaign** with an event trigger (e.g. `APP_STARTED`) is displayed **automatically by the SDK** — the app does NOT call `fetchPresentation` / `presentPresentation` for it. The only required code is `Purchasely.readyToOpenDeeplink(true)` once the splash/onboarding is ready. Do not conflate Campaigns with Placements: a Campaign can be triggered by an event, by a placement, or both. Trigger-based delivery is automatic; placement-based delivery overrides the placement's default screen when the app calls `fetchPresentation(placementId)`. See `references/concepts/campaigns.md`.
68+
17. **Lottie is a weak native dependency**: Screen Composer Lottie blocks require Airbnb Lottie plus a Purchasely bridge in the app. iOS needs `@objc(PLYLottieBridge)`; Android needs `PLYLottieInterface` and `Purchasely.lottieView = { ... }`. React Native / Flutter / Cordova apps configure the underlying native host projects. See `references/concepts/lottie-animations.md`.
6869

6970
## Support-Derived Known Issues / Fixes
7071

@@ -99,6 +100,7 @@ If the bundled reference is missing a detail, looks stale, or the question depen
99100
- `subscription-management.md` — Manage Subscription native page
100101
- `promotional-offers.md` — offer types, Apple promo, Google dev-determined, offer codes, win-back
101102
- `campaigns.md` — no-code Console automations, `readyToOpenDeeplink`, SDK ≥ 5.1.0
103+
- `lottie-animations.md` — Lottie weak dependency bridge for iOS / Android native rendering
102104
- `analytics-integration.md` — server-side + client-side event forwarding, analytics wrapper
103105

104106
**Platform-specific** — load the one matching the user's platform:
@@ -151,4 +153,5 @@ Use `Glob` and `Read` tools to access these files when you need precise API sign
151153
- **Both** → Campaign fires on trigger AND can override on placement; the SDK handles routing.
152154
Always mention `readyToOpenDeeplink(true)` for trigger-based, the `CAMPAIGN_TRIGGERED` / `CAMPAIGN_DISPLAYED` / `CAMPAIGN_NOT_DISPLAYED` analytics events, and SDK ≥ 5.1.0. Never claim "the campaign activates automatically through `fetchPresentation`" — that conflates the two delivery modes.
153155
13. **For BYOS / "show my own screen inside a paywall or Flow" / "native login step inside a Flow" / "embed my legacy paywall as a Purchasely variant"**: load `references/concepts/byos.md` FIRST. Confirm SDK ≥ 5.6.0 and the platform is iOS (Swift/SwiftUI) or Android (Kotlin) — BYOS is **not** available on React Native, Flutter, or Cordova yet. Steer users away from anti-patterns (presenting their own VC over the Purchasely controller, calling `Purchasely.close()` then pushing their screen, skipping `display()`). The supported path is: Console creates a Screen with layout `Bring Your Own Screen` and connections; the app sets `Purchasely.setCustomScreenViewControllerDelegate(...)` / `setCustomScreenViewDelegate(...)` (iOS) or `setCustomScreenProvider(...)` (Android); when the user finishes the step, the app calls `presentation.executeConnection(...)` / `presentation.execute(connection)` with the matching `PLYConnection`. Remind callers to `Purchasely.synchronize()` after any purchase performed inside a Custom Screen (especially for A/B and A/A tests).
154-
14. **For Console-driven questions** — campaigns, audiences, A/B tests, placement configuration, Screen Composer, scheduling, capping, Flows, surveys, or anything the user configures in the Purchasely Console rather than in code: the local references are the fast path but Console behavior evolves quickly. BEFORE answering, fetch the current official doc with `ctx_fetch_and_index(url: "https://docs.purchasely.com/docs/<topic>", source: "purchasely-<topic>-doc")` then `ctx_search(...)` against it, and reconcile with the bundled reference. Useful entry points: `https://docs.purchasely.com/llms.txt` (full index for AI agents), `/docs/campaigns`, `/docs/campaign-configuration`, `/docs/campaigns-implementation`, `/docs/audiences`, `/docs/ab-tests`, `/docs/displaying-screens-placements`, `/docs/screens`, `/docs/flows`. If the doc fetch and the local reference disagree, trust the online doc and flag the discrepancy in your answer so the reference can be updated.
156+
14. **For Lottie questions** — any mention of *Lottie / animation JSON / animated paywall / blank animation / static animation*: load `references/concepts/lottie-animations.md` FIRST. Explain the weak dependency model and give native setup for the target platform: iOS `@objc(PLYLottieBridge)` + `lottie-ios`; Android `PLYLottieInterface` + `Purchasely.lottieView`; React Native / Flutter / Cordova require the same setup in their iOS/Android host projects. Mention the 2 MB JSON guidance and Console template availability.
157+
15. **For Console-driven questions** — campaigns, audiences, A/B tests, placement configuration, Screen Composer, scheduling, capping, Flows, surveys, Lottie blocks, or anything the user configures in the Purchasely Console rather than in code: the local references are the fast path but Console behavior evolves quickly. BEFORE answering, fetch the current official doc with `ctx_fetch_and_index(url: "https://docs.purchasely.com/docs/<topic>", source: "purchasely-<topic>-doc")` then `ctx_search(...)` against it, and reconcile with the bundled reference. Useful entry points: `https://docs.purchasely.com/llms.txt` (full index for AI agents), `/docs/campaigns`, `/docs/campaign-configuration`, `/docs/campaigns-implementation`, `/docs/lottie-animations`, `/docs/audiences`, `/docs/ab-tests`, `/docs/displaying-screens-placements`, `/docs/screens`, `/docs/flows`. If the doc fetch and the local reference disagree, trust the online doc and flag the discrepancy in your answer so the reference can be updated.

purchasely/references/concepts/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ When a topic also has a deeper platform-specific take (e.g. SwiftUI lifecycle, J
1414
| [paywall-actions.md](paywall-actions.md) | `PLYPresentationAction` enum + interceptor `proceed/processAction` rules + chaining multiple actions on a single button (purchase + open_screen / open_placement / deeplink) |
1515
| [presentation-types.md](presentation-types.md) | `PLYPresentationType` enum (NORMAL / FALLBACK / DEACTIVATED / CLIENT) guard |
1616
| [byos.md](byos.md) | Bring Your Own Screen — embed native screens (login, custom forms, legacy paywall) inside a Flow; iOS + Android only, SDK ≥ 5.6.0 |
17+
| [lottie-animations.md](lottie-animations.md) | Lottie animations in Purchasely Screens — weak dependency bridge for iOS / Android native rendering |
1718
| [presentation-cache.md](presentation-cache.md) | App-side caching + preload pattern (avoid `FlowsManager.flowSteps` accumulation) |
1819
| [observer-mode-post-purchase.md](observer-mode-post-purchase.md) | `proceed → dismiss` ordering, native vs bridge close APIs, chaining follow-up placements |
1920
| [user-attributes-targeting.md](user-attributes-targeting.md) | Setting user attributes for audience targeting + GDPR consent |
@@ -41,5 +42,6 @@ When a topic also has a deeper platform-specific take (e.g. SwiftUI lifecycle, J
4142
| Improving paywall perceived performance | `presentation-cache.md` (preload pattern) |
4243
| Debugging stuck paywalls / blank presentations | `presentation-types.md`, `presentation-cache.md`, `paywall-actions.md` |
4344
| Embedding a native login / custom form / legacy paywall inside a Flow | `byos.md` (iOS + Android, SDK ≥ 5.6.0) |
45+
| Adding or debugging Lottie animations in Purchasely Screens | `lottie-animations.md` (iOS / Android native bridge; cross-platform apps configure host projects) |
4446
| Configuring multi-step buttons (purchase + next step, purchase + placement) | `paywall-actions.md` § Chaining multiple actions |
4547
| Reviewing an existing integration | all of the above |
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Lottie Animations — Universal Concept
2+
3+
Applies to: **iOS and Android native paywall rendering**. React Native, Flutter and Cordova apps must configure the underlying iOS / Android host projects if their Purchasely Screens use Lottie blocks.
4+
5+
Purchasely supports Lottie animations in Screens, but Lottie is a **weak dependency**: the SDK does not bundle Airbnb Lottie to keep the SDK lightweight. If a Screen contains a Lottie block, the app must provide both:
6+
7+
1. the Lottie dependency in the app, and
8+
2. a small bridge/interface with the stable methods Purchasely calls at runtime.
9+
10+
Official doc: <https://docs.purchasely.com/docs/lottie-animations>
11+
12+
## Console availability
13+
14+
The Lottie option is added template by template in the Purchasely Console. If a team needs Lottie in a template where the option is not visible, contact Purchasely Support rather than trying to fix it in app code.
15+
16+
## Runtime behavior
17+
18+
At runtime the SDK detects Lottie support and inserts the animation view into the paywall. The SDK can configure:
19+
20+
- animation URL,
21+
- repeat vs play once,
22+
- aspect behavior (`fill` / `fit`).
23+
24+
If the bridge is missing, the app can display the paywall but the Lottie animation will not render correctly.
25+
26+
## iOS setup
27+
28+
Add Airbnb Lottie to the app if it is not already present, for example via CocoaPods or SPM (`lottie-ios`, module `Lottie`). Then add a Swift class named `PLYLottieBridge` and expose it to Objective-C with `@objc(PLYLottieBridge)`.
29+
30+
```swift
31+
import UIKit
32+
import Lottie
33+
34+
@objc(PLYLottieBridge)
35+
class PLYLottieBridge: NSObject {
36+
var animationView: LottieAnimationView?
37+
38+
@objc class func bridge(with animationURL: URL) -> PLYLottieBridge? {
39+
let result = PLYLottieBridge()
40+
result.animationView = LottieAnimationView(url: animationURL, closure: { [weak result] _ in
41+
result?.animationView?.play()
42+
}, animationCache: nil)
43+
result.animationView?.loopMode = .loop
44+
return result
45+
}
46+
47+
@objc func view() -> UIView? {
48+
return animationView
49+
}
50+
51+
@objc func loop(_ loop: Bool) {
52+
animationView?.loopMode = loop ? .loop : .playOnce
53+
}
54+
55+
@objc func fill(_ fill: Bool) {
56+
animationView?.contentMode = fill ? .scaleAspectFill : .scaleAspectFit
57+
}
58+
59+
@objc func play() {
60+
animationView?.play { _ in }
61+
}
62+
63+
@objc func pause() {
64+
animationView?.pause()
65+
}
66+
67+
@objc func stop() {
68+
animationView?.stop()
69+
}
70+
}
71+
```
72+
73+
Notes:
74+
75+
- Keep the Objective-C name exactly `PLYLottieBridge`; the SDK looks for that stable bridge.
76+
- If the app already has Lottie, reuse its pinned version unless there is a known incompatibility.
77+
78+
## Android setup
79+
80+
Android support requires Purchasely SDK **3.6.0+**; all current 5.x SDKs qualify. Add Airbnb Lottie to the app if needed:
81+
82+
```kotlin
83+
dependencies {
84+
implementation("com.airbnb.android:lottie:<app-approved-version>")
85+
}
86+
```
87+
88+
Create a view implementing `PLYLottieInterface`:
89+
90+
```kotlin
91+
import android.animation.ValueAnimator
92+
import android.content.Context
93+
import android.util.Log
94+
import android.widget.ImageView
95+
import androidx.annotation.Keep
96+
import com.airbnb.lottie.LottieAnimationView
97+
import com.airbnb.lottie.LottieDrawable
98+
import io.purchasely.views.presentation.interfaces.PLYLottieInterface
99+
100+
private const val TAG = "PLYLottie"
101+
102+
@Keep
103+
class AnimationView(context: Context) : LottieAnimationView(context), PLYLottieInterface {
104+
override fun setup(url: String, repeat: Boolean, scaleType: ImageView.ScaleType) {
105+
setAnimationFromUrl(url)
106+
enableMergePathsForKitKatAndAbove(true)
107+
repeatMode = LottieDrawable.RESTART
108+
repeatCount = if (repeat) ValueAnimator.INFINITE else 0
109+
this.scaleType = scaleType
110+
setFailureListener { error ->
111+
Log.e(TAG, "Unable to load Lottie animation", error)
112+
}
113+
play()
114+
}
115+
116+
override fun play() {
117+
playAnimation()
118+
}
119+
120+
override fun stop() {
121+
cancelAnimation()
122+
}
123+
}
124+
```
125+
126+
Register the factory during app initialization, before displaying paywalls:
127+
128+
```kotlin
129+
Purchasely.lottieView = { context -> AnimationView(context) }
130+
```
131+
132+
## Cross-platform apps
133+
134+
React Native, Flutter and Cordova paywalls are rendered by the native iOS / Android Purchasely SDKs. If a cross-platform app uses Screens with Lottie:
135+
136+
- add the iOS `PLYLottieBridge` + Lottie dependency in the iOS host project;
137+
- add the Android `PLYLottieInterface` implementation + `Purchasely.lottieView` factory in the Android host project;
138+
- keep the cross-platform Purchasely packages aligned with `../sdk-versions.md` as usual.
139+
140+
## Troubleshooting
141+
142+
| Symptom | Check |
143+
|---------|-------|
144+
| Lottie area is blank / static | Native bridge missing or not loaded before the paywall display. |
145+
| Crash or error while loading animation | Add Android `setFailureListener`, check device logs, verify the remote JSON URL is reachable. |
146+
| Animation works in preview but not in app | Confirm the app includes Lottie and the bridge on the target platform. |
147+
| Animation renders badly or not at all | Test the file in LottieFiles Preview and check for unsupported expressions/effects. |
148+
| Memory / rendering issues | Keep Lottie JSON under **2 MB**; larger files can fail or cause memory pressure. |
149+
| Lottie option missing in Console | Feature is enabled template-by-template; ask Purchasely Support for that template. |
150+
151+
## See also
152+
153+
- [screen-issue-report.md](../troubleshooting/screen-issue-report.md) — package a reproducible Composer issue for Support
154+
- [debug-mode.md](../troubleshooting/debug-mode.md) — preview draft Screens on device
155+
- [presentation-types.md](presentation-types.md) — guard `NORMAL` / `FALLBACK` / `DEACTIVATED` before display

purchasely/skills/purchasely-debug/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The bundled references are intentionally curated, not a full copy of the public
2828
- `../../references/concepts/subscription-management.md` — "user cancelled but the app doesn't reflect it" (foreground resync)
2929
- `../../references/concepts/promotional-offers.md` — promo offer not applied / charged at regular price / `invalidOfferSignature`
3030
- `../../references/concepts/campaigns.md` — trigger-based campaigns silently don't fire (missing `readyToOpenDeeplink`)
31+
- `../../references/concepts/lottie-animations.md` — blank/static Lottie blocks, missing native bridge/dependency, oversized animation JSON
3132
- `../../references/concepts/analytics-integration.md` — events fire but don't reach Firebase/Amplitude/AppsFlyer (or duplicate)
3233
- `../../references/architecture-patterns.md` — for projects using a wrapper class, diagnose wrapper-side issues (init order, decoupled Observer billing)
3334
- `../../references/sdk-versions.md` — minimum versions for APIs (e.g. `closeAllScreens()`, Campaigns ≥ 5.1.0, promo offers ≥ 4.0.0)
@@ -190,6 +191,7 @@ When you identify one of these patterns, apply the known fix immediately:
190191
| RN/Flutter/Cordova: same stuck-paywall / repeated-fetch issue as iOS above | Same SDK quirk — the cross-platform bridge calls native fetch every time and has no shared cache | Apply the universal cache pattern from `../../references/concepts/presentation-cache.md` (skeleton implementations included for RN, Flutter, Cordova) |
191192
| RN/Flutter/Cordova: `closeAllScreens()` not exposed on JS/Dart side | Expected public bridge API mismatch | Use `closePresentation()` on the public JS/Dart side. `closeAllScreens()` is the native iOS/Android method unless the app has added a custom bridge |
192193
| RN/Flutter/Cordova: Flow paywall opens but cannot be closed (no X, no step transitions) | App uses the shorthand `Purchasely.presentPresentationForPlacement(...)`. On plugin ≤ 5.7.x the cross-platform bridge does not branch on Flow — Flutter routes Flows through `PLYProductActivity` / `showController(_, type: .productPage)`, bypassing `presentation.display()`. The Flow manager never owns the window, so close affordance and step navigation are absent. `presentPresentationForPlacement` itself remains valid for simple non-Flow paywalls; the bug only surfaces when a Flow is assigned to the placement | Switch to the doc-recommended path: `fetchPresentation(placementId)``presentPresentation(presentation)`. The `presentPresentation` bridge correctly checks `isFlow` / `flowId != null` and calls native `display()`. See https://docs.purchasely.com/docs/general-in-app-experiences-display#how-to-display-an-in-app-experience-associated-to-a-placement |
194+
| Lottie block is blank, static, or crashes while loading | Lottie is a weak dependency: the app is missing Airbnb Lottie, the iOS `PLYLottieBridge`, the Android `PLYLottieInterface` / `Purchasely.lottieView` registration, or the JSON is too large/unsupported | Add the native bridge and dependency from `../../references/concepts/lottie-animations.md`; keep JSON under 2 MB and validate it in LottieFiles Preview |
193195
| RN/Flutter/Cordova: native crash on init or missing API | Plugin packages out of alignment (e.g. `react-native-purchasely 5.7.3` + `@purchasely/react-native-purchasely-google 5.6.0`) | Pin all plugin packages to the same `5.7.3`. See `../../references/sdk-versions.md` |
194196

195197
## Step 5: Escalate to Purchasely Support (when the root cause is in the Screen / Console)

0 commit comments

Comments
 (0)