diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 62234f8f..e1ae0ee4 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -220,7 +220,24 @@ jobs: - name: 🔍 Validate package run: | echo "Running pub publish dry-run to validate package..." + # Run dry-run and capture exit code + # Exit code 65 = warnings only (acceptable, e.g., gitignored files) + # Exit code 0 = success + # Other exit codes = real errors + set +e flutter pub publish --dry-run + EXIT_CODE=$? + set -e + + if [ $EXIT_CODE -eq 0 ]; then + echo "✅ Package validation passed with no warnings" + elif [ $EXIT_CODE -eq 65 ]; then + echo "âš ī¸ Package validation passed with warnings (acceptable)" + echo "Warnings don't prevent publishing" + else + echo "❌ Package validation failed with exit code $EXIT_CODE" + exit $EXIT_CODE + fi - name: 📝 Check pub.dev credentials run: | diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index d7096497..dbbd0967 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -1,97 +1,208 @@ -name: Promote Release - Merge on QA Pass and Publish +# ============================================================================= +# Promote Release - Prepare Release Branch for Production +# ============================================================================= +# +# Purpose: When QA approves an RC, this workflow prepares the release branch +# for production by removing the -rc suffix from version numbers. +# +# IMPORTANT: This workflow does NOT merge the PR (org rules prevent bot merges). +# Instead, it updates the release branch so when a human merges, the version +# is clean (e.g., 6.17.8 instead of 6.17.8-rc1). +# +# Flow: +# 1. QA tests the RC version +# 2. QA adds label "pass QA ready for deploy" to the PR +# 3. This workflow triggers and: +# - Updates the release branch to remove -rcN suffix +# - Updates all version files +# - Commits changes to the release branch +# 4. Human reviews and manually merges the PR +# 5. production-release.yml triggers on merge +# +# ============================================================================= + +name: Promote Release - Prepare for Production on: pull_request: - types: [labeled, synchronize, reopened, ready_for_review] - branches: - - master - pull_request_review: - types: [submitted] + types: [labeled] branches: - master concurrency: - group: promote-release-${{ github.event.pull_request.number || github.run_id }} + group: promote-release-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: - gate-and-merge: - name: 🔐 Gate, Verify Checks, and Merge - if: >- - ${ { github.event.pull_request.head.ref } } == '' || startsWith(github.event.pull_request.head.ref, 'releases/') + # =========================================================================== + # Job 1: Prepare Release Branch for Production + # =========================================================================== + prepare-for-production: + name: 🚀 Prepare Release for Production + # Only run when the specific label is added AND it's from a releases/ branch + if: | + github.event.label.name == 'pass QA ready for deploy' && + startsWith(github.event.pull_request.head.ref, 'releases/') runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - checks: read - statuses: read + outputs: - merged: ${{ steps.merge.outputs.merged }} - version: ${{ steps.version.outputs.version }} + version: ${{ steps.compute-version.outputs.version }} + release_branch: ${{ steps.compute-version.outputs.release_branch }} + steps: - - name: 🧠 Evaluate conditions - id: eval - uses: actions/github-script@v7 + - name: đŸ“Ĩ Checkout release branch + uses: actions/checkout@v4 with: - script: | - const core = require('@actions/core'); - const pr = context.payload.pull_request || (await github.rest.pulls.get({owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.pull_request?.number || context.issue.number})).data; - if (!pr) core.setFailed('No PR context'); - const hasLabel = pr.labels.some(l => l.name === 'pass QA ready for deploy'); - if (!hasLabel) core.setFailed('Required label not present: pass QA ready for deploy'); - // Check approvals - const reviews = await github.rest.pulls.listReviews({owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number}); - const approved = reviews.data.some(r => r.state === 'APPROVED'); - if (!approved) core.setFailed('No approval found on the PR'); - core.setOutput('pr_number', pr.number.toString()); - - name: âŗ Wait for required status checks to pass + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: 🔍 Compute production version + id: compute-version + run: | + RELEASE_BRANCH="${{ github.event.pull_request.head.ref }}" + echo "Release branch: $RELEASE_BRANCH" + + # Get current version from pubspec.yaml + CURRENT_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') + echo "Current version: $CURRENT_VERSION" + + # Remove -rcN suffix to get production version + PROD_VERSION=$(echo "$CURRENT_VERSION" | sed 's/-rc[0-9]*$//') + echo "Production version: $PROD_VERSION" + + # Validate it's different (was an RC version) + if [[ "$CURRENT_VERSION" == "$PROD_VERSION" ]]; then + echo "âš ī¸ Version doesn't have -rc suffix. Already production ready?" + echo "Current: $CURRENT_VERSION" + fi + + echo "version=$PROD_VERSION" >> $GITHUB_OUTPUT + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT + + - name: 📝 Update pubspec.yaml to production version + run: | + VERSION='${{ steps.compute-version.outputs.version }}' + echo "Updating pubspec.yaml to production version: $VERSION" + sed -i "s/^version: .*/version: $VERSION/" pubspec.yaml + grep "^version:" pubspec.yaml + + - name: 📝 Update plugin version constants (Android) + run: | + VERSION='${{ steps.compute-version.outputs.version }}' + FILE="android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java" + if [ -f "$FILE" ]; then + sed -i "s/kPluginVersion = \".*\"/kPluginVersion = \"$VERSION\"/" "$FILE" + echo "Updated Android constants:" + grep "kPluginVersion" "$FILE" + fi + + - name: 📝 Update plugin version constants (iOS) + run: | + VERSION='${{ steps.compute-version.outputs.version }}' + FILE="ios/Classes/AppsflyerSdkPlugin.m" + if [ -f "$FILE" ]; then + sed -i "s/kPluginVersion = @\".*\"/kPluginVersion = @\"$VERSION\"/" "$FILE" + echo "Updated iOS constants:" + grep "kPluginVersion" "$FILE" + fi + + - name: 💾 Commit and push version changes + run: | + VERSION='${{ steps.compute-version.outputs.version }}' + CURRENT='${{ steps.compute-version.outputs.current_version }}' + + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + + if [[ -n $(git status -s) ]]; then + git add pubspec.yaml android/ ios/ + git commit -m "chore: prepare production release $VERSION (from $CURRENT)" + git push + echo "✅ Pushed version update to release branch" + else + echo "â„šī¸ No version changes needed" + fi + + - name: 📝 Update PR description uses: actions/github-script@v7 with: script: | - const prNumber = Number(core.getInput('pr_number', { required: false })) || ${{ steps.eval.outputs.pr_number || '0' }}; - const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }); - const ref = pr.head.sha; - const start = Date.now(); - const timeoutMs = 60*60*1000; // 60 minutes - const sleep = ms => new Promise(r => setTimeout(r, ms)); - while (true) { - const { data: combined } = await github.rest.repos.getCombinedStatusForRef({ owner: context.repo.owner, repo: context.repo.repo, ref }); - const checksOk = combined.state === 'success'; - if (checksOk) break; - if (Date.now() - start > timeoutMs) throw new Error('Timeout waiting for status checks to pass'); - core.info(`Waiting for checks. Current state: ${combined.state}`); - await sleep(15000); - } - - name: đŸ“Ĩ Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: 🔀 Merge PR immediately - id: merge + const version = '${{ steps.compute-version.outputs.version }}'; + const currentVersion = '${{ steps.compute-version.outputs.current_version }}'; + const pr = context.payload.pull_request; + + const newBody = `### Production Release ${version} + + **Status:** ✅ Ready for manual merge + + **Version updated:** ${currentVersion} → ${version} + + --- + + ${pr.body || ''} + + --- + + **Next steps:** + 1. ✅ QA approved (label added) + 2. ✅ Version updated to production (${version}) + 3. âŗ **Awaiting manual merge** by a maintainer + 4. âŗ Production release will trigger automatically after merge + `; + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + body: newBody + }); + + - name: đŸ“ĸ Add comment to PR uses: actions/github-script@v7 with: script: | - const prNumber = Number(${ { steps.eval.outputs.pr_number } }); - const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }); - if (pr.merged) { core.setOutput('merged', 'true'); return; } - const method = 'merge'; // use repo default merge method - await github.rest.pulls.merge({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, merge_method: method }); - core.setOutput('merged', 'true'); - - name: 📝 Read version from pubspec on master - id: version - run: | - git fetch origin master:master - git checkout master - VER=$(grep '^version:' pubspec.yaml | sed 's/version: //' | tr -d ' ') - echo "version=$VER" >> $GITHUB_OUTPUT + const version = '${{ steps.compute-version.outputs.version }}'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: `## 🚀 Ready for Production Release - call-production: - name: 🚀 Production Release - needs: gate-and-merge - if: needs.gate-and-merge.outputs.merged == 'true' - uses: ./.github/workflows/production-release.yml - with: - version: ${{ needs.gate-and-merge.outputs.version }} - skip_tests: false - dry_run: false - secrets: inherit + The release branch has been updated: + - **Version:** \`${version}\` (removed -rc suffix) + - **All version files updated** + + ### Next Steps + 1. **Review the changes** in this PR + 2. **Merge this PR** when ready + 3. The production release workflow will automatically: + - Publish \`${version}\` to pub.dev + - Create GitHub release + - Send notifications + + > âš ī¸ **Note:** This PR requires manual merge due to branch protection rules.` + }); + + # =========================================================================== + # Job 2: Notify Team + # =========================================================================== + notify-ready: + name: đŸ“ĸ Notify Ready for Merge + needs: prepare-for-production + runs-on: ubuntu-latest + if: always() && needs.prepare-for-production.result == 'success' + + steps: + - name: 📨 Send Slack notification + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:white_check_mark: *Flutter Plugin Ready for Production*\n\nVersion: ${{ needs.prepare-for-production.outputs.version }}\nPR: ${{ github.event.pull_request.html_url }}\n\n*Action Required:* A maintainer needs to manually merge the PR to trigger the production release." + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} + continue-on-error: true # Will gracefully fail if webhook not configured diff --git a/.gitignore b/.gitignore index bd18aee4..a8a6cdfc 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,8 @@ node_modules/ covBadgeGen.js coverage/ .env + +# AI agent tooling and E2E scripts (not part of plugin release) +.claude/ +scripts/ +example/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 77abd659..73e47ffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Versions +## 6.17.9 + +- Updated Android SDK from 6.17.5 to 6.17.6 +- Updated iOS SDK from 6.17.8 to 6.17.9 +- Updated iOS Purchase Connector from 6.17.8 to 6.17.9 + ## 6.17.8 - Updated Android SDK from 6.17.4 to 6.17.5 diff --git a/README.md b/README.md index 78385505..3c310168 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/ar ## SDK Versions -- Android AppsFlyer SDK **v6.17.5** -- iOS AppsFlyer SDK **v6.17.8** +- Android AppsFlyer SDK **v6.17.6** +- iOS AppsFlyer SDK **v6.17.9** ### Purchase Connector versions - Android 2.2.0 -- iOS 6.17.8 +- iOS 6.17.9 ## ❗❗ Breaking changes when updating to v6.x.x❗❗ diff --git a/android/build.gradle b/android/build.gradle index db95fa52..fff9a125 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,7 +53,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'com.appsflyer:af-android-sdk:6.17.5' + implementation 'com.appsflyer:af-android-sdk:6.17.6' implementation 'com.android.installreferrer:installreferrer:2.2' // implementation 'androidx.core:core-ktx:1.13.1' if (includeConnector) { diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java index 0b05df7c..e782f1c1 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java @@ -1,7 +1,7 @@ package com.appsflyer.appsflyersdk; public final class AppsFlyerConstants { - final static String PLUGIN_VERSION = "6.17.8"; + final static String PLUGIN_VERSION = "6.17.9"; final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink"; final static String AF_HOST_PREFIX = "hostPrefix"; final static String AF_HOST_NAME = "hostName"; diff --git a/covBadgeGen.js b/covBadgeGen.js deleted file mode 100755 index 16a32cdb..00000000 --- a/covBadgeGen.js +++ /dev/null @@ -1,20 +0,0 @@ -const lcov2badge = require("lcov2badge"); -const fs = require("fs"); - -lcov2badge.badge("./coverage/lcov.info", function (err, svgBadge) { - if (err) throw err; - - try { - if (fs.existsSync("./coverage_badge.svg")) { - fs.unlinkSync("./coverage_badge.svg"); - console.log("[INFO] remove old file"); - } - } catch (err) { - console.error(err); - } - - console.log("[INFO] generate coverage image"); - fs.writeFile("./coverage_badge.svg", svgBadge, (_) => - console.log("[INFO] complete") - ); -}); \ No newline at end of file diff --git a/example/.env b/example/.env deleted file mode 100644 index 644f4b18..00000000 --- a/example/.env +++ /dev/null @@ -1,2 +0,0 @@ -DEV_KEY="7jKdYxdnYcbSQ5iWrGytWc" -APP_ID="112233554" \ No newline at end of file diff --git a/example/.metadata b/example/.metadata deleted file mode 100644 index 417ad2fc..00000000 --- a/example/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "ef1af02aead6fe2414f3aafa5a61087b610e1332" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - - platform: android - create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - - platform: ios - create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - - platform: linux - create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - - platform: macos - create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - - platform: web - create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - - platform: windows - create_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - base_revision: ef1af02aead6fe2414f3aafa5a61087b610e1332 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/ios/Classes/AppsflyerSdkPlugin.h b/ios/Classes/AppsflyerSdkPlugin.h index 5b38dd3c..5a334696 100644 --- a/ios/Classes/AppsflyerSdkPlugin.h +++ b/ios/Classes/AppsflyerSdkPlugin.h @@ -6,7 +6,7 @@ #import "AppsFlyerLib.h" #endif -@interface AppsflyerSdkPlugin: NSObject +@interface AppsflyerSdkPlugin: NSObject @property (readwrite, nonatomic) BOOL isManualStart; @@ -18,7 +18,7 @@ @end // Appsflyer JS objects -#define kAppsFlyerPluginVersion @"6.17.8" +#define kAppsFlyerPluginVersion @"6.17.9" #define afDevKey @"afDevKey" #define afAppId @"afAppId" #define afIsDebug @"isDebug" diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/Classes/AppsflyerSdkPlugin.m index ca06d5df..bb36b1ba 100644 --- a/ios/Classes/AppsflyerSdkPlugin.m +++ b/ios/Classes/AppsflyerSdkPlugin.m @@ -68,7 +68,9 @@ + (void)registerWithRegistrar:(NSObject*)registrar { [registrar addMethodCallDelegate:instance channel:channel]; [registrar addMethodCallDelegate:instance channel:callbackChannel]; [registrar addApplicationDelegate:instance]; - + if (@available(iOS 13.0, *)) { + [registrar addSceneDelegate:instance]; + } } @@ -947,10 +949,36 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceAppl // Open Universal Links - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { [[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:restorationHandler]; - + // Results of this are ORed and NO doesn't affect other delegate interceptors' result. return NO; } +#pragma mark - FlutterSceneLifeCycleDelegate + +// UIScene-based URI-scheme deep links (iOS 13+, Flutter 3.41+ UIScene migration) +- (BOOL)scene:(UIScene*)scene openURLContexts:(NSSet*)URLContexts API_AVAILABLE(ios(13.0)) { + for (UIOpenURLContext *context in URLContexts) { + [[AppsFlyerAttribution shared] handleOpenUrl:context.URL options:@{}]; + } + return NO; +} + +// Cold-start URI-scheme deep links delivered via UISceneConnectionOptions (iOS 13+) +- (BOOL)scene:(UIScene*)scene + willConnectToSession:(UISceneSession*)session + options:(UISceneConnectionOptions*)connectionOptions API_AVAILABLE(ios(13.0)) { + for (UIOpenURLContext *context in connectionOptions.URLContexts) { + [[AppsFlyerAttribution shared] handleOpenUrl:context.URL options:@{}]; + } + return NO; +} + +// UIScene-based Universal Links (iOS 13+) +- (BOOL)scene:(UIScene*)scene continueUserActivity:(NSUserActivity*)userActivity API_AVAILABLE(ios(13.0)) { + [[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:nil]; + return NO; +} + @end diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index c7656f9a..03850085 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'appsflyer_sdk' - s.version = '6.17.8' + s.version = '6.17.9' s.summary = 'AppsFlyer Integration for Flutter' s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' @@ -21,12 +21,12 @@ Pod::Spec.new do |s| ss.source_files = 'Classes/**/*' ss.public_header_files = 'Classes/**/*.h' ss.dependency 'Flutter' - ss.ios.dependency 'AppsFlyerFramework','6.17.8' + ss.ios.dependency 'AppsFlyerFramework','6.17.9' end s.subspec 'PurchaseConnector' do |ss| ss.dependency 'Flutter' - ss.ios.dependency 'PurchaseConnector', '6.17.8' + ss.ios.dependency 'PurchaseConnector', '6.17.9' ss.source_files = 'PurchaseConnector/**/*' ss.public_header_files = 'PurchaseConnector/**/*.h' diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index ed032c3e..91f3fdf5 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,7 +3,7 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { - static const String PLUGIN_VERSION = "6.17.8"; + static const String PLUGIN_VERSION = "6.17.9"; static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; diff --git a/pubspec.yaml b/pubspec.yaml index f00c0561..9193c3ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.17.8 +version: 6.17.9 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk