diff --git a/.cursor/rules/ci-cd.mdc b/.cursor/rules/ci-cd.mdc new file mode 100644 index 0000000..cf0ea67 --- /dev/null +++ b/.cursor/rules/ci-cd.mdc @@ -0,0 +1,53 @@ +--- +description: CI/CD pipeline conventions and constraints +globs: ".github/**,scripts/**" +alwaysApply: false +--- + +# CI/CD Conventions + +## Pipeline Overview (GitHub Actions) + +Triggers: push to `master`, version tags, PRs, manual dispatch. + +``` +format -> lint -> pub-dry-run + | + +-------------+-------------+ + | | | + test-flutter test-android test-ios + | | | + build-android build-ios | + | | | + +-------------+-------------+ + | + release (tags only) +``` + +## Pinned Versions + +- Flutter: `3.22.3` +- Gradle: `8.7` +- Xcode: `15.4` +- Container: `ghcr.io/cirruslabs/flutter:3.22.3` + +Do **not** change these without testing the full pipeline. + +## Scripts (`scripts/`) + +| Script | Purpose | +|--------|---------| +| `check_format.sh` | `dart format --line-length=80 --set-exit-if-changed .` | +| `assert_export.sh` | Ensures all public files are exported | +| `check_coverage.dart` | Validates test coverage thresholds | +| `check_release_version.dart` | Validates tag matches pubspec version | +| `check_gradle_version.dart` | Validates Gradle wrapper version | +| `setup_gradle.sh` | Downloads and configures Gradle wrapper | +| `ios_unit_tests.sh` | Runs iOS unit tests via xcodebuild | + +## Rules + +- All CI scripts must be executable and use `#!/bin/sh -xe` for traceability +- Never add `--no-analyze` or skip lint steps +- Coverage thresholds are enforced - don't lower them without team approval +- Android tests use Kover for coverage, Dart uses lcov, iOS uses xccov diff --git a/.cursor/rules/dart-conventions.mdc b/.cursor/rules/dart-conventions.mdc new file mode 100644 index 0000000..222437a --- /dev/null +++ b/.cursor/rules/dart-conventions.mdc @@ -0,0 +1,35 @@ +--- +description: Dart coding conventions for the Usercentrics Flutter SDK +globs: "**/*.dart" +alwaysApply: false +--- + +# Dart Conventions + +## Formatting + +- Line length: **80 characters** (enforced by `dart format --line-length=80`) +- Use `flutter_lints` rules from `analysis_options.yaml` +- Missing enum constants in switch statements are treated as **errors** + +## Code Style + +- Public API classes use **static methods** delegating to platform interface (see `Usercentrics` class) +- Internal code lives under `lib/src/internal/` and is **not exported** +- All public models are exported via barrel file `lib/src/model/model.dart` +- Custom exceptions implement `Exception` (not `Error`) +- Use `@visibleForTesting` for test-only accessors +- Use `@Deprecated('Use [replacement] instead')` with migration guidance + +## File Organization + +- One class/enum per file, file name matches class in snake_case +- Barrel exports via `bridge.dart`, `model.dart`, `internal.dart` +- Bridges: `lib/src/internal/bridge/_bridge.dart` +- Serializers: `lib/src/internal/serializer/_serializer.dart` + +## Public API Rules + +- Every public file in `lib/src/` **must** be exported (enforced by `scripts/assert_export.sh`) +- Document all public APIs with `///` doc comments +- Include parameter descriptions for non-obvious args diff --git a/.cursor/rules/kotlin-conventions.mdc b/.cursor/rules/kotlin-conventions.mdc new file mode 100644 index 0000000..1ab384b --- /dev/null +++ b/.cursor/rules/kotlin-conventions.mdc @@ -0,0 +1,35 @@ +--- +description: Kotlin coding conventions for the Android plugin +globs: "**/*.kt" +alwaysApply: false +--- + +# Kotlin Conventions (Android Plugin) + +## Project Setup + +- Package: `com.usercentrics.sdk.flutter` +- Kotlin: 1.9.24, JVM target: 1.8 +- Min SDK: 16, Compile SDK: 31 +- Dependencies managed in `android/build.gradle` + +## Architecture + +- `UsercentricsPlugin.kt` - Main Flutter plugin entry point +- `api/` - Proxy abstractions (`UsercentricsProxy`, `UsercentricsBannerProxy`, `FlutterResult`, `FlutterMethodCall`) +- `bridge/` - One bridge per SDK method, extending `MethodBridge` +- `serializer/` - One serializer per data type, converting to/from `Map` + +## Patterns + +- Each bridge handles a single MethodChannel call and returns results via `FlutterResult` +- Serializers convert between native Usercentrics SDK types and Flutter-compatible Maps +- Use `io.mockk` for mocking in tests +- Tests live in `android/src/test/java/` + +## Code Style + +- Follow Kotlin coding conventions +- Prefer `val` over `var` +- Use data classes for pure data holders +- Keep bridge classes focused on single method handling diff --git a/.cursor/rules/platform-bridge-pattern.mdc b/.cursor/rules/platform-bridge-pattern.mdc new file mode 100644 index 0000000..489c989 --- /dev/null +++ b/.cursor/rules/platform-bridge-pattern.mdc @@ -0,0 +1,38 @@ +--- +description: Platform bridge pattern used across Dart, Kotlin, and Swift +globs: "**/bridge/**" +alwaysApply: false +--- + +# Platform Bridge Pattern + +## Overview + +Every SDK method is implemented as a dedicated Bridge class on all 3 platforms. This keeps each method's serialization and channel logic isolated. + +## Adding a New Bridge + +When adding a new SDK method, you must create **3 bridge files** + update barrel exports: + +### 1. Dart (`lib/src/internal/bridge/`) +- Create `_bridge.dart` +- Add export to `bridge.dart` barrel file +- Wire it in `method_channel_usercentrics.dart` + +### 2. Kotlin (`android/src/main/kotlin/.../bridge/`) +- Create `Bridge.kt` extending `MethodBridge` +- Register in `UsercentricsPlugin.kt` + +### 3. Swift (`ios/Classes/Bridge/`) +- Create `Bridge.swift` conforming to `MethodBridge` +- Register in `SwiftUsercentricsPlugin.swift` + +### 4. Tests +- Add tests for each platform's bridge implementation + +## MethodChannel Contract + +- Channel name must match across all 3 platforms +- Arguments are passed as `Map` (Dart) / `Map` (Kotlin) / `[String: Any?]` (Swift) +- Results are returned as Maps serialized by the corresponding Serializer +- Errors use `FlutterError` with code, message, and details diff --git a/.cursor/rules/project-overview.mdc b/.cursor/rules/project-overview.mdc new file mode 100644 index 0000000..4e51270 --- /dev/null +++ b/.cursor/rules/project-overview.mdc @@ -0,0 +1,39 @@ +--- +description: Usercentrics Flutter SDK project overview and architecture +alwaysApply: true +--- + +# Usercentrics Flutter SDK + +This is a **Flutter plugin** (`usercentrics_sdk`) providing a Consent Management Platform (CMP) for mobile apps. It wraps native Usercentrics SDKs for Android and iOS. + +## Architecture + +- **`lib/`** - Public Dart API and internal implementation + - `lib/src/model/` - Data models (exported publicly) + - `lib/src/platform/` - Platform interface (abstract contract) + - `lib/src/internal/bridge/` - Method channel bridge implementations + - `lib/src/internal/serializer/` - JSON serialization/deserialization + - `lib/src/internal/platform/` - MethodChannel platform implementation +- **`android/`** - Kotlin plugin (mirrors bridge/serializer pattern) +- **`ios/`** - Swift/ObjC plugin (mirrors bridge/serializer pattern) +- **`example/`** - Demo Flutter app +- **`test/`** - Dart unit tests +- **`scripts/`** - CI helper scripts + +## Key Patterns + +- Entry point: `Usercentrics` static class delegates to `UsercentricsPlatform` +- All platform communication goes through Flutter MethodChannel +- Each SDK method has a dedicated Bridge class on all 3 platforms (Dart, Kotlin, Swift) +- Each data type has a dedicated Serializer class on all 3 platforms +- Version must stay in sync across: `pubspec.yaml`, `android/build.gradle`, `ios/usercentrics_sdk.podspec`, `CHANGELOG.md` + +## Tech Stack + +| Layer | Language | Build System | +|-------|----------|-------------| +| Dart | Dart (SDK >=2.17.1 <4.0.0) | pub | +| Android | Kotlin 1.9.24 | Gradle 8.7 | +| iOS | Swift 5.3 | CocoaPods | +| CI | GitHub Actions | Flutter 3.22.3 | diff --git a/.cursor/rules/security-snyk.mdc b/.cursor/rules/security-snyk.mdc new file mode 100644 index 0000000..13415bd --- /dev/null +++ b/.cursor/rules/security-snyk.mdc @@ -0,0 +1,53 @@ +--- +description: Security best practices and Snyk compliance rules +alwaysApply: true +--- + +# Security & Snyk Compliance + +## Dependency Management + +- **Never** pin to wildcard versions (`any`) - always use bounded ranges +- Keep dependencies minimal; this plugin only depends on `flutter` SDK at runtime +- Dev dependencies (`flutter_test`, `test`, `flutter_lints`) must also use version constraints +- Regularly check for outdated deps: `flutter pub outdated` +- Native deps (Gradle, CocoaPods) must also use explicit version pins + +## Snyk Scans - What Gets Checked + +Snyk analyzes these manifests in the pipeline: +- `pubspec.yaml` / `pubspec.lock` - Dart/Flutter dependencies +- `android/build.gradle` - Android/Gradle dependencies +- `ios/usercentrics_sdk.podspec` - iOS CocoaPods dependencies +- `example/pubspec.yaml` - Example app dependencies +- `example/android/build.gradle` - Example Android dependencies +- `example/ios/Podfile` / `Podfile.lock` - Example iOS dependencies + +## Vulnerability Rules + +- **Never** ignore Snyk findings without a documented reason +- Address critical/high severity vulns before merging to master +- If a transitive dependency has a vuln, upgrade the direct parent dependency +- For native SDK vulns (Usercentrics Android/iOS), coordinate with Usercentrics team + +## Secrets & Credentials + +- **Never** commit API keys, secrets, tokens, or credentials +- `settingsId` and `ruleSetId` in example code are public demo IDs only +- Files that must never be committed: `local.properties`, `key.properties`, `.env`, `*.keystore`, `*.jks`, `*.p12`, `*.mobileprovision` +- The `android/key.properties` pattern is already in `.gitignore` - keep it there + +## Code Security Patterns + +- Validate all data coming from MethodChannel before use +- Serializers must handle null/missing fields gracefully (no crashes on malformed data) +- Never log sensitive user consent data at production log levels +- Use `UsercentricsLoggerLevel.none` as default, only `debug`/`warning`/`error` when explicitly set +- ProGuard rules (`proguard-rules.pro`) must be maintained for release builds + +## Secure CI/CD + +- CI uses `ghcr.io/cirruslabs/flutter:3.22.3` container - keep this image version explicit +- Release job uses `id-token: write` for OIDC-based pub.dev publishing (no long-lived tokens) +- Never add `--no-verify` or skip security checks in CI scripts +- Gradle and Xcode versions are pinned in CI - don't use `latest` diff --git a/.cursor/rules/serialization.mdc b/.cursor/rules/serialization.mdc new file mode 100644 index 0000000..72c1829 --- /dev/null +++ b/.cursor/rules/serialization.mdc @@ -0,0 +1,34 @@ +--- +description: Serialization pattern for data transfer across platform channels +globs: "**/serializer/**,**/Serializer/**" +alwaysApply: false +--- + +# Serialization Pattern + +## Overview + +Each data type has a dedicated Serializer on all 3 platforms (Dart, Kotlin, Swift) that converts between native types and MethodChannel-compatible Maps/Dictionaries. + +## Adding a New Serializer + +### Dart (`lib/src/internal/serializer/`) +- File: `_serializer.dart` +- Export in `serializer.dart` barrel +- Convert `Map` to/from model class + +### Kotlin (`android/.../serializer/`) +- File: `Serializer.kt` +- Convert `Map` to/from native SDK type + +### Swift (`ios/Classes/Serializer/`) +- File: `Serializer.swift` +- Convert `[String: Any?]` to/from native SDK type + +## Rules + +- Handle null/missing keys defensively - never force-unwrap without a fallback +- Keep serialization logic pure (no side effects, no network calls) +- Enum serialization uses string values, not ordinals +- Nested objects delegate to their own serializer +- Test round-trip: serialize -> deserialize -> assert structural equality diff --git a/.cursor/rules/swift-conventions.mdc b/.cursor/rules/swift-conventions.mdc new file mode 100644 index 0000000..063bb68 --- /dev/null +++ b/.cursor/rules/swift-conventions.mdc @@ -0,0 +1,34 @@ +--- +description: Swift coding conventions for the iOS plugin +globs: "**/*.swift" +alwaysApply: false +--- + +# Swift Conventions (iOS Plugin) + +## Project Setup + +- Swift 5.3, iOS deployment target: 11.0 +- Pod: `usercentrics_sdk`, depends on `UsercentricsUI` +- Plugin registration via ObjC (`UsercentricsPlugin.m/.h`) + +## Architecture + +- `SwiftUsercentricsPlugin.swift` - Main plugin logic +- `API/` - Proxy abstractions (`UsercentricsProxy`, `UsercentricsBannerProxy`, `FlutterAssetProvider`) +- `Bridge/` - One bridge per SDK method, conforming to `MethodBridge` +- `Serializer/` - One serializer per data type + +## Patterns + +- Each bridge class handles one MethodChannel call +- Serializers convert native Usercentrics types to Flutter-compatible dictionaries +- Tests live in `example/ios/RunnerTests/` using XCTest +- `UCFlutterFlag` controls feature flags + +## Code Style + +- Follow Swift API design guidelines +- Use `struct` for value types, `class` for reference types +- Keep bridge classes single-responsibility +- Prefer guard-let over nested if-let diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc new file mode 100644 index 0000000..5322c4f --- /dev/null +++ b/.cursor/rules/testing.mdc @@ -0,0 +1,34 @@ +--- +description: Testing conventions across all platforms +globs: "**/test/**,**/test_*,**/*_test.dart,**/*Test.kt,**/*Tests.swift" +alwaysApply: false +--- + +# Testing Conventions + +## Dart Tests (`test/`) + +- Framework: `flutter_test` + `test` +- Run: `flutter test --reporter=expanded --coverage` +- Coverage check: `dart scripts/check_coverage.dart coverage/lcov.info` +- Structure mirrors `lib/src/` (e.g. `test/internal/bridge/` maps to `lib/src/internal/bridge/`) +- Name files `_test.dart` + +## Android Tests (`android/src/test/java/`) + +- Framework: JUnit 4 + MockK +- Run via Gradle: `./gradlew :usercentrics_sdk:testDebugUnitTest` +- Coverage: Kover (`koverHtmlReportDebug`) + +## iOS Tests (`example/ios/RunnerTests/`) + +- Framework: XCTest +- Run: `scripts/ios_unit_tests.sh` +- Coverage: `xcrun xccov view --report TestResults.xcresult` + +## Guidelines + +- Every bridge and serializer should have corresponding tests on all platforms +- Mock native SDK dependencies, never call real Usercentrics APIs in tests +- Test serialization round-trips (serialize -> deserialize -> assert equality) +- Test error/exception paths, not just happy paths diff --git a/.cursor/rules/versioning.mdc b/.cursor/rules/versioning.mdc new file mode 100644 index 0000000..b7df23e --- /dev/null +++ b/.cursor/rules/versioning.mdc @@ -0,0 +1,28 @@ +--- +description: Version management rules - version must stay in sync across files +globs: "pubspec.yaml,android/build.gradle,ios/usercentrics_sdk.podspec,CHANGELOG.md" +alwaysApply: false +--- + +# Version Management + +## Synced Version Files + +The SDK version **must** be identical across these 4 files: + +1. `pubspec.yaml` - `version: X.Y.Z` +2. `android/build.gradle` - `def usercentrics_version = "X.Y.Z"` +3. `ios/usercentrics_sdk.podspec` - `s.version = 'X.Y.Z'` +4. `CHANGELOG.md` - new entry header `## X.Y.Z` + +## Release Process + +- CI validates version on tag push via `scripts/check_release_version.dart` +- Tags follow `[0-9]+.[0-9]+.[0-9]+*` pattern (semver, optional pre-release suffix) +- Publishing is automated via `flutter pub publish -f` using OIDC token + +## Rules + +- When bumping version, **always** update all 4 files in the same commit +- Native SDK dependency version (`UsercentricsUI`) matches the plugin version +- Never use `-SNAPSHOT` or other mutable version suffixes in release commits diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..0e62fd5 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,69 @@ +# Build outputs +build/ +**/android/build/ +**/ios/DerivedData/ + +# Dart/Flutter caches +.dart_tool/ +.pub-cache/ +.pub/ +.packages +.flutter-plugins +.flutter-plugins-dependencies + +# Gradle caches and wrappers +**/android/.gradle +**/android/**/gradle-wrapper.jar +**/android/gradlew +**/android/gradlew.bat +**/ci-cache/ + +# CocoaPods +**/ios/Pods/ +**/ios/**/.symlinks/ + +# Generated files +lib/generated_plugin_registrant.dart +**/GeneratedPluginRegistrant.* +**/ios/.generated/ +**/ios/Flutter/*.framework +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_export_environment.sh +**/ios/Flutter/.last_build_id + +# Coverage reports +coverage/ + +# IDE +.idea/ +.vscode/ +*.iml + +# OS +.DS_Store + +# Xcode noise +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/xcuserdata +**/ios/ServiceDefinitions.json + +# Lock files (not useful for AI context) +pubspec.lock +**/pubspec.lock +**/Podfile.lock + +# Binary/asset files +**/Assets.xcassets/ +*.storyboard +*.pbxproj +*.xcworkspace/ diff --git a/.cursorindexingignore b/.cursorindexingignore new file mode 100644 index 0000000..23c8a09 --- /dev/null +++ b/.cursorindexingignore @@ -0,0 +1,29 @@ +# Example app - useful for reference but shouldn't pollute index +example/android/ +example/ios/ +example/test/ + +# Xcode project files +**/*.xcodeproj/ +**/*.xcworkspace/ + +# Android config files +**/local.properties +**/key.properties +**/gradle-wrapper.properties +**/gradle.properties + +# iOS config/plist +example/**/*.plist +example/**/*.storyboard +example/**/*.xcassets/ + +# Proguard +**/proguard-rules.pro + +# HTML assets +**/*.html + +# Git config +.git/ +.github/ISSUE_TEMPLATE/