From 0db68ac81a431acca77e800dfc40644218a12141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:18:43 +0200 Subject: [PATCH 1/7] Fix cold start deep link handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add deep link caching in Superwall class for links received before SDK configuration - Implement _processPendingDeepLink() to handle cached links after configuration completes - Add handleInitialDeepLink() helper method for cold start scenarios - Update example app to handle initial deep links using appLinks.getInitialLink() - Move deep link setup before SDK configuration to prevent race conditions - Improve logging with more descriptive messages for debugging Fixes issue where deep links on cold start were not consistently triggering paywalls due to timing issues between app initialization and SDK configuration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- example/lib/main.dart | 16 +++++++++++++-- lib/src/public/Superwall.dart | 37 +++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5af8f1c..274e058 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -29,6 +29,8 @@ class _MyAppState extends State { const useRevenueCat = true; super.initState(); + _handleIncomingLinks(); // Set up deep link handling before configuration + _handleInitialLink(); // Handle cold start deep links configureSuperwall(useRevenueCat); } @@ -40,13 +42,24 @@ class _MyAppState extends State { void _handleIncomingLinks() { appLinks.uriLinkStream.listen((Uri uri) { - debugPrint('uri: $uri'); + debugPrint('Incoming deep link: $uri'); Superwall.shared.handleDeepLink(uri); }, onError: (Object err) { print('Error receiving incoming link: $err'); }); } + void _handleInitialLink() { + appLinks.getInitialLink().then((Uri? initialUri) { + if (initialUri != null) { + debugPrint('Initial deep link (cold start): $initialUri'); + Superwall.handleInitialDeepLink(initialUri); + } + }).catchError((Object err) { + print('Error getting initial link: $err'); + }); + } + @override void dispose() { _subscription?.cancel(); @@ -84,7 +97,6 @@ class _MyAppState extends State { print('Executing Superwall configure completion block'); listenForPurchases(); }); - _handleIncomingLinks(); Superwall.shared.setDelegate(delegate); // MARK: Step 3 – Configure RevenueCat and Sync Subscription Status /// Always configure RevenueCat after Superwall and keep Superwall's diff --git a/lib/src/public/Superwall.dart b/lib/src/public/Superwall.dart index f9a9308..3dc45ae 100644 --- a/lib/src/public/Superwall.dart +++ b/lib/src/public/Superwall.dart @@ -20,6 +20,10 @@ class Superwall { static final generated.PSuperwallHostApi hostApi = generated.PSuperwallHostApi(); + // Deep link caching for cold start scenarios + static Uri? _pendingDeepLink; + static bool _isConfigured = false; + // Private constructor Superwall._(); @@ -46,8 +50,17 @@ class Superwall { final completionHost = completion != null ? generated.PConfigureCompletionHost() : null; - final completionProxy = completion != null - ? ConfigureCompletionProxy.register(completion) + // Wrap the original completion to handle pending deep links + final wrappedCompletion = () { + _isConfigured = true; + _processPendingDeepLink(); + if (completion != null) { + completion(); + } + }; + + final completionProxy = generated.PConfigureCompletionHost() != null + ? ConfigureCompletionProxy.register(wrappedCompletion) : null; hostApi.configure(apiKey, @@ -351,9 +364,29 @@ class Superwall { // Handles deep links for paywall previews Future handleDeepLink(Uri url) async { + if (!_isConfigured) { + // Cache the deep link if SDK is not yet configured + _pendingDeepLink = url; + return false; + } return await hostApi.handleDeepLink(url.toString()); } + // Process any pending deep link after configuration completes + static void _processPendingDeepLink() { + if (_pendingDeepLink != null) { + shared.handleDeepLink(_pendingDeepLink!); + _pendingDeepLink = null; + } + } + + // Helper method for apps to handle initial deep links on cold start + static Future handleInitialDeepLink(Uri? initialUri) async { + if (initialUri != null) { + await shared.handleDeepLink(initialUri); + } + } + // Toggles the paywall loading spinner Future togglePaywallSpinner(bool isHidden) async { await hostApi.togglePaywallSpinner(isHidden); From 3fd8ec1b91287a86c1adf3addd6fb5ba4a7134d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:31:33 +0200 Subject: [PATCH 2/7] Simplify deep link fix by leveraging native iOS static method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove custom caching mechanism in favor of native iOS SDK queuing - iOS SDK already has static Superwall.handleDeepLink() that queues links before configure - Android uses instance method but keeps existing behavior - Keep existing instance method API for backward compatibility - Add documentation about cross-platform behavior differences The iOS native SDK already handles queuing deep links received before configuration via the static handleDeepLink method and DeepLinkRouter.storeDeepLink(). This is much cleaner than implementing our own caching layer. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 8 ++ CLAUDE.md | 134 ++++++++++++++++++++++++++++++++++ example/lib/main.dart | 3 +- lib/src/public/Superwall.dart | 39 +--------- 4 files changed, 148 insertions(+), 36 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..22dc696 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:superwall.com)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3e20223 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the Superwall Flutter SDK - a Flutter plugin that wraps native iOS and Android SDKs for paywall management and subscription handling. The project uses **Pigeon** for type-safe communication between Flutter and native platforms. + +## Key Architecture + +### Pigeon-Based Communication +- **Single Source of Truth**: `pigeons/configure.dart` defines all data models and API interfaces +- **Code Generation**: Pigeon generates type-safe interfaces for Dart, Kotlin (Android), and Swift (iOS) +- **Communication Flow**: Flutter ↔ Generated Interface ↔ Method Channel ↔ Native Implementation ↔ Native SDK + +### Core Components +- **Flutter Layer**: `lib/src/public/Superwall.dart` - Main SDK class, singleton pattern +- **Generated Code**: `lib/src/generated/superwallhost.g.dart` - Auto-generated from Pigeon +- **Native Hosts**: + - Android: `android/src/main/kotlin/.../SuperwallHost.kt` + - iOS: `ios/Classes/SuperwallHost.swift` +- **Proxy Classes**: `lib/src/private/*Proxy.dart` - Handle callback routing between Flutter and native + +### Data Flow Patterns +- **Host API**: Native-to-Flutter calls (e.g., `PSuperwallHostApi`) +- **Flutter API**: Flutter-to-Native calls (e.g., `PSuperwallDelegateGenerated`) +- **Event Streams**: For continuous updates (e.g., `SubscriptionStatusStream`) + +## Development Commands + +### Code Generation +Generate Pigeon interfaces after modifying `pigeons/configure.dart`: +```bash +flutter pub run pigeon --input pigeons/configure.dart +``` + +### Testing +Run unit tests: +```bash +flutter test +``` + +Run integration tests (non-UI methods): +```bash +cd test_app +flutter test integration_test/sdk_methods_test.dart +``` + +Run UI tests with Maestro: +```bash +# iOS +./run_tests.sh ios + +# Android +./run_tests.sh android +``` + +Individual Maestro test: +```bash +# iOS +maestro test -e PLATFORM_ID=com.superwall.Advanced test_app/maestro/handler/flow.yaml + +# Android +maestro test -e PLATFORM_ID=com.superwall.superapp test_app/maestro/handler/flow.yaml +``` + +### Build Commands +Build example app: +```bash +cd example +flutter build ios --no-codesign # iOS +flutter build apk # Android +``` + +### Analysis and Linting +```bash +flutter analyze +dart format . +``` + +## Adding New Methods + +1. **Define in Pigeon**: Add method and any new data classes to `pigeons/configure.dart` + - Prefix Pigeon classes with `P` (e.g., `PPaywallInfo`) + - Use sealed classes for polymorphic types +2. **Generate Code**: Run `flutter pub run pigeon --input pigeons/configure.dart` +3. **Implement Native**: Add implementations in `SuperwallHost.kt` and `SuperwallHost.swift` +4. **Flutter Integration**: Add public method in `lib/src/public/Superwall.dart` +5. **Mapping**: Create mapping functions between public classes and Pigeon classes + +## Code Conventions + +### File Organization +- **Public API**: `lib/src/public/` - Classes exposed to SDK users +- **Private API**: `lib/src/private/` - Internal implementation details +- **Generated**: `lib/src/generated/` - Auto-generated Pigeon code (do not edit) + +### Naming Patterns +- **Pigeon Classes**: Prefixed with `P` (e.g., `PSuperwallOptions`) +- **Public Classes**: No prefix (e.g., `SuperwallOptions`) +- **Proxy Classes**: Suffixed with `Proxy` (e.g., `PurchaseControllerProxy`) + +### Data Mapping +- Always map between public SDK classes and Pigeon classes +- Use helper methods like `_convertToGeneratedOptions()` in `Superwall.dart` +- Handle sealed class conversions with switch statements + +## Testing Structure + +### Integration Tests +- Location: `test_app/integration_test/sdk_methods_test.dart` +- Purpose: Test SDK methods without UI dependencies +- Run with: `flutter test integration_test/sdk_methods_test.dart` + +### UI Tests (Maestro) +- Location: `test_app/maestro/` +- Test flows: handler, delegate, purchase controller +- Platform-specific app IDs: + - iOS: `com.superwall.Advanced` + - Android: `com.superwall.superapp` + +## Project Structure Notes + +- **Multiple Example Apps**: Both `example/` and `test_app/` serve different testing purposes +- **Platform-Specific Code**: Native implementations in `android/` and `ios/` directories +- **Shared Configuration**: `analysis_options.yaml` enables inline-class experiment +- **Asset Management**: `pubspec.yaml` included as asset for SDK functionality + +## Important Files + +- `pigeons/configure.dart` - API definition and data models +- `lib/src/public/Superwall.dart` - Main SDK entry point +- `CONTRIBUTING.md` - Detailed architecture documentation +- `run_tests.sh` - Automated testing script for both platforms \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 274e058..c0e52ea 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -53,7 +53,8 @@ class _MyAppState extends State { appLinks.getInitialLink().then((Uri? initialUri) { if (initialUri != null) { debugPrint('Initial deep link (cold start): $initialUri'); - Superwall.handleInitialDeepLink(initialUri); + // Safe to call before configure() - iOS SDK queues the link automatically + Superwall.shared.handleDeepLink(initialUri); } }).catchError((Object err) { print('Error getting initial link: $err'); diff --git a/lib/src/public/Superwall.dart b/lib/src/public/Superwall.dart index 3dc45ae..3408faf 100644 --- a/lib/src/public/Superwall.dart +++ b/lib/src/public/Superwall.dart @@ -20,9 +20,6 @@ class Superwall { static final generated.PSuperwallHostApi hostApi = generated.PSuperwallHostApi(); - // Deep link caching for cold start scenarios - static Uri? _pendingDeepLink; - static bool _isConfigured = false; // Private constructor Superwall._(); @@ -50,17 +47,8 @@ class Superwall { final completionHost = completion != null ? generated.PConfigureCompletionHost() : null; - // Wrap the original completion to handle pending deep links - final wrappedCompletion = () { - _isConfigured = true; - _processPendingDeepLink(); - if (completion != null) { - completion(); - } - }; - - final completionProxy = generated.PConfigureCompletionHost() != null - ? ConfigureCompletionProxy.register(wrappedCompletion) + final completionProxy = completion != null + ? ConfigureCompletionProxy.register(completion) : null; hostApi.configure(apiKey, @@ -362,31 +350,12 @@ class Superwall { await hostApi.preloadPaywallsForPlacements(placementNames.toList()); } - // Handles deep links for paywall previews + // Instance method - handles deep links for paywall previews + // Safe to call before configure() - iOS SDK will queue links, Android will attempt immediate processing Future handleDeepLink(Uri url) async { - if (!_isConfigured) { - // Cache the deep link if SDK is not yet configured - _pendingDeepLink = url; - return false; - } return await hostApi.handleDeepLink(url.toString()); } - // Process any pending deep link after configuration completes - static void _processPendingDeepLink() { - if (_pendingDeepLink != null) { - shared.handleDeepLink(_pendingDeepLink!); - _pendingDeepLink = null; - } - } - - // Helper method for apps to handle initial deep links on cold start - static Future handleInitialDeepLink(Uri? initialUri) async { - if (initialUri != null) { - await shared.handleDeepLink(initialUri); - } - } - // Toggles the paywall loading spinner Future togglePaywallSpinner(bool isHidden) async { await hostApi.togglePaywallSpinner(isHidden); From b56be7eafc87174c1c5bbb98426d2eef52abae90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:44:46 +0200 Subject: [PATCH 3/7] Fix cold start deep link handling using existing API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Keep existing Superwall.shared.handleDeepLink() API for consistency - iOS implementation now uses static Superwall.handleDeepLink() that safely queues links before configure - Android keeps existing instance method behavior - Example app uses getInitialLink() for cold start and handles before configure - Move deep link setup before SDK configuration to prevent timing issues The key insight is that iOS already has proper queuing via the static method, so we just need to ensure the Flutter wrapper uses it correctly. Much cleaner than adding new APIs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 7 +++- example/lib/main.dart | 2 +- example/pubspec.lock | 57 +++++++++++++-------------------- ios/Classes/SuperwallHost.swift | 1 + lib/src/public/Superwall.dart | 4 +-- 5 files changed, 32 insertions(+), 39 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 22dc696..38d6802 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,12 @@ { "permissions": { "allow": [ - "WebFetch(domain:superwall.com)" + "WebFetch(domain:superwall.com)", + "Bash(gh pr edit:*)", + "Bash(flutter pub run pigeon:*)", + "Bash(flutter analyze:*)", + "Bash(flutter build:*)", + "Bash(dart run pigeon:*)" ], "deny": [] } diff --git a/example/lib/main.dart b/example/lib/main.dart index c0e52ea..50eb48e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -53,7 +53,7 @@ class _MyAppState extends State { appLinks.getInitialLink().then((Uri? initialUri) { if (initialUri != null) { debugPrint('Initial deep link (cold start): $initialUri'); - // Safe to call before configure() - iOS SDK queues the link automatically + // Safe to call before configure() - iOS uses static method that queues links Superwall.shared.handleDeepLink(initialUri); } }).catchError((Object err) { diff --git a/example/pubspec.lock b/example/pubspec.lock index 5046cde..ffca505 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,23 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "72.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "85.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: f648ac7103c7128c367c99c6684869bf47de4261afcef31f9c70c0f793b21f59 url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "7.5.3" app_links: dependency: "direct main" description: @@ -122,10 +117,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -272,18 +267,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -300,14 +295,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -400,7 +387,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -421,10 +408,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -437,17 +424,17 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" superwallkit_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "2.2.0-beta.2" + version: "2.3.4" sync_http: dependency: transitive description: @@ -468,10 +455,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" typed_data: dependency: transitive description: @@ -500,10 +487,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" watcher: dependency: transitive description: @@ -524,10 +511,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" yaml: dependency: transitive description: diff --git a/ios/Classes/SuperwallHost.swift b/ios/Classes/SuperwallHost.swift index 15260de..2260a50 100644 --- a/ios/Classes/SuperwallHost.swift +++ b/ios/Classes/SuperwallHost.swift @@ -237,6 +237,7 @@ final class SuperwallHost : NSObject, PSuperwallHostApi { func handleDeepLink(url: String) -> Bool { guard let url = URL(string: url) else { return false } + // Use static method that safely queues deep links before configuration return Superwall.handleDeepLink(url) } diff --git a/lib/src/public/Superwall.dart b/lib/src/public/Superwall.dart index 3408faf..8a41dea 100644 --- a/lib/src/public/Superwall.dart +++ b/lib/src/public/Superwall.dart @@ -350,8 +350,8 @@ class Superwall { await hostApi.preloadPaywallsForPlacements(placementNames.toList()); } - // Instance method - handles deep links for paywall previews - // Safe to call before configure() - iOS SDK will queue links, Android will attempt immediate processing + // Handles deep links for paywall previews + // Safe to call before configure() - iOS uses static method that queues links, Android attempts immediate processing Future handleDeepLink(Uri url) async { return await hostApi.handleDeepLink(url.toString()); } From 1429321a4bae6696bd38db3fc29f175be6eb9a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:48:13 +0200 Subject: [PATCH 4/7] Make handleDeepLink a static method for safe cold start usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert handleDeepLink to static method that bypasses shared instance - Safe to call before configure() as it directly uses hostApi without instance dependency - Update example app to use static Superwall.handleDeepLink() consistently - iOS: Uses static Superwall.handleDeepLink() that queues links before configuration - Android: Attempts immediate processing, gracefully handles if SDK not ready This provides a clean API where Superwall.handleDeepLink() can be safely called before configure() without worrying about shared instance initialization. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- example/lib/main.dart | 6 +++--- lib/src/public/Superwall.dart | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 50eb48e..98c047c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -43,7 +43,7 @@ class _MyAppState extends State { void _handleIncomingLinks() { appLinks.uriLinkStream.listen((Uri uri) { debugPrint('Incoming deep link: $uri'); - Superwall.shared.handleDeepLink(uri); + Superwall.handleDeepLink(uri); }, onError: (Object err) { print('Error receiving incoming link: $err'); }); @@ -53,8 +53,8 @@ class _MyAppState extends State { appLinks.getInitialLink().then((Uri? initialUri) { if (initialUri != null) { debugPrint('Initial deep link (cold start): $initialUri'); - // Safe to call before configure() - iOS uses static method that queues links - Superwall.shared.handleDeepLink(initialUri); + // Safe to call before configure() - static method bypasses shared instance + Superwall.handleDeepLink(initialUri); } }).catchError((Object err) { print('Error getting initial link: $err'); diff --git a/lib/src/public/Superwall.dart b/lib/src/public/Superwall.dart index 8a41dea..d15e939 100644 --- a/lib/src/public/Superwall.dart +++ b/lib/src/public/Superwall.dart @@ -350,9 +350,10 @@ class Superwall { await hostApi.preloadPaywallsForPlacements(placementNames.toList()); } - // Handles deep links for paywall previews - // Safe to call before configure() - iOS uses static method that queues links, Android attempts immediate processing - Future handleDeepLink(Uri url) async { + // Static method safe to call before configure() - bypasses shared instance + // iOS: Uses static Superwall.handleDeepLink() that queues links before configuration + // Android: Attempts immediate processing with instance method + static Future handleDeepLink(Uri url) async { return await hostApi.handleDeepLink(url.toString()); } From 2abb5b7b641864d0d2853edee2f5c8a1eabcaf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 2 Jul 2025 20:51:52 +0200 Subject: [PATCH 5/7] Fix cold start deep link handling with backward compatible solution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add handleDeepLink() instance method back for backward compatibility - Add handleDeepLinkStatic() static method safe to call before configure() - Update example app to use static method for cold start scenarios - Move deep link setup before SDK configuration in example - Add getInitialLink() handling for cold start deep links Key changes: - iOS: Both methods use static Superwall.handleDeepLink() that safely queues links - Android: Both methods use instance method, gracefully handle if not configured - hostApi available immediately as it just creates method channel communication - Provides both instance (shared.handleDeepLink) and static (handleDeepLinkStatic) APIs This solves the cold start deep link issue where links weren't consistently triggering paywalls due to timing between app launch and SDK configuration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- example/lib/main.dart | 2 +- lib/src/public/Superwall.dart | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 98c047c..cd620e3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -54,7 +54,7 @@ class _MyAppState extends State { if (initialUri != null) { debugPrint('Initial deep link (cold start): $initialUri'); // Safe to call before configure() - static method bypasses shared instance - Superwall.handleDeepLink(initialUri); + Superwall.handleDeepLinkStatic(initialUri); } }).catchError((Object err) { print('Error getting initial link: $err'); diff --git a/lib/src/public/Superwall.dart b/lib/src/public/Superwall.dart index d15e939..aa64673 100644 --- a/lib/src/public/Superwall.dart +++ b/lib/src/public/Superwall.dart @@ -350,10 +350,15 @@ class Superwall { await hostApi.preloadPaywallsForPlacements(placementNames.toList()); } + // Instance method for backward compatibility + Future handleDeepLink(Uri url) async { + return await hostApi.handleDeepLink(url.toString()); + } + // Static method safe to call before configure() - bypasses shared instance // iOS: Uses static Superwall.handleDeepLink() that queues links before configuration // Android: Attempts immediate processing with instance method - static Future handleDeepLink(Uri url) async { + static Future handleDeepLinkStatic(Uri url) async { return await hostApi.handleDeepLink(url.toString()); } From a2a184e913dae56ba61825e4ac63056a2506ff69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:00:58 +0200 Subject: [PATCH 6/7] Revert to simple solution using original shared.handleDeepLink instance method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Keep only the original shared.handleDeepLink() instance method for simplicity - Handle initial deep links after configure() completes in the completion callback - iOS: Uses static Superwall.handleDeepLink() native method that safely queues links - Android: Uses instance method after configuration - Move _handleInitialLink() call to configure completion to ensure SDK is ready This is the cleanest approach that leverages the native iOS queuing without adding complexity or breaking changes to the Flutter API. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- example/lib/main.dart | 10 +++++----- lib/src/public/Superwall.dart | 11 +++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index cd620e3..c73a37d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -29,8 +29,7 @@ class _MyAppState extends State { const useRevenueCat = true; super.initState(); - _handleIncomingLinks(); // Set up deep link handling before configuration - _handleInitialLink(); // Handle cold start deep links + _handleIncomingLinks(); // Set up deep link handling configureSuperwall(useRevenueCat); } @@ -43,7 +42,7 @@ class _MyAppState extends State { void _handleIncomingLinks() { appLinks.uriLinkStream.listen((Uri uri) { debugPrint('Incoming deep link: $uri'); - Superwall.handleDeepLink(uri); + Superwall.shared.handleDeepLink(uri); }, onError: (Object err) { print('Error receiving incoming link: $err'); }); @@ -53,8 +52,8 @@ class _MyAppState extends State { appLinks.getInitialLink().then((Uri? initialUri) { if (initialUri != null) { debugPrint('Initial deep link (cold start): $initialUri'); - // Safe to call before configure() - static method bypasses shared instance - Superwall.handleDeepLinkStatic(initialUri); + // Handle after configure completes - iOS SDK will queue it automatically + Superwall.shared.handleDeepLink(initialUri); } }).catchError((Object err) { print('Error getting initial link: $err'); @@ -97,6 +96,7 @@ class _MyAppState extends State { logging.info('Executing Superwall configure completion block'); print('Executing Superwall configure completion block'); listenForPurchases(); + _handleInitialLink(); // Handle cold start deep links after configure }); Superwall.shared.setDelegate(delegate); // MARK: Step 3 – Configure RevenueCat and Sync Subscription Status diff --git a/lib/src/public/Superwall.dart b/lib/src/public/Superwall.dart index aa64673..90dfdcd 100644 --- a/lib/src/public/Superwall.dart +++ b/lib/src/public/Superwall.dart @@ -350,18 +350,13 @@ class Superwall { await hostApi.preloadPaywallsForPlacements(placementNames.toList()); } - // Instance method for backward compatibility + // Handles deep links for paywall previews + // iOS: Uses static Superwall.handleDeepLink() that safely queues links before configuration + // Android: Uses instance method, works after configuration Future handleDeepLink(Uri url) async { return await hostApi.handleDeepLink(url.toString()); } - // Static method safe to call before configure() - bypasses shared instance - // iOS: Uses static Superwall.handleDeepLink() that queues links before configuration - // Android: Attempts immediate processing with instance method - static Future handleDeepLinkStatic(Uri url) async { - return await hostApi.handleDeepLink(url.toString()); - } - // Toggles the paywall loading spinner Future togglePaywallSpinner(bool isHidden) async { await hostApi.togglePaywallSpinner(isHidden); From aa44a56e28cd50550f794c618f2578fc03ab2f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:09:56 +0200 Subject: [PATCH 7/7] Update main.dart --- example/lib/main.dart | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index c73a37d..cd8b5c2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -48,18 +48,6 @@ class _MyAppState extends State { }); } - void _handleInitialLink() { - appLinks.getInitialLink().then((Uri? initialUri) { - if (initialUri != null) { - debugPrint('Initial deep link (cold start): $initialUri'); - // Handle after configure completes - iOS SDK will queue it automatically - Superwall.shared.handleDeepLink(initialUri); - } - }).catchError((Object err) { - print('Error getting initial link: $err'); - }); - } - @override void dispose() { _subscription?.cancel(); @@ -96,7 +84,6 @@ class _MyAppState extends State { logging.info('Executing Superwall configure completion block'); print('Executing Superwall configure completion block'); listenForPurchases(); - _handleInitialLink(); // Handle cold start deep links after configure }); Superwall.shared.setDelegate(delegate); // MARK: Step 3 – Configure RevenueCat and Sync Subscription Status