From c226b5a00d010db9bdcbd9f1644838ba64381b29 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:06:16 +0200 Subject: [PATCH 1/4] Release 6.17.8 (#431) * SDK updates deprecate V1 purchase validation, improve iOS error handling (#429) * SDK updates deprecate V1 purchase validation, improve iOS error handling SDK Version Updates: - Update Android SDK from 6.17.4 to 6.17.5 - Update iOS SDK from 6.17.7 to 6.17.8 - Update iOS Purchase Connector from 6.17.7 to 6.17.8 - Bump plugin version to 6.17.8 across all platforms API Changes: - Deprecate validateAndLogInAppAndroidPurchase (V1) - Deprecate validateAndLogInAppIosPurchase (V1) - Enhance iOS error handling for validateAndLogInAppPurchaseV2 with NSError parsing (code, domain, userInfo) Documentation Updates: - Remove "Beta" label from validateAndLogInAppPurchaseV2 API - Mark V1 purchase validation APIs as Deprecated - Add comprehensive PlatformException error handling examples - Add iOS token format explanation for uninstall measurement - Add cross-platform Firebase Messaging example for uninstall tokens * lint * CHANGELOG update * fix implementation for validateAndLogInAppPurchaseV2 * fix: remove toJSON() causing extra quotes in PR creation * fix: add .pubignore and handle RC dry-run warnings gracefully * lint * chore: prepare RC (iOS 6.17.8, Android 6.17.5) --- .github/workflows/production-release.yml | 31 +- .github/workflows/promote-release.yml | 97 ++++ .github/workflows/rc-release.yml | 442 +++++++++++------- .pubignore | 23 + CHANGELOG.md | 15 + README.md | 6 +- android/build.gradle | 2 +- .../appsflyersdk/AppsFlyerConstants.java | 2 +- doc/AdvancedAPI.md | 70 ++- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 4 +- ios/Classes/AppsflyerSdkPlugin.h | 2 +- ios/Classes/AppsflyerSdkPlugin.m | 126 ++--- ios/appsflyer_sdk.podspec | 6 +- lib/src/appsflyer_constants.dart | 2 +- lib/src/appsflyer_sdk.dart | 10 +- pubspec.yaml | 2 +- 17 files changed, 550 insertions(+), 292 deletions(-) create mode 100644 .github/workflows/promote-release.yml create mode 100644 .pubignore diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index cbfd624c..62234f8f 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -26,7 +26,7 @@ name: Production Release - Publish to pub.dev on: - # Trigger when PR to master is merged + # Trigger when PR to master is merged (legacy path; promotion flow now preferred) pull_request: types: - closed @@ -51,6 +51,21 @@ on: type: boolean default: false + # Allow being called from other workflows + workflow_call: + inputs: + version: + required: true + type: string + skip_tests: + required: false + type: boolean + default: false + dry_run: + required: false + type: boolean + default: false + # Ensure only one production release runs at a time concurrency: group: production-release @@ -67,8 +82,8 @@ jobs: name: πŸ” Validate Release runs-on: ubuntu-latest - # Only run if PR was actually merged (not just closed) - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true + # Run when manually dispatched, called by another workflow, or when a PR merge event happens + if: github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event.pull_request.merged == true outputs: version: ${{ steps.get-version.outputs.version }} @@ -84,8 +99,8 @@ jobs: - name: πŸ” Validate release source id: validate run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - echo "Manual workflow dispatch - skipping branch validation" + if [[ "${{ github.event_name }}" == "workflow_dispatch" || "${{ github.event_name }}" == "workflow_call" ]]; then + echo "Manual/called run - skipping branch validation" echo "is_release_branch=true" >> $GITHUB_OUTPUT echo "is_valid=true" >> $GITHUB_OUTPUT else @@ -109,9 +124,9 @@ jobs: - name: πŸ“ Get version from pubspec.yaml id: get-version run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - VERSION="${{ github.event.inputs.version }}" - echo "Using manual version: $VERSION" + if [[ "${{ github.event_name }}" == "workflow_dispatch" || "${{ github.event_name }}" == "workflow_call" ]]; then + VERSION="${{ inputs.version || github.event.inputs.version }}" + echo "Using provided version: $VERSION" else # Extract version from pubspec.yaml VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml new file mode 100644 index 00000000..d7096497 --- /dev/null +++ b/.github/workflows/promote-release.yml @@ -0,0 +1,97 @@ +name: Promote Release - Merge on QA Pass and Publish + +on: + pull_request: + types: [labeled, synchronize, reopened, ready_for_review] + branches: + - master + pull_request_review: + types: [submitted] + branches: + - master + +concurrency: + group: promote-release-${{ github.event.pull_request.number || github.run_id }} + 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/') + 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 }} + steps: + - name: 🧠 Evaluate conditions + id: eval + uses: actions/github-script@v7 + 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 + 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 + 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 + + 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 diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index ab9e24f5..a08acf6a 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -29,28 +29,46 @@ name: RC - Release Candidate on: - # Trigger on push to release branches - push: - branches: - - 'releases/**' - - # Allow manual triggering with version specification + # Manual-only triggering with required parameters workflow_dispatch: inputs: - version: - description: 'Release version (e.g., 6.18.0-rc1)' + base_branch: + description: 'Base branch to create the release branch from (e.g., development)' + required: false + default: development + type: string + flutter_version: + description: 'Flutter plugin version for this RC (e.g., 6.18.0-rc1 or 6.18.0+1-rc1)' + required: true + type: string + ios_sdk_version: + description: 'iOS native AppsFlyer SDK version (e.g., 6.17.7)' + required: true + type: string + android_sdk_version: + description: 'Android native AppsFlyer SDK version (e.g., 6.17.4)' required: true type: string + deploy_to_qa: + description: 'Open PR to master and publish RC to pub.dev' + required: false + type: boolean + default: false skip_tests: - description: 'Skip tests and builds (for testing workflow only)' + description: 'Skip reusable CI when running this workflow (PR and production flows still run CI)' required: false type: boolean default: false + dry_run: + description: 'Do not publish RC to pub.dev (still opens PR and creates prerelease)' + required: false + type: boolean + default: true # Prevent multiple RC workflows from running simultaneously concurrency: - group: rc-release-${{ github.ref }} - cancel-in-progress: false # Don't cancel, let it finish + group: rc-release-${{ github.run_id }}-${{ github.event.inputs.flutter_version || github.ref }} + cancel-in-progress: true jobs: # =========================================================================== @@ -60,94 +78,73 @@ jobs: # =========================================================================== validate-release: - name: πŸ” Validate Release Branch + name: πŸ” Validate Inputs & Compute Branch runs-on: ubuntu-latest outputs: - version: ${{ steps.extract-version.outputs.version }} - is_rc: ${{ steps.extract-version.outputs.is_rc }} - is_valid: ${{ steps.extract-version.outputs.is_valid }} + version: ${{ steps.compute.outputs.version }} + base_version: ${{ steps.compute.outputs.base_version }} + podspec_version: ${{ steps.compute.outputs.podspec_version }} + is_rc: ${{ steps.compute.outputs.is_rc }} + is_valid: ${{ steps.compute.outputs.is_valid }} + base_branch: ${{ steps.compute.outputs.base_branch }} + release_branch: ${{ steps.compute.outputs.release_branch }} + ios_sdk_version: ${{ steps.compute.outputs.ios_sdk_version }} + android_sdk_version: ${{ steps.compute.outputs.android_sdk_version }} + deploy_to_qa: ${{ steps.compute.outputs.deploy_to_qa }} + dry_run: ${{ steps.compute.outputs.dry_run }} steps: - name: πŸ“₯ Checkout repository uses: actions/checkout@v4 - - name: πŸ” Extract and validate version - id: extract-version + - name: πŸ” Validate and compute + id: compute run: | - # Determine version from branch name or manual input - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - VERSION="${{ github.event.inputs.version }}" - echo "Using manual version: $VERSION" - else - # Extract version from branch name: releases/X.x.x/X.Y.x/X.Y.Z[-rcN][+build] - # Examples: - # - releases/6.x.x/6.18.x/6.18.0-rc1 - # - releases/6.x.x/6.17.x/6.17.4+1-rc1 - # - releases/6.x.x/6.17.x/6.17.4+1 - BRANCH_NAME="${{ github.ref_name }}" - echo "Branch name: $BRANCH_NAME" - - # Extract version using regex - now supports +build suffix - # Pattern: releases/X.x.x/X.Y.x/X.Y.Z[+build][-rcN] where x is literal 'x' - if [[ $BRANCH_NAME =~ releases/([0-9]+)\.x\.x/([0-9]+\.[0-9]+)\.x/([0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?(-rc[0-9]+)?)$ ]]; then - VERSION="${BASH_REMATCH[3]}" - MAJOR="${BASH_REMATCH[1]}" - MAJOR_MINOR="${BASH_REMATCH[2]}" - - echo "Extracted version: $VERSION" - echo "Major version: $MAJOR" - echo "Major.Minor: $MAJOR_MINOR" - - # Validate that version starts with the correct major.minor (before any +build suffix) - VERSION_PREFIX=$(echo "$VERSION" | grep -oE '^[0-9]+\.[0-9]+') - if [[ "$VERSION_PREFIX" != "$MAJOR_MINOR" ]]; then - echo "❌ Version mismatch!" - echo "Expected version to start with: $MAJOR_MINOR" - echo "Got: $VERSION" - exit 1 - fi - - # Validate that major version matches - VERSION_MAJOR=$(echo "$VERSION" | grep -oE '^[0-9]+') - if [[ "$VERSION_MAJOR" != "$MAJOR" ]]; then - echo "❌ Major version mismatch!" - echo "Expected major version: $MAJOR" - echo "Got: $VERSION_MAJOR" - exit 1 - fi - else - echo "❌ Invalid branch name format!" - echo "Expected: releases/X.x.x/X.Y.x/X.Y.Z[-rcN][+build]" - echo "Examples:" - echo " - releases/6.x.x/6.18.x/6.18.0-rc1" - echo " - releases/6.x.x/6.17.x/6.17.4+1-rc1" - echo " - releases/6.x.x/6.17.x/6.17.4+1" - echo "Got: $BRANCH_NAME" - exit 1 - fi + set -euo pipefail + VERSION="${{ github.event.inputs.flutter_version }}" + IOS_VER="${{ github.event.inputs.ios_sdk_version }}" + AND_VER="${{ github.event.inputs.android_sdk_version }}" + BASE_BRANCH_INPUT="${{ github.event.inputs.base_branch }}" + DEPLOY_TO_QA="${{ github.event.inputs.deploy_to_qa }}" + DRY_RUN_INPUT="${{ github.event.inputs.dry_run }}" + + if [[ -z "$VERSION" || -z "$IOS_VER" || -z "$AND_VER" ]]; then + echo "❌ Missing required inputs"; exit 1 fi - - # Validate version format (supports +build suffix) - if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?(-rc[0-9]+)?$ ]]; then - echo "βœ… Valid version format: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "is_valid=true" >> $GITHUB_OUTPUT - - # Check if it's an RC version - if [[ $VERSION =~ -rc[0-9]+$ ]]; then - echo "is_rc=true" >> $GITHUB_OUTPUT - echo "πŸ“¦ This is a Release Candidate" - else - echo "is_rc=false" >> $GITHUB_OUTPUT - echo "πŸ“¦ This is a production version" - fi - else - echo "❌ Invalid version format: $VERSION" - echo "is_valid=false" >> $GITHUB_OUTPUT - exit 1 + + # Validate formats + if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?-rc[0-9]+$ ]]; then + echo "❌ flutter_version must be a prerelease like X.Y.Z[-build]+?-rcN (e.g., 6.18.0-rc1 or 6.18.0+1-rc1)"; exit 1 + fi + if [[ ! $IOS_VER =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ ios_sdk_version must be X.Y.Z"; exit 1 + fi + if [[ ! $AND_VER =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ android_sdk_version must be X.Y.Z"; exit 1 fi + # Compute base version (remove -rcN), keep +build if present + BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') + # Podspec version must remove both +build and -rcN + PODSPEC_VERSION=$(echo "$VERSION" | sed -E 's/(\+[0-9]+)?(-rc[0-9]+)?$//') + + MAJOR_MINOR=$(echo "$BASE_VERSION" | grep -oE '^[0-9]+\.[0-9]+') + MAJOR=$(echo "$BASE_VERSION" | grep -oE '^[0-9]+') + RELEASE_BRANCH="releases/${MAJOR}.x.x/${MAJOR_MINOR}.x/${VERSION}" + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT + echo "podspec_version=$PODSPEC_VERSION" >> $GITHUB_OUTPUT + echo "is_rc=true" >> $GITHUB_OUTPUT + echo "is_valid=true" >> $GITHUB_OUTPUT + echo "base_branch=$BASE_BRANCH_INPUT" >> $GITHUB_OUTPUT + echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT + echo "ios_sdk_version=$IOS_VER" >> $GITHUB_OUTPUT + echo "android_sdk_version=$AND_VER" >> $GITHUB_OUTPUT + echo "deploy_to_qa=$DEPLOY_TO_QA" >> $GITHUB_OUTPUT + echo "dry_run=$DRY_RUN_INPUT" >> $GITHUB_OUTPUT + # =========================================================================== # Job 2: Run CI Pipeline # =========================================================================== @@ -162,121 +159,120 @@ jobs: secrets: inherit # =========================================================================== - # Job 3: Update Version Files - # =========================================================================== - # Updates pubspec.yaml and other version-related files + # Job 3: Create/Update Release Branch and Apply Changes # =========================================================================== - update-version: - name: πŸ“ Update Version Files + prepare-branch: + name: 🌿 Create Release Branch & Apply Changes runs-on: ubuntu-latest needs: [validate-release, run-ci] if: always() && needs.validate-release.outputs.is_valid == 'true' - + outputs: + release_branch: ${{ steps.push.outputs.release_branch }} steps: - - name: πŸ“₯ Checkout repository + - name: πŸ“₯ Checkout base branch uses: actions/checkout@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 # Fetch all history for proper tagging - + ref: ${{ needs.validate-release.outputs.base_branch }} + fetch-depth: 0 + - name: πŸ”§ Setup Flutter SDK uses: subosito/flutter-action@v2 with: channel: 'stable' cache: true - - - name: πŸ“ Update pubspec.yaml version + + - name: 🌿 Create release branch + id: branch run: | - VERSION="${{ needs.validate-release.outputs.version }}" - - # Remove -rcN suffix for pubspec.yaml (pub.dev doesn't support pre-release tags) - # But preserve +build suffix if present (pub.dev supports build numbers) - PUBSPEC_VERSION=$(echo $VERSION | sed 's/-rc[0-9]*$//') - - echo "Full version: $VERSION" - echo "Updating pubspec.yaml to version: $PUBSPEC_VERSION" - - # Update version in pubspec.yaml - sed -i.bak "s/^version: .*/version: $PUBSPEC_VERSION/" pubspec.yaml + set -e + REL_BRANCH="${{ needs.validate-release.outputs.release_branch }}" + echo "Target release branch: $REL_BRANCH" + if git ls-remote --exit-code --heads origin "$REL_BRANCH" >/dev/null 2>&1; then + echo "Branch already exists on remote. Checking it out." + git fetch origin "$REL_BRANCH":"$REL_BRANCH" + git checkout "$REL_BRANCH" + else + git checkout -b "$REL_BRANCH" + fi + + - name: πŸ“ Update pubspec.yaml version (RC full) + run: | + VERSION='${{ needs.validate-release.outputs.version }}' + echo "Setting pubspec.yaml version to $VERSION (includes -rcN)" + sed -i.bak "s/^version: .*/version: $VERSION/" pubspec.yaml rm pubspec.yaml.bak - - # Verify the change - echo "Updated pubspec.yaml:" grep "^version:" pubspec.yaml - - - name: πŸ“ Update Android plugin version constant + + - name: πŸ“ Update Android SDK dependency run: | - VERSION="${{ needs.validate-release.outputs.version }}" - - # Find and update kPluginVersion in Android constants - ANDROID_CONSTANTS_FILE="android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerConstants.java" - - if [ -f "$ANDROID_CONSTANTS_FILE" ]; then - echo "Updating Android plugin version to: $VERSION" - sed -i.bak "s/kPluginVersion = \".*\"/kPluginVersion = \"$VERSION\"/" "$ANDROID_CONSTANTS_FILE" - rm "${ANDROID_CONSTANTS_FILE}.bak" - - echo "Updated Android constants:" - grep "kPluginVersion" "$ANDROID_CONSTANTS_FILE" || echo "Pattern not found" - else - echo "⚠️ Android constants file not found, skipping" - fi - - - name: πŸ“ Update iOS plugin version constant + AND_VER='${{ needs.validate-release.outputs.android_sdk_version }}' + sed -i.bak "s/com.appsflyer:af-android-sdk:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/com.appsflyer:af-android-sdk:${AND_VER}/" android/build.gradle + rm android/build.gradle.bak + grep "af-android-sdk:" -n android/build.gradle | head -1 + + - name: πŸ“ Update iOS podspec version and dependencies run: | - VERSION="${{ needs.validate-release.outputs.version }}" - - # Find and update kPluginVersion in iOS constants - IOS_PLUGIN_FILE="ios/Classes/AppsflyerSdkPlugin.m" - - if [ -f "$IOS_PLUGIN_FILE" ]; then - echo "Updating iOS plugin version to: $VERSION" - sed -i.bak "s/kPluginVersion = @\".*\"/kPluginVersion = @\"$VERSION\"/" "$IOS_PLUGIN_FILE" - rm "${IOS_PLUGIN_FILE}.bak" - - echo "Updated iOS plugin file:" - grep "kPluginVersion" "$IOS_PLUGIN_FILE" || echo "Pattern not found" + PODSPEC_VERSION='${{ needs.validate-release.outputs.podspec_version }}' + IOS_VER='${{ needs.validate-release.outputs.ios_sdk_version }}' + FILE='ios/appsflyer_sdk.podspec' + if [ -f "$FILE" ]; then + sed -i.bak "s/s\.version\s*=\s*'.*'/s.version = '${PODSPEC_VERSION}'/" "$FILE" + sed -i.bak "s/ss\.ios\.dependency 'AppsFlyerFramework','[^']*'/ss.ios.dependency 'AppsFlyerFramework','${IOS_VER}'/" "$FILE" + # PurchaseConnector line may or may not exist + if grep -q "PurchaseConnector', '" "$FILE"; then + sed -i.bak "s/ss\.ios\.dependency 'PurchaseConnector', '[^']*'/ss.ios.dependency 'PurchaseConnector', '${IOS_VER}'/" "$FILE" || true + fi + rm ${FILE}.bak || true + echo "Updated podspec lines:" + grep -n "s.version\|AppsFlyerFramework\|PurchaseConnector" "$FILE" || true else - echo "⚠️ iOS plugin file not found, skipping" + echo "⚠️ $FILE not found" fi - - - name: πŸ“ Update podspec version + + - name: πŸ“ Update plugin version constants (Android/iOS) run: | - VERSION="${{ needs.validate-release.outputs.version }}" - # Remove -rcN suffix but preserve +build for podspec - PODSPEC_VERSION=$(echo $VERSION | sed 's/-rc[0-9]*$//') - - PODSPEC_FILE="ios/appsflyer_sdk.podspec" - - if [ -f "$PODSPEC_FILE" ]; then - echo "Full version: $VERSION" - echo "Updating podspec to version: $PODSPEC_VERSION" - sed -i.bak "s/s\.version.*=.*/s.version = '$PODSPEC_VERSION'/" "$PODSPEC_FILE" - rm "${PODSPEC_FILE}.bak" - - echo "Updated podspec:" - grep "s.version" "$PODSPEC_FILE" - else - echo "⚠️ Podspec file not found, skipping" + VERSION='${{ needs.validate-release.outputs.version }}' + AND_FILE="android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerConstants.java" + IOS_FILE="ios/Classes/AppsflyerSdkPlugin.m" + if [ -f "$AND_FILE" ]; then + sed -i.bak "s/kPluginVersion = \".*\"/kPluginVersion = \"$VERSION\"/" "$AND_FILE" && rm "$AND_FILE.bak" || true fi - - - name: πŸ’Ύ Commit version changes + if [ -f "$IOS_FILE" ]; then + sed -i.bak "s/kPluginVersion = @\".*\"/kPluginVersion = @\"$VERSION\"/" "$IOS_FILE" && rm "$IOS_FILE.bak" || true + fi + + - name: πŸ“ Update README SDK versions run: | - VERSION="${{ needs.validate-release.outputs.version }}" - - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - - # Check if there are changes to commit + IOS_VER='${{ needs.validate-release.outputs.ios_sdk_version }}' + AND_VER='${{ needs.validate-release.outputs.android_sdk_version }}' + sed -i.bak -E "s/- Android AppsFlyer SDK \*\*v[0-9.]+\*\*/- Android AppsFlyer SDK **v${AND_VER}**/" README.md + sed -i.bak -E "s/- iOS AppsFlyer SDK \*\*v[0-9.]+\*\*/- iOS AppsFlyer SDK **v${IOS_VER}**/" README.md + rm README.md.bak + echo "README updated SDK versions:" + sed -n '12,20p' README.md | cat + + - name: πŸ’Ύ Commit & push changes + id: push + run: | + set -e + REL_BRANCH='${{ needs.validate-release.outputs.release_branch }}' + 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: bump version to $VERSION" - git push - echo "βœ… Version changes committed and pushed" + git add pubspec.yaml android/ ios/ README.md || true + git commit -m "chore: prepare RC ${VERSION} (iOS ${{ needs.validate-release.outputs.ios_sdk_version }}, Android ${{ needs.validate-release.outputs.android_sdk_version }})" + git push --set-upstream origin "$REL_BRANCH" else - echo "ℹ️ No version changes to commit" + echo "No changes to commit" + # Ensure branch exists on remote + if ! git ls-remote --exit-code --heads origin "$REL_BRANCH" >/dev/null 2>&1; then + git push --set-upstream origin "$REL_BRANCH" + fi fi + echo "release_branch=$REL_BRANCH" >> $GITHUB_OUTPUT + + # (Deprecated) Legacy update-version job removed; handled by prepare-branch # =========================================================================== # Job 4: Create Pre-Release @@ -287,15 +283,15 @@ jobs: create-prerelease: name: 🏷️ Create Pre-Release runs-on: ubuntu-latest - needs: [validate-release, run-ci, update-version] - if: always() && needs.validate-release.outputs.is_rc == 'true' + needs: [validate-release, run-ci, prepare-branch] + if: always() && needs.validate-release.outputs.is_rc == 'true' && needs.validate-release.outputs.deploy_to_qa == 'true' steps: - name: πŸ“₯ Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ github.ref }} + ref: ${{ needs.prepare-branch.outputs.release_branch }} - name: πŸ“ Generate release notes id: release-notes @@ -361,6 +357,90 @@ jobs: # Sends notification to Slack channel about the RC release # =========================================================================== + open-pr: + name: πŸ”€ Open PR to master + runs-on: ubuntu-latest + needs: [validate-release, prepare-branch] + if: always() && needs.validate-release.outputs.deploy_to_qa == 'true' + steps: + - name: πŸ“₯ Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.prepare-branch.outputs.release_branch }} + - name: 🧠 Create or update PR + uses: actions/github-script@v7 + with: + script: | + const version = '${{ needs.validate-release.outputs.base_version }}'; + const head = '${{ needs.prepare-branch.outputs.release_branch }}'; + const base = 'master'; + // Find existing PR + const { data: prs } = await github.rest.pulls.list({ owner: context.repo.owner, repo: context.repo.repo, state: 'open', head: `${context.repo.owner}:${head}` }); + const body = `### Release ${version}\n\n- Android SDK: ${{ needs.validate-release.outputs.android_sdk_version }}\n- iOS SDK: ${{ needs.validate-release.outputs.ios_sdk_version }}\n\nThis PR was opened by the RC workflow.`; + if (prs.length > 0) { + const pr = prs[0]; + await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, title: `Release ${version}`, body }); + core.setOutput('pr_number', pr.number); + } else { + const { data: pr } = await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, head, base, title: `Release ${version}`, body, maintainer_can_modify: true }); + core.setOutput('pr_number', pr.number); + } + + publish-rc: + name: πŸ“¦ Publish RC to pub.dev + runs-on: ubuntu-latest + needs: [validate-release, prepare-branch] + if: always() && needs.validate-release.outputs.deploy_to_qa == 'true' + steps: + - name: πŸ“₯ Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.prepare-branch.outputs.release_branch }} + - name: πŸ”§ Setup Flutter SDK + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + - name: πŸ“¦ Get dependencies + run: flutter pub get + - name: πŸ“ Validate package (dry-run) + run: | + # Run dry-run and capture output + # Exit code 65 = warnings only (acceptable for RC builds since CHANGELOG won't have -rcN suffix) + # 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 (expected for RC builds)" + echo "Note: CHANGELOG warning for RC version suffix is expected" + else + echo "❌ Package validation failed with exit code $EXIT_CODE" + exit $EXIT_CODE + fi + - name: ℹ️ RC dry-run active β€” skipping publish + if: ${{ needs.validate-release.outputs.dry_run == 'true' }} + run: | + echo "RC dry_run is true β€” will not publish to pub.dev." + - name: πŸš€ Publish RC to pub.dev + if: ${{ needs.validate-release.outputs.dry_run != 'true' }} + env: + PUB_DEV_CREDENTIALS: ${{ secrets.PUB_DEV_CREDENTIALS }} + run: | + if [[ -z "${PUB_DEV_CREDENTIALS}" ]]; then + echo "PUB_DEV_CREDENTIALS is missing"; exit 1; fi + mkdir -p ~/.config/dart + echo "${PUB_DEV_CREDENTIALS}" > ~/.config/dart/pub-credentials.json + flutter pub publish --force + rm -f ~/.config/dart/pub-credentials.json + notify-team: name: πŸ“’ Notify Team runs-on: ubuntu-latest @@ -521,7 +601,7 @@ jobs: rc-summary: name: πŸ“‹ RC Summary runs-on: ubuntu-latest - needs: [validate-release, run-ci, update-version, create-prerelease] + needs: [validate-release, run-ci, prepare-branch, create-prerelease] if: always() steps: @@ -536,7 +616,7 @@ jobs: echo "-----------------------------------------" echo "Validation: ${{ needs.validate-release.result }}" echo "CI Pipeline: ${{ needs.run-ci.result }}" - echo "Version Update: ${{ needs.update-version.result }}" + echo "Prepare Branch: ${{ needs.prepare-branch.result }}" echo "Pre-Release: ${{ needs.create-prerelease.result }}" echo "=========================================" diff --git a/.pubignore b/.pubignore new file mode 100644 index 00000000..b04bfdbe --- /dev/null +++ b/.pubignore @@ -0,0 +1,23 @@ +# Files to exclude from pub.dev package + +# Development/build files +covBadgeGen.js +coverage_badge.svg +package.json +package-lock.json +node_modules/ +local.properties +.cursor/ + +# Example app files that shouldn't be in package +example/.env +example/.metadata +example/build/ +example/ios/Podfile.lock +example/ios/Pods/ +example/ios/Flutter/ephemeral/ +example/android/local.properties + +# CI/CD files +.github/ +.travis.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6724bb44..77abd659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Versions +## 6.17.8 + +- Updated Android SDK from 6.17.4 to 6.17.5 +- Updated iOS SDK from 6.17.7 to 6.17.8 +- Updated iOS Purchase Connector from 6.17.7 to 6.17.8 +- Deprecated `validateAndLogInAppAndroidPurchase` (V1) - use `validateAndLogInAppPurchaseV2` instead +- Deprecated `validateAndLogInAppIosPurchase` (V1) - use `validateAndLogInAppPurchaseV2` instead +- Enhanced iOS error handling for `validateAndLogInAppPurchaseV2` with comprehensive NSError parsing (code, domain, userInfo) +- **Documentation Updates:** + - Removed "Beta" label from `validateAndLogInAppPurchaseV2` API + - Marked V1 purchase validation APIs as Deprecated + - Added comprehensive `PlatformException` error handling examples for V2 API + - Added iOS token format explanation for uninstall measurement + - Added cross-platform Firebase Messaging example for uninstall tokens + ## 6.17.7+1 - Update Android SDK version to 6.17.4 diff --git a/README.md b/README.md index eddca451..78385505 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.3** -- iOS AppsFlyer SDK **v6.17.7** +- Android AppsFlyer SDK **v6.17.5** +- iOS AppsFlyer SDK **v6.17.8** ### Purchase Connector versions - Android 2.2.0 -- iOS 6.17.7 +- iOS 6.17.8 ## ❗❗ Breaking changes when updating to v6.x.x❗❗ diff --git a/android/build.gradle b/android/build.gradle index 3ce2527f..db95fa52 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.4' + implementation 'com.appsflyer:af-android-sdk:6.17.5' 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 59af0662..0b05df7c 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.7+1"; + final static String PLUGIN_VERSION = "6.17.8"; 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/doc/AdvancedAPI.md b/doc/AdvancedAPI.md index d46b8dd8..3c624d03 100644 --- a/doc/AdvancedAPI.md +++ b/doc/AdvancedAPI.md @@ -39,16 +39,47 @@ You can register the uninstall token with AppsFlyer by calling the following API appsFlyerSdk.updateServerUninstallToken("token"); ``` +> **Note:** When using this method on iOS, the token should be passed as a **hexadecimal string representation** of the device token. The plugin will automatically convert the hex string to the required `NSData` format for the AppsFlyer SDK. +> +> If you're using the [firebase_messaging](https://pub.dev/packages/firebase_messaging) plugin, you can get the APNs token on iOS using `FirebaseMessaging.instance.getAPNSToken()` which returns the token as a hex string, which is the expected format for this method. + ### Android It is possible to utilize the [Firebase Messaging Plugin for Flutter](https://pub.dev/packages/firebase_messaging) for everything related to the uninstall token. You can read more about Android Uninstall Measurement in our [knowledge base](https://support.appsflyer.com/hc/en-us/articles/4408933557137) and you can follow our guide for Uninstall measurement using FCM on our [DevHub](https://dev.appsflyer.com/hc/docs/uninstall-measurement-android). -On the flutter side, you can register the uninstall token with AppsFlyer by calling the following API with your uninstall token: +On the Flutter side, you can register the uninstall token with AppsFlyer by calling the following API with your uninstall token: ```dart appsFlyerSdk.updateServerUninstallToken("token"); ``` +**Example using Firebase Messaging (cross-platform):** +```dart +import 'dart:io' show Platform; +import 'package:firebase_messaging/firebase_messaging.dart'; + +// Update uninstall token for AppsFlyer +void _updateUninstallToken(appsFlyerSdk) { + if (Platform.isAndroid) { + FirebaseMessaging.instance.getToken().then((token) { + if (token != null) { + appsFlyerSdk.updateServerUninstallToken(token); + } + }); + } else if (Platform.isIOS) { + FirebaseMessaging.instance.getAPNSToken().then((token) { + if (token != null) { + appsFlyerSdk.updateServerUninstallToken(token); + } + }); + } +} +``` +**Note:** +- On Android, `getToken()` returns the FCM token. +- On iOS, `getAPNSToken()` returns the APNs token as a hex string, suitable for `updateServerUninstallToken`. +- Replace `appsFlyerSdk` with your instance of `AppsflyerSdk`. + --- ## User invite @@ -114,11 +145,9 @@ appsFlyerSdk.generateInviteLink(inviteLinkParams, Receipt validation is a secure mechanism whereby the payment platform (e.g. Apple or Google) validates that an in-app purchase indeed occurred as reported.
Learn more - https://support.appsflyer.com/hc/en-us/articles/207032106-Receipt-validation-for-in-app-purchases
-**Cross-Platform V2 API (Recommended - SDK v6.17.3+) - BETA:** +**Cross-Platform V2 API (Recommended - SDK v6.17.3+):** -> ⚠️ **BETA Feature**: This API is currently in beta. While it's stable and recommended for new implementations, please test thoroughly in your environment before production use. - -The new unified purchase validation API that works across both Android and iOS platforms: +The unified purchase validation API that works across both Android and iOS platforms: ```dart Future> validateAndLogInAppPurchaseV2( @@ -135,7 +164,7 @@ AFPurchaseDetails( ) ``` -Example: +**Example:** ```dart // Create purchase details AFPurchaseDetails purchaseDetails = AFPurchaseDetails( @@ -151,24 +180,40 @@ try { additionalParameters: {"custom_param": "value"} ); print("Validation successful: $result"); +} on PlatformException catch (e) { + // Handle platform-specific errors with detailed information + print("Validation failed: ${e.message}"); + print("Error code: ${e.code}"); + if (e.details != null) { + // Access detailed error information + final details = e.details as Map; + print("Error details: $details"); + // On iOS, additional fields may include: + // - error_code: The NSError code + // - error_domain: The NSError domain + // - error_user_info: Additional error context + } } catch (e) { - print("Validation failed: $e"); + print("Unexpected error: $e"); } ``` **Benefits of V2 API:** - βœ… **Cross-platform**: Single API works on both Android and iOS - βœ… **Type-safe**: Uses structured data classes instead of raw strings -- βœ… **Better error handling**: Returns structured error information +- βœ… **Comprehensive error handling**: Returns structured error information including NSError details on iOS - βœ… **Enhanced validation**: Uses AppsFlyer's latest validation infrastructure - βœ… **Future-proof**: Built for AppsFlyer's V2 validation endpoints --- -**Legacy Platform-Specific APIs:** +**Deprecated Platform-Specific APIs:** + +> ⚠️ **Deprecated**: The following platform-specific APIs are deprecated and will be removed in a future version. Please migrate to `validateAndLogInAppPurchaseV2` for cross-platform support. -**Android:** +**Android (Deprecated):** ```dart +@Deprecated('Use validateAndLogInAppPurchaseV2 instead') Future validateAndLogInAppAndroidPurchase( String publicKey, String signature, @@ -179,6 +224,7 @@ Future validateAndLogInAppAndroidPurchase( ``` Example: ```dart +// Deprecated - migrate to validateAndLogInAppPurchaseV2 appsFlyerSdk.validateAndLogInAppAndroidPurchase( "publicKey", "signature", @@ -188,12 +234,13 @@ appsFlyerSdk.validateAndLogInAppAndroidPurchase( {"fs": "fs"}); ``` -**iOS:** +**iOS (Deprecated):** ❗Important❗ for iOS - set SandBox to ```true```
```appsFlyer.useReceiptValidationSandbox(true);``` ```dart +@Deprecated('Use validateAndLogInAppPurchaseV2 instead') Future validateAndLogInAppIosPurchase( String productIdentifier, String price, @@ -204,6 +251,7 @@ Future validateAndLogInAppIosPurchase( Example: ```dart +// Deprecated - migrate to validateAndLogInAppPurchaseV2 appsFlyerSdk.validateAndLogInAppIosPurchase( "productIdentifier", "price", diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c569640..1dc6cf76 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d795332e..95d6e55f 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> validateAndLogInAppAndroidPurchase( String publicKey, String signature, @@ -423,7 +427,11 @@ class AppsflyerSdk { }); } - ///Accessing AppsFlyer purchase validation data + /// Accessing AppsFlyer purchase validation data for iOS. + /// + /// @Deprecated Use [validateAndLogInAppPurchaseV2] instead. This API will be removed in a future version. + @Deprecated( + 'Use validateAndLogInAppPurchaseV2 instead for cross-platform purchase validation') Future validateAndLogInAppIosPurchase( String productIdentifier, String price, diff --git a/pubspec.yaml b/pubspec.yaml index 58be8e9c..6e99d99f 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.7+1 +version: 6.17.8-rc1 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk From 02bfa046debb2b73fa0ce4cb9e1f6f7cbca282fa Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:17:19 +0200 Subject: [PATCH 2/4] hotfix: remove -rc1 suffix from version 6.17.8 (#432) Version in pubspec.yaml was incorrectly merged with RC suffix. This fixes the version to be production-ready. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6e99d99f..f00c0561 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-rc1 +version: 6.17.8 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk From 91a6133f4a7b06ed75efa5febedf70bb8f7d0f87 Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 1 Feb 2026 16:10:55 +0200 Subject: [PATCH 3/4] Convert plugin to singelton + delegate --- .../appsflyersdk/AppsflyerSdkPlugin.java | 125 ++++++++++++++++-- .../AppsflyerSdkPluginDelegate.java | 87 ++++++++++++ .../appsflyer/appsflyersdk/PluginLogger.java | 67 ++++++++++ example/lib/main_page.dart | 28 ++-- pubspec.yaml | 2 +- 5 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPluginDelegate.java create mode 100644 android/src/main/java/com/appsflyer/appsflyersdk/PluginLogger.java diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 27c19675..0dbf6846 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -59,9 +59,21 @@ import androidx.annotation.NonNull; /** - * AppsflyerSdkPlugin + * AppsflyerSdkPlugin - Singleton implementation of the AppsFlyer Flutter plugin. + * + * This class holds all the plugin logic and state. It is designed as a singleton to ensure + * state is preserved when the Flutter engine is recreated (e.g., when the app goes to + * background via back button and returns to foreground). + * + * For Flutter plugin registration, use {@link AppsflyerSdkPluginDelegate} which delegates + * all calls to this singleton instance. + * + * @see AppsflyerSdkPluginDelegate */ public class AppsflyerSdkPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { + // Singleton instance to ensure plugin reuse across engine recreations + private static AppsflyerSdkPlugin instance; + // RD-65582 private static boolean saveCallbacks; private static Map cachedOnConversionDataSuccess; @@ -89,9 +101,34 @@ public class AppsflyerSdkPlugin implements MethodCallHandler, FlutterPlugin, Act private Boolean isSetDisableAdvertisingIdentifiersEnable = false; private Map> mCallbacks = new HashMap<>(); + /** + * Returns the singleton instance of the plugin. + * Creates a new instance if one doesn't exist. + * + * @return The singleton AppsflyerSdkPlugin instance. + */ + public static synchronized AppsflyerSdkPlugin getInstance() { + if (instance == null) { + PluginLogger.d("Singleton: Creating new AppsflyerSdkPlugin instance"); + instance = new AppsflyerSdkPlugin(); + } else { + PluginLogger.d("Singleton: Returning existing AppsflyerSdkPlugin instance"); + } + return instance; + } + + /** + * Private constructor to enforce singleton pattern. + * Use {@link #getInstance()} to get the singleton instance. + */ + private AppsflyerSdkPlugin() { + PluginLogger.d("Singleton: AppsflyerSdkPlugin constructor called"); + } + PluginRegistry.NewIntentListener onNewIntentListener = new PluginRegistry.NewIntentListener() { @Override public boolean onNewIntent(Intent intent) { + PluginLogger.d("onNewIntent: Received new intent"); activity.setIntent(intent); return false; } @@ -100,11 +137,14 @@ public boolean onNewIntent(Intent intent) { private final AppsFlyerConversionListener afConversionListener = new AppsFlyerConversionListener() { @Override public void onConversionDataSuccess(Map map) { + PluginLogger.d("ConversionListener: onConversionDataSuccess received"); if (saveCallbacks) { + PluginLogger.d("ConversionListener: Caching conversion data (activity detached)"); cachedOnConversionDataSuccess = map; return; } if (gcdCallback) { + PluginLogger.d("ConversionListener: Sending conversion data to Flutter"); JSONObject dataObj = new JSONObject(replaceNullValues(map)); runOnUIThread(dataObj, AppsFlyerConstants.AF_GCD_CALLBACK, AF_SUCCESS); } @@ -112,7 +152,9 @@ public void onConversionDataSuccess(Map map) { @Override public void onConversionDataFail(String s) { + PluginLogger.d("ConversionListener: onConversionDataFail - " + s); if (saveCallbacks) { + PluginLogger.d("ConversionListener: Caching conversion failure (activity detached)"); cachedOnConversionDataFail = s; return; } @@ -124,12 +166,15 @@ public void onConversionDataFail(String s) { @Override public void onAppOpenAttribution(Map map) { + PluginLogger.d("ConversionListener: onAppOpenAttribution received"); if (saveCallbacks) { + PluginLogger.d("ConversionListener: Caching app open attribution (activity detached)"); cachedOnAppOpenAttribution = map; return; } Map objMap = (Map) map; if (oaoaCallback) { + PluginLogger.d("ConversionListener: Sending app open attribution to Flutter"); JSONObject obj = new JSONObject(replaceNullValues(objMap)); runOnUIThread(obj, AppsFlyerConstants.AF_OAOA_CALLBACK, AF_SUCCESS); } @@ -137,7 +182,9 @@ public void onAppOpenAttribution(Map map) { @Override public void onAttributionFailure(String errorMessage) { + PluginLogger.d("ConversionListener: onAttributionFailure - " + errorMessage); if (saveCallbacks) { + PluginLogger.d("ConversionListener: Caching attribution failure (activity detached)"); cachedOnAttributionFailure = errorMessage; return; } @@ -151,11 +198,14 @@ public void onAttributionFailure(String errorMessage) { @Override public void onDeepLinking(DeepLinkResult deepLinkResult) { + PluginLogger.d("DeepLinkListener: onDeepLinking - status: " + deepLinkResult.getStatus()); if (saveCallbacks) { + PluginLogger.d("DeepLinkListener: Caching deep link result (activity detached)"); cachedDeepLinkResult = deepLinkResult; return; } if (udlCallback) { + PluginLogger.d("DeepLinkListener: Sending deep link to Flutter"); runOnUIThread(deepLinkResult, AppsFlyerConstants.AF_UDL_CALLBACK, AF_SUCCESS); } } @@ -174,18 +224,20 @@ public void onMethodCall(MethodCall call, Result result) { }; private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { + PluginLogger.d("Setting up method channels"); this.mContext = applicationContext; this.mEventChannel = new EventChannel(messenger, AF_EVENTS_CHANNEL); mMethodChannel = new MethodChannel(messenger, AppsFlyerConstants.AF_METHOD_CHANNEL); mMethodChannel.setMethodCallHandler(this); mCallbackChannel = new MethodChannel(messenger, AppsFlyerConstants.AF_CALLBACK_CHANNEL); mCallbackChannel.setMethodCallHandler(callbacksHandler); + PluginLogger.d("Method channels setup complete"); } private void startListening(Object arguments, Result rawResult) { - // Get callback id String callbackName = (String) arguments; + PluginLogger.d("startListening: Registering callback - " + callbackName); if (callbackName.equals(AppsFlyerConstants.AF_GCD_CALLBACK)) { gcdCallback = true; } @@ -207,7 +259,9 @@ private void startListening(Object arguments, Result rawResult) { @Override public void onMethodCall(MethodCall call, Result result) { + PluginLogger.d("onMethodCall: " + call.method); if (activity == null) { + PluginLogger.d("onMethodCall: Activity is null, returning error"); Log.d(AF_PLUGIN_TAG, LogMessages.ACTIVITY_NOT_ATTACHED_TO_ENGINE); result.error("NO_ACTIVITY", "The current activity is null", null); return; @@ -393,12 +447,14 @@ private void anonymizeUser(MethodCall call, Result result) { } private void startSDKwithHandler(MethodCall call, final Result result) { + PluginLogger.d("startSDKwithHandler: Starting SDK with response handler"); try { final AppsFlyerLib appsFlyerLib = AppsFlyerLib.getInstance(); appsFlyerLib.start(activity, null, new AppsFlyerRequestListener() { @Override public void onSuccess() { + PluginLogger.d("startSDKwithHandler: SDK started successfully"); uiThreadHandler.post(() -> { if (mMethodChannel != null) { mMethodChannel.invokeMethod("onSuccess", null); @@ -410,6 +466,7 @@ public void onSuccess() { @Override public void onError(final int errorCode, final String errorMessage) { + PluginLogger.d("startSDKwithHandler: SDK start failed - code: " + errorCode + ", message: " + errorMessage); uiThreadHandler.post(() -> { if (mMethodChannel != null) { HashMap errorDetails = new HashMap<>(); @@ -424,6 +481,7 @@ public void onError(final int errorCode, final String errorMessage) { }); result.success(null); } catch (Throwable t) { + PluginLogger.d("startSDKwithHandler: Unexpected error - " + t.getMessage()); result.error("UNEXPECTED_ERROR", t.getMessage(), null); } } @@ -433,8 +491,10 @@ public void onError(final int errorCode, final String errorMessage) { * only submitted once, preventing the "Reply already submitted" exception in Flutter. */ private void startSDK(MethodCall call, final Result result) { + PluginLogger.d("startSDK: Starting SDK"); final AppsFlyerLib instance = AppsFlyerLib.getInstance(); instance.start(activity); + PluginLogger.d("startSDK: SDK started"); result.success(null); } @@ -1047,54 +1107,69 @@ private void setHost(MethodCall call, MethodChannel.Result result) { } private void initSdk(MethodCall call, final MethodChannel.Result result) { + PluginLogger.d("initSdk: Starting SDK initialization"); AppsFlyerConversionListener gcdListener = null; DeepLinkListener udlListener = null; AppsFlyerLib instance = AppsFlyerLib.getInstance(); boolean isManualStartMode = (boolean) call.argument(AppsFlyerConstants.AF_MANUAL_START); + PluginLogger.d("initSdk: manualStart=" + isManualStartMode); String afDevKey = (String) call.argument(AppsFlyerConstants.AF_DEV_KEY); if (afDevKey == null || afDevKey.equals("")) { + PluginLogger.d("initSdk: ERROR - Dev key is empty"); Log.e(AF_PLUGIN_TAG, LogMessages.AF_DEV_KEY_IS_EMPTY); result.error("INIT_ERROR", LogMessages.AF_DEV_KEY_IS_EMPTY, null); return; } + PluginLogger.d("initSdk: Dev key configured"); boolean advertiserIdDisabled = (boolean) call.argument(AppsFlyerConstants.DISABLE_ADVERTISING_IDENTIFIER); if (advertiserIdDisabled) { + PluginLogger.d("initSdk: Disabling advertising identifiers"); instance.setDisableAdvertisingIdentifiers(true); } boolean getGCD = (boolean) call.argument(AppsFlyerConstants.AF_GCD); if (getGCD) { + PluginLogger.d("initSdk: Registering conversion data listener"); gcdListener = afConversionListener; } // added Unified deeplink boolean getUdl = (boolean) call.argument(AppsFlyerConstants.AF_UDL); if (getUdl) { + PluginLogger.d("initSdk: Subscribing for deep links (UDL)"); instance.subscribeForDeepLink(afDeepLinkListener); } boolean isDebug = (boolean) call.argument(AppsFlyerConstants.AF_IS_DEBUG); if (isDebug) { - instance.setLogLevel(AFLogger.LogLevel.DEBUG); + PluginLogger.d("initSdk: Debug mode enabled"); + PluginLogger.setDebugLoggingEnabled(true); // Enable plugin logging when SDK debug is on instance.setDebugLog(true); + instance.setLogLevel(AFLogger.LogLevel.VERBOSE); } else { instance.setDebugLog(false); } PluginInfo pluginInfo = new PluginInfo(Plugin.FLUTTER, AppsFlyerConstants.PLUGIN_VERSION); instance.setPluginInfo(pluginInfo); + PluginLogger.d("initSdk: Plugin version " + AppsFlyerConstants.PLUGIN_VERSION); instance.init(afDevKey, gcdListener, mContext); + PluginLogger.d("initSdk: SDK initialized"); String appInviteOneLink = (String) call.argument(AppsFlyerConstants.AF_APP_INVITE_ONE_LINK); if (appInviteOneLink != null) { + PluginLogger.d("initSdk: Setting app invite OneLink: " + appInviteOneLink); instance.setAppInviteOneLink(appInviteOneLink); } if (!isManualStartMode) { + PluginLogger.d("initSdk: Auto-starting SDK"); instance.start(activity); + } else { + PluginLogger.d("initSdk: Manual start mode - SDK not started yet"); } if (saveCallbacks) { @@ -1102,16 +1177,18 @@ private void initSdk(MethodCall call, final MethodChannel.Result result) { sendCachedCallbacksToDart(); } + PluginLogger.d("initSdk: Initialization complete"); result.success("success"); } private void logEvent(MethodCall call, MethodChannel.Result result) { - AppsFlyerLib instance = AppsFlyerLib.getInstance(); final String eventName = call.argument(AppsFlyerConstants.AF_EVENT_NAME); final Map eventValues = call.argument(AppsFlyerConstants.AF_EVENT_VALUES); + PluginLogger.d("logEvent: " + eventName + " with " + (eventValues != null ? eventValues.size() : 0) + " parameters"); + // Send event data through appsflyer sdk instance.logEvent(mContext, eventName, eventValues); @@ -1170,23 +1247,36 @@ private T requireNonNullArgument(MethodCall call, String argumentName) throw //RD-65582 private void sendCachedCallbacksToDart() { + boolean hasCachedData = cachedDeepLinkResult != null || cachedOnConversionDataSuccess != null + || cachedOnAppOpenAttribution != null || cachedOnAttributionFailure != null + || cachedOnConversionDataFail != null; + + if (hasCachedData) { + PluginLogger.d("Singleton: Sending cached callbacks to Dart"); + } + if (cachedDeepLinkResult != null) { + PluginLogger.d("Singleton: Sending cached deep link result"); afDeepLinkListener.onDeepLinking(cachedDeepLinkResult); cachedDeepLinkResult = null; } if (cachedOnConversionDataSuccess != null) { + PluginLogger.d("Singleton: Sending cached conversion data success"); afConversionListener.onConversionDataSuccess(cachedOnConversionDataSuccess); cachedOnConversionDataSuccess = null; } if (cachedOnAppOpenAttribution != null) { + PluginLogger.d("Singleton: Sending cached app open attribution"); afConversionListener.onAppOpenAttribution(cachedOnAppOpenAttribution); cachedOnAppOpenAttribution = null; } if (cachedOnAttributionFailure != null) { + PluginLogger.d("Singleton: Sending cached attribution failure"); afConversionListener.onAttributionFailure(cachedOnAttributionFailure); cachedOnAttributionFailure = null; } if (cachedOnConversionDataFail != null) { + PluginLogger.d("Singleton: Sending cached conversion data fail"); afConversionListener.onConversionDataFail(cachedOnConversionDataFail); cachedOnConversionDataFail = null; } @@ -1225,23 +1315,34 @@ private void disableAppSetId(MethodCall call, Result result) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { + PluginLogger.d("Singleton: onAttachedToEngine - setting up channels"); onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); AppsFlyerPurchaseConnector.INSTANCE.onAttachedToEngine(binding); } @Override public void onDetachedFromEngine(FlutterPluginBinding binding) { - mMethodChannel.setMethodCallHandler(null); - mMethodChannel = null; - mEventChannel.setStreamHandler(null); - mEventChannel = null; + PluginLogger.d("Singleton: onDetachedFromEngine - cleaning up channels"); + if (mMethodChannel != null) { + mMethodChannel.setMethodCallHandler(null); + mMethodChannel = null; + } + if (mEventChannel != null) { + mEventChannel.setStreamHandler(null); + mEventChannel = null; + } + if (mCallbackChannel != null) { + mCallbackChannel.setMethodCallHandler(null); + mCallbackChannel = null; + } AppsFlyerPurchaseConnector.INSTANCE.onDetachedFromEngine(binding); - mContext = null; - mApplication = null; + // Note: Don't null out mContext and mApplication to preserve state for potential reattachment + PluginLogger.d("Singleton: onDetachedFromEngine - channels cleaned, preserving context for reattachment"); } @Override public void onAttachedToActivity(ActivityPluginBinding binding) { + PluginLogger.d("Singleton: onAttachedToActivity - activity: " + binding.getActivity().getClass().getSimpleName()); activity = binding.getActivity(); mApplication = binding.getActivity().getApplication(); binding.addOnNewIntentListener(onNewIntentListener); @@ -1249,11 +1350,13 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { @Override public void onDetachedFromActivityForConfigChanges() { + PluginLogger.d("Singleton: onDetachedFromActivityForConfigChanges - clearing activity reference"); this.activity = null; } @Override public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + PluginLogger.d("Singleton: onReattachedToActivityForConfigChanges - restoring activity: " + binding.getActivity().getClass().getSimpleName()); sendCachedCallbacksToDart(); binding.addOnNewIntentListener(onNewIntentListener); activity = binding.getActivity(); @@ -1261,9 +1364,9 @@ public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding @Override public void onDetachedFromActivity() { + PluginLogger.d("Singleton: onDetachedFromActivity - clearing activity, enabling callback caching"); activity = null; saveCallbacks = true; - AppsFlyerLib.getInstance().unregisterConversionListener(); } } diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPluginDelegate.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPluginDelegate.java new file mode 100644 index 00000000..bf638a4d --- /dev/null +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPluginDelegate.java @@ -0,0 +1,87 @@ +package com.appsflyer.appsflyersdk; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +/** + * Delegate class for AppsflyerSdkPlugin. + * + * Flutter's plugin registration mechanism creates new instances of plugins when the engine + * is recreated (e.g., when the app goes to background via back button and returns to foreground). + * + * This delegate class is the entry point that Flutter instantiates. It delegates all calls + * to the singleton {@link AppsflyerSdkPlugin} instance, ensuring state is preserved across + * engine recreations. + * + * Note: This class must be registered in the plugin's build.gradle or GeneratedPluginRegistrant + * instead of AppsflyerSdkPlugin directly. + */ +public class AppsflyerSdkPluginDelegate implements FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler { + + private static int instanceCounter = 0; + private final int instanceId; + + public AppsflyerSdkPluginDelegate() { + instanceId = ++instanceCounter; + PluginLogger.d("Delegate #" + instanceId + " created (Flutter instantiated a new delegate)"); + } + + /** + * Gets the singleton implementation instance. + */ + private AppsflyerSdkPlugin getImpl() { + return AppsflyerSdkPlugin.getInstance(); + } + + // ==================== FlutterPlugin ==================== + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + PluginLogger.d("Delegate #" + instanceId + " onAttachedToEngine -> delegating to singleton"); + getImpl().onAttachedToEngine(binding); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + PluginLogger.d("Delegate #" + instanceId + " onDetachedFromEngine -> delegating to singleton"); + getImpl().onDetachedFromEngine(binding); + } + + // ==================== ActivityAware ==================== + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + PluginLogger.d("Delegate #" + instanceId + " onAttachedToActivity -> delegating to singleton"); + getImpl().onAttachedToActivity(binding); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + PluginLogger.d("Delegate #" + instanceId + " onDetachedFromActivityForConfigChanges -> delegating to singleton"); + getImpl().onDetachedFromActivityForConfigChanges(); + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + PluginLogger.d("Delegate #" + instanceId + " onReattachedToActivityForConfigChanges -> delegating to singleton"); + getImpl().onReattachedToActivityForConfigChanges(binding); + } + + @Override + public void onDetachedFromActivity() { + PluginLogger.d("Delegate #" + instanceId + " onDetachedFromActivity -> delegating to singleton"); + getImpl().onDetachedFromActivity(); + } + + // ==================== MethodCallHandler ==================== + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + getImpl().onMethodCall(call, result); + } +} diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/PluginLogger.java b/android/src/main/java/com/appsflyer/appsflyersdk/PluginLogger.java new file mode 100644 index 00000000..4f7b98f0 --- /dev/null +++ b/android/src/main/java/com/appsflyer/appsflyersdk/PluginLogger.java @@ -0,0 +1,67 @@ +package com.appsflyer.appsflyersdk; + +import android.util.Log; + +import java.lang.reflect.Method; + +import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_PLUGIN_TAG; + +/** + * Utility class for plugin logging controlled by system property. + * + * Enable logging via ADB (requires app restart): + * adb shell setprop debug.appsflyer.flutter true + * + * Disable logging via ADB (requires app restart): + * adb shell setprop debug.appsflyer.flutter false + * + * Or enable programmatically: + * PluginLogger.setDebugLoggingEnabled(true) + */ +public final class PluginLogger { + + private static final String SYSTEM_PROP_KEY = "debug.appsflyer.flutter"; + + // Read system property once at class load time + private static boolean debugEnabled = readSystemProperty(); + + private PluginLogger() { + // Prevent instantiation + } + + /** + * Enable or disable debug logging. + * + * @param enabled true to enable logging, false to disable + */ + public static void setDebugLoggingEnabled(boolean enabled) { + debugEnabled = enabled; + } + + /** + * Log a debug message if logging is enabled. + * + * @param message The message to log + */ + public static void d(String message) { + if (debugEnabled) { + Log.d(AF_PLUGIN_TAG, message); + } + } + + /** + * Read the system property using reflection (called once at class load). + * + * @return true if the property is set to "true", false otherwise + */ + private static boolean readSystemProperty() { + try { + Class systemPropertiesClass = Class.forName("android.os.SystemProperties"); + Method getMethod = systemPropertiesClass.getMethod("get", String.class, String.class); + String value = (String) getMethod.invoke(null, SYSTEM_PROP_KEY, "false"); + return "true".equalsIgnoreCase(value); + } catch (Exception e) { + return false; + } + } +} diff --git a/example/lib/main_page.dart b/example/lib/main_page.dart index a3fd1130..b67758a2 100644 --- a/example/lib/main_page.dart +++ b/example/lib/main_page.dart @@ -33,7 +33,7 @@ class MainPageState extends State { appId: dotenv.env["APP_ID"]!, showDebug: true, timeToWaitForATTUserAuthorization: 15, - manualStart: true); + manualStart: false); /* final Map? map = { 'afDevKey': dotenv.env["DEV_KEY"]!, @@ -56,15 +56,11 @@ class MainPageState extends State { _appsflyerSdk.setConsentData(nonGdpr); */ - // Init of AppsFlyer SDK - await _appsflyerSdk.initSdk( - registerConversionDataCallback: true, - registerOnAppOpenAttributionCallback: true, - registerOnDeepLinkingCallback: true); // Conversion data callback _appsflyerSdk.onInstallConversionData((res) { print("onInstallConversionData res: $res"); + showMessage("onInstallConversionData res: $res"); setState(() { _gcd = res; }); @@ -73,6 +69,7 @@ class MainPageState extends State { // App open attribution callback _appsflyerSdk.onAppOpenAttribution((res) { print("onAppOpenAttribution res: $res"); + showMessage("onAppOpenAttribution res: $res"); setState(() { _deepLinkData = res; }); @@ -83,28 +80,39 @@ class MainPageState extends State { switch (dp.status) { case Status.FOUND: print(dp.deepLink?.toString()); + showMessage(dp.deepLink?.toString() ?? "nil"); print("deep link value: ${dp.deepLink?.deepLinkValue}"); break; case Status.NOT_FOUND: print("deep link not found"); + showMessage("deep link not found"); break; case Status.ERROR: print("deep link error: ${dp.error}"); + showMessage("deep link error: ${dp.error}"); break; case Status.PARSE_ERROR: print("deep link status parsing error"); + showMessage("deep link status parsing error"); break; } print("onDeepLinking res: $dp"); + showMessage("onDeepLinking res: $dp"); setState(() { _deepLinkData = dp.toJson(); }); }); + // Init of AppsFlyer SDK + await _appsflyerSdk.initSdk( + registerConversionDataCallback: true, + registerOnAppOpenAttributionCallback: true, + registerOnDeepLinkingCallback: false); - //_appsflyerSdk.anonymizeUser(true); - if (Platform.isAndroid) { - _appsflyerSdk.performOnDeepLinking(); - } + + // //_appsflyerSdk.anonymizeUser(true); + // if (Platform.isAndroid) { + // _appsflyerSdk.performOnDeepLinking(); + // } setState(() {}); // Call setState to rebuild the widget } diff --git a/pubspec.yaml b/pubspec.yaml index f00c0561..132791c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ flutter: platforms: android: package: com.appsflyer.appsflyersdk - pluginClass: AppsflyerSdkPlugin + pluginClass: AppsflyerSdkPluginDelegate ios: pluginClass: AppsflyerSdkPlugin package: com.appsflyer.appsflyersdk From 4eabf14f3a7dd5bebd5688432eedac2bef03c53c Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 1 Feb 2026 16:16:26 +0200 Subject: [PATCH 4/4] debug logs --- .../java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 0dbf6846..6ab0cce4 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -1144,10 +1144,10 @@ private void initSdk(MethodCall call, final MethodChannel.Result result) { boolean isDebug = (boolean) call.argument(AppsFlyerConstants.AF_IS_DEBUG); if (isDebug) { - PluginLogger.d("initSdk: Debug mode enabled"); PluginLogger.setDebugLoggingEnabled(true); // Enable plugin logging when SDK debug is on + PluginLogger.d("initSdk: Debug mode enabled"); + instance.setLogLevel(AFLogger.LogLevel.DEBUG); instance.setDebugLog(true); - instance.setLogLevel(AFLogger.LogLevel.VERBOSE); } else { instance.setDebugLog(false); }