diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..38d6802 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:superwall.com)", + "Bash(gh pr edit:*)", + "Bash(flutter pub run pigeon:*)", + "Bash(flutter analyze:*)", + "Bash(flutter build:*)", + "Bash(dart run pigeon:*)" + ], + "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 5af8f1c..cd8b5c2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -29,6 +29,7 @@ class _MyAppState extends State { const useRevenueCat = true; super.initState(); + _handleIncomingLinks(); // Set up deep link handling configureSuperwall(useRevenueCat); } @@ -40,7 +41,7 @@ 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'); @@ -84,7 +85,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/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 f9a9308..90dfdcd 100644 --- a/lib/src/public/Superwall.dart +++ b/lib/src/public/Superwall.dart @@ -20,6 +20,7 @@ class Superwall { static final generated.PSuperwallHostApi hostApi = generated.PSuperwallHostApi(); + // Private constructor Superwall._(); @@ -350,6 +351,8 @@ class Superwall { } // 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()); }