diff --git a/.github/git-release.sh b/.github/git-release.sh index c8d903e15..5b0b7a514 100755 --- a/.github/git-release.sh +++ b/.github/git-release.sh @@ -1,5 +1,5 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail podspec_file="Mindbox.podspec" version=$(grep -E '^\s+spec.version\s+=' "$podspec_file" | cut -d'"' -f2) @@ -9,7 +9,8 @@ if [[ $version == *"rc"* ]]; then is_beta=true fi -branch=${GITHUB_REF#refs/heads/} +branch=${RELEASE_BRANCH:-${GITHUB_REF:-}} +branch=${branch#refs/heads/} echo "Version: $version" echo "Is Beta: $is_beta" @@ -18,29 +19,32 @@ echo "Branch: $branch" echo "Fetching tags from remote repository." git fetch --tags -if git rev-parse "$version" >/dev/null 2>&1; then +if git rev-parse -q --verify "refs/tags/$version" >/dev/null; then echo "Local tag $version already exists." else echo "Creating local tag $version." - git tag $version + git tag "$version" fi -if git ls-remote --tags origin | grep -q "refs/tags/$version"; then +if [ -n "$(git ls-remote --tags origin "refs/tags/$version")" ]; then echo "Remote tag $version already exists." else echo "Pushing local tag $version to remote." - git push origin $version + git push origin "$version" fi # Create release release_title="Release $version" -text="Autogenerated release. Check more details at https://developers.mindbox.ru/docs/ios-sdk-integration" +text="Autogenerated release. Check more details at https://developers.mindbox.ru/docs/ios-sdk" echo "Release title: $release_title" echo "Text: $text" -prerelease_flag=$([ "$is_beta" = true ] && echo "--prerelease" || echo "") +release_args=("$version" --target "$branch" --title "$release_title" --notes "$text") +if [ "$is_beta" = true ]; then + release_args+=(--prerelease) +fi -gh release create "$version" --target "$branch" --title "$release_title" --notes "$text" $prerelease_flag +gh release create "${release_args[@]}" echo "Release $version created for repo on branch $branch" diff --git a/.github/pod-trunk-push.sh b/.github/pod-trunk-push.sh new file mode 100755 index 000000000..6083f783f --- /dev/null +++ b/.github/pod-trunk-push.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Wrapper around `pod trunk push` that treats a 409 Conflict +# ("Unable to accept duplicate entry") from trunk as success. +# +# Why: trunk sometimes returns 504 Gateway Timeout while the spec +# still lands in CocoaPods/Specs (Heroku 30s router cap on the +# GitHub Commits API call). Without this wrapper, re-running the +# failed publish job fails again with 409 "duplicate entry", +# keeping the workflow red even though the release is actually done. + +set -uo pipefail + +if [ $# -lt 1 ]; then + echo "usage: $0 [pod trunk push args...]" >&2 + exit 64 +fi + +PODSPEC="$1"; shift + +set +e +OUT="$(pod trunk push "$PODSPEC" "$@" 2>&1)" +RC=$? +set -e +printf '%s\n' "$OUT" + +if [ "$RC" -eq 0 ]; then + exit 0 +fi + +if grep -qE 'HTTP/1\.[01] 409 Conflict|Unable to accept duplicate entry' <<<"$OUT"; then + echo "[pod-trunk-push] trunk returned 409 — version already published, treating as success" + exit 0 +fi + +exit "$RC" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 005788e32..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,29 +0,0 @@ -Ticket: https://tracker.yandex.ru/MOBILE- - -## Summary - - - -## Type of Change - -- [ ] Feature -- [ ] Bug fix -- [ ] Refactor -- [ ] Tests -- [ ] CI/CD -- [ ] Dependencies -- [ ] Other: ___ - -## Changes - - - -- -- - -## Testing - - - -- [ ] Unit tests added/updated -- [ ] Manual testing performed diff --git a/.github/scripts/generate_coverage_site.py b/.github/scripts/generate_coverage_site.py new file mode 100644 index 000000000..53dd054d2 --- /dev/null +++ b/.github/scripts/generate_coverage_site.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Builds a static coverage site (index.html + history.json + coverage.csv) from an .xcresult. + +Usage: generate_coverage_site.py [previous_history.json] + +Coverage comes from `xcrun xccov view --report --json`; *.xctest bundles are +excluded so the numbers reflect the shipped frameworks only. History is carried +forward by the caller: the workflow downloads the currently published +history.json from the live Pages site and passes it in; this script appends +today's point and ships the merged file with the new site, so the trend +survives even though every Pages deploy replaces the whole site. + +The page is fully self-contained: inline CSS/JS, no external resources. +""" +import csv +import html +import json +import os +import subprocess +import sys +from datetime import datetime, timezone + +HISTORY_LIMIT = 1000 + + +def pct(value): + return f"{value * 100:.1f}%" + + +def bar(value): + width = round(value * 100, 1) + color = "#2da44e" if value >= 0.7 else "#d4a72c" if value >= 0.4 else "#cf222e" + return (f'
') + + +def load_history(path): + if not path or not os.path.exists(path): + return [] + try: + history = json.load(open(path)) + if not isinstance(history, list): + return [] + except (json.JSONDecodeError, OSError): + return [] + # Points written before the ts field existed carry only the human-readable + # UTC string; synthesize ts from it so all dates render uniformly. + for point in history: + if "ts" not in point: + try: + parsed = datetime.strptime(point.get("date", ""), "%Y-%m-%d %H:%M UTC") + point["ts"] = parsed.replace(tzinfo=timezone.utc).isoformat(timespec="seconds") + except ValueError: + pass + return history + + +def repo_relative(path, repo): + workspace = os.environ.get("GITHUB_WORKSPACE") + if workspace and path.startswith(workspace + "/"): + return path[len(workspace) + 1:] + marker = f"/{repo.rsplit('/', 1)[-1]}/" + idx = path.rfind(marker) + return path[idx + len(marker):] if idx >= 0 else None + + +def trend_svg(history): + points = [p["coverage"] for p in history][-90:] + if len(points) < 2: + return "

Trend appears after the second deploy.

" + lo, hi = min(points), max(points) + pad = max((hi - lo) * 0.15, 0.5) + lo, hi = lo - pad, hi + pad + w, h = 640, 120 + step = w / (len(points) - 1) + coords = " ".join(f"{i * step:.1f},{h - (v - lo) / (hi - lo) * h:.1f}" for i, v in enumerate(points)) + return (f'' + f'' + f'

last {len(points)} deploys, {points[0]:.1f}% → {points[-1]:.1f}%

') + + +def write_csv(path, targets, repo): + with open(path, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["target", "file", "path", "executable_lines", + "covered_lines", "uncovered_lines", "line_coverage_percent"]) + for t in targets: + for fl in t.get("files", []): + writer.writerow([ + t["name"], fl["name"], + repo_relative(fl.get("path", ""), repo) or fl.get("path", ""), + fl["executableLines"], fl["coveredLines"], + fl["executableLines"] - fl["coveredLines"], + round(fl["lineCoverage"] * 100, 2), + ]) + + +def main(): + xcresult, outdir = sys.argv[1], sys.argv[2] + previous_history = sys.argv[3] if len(sys.argv) > 3 else None + repo = os.environ.get("GITHUB_REPOSITORY", "mindbox-cloud/ios-sdk") + branch = os.environ.get("GITHUB_REF_NAME", "local") + sha = os.environ.get("GITHUB_SHA", "local")[:9] + now = datetime.now(timezone.utc) + date = now.strftime("%Y-%m-%d %H:%M UTC") + iso = now.isoformat(timespec="seconds") + + report = json.loads(subprocess.run( + ["xcrun", "xccov", "view", "--report", "--json", xcresult], + check=True, capture_output=True).stdout) + targets = [t for t in report["targets"] if not t["name"].endswith(".xctest")] + executable = sum(t["executableLines"] for t in targets) + covered = sum(t["coveredLines"] for t in targets) + overall = covered / executable if executable else 0.0 + + history = load_history(previous_history) + if not history or history[-1].get("sha") != sha: + history.append({ + "date": date, + "ts": iso, + "branch": branch, + "sha": sha, + "coverage": round(overall * 100, 2), + "targets": {t["name"]: round(t["lineCoverage"] * 100, 2) for t in targets}, + }) + history = history[-HISTORY_LIMIT:] + + target_rows = "".join( + f"{html.escape(t['name'])}{pct(t['lineCoverage'])}" + f"{bar(t['lineCoverage'])}{t['coveredLines']}/{t['executableLines']}" + for t in targets) + + file_sections = [] + for t in targets: + rows = [] + # Impact order: most uncovered lines first (size x (1 - coverage)), + # percentage as the tie-breaker. + for f in sorted(t.get("files", []), + key=lambda f: (f["coveredLines"] - f["executableLines"], f["lineCoverage"])): + name = html.escape(f["name"]) + rel = repo_relative(f.get("path", ""), repo) + cell = (f'{name}' + if rel else name) + rows.append( + f'{cell}' + f"{f['executableLines'] - f['coveredLines']}" + f"{pct(f['lineCoverage'])}{bar(f['lineCoverage'])}" + f"{f['coveredLines']}/{f['executableLines']}") + file_sections.append( + f"
{html.escape(t['name'])} — {pct(t['lineCoverage'])}, " + f"{len(t.get('files', []))} files (most uncovered lines first)" + f"{''.join(rows)}
FileUncoveredCoverageLines
") + + history_rows = "".join( + f"{html.escape(p['date'])}" + f"{html.escape(p['sha'])}" + f"{p['coverage']:.2f}%" + for p in reversed(history[-30:])) + + page = f""" +Mindbox iOS SDK — Code Coverage + + +

Mindbox iOS SDK — Code Coverage

+

{pct(overall)}

+

{branch} @ {sha} · {date} · test bundles excluded

+

Export: per-file CSV · trend JSON

+

Targets

+{target_rows}
TargetCoverageLines
+

Trend

+{trend_svg(history)} +

Files

+

+ + + +

+{''.join(file_sections)} +

Recent deploys

+{history_rows}
DateCommitTotal
+ + +""" + os.makedirs(outdir, exist_ok=True) + with open(os.path.join(outdir, "index.html"), "w") as f: + f.write(page) + with open(os.path.join(outdir, "history.json"), "w") as f: + json.dump(history, f, indent=1) + write_csv(os.path.join(outdir, "coverage.csv"), targets, repo) + print(f"coverage site: {pct(overall)} overall, {len(targets)} targets, " + f"{len(history)} history points -> {outdir}/") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/distribute-develop-mission.yml b/.github/workflows/distribute-develop-mission.yml index ad456db10..1ada48d22 100644 --- a/.github/workflows/distribute-develop-mission.yml +++ b/.github/workflows/distribute-develop-mission.yml @@ -8,10 +8,14 @@ on: types: - closed +permissions: + contents: read + jobs: call-reusable: if: ${{ github.event.pull_request.merged == true }} uses: ./.github/workflows/distribute-reusable.yml with: branch: ${{ github.base_ref }} - secrets: inherit + secrets: + GITLAB_TRIGGER_TOKEN: ${{ secrets.GITLAB_TRIGGER_TOKEN }} diff --git a/.github/workflows/distribute-manual.yml b/.github/workflows/distribute-manual.yml index 5b0bee0f9..ef11334b3 100644 --- a/.github/workflows/distribute-manual.yml +++ b/.github/workflows/distribute-manual.yml @@ -8,10 +8,14 @@ on: required: false default: "" +permissions: + contents: read + jobs: call-reusable: uses: ./.github/workflows/distribute-reusable.yml with: branch: ${{ github.ref_name }} # SDK branch = current branch app_ref: ${{ inputs.app_ref }} # optional override for GitLab ref - secrets: inherit + secrets: + GITLAB_TRIGGER_TOKEN: ${{ secrets.GITLAB_TRIGGER_TOKEN }} diff --git a/.github/workflows/distribute-release-support-mission.yml b/.github/workflows/distribute-release-support-mission.yml index 06ffa4cb3..0c22fc879 100644 --- a/.github/workflows/distribute-release-support-mission.yml +++ b/.github/workflows/distribute-release-support-mission.yml @@ -10,10 +10,14 @@ on: - opened - synchronize +permissions: + contents: read + jobs: call-reusable: if: ${{ startsWith(github.event.pull_request.head.ref, 'release/') }} uses: ./.github/workflows/distribute-reusable.yml with: branch: ${{ github.event.pull_request.head.ref }} - secrets: inherit + secrets: + GITLAB_TRIGGER_TOKEN: ${{ secrets.GITLAB_TRIGGER_TOKEN }} diff --git a/.github/workflows/distribute-reusable.yml b/.github/workflows/distribute-reusable.yml index 6dfe4a47c..300c689bf 100644 --- a/.github/workflows/distribute-reusable.yml +++ b/.github/workflows/distribute-reusable.yml @@ -14,12 +14,15 @@ on: GITLAB_TRIGGER_TOKEN: required: true +permissions: + contents: read + jobs: distribution: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} fetch-depth: 3 @@ -29,9 +32,12 @@ jobs: run: | set -euo pipefail commits="$(git log -3 --pretty=format:"%s")" - echo "commits<> "$GITHUB_ENV" - echo "$commits" >> "$GITHUB_ENV" - echo "EOF" >> "$GITHUB_ENV" + delim="EOF_$(openssl rand -hex 8)" + { + echo "commits<<$delim" + echo "$commits" + echo "$delim" + } >> "$GITHUB_ENV" - name: Get Mindbox SDK Version shell: bash diff --git a/.github/workflows/gitleaks-secrets-validate.yml b/.github/workflows/gitleaks-secrets-validate.yml new file mode 100644 index 000000000..31312bc20 --- /dev/null +++ b/.github/workflows/gitleaks-secrets-validate.yml @@ -0,0 +1,23 @@ +name: Gitleaks Secrets Validate +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + scan: + name: gitleaks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + + - name: Run Gitleaks + uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_LICENSE: ${{ secrets.MINDBOX_GITLEAKS_LICENSE }} diff --git a/.github/workflows/linter_and_unit_tests.yml b/.github/workflows/linter_and_unit_tests.yml index c344f76e2..35404015c 100644 --- a/.github/workflows/linter_and_unit_tests.yml +++ b/.github/workflows/linter_and_unit_tests.yml @@ -11,21 +11,28 @@ on: - reopened - synchronize +permissions: + contents: read + jobs: SwiftLint: runs-on: ubuntu-latest container: - image: ghcr.io/realm/swiftlint:latest + image: ghcr.io/realm/swiftlint:0.63.3@sha256:b70b763c949399cc3e4fdcfbc7588944d9503d498b6d96e853dfa07a944256e6 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - run: swiftlint --config .swiftlint.yml --reporter github-actions-logging --strict build: runs-on: macos-26 timeout-minutes: 20 + permissions: + contents: read + checks: write + pull-requests: write steps: - - uses: actions/checkout@v6 - - uses: maxim-lobanov/setup-xcode@v1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: xcode-version: latest-stable @@ -35,8 +42,80 @@ jobs: run: bundle install - name: Install yeetd run: | - wget https://github.com/biscuitehh/yeetd/releases/download/1.0/yeetd-normal.pkg + YEETD_SHA256="adb04eb720581a32d94daef3ea61108c74494083a79f00836556a36214f354be" + wget -q https://github.com/biscuitehh/yeetd/releases/download/1.0/yeetd-normal.pkg + echo "$YEETD_SHA256 yeetd-normal.pkg" | shasum -a 256 -c - sudo installer -pkg yeetd-normal.pkg -target / yeetd & - name: Run unit tests run: bundle exec fastlane unitTestLane + + # trainer builds report.junit from the xcresult, so Swift Testing results + # survive regardless of the console formatter. Only failures are listed + # (include_passed: false), so the PR comment stays compact even on green runs. + - name: Publish unit-test summary + uses: mikepenz/action-junit-report@3a81627bfac62268172037048872e8ebd4207e6d # v6.4.1 + if: ${{ !cancelled() }} + with: + report_paths: test_output/report.junit + check_name: Unit tests report + detailed_summary: true + include_passed: false + include_time_in_summary: true + group_suite: true + flaky_summary: true + skip_success_summary: false + simplified_summary: true + include_empty_in_summary: false + verbose_summary: false + require_tests: true + fail_on_failure: true + fail_on_parse_error: true + comment: ${{ github.event_name == 'pull_request' }} + updateComment: true + skip_comment_without_tests: true + + - name: Upload xcresult bundle + id: xcresult + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: unit-tests-xcresult + path: test_output/*.xcresult + + # Surface the artifact link in the job summary - GitHub otherwise only lists + # artifacts at the very bottom of the run page, easy to miss on failed runs. + - name: Link xcresult in summary + if: ${{ !cancelled() }} + env: + ART_URL: ${{ steps.xcresult.outputs.artifact-url }} + run: echo "⬇️ [Download Mindbox.xcresult]($ART_URL) — open in Xcode for coverage & full logs" >> "$GITHUB_STEP_SUMMARY" + + # Coverage site for GitHub Pages. The previously published history.json is + # carried forward so the trend survives full-site redeploys. + - name: Generate coverage site + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }} + run: | + curl -fsSL "https://mindbox-cloud.github.io/ios-sdk/history.json" -o /tmp/prev-history.json || echo "[]" > /tmp/prev-history.json + python3 .github/scripts/generate_coverage_site.py test_output/Mindbox.xcresult coverage-site /tmp/prev-history.json + + - name: Upload Pages artifact + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }} + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: coverage-site + + deploy-coverage: + runs-on: ubuntu-latest + needs: build + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }} + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deploy + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/manual-prepare_release_branch.yml b/.github/workflows/manual-prepare_release_branch.yml index 7271fb421..bb3f2a581 100644 --- a/.github/workflows/manual-prepare_release_branch.yml +++ b/.github/workflows/manual-prepare_release_branch.yml @@ -15,14 +15,19 @@ on: required: true default: 'master' +permissions: + contents: read + jobs: validate-input: name: Validate release_version format runs-on: ubuntu-latest steps: - name: Check version matches semver or semver-rc + env: + RELEASE_VERSION: ${{ github.event.inputs.release_version }} run: | - VER="${{ github.event.inputs.release_version }}" + VER="$RELEASE_VERSION" echo "→ release_version = $VER" if ! [[ "$VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc)?$ ]]; then echo "❌ release_version must be X.Y.Z or X.Y.Z-rc" @@ -35,22 +40,26 @@ jobs: needs: validate-input steps: - name: Checkout minimal repo - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Validate source branch exists + env: + SOURCE_BRANCH: ${{ github.event.inputs.source_branch }} run: | - SRC="${{ github.event.inputs.source_branch }}" - if ! git ls-remote --heads origin "$SRC" | grep -q "$SRC"; then + SRC="$SOURCE_BRANCH" + if [ -z "$(git ls-remote --heads origin "refs/heads/$SRC")" ]; then echo "❌ source_branch '$SRC' does not exist on origin" exit 1 fi - name: Validate target branch exists + env: + TARGET_BRANCH: ${{ github.event.inputs.target_branch }} run: | - DST="${{ github.event.inputs.target_branch }}" - if ! git ls-remote --heads origin "$DST" | grep -q "$DST"; then + DST="$TARGET_BRANCH" + if [ -z "$(git ls-remote --heads origin "refs/heads/$DST")" ]; then echo "❌ target_branch '$DST' does not exist on origin" exit 1 fi @@ -59,20 +68,25 @@ jobs: name: Create release branch & bump version runs-on: macos-26 needs: validate-branches + permissions: + contents: write outputs: release_branch: ${{ steps.bump.outputs.release_branch }} steps: - name: Checkout source branch - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.inputs.source_branch }} fetch-depth: 0 - name: Create release branch & bump version id: bump + env: + RELEASE_VERSION: ${{ github.event.inputs.release_version }} + SOURCE_BRANCH: ${{ github.event.inputs.source_branch }} run: | - VERSION="${{ github.event.inputs.release_version }}" - SRC="${{ github.event.inputs.source_branch }}" + VERSION="$RELEASE_VERSION" + SRC="$SOURCE_BRANCH" REL="release/$VERSION" echo "→ Branching from $SRC into $REL" @@ -81,10 +95,12 @@ jobs: echo "→ Running bump script on $REL" ./.github/git-release-branch-create.sh "$VERSION" - echo "release_branch=$REL" >> $GITHUB_OUTPUT + echo "release_branch=$REL" >> "$GITHUB_OUTPUT" - name: Push release branch - run: git push origin "${{ steps.bump.outputs.release_branch }}" + env: + RELEASE_BRANCH: ${{ steps.bump.outputs.release_branch }} + run: git push origin "$RELEASE_BRANCH" check_sdk_version: name: Check SDK Version @@ -92,7 +108,7 @@ jobs: needs: bump_and_branch steps: - name: Checkout the release branch - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ needs.bump_and_branch.outputs.release_branch }} fetch-depth: 0 @@ -101,8 +117,10 @@ jobs: run: git pull - name: Validate sdkVersion + env: + RELEASE_VERSION: ${{ github.event.inputs.release_version }} run: | - EXPECT="${{ github.event.inputs.release_version }}" + EXPECT="$RELEASE_VERSION" SDK_VERSION=$(sed -n 's/^.*sdkVersion = "\(.*\)"/\1/p' SDKVersionProvider/SDKVersionProvider.swift) echo "→ Found in code: $SDK_VERSION, expected: $EXPECT" if [ "$SDK_VERSION" != "$EXPECT" ]; then @@ -121,11 +139,12 @@ jobs: SRC: ${{ needs.bump_and_branch.outputs.release_branch }} DST: ${{ github.event.inputs.target_branch }} REPO: ${{ github.repository }} + RELEASE_VERSION: ${{ github.event.inputs.release_version }} run: | echo "→ Creating PR from $SRC into $DST" gh pr create \ --repo "$REPO" \ --base "$DST" \ --head "$SRC" \ - --title "Release ${{ github.event.inputs.release_version }}" \ - --body "Updates the release version to ${{ github.event.inputs.release_version }}. Automated PR: merge $SRC into $DST" + --title "Release $RELEASE_VERSION" \ + --body "Updates the release version to $RELEASE_VERSION. Automated PR: merge $SRC into $DST" diff --git a/.github/workflows/prepare_release_branch.yml b/.github/workflows/prepare_release_branch.yml index d49ec3265..ebb90db96 100644 --- a/.github/workflows/prepare_release_branch.yml +++ b/.github/workflows/prepare_release_branch.yml @@ -6,6 +6,9 @@ on: - 'release/*.*.*' - 'support/*.*.*' +permissions: + contents: read + jobs: extract_version: if: github.event.created @@ -16,31 +19,39 @@ jobs: steps: - name: Extract version from branch name id: extract + env: + REF_NAME: ${{ github.ref_name }} run: | - BRANCH_NAME="${{ github.ref_name }}" + BRANCH_NAME="$REF_NAME" echo "BRANCH_NAME: $BRANCH_NAME" VERSION="${BRANCH_NAME#release/}" VERSION="${VERSION#support/}" echo "VERSION: $VERSION" - echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" bump_version: name: Bump Version runs-on: macos-26 needs: extract_version + permissions: + contents: write outputs: version2: ${{ steps.bump.outputs.version2 }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Bump version - run: ./.github/git-release-branch-create.sh "${{ needs.extract_version.outputs.version }}" + env: + VERSION: ${{ needs.extract_version.outputs.version }} + run: ./.github/git-release-branch-create.sh "$VERSION" - - name: Ouput version + - name: Output version id: bump + env: + VERSION: ${{ needs.extract_version.outputs.version }} run: | - echo "version2=${{ needs.extract_version.outputs.version }}" >> $GITHUB_OUTPUT + echo "version2=$VERSION" >> "$GITHUB_OUTPUT" check_sdk_version: name: Check SDK Version @@ -48,7 +59,7 @@ jobs: needs: bump_version steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 @@ -56,10 +67,12 @@ jobs: run: git pull - name: Check if sdkVersion matches VERSION + env: + VERSION2: ${{ needs.bump_version.outputs.version2 }} run: | SDK_VERSION=$(sed -n 's/^.*sdkVersion = "\(.*\)"/\1/p' SDKVersionProvider/SDKVersionProvider.swift) - if [ "$SDK_VERSION" != "${{ needs.bump_version.outputs.version2 }}" ]; then - echo "SDK version ($SDK_VERSION) does not match the branch version (${{ needs.bump_version.outputs.version2 }})." + if [ "$SDK_VERSION" != "$VERSION2" ]; then + echo "SDK version ($SDK_VERSION) does not match the branch version ($VERSION2)." exit 1 fi shell: bash @@ -70,17 +83,18 @@ jobs: needs: check_sdk_version steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.PAT_FOR_TRIGGERING_BRANCH_PROTECTION }} + REF_NAME: ${{ github.ref_name }} run: | gh pr create \ --base master \ - --head ${{ github.ref_name }} \ - --title "${{ github.ref_name }}" \ - --body "Updates the release version to ${{ github.ref_name }}" + --head "$REF_NAME" \ + --title "$REF_NAME" \ + --body "Updates the release version to $REF_NAME" PR_URL=$(gh pr view --json url --jq '.url') - echo "PR_URL=$PR_URL" >> $GITHUB_ENV - env: - GH_TOKEN: ${{ secrets.PAT_FOR_TRIGGERING_BRANCH_PROTECTION }} + echo "PR_URL=$PR_URL" >> "$GITHUB_ENV" diff --git a/.github/workflows/publish-from-master-or-support.yml b/.github/workflows/publish-from-master-or-support.yml index adebf2edc..0de170655 100644 --- a/.github/workflows/publish-from-master-or-support.yml +++ b/.github/workflows/publish-from-master-or-support.yml @@ -7,10 +7,17 @@ on: - 'master' - 'support/*' +permissions: + contents: read + jobs: call-reusable: if: ${{ github.event.pull_request.merged == true }} + permissions: + contents: write uses: ./.github/workflows/publish-reusable.yml with: branch: ${{ github.base_ref }} - secrets: inherit + secrets: + COCOAPODS_TOKEN: ${{ secrets.COCOAPODS_TOKEN }} + PAT_FOR_TRIGGERING_BRANCH_PROTECTION: ${{ secrets.PAT_FOR_TRIGGERING_BRANCH_PROTECTION }} diff --git a/.github/workflows/publish-manual.yml b/.github/workflows/publish-manual.yml index eb9a6d46c..d51966532 100644 --- a/.github/workflows/publish-manual.yml +++ b/.github/workflows/publish-manual.yml @@ -3,21 +3,30 @@ name: SDK publish RC manual on: workflow_dispatch: +permissions: + contents: read + jobs: check-branch: name: Check RC pattern for publish runs-on: ubuntu-latest steps: - name: Check if branch matches pattern + env: + REF_NAME: ${{ github.ref_name }} run: | - if ! echo "${{ github.ref_name }}" | grep -q "release/.*-rc"; then + if ! echo "$REF_NAME" | grep -q "release/.*-rc"; then echo "Branch name must match pattern 'release/*-rc' (e.g. release/2.13.2-rc)" exit 1 fi call-publish-reusable: needs: check-branch + permissions: + contents: write uses: ./.github/workflows/publish-reusable.yml with: branch: ${{ github.ref_name }} - secrets: inherit + secrets: + COCOAPODS_TOKEN: ${{ secrets.COCOAPODS_TOKEN }} + PAT_FOR_TRIGGERING_BRANCH_PROTECTION: ${{ secrets.PAT_FOR_TRIGGERING_BRANCH_PROTECTION }} diff --git a/.github/workflows/publish-reusable.yml b/.github/workflows/publish-reusable.yml index caef3660d..02d4ce435 100644 --- a/.github/workflows/publish-reusable.yml +++ b/.github/workflows/publish-reusable.yml @@ -6,15 +6,23 @@ on: branch: required: true type: string + secrets: + COCOAPODS_TOKEN: + required: true + PAT_FOR_TRIGGERING_BRANCH_PROTECTION: + required: true + +permissions: + contents: read jobs: unit-tests: runs-on: macos-26 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} - - uses: maxim-lobanov/setup-xcode@v1 + - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: xcode-version: latest-stable - name: Update bundler @@ -23,29 +31,45 @@ jobs: run: bundle install - name: Run unit tests run: bundle exec fastlane unitTestLane + - name: Upload xcresult bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() }} + with: + name: unit-tests-xcresult + path: test_output/*.xcresult set-tag: needs: [unit-tests] runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} - name: Extract SDK version run: | SDK_VERSION=$(sed -n 's/^.*sdkVersion = "\(.*\)"/\1/p' SDKVersionProvider/SDKVersionProvider.swift) - echo "SDK_VERSION=$SDK_VERSION" >> $GITHUB_ENV + echo "SDK_VERSION=$SDK_VERSION" >> "$GITHUB_ENV" echo "Extracted SDK version: $SDK_VERSION" - name: Create tag run: | - git tag ${{ env.SDK_VERSION }} - git push origin ${{ env.SDK_VERSION }} + if git rev-parse -q --verify "refs/tags/$SDK_VERSION" >/dev/null; then + echo "Local tag $SDK_VERSION already exists." + else + git tag "$SDK_VERSION" + fi + if [ -n "$(git ls-remote --tags origin "refs/tags/$SDK_VERSION")" ]; then + echo "Remote tag $SDK_VERSION already exists." + else + git push origin "$SDK_VERSION" + fi publish-MindboxLogger: needs: [set-tag] runs-on: macos-26 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} - name: Update bundler @@ -55,7 +79,7 @@ jobs: - name: Deploy to Cocoapods MindboxLogger run: | pod lib lint MindboxLogger.podspec --allow-warnings - pod trunk push MindboxLogger.podspec --allow-warnings --verbose + ./.github/pod-trunk-push.sh MindboxLogger.podspec --allow-warnings --verbose env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TOKEN }} @@ -70,14 +94,14 @@ jobs: needs: [delay] runs-on: macos-26 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} - name: Update bundler run: gem install bundler - name: Install bundler dependencies run: bundle install - - uses: nick-fields/retry@v4 + - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 with: timeout_minutes: 10 max_attempts: 20 @@ -91,7 +115,7 @@ jobs: needs: [check-podspecs-with-retry] runs-on: macos-26 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} - name: Update bundler @@ -103,7 +127,7 @@ jobs: pod repo update set -eo pipefail pod lib lint MindboxNotifications.podspec --allow-warnings - pod trunk push MindboxNotifications.podspec --allow-warnings --verbose + ./.github/pod-trunk-push.sh MindboxNotifications.podspec --allow-warnings --verbose env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TOKEN }} @@ -118,7 +142,7 @@ jobs: needs: [second-delay] runs-on: macos-26 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} - name: Update bundler @@ -130,21 +154,24 @@ jobs: pod repo update set -eo pipefail pod lib lint Mindbox.podspec --allow-warnings - pod trunk push Mindbox.podspec --allow-warnings --verbose + ./.github/pod-trunk-push.sh Mindbox.podspec --allow-warnings --verbose env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TOKEN }} release-github: needs: [publish-MindboxLogger, publish-MindboxNotifications, publish-Mindbox] runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} - name: Release generation run: ./.github/git-release.sh env: GH_TOKEN: ${{ github.token }} + RELEASE_BRANCH: ${{ inputs.branch }} merge: needs: [publish-MindboxLogger, publish-MindboxNotifications, publish-Mindbox] @@ -154,7 +181,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.PAT_FOR_TRIGGERING_BRANCH_PROTECTION }} steps: - name: Checkout develop branch - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: develop - name: Create Pull Request @@ -162,67 +189,4 @@ jobs: - name: Merge Pull Request run: | pr_number=$(gh pr list --base develop --head master --json number --jq '.[0].number') - gh pr merge $pr_number --merge --auto - - message-to-loop-if-success: - needs: [release-github] - runs-on: ubuntu-latest - steps: - - name: Send message to LOOP - env: - LOOP_NOTIFICATION_WEBHOOK_URL: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK_URL }} - VERSION: ${{ github.ref_name }} - run: | - MESSAGE=$(cat <> $GITHUB_ENV - - name: Send message to LOOP - env: - LOOP_NOTIFICATION_WEBHOOK_URL: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK_URL }} - VERSION: ${{ github.ref_name }} - FAILED_JOB: ${{ env.FAILED_JOB }} - run: | - MESSAGE=$(cat <> $GITHUB_ENV - echo "RELEASE_SDK_VERSION=$RELEASE_SDK_VERSION" >> $GITHUB_ENV + echo "MASTER_SDK_VERSION=$MASTER_SDK_VERSION" >> "$GITHUB_ENV" + echo "RELEASE_SDK_VERSION=$RELEASE_SDK_VERSION" >> "$GITHUB_ENV" - name: Compare versions - uses: jackbilestech/semver-compare@1.0.4 + uses: jackbilestech/semver-compare@b6b063c569b77bea4a0ab627192cbdabf75de3f5 # 1.0.4 with: head: ${{ env.RELEASE_SDK_VERSION }} base: ${{ env.MASTER_SDK_VERSION }} diff --git a/.gitignore b/.gitignore index 9324e490a..32200107b 100755 --- a/.gitignore +++ b/.gitignore @@ -267,6 +267,7 @@ Package.resolved fastlane/report.xml fastlane/Preview.html +fastlane/README.md fastlane/screenshots fastlane/test_output test_output @@ -308,6 +309,4 @@ tags !*.xcworkspace/contents.xcworkspacedata /*.gcno -FastlaneRunner - # End of https://www.gitignore.io/api/osx,vim,ruby,macos,xcode,swift,appcode,swiftpm,carthage,intellij,cocoapods,swiftpackagemanager diff --git a/Example/Example/NotificationCenter/SwiftDataManager.swift b/Example/Example/NotificationCenter/SwiftDataManager.swift index 11c8fa4d1..d2f9f1d94 100644 --- a/Example/Example/NotificationCenter/SwiftDataManager.swift +++ b/Example/Example/NotificationCenter/SwiftDataManager.swift @@ -78,7 +78,9 @@ public struct SwiftDataManager { print("Application Support directory already exists at \(applicationSupportURL.path)") } } else { - fatalError("Could not find App Group container URL.") + // Best-effort: the SwiftData store falls back to the app's default + // location, so a missing App Group container must not crash the host. + print("App Group container URL is unavailable; skipping SwiftData directory setup.") } } } diff --git a/Gemfile b/Gemfile index 5587c0d85..bb94aef51 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,4 @@ source "https://rubygems.org" gem "fastlane" -gem "cocoapods" -gem 'abbrev' \ No newline at end of file +gem "cocoapods" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 50798be9e..3f1deda51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,8 +23,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1241.0) - aws-sdk-core (3.246.0) + aws-partitions (1.1267.0) + aws-sdk-core (3.253.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -32,24 +32,24 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.123.0) - aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-kms (1.129.0) + aws-sdk-core (~> 3, >= 3.248.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.220.0) - aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-s3 (1.226.0) + aws-sdk-core (~> 3, >= 3.248.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) + base64 (0.3.0) benchmark (0.5.0) bigdecimal (4.1.2) claide (1.1.0) - cocoapods (1.16.2) + cocoapods (1.17.0) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.16.2) + cocoapods-core (= 1.17.0) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -57,14 +57,13 @@ GEM cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) - escape (~> 0.0.4) fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.27.0, < 2.0) - cocoapods-core (1.16.2) + ruby-macho (~> 4.1.0) + xcodeproj (>= 1.28.1, < 2.0) + cocoapods-core (1.17.0) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -87,7 +86,7 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.3.6) + concurrent-ruby (1.3.7) connection_pool (3.0.2) csv (3.3.5) declarative (0.0.20) @@ -97,12 +96,12 @@ GEM dotenv (2.8.1) drb (2.2.3) emoji_regex (3.2.3) - escape (0.0.4) ethon (0.18.0) ffi (>= 1.15.0) logger - excon (0.112.0) - faraday (1.10.5) + excon (1.5.0) + logger + faraday (1.10.6) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -131,22 +130,22 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.1) - fastlane (2.233.0) - CFPropertyList (>= 2.3, < 4.0.0) - abbrev (~> 0.1.2) - addressable (>= 2.8, < 3.0.0) + fastlane (2.237.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.197) babosa (>= 1.0.3, < 2.0.0) - base64 (~> 0.2.0) + base64 (~> 0.2) benchmark (>= 0.1.0) - bundler (>= 1.17.3, < 5.0.0) + bundler (>= 2.4.0, < 5.0.0) colored (~> 1.2) commander (~> 4.6) csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) + excon (>= 0.71.0, < 2.0.0) faraday (~> 1.0) faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) @@ -155,18 +154,19 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, <= 2.1.1) + google-cloud-env (>= 1.6.0, < 2.3.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) json (< 3.0.0) - jwt (>= 2.1.0, < 3) + jwt (>= 2.10.3, < 4) logger (>= 1.6, < 2.0) mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) multipart-post (>= 2.0.0, < 3.0.0) - mutex_m (~> 0.3.0) + mutex_m (~> 0.3) naturally (~> 2.2) - nkf (~> 0.2.0) + nkf (~> 0.2) optparse (>= 0.1.1, < 1.0.0) ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) @@ -196,7 +196,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.99.0) + google-apis-androidpublisher_v3 (0.104.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -206,19 +206,20 @@ GEM mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - google-apis-iamcredentials_v1 (0.27.0) + google-apis-iamcredentials_v1 (0.28.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.17.0) + google-apis-playcustomapp_v1 (0.18.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.62.0) + google-apis-storage_v1 (0.64.0) google-apis-core (>= 0.15.0, < 2.a) - google-cloud-core (1.8.0) + google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.1) + google-cloud-env (2.2.2) + base64 (~> 0.2) faraday (>= 1.0, < 3.a) google-cloud-errors (1.6.0) - google-cloud-storage (1.59.0) + google-cloud-storage (1.62.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-core (>= 0.18, < 2) @@ -227,60 +228,62 @@ GEM google-cloud-core (~> 1.6) googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.11.2) + google-logging-utils (0.2.0) + googleauth (1.17.1) faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) os (>= 0.9, < 2.0) + pstore (~> 0.1) signet (>= 0.16, < 2.a) highline (2.0.3) http-cookie (1.0.8) domain_name (~> 0.5) httpclient (2.9.0) mutex_m - i18n (1.14.8) + i18n (1.15.2) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.19.4) - jwt (2.10.2) + json (2.20.0) + jwt (3.2.0) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.27.0) molinillo (0.8.0) - multi_json (1.20.1) + multi_json (1.21.1) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) nap (1.1.0) naturally (2.3.0) netrc (0.11.0) - nkf (0.2.0) + nkf (0.3.0) optparse (0.8.1) os (1.1.4) ostruct (0.6.3) plist (3.7.2) + pstore (0.2.1) public_suffix (4.0.7) rake (13.4.2) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.4.1) + retriable (3.8.0) rexml (3.4.4) rouge (3.28.0) - ruby-macho (2.5.1) + ruby-macho (4.1.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) securerandom (0.4.1) security (0.1.5) - signet (0.21.0) + signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally @@ -299,12 +302,14 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.27.0) + xcodeproj (1.28.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) + base64 claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.4.0) + nkf rexml (>= 3.3.6, < 4.0) xcpretty (0.4.1) rouge (~> 3.28.0) @@ -325,9 +330,144 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - abbrev cocoapods fastlane +CHECKSUMS + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + activesupport (7.2.3.1) sha256=11ebed516a43a0bb47346227a35ebae4d9427465a7c9eb197a03d5c8d283cb34 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + algoliasearch (1.27.5) sha256=26c1cddf3c2ec4bd60c148389e42702c98fdac862881dc6b07a4c0b89ffec853 + artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 + atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1267.0) sha256=15a4c6bef8c303b09a1452c675cecb1ace0238dc91e26fe4646c5d761cb2221e + aws-sdk-core (3.253.0) sha256=b3fca1fbdfff3a92d371e4b4244bcd3806d861947eda7e06237031e4060d6821 + aws-sdk-kms (1.129.0) sha256=363f548df321f4a4fcfd05523384e591060b400f8e65133ed7ef0793155a3343 + aws-sdk-s3 (1.226.0) sha256=e599f431e006ec9b92c61ee0f14d3f658a1f6c8a1d623d2160be927ac958e2bf + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.15) sha256=a4ceb882fe94a0e0ac63cd0813932bbfd631a14e5ac0b7975189b19a4d28d9e7 + claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e + cocoapods (1.17.0) sha256=dacf6f11ac3b00d60e6dd326485b616935230aacf95f385d145db27bfdf284af + cocoapods-core (1.17.0) sha256=a9e3d0dd36ab1b48935236d77a15cad9171217f13c6010c8e2ae3c0f455daf5b + cocoapods-deintegrate (1.0.5) sha256=517c2a448ef563afe99b6e7668704c27f5de9e02715a88ee9de6974dc1b3f6a2 + cocoapods-downloader (2.1) sha256=bb6ebe1b3966dc4055de54f7a28b773485ac724fdf575d9bee2212d235e7b6d1 + cocoapods-plugins (1.0.0) sha256=725d17ce90b52f862e73476623fd91441b4430b742d8a071000831efb440ca9a + cocoapods-search (1.0.1) sha256=1b133b0e6719ed439bd840e84a1828cca46425ab73a11eff5e096c3b2df05589 + cocoapods-trunk (1.6.0) sha256=5f5bda8c172afead48fa2d43a718cf534b1313c367ba1194cebdeb9bfee9ed31 + cocoapods-try (1.2.0) sha256=145b946c6e7747ed0301d975165157951153d27469e6b2763c83e25c84b9defe + colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c + colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a + commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 + concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0 + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 + dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b + ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2 + excon (1.5.0) sha256=c503ad1d0123bc8ab2a062ff3789dc891ec368cb9e13765ab88a9c58c8bb6d50 + faraday (1.10.6) sha256=7ff4802a6b312876a2241b3e641ce0d5045e168dd871b422c35b505e5261ad4d + faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb + faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689 + faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750 + faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940 + faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682 + faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335 + faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7 + faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0 + faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa + faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9 + fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 + fastlane (2.237.0) sha256=bb1e867bc070fb328741b5e6e7606d5ba596941a9ecca0478f60ef22d1009db9 + fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fourflusher (2.3.1) sha256=1b3de61c7c791b6a4e64f31e3719eb25203d151746bb519a0292bff1065ccaa9 + fuzzy_match (2.0.4) sha256=b5de4f95816589c5b5c3ad13770c0af539b75131c158135b3f3bbba75d0cfca5 + gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 + google-apis-androidpublisher_v3 (0.104.0) sha256=3bf7f0dee23ae71e070e279848374834853204e304831cf6aa76fa435a4d6eff + google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee + google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 + google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254 + google-apis-storage_v1 (0.64.0) sha256=75b11afa2edcee859b84c7a6972ee4456314eeef5f762827fd6cf5c5ffaf93f2 + google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 + google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7 + google-cloud-errors (1.6.0) sha256=1da8476dd706ad04b9d32e3c4b90d07d3463b37d6407cb56d41342ea7647d0a1 + google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330 + google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b + googleauth (1.17.1) sha256=0f7e6fc70e204cee1b2d71f1e1de2d3b349d432404197fe68ebf7fa23d0821b9 + highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 + http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 + httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8 + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.20.0) sha256=9362bc6e55a952b056abf9167cf053358181c904cb70cd6eee0808ea830fc32b + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5 + molinillo (0.8.0) sha256=efbff2716324e2a30bccd3eba1ff3a735f4d5d53ffddbc6a2f32c0ca9433045d + multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 + nap (1.1.0) sha256=949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576 + naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f + nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895 + optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a + public_suffix (4.0.7) sha256=8be161e2421f8d45b0098c042c06486789731ea93dc3a896d30554ee38b573b8 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + retriable (3.8.0) sha256=9f2f1b0207594c7817f17f671587b8ec7587387ac6cebda6c941a802bb98a8e5 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 + ruby-macho (4.1.0) sha256=23dab37f7de0fe1e14f3bfa73bebc423ae8cd1d4fdb3e5585abc45a841eca920 + ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 + simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b + terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea + terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 + typhoeus (1.6.0) sha256=bacc41c23e379547e29801dc235cd1699b70b955a1ba3d32b2b877aa844c331d + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 + xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc + xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 + xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93 + BUNDLED WITH - 2.6.6 + 4.0.15 diff --git a/Mindbox.podspec b/Mindbox.podspec index 2868119b0..dead59569 100644 --- a/Mindbox.podspec +++ b/Mindbox.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "Mindbox" - spec.version = "2.15.1" + spec.version = "2.15.2" spec.summary = "SDK for integration with Mindbox" spec.description = "This library allows you to integrate data transfer to Mindbox Marketing Cloud" spec.homepage = "https://github.com/mindbox-cloud/ios-sdk" @@ -14,6 +14,6 @@ Pod::Spec.new do |spec| 'Mindbox' => ['Mindbox/**/*.xcassets', 'Mindbox/**/*.xcdatamodeld', 'Mindbox/**/*.xcprivacy'] } spec.swift_version = "5" - spec.dependency 'MindboxLogger', '2.15.1' + spec.dependency 'MindboxLogger', '2.15.2' end diff --git a/Mindbox.xcodeproj/project.pbxproj b/Mindbox.xcodeproj/project.pbxproj index cf85e01b1..6f31cb621 100644 --- a/Mindbox.xcodeproj/project.pbxproj +++ b/Mindbox.xcodeproj/project.pbxproj @@ -7,9 +7,25 @@ objects = { /* Begin PBXBuildFile section */ + A881A16C702E41AE93848BD3 /* InAppWebViewLearnedHostsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B76146361C3402193A00F88 /* InAppWebViewLearnedHostsStore.swift */; }; + F389B3A639904206A3DADFFA /* InAppWebViewPrewarmPlanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC6F9CEEF24C2BAB624DD5 /* InAppWebViewPrewarmPlanner.swift */; }; + 8A94D721DAEF4395A360C5B2 /* InAppWebViewPrewarmNavigationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A50D5650094A23A2C667CE /* InAppWebViewPrewarmNavigationPolicy.swift */; }; + 23335221BB1D4F8C8D5C864D /* InAppWebViewPrewarmService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F423FA6C2E243F68A76E9C2 /* InAppWebViewPrewarmService.swift */; }; + 23E4A509BFE94104BD01B61A /* MindboxWebBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CC5ED4F76B046798978B4B9 /* MindboxWebBridgeTests.swift */; }; + AE190755CA97473B83E1568D /* MBContainerConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F5CEE405E4BE29F97E43B /* MBContainerConcurrencyTests.swift */; }; + 3FDBA6E4D39A49369A335CF4 /* SDKUserAgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45E06561604E6185C33366 /* SDKUserAgentTests.swift */; }; + 16311BEB069E4B3983FAF594 /* InAppWebViewCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895F8C05EA8B4716B2A2941A /* InAppWebViewCacheTests.swift */; }; + 28375259024D42658E146900 /* InAppWebViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1627DAC8D54E448C6D7BF0 /* InAppWebViewFactory.swift */; }; + B58D5207F49F4531AA1C516B /* InAppWebViewHTMLFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85CCBEB1D874078954BB771 /* InAppWebViewHTMLFetcher.swift */; }; + 2DBF19F24FEA48AAA39C33E6 /* InAppWebViewDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D096F433EBB44C8B3490CF6 /* InAppWebViewDataStore.swift */; }; + B9025268DDC24820B95ADE10 /* WebViewTimeoutErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E54FE3B51649DCBD5ABE33 /* WebViewTimeoutErrorTests.swift */; }; + CB91323FAD66404B9422B6E0 /* WebViewReadyCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D716044131845AEA04A9858 /* WebViewReadyCheckerTests.swift */; }; + C830BF4C287849CE95BB4ED9 /* WebViewReadyChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2DAFC2435264F15B8033CCE /* WebViewReadyChecker.swift */; }; + 46E95250B5904CC18E079C54 /* SDKUserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68184C936B4243C28CC10829 /* SDKUserAgent.swift */; }; 0A3D045A2BC6803E00E1FC52 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3D04592BC6803E00E1FC52 /* ImageFormat.swift */; }; 0E7A224A082FA2DA35706CC7 /* MotionServiceResolvePositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8192B8B7043EF74D05B36B /* MotionServiceResolvePositionTests.swift */; }; 0E7A224A082FA2DA35706CC8 /* MotionServiceShakeToEditTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8192B8B7043EF74D05B36C /* MotionServiceShakeToEditTests.swift */; }; + 1E28BAF922E64B9CB6E22A0B /* DateFormatMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1A11C4A4B940898BA80035 /* DateFormatMigrationTests.swift */; }; 1E3BD63AB3F1521C253CB818 /* MBNetworkFetcherResponseHandlingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97FEDDEB5F71A67F1C4C675F /* MBNetworkFetcherResponseHandlingTests.swift */; }; 302E35788CBDA959283569F4 /* MotionServiceBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB93A7997961CA7C2BE917 /* MotionServiceBehaviorTests.swift */; }; 313B233A25ADEA0F00A1CB72 /* Mindbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 313B233025ADEA0F00A1CB72 /* Mindbox.framework */; }; @@ -99,6 +115,7 @@ 33E42E5C268323E60046CBCB /* CashdeskRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33E42E5B268323E60046CBCB /* CashdeskRequest.swift */; }; 33EBF0B0264E6283002A35D5 /* MBSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33EBF0AF264E6283002A35D5 /* MBSessionManager.swift */; }; 3FB93AC126964AD3A061B4A7 /* FeatureToggleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3456BAC56F984378B6CED7CB /* FeatureToggleManager.swift */; }; + B202CDC049DDB7A2F9833ADF /* InAppTagsGating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E8D4000B12B31961251F6C7 /* InAppTagsGating.swift */; }; 472179A72C80755A00C15E7F /* ShownInAppsIDsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472179A62C80755A00C15E7F /* ShownInAppsIDsMigration.swift */; }; 472765522E0D52A500A5A060 /* RemoveBackgroundTaskDataMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472765512E0D52A500A5A060 /* RemoveBackgroundTaskDataMigration.swift */; }; 472F549E2C6E272A0008C465 /* MBPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472F549D2C6E272A0008C465 /* MBPushNotification.swift */; }; @@ -206,10 +223,6 @@ 474142622E8AAC8900839AD8 /* DatabaseRepository_NoopContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474142612E8AAC8900839AD8 /* DatabaseRepository_NoopContractTests.swift */; }; 4741DAC42E85C49F00EB2497 /* DatabaseLoaderFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4741DAC32E85C49F00EB2497 /* DatabaseLoaderFlowTests.swift */; }; 4741DAC62E85DC1600EB2497 /* MBDatabaseRepositoryMemoryWarningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4741DAC52E85DC1600EB2497 /* MBDatabaseRepositoryMemoryWarningTests.swift */; }; - 474230DC2E72236500282764 /* TestContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474230DB2E72236500282764 /* TestContainers.swift */; }; - 4747708B2C6B838B00C36FC8 /* SharedInternalMethodsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4747708A2C6B838B00C36FC8 /* SharedInternalMethodsTests.swift */; }; - 4747708F2C6B93AC00C36FC8 /* MindboxNotificationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4747708E2C6B93AC00C36FC8 /* MindboxNotificationServiceTests.swift */; }; - 474770912C6B9A7200C36FC8 /* MindboxNotificationContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474770902C6B9A7200C36FC8 /* MindboxNotificationContentTests.swift */; }; 474851C12C6A622E0026C38E /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474851C02C6A622E0026C38E /* NotificationService.swift */; }; 474851C32C6A627F0026C38E /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474851C22C6A627F0026C38E /* Constants.swift */; }; 474851C52C6A62AE0026C38E /* NotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474851C42C6A62AE0026C38E /* NotificationContent.swift */; }; @@ -220,13 +233,10 @@ 4766A8962C92FF3A002D15A4 /* TimeToLiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4766A8952C92FF3A002D15A4 /* TimeToLiveModel.swift */; }; 4766A8AE2C9325B0002D15A4 /* ABTestsConfigParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4766A8AD2C9325B0002D15A4 /* ABTestsConfigParsingTests.swift */; }; 47980DB12E3B96EF0020EB34 /* CheckNotificationsStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47980DB02E3B96EF0020EB34 /* CheckNotificationsStatusOperation.swift */; }; - 47A220A52E2158A00001507C /* IsolatedMBLoggerCoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A220A42E2158A00001507C /* IsolatedMBLoggerCoreDataManager.swift */; }; 47A4FA6E2E7335A200569870 /* LoggerDatabaseLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A4FA6D2E7335A200569870 /* LoggerDatabaseLoader.swift */; }; 47A4FA702E73421200569870 /* LoggerDBConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A4FA6F2E73421200569870 /* LoggerDBConfig.swift */; }; 47A4FA722E73565400569870 /* LogStoreTrimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A4FA712E73565400569870 /* LogStoreTrimmer.swift */; }; 47A4FA742E73569F00569870 /* SQLiteLogicalSizeMeasurer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A4FA732E73569F00569870 /* SQLiteLogicalSizeMeasurer.swift */; }; - 47A4FA762E735C5200569870 /* LogStoreTrimmerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A4FA752E735C5200569870 /* LogStoreTrimmerTests.swift */; }; - 47A4FA782E73741700569870 /* LoggerDatabaseLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A4FA772E73741700569870 /* LoggerDatabaseLoaderTests.swift */; }; 47ADB8152E8D3CEA00AFCCB2 /* DatabaseMetadataMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47ADB8142E8D3CEA00AFCCB2 /* DatabaseMetadataMigration.swift */; }; 47B1B6AB2F6174FB000A4B67 /* WebViewLocalStateStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B1B6AA2F6174FB000A4B67 /* WebViewLocalStateStorage.swift */; }; 47B90E2F2C625F9A00BD93E7 /* TestBaseMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B90E2E2C625F9A00BD93E7 /* TestBaseMigrations.swift */; }; @@ -248,6 +258,7 @@ 47EFF0FD2E8D85B700E72D0A /* DatabaseMetadataMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EFF0FC2E8D85B700E72D0A /* DatabaseMetadataMigrationTests.swift */; }; 47FDF0BA2C5BDAB80051F08C /* MigrationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FDF0B92C5BDAB80051F08C /* MigrationManagerProtocol.swift */; }; 47FDF0BC2C5BE8BB0051F08C /* MigrationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FDF0BB2C5BE8BB0051F08C /* MigrationProtocol.swift */; }; + 4841683F6839440F84477966 /* OperationNameValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF1D88A18D64D60BD03ABB8 /* OperationNameValidator.swift */; }; 5D3FCB95C2AF59DF36A61254 /* WebViewLocalStateStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFCC82B8014DE7276C217CD /* WebViewLocalStateStorageTests.swift */; }; 6182078DDFC681D168546DAD /* HapticService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1BE43AA9EAEA03F8ED4008 /* HapticService.swift */; }; 6182078DDFC681D168546DAE /* HapticRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1BE43AA9EAEA03F8ED4009 /* HapticRequest.swift */; }; @@ -278,6 +289,7 @@ 6FDD1461266F7CE300A50C35 /* DiscountAmountTypeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDD1460266F7CE300A50C35 /* DiscountAmountTypeResponse.swift */; }; 6FDD1463266F7CED00A50C35 /* ProductElementReponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDD1462266F7CED00A50C35 /* ProductElementReponse.swift */; }; 6FDD1465266F7CFE00A50C35 /* ItemProductResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDD1464266F7CFE00A50C35 /* ItemProductResponse.swift */; }; + 7764BA10E25046CEA7035ED8 /* DateFormatMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8755088A5E704C65A1C3F6DB /* DateFormatMigration.swift */; }; 813FE960DC0543F681C94275 /* SettingsRequestParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FC619AC23245CC8CD45E63 /* SettingsRequestParser.swift */; }; 840042A12614CE0000CA17C5 /* ClickNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840042A02614CE0000CA17C5 /* ClickNotificationManager.swift */; }; 840C387225CC1AF200D50183 /* CDEvent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840C387025CC1AF200D50183 /* CDEvent+CoreDataClass.swift */; }; @@ -359,13 +371,13 @@ 9BC24E7A28F6C08700C2619C /* InAppConfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 9BC24E7928F6C08700C2619C /* InAppConfiguration.json */; }; A11C2D3E4F5566778899AA01 /* FirstInitializationDateTimeRuntimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11C2D3E4F5566778899AA01 /* FirstInitializationDateTimeRuntimeTests.swift */; }; A11C2D3E4F5566778899AA02 /* FirstInitializationDateTimeMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11C2D3E4F5566778899AA02 /* FirstInitializationDateTimeMigrationTests.swift */; }; + A11C2D3E4F5566778899AA11 /* DeviceUUIDInitializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11C2D3E4F5566778899AA11 /* DeviceUUIDInitializationTests.swift */; }; + A11C2D3E4F5566778899AA12 /* MBPersistenceStorageDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11C2D3E4F5566778899AA12 /* MBPersistenceStorageDateTests.swift */; }; A11FBE8929DD76BF00F5FB7B /* InAppMessagesEventSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11FBE8829DD76BF00F5FB7B /* InAppMessagesEventSender.swift */; }; - A1515DB229B228F40025E2EE /* MindboxLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A17853BE29AF7E940072578F /* MindboxLogger.framework */; }; A153E03B29BAFE01003C34D4 /* CustomOperationTargeting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A153E03A29BAFE01003C34D4 /* CustomOperationTargeting.swift */; }; A153E03D29BAFEC1003C34D4 /* CustomOperationChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A153E03C29BAFEC0003C34D4 /* CustomOperationChecker.swift */; }; A153E03F29BB002A003C34D4 /* SessionTemporaryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A153E03E29BB002A003C34D4 /* SessionTemporaryStorage.swift */; }; A153E04129BB0A8B003C34D4 /* InAppConfigurationWithOperations.json in Resources */ = {isa = PBXBuildFile; fileRef = A153E04029BB0A8B003C34D4 /* InAppConfigurationWithOperations.json */; }; - A154E304299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E303299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift */; }; A154E32E299E0D8900F8F074 /* SDKLogManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E32D299E0D8900F8F074 /* SDKLogManagerTests.swift */; }; A154E330299E0F1600F8F074 /* InAppGeoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E32F299E0F1600F8F074 /* InAppGeoResponse.swift */; }; A154E334299E110E00F8F074 /* EventRepositoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E333299E110E00F8F074 /* EventRepositoryMock.swift */; }; @@ -424,6 +436,9 @@ A1B2C3D400000014E1010101 /* PushNotificationsPermissionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000015E1010101 /* PushNotificationsPermissionHandler.swift */; }; A1B2C3D400000020E1010101 /* PushPermissionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000021E1010101 /* PushPermissionHelper.swift */; }; A1B2C3D400000030E1010101 /* PushPermissionHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D400000031E1010101 /* PushPermissionHelperTests.swift */; }; + A1B2C3D4E5F60718293A4B01 /* MindboxOperationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B02 /* MindboxOperationsTests.swift */; }; + A1B2C3D4E5F60718293A4B03 /* OperationNameValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B04 /* OperationNameValidatorTests.swift */; }; + A1B2C3D4E5F60718293A4B05 /* PollUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B06 /* PollUntil.swift */; }; A1B2C3D4E5F6A7B8C9D0E1F2 /* MotionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F3 /* MotionService.swift */; }; A1B940B7298104ED00B0F994 /* UnknownTargetingsModel.json in Resources */ = {isa = PBXBuildFile; fileRef = A1B940B6298104ED00B0F994 /* UnknownTargetingsModel.json */; }; A1D017E72976CBE100CD9F99 /* TargetingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D017E62976CBE100CD9F99 /* TargetingModel.swift */; }; @@ -451,6 +466,7 @@ B4E438702D8AFA5700603F3A /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E4386E2D8AFA5700603F3A /* WebViewController.swift */; }; B4E438722D8AFA6700603F3A /* WebViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E438712D8AFA6700603F3A /* WebViewFactory.swift */; }; B4E438742D8AFAA700603F3A /* WebviewPresentationStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E438732D8AFAA700603F3A /* WebviewPresentationStrategy.swift */; }; + B7705CA22E0A1F0000C0FFEE /* AppGroupUnavailableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7705CA12E0A1F0000C0FFEE /* AppGroupUnavailableTests.swift */; }; BB4D7CC72BDEC51D008E3AB8 /* Notification+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4D7CC62BDEC51D008E3AB8 /* Notification+Extensions.swift */; }; BB6563102BE3BA430090C473 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB65630F2BE3BA430090C473 /* UIApplication+Extensions.swift */; }; BBAAC17C2BB2FC9100E1E25E /* MockEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAAC17B2BB2FC9100E1E25E /* MockEvent.swift */; }; @@ -463,7 +479,6 @@ D2F7E24C2BADC4CA00B24BB8 /* MockSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F7E24B2BADC4CA00B24BB8 /* MockSessionManager.swift */; }; DEC482157E5249DBBFAEFC9A /* FeatureTogglesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9778038796A8426ABDED1E97 /* FeatureTogglesModel.swift */; }; EA395B77BB16CEFE6DC91D1D /* TransparentViewSyncOperationResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7DAFAB687945FA908DB1AC /* TransparentViewSyncOperationResponseTests.swift */; }; - F26DFF81C3FF57C3DE68DEDC /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326423031CA9C6BF0E62BEFD /* Date+Extensions.swift */; }; F30005442CFF3F7D004BE915 /* ABTestStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30005432CFF3F7D004BE915 /* ABTestStubs.swift */; }; F306291A2BD27D7500EF6609 /* InappFrequencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30629192BD27D7500EF6609 /* InappFrequencyTests.swift */; }; F30654BB2F1A83520058808C /* MindboxWebViewFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30654BA2F1A83520058808C /* MindboxWebViewFacade.swift */; }; @@ -556,6 +571,8 @@ F34A103D2F455B840065392A /* FeatureTogglesConfigParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34A103A2F455B840065392A /* FeatureTogglesConfigParsingTests.swift */; }; F34A10442F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorMissing.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A10402F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorMissing.json */; }; F34A10452F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorFalse.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A103F2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorFalse.json */; }; + F34A104B2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsFalse.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A10492F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsFalse.json */; }; + F34A104C2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A104A2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsTypeError.json */; }; F34A10462F455C5B0065392A /* SettingsFeatureTogglesError.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A103E2F455C5B0065392A /* SettingsFeatureTogglesError.json */; }; F34A10472F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A10412F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json */; }; F34A10482F455C5B0065392A /* SettingsFeatureTogglesTypeError.json in Resources */ = {isa = PBXBuildFile; fileRef = F34A10422F455C5B0065392A /* SettingsFeatureTogglesTypeError.json */; }; @@ -564,6 +581,7 @@ F351F1C02CE380A40053423E /* InappMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F351F1BF2CE380A40053423E /* InappMapper.swift */; }; F351F1C22CE5F23A0053423E /* InappMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F351F1C12CE5F23A0053423E /* InappMapperTests.swift */; }; F351F1C42CE60CA90053423E /* 1-Targeting.json in Resources */ = {isa = PBXBuildFile; fileRef = F351F1C32CE60CA90053423E /* 1-Targeting.json */; }; + 7A3F1B2C9D4E5F60718293A7 /* Tags-FailedTargeting.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A3F1B2C9D4E5F60718293A6 /* Tags-FailedTargeting.json */; }; F351F1C62CE626450053423E /* 15-Targeting.json in Resources */ = {isa = PBXBuildFile; fileRef = F351F1C52CE626450053423E /* 15-Targeting.json */; }; F351F1C82CE72B300053423E /* 44-Targeting.json in Resources */ = {isa = PBXBuildFile; fileRef = F351F1C72CE72B300053423E /* 44-Targeting.json */; }; F351F1CB2CE72D460053423E /* 46-Targeting.json in Resources */ = {isa = PBXBuildFile; fileRef = F351F1CA2CE72D460053423E /* 46-Targeting.json */; }; @@ -575,6 +593,7 @@ F361284D2BA31E36000382D9 /* PushPermissionActionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F361284C2BA31E36000382D9 /* PushPermissionActionUseCase.swift */; }; F367301B2B7B6E7600DD0039 /* MindboxPushValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367301A2B7B6E7600DD0039 /* MindboxPushValidatorTests.swift */; }; F367301D2B7B8B6A00DD0039 /* NotificationFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367301C2B7B8B6A00DD0039 /* NotificationFormatTests.swift */; }; + F367D3782FE43B2900455DB0 /* ModalViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F367D3772FE43B2900455DB0 /* ModalViewControllerLayoutTests.swift */; }; F370940B2B8F9CDE00655AC7 /* InAppConfigurationDataFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = F370940A2B8F9CDE00655AC7 /* InAppConfigurationDataFacade.swift */; }; F37613E12A6A8CFF009F2EE4 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37613E02A6A8CFF009F2EE4 /* UIColor+Extensions.swift */; }; F382F20B2BAC50D000BC97FF /* VisitTargeting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F382F20A2BAC50D000BC97FF /* VisitTargeting.swift */; }; @@ -662,6 +681,10 @@ F3C1A0022F5B100100ABC001 /* InappShowFailureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C1A0012F5B100100ABC001 /* InappShowFailureManager.swift */; }; F3C1A0042F5B100100ABC001 /* InAppShowFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C1A0032F5B100100ABC001 /* InAppShowFailure.swift */; }; F3C1A0062F5B100100ABC001 /* InappShowFailureManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C1A0052F5B100100ABC001 /* InappShowFailureManagerTests.swift */; }; + FA39EE97042009DCEC24E971 /* InAppTagsGatingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045EA348681AB859FB298A57 /* InAppTagsGatingTests.swift */; }; + 1E316CC518D9111F0BD25590 /* InAppMessagesTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60DC2E1435521EBF0E2C5E4 /* InAppMessagesTrackerTests.swift */; }; + AB0E83C770AAB9A2DC8E9BF5 /* JSONValueTagsMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E67F6257F15F143DEA87DE /* JSONValueTagsMergeTests.swift */; }; + 7A3F1B2C9D4E5F60718293A5 /* TransparentViewJSBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F1B2C9D4E5F60718293A4 /* TransparentViewJSBridgeTests.swift */; }; F3CD20262F600A800065392A /* MBConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD20272F600A800065392A /* MBConfigurationTests.swift */; }; F3CD20292F600A800065392A /* HostNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202A2F600A800065392A /* HostNormalizer.swift */; }; F3CD202B2F600A800065392A /* HostNormalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CD202C2F600A800065392A /* HostNormalizerTests.swift */; }; @@ -716,13 +739,6 @@ remoteGlobalIDString = 3333C1972681D3CF00B60D84; remoteInfo = MindboxNotifications; }; - A1515DB429B228F40025E2EE /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 313B232725ADEA0F00A1CB72 /* Project object */; - proxyType = 1; - remoteGlobalIDString = A17853BD29AF7E940072578F; - remoteInfo = MindboxLogger; - }; A170EDDE29B0883800CE547F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 313B232725ADEA0F00A1CB72 /* Project object */; @@ -740,9 +756,25 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1B76146361C3402193A00F88 /* InAppWebViewLearnedHostsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewLearnedHostsStore.swift; sourceTree = ""; }; + DDCC6F9CEEF24C2BAB624DD5 /* InAppWebViewPrewarmPlanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewPrewarmPlanner.swift; sourceTree = ""; }; + 96A50D5650094A23A2C667CE /* InAppWebViewPrewarmNavigationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewPrewarmNavigationPolicy.swift; sourceTree = ""; }; + 1F423FA6C2E243F68A76E9C2 /* InAppWebViewPrewarmService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewPrewarmService.swift; sourceTree = ""; }; + 9CC5ED4F76B046798978B4B9 /* MindboxWebBridgeTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MindboxWebBridgeTests.swift; sourceTree = ""; }; + 4B4F5CEE405E4BE29F97E43B /* MBContainerConcurrencyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MBContainerConcurrencyTests.swift; sourceTree = ""; }; + 4E45E06561604E6185C33366 /* SDKUserAgentTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SDKUserAgentTests.swift; sourceTree = ""; }; + 895F8C05EA8B4716B2A2941A /* InAppWebViewCacheTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppWebViewCacheTests.swift; sourceTree = ""; }; + FD1627DAC8D54E448C6D7BF0 /* InAppWebViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewFactory.swift; sourceTree = ""; }; + A85CCBEB1D874078954BB771 /* InAppWebViewHTMLFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewHTMLFetcher.swift; sourceTree = ""; }; + 6D096F433EBB44C8B3490CF6 /* InAppWebViewDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewDataStore.swift; sourceTree = ""; }; + 11E54FE3B51649DCBD5ABE33 /* WebViewTimeoutErrorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewTimeoutErrorTests.swift; sourceTree = ""; }; + 4D716044131845AEA04A9858 /* WebViewReadyCheckerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewReadyCheckerTests.swift; sourceTree = ""; }; + F2DAFC2435264F15B8033CCE /* WebViewReadyChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewReadyChecker.swift; sourceTree = ""; }; + 68184C936B4243C28CC10829 /* SDKUserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKUserAgent.swift; sourceTree = ""; }; 0475E8755F63483597539A50 /* TrackVisitManagerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackVisitManagerTests.swift; sourceTree = ""; }; 0A3D04592BC6803E00E1FC52 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; 0CFCC82B8014DE7276C217CD /* WebViewLocalStateStorageTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewLocalStateStorageTests.swift; sourceTree = ""; }; + 0EF1D88A18D64D60BD03ABB8 /* OperationNameValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationNameValidator.swift; sourceTree = ""; }; 30FC619AC23245CC8CD45E63 /* SettingsRequestParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsRequestParser.swift; sourceTree = ""; }; 313B233025ADEA0F00A1CB72 /* Mindbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mindbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 313B233325ADEA0F00A1CB72 /* Mindbox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mindbox.h; sourceTree = ""; }; @@ -765,7 +797,6 @@ 31ED2DF025C4456600301FAD /* TestConfig_Invalid_1.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_1.plist; sourceTree = ""; }; 31ED2DF125C4456600301FAD /* TestConfig_Invalid_3.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_3.plist; sourceTree = ""; }; 31ED2DF925C4459400301FAD /* TestConfig_Invalid_4.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_4.plist; sourceTree = ""; }; - 326423031CA9C6BF0E62BEFD /* Date+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; 33072F2D2664C24F001F1AB2 /* AreaResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreaResponse.swift; sourceTree = ""; }; 33072F2F2664C2E4001F1AB2 /* SubscriptionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionResponse.swift; sourceTree = ""; }; 33072F312664C357001F1AB2 /* ProductListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListResponse.swift; sourceTree = ""; }; @@ -788,7 +819,6 @@ 3333C19A2681D3CF00B60D84 /* MindboxNotifications.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MindboxNotifications.h; sourceTree = ""; }; 3333C19B2681D3CF00B60D84 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3333C1A02681D3CF00B60D84 /* MindboxNotificationsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MindboxNotificationsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3333C1A72681D3CF00B60D84 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3333C1B12681D42000B60D84 /* Payload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payload.swift; sourceTree = ""; }; 3333C1B32681D43C00B60D84 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; 3333C1DD2681E9F300B60D84 /* URLRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestBuilder.swift; sourceTree = ""; }; @@ -838,6 +868,7 @@ 33E42E5B268323E60046CBCB /* CashdeskRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashdeskRequest.swift; sourceTree = ""; }; 33EBF0AF264E6283002A35D5 /* MBSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBSessionManager.swift; sourceTree = ""; }; 3456BAC56F984378B6CED7CB /* FeatureToggleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureToggleManager.swift; sourceTree = ""; }; + 3E8D4000B12B31961251F6C7 /* InAppTagsGating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppTagsGating.swift; sourceTree = ""; }; 472179A62C80755A00C15E7F /* ShownInAppsIDsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShownInAppsIDsMigration.swift; sourceTree = ""; }; 472765512E0D52A500A5A060 /* RemoveBackgroundTaskDataMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveBackgroundTaskDataMigration.swift; sourceTree = ""; }; 472F549D2C6E272A0008C465 /* MBPushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBPushNotification.swift; sourceTree = ""; }; @@ -945,10 +976,6 @@ 474142612E8AAC8900839AD8 /* DatabaseRepository_NoopContractTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRepository_NoopContractTests.swift; sourceTree = ""; }; 4741DAC32E85C49F00EB2497 /* DatabaseLoaderFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseLoaderFlowTests.swift; sourceTree = ""; }; 4741DAC52E85DC1600EB2497 /* MBDatabaseRepositoryMemoryWarningTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBDatabaseRepositoryMemoryWarningTests.swift; sourceTree = ""; }; - 474230DB2E72236500282764 /* TestContainers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestContainers.swift; sourceTree = ""; }; - 4747708A2C6B838B00C36FC8 /* SharedInternalMethodsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedInternalMethodsTests.swift; sourceTree = ""; }; - 4747708E2C6B93AC00C36FC8 /* MindboxNotificationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxNotificationServiceTests.swift; sourceTree = ""; }; - 474770902C6B9A7200C36FC8 /* MindboxNotificationContentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxNotificationContentTests.swift; sourceTree = ""; }; 474851C02C6A622E0026C38E /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 474851C22C6A627F0026C38E /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 474851C42C6A62AE0026C38E /* NotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContent.swift; sourceTree = ""; }; @@ -959,13 +986,10 @@ 4766A8952C92FF3A002D15A4 /* TimeToLiveModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeToLiveModel.swift; sourceTree = ""; }; 4766A8AD2C9325B0002D15A4 /* ABTestsConfigParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABTestsConfigParsingTests.swift; sourceTree = ""; }; 47980DB02E3B96EF0020EB34 /* CheckNotificationsStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckNotificationsStatusOperation.swift; sourceTree = ""; }; - 47A220A42E2158A00001507C /* IsolatedMBLoggerCoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsolatedMBLoggerCoreDataManager.swift; sourceTree = ""; }; 47A4FA6D2E7335A200569870 /* LoggerDatabaseLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerDatabaseLoader.swift; sourceTree = ""; }; 47A4FA6F2E73421200569870 /* LoggerDBConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerDBConfig.swift; sourceTree = ""; }; 47A4FA712E73565400569870 /* LogStoreTrimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStoreTrimmer.swift; sourceTree = ""; }; 47A4FA732E73569F00569870 /* SQLiteLogicalSizeMeasurer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLiteLogicalSizeMeasurer.swift; sourceTree = ""; }; - 47A4FA752E735C5200569870 /* LogStoreTrimmerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStoreTrimmerTests.swift; sourceTree = ""; }; - 47A4FA772E73741700569870 /* LoggerDatabaseLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerDatabaseLoaderTests.swift; sourceTree = ""; }; 47ADB8142E8D3CEA00AFCCB2 /* DatabaseMetadataMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseMetadataMigration.swift; sourceTree = ""; }; 47B1B6AA2F6174FB000A4B67 /* WebViewLocalStateStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewLocalStateStorage.swift; sourceTree = ""; }; 47B90E2E2C625F9A00BD93E7 /* TestBaseMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBaseMigrations.swift; sourceTree = ""; }; @@ -1066,6 +1090,7 @@ 84FCD3B425CA0FD300D1E574 /* MockPersistenceStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPersistenceStorage.swift; sourceTree = ""; }; 84FCD3B825CA109E00D1E574 /* MockNetworkFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkFetcher.swift; sourceTree = ""; }; 84FCD3BC25CA10F600D1E574 /* SuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = SuccessResponse.json; sourceTree = ""; }; + 8755088A5E704C65A1C3F6DB /* DateFormatMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatMigration.swift; sourceTree = ""; }; 9778038796A8426ABDED1E97 /* FeatureTogglesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureTogglesModel.swift; sourceTree = ""; }; 97FEDDEB5F71A67F1C4C675F /* MBNetworkFetcherResponseHandlingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MBNetworkFetcherResponseHandlingTests.swift; sourceTree = ""; }; 9B24FAAB28C74B8300F10B5D /* InAppConfigurationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppConfigurationRepository.swift; sourceTree = ""; }; @@ -1096,7 +1121,6 @@ A153E03C29BAFEC0003C34D4 /* CustomOperationChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOperationChecker.swift; sourceTree = ""; }; A153E03E29BB002A003C34D4 /* SessionTemporaryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTemporaryStorage.swift; sourceTree = ""; }; A153E04029BB0A8B003C34D4 /* InAppConfigurationWithOperations.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InAppConfigurationWithOperations.json; sourceTree = ""; }; - A154E303299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBLoggerCoreDataManagerTests.swift; sourceTree = ""; }; A154E32D299E0D8900F8F074 /* SDKLogManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKLogManagerTests.swift; sourceTree = ""; }; A154E32F299E0F1600F8F074 /* InAppGeoResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppGeoResponse.swift; sourceTree = ""; }; A154E333299E110E00F8F074 /* EventRepositoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRepositoryMock.swift; sourceTree = ""; }; @@ -1155,6 +1179,9 @@ A1B2C3D400000015E1010101 /* PushNotificationsPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsPermissionHandler.swift; sourceTree = ""; }; A1B2C3D400000021E1010101 /* PushPermissionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushPermissionHelper.swift; sourceTree = ""; }; A1B2C3D400000031E1010101 /* PushPermissionHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushPermissionHelperTests.swift; sourceTree = ""; }; + A1B2C3D4E5F60718293A4B02 /* MindboxOperationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxOperationsTests.swift; sourceTree = ""; }; + A1B2C3D4E5F60718293A4B04 /* OperationNameValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationNameValidatorTests.swift; sourceTree = ""; }; + A1B2C3D4E5F60718293A4B06 /* PollUntil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollUntil.swift; sourceTree = ""; }; A1B2C3D4E5F6A7B8C9D0E1F3 /* MotionService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MotionService.swift; sourceTree = ""; }; A1B940B6298104ED00B0F994 /* UnknownTargetingsModel.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = UnknownTargetingsModel.json; sourceTree = ""; }; A1D017E62976CBE100CD9F99 /* TargetingModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TargetingModel.swift; sourceTree = ""; }; @@ -1167,6 +1194,8 @@ A1D23AEF29DE082E00A75179 /* InAppProductSegmentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppProductSegmentResponse.swift; sourceTree = ""; }; B11C2D3E4F5566778899AA01 /* FirstInitializationDateTimeRuntimeTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FirstInitializationDateTimeRuntimeTests.swift; sourceTree = ""; }; B11C2D3E4F5566778899AA02 /* FirstInitializationDateTimeMigrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FirstInitializationDateTimeMigrationTests.swift; sourceTree = ""; }; + B11C2D3E4F5566778899AA11 /* DeviceUUIDInitializationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceUUIDInitializationTests.swift; sourceTree = ""; }; + B11C2D3E4F5566778899AA12 /* MBPersistenceStorageDateTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MBPersistenceStorageDateTests.swift; sourceTree = ""; }; B33127AE2760C53700CF747E /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; B36D57842696E59400FEDFD6 /* RetailOrderStatisticsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetailOrderStatisticsResponse.swift; sourceTree = ""; }; B3A6254B2689F83100B6A3B7 /* PersonalOffersResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalOffersResponse.swift; sourceTree = ""; }; @@ -1185,6 +1214,7 @@ B4E4386E2D8AFA5700603F3A /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; B4E438712D8AFA6700603F3A /* WebViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewFactory.swift; sourceTree = ""; }; B4E438732D8AFAA700603F3A /* WebviewPresentationStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebviewPresentationStrategy.swift; sourceTree = ""; }; + B7705CA12E0A1F0000C0FFEE /* AppGroupUnavailableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupUnavailableTests.swift; sourceTree = ""; }; BB4D7CC62BDEC51D008E3AB8 /* Notification+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Extensions.swift"; sourceTree = ""; }; BB65630F2BE3BA430090C473 /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; BBAAC17B2BB2FC9100E1E25E /* MockEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEvent.swift; sourceTree = ""; }; @@ -1195,6 +1225,7 @@ BD1BE43AA9EAEA03F8ED400C /* HapticRequestParserTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HapticRequestParserTests.swift; sourceTree = ""; }; BD1BE43AA9EAEA03F8ED400D /* HapticRequestValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HapticRequestValidatorTests.swift; sourceTree = ""; }; BECF3D292B29C1894F80948F /* MBEventRepositorySendRawTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MBEventRepositorySendRawTests.swift; sourceTree = ""; }; + BF1A11C4A4B940898BA80035 /* DateFormatMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatMigrationTests.swift; sourceTree = ""; }; D216DE502C0716B70020F58A /* StringExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionsTests.swift; sourceTree = ""; }; D216DE522C0716B80020F58A /* TimeIntervalTimeSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeIntervalTimeSpanTests.swift; sourceTree = ""; }; D2F7E2412BADB89900B24BB8 /* UserVisitManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserVisitManager.swift; sourceTree = ""; }; @@ -1294,6 +1325,8 @@ F34A103B2F455B840065392A /* SettingsConfigParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsConfigParsingTests.swift; sourceTree = ""; }; F34A103E2F455C5B0065392A /* SettingsFeatureTogglesError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesError.json; sourceTree = ""; }; F34A103F2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorFalse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesShouldSendInAppShowErrorFalse.json; sourceTree = ""; }; + F34A10492F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsFalse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesShouldSendInAppTagsFalse.json; sourceTree = ""; }; + F34A104A2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesShouldSendInAppTagsTypeError.json; sourceTree = ""; }; F34A10402F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorMissing.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesShouldSendInAppShowErrorMissing.json; sourceTree = ""; }; F34A10412F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json; sourceTree = ""; }; F34A10422F455C5B0065392A /* SettingsFeatureTogglesTypeError.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SettingsFeatureTogglesTypeError.json; sourceTree = ""; }; @@ -1302,6 +1335,7 @@ F351F1BF2CE380A40053423E /* InappMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappMapper.swift; sourceTree = ""; }; F351F1C12CE5F23A0053423E /* InappMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappMapperTests.swift; sourceTree = ""; }; F351F1C32CE60CA90053423E /* 1-Targeting.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "1-Targeting.json"; sourceTree = ""; }; + 7A3F1B2C9D4E5F60718293A6 /* Tags-FailedTargeting.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Tags-FailedTargeting.json"; sourceTree = ""; }; F351F1C52CE626450053423E /* 15-Targeting.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "15-Targeting.json"; sourceTree = ""; }; F351F1C72CE72B300053423E /* 44-Targeting.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "44-Targeting.json"; sourceTree = ""; }; F351F1C92CE72D460053423E /* 45-Targeting.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "45-Targeting.json"; sourceTree = ""; }; @@ -1313,6 +1347,7 @@ F361284C2BA31E36000382D9 /* PushPermissionActionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushPermissionActionUseCase.swift; sourceTree = ""; }; F367301A2B7B6E7600DD0039 /* MindboxPushValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MindboxPushValidatorTests.swift; sourceTree = ""; }; F367301C2B7B8B6A00DD0039 /* NotificationFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFormatTests.swift; sourceTree = ""; }; + F367D3772FE43B2900455DB0 /* ModalViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalViewControllerLayoutTests.swift; sourceTree = ""; }; F370940A2B8F9CDE00655AC7 /* InAppConfigurationDataFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppConfigurationDataFacade.swift; sourceTree = ""; }; F37613E02A6A8CFF009F2EE4 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; F382F20A2BAC50D000BC97FF /* VisitTargeting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitTargeting.swift; sourceTree = ""; }; @@ -1400,6 +1435,10 @@ F3C1A0012F5B100100ABC001 /* InappShowFailureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappShowFailureManager.swift; sourceTree = ""; }; F3C1A0032F5B100100ABC001 /* InAppShowFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppShowFailure.swift; sourceTree = ""; }; F3C1A0052F5B100100ABC001 /* InappShowFailureManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappShowFailureManagerTests.swift; sourceTree = ""; }; + 045EA348681AB859FB298A57 /* InAppTagsGatingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppTagsGatingTests.swift; sourceTree = ""; }; + F60DC2E1435521EBF0E2C5E4 /* InAppMessagesTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagesTrackerTests.swift; sourceTree = ""; }; + 55E67F6257F15F143DEA87DE /* JSONValueTagsMergeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONValueTagsMergeTests.swift; sourceTree = ""; }; + 7A3F1B2C9D4E5F60718293A4 /* TransparentViewJSBridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentViewJSBridgeTests.swift; sourceTree = ""; }; F3CD20272F600A800065392A /* MBConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBConfigurationTests.swift; sourceTree = ""; }; F3CD202A2F600A800065392A /* HostNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostNormalizer.swift; sourceTree = ""; }; F3CD202C2F600A800065392A /* HostNormalizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostNormalizerTests.swift; sourceTree = ""; }; @@ -1441,39 +1480,13 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - F385631E2DB6729000D91208 /* InappConfigurationDataFacade */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = InappConfigurationDataFacade; - sourceTree = ""; - }; - F397DE1C2CFF568800B72DA9 /* JSONs */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = JSONs; - sourceTree = ""; - }; - F3DEB38C2D47CBA200D0EFA4 /* InappSessionManagerTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = InappSessionManagerTests; - sourceTree = ""; - }; + 17B157F9FE3789FA97A7085E /* MindboxLoggerTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MindboxLoggerTests; sourceTree = ""; }; + 471D4C2D2FEECF8800856EA5 /* MindboxNotificationsTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MindboxNotificationsTests; sourceTree = ""; }; + 471D4C3B2FEED16200856EA5 /* TestPlans */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TestPlans; sourceTree = ""; }; + F385631E2DB6729000D91208 /* InappConfigurationDataFacade */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = InappConfigurationDataFacade; sourceTree = ""; }; + F397DE1C2CFF568800B72DA9 /* JSONs */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = JSONs; sourceTree = ""; }; + F3DEB38C2D47CBA200D0EFA4 /* InappSessionManagerTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = InappSessionManagerTests; sourceTree = ""; }; + A8C878878353491FA01AC096 /* WebViewPrewarmTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WebViewPrewarmTests; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1497,7 +1510,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A1515DB229B228F40025E2EE /* MindboxLogger.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1531,6 +1543,7 @@ isa = PBXGroup; children = ( 3456BAC56F984378B6CED7CB /* FeatureToggleManager.swift */, + 3E8D4000B12B31961251F6C7 /* InAppTagsGating.swift */, ); path = FeatureToggleManager; sourceTree = ""; @@ -1538,7 +1551,13 @@ 2B4C84F6EDD4B7D977F67A95 /* WebView */ = { isa = PBXGroup; children = ( + 9CC5ED4F76B046798978B4B9 /* MindboxWebBridgeTests.swift */, + 895F8C05EA8B4716B2A2941A /* InAppWebViewCacheTests.swift */, + 11E54FE3B51649DCBD5ABE33 /* WebViewTimeoutErrorTests.swift */, + 4D716044131845AEA04A9858 /* WebViewReadyCheckerTests.swift */, 0CFCC82B8014DE7276C217CD /* WebViewLocalStateStorageTests.swift */, + 55E67F6257F15F143DEA87DE /* JSONValueTagsMergeTests.swift */, + 7A3F1B2C9D4E5F60718293A4 /* TransparentViewJSBridgeTests.swift */, A1B2C3D400000006E1010101 /* BridgeMessagePermissionTests.swift */, BD1BE43AA9EAEA03F8ED400C /* HapticRequestParserTests.swift */, BD1BE43AA9EAEA03F8ED400D /* HapticRequestValidatorTests.swift */, @@ -1556,11 +1575,13 @@ 313B233225ADEA0F00A1CB72 /* Mindbox */, 313B233D25ADEA0F00A1CB72 /* MindboxTests */, 3333C1992681D3CF00B60D84 /* MindboxNotifications */, - 3333C1A42681D3CF00B60D84 /* MindboxNotificationsTests */, + 471D4C2D2FEECF8800856EA5 /* MindboxNotificationsTests */, 9BC24E6C28F694CA00C2619C /* SDKVersionProvider */, A17853BF29AF7E940072578F /* MindboxLogger */, 47D395612E72186600C44CFE /* Frameworks */, 313B233125ADEA0F00A1CB72 /* Products */, + 17B157F9FE3789FA97A7085E /* MindboxLoggerTests */, + 471D4C3B2FEED16200856EA5 /* TestPlans */, ); sourceTree = ""; }; @@ -1636,6 +1657,7 @@ 84B625EE25C98A8000AB6228 /* Validators */, F3CD20282F600A800065392A /* Configuration */, 313B233E25ADEA0F00A1CB72 /* MindboxTests.swift */, + A1B2C3D4E5F60718293A4B02 /* MindboxOperationsTests.swift */, 84DC49D525D185A600D5D758 /* Supporting Files */, 313B234025ADEA0F00A1CB72 /* Info.plist */, 73662EFB100A1A3520058D4E /* TrackVisitManager */, @@ -1675,6 +1697,7 @@ 31A20D4A25B6E09900AAA0A3 /* Utilities */ = { isa = PBXGroup; children = ( + 68184C936B4243C28CC10829 /* SDKUserAgent.swift */, 47CFDAB82F3A05430034F310 /* SystemInfo */, F31909972E979D8200373E2F /* AppDelegateProxy */, 47BD5BF82C578AD700F965C0 /* Migrations */, @@ -1713,17 +1736,6 @@ path = MindboxNotifications; sourceTree = ""; }; - 3333C1A42681D3CF00B60D84 /* MindboxNotificationsTests */ = { - isa = PBXGroup; - children = ( - 4747708E2C6B93AC00C36FC8 /* MindboxNotificationServiceTests.swift */, - 474770902C6B9A7200C36FC8 /* MindboxNotificationContentTests.swift */, - 4747708A2C6B838B00C36FC8 /* SharedInternalMethodsTests.swift */, - 3333C1A72681D3CF00B60D84 /* Info.plist */, - ); - path = MindboxNotificationsTests; - sourceTree = ""; - }; 3333C1BE2681DE6C00B60D84 /* Models */ = { isa = PBXGroup; children = ( @@ -1869,6 +1881,7 @@ 472179A62C80755A00C15E7F /* ShownInAppsIDsMigration.swift */, F34975372DEE2C6A00BEC667 /* ShownInAppsDictionaryMigration.swift */, 472765512E0D52A500A5A060 /* RemoveBackgroundTaskDataMigration.swift */, + 8755088A5E704C65A1C3F6DB /* DateFormatMigration.swift */, 47ADB8142E8D3CEA00AFCCB2 /* DatabaseMetadataMigration.swift */, F3266A382F6295A600CE6137 /* FirstInitializationDateTimeMigration.swift */, ); @@ -2161,6 +2174,7 @@ 47B90E2D2C625F8A00BD93E7 /* TestsMigrations */, 475558C22C59300400CDA026 /* MigrationManagerTests.swift */, B11C2D3E4F5566778899AA02 /* FirstInitializationDateTimeMigrationTests.swift */, + BF1A11C4A4B940898BA80035 /* DateFormatMigrationTests.swift */, 476689C62C85B6AE0066BB12 /* ShownInAppsIdsMigrationTests.swift */, F34975392DEE2CB700BEC667 /* ShownInAppsDictionaryMigrationTests.swift */, 47C464EE2E0EB88C00F50B21 /* RemoveBackgroundTaskDataMigrationTests.swift */, @@ -2280,6 +2294,7 @@ 64FD3F7576619DCF106D10B6 /* Network */ = { isa = PBXGroup; children = ( + 4E45E06561604E6185C33366 /* SDKUserAgentTests.swift */, 97FEDDEB5F71A67F1C4C675F /* MBNetworkFetcherResponseHandlingTests.swift */, F3BA5E000130A000C0000006 /* OperationsURLRoutingTests.swift */, F3CD202C2F600A800065392A /* HostNormalizerTests.swift */, @@ -2354,6 +2369,7 @@ 840C38B725D13A7D00D50183 /* EventGenerator.swift */, 848A89582620C3AE00EDFB6D /* APNSTokenGenerator.swift */, 47D0BC2B2E093F8A00182DB2 /* ClockAndMockClock.swift */, + A1B2C3D4E5F60718293A4B06 /* PollUntil.swift */, ); path = Helpers; sourceTree = ""; @@ -2438,6 +2454,7 @@ children = ( F3EB95702DE44684000E221B /* InappPresentationValidator.swift */, 84B625E325C988FA00AB6228 /* URLValidator.swift */, + 0EF1D88A18D64D60BD03ABB8 /* OperationNameValidator.swift */, 84B625E825C989C100AB6228 /* UDIDValidator.swift */, F3A8B9932A3A409C00E9C055 /* Validator.swift */, F3A8B9912A3A408C00E9C055 /* SDKVersionValidator.swift */, @@ -2456,6 +2473,7 @@ F3A961D52DE9C5220016D5D3 /* InAppPresentationValidatorTests.swift */, 84B625EF25C98B1200AB6228 /* ValidatorsTestCase.swift */, F3CD202E2F600A800065392A /* URLValidatorTests.swift */, + A1B2C3D4E5F60718293A4B04 /* OperationNameValidatorTests.swift */, F3A8B9972A3A421C00E9C055 /* SDKVersionValidatorTests.swift */, F30629192BD27D7500EF6609 /* InappFrequencyTests.swift */, ); @@ -2465,11 +2483,13 @@ 84B625F525C98EE000AB6228 /* DI */ = { isa = PBXGroup; children = ( + 4B4F5CEE405E4BE29F97E43B /* MBContainerConcurrencyTests.swift */, F3FEEAA72C25CC8F000E9D0F /* Injections */, F3FEEA9E2C25AF39000E9D0F /* StubContainer.swift */, F32E536E2C3F2B05002C7CA0 /* DITests.swift */, F32E53782C3FD64A002C7CA0 /* DIMainModuleRegistrationTests.swift */, F32E537A2C3FDA30002C7CA0 /* DIMainModuleReplaceableTests.swift */, + B7705CA12E0A1F0000C0FFEE /* AppGroupUnavailableTests.swift */, F32E537C2C3FDB6D002C7CA0 /* DITestModuleReplaceableTests.swift */, ); path = DI; @@ -2714,6 +2734,8 @@ 9B5256FD28D1A86F0029B1BC /* Tests */ = { isa = PBXGroup; children = ( + F367D3772FE43B2900455DB0 /* ModalViewControllerLayoutTests.swift */, + A8C878878353491FA01AC096 /* WebViewPrewarmTests */, F385631E2DB6729000D91208 /* InappConfigurationDataFacade */, F3DEB38C2D47CBA200D0EFA4 /* InappSessionManagerTests */, F39B67B62A3FAA62005C0CCA /* ABTesting */, @@ -2727,6 +2749,8 @@ F3B70A042F250A0100AABB03 /* TimeToDisplayBackgroundTests.swift */, F3A0B0012F28A00100CE7E63 /* TransparentViewTests.swift */, F3C1A0052F5B100100ABC001 /* InappShowFailureManagerTests.swift */, + 045EA348681AB859FB298A57 /* InAppTagsGatingTests.swift */, + F60DC2E1435521EBF0E2C5E4 /* InAppMessagesTrackerTests.swift */, 2B4C84F6EDD4B7D977F67A95 /* WebView */, A1B2C3D400000007E1010101 /* Permissions */, 4B7DAFAB687945FA908DB1AC /* TransparentViewSyncOperationResponseTests.swift */, @@ -2799,6 +2823,8 @@ isa = PBXGroup; children = ( B11C2D3E4F5566778899AA01 /* FirstInitializationDateTimeRuntimeTests.swift */, + B11C2D3E4F5566778899AA11 /* DeviceUUIDInitializationTests.swift */, + B11C2D3E4F5566778899AA12 /* MBPersistenceStorageDateTests.swift */, ); path = InitializationTests; sourceTree = ""; @@ -2807,10 +2833,7 @@ isa = PBXGroup; children = ( A154E332299E10F400F8F074 /* Mocks */, - A154E303299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift */, A154E32D299E0D8900F8F074 /* SDKLogManagerTests.swift */, - 47A4FA772E73741700569870 /* LoggerDatabaseLoaderTests.swift */, - 47A4FA752E735C5200569870 /* LogStoreTrimmerTests.swift */, ); path = MindboxLogger; sourceTree = ""; @@ -2819,8 +2842,6 @@ isa = PBXGroup; children = ( A154E333299E110E00F8F074 /* EventRepositoryMock.swift */, - 47A220A42E2158A00001507C /* IsolatedMBLoggerCoreDataManager.swift */, - 474230DB2E72236500282764 /* TestContainers.swift */, ); path = Mocks; sourceTree = ""; @@ -3052,9 +3073,25 @@ path = InAppTargetingChecker; sourceTree = ""; }; + 0E8BCE24EE9540D6BD7F655D /* Prewarm */ = { + isa = PBXGroup; + children = ( + 1B76146361C3402193A00F88 /* InAppWebViewLearnedHostsStore.swift */, + DDCC6F9CEEF24C2BAB624DD5 /* InAppWebViewPrewarmPlanner.swift */, + 96A50D5650094A23A2C667CE /* InAppWebViewPrewarmNavigationPolicy.swift */, + 1F423FA6C2E243F68A76E9C2 /* InAppWebViewPrewarmService.swift */, + ); + path = Prewarm; + sourceTree = ""; + }; B4E4386F2D8AFA5700603F3A /* WebView */ = { isa = PBXGroup; children = ( + FD1627DAC8D54E448C6D7BF0 /* InAppWebViewFactory.swift */, + A85CCBEB1D874078954BB771 /* InAppWebViewHTMLFetcher.swift */, + 6D096F433EBB44C8B3490CF6 /* InAppWebViewDataStore.swift */, + F2DAFC2435264F15B8033CCE /* WebViewReadyChecker.swift */, + 0E8BCE24EE9540D6BD7F655D /* Prewarm */, 47B1B6A92F6174E0000A4B67 /* LocalState */, F3BD9F802F273BC800647BAF /* Bridge */, F30654B92F1A83430058808C /* Debug */, @@ -3118,6 +3155,7 @@ isa = PBXGroup; children = ( F351F1C32CE60CA90053423E /* 1-Targeting.json */, + 7A3F1B2C9D4E5F60718293A6 /* Tags-FailedTargeting.json */, F31470882B96387C00E01E5C /* 3-4-5-TargetingRequests.json */, F314708B2B96464900E01E5C /* 7-TargetingRequests.json */, F314708D2B964BFF00E01E5C /* 8-TargetingRequests.json */, @@ -3533,6 +3571,8 @@ F34A10402F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorMissing.json */, F34A10412F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json */, F34A10422F455C5B0065392A /* SettingsFeatureTogglesTypeError.json */, + F34A10492F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsFalse.json */, + F34A104A2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsTypeError.json */, ); path = FeatureTogglesError; sourceTree = ""; @@ -3573,7 +3613,6 @@ BB4D7CC62BDEC51D008E3AB8 /* Notification+Extensions.swift */, BB65630F2BE3BA430090C473 /* UIApplication+Extensions.swift */, F31DB4072F56A50E00DCEB85 /* NSError+Extensions.swift */, - 326423031CA9C6BF0E62BEFD /* Date+Extensions.swift */, F3482F292A65DCFC002A41EC /* String+Extensions.swift */, ); path = Extensions; @@ -3908,6 +3947,7 @@ 313B233C25ADEA0F00A1CB72 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( + A8C878878353491FA01AC096 /* WebViewPrewarmTests */, F385631E2DB6729000D91208 /* InappConfigurationDataFacade */, F397DE1C2CFF568800B72DA9 /* JSONs */, F3DEB38C2D47CBA200D0EFA4 /* InappSessionManagerTests */, @@ -3929,7 +3969,6 @@ buildRules = ( ); dependencies = ( - A1515DB529B228F40025E2EE /* PBXTargetDependency */, ); name = MindboxNotifications; productName = MindboxNotifications; @@ -3949,6 +3988,9 @@ dependencies = ( 3333C1A32681D3CF00B60D84 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 471D4C2D2FEECF8800856EA5 /* MindboxNotificationsTests */, + ); name = MindboxNotificationsTests; productName = MindboxNotificationsTests; productReference = 3333C1A02681D3CF00B60D84 /* MindboxNotificationsTests.xctest */; @@ -3985,6 +4027,9 @@ dependencies = ( A17853C829AF7E950072578F /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 17B157F9FE3789FA97A7085E /* MindboxLoggerTests */, + ); name = MindboxLoggerTests; productName = MindboxLoggerTests; productReference = A17853C529AF7E950072578F /* MindboxLoggerTests.xctest */; @@ -4119,6 +4164,8 @@ F34A10462F455C5B0065392A /* SettingsFeatureTogglesError.json in Resources */, F34A10472F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppShowErrorTypeError.json in Resources */, F34A10482F455C5B0065392A /* SettingsFeatureTogglesTypeError.json in Resources */, + F34A104B2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsFalse.json in Resources */, + F34A104C2F455C5B0065392A /* SettingsFeatureTogglesShouldSendInAppTagsTypeError.json in Resources */, F3BA10552F500A800065392A /* SettingsBaseAddressesError.json in Resources */, F3BA10562F500A800065392A /* SettingsBaseAddressesTypeError.json in Resources */, F3BA10572F500A800065392A /* SettingsBaseAddressesOperationsError.json in Resources */, @@ -4204,6 +4251,7 @@ F39117112AB4BD4500852298 /* missingBackgroundSection.json in Resources */, F31470962B96681F00E01E5C /* 27-TargetingRequests.json in Resources */, F351F1C42CE60CA90053423E /* 1-Targeting.json in Resources */, + 7A3F1B2C9D4E5F60718293A7 /* Tags-FailedTargeting.json in Resources */, 31EB907425C402F900368FFB /* TestConfig2.plist in Resources */, 31EB907325C402F900368FFB /* TestConfig3.plist in Resources */, F391170F2AB4B31800852298 /* unknownVariantType.json in Resources */, @@ -4296,6 +4344,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A881A16C702E41AE93848BD3 /* InAppWebViewLearnedHostsStore.swift in Sources */, + F389B3A639904206A3DADFFA /* InAppWebViewPrewarmPlanner.swift in Sources */, + 8A94D721DAEF4395A360C5B2 /* InAppWebViewPrewarmNavigationPolicy.swift in Sources */, + 23335221BB1D4F8C8D5C864D /* InAppWebViewPrewarmService.swift in Sources */, + 28375259024D42658E146900 /* InAppWebViewFactory.swift in Sources */, + B58D5207F49F4531AA1C516B /* InAppWebViewHTMLFetcher.swift in Sources */, + 2DBF19F24FEA48AAA39C33E6 /* InAppWebViewDataStore.swift in Sources */, + C830BF4C287849CE95BB4ED9 /* WebViewReadyChecker.swift in Sources */, + 46E95250B5904CC18E079C54 /* SDKUserAgent.swift in Sources */, 47EFBB2F2CB92B240023A4B9 /* SegmentationCheckResponse.swift in Sources */, F331DD062A83A56500222120 /* ModalViewController.swift in Sources */, F331DCC12A80993600222120 /* ContentElement.swift in Sources */, @@ -4552,6 +4609,7 @@ F3A4EFDC2D5224C700DB96A8 /* SlidingExpirationModel.swift in Sources */, DEC482157E5249DBBFAEFC9A /* FeatureTogglesModel.swift in Sources */, 3FB93AC126964AD3A061B4A7 /* FeatureToggleManager.swift in Sources */, + B202CDC049DDB7A2F9833ADF /* InAppTagsGating.swift in Sources */, 47DF1FB02E7D6F90009BC4A0 /* DatabaseLoaderProtocol.swift in Sources */, F31A94802BC7E61800E6C978 /* InappFrequencyValidator.swift in Sources */, 33072F322664C357001F1AB2 /* ProductListResponse.swift in Sources */, @@ -4572,6 +4630,7 @@ F3A8B9922A3A408C00E9C055 /* SDKVersionValidator.swift in Sources */, F31A94782BC6995500E6C978 /* InappFrequency.swift in Sources */, F3266A392F6295A600CE6137 /* FirstInitializationDateTimeMigration.swift in Sources */, + 7764BA10E25046CEA7035ED8 /* DateFormatMigration.swift in Sources */, F3482F212A65DC2C002A41EC /* URLInappMessageDelegate.swift in Sources */, BB6563102BE3BA430090C473 /* UIApplication+Extensions.swift in Sources */, 84A312C325DA65690096A017 /* BackgroundTaskManagerType.swift in Sources */, @@ -4593,6 +4652,7 @@ 314B390025AEE96F00E947B9 /* CoreController.swift in Sources */, F331DCCB2A80993600222120 /* ContentBackgroundLayerAction.swift in Sources */, 84B625E425C988FA00AB6228 /* URLValidator.swift in Sources */, + 4841683F6839440F84477966 /* OperationNameValidator.swift in Sources */, F39B67A52A3C6BE3005C0CCA /* SegmentationCheckRequest.swift in Sources */, F30F1A282B9F0C8A0099DFD9 /* PushPermissionLayerAction.swift in Sources */, A184654329C3102A00E64780 /* CategoryIDTargeting.swift in Sources */, @@ -4657,7 +4717,6 @@ B3A625502689F8B600B6A3B7 /* BenefitResponse.swift in Sources */, 84C65E6425D4FBBB008996FA /* MobileApplicationInfoUpdated.swift in Sources */, 84DEE8AD25CC036A00C98CC7 /* MBDatabaseRepository.swift in Sources */, - F26DFF81C3FF57C3DE68DEDC /* Date+Extensions.swift in Sources */, 6182078DDFC681D168546DAD /* HapticService.swift in Sources */, A1B2C3D4E5F6A7B8C9D0E1F2 /* MotionService.swift in Sources */, 6182078DDFC681D168546DAE /* HapticRequest.swift in Sources */, @@ -4671,16 +4730,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 23E4A509BFE94104BD01B61A /* MindboxWebBridgeTests.swift in Sources */, + AE190755CA97473B83E1568D /* MBContainerConcurrencyTests.swift in Sources */, + 3FDBA6E4D39A49369A335CF4 /* SDKUserAgentTests.swift in Sources */, + 16311BEB069E4B3983FAF594 /* InAppWebViewCacheTests.swift in Sources */, + B9025268DDC24820B95ADE10 /* WebViewTimeoutErrorTests.swift in Sources */, + CB91323FAD66404B9422B6E0 /* WebViewReadyCheckerTests.swift in Sources */, F349753A2DEE2CB700BEC667 /* ShownInAppsDictionaryMigrationTests.swift in Sources */, 4766A8AE2C9325B0002D15A4 /* ABTestsConfigParsingTests.swift in Sources */, F3FEEA9F2C25AF39000E9D0F /* StubContainer.swift in Sources */, 47EFF0FD2E8D85B700E72D0A /* DatabaseMetadataMigrationTests.swift in Sources */, 840C38B325D133B000D50183 /* GuaranteedDeliveryTestCase.swift in Sources */, F32E537B2C3FDA30002C7CA0 /* DIMainModuleReplaceableTests.swift in Sources */, + B7705CA22E0A1F0000C0FFEE /* AppGroupUnavailableTests.swift in Sources */, F32B68882CF83B030088BCDD /* InappConfigurationTests.swift in Sources */, 84FCD3B925CA109E00D1E574 /* MockNetworkFetcher.swift in Sources */, 9B9C9538292111A700BB29DA /* MockUUIDDebugService.swift in Sources */, - 47A4FA782E73741700569870 /* LoggerDatabaseLoaderTests.swift in Sources */, 84B625F025C98B1200AB6228 /* ValidatorsTestCase.swift in Sources */, F3CD202D2F600A800065392A /* URLValidatorTests.swift in Sources */, 4741425E2E8A688300839AD8 /* DataBaseLoading_StubDatabaseLoaderContractTests.swift in Sources */, @@ -4692,7 +4757,6 @@ A17958992978E04600609E91 /* InAppStub.swift in Sources */, F39B67A72A3C6C6A005C0CCA /* SegmentationServiceTests.swift in Sources */, 476689C72C85B6AE0066BB12 /* ShownInAppsIdsMigrationTests.swift in Sources */, - 474230DC2E72236500282764 /* TestContainers.swift in Sources */, F3D925AB2A120C0F00135C87 /* InAppImageDownloaderMock.swift in Sources */, F3E3EEA62CFA27EC00AAC91A /* Tag+Extensions.swift in Sources */, F367301D2B7B8B6A00DD0039 /* NotificationFormatTests.swift in Sources */, @@ -4720,6 +4784,10 @@ F3B70A052F250A0100AABB03 /* TimeToDisplayBackgroundTests.swift in Sources */, F3A0B0022F28A00100CE7E63 /* TransparentViewTests.swift in Sources */, F3C1A0062F5B100100ABC001 /* InappShowFailureManagerTests.swift in Sources */, + FA39EE97042009DCEC24E971 /* InAppTagsGatingTests.swift in Sources */, + 1E316CC518D9111F0BD25590 /* InAppMessagesTrackerTests.swift in Sources */, + AB0E83C770AAB9A2DC8E9BF5 /* JSONValueTagsMergeTests.swift in Sources */, + 7A3F1B2C9D4E5F60718293A5 /* TransparentViewJSBridgeTests.swift in Sources */, D216DE512C0716B70020F58A /* StringExtensionsTests.swift in Sources */, D216DE532C0716B80020F58A /* TimeIntervalTimeSpanTests.swift in Sources */, A17958812978B3FA00609E91 /* InAppResponseModelTests.swift in Sources */, @@ -4729,6 +4797,9 @@ 848A895E2620C54900EDFB6D /* VersioningTestCase.swift in Sources */, A1A916E529C9191900D59D9E /* InAppConfigStub.swift in Sources */, 313B233F25ADEA0F00A1CB72 /* MindboxTests.swift in Sources */, + A1B2C3D4E5F60718293A4B01 /* MindboxOperationsTests.swift in Sources */, + A1B2C3D4E5F60718293A4B03 /* OperationNameValidatorTests.swift in Sources */, + A1B2C3D4E5F60718293A4B05 /* PollUntil.swift in Sources */, F39116EE2AA53EE400852298 /* VariantImageUrlExtractorServiceTests.swift in Sources */, A154E334299E110E00F8F074 /* EventRepositoryMock.swift in Sources */, F3CD20262F600A800065392A /* MBConfigurationTests.swift in Sources */, @@ -4741,20 +4812,18 @@ A17958972978D2B300609E91 /* InAppTargetingCheckerTests.swift in Sources */, F39B67AE2A3C737F005C0CCA /* ImageDownloadServiceTests.swift in Sources */, 47B90E2F2C625F9A00BD93E7 /* TestBaseMigrations.swift in Sources */, - 47A4FA762E735C5200569870 /* LogStoreTrimmerTests.swift in Sources */, 84A0CD5A260B021F004CD91B /* MockFailureNetworkFetcher.swift in Sources */, 475558C32C59300400CDA026 /* MigrationManagerTests.swift in Sources */, 84E1D6A9261D8D54002BF03A /* DatabaseLoaderTest.swift in Sources */, 840C38B825D13A7D00D50183 /* EventGenerator.swift in Sources */, 84CC79A525CAE14200C062BD /* EventRepositoryTestCase.swift in Sources */, F367301B2B7B6E7600DD0039 /* MindboxPushValidatorTests.swift in Sources */, - 47A220A52E2158A00001507C /* IsolatedMBLoggerCoreDataManager.swift in Sources */, - A154E304299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift in Sources */, F32E537D2C3FDB6D002C7CA0 /* DITestModuleReplaceableTests.swift in Sources */, 840C388525CD169400D50183 /* DatabaseRepositoryTestCase.swift in Sources */, 84FCD3B525CA0FD300D1E574 /* MockPersistenceStorage.swift in Sources */, F3A8B99C2A3A47F800E9C055 /* ABTestVariantsValidatorTests.swift in Sources */, 848A89592620C3AE00EDFB6D /* APNSTokenGenerator.swift in Sources */, + F367D3782FE43B2900455DB0 /* ModalViewControllerLayoutTests.swift in Sources */, F32E536F2C3F2B05002C7CA0 /* DITests.swift in Sources */, F3D818B32A3885F50002957C /* ABTestDeviceMixerTests.swift in Sources */, F391170B2AB2E74F00852298 /* InappFilterServiceTests.swift in Sources */, @@ -4766,7 +4835,10 @@ F306291A2BD27D7500EF6609 /* InappFrequencyTests.swift in Sources */, F3AF0AD02BC40FCF0063BA58 /* InappTTLTests.swift in Sources */, A11C2D3E4F5566778899AA01 /* FirstInitializationDateTimeRuntimeTests.swift in Sources */, + A11C2D3E4F5566778899AA11 /* DeviceUUIDInitializationTests.swift in Sources */, + A11C2D3E4F5566778899AA12 /* MBPersistenceStorageDateTests.swift in Sources */, A11C2D3E4F5566778899AA02 /* FirstInitializationDateTimeMigrationTests.swift in Sources */, + 1E28BAF922E64B9CB6E22A0B /* DateFormatMigrationTests.swift in Sources */, 5D3FCB95C2AF59DF36A61254 /* WebViewLocalStateStorageTests.swift in Sources */, A1B2C3D400000001E1010101 /* MockPermissionHandler.swift in Sources */, A1B2C3D400000003E1010101 /* PermissionHandlerRegistryTests.swift in Sources */, @@ -4811,9 +4883,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4747708B2C6B838B00C36FC8 /* SharedInternalMethodsTests.swift in Sources */, - 4747708F2C6B93AC00C36FC8 /* MindboxNotificationServiceTests.swift in Sources */, - 474770912C6B9A7200C36FC8 /* MindboxNotificationContentTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4871,11 +4940,6 @@ target = 3333C1972681D3CF00B60D84 /* MindboxNotifications */; targetProxy = 3333C1A22681D3CF00B60D84 /* PBXContainerItemProxy */; }; - A1515DB529B228F40025E2EE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A17853BD29AF7E940072578F /* MindboxLogger */; - targetProxy = A1515DB429B228F40025E2EE /* PBXContainerItemProxy */; - }; A170EDDF29B0883800CE547F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A17853BD29AF7E940072578F /* MindboxLogger */; @@ -5100,7 +5164,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - PRODUCT_BUNDLE_IDENTIFIER = Mindbox.MindboxTests; + PRODUCT_BUNDLE_IDENTIFIER = cloud.MindboxTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5119,7 +5183,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - PRODUCT_BUNDLE_IDENTIFIER = Mindbox.MindboxTests; + PRODUCT_BUNDLE_IDENTIFIER = cloud.MindboxTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5133,7 +5197,7 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = N39VVWZXXP; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -5165,7 +5229,7 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = N39VVWZXXP; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -5194,9 +5258,9 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = N39VVWZXXP; - INFOPLIST_FILE = MindboxNotificationsTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5214,9 +5278,9 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = N39VVWZXXP; - INFOPLIST_FILE = MindboxNotificationsTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5238,7 +5302,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = N39VVWZXXP; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -5255,9 +5319,10 @@ MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = cloud.organization.MindboxLogger; + PRODUCT_BUNDLE_IDENTIFIER = cloud.MindboxLogger; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5272,7 +5337,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = N39VVWZXXP; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -5289,9 +5354,10 @@ MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; - PRODUCT_BUNDLE_IDENTIFIER = cloud.organization.MindboxLogger; + PRODUCT_BUNDLE_IDENTIFIER = cloud.MindboxLogger; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5304,12 +5370,12 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = N39VVWZXXP; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = cloud.organization.MindboxLoggerTests; + PRODUCT_BUNDLE_IDENTIFIER = cloud.MindboxLoggerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -5323,12 +5389,12 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = N39VVWZXXP; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = cloud.organization.MindboxLoggerTests; + PRODUCT_BUNDLE_IDENTIFIER = cloud.MindboxLoggerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; diff --git a/Mindbox.xcodeproj/xcshareddata/xcschemes/Mindbox.xcscheme b/Mindbox.xcodeproj/xcshareddata/xcschemes/Mindbox.xcscheme index 17b2e4202..d938947a3 100644 --- a/Mindbox.xcodeproj/xcshareddata/xcschemes/Mindbox.xcscheme +++ b/Mindbox.xcodeproj/xcshareddata/xcschemes/Mindbox.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -41,6 +41,18 @@ isEnabled = "YES"> + + + + + + + + Self { register(InAppMessagesTracker.self) { let databaseRepository = DI.injectOrFail(DatabaseRepositoryProtocol.self) - return InAppMessagesTracker(databaseRepository: databaseRepository) + let featureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + return InAppMessagesTracker(databaseRepository: databaseRepository, featureToggleManager: featureToggleManager) } register(PresentationDisplayUseCase.self) { @@ -129,6 +130,11 @@ extension MBContainer { ) } + register(InAppWebViewPrewarmServiceProtocol.self) { + let persistenceStorage = DI.injectOrFail(PersistenceStorage.self) + return InAppWebViewPrewarmService(persistenceStorage: persistenceStorage) + } + return self } } diff --git a/Mindbox/DI/Injections/InjectReplaceable.swift b/Mindbox/DI/Injections/InjectReplaceable.swift index b75a53212..fa41f206d 100644 --- a/Mindbox/DI/Injections/InjectReplaceable.swift +++ b/Mindbox/DI/Injections/InjectReplaceable.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import MindboxLogger extension MBContainer { func registerReplaceableUtilities() -> Self { @@ -29,10 +30,21 @@ extension MBContainer { register(PersistenceStorage.self) { let utilitiesFetcher = DI.injectOrFail(UtilitiesFetcher.self) - guard let defaults = UserDefaults(suiteName: utilitiesFetcher.applicationGroupIdentifier) else { - assertionFailure("Failed to create UserDefaults with suite name: \(utilitiesFetcher.applicationGroupIdentifier). Check and set up your AppGroups correctly.") - return MBPersistenceStorage(defaults: UserDefaults.standard) + let appGroup = utilitiesFetcher.applicationGroupIdentifier + + guard !appGroup.isEmpty else { + // App Group unavailable (already reported by the fetcher) — fall back to + // `.standard` so the SDK keeps working instead of crashing the host (issue #705). + // A later App Group fix re-registers the install; see `AppGroupStorageTransitionReporter`. + return MBPersistenceStorage(defaults: .standard) + } + + guard let defaults = UserDefaults(suiteName: appGroup) else { + // Unreachable for a resolved App Group id; degrade without trapping (issue #705). + Logger.common(message: "[PersistenceStorage] UserDefaults(suiteName: \(appGroup)) failed; using .standard.", level: .fault, category: .general) + return MBPersistenceStorage(defaults: .standard) } + return MBPersistenceStorage(defaults: defaults) } diff --git a/Mindbox/DI/MBContainer.swift b/Mindbox/DI/MBContainer.swift index b798c12e6..7dfc9ff12 100644 --- a/Mindbox/DI/MBContainer.swift +++ b/Mindbox/DI/MBContainer.swift @@ -16,15 +16,21 @@ enum ObjectScope { class MBContainer { private var factories: [String: (ObjectScope, () -> Any)] = [:] private var singletons: [String: Any] = [:] + // Recursive: factories resolve their own dependencies on the same thread. + private let lock = NSRecursiveLock() func register(_ type: T.Type, scope: ObjectScope = .container, factory: @escaping () -> T) { let key = String(describing: type) + lock.lock() + defer { lock.unlock() } factories[key] = (scope, factory) } func resolve(_ type: T.Type) -> T? { let key = String(describing: type) + lock.lock() + defer { lock.unlock() } if let (scope, factory) = factories[key] { switch scope { case .container: diff --git a/Mindbox/Extensions/Date+Extensions.swift b/Mindbox/Extensions/Date+Extensions.swift deleted file mode 100644 index 6d7658fa1..000000000 --- a/Mindbox/Extensions/Date+Extensions.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Date+Extensions.swift -// Mindbox -// -// Created by Mindbox on 09.03.2026. -// Copyright © 2026 Mindbox. All rights reserved. -// - -import Foundation - -extension Date { - - private static let iso8601Formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.calendar = Calendar(identifier: .iso8601) - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" - return formatter - }() - - private static let iso8601WithMillisecondsFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.calendar = Calendar(identifier: .iso8601) - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" - return formatter - }() - - var iso8601: String { - Self.iso8601Formatter.string(from: self) - } - - static func fromISO8601(_ string: String) -> Date? { - iso8601Formatter.date(from: string) - ?? iso8601WithMillisecondsFormatter.date(from: string) - } -} diff --git a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift index 09f17ebe6..5cd4adf39 100644 --- a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift +++ b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift @@ -33,19 +33,22 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { private let inAppConfigAPI: InAppConfigurationAPI private let persistenceStorage: PersistenceStorage private let featureToggleManager: FeatureToggleManager + private let webViewPrewarmService: InAppWebViewPrewarmServiceProtocol init( inAppConfigAPI: InAppConfigurationAPI, inAppConfigRepository: InAppConfigurationRepository, inappMapper: InappMapperProtocol?, persistenceStorage: PersistenceStorage, - featureToggleManager: FeatureToggleManager + featureToggleManager: FeatureToggleManager, + webViewPrewarmService: InAppWebViewPrewarmServiceProtocol ) { self.inAppConfigRepository = inAppConfigRepository self.inappMapper = inappMapper self.inAppConfigAPI = inAppConfigAPI self.persistenceStorage = persistenceStorage self.featureToggleManager = featureToggleManager + self.webViewPrewarmService = webViewPrewarmService } weak var delegate: InAppConfigurationDelegate? @@ -101,6 +104,11 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { Logger.common(message: "Failed to download InApp configuration. Error: \(error.localizedDescription)", level: .error, category: .inAppMessages) } + // Prewarm stage 2: warm what the config's webview in-apps need (or release the + // warm instance when the config proves there are none). + if let configResponse { + webViewPrewarmService.prewarmResources(for: configResponse) + } self.delegate?.didPreparedConfiguration() sendNotification(with: configResponse?.settings?.slidingExpiration?.pushTokenKeepalive) } diff --git a/Mindbox/InAppMessages/Configuration/InAppConfigurationRepository.swift b/Mindbox/InAppMessages/Configuration/InAppConfigurationRepository.swift index 329009384..1baa0228e 100644 --- a/Mindbox/InAppMessages/Configuration/InAppConfigurationRepository.swift +++ b/Mindbox/InAppMessages/Configuration/InAppConfigurationRepository.swift @@ -27,9 +27,20 @@ class InAppConfigurationRepository { } } + /// The single home of the lenient cached-config decode for launch-time readers (the + /// prewarm stages, the WebView cache toggle). The config manager keeps its own richer + /// variant with per-outcome logging. + func fetchDecodedConfigFromCache() -> ConfigResponse? { + guard let data = fetchConfigFromCache() else { return nil } + return try? JSONDecoder().decode(ConfigResponse.self, from: data) + } + func saveConfigToCache(_ data: Data) { do { - try data.write(to: inAppConfigFileUrl) + // Atomic (write-then-rename): the init-time prewarm reads this file from a + // background queue while the config download rewrites it — a concurrent + // reader must see the old or the new config, never a torn file. + try data.write(to: inAppConfigFileUrl, options: .atomic) Logger.common(message: "Successfuly saved config file on a disk.", level: .debug, category: .inAppMessages) } catch { Logger.common(message: "Failed to save config file on a disk. Error: \(error.localizedDescription)", level: .debug, category: .inAppMessages) diff --git a/Mindbox/InAppMessages/FeatureToggleManager/FeatureToggleManager.swift b/Mindbox/InAppMessages/FeatureToggleManager/FeatureToggleManager.swift index 1f4cc3bb2..1f63bf498 100644 --- a/Mindbox/InAppMessages/FeatureToggleManager/FeatureToggleManager.swift +++ b/Mindbox/InAppMessages/FeatureToggleManager/FeatureToggleManager.swift @@ -11,23 +11,41 @@ import MindboxLogger enum FeatureFlag { case shouldSendInAppShowError + case shouldSendInAppTags + case shouldPrewarmInAppWebView + case shouldCacheInAppWebView var defaultValue: Bool { switch self { - case .shouldSendInAppShowError: + case .shouldSendInAppShowError, .shouldPrewarmInAppWebView, .shouldCacheInAppWebView: + return true + case .shouldSendInAppTags: return true } } } +/// Holds the toggles applied from a freshly downloaded config. The WebView flags +/// (`shouldPrewarmInAppWebView`, `shouldCacheInAppWebView`) are deliberately read +/// config-scoped by their consumers instead (each prewarm stage reads the config it works +/// with; the data store latches from the config cache) — those reads happen before any +/// fresh config can be applied here. final class FeatureToggleManager { + /// Guards `featureToggles`: it is written on the config-fetch queue and read from arbitrary queues (tracking, WebView JS-bridge). + private let lock = NSLock() private var featureToggles: Settings.FeatureToggles? func applyFeatureToggles(_ featureToggles: Settings.FeatureToggles?) { + lock.lock() self.featureToggles = featureToggles + lock.unlock() + let flags: [String] = [ - featureToggles?.shouldSendInAppShowError.map { "MobileSdkShouldSendInAppShowError=\($0)" } + featureToggles?.shouldSendInAppShowError.map { "MobileSdkShouldSendInAppShowError=\($0)" }, + featureToggles?.shouldSendInAppTags.map { "MobileSdkShouldSendInAppTags=\($0)" }, + featureToggles?.shouldPrewarmInAppWebView.map { "MobileSdkShouldPrewarmInAppWebView=\($0)" }, + featureToggles?.shouldCacheInAppWebView.map { "MobileSdkShouldCacheInAppWebView=\($0)" } ].compactMap { $0 } Logger.common( message: "[FeatureToggles] \(flags)", @@ -35,11 +53,21 @@ final class FeatureToggleManager { category: .inAppMessages ) } - + func isFeatureEnabled(_ feature: FeatureFlag) -> Bool { + lock.lock() + let featureToggles = self.featureToggles + lock.unlock() + switch feature { case .shouldSendInAppShowError: return featureToggles?.shouldSendInAppShowError ?? feature.defaultValue + case .shouldSendInAppTags: + return featureToggles?.shouldSendInAppTags ?? feature.defaultValue + case .shouldPrewarmInAppWebView: + return featureToggles?.shouldPrewarmInAppWebView ?? feature.defaultValue + case .shouldCacheInAppWebView: + return featureToggles?.shouldCacheInAppWebView ?? feature.defaultValue } } } diff --git a/Mindbox/InAppMessages/FeatureToggleManager/InAppTagsGating.swift b/Mindbox/InAppMessages/FeatureToggleManager/InAppTagsGating.swift new file mode 100644 index 000000000..2078d698d --- /dev/null +++ b/Mindbox/InAppMessages/FeatureToggleManager/InAppTagsGating.swift @@ -0,0 +1,22 @@ +// +// InAppTagsGating.swift +// Mindbox +// +// Created by Akylbek Utekeshev on 01.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +extension FeatureToggleManager { + /// Returns `tags` when `isEnabled` is `true` and the dictionary is non-empty, `nil` otherwise. + static func gatedTags(_ tags: [String: String]?, isEnabled: Bool) -> [String: String]? { + guard isEnabled, let tags, !tags.isEmpty else { return nil } + return tags + } + + /// Returns `tags` when `MobileSdkShouldSendInAppTags` is enabled and the dictionary is non-empty, `nil` otherwise. + func gatedTags(_ tags: [String: String]?) -> [String: String]? { + Self.gatedTags(tags, isEnabled: isFeatureEnabled(.shouldSendInAppTags)) + } +} diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/InappMapper.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/InappMapper.swift index dd3db5b7e..ec6e4d885 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/InappMapper.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/InappMapper.swift @@ -91,7 +91,11 @@ class InappMapper: InappMapperProtocol { let suitableInapps = self.inappFilterService.filterInappsByTargeting(inapps: inapps, targetingChecker: self.targetingChecker) let suitableIds = Set(suitableInapps.map(\.inAppId)) let failedTargetingInappIds = Set(inapps.map(\.id)).subtracting(suitableIds) - self.dataFacade.collectTargetingFailures(forFailedTargetingInappIds: failedTargetingInappIds) + let tagsByInappId: [String: [String: String]] = inapps.reduce(into: [:]) { result, inapp in + guard failedTargetingInappIds.contains(inapp.id), let tags = inapp.tags else { return } + result[inapp.id] = tags + } + self.dataFacade.collectTargetingFailures(forFailedTargetingInappIds: failedTargetingInappIds, tagsByInappId: tagsByInappId) if suitableInapps.isEmpty { completion(nil) @@ -152,7 +156,7 @@ class InappMapper: InappMapperProtocol { for imageValue in imageValues { group.enter() Logger.common(message: "[InappMapper] Initiating the process of image loading from the URL: \(imageValue)", level: .debug, category: .inAppMessages) - self.dataFacade.downloadImage(withUrl: imageValue, inappId: inapp.inAppId) { result in + self.dataFacade.downloadImage(withUrl: imageValue, inappId: inapp.inAppId, tags: inapp.tags) { result in defer { group.leave() } @@ -189,7 +193,7 @@ class InappMapper: InappMapperProtocol { group.notify(queue: .main) { DispatchQueue.main.async { [weak self] in if let id = formData?.inAppId { - self?.dataFacade.trackTargeting(id: id) + self?.dataFacade.trackTargeting(id: id, tags: formData?.tags) self?.shownInappIDWithHashValue[id] = self?.getEventHashValue() } @@ -239,7 +243,7 @@ class InappMapper: InappMapperProtocol { if self.shownInappIDWithHashValue[inapp.inAppId] != self.getEventHashValue(), let inapp = self.inappFilterService.validInapps.first(where: { $0.id == inapp.inAppId }), self.targetingChecker.check(targeting: inapp.targeting) { - self.dataFacade.trackTargeting(id: inapp.id) + self.dataFacade.trackTargeting(id: inapp.id, tags: inapp.tags) } } diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/Services/InAppConfigurationDataFacade.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/Services/InAppConfigurationDataFacade.swift index e0b38be1b..f9b16cea5 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/Services/InAppConfigurationDataFacade.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/Services/InAppConfigurationDataFacade.swift @@ -16,9 +16,9 @@ protocol InAppConfigurationDataFacadeProtocol { shouldCollectFailures: Bool, _ completion: @escaping () -> Void ) - func collectTargetingFailures(forFailedTargetingInappIds failedTargetingInappIds: Set) - func downloadImage(withUrl url: String, inappId: String, completion: @escaping (Result) -> Void) - func trackTargeting(id: String?) + func collectTargetingFailures(forFailedTargetingInappIds failedTargetingInappIds: Set, tagsByInappId: [String: [String: String]]) + func downloadImage(withUrl url: String, inappId: String, tags: [String: String]?, completion: @escaping (Result) -> Void) + func trackTargeting(id: String?, tags: [String: String]?) } extension InAppConfigurationDataFacadeProtocol { @@ -70,7 +70,7 @@ class InAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { } } - func collectTargetingFailures(forFailedTargetingInappIds failedTargetingInappIds: Set) { + func collectTargetingFailures(forFailedTargetingInappIds failedTargetingInappIds: Set, tagsByInappId: [String: [String: String]]) { defer { pendingTargetingFailureDetails.removeAll() } @@ -82,12 +82,12 @@ class InAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { pendingTargetingFailureDetails.forEach { reason, details in let inappIds = inappIds(for: reason) failedTargetingInappIds.intersection(inappIds).forEach { - failureManager.addFailure(inappId: $0, reason: reason, details: details) + failureManager.addFailure(inappId: $0, reason: reason, details: details, tags: tagsByInappId[$0]) } } } - func downloadImage(withUrl url: String, inappId: String, completion: @escaping (Result) -> Void) { + func downloadImage(withUrl url: String, inappId: String, tags: [String: String]?, completion: @escaping (Result) -> Void) { imageService.downloadImage(withUrl: url) { result in if case .failure(let error) = result { switch error { @@ -96,7 +96,8 @@ class InAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { self.failureManager.addFailure( inappId: inappId, reason: .imageDownloadFailed, - details: details + details: details, + tags: tags ) default: break @@ -106,10 +107,10 @@ class InAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { } } - func trackTargeting(id: String?) { + func trackTargeting(id: String?, tags: [String: String]?) { if let id = id { do { - try self.tracker.trackTargeting(id: id) + try self.tracker.trackTargeting(id: id, tags: tags) Logger.common(message: "Track InApp.Targeting. Id \(id)", level: .info, category: .inAppMessages) } catch { Logger.common(message: "Track InApp.Targeting failed with error: \(error)", level: .error, category: .inAppMessages) diff --git a/Mindbox/InAppMessages/InAppMessagesTracker.swift b/Mindbox/InAppMessages/InAppMessagesTracker.swift index 5a930a78d..562417ddd 100644 --- a/Mindbox/InAppMessages/InAppMessagesTracker.swift +++ b/Mindbox/InAppMessages/InAppMessagesTracker.swift @@ -9,60 +9,52 @@ import Foundation protocol InappTargetingTrackProtocol: AnyObject { - func trackTargeting(id: String) throws + func trackTargeting(id: String, tags: [String: String]?) throws } protocol InAppMessagesTrackerProtocol: AnyObject { func trackView(id: String, timeToDisplay: String?, tags: [String: String]?) throws - func trackClick(id: String) throws + func trackClick(id: String, tags: [String: String]?) throws } class InAppMessagesTracker: InAppMessagesTrackerProtocol, InappTargetingTrackProtocol { - + struct InAppShowBody: Encodable { let inappId: String let timeToDisplay: String? let tags: [String: String]? - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(inappId, forKey: .inappId) - try container.encodeIfPresent(timeToDisplay, forKey: .timeToDisplay) - if let tags = tags, !tags.isEmpty { - try container.encode(tags, forKey: .tags) - } - } - - private enum CodingKeys: String, CodingKey { - case inappId, timeToDisplay, tags - } } - struct InAppBody: Codable { + struct InAppBody: Encodable { let inappId: String + let tags: [String: String]? } private let databaseRepository: DatabaseRepositoryProtocol + private let featureToggleManager: FeatureToggleManager - init(databaseRepository: DatabaseRepositoryProtocol) { + init(databaseRepository: DatabaseRepositoryProtocol, featureToggleManager: FeatureToggleManager) { self.databaseRepository = databaseRepository + self.featureToggleManager = featureToggleManager } func trackView(id: String, timeToDisplay: String?, tags: [String: String]?) throws { - let encodable = InAppShowBody(inappId: id, timeToDisplay: timeToDisplay, tags: tags) + let encodable = InAppShowBody(inappId: id, timeToDisplay: timeToDisplay, tags: featureToggleManager.gatedTags(tags)) let event = Event(type: .inAppViewEvent, body: BodyEncoder(encodable: encodable).body) try databaseRepository.create(event: event) } - func trackClick(id: String) throws { - let encodable = InAppBody(inappId: id) - let event = Event(type: .inAppClickEvent, body: BodyEncoder(encodable: encodable).body) - try databaseRepository.create(event: event) + func trackClick(id: String, tags: [String: String]?) throws { + try track(id: id, tags: tags, type: .inAppClickEvent) + } + + func trackTargeting(id: String, tags: [String: String]?) throws { + try track(id: id, tags: tags, type: .inAppTargetingEvent) } - func trackTargeting(id: String) throws { - let encodable = InAppBody(inappId: id) - let event = Event(type: .inAppTargetingEvent, body: BodyEncoder(encodable: encodable).body) + private func track(id: String, tags: [String: String]?, type: Event.Operation) throws { + let encodable = InAppBody(inappId: id, tags: featureToggleManager.gatedTags(tags)) + let event = Event(type: type, body: BodyEncoder(encodable: encodable).body) try databaseRepository.create(event: event) } } diff --git a/Mindbox/InAppMessages/InappScheduleManager.swift b/Mindbox/InAppMessages/InappScheduleManager.swift index 8f86fe7fe..f518494b0 100644 --- a/Mindbox/InAppMessages/InappScheduleManager.swift +++ b/Mindbox/InAppMessages/InappScheduleManager.swift @@ -153,7 +153,8 @@ internal extension InappScheduleManager { self.failureManager.addFailure( inappId: inapp.inAppId, reason: error.failureReason, - details: error.failureDetails + details: error.failureDetails, + tags: inapp.tags ) self.failureManager.sendFailures() } diff --git a/Mindbox/InAppMessages/InappShowFailureManager.swift b/Mindbox/InAppMessages/InappShowFailureManager.swift index e35e0c80b..be89bd655 100644 --- a/Mindbox/InAppMessages/InappShowFailureManager.swift +++ b/Mindbox/InAppMessages/InappShowFailureManager.swift @@ -10,7 +10,7 @@ import Foundation import MindboxLogger protocol InappShowFailureManagerProtocol { - func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?) + func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?, tags: [String: String]?) func clearFailures() func sendFailures() } @@ -34,12 +34,14 @@ final class InappShowFailureManager: InappShowFailureManagerProtocol { self.featureToggleManager = featureToggleManager } - func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?) { + func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?, tags: [String: String]?) { guard featureToggleManager.isFeatureEnabled(.shouldSendInAppShowError) else { Logger.common(message: "[InappShowFailureManager] addFailure ignored, feature is disabled", category: .inAppMessages) return } + let gatedTags = featureToggleManager.gatedTags(tags) + let truncatedDetails = details.map { original -> String in let truncated = original.truncated(toUTF8ByteLimit: Self.errorDetailsLimit) if truncated != original { @@ -63,13 +65,13 @@ final class InappShowFailureManager: InappShowFailureManagerProtocol { ) return } - failures[existingIndex] = makeFailure(inappId: inappId, reason: reason, details: truncatedDetails) + failures[existingIndex] = makeFailure(inappId: inappId, reason: reason, details: truncatedDetails, tags: gatedTags) Logger.common(message: "[InappShowFailureManager] Failure reason updated. inappId=\(inappId), reason=\(reason.rawValue)", category: .inAppMessages) return } - failures.append(makeFailure(inappId: inappId, reason: reason, details: truncatedDetails)) + failures.append(makeFailure(inappId: inappId, reason: reason, details: truncatedDetails, tags: gatedTags)) } } @@ -111,12 +113,13 @@ final class InappShowFailureManager: InappShowFailureManagerProtocol { } } - private func makeFailure(inappId: String, reason: InAppShowFailureReason, details: String?) -> InAppShowFailure { + private func makeFailure(inappId: String, reason: InAppShowFailureReason, details: String?, tags: [String: String]?) -> InAppShowFailure { InAppShowFailure( inappId: inappId, failureReason: reason, errorDetails: details, - dateTimeUtc: Date().toString(withFormat: .utc) + dateTimeUtc: Date().toString(withFormat: .utc), + tags: tags ) } diff --git a/Mindbox/InAppMessages/Models/Config/FeatureTogglesModel.swift b/Mindbox/InAppMessages/Models/Config/FeatureTogglesModel.swift index 9bd228360..f8e45bb3f 100644 --- a/Mindbox/InAppMessages/Models/Config/FeatureTogglesModel.swift +++ b/Mindbox/InAppMessages/Models/Config/FeatureTogglesModel.swift @@ -11,9 +11,15 @@ import Foundation extension Settings { struct FeatureToggles: Decodable, Equatable { let shouldSendInAppShowError: Bool? + let shouldSendInAppTags: Bool? + let shouldPrewarmInAppWebView: Bool? + let shouldCacheInAppWebView: Bool? enum CodingKeys: String, CodingKey { case shouldSendInAppShowError = "MobileSdkShouldSendInAppShowError" + case shouldSendInAppTags = "MobileSdkShouldSendInAppTags" + case shouldPrewarmInAppWebView = "MobileSdkShouldPrewarmInAppWebView" + case shouldCacheInAppWebView = "MobileSdkShouldCacheInAppWebView" } } } @@ -22,5 +28,8 @@ extension Settings.FeatureToggles { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.shouldSendInAppShowError = try? container.decodeIfPresent(Bool.self, forKey: .shouldSendInAppShowError) + self.shouldSendInAppTags = try? container.decodeIfPresent(Bool.self, forKey: .shouldSendInAppTags) + self.shouldPrewarmInAppWebView = try? container.decodeIfPresent(Bool.self, forKey: .shouldPrewarmInAppWebView) + self.shouldCacheInAppWebView = try? container.decodeIfPresent(Bool.self, forKey: .shouldCacheInAppWebView) } } diff --git a/Mindbox/InAppMessages/Models/InAppShowFailure.swift b/Mindbox/InAppMessages/Models/InAppShowFailure.swift index 0fae8a9ed..5fe21e3cd 100644 --- a/Mindbox/InAppMessages/Models/InAppShowFailure.swift +++ b/Mindbox/InAppMessages/Models/InAppShowFailure.swift @@ -24,4 +24,5 @@ struct InAppShowFailure: Codable { let failureReason: InAppShowFailureReason let errorDetails: String? let dateTimeUtc: String + let tags: [String: String]? } diff --git a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PresentationClickTracker.swift b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PresentationClickTracker.swift index 7a35a7b1f..bd0c1f296 100644 --- a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PresentationClickTracker.swift +++ b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PresentationClickTracker.swift @@ -20,15 +20,15 @@ class PresentationClickTracker { self.tracker = tracker } - func trackClick(id: String) { + func trackClick(id: String, tags: [String: String]?) { if SessionTemporaryStorage.shared.lastInappClickedID == id { return } - + SessionTemporaryStorage.shared.lastInappClickedID = id - + do { - try tracker.trackClick(id: id) + try tracker.trackClick(id: id, tags: tags) Logger.common(message: "Track InApp.Click. Id \(id)", level: .info, category: .notification) } catch { Logger.common(message: "Track InApp.Click failed with error: \(error)", level: .error, category: .notification) diff --git a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationDisplayUseCase/PresentationDisplayUseCase.swift b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationDisplayUseCase/PresentationDisplayUseCase.swift index eff7e9499..40ed33791 100644 --- a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationDisplayUseCase/PresentationDisplayUseCase.swift +++ b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationDisplayUseCase/PresentationDisplayUseCase.swift @@ -77,7 +77,7 @@ final class PresentationDisplayUseCase: PresentationDisplayUseCaseProtocol { wrappedTapAction = onTapAction } else { wrappedTapAction = { [weak self] url, payload in - self?.clickTracker.trackClick(id: model.inAppId) + self?.clickTracker.trackClick(id: model.inAppId, tags: model.tags) onTapAction(url, payload) } } @@ -90,7 +90,8 @@ final class PresentationDisplayUseCase: PresentationDisplayUseCaseProtocol { onTapAction: wrappedTapAction, onClose: onClose, onError: onError, - operation: model.operation) + operation: model.operation, + tags: model.tags) guard let viewController = factory.create(with: parameters) else { onError(.failed("[PresentationDisplayUseCase] Failed to create in-app view controller.")) diff --git a/Mindbox/InAppMessages/Presentation/Views/ModalView/ModalViewController.swift b/Mindbox/InAppMessages/Presentation/Views/ModalView/ModalViewController.swift index 807cb62a9..daf86c5e1 100644 --- a/Mindbox/InAppMessages/Presentation/Views/ModalView/ModalViewController.swift +++ b/Mindbox/InAppMessages/Presentation/Views/ModalView/ModalViewController.swift @@ -50,6 +50,7 @@ final class ModalViewController: UIViewController, InappViewControllerProtocol { private let onTapAction: InAppMessageTapAction private var viewWillAppearWasCalled = false + private var lastElementsLayoutSize: CGSize = .zero private enum Constants { static let defaultAlphaBackgroundColor: CGFloat = 0.2 @@ -96,14 +97,21 @@ final class ModalViewController: UIViewController, InappViewControllerProtocol { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - if let inappView = layers.first(where: { $0 is InAppImageOnlyView }) { - Logger.common(message: "In-app modal height: [\(inappView.frame.height) pt]") - Logger.common(message: "In-app modal width: [\(inappView.frame.width) pt]") + guard let inappView = layers.first(where: { $0 is InAppImageOnlyView }) else { + return } - elements.forEach({ - $0.removeFromSuperview() - }) + // Rebuild elements only when the content size actually changes. + // Doing it on every pass is not allowed: addSubview + activate(constraints) + // mark the layout dirty again → infinite viewDidLayoutSubviews loop. + let size = inappView.frame.size + guard size != lastElementsLayoutSize else { + return + } + lastElementsLayoutSize = size + + Logger.common(message: "In-app modal height: [\(inappView.frame.height) pt]") + Logger.common(message: "In-app modal width: [\(inappView.frame.width) pt]") setupElements() } @@ -141,6 +149,9 @@ final class ModalViewController: UIViewController, InappViewControllerProtocol { } private func setupElements() { + elements.forEach { $0.removeFromSuperview() } + elements.removeAll() + guard let elements = model.content.elements, let inappView = layers.first(where: { $0 is InAppImageOnlyView }) else { return diff --git a/Mindbox/InAppMessages/Presentation/Views/ViewFactory/ViewFactoryProtocol.swift b/Mindbox/InAppMessages/Presentation/Views/ViewFactory/ViewFactoryProtocol.swift index 48907b231..e36599912 100644 --- a/Mindbox/InAppMessages/Presentation/Views/ViewFactory/ViewFactoryProtocol.swift +++ b/Mindbox/InAppMessages/Presentation/Views/ViewFactory/ViewFactoryProtocol.swift @@ -19,6 +19,7 @@ struct ViewFactoryParameters { let onClose: () -> Void let onError: (InAppPresentationError) -> Void let operation: (name: String, body: String)? + let tags: [String: String]? } protocol ViewFactoryProtocol { diff --git a/Mindbox/InAppMessages/Presentation/Views/ViewFactory/WebViewFactory.swift b/Mindbox/InAppMessages/Presentation/Views/ViewFactory/WebViewFactory.swift index cb558bad0..93d866f72 100644 --- a/Mindbox/InAppMessages/Presentation/Views/ViewFactory/WebViewFactory.swift +++ b/Mindbox/InAppMessages/Presentation/Views/ViewFactory/WebViewFactory.swift @@ -20,7 +20,8 @@ class WebViewFactory: ViewFactoryProtocol { onTapAction: params.onTapAction, onCloseInApp: params.onClose, onError: params.onError, - operation: params.operation) + operation: params.operation, + tags: params.tags) myViewController = viewController return viewController } diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/BridgeMessage.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/BridgeMessage.swift index 1b57d9d57..8c224fac3 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/BridgeMessage.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/BridgeMessage.swift @@ -7,6 +7,7 @@ // import Foundation +import MindboxLogger @_spi(Internal) public enum JSONValue: Codable, Equatable { @@ -134,6 +135,40 @@ public enum JSONValue: Codable, Equatable { private var containerValue: Any { anyValue ?? NSNull() } + + /// Merges in-app tags into the root of a custom operation `body`. + /// + /// - No `tags` key (or `null`) in `body` → sets it to the tags object. + /// - `tags` already an object → adds only missing keys; existing client keys win. + /// - `tags` present but not an object (string/array/etc.) → left untouched. + static func mergingInAppTags(_ tags: [String: String]?, into body: JSONValue) -> JSONValue { + guard let tags, !tags.isEmpty else { return body } + guard case .object(var dict) = body else { return body } + + switch dict["tags"] { + case .none, .some(.null): + dict["tags"] = .object(tags.mapValues { .string($0) }) + case .some(.object(var existingTags)): + for (key, value) in tags { + if existingTags[key] != nil { + Logger.common( + message: "[WebView] Tags merge: key '\(key)' already present in operation body, keeping client value", + category: .inAppMessages + ) + continue + } + existingTags[key] = .string(value) + } + dict["tags"] = .object(existingTags) + default: + Logger.common( + message: "[WebView] Tags merge: 'tags' in operation body is not an object, keeping client value", + category: .inAppMessages + ) + } + + return .object(dict) + } } @_spi(Internal) diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/MindboxWebBridge.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/MindboxWebBridge.swift index a1bef0b74..be807357d 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/MindboxWebBridge.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/Bridge/MindboxWebBridge.swift @@ -45,19 +45,28 @@ public final class MindboxWebBridge: NSObject { private var pendingRequestIds = Set() private var contentURL: URL? + // A reused (pre-warmed) WKWebView can deliver navigation callbacks and script messages + // that belong to a previous owner's load. Callbacks are filtered by navigation identity; + // messages are gated until the show's own document commits (the document-swap point — + // whoever posts before it is not this show's page). Main-thread only. + private var expectedNavigation: WKNavigation? + private var expectedNavigationFinished = false + private var expectedNavigationCommitted = false + private var contentLoadIssued = false + init(webView: WKWebView) { self.webView = webView super.init() let controller = webView.configuration.userContentController + // Idempotent: a reused WebView may still carry a previous show's handler of this name. + controller.removeScriptMessageHandler(forName: Constants.WebViewBridgeJS.handlerName) controller.add(self, name: Constants.WebViewBridgeJS.handlerName) webView.navigationDelegate = self } - deinit { - webView?.configuration.userContentController.removeScriptMessageHandler(forName: Constants.WebViewBridgeJS.handlerName) - webView?.navigationDelegate = nil - } + // No deinit teardown needed: `navigationDelegate` is weak (zeroes automatically), and + // the successor's init does remove-then-add on the script handler. func send(_ message: BridgeMessage) { guard let json = message.jsonString() else { @@ -132,6 +141,32 @@ public final class MindboxWebBridge: NSObject { func updateContentURL(_ url: URL?) { contentURL = url } + + /// Registers the navigation issued by this show's own load (loadHTMLString/reload). + /// Until it finishes, callbacks for any other navigation are treated as stale leftovers. + func expectContentNavigation(_ navigation: WKNavigation?) { + contentLoadIssued = true + expectedNavigation = navigation + expectedNavigationFinished = false + expectedNavigationCommitted = false + } + + private func isStaleNavigation(_ navigation: WKNavigation?) -> Bool { + if expectedNavigationFinished { return false } + guard contentLoadIssued else { return true } + // WebKit occasionally delivers a nil navigation (early provisional failures): it + // can't be proven to be a leftover — fail open, like the nil-expected case below. + guard let navigation else { return false } + guard let expected = expectedNavigation else { return false } + return navigation !== expected + } + + private func logStaleNavigation(_ event: String) { + Logger.common( + message: "[WebView] Bridge: ignoring stale navigation \(event) (leftover load on reused WebView)", + category: .webViewInAppMessages + ) + } } extension MindboxWebBridge: WKScriptMessageHandler { @@ -145,6 +180,16 @@ extension MindboxWebBridge: WKScriptMessageHandler { return } + // Mirror of the navigation staleness filter at message level: until the show's own + // document has committed, whoever is posting is not this show's page — drop it. + guard expectedNavigationCommitted else { + Logger.common( + message: "[WebView] Bridge: ignoring JS message before the show's document committed (leftover page on reused WebView)", + category: .webViewInAppMessages + ) + return + } + guard let bridgeMessage = BridgeMessage.from(body: message.body) else { Logger.common( message: "[WebView] Bridge: failed to parse message from JS. Body: \(String(describing: message.body))", @@ -183,22 +228,53 @@ extension MindboxWebBridge: WKScriptMessageHandler { extension MindboxWebBridge: WKNavigationDelegate { public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + guard !isStaleNavigation(navigation) else { + logStaleNavigation("start") + return + } navigationDelegate?.webBridge(self, didStartProvisionalNavigation: webView.url) } + public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + guard !isStaleNavigation(navigation) else { + logStaleNavigation("commit") + return + } + // The old document is gone from this point: script messages are now this show's. + expectedNavigationCommitted = true + } + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - navigationDelegate?.webBridge(self, didFinishNavigation: contentURL ?? webView.url) + guard !isStaleNavigation(navigation) else { + logStaleNavigation("finish") + return + } + // Only the show's own load reports the content URL; a page-initiated navigation + // reports its real URL so upstream can tell the documents apart. + let isExpected = navigation === expectedNavigation + if isExpected { + expectedNavigationFinished = true + } + navigationDelegate?.webBridge(self, didFinishNavigation: isExpected ? (contentURL ?? webView.url) : webView.url) } public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + guard !isStaleNavigation(navigation) else { + logStaleNavigation("provisional failure: \(error.localizedDescription)") + return + } navigationDelegate?.webBridge(self, didFailProvisionalNavigation: webView.url, error: error) } public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + guard !isStaleNavigation(navigation) else { + logStaleNavigation("failure: \(error.localizedDescription)") + return + } navigationDelegate?.webBridge(self, didFailProvisionalNavigation: webView.url, error: error) } diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/Debug/MindboxWebViewFacade.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/Debug/MindboxWebViewFacade.swift index 3da7bf9f2..2df623328 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/Debug/MindboxWebViewFacade.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/Debug/MindboxWebViewFacade.swift @@ -8,6 +8,7 @@ import UIKit import WebKit +import MindboxLogger private enum PayloadKey { static let sdkVersion = "sdkVersion" @@ -80,24 +81,32 @@ public final class MindboxWebViewFacade: MindboxInternalWebViewFacadeProtocol { private let log: WebViewLog private let logError: WebViewLogError + // Main-confined. A borrowed prewarmed `webView` outlives this facade (the prewarm + // service keeps it warm across shows), so a `[weak webView]` guard is not enough to + // stop a slow fetch that completes after the show closed — it would load the dead + // show's page into the parked hidden instance. `isClosed` gates the load and the + // in-flight fetch is cancelled outright on teardown. + private var fetchTask: URLSessionDataTask? + private var isClosed = false + public init(params: [String: JSONValue]?, operation: (name: String, body: String)? = nil, userAgent: String, inAppId: String = "", log: @escaping WebViewLog = { _ in }, logError: @escaping WebViewLogError = { _ in }) { - let config = WKWebViewConfiguration() - config.websiteDataStore = .nonPersistent() - config.applicationNameForUserAgent = userAgent - config.allowsInlineMediaPlayback = true - config.mediaTypesRequiringUserActionForPlayback = [] - - let webView = WKWebView(frame: .zero, configuration: config) - #if DEBUG - if #available(iOS 16.4, *) { - webView.isInspectable = true + // Borrow the prewarmed live instance when available (kept across shows — hidden, + // not destroyed); otherwise create one on the same shared data store so cached + // resources stay visible either way. A warm instance can only serve a caller that + // wants the stock UA: its applicationNameForUserAgent was baked at prewarm + // creation and cannot change on a live WKWebView. + let webView: WKWebView + if userAgent == SDKUserAgent.build(), + let warm = DI.injectOrFail(InAppWebViewPrewarmServiceProtocol.self).borrowWarmWebView() { + webView = warm + } else { + webView = InAppWebViewFactory.make(userAgent: userAgent) } - #endif let bridge = MindboxWebBridge(webView: webView) self.webView = webView @@ -109,6 +118,12 @@ public final class MindboxWebViewFacade: MindboxInternalWebViewFacadeProtocol { self.logError = logError } + deinit { + // The fetch completion holds only a weak self, so a pending request can outlive + // the facade; cancel it so it can never resume work against a reused webView. + fetchTask?.cancel() + } + public func makeView() -> UIView { webView } @@ -120,36 +135,35 @@ public final class MindboxWebViewFacade: MindboxInternalWebViewFacadeProtocol { let contentURL = URL(string: contentUrl) bridge.updateContentURL(contentURL) - fetchHTML(from: contentUrl) { [weak webView] html in - guard let webView else { - DispatchQueue.main.async { - onFailure() - } - return - } - - if let html { - DispatchQueue.main.async { - webView.loadHTMLString(html, baseURL: url) - } - } else { - DispatchQueue.main.async { + fetchHTML(from: contentUrl) { [weak self] html in + DispatchQueue.main.async { + // A show that closed while the fetch was in flight must not load into the + // (possibly reused) webView, and must not re-report failure — it is already + // tearing down. + guard let self, !self.isClosed else { return } + guard let html else { onFailure() + return } + self.bridge.expectContentNavigation(self.webView.loadHTMLString(html, baseURL: url)) } } } - + public func reloadWebView() { - DispatchQueue.main.async { [weak webView] in - webView?.reload() + DispatchQueue.main.async { [weak self] in + guard let self, !self.isClosed else { return } + self.bridge.expectContentNavigation(self.webView.reload()) } } - + public func cleanWebView() { - DispatchQueue.main.async { [weak webView] in - guard let webView else { return } - webView.stopLoading() + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.isClosed = true + self.fetchTask?.cancel() + self.fetchTask = nil + self.webView.stopLoading() } } @@ -238,24 +252,21 @@ extension MindboxWebViewFacade { ] if let firstInitDate = persistenceStorage.firstInitializationDateTime { - params[PayloadKey.firstInitializationDateTime] = firstInitDate.iso8601 + params[PayloadKey.firstInitializationDateTime] = firstInitDate.toString(withFormat: .utc) } return params } - // Add operation data private func addOperationParams(to params: inout [String: Any]) { guard let operation else { return } params[PayloadKey.operationName] = operation.name params[PayloadKey.operationBody] = operation.body } - // Add system info (theme, platform, locale, version) private func addSystemInfo(to params: inout [String: Any], systemInfoProvider: SystemInfoProvider) { params.merge(systemInfoProvider.getBasicSystemInfo()) { _, new in new } - // Add safe area insets let insets = systemInfoProvider.getSafeAreaInsets(from: webView) params[PayloadKey.Insets.key] = [ PayloadKey.Insets.top: insets.top, @@ -264,14 +275,12 @@ extension MindboxWebViewFacade { PayloadKey.Insets.right: insets.right ] - // Add granted permissions let permissions = systemInfoProvider.getGrantedPermissions() if !permissions.isEmpty { params[PayloadKey.permissions] = permissions.mapValues { $0.toDictionary() } } } - // Merge params from configuration private func mergeCustomParams(into params: inout [String: Any]) { guard let customParams = self.params, !customParams.isEmpty else { return } for (key, value) in customParams { @@ -279,7 +288,6 @@ extension MindboxWebViewFacade { } } - // Add last track-visit data private func addTrackVisitParams(to params: inout [String: Any]) { guard let lastTrackVisit = SessionTemporaryStorage.shared.lastTrackVisit else { return } if let source = lastTrackVisit.source { @@ -290,7 +298,6 @@ extension MindboxWebViewFacade { } } - // Serialize to JSON string private func serializeToJSONString(_ params: [String: Any]) -> JSONValue { do { let data = try JSONSerialization.data(withJSONObject: params, options: []) @@ -311,16 +318,12 @@ extension MindboxWebViewFacade { completion(nil) return } - - let config = URLSessionConfiguration.ephemeral - config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData - config.urlCache = nil - - let session = URLSession(configuration: config) - + + let (session, request) = InAppWebViewHTMLFetcher.sessionAndRequest(for: url) + log("Fetching HTML from \(url.absoluteString)") - - let task = session.dataTask(with: url) { [weak self] data, response, error in + + let task = session.dataTask(with: request) { [weak self] data, response, error in if let error { self?.logError("Error fetching HTML: \(error.localizedDescription)") completion(nil) @@ -345,6 +348,9 @@ extension MindboxWebViewFacade { completion(htmlString) } + // Retained so teardown can cancel an in-flight request. Main-confined: `loadHTML` + // (the sole caller) runs on the main thread. + fetchTask = task task.resume() } } diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewDataStore.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewDataStore.swift new file mode 100644 index 000000000..7fa6df754 --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewDataStore.swift @@ -0,0 +1,63 @@ +// +// InAppWebViewDataStore.swift +// Mindbox +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import WebKit + +/// The single `WKWebsiteDataStore` used by every Mindbox WebView (prewarm and shows alike). +/// +/// Persistence is what lets WebKit's header-driven HTTP cache survive relaunches. The disk +/// cache is keyed per data store, so the prewarm and the shows MUST share this instance for +/// prewarmed entries to be visible to shows. +/// +/// On iOS 17+ the store is isolated from the host app's `.default()` store so Mindbox web +/// content never shares cookies/storage with the host's own WebViews. Earlier systems fall +/// back to `.default()` — a deliberate product decision: some disk cache beats none, and +/// sharing the host's store is accepted there. The cache kill switch still restores the +/// pre-feature `.nonPersistent()` isolation on every OS. +enum InAppWebViewDataStore { + // Stable identifier for the isolated store; changing it orphans the previous store. + // UUIDv5 (SHA-1, RFC 4122 DNS namespace) of "cloud.Mindbox.InAppWebViewDataStore": + // python3 -c 'import uuid; print(uuid.uuid5(uuid.NAMESPACE_DNS, "cloud.Mindbox.InAppWebViewDataStore"))' + // swiftlint:disable:next force_unwrapping + private static let identifier = UUID(uuidString: "9E350642-BB9F-5D4C-9981-94FFD93C2B57")! + + /// The cache half of the WebView feature toggles. Latched at first WebView creation for + /// the rest of the launch: flipping stores mid-session would split the cache between + /// partitions. Read from the config cache, which holds the freshest config the SDK has + /// seen — a freshly downloaded config is saved there before any show or stage-2 prewarm + /// can run. + static let isCacheFeatureEnabled: Bool = + isCacheEnabled(in: InAppConfigurationRepository().fetchDecodedConfigFromCache()) + + /// Absent key, absent section, absent/unreadable config all mean "enabled" — the toggle + /// is a kill switch, not an opt-in. + static func isCacheEnabled(in config: ConfigResponse?) -> Bool { + config?.settings?.featureToggles?.shouldCacheInAppWebView + ?? FeatureFlag.shouldCacheInAppWebView.defaultValue + } + + // One live store object per process: a single instance also guarantees prewarm and + // shows share the same in-memory session (connection pool, memory cache). + private static let instance: WKWebsiteDataStore = { + if #available(iOS 17.0, *) { + return WKWebsiteDataStore(forIdentifier: identifier) + } else { + // Product decision: no isolated named stores before iOS 17, and some disk + // cache beats none — Mindbox web content shares the host app's default store + // (cookies/storage included) on old systems. + return WKWebsiteDataStore.default() + } + }() + + static func shared() -> WKWebsiteDataStore { + // Toggle off restores the pre-feature behavior exactly: every WebView gets its + // own fresh in-memory store, nothing is shared and nothing persists. + guard isCacheFeatureEnabled else { return WKWebsiteDataStore.nonPersistent() } + return instance + } +} diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewFactory.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewFactory.swift new file mode 100644 index 000000000..07618c789 --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewFactory.swift @@ -0,0 +1,30 @@ +// +// InAppWebViewFactory.swift +// Mindbox +// +// Created by Sergei Semko on 07.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import WebKit + +/// The single place a Mindbox in-app WKWebView is configured. A borrowed prewarmed +/// instance and a cold-created one must be indistinguishable — a knob added here +/// reaches both. +enum InAppWebViewFactory { + + static func make(userAgent: String = SDKUserAgent.build()) -> WKWebView { + let config = WKWebViewConfiguration() + config.websiteDataStore = InAppWebViewDataStore.shared() + config.applicationNameForUserAgent = userAgent + config.allowsInlineMediaPlayback = true + config.mediaTypesRequiringUserActionForPlayback = [] + let webView = WKWebView(frame: .zero, configuration: config) + #if DEBUG + if #available(iOS 16.4, *) { + webView.isInspectable = true + } + #endif + return webView + } +} diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewHTMLFetcher.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewHTMLFetcher.swift new file mode 100644 index 000000000..f54e06abe --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/InAppWebViewHTMLFetcher.swift @@ -0,0 +1,53 @@ +// +// InAppWebViewHTMLFetcher.swift +// Mindbox +// +// Created by Sergei Semko on 07.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +/// The single home of the in-app HTML fetch transport: the prewarm and the shows MUST use +/// identical cache semantics, or the prewarm fills a cache the shows never read. +enum InAppWebViewHTMLFetcher { + + /// Caching path: revalidates with the server on every fetch (ETag match → a ~0-byte 304 + /// and the cache supplies the stored body). A dedicated cookie-less session keeps the + /// SDK out of the host app's shared cookie jar and URLCache — the pre-feature fetch + /// carried no cookies either. + private static let cachingSession: URLSession = { + let config = URLSessionConfiguration.default + config.httpShouldSetCookies = false + config.httpCookieAcceptPolicy = .never + config.httpCookieStorage = nil + if #available(iOS 13.0, *) { + // Holds ONLY the in-app index HTML (a few small entries per endpoint) — page + // resources live in WebKit's own cache. Sized accordingly. + let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first? + .appendingPathComponent("cloud.Mindbox.InAppWebViewHTML", isDirectory: true) + config.urlCache = URLCache(memoryCapacity: 512 * 1024, + diskCapacity: 4 * 1024 * 1024, + directory: directory) + } + return URLSession(configuration: config) + }() + + /// Cache-less path — the exact pre-feature transport. + private static let ephemeralSession: URLSession = { + let config = URLSessionConfiguration.ephemeral + config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData + config.urlCache = nil + return URLSession(configuration: config) + }() + + static func sessionAndRequest(for url: URL) -> (session: URLSession, request: URLRequest) { + // Foundation implements .reloadRevalidatingCacheData starting with iOS 13; on + // iOS 12 it silently degrades to the protocol policy and could serve a stale + // index without a server round-trip — old systems keep the cache-less fetch. + guard #available(iOS 13.0, *), InAppWebViewDataStore.isCacheFeatureEnabled else { + return (ephemeralSession, URLRequest(url: url)) + } + return (cachingSession, URLRequest(url: url, cachePolicy: .reloadRevalidatingCacheData)) + } +} diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewLearnedHostsStore.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewLearnedHostsStore.swift new file mode 100644 index 000000000..aae01e820 --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewLearnedHostsStore.swift @@ -0,0 +1,52 @@ +// +// InAppWebViewLearnedHostsStore.swift +// Mindbox +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +/// Persists resource hosts observed during real shows, per endpoint. These are the hosts +/// the config cannot know (image CDNs, the web runtime's static hosts, fonts) and they +/// let the next launch's preconnect cover the heavy part of the page. A stale entry is +/// harmless — preconnect to an unused host is a no-op. +final class InAppWebViewLearnedHostsStore { + static let maxHosts = 12 + + private let persistenceStorage: PersistenceStorage + + init(persistenceStorage: PersistenceStorage) { + self.persistenceStorage = persistenceStorage + } + + func hosts(endpoint: String) -> [String] { + persistenceStorage.webViewLearnedHosts?[endpoint] ?? [] + } + + /// Appends newly observed hosts, keeping insertion order and dropping the oldest + /// beyond the cap. Values come from page JS — only bare https hosts are accepted. + /// + /// Main-thread only: this is an unsynchronized read-modify-write, safe because the + /// single caller (the show's evaluateJavaScript completion) always runs on main. + func remember(_ observed: [String], endpoint: String) { + guard !observed.isEmpty else { return } + var all = persistenceStorage.webViewLearnedHosts ?? [:] + var merged = all[endpoint] ?? [] + var didAppend = false + for host in observed where InAppWebViewPrewarmPlanner.isValidHost(host) && !merged.contains(host) { + merged.append(host) + didAppend = true + } + // Steady state after the first shows: every observed host is already known. Skip + // the app-group UserDefaults rewrite (a synchronous cfprefsd round-trip on the + // main thread) when nothing was added. + guard didAppend else { return } + if merged.count > Self.maxHosts { + merged.removeFirst(merged.count - Self.maxHosts) + } + all[endpoint] = merged + persistenceStorage.webViewLearnedHosts = all + } +} diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmNavigationPolicy.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmNavigationPolicy.swift new file mode 100644 index 000000000..c8c6a4de7 --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmNavigationPolicy.swift @@ -0,0 +1,44 @@ +// +// InAppWebViewPrewarmNavigationPolicy.swift +// Mindbox +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import WebKit +import MindboxLogger + +/// Pins the hidden prewarm instance to the documents the SDK itself loads: any +/// page-initiated main-frame navigation (JS redirect, meta refresh) is cancelled — a +/// hidden WebView must never wander off and keep fetching arbitrary URLs. Active only +/// until the first borrow; the show's bridge installs its own delegate then. +final class InAppWebViewPrewarmNavigationPolicy: NSObject, WKNavigationDelegate { + + // Main-thread confined, like every WKWebView interaction around it. + private var allowedURLs: Set = ["about:blank"] + + /// Registers a document URL the service is about to load itself. + func allow(_ url: URL) { + allowedURLs.insert(url.absoluteString) + } + + /// Pure decision core (WKNavigationAction cannot be faked in tests). Subframes are + /// the page's own business; a nil target frame (window.open) is pinned like the top frame. + func decision(for url: URL?, targetIsMainFrame: Bool?) -> WKNavigationActionPolicy { + if targetIsMainFrame == false { return .allow } + return allowedURLs.contains(url?.absoluteString ?? "about:blank") ? .allow : .cancel + } + + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + let verdict = decision(for: navigationAction.request.url, + targetIsMainFrame: navigationAction.targetFrame?.isMainFrame) + if verdict == .cancel { + Logger.common(message: "[WebView] Prewarm: blocked page-initiated navigation to \(navigationAction.request.url?.absoluteString ?? "nil")", + level: .info, category: .webViewInAppMessages) + } + decisionHandler(verdict) + } +} diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmPlanner.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmPlanner.swift new file mode 100644 index 000000000..a89d67745 --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmPlanner.swift @@ -0,0 +1,106 @@ +// +// InAppWebViewPrewarmPlanner.swift +// Mindbox +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +/// Derives everything the webview prewarm needs from the in-app config. Pure functions. +enum InAppWebViewPrewarmPlanner { + + /// WebKit partitions its HTTP cache by the top-level document's host, which for + /// `loadHTMLString` is the `baseURL` host — so the prewarm MUST use the same baseUrl + /// the shows use, and both URLs MUST come from the same layer (mixing layers would + /// warm one layer's content under another layer's partition, invisible to its show). + static func prewarmSource(for layers: [WebviewContentBackgroundLayerDTO]) -> (baseURL: URL, contentURL: URL)? { + for layer in layers { + guard let baseURL = layer.baseUrl.flatMap(URL.init(string:)), + baseURL.scheme?.lowercased() == "https", baseURL.host != nil, + let contentURL = layer.contentUrl.flatMap(URL.init(string:)), + contentURL.scheme?.lowercased() == "https" else { continue } + return (baseURL, contentURL) + } + return nil + } + + /// Hosts worth opening DNS+TCP+TLS to before the first show: every webview layer's + /// `contentUrl` host, the configured API domain, plus hosts learned from previous shows. + static func preconnectHosts(layers: [WebviewContentBackgroundLayerDTO], + apiDomain: String?, + learnedHosts: [String]) -> [String] { + var hosts = Set(layers.compactMap { layer -> String? in + guard let contentUrl = layer.contentUrl, let host = URL(string: contentUrl)?.host else { return nil } + return host + }) + if let apiDomain, !apiDomain.isEmpty { + hosts.insert(apiDomain) + } + hosts.formUnion(learnedHosts) + return hosts.sorted() + } + + /// A page of `` hints: WebKit opens pooled connections to each + /// host without downloading anything; the show reuses the pool. Every host is + /// re-validated at this single choke point — hosts get interpolated into markup. + static func preconnectHTML(hosts: [String]) -> String { + let links = hosts + .filter(isValidHost) + .map { "" } + .joined() + return "\(links)" + } + + /// Accepts only strings that pass the SDK-wide RFC 1123 host validation, so no source + /// (page JS, config, API domain) can ever turn a host slot into markup. An explicit + /// charset, unlike a `URL(string:)` round-trip, doesn't ride on Foundation's parser + /// semantics and rejects sub-delims (`&`, `'`, `;`, …) that survive a URL host slot. + static func isValidHost(_ host: String) -> Bool { + URLValidator.isValidHost(host) + } + + /// The official prewarm contract with the web runtime: the prewarm content page is + /// loaded with these parameters on its document URL (`loadHTMLString` baseURL → + /// `location.search`), and a runtime that knows the contract boots tracker-only — + /// no `ready` handshake, no form, byendpoint straight into the HTTP cache. Older + /// runtimes ignore the parameters (plain page warm). Shows never get these parameters. + static func prewarmContentBaseURL(from baseURL: URL, endpoint: String, deviceUUID: String) -> URL { + guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { return baseURL } + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "prewarm", value: "1")) + queryItems.append(URLQueryItem(name: "endpointId", value: endpoint)) + queryItems.append(URLQueryItem(name: "deviceUuid", value: deviceUUID)) + components.queryItems = queryItems + return components.url ?? baseURL + } + + static func webviewLayers(in config: ConfigResponse) -> [WebviewContentBackgroundLayerDTO] { + var result: [WebviewContentBackgroundLayerDTO] = [] + for inapp in config.inapps?.elements ?? [] { + for variant in inapp.form.variants ?? [] { + let layers: [ContentBackgroundLayerDTO]? + switch variant { + case .modal(let modal): layers = modal.content?.background?.layers + case .snackbar(let snackbar): layers = snackbar.content?.background?.layers + case .unknown: layers = nil + } + for layer in layers ?? [] { + if case .webview(let webview) = layer { + result.append(webview) + } + } + } + } + return result + } + + /// JS evaluated in a shown WebView to collect the distinct https hosts its resources + /// actually came from. Host names only — no URLs, no payloads. + static let observedHostsScript = """ + Array.from(new Set(performance.getEntriesByType('resource').map(function (r) { + try { var u = new URL(r.name); return u.protocol === 'https:' ? u.host : null } catch (e) { return null } + }).filter(Boolean))) + """ +} diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmService.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmService.swift new file mode 100644 index 000000000..22267346b --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/Prewarm/InAppWebViewPrewarmService.swift @@ -0,0 +1,307 @@ +// +// InAppWebViewPrewarmService.swift +// Mindbox +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import UIKit +import WebKit +import MindboxLogger + +protocol InAppWebViewPrewarmServiceProtocol: AnyObject { + /// Stage 1, at SDK initialization: when the previous launch left a cached config with + /// webview in-apps, creates the warm instance and starts the resource prewarm ahead of + /// the fresh config download. Hosts without webview in-apps never pay for a + /// web-content process — nothing is created until a config proves it's needed. + func prewarmProcess() + + /// Stage 2, when a freshly downloaded config has been parsed: starts the resource + /// prewarm if it hasn't run from the cached config yet, or releases the stage-1 + /// instance when the config has no webview in-apps. + func prewarmResources(for config: ConfigResponse) + + /// Hands the warm instance to a show (borrow, not consume — the same live instance + /// serves subsequent shows). Any prewarm page is torn down first. + func borrowWarmWebView() -> WKWebView? + + /// Called when a show is torn down: navigates the parked instance to a blank page so + /// the closed in-app's JS stops running hidden, keeping the process warm for the next show. + func parkWarmWebView() + + /// Called by a show when its page is done: persists the observed resource hosts for + /// the next launch's preconnect. + func rememberObservedHosts(_ hosts: [String]) +} + +final class InAppWebViewPrewarmService: InAppWebViewPrewarmServiceProtocol { + + private let persistenceStorage: PersistenceStorage + private let learnedHostsStore: InAppWebViewLearnedHostsStore + private let makeWebView: () -> WKWebView + private let fetchHTML: (URL, @escaping (String?) -> Void) -> Void + private let loadCachedConfig: () -> ConfigResponse? + + // Main-thread confined (WKWebView requirement); all mutations hop to main. The flags + // latch for the rest of the launch by design: after the first borrow the cache is + // warmer than any prewarm could make it, and re-running the prewarm would navigate a + // live show's webview. + private var warmWebView: WKWebView? + private var hasBeenBorrowed = false + // Write-once, decided on the main hop: set when the resource prewarm starts OR when a + // config-driven release fires. The release must latch too — one that lands on main + // before the slow stage-1 hop has nothing to drop yet, and without the latch the stale + // stage-1 block would then create the instance a fresh config already said to kill. + private var resourcePrewarmLatched = false + // True between borrow and park: `superview` alone can't tell (there is a window + // between borrow and addSubview). + private var isLentToShow = false + private var memoryWarningObserver: NSObjectProtocol? + // Retained here because navigationDelegate is weak; armed until the first borrow. + private let prewarmNavigationPolicy = InAppWebViewPrewarmNavigationPolicy() + + init(persistenceStorage: PersistenceStorage, + makeWebView: @escaping () -> WKWebView = { InAppWebViewFactory.make() }, + fetchHTML: @escaping (URL, @escaping (String?) -> Void) -> Void = InAppWebViewPrewarmService.defaultFetchHTML, + loadCachedConfig: @escaping () -> ConfigResponse? = InAppWebViewPrewarmService.defaultLoadCachedConfig) { + self.persistenceStorage = persistenceStorage + self.learnedHostsStore = InAppWebViewLearnedHostsStore(persistenceStorage: persistenceStorage) + self.makeWebView = makeWebView + self.fetchHTML = fetchHTML + self.loadCachedConfig = loadCachedConfig + startMemoryWarningObserver() + } + + deinit { + // Block-based observers are not auto-removed. The token is optional solely + // because its block captures self and can't be created during two-phase init. + if let memoryWarningObserver { + NotificationCenter.default.removeObserver(memoryWarningObserver) + } + } + + /// Frees the parked warm instance under memory pressure: the HTTP cache is disk-level + /// and survives, so the next show only re-pays the web-content process spin-up. An + /// instance lent to a live show is left alone — the show owns it. + private func startMemoryWarningObserver() { + memoryWarningObserver = NotificationCenter.default.addObserver( + forName: UIApplication.didReceiveMemoryWarningNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self, let webView = self.warmWebView, !self.isLentToShow else { return } + webView.stopLoading() + self.warmWebView = nil + Logger.common(message: "[WebView] Prewarm: memory warning — releasing the parked warm instance", + level: .info, category: .webViewInAppMessages) + } + } + + func prewarmProcess() { + // Boots from the PREVIOUS launch's cached config: waiting for the fresh one would + // lose the race to a launch-triggered show. A stale URL is harmless — the next + // launch picks up the fresh config. + DispatchQueue.global(qos: .utility).async { [weak self] in + // Warm the cache-toggle latch here: its initializer reads and decodes the + // cached config, and every other first-access path runs on the main thread. + _ = InAppWebViewDataStore.isCacheFeatureEnabled + guard let self, let config = self.loadCachedConfig() else { return } + guard Self.isPrewarmEnabled(in: config) else { return } + let layers = InAppWebViewPrewarmPlanner.webviewLayers(in: config) + guard !layers.isEmpty else { return } + self.startResourcePrewarm(layers: layers) + } + } + + func prewarmResources(for config: ConfigResponse) { + // A fresh config that turns the toggle off also kills a stage-1 instance started + // under the previous launch's config. + guard Self.isPrewarmEnabled(in: config) else { + releaseUnusedWarmWebView(reason: .featureToggleOff) + return + } + let layers = InAppWebViewPrewarmPlanner.webviewLayers(in: config) + guard !layers.isEmpty else { + releaseUnusedWarmWebView(reason: .noWebviewInApps) + return + } + startResourcePrewarm(layers: layers) + } + + /// The prewarm half of the WebView feature toggles, read from the config each stage + /// works with: stage 1 sees the previous launch's cached toggles, stage 2 the fresh + /// ones. An absent key or section means "enabled" — a kill switch, not an opt-in. + private static func isPrewarmEnabled(in config: ConfigResponse) -> Bool { + config.settings?.featureToggles?.shouldPrewarmInAppWebView + ?? FeatureFlag.shouldPrewarmInAppWebView.defaultValue + } + + func borrowWarmWebView() -> WKWebView? { + // Never trap in a host app: an off-main caller just doesn't get the warm + // instance — returning nil is always correct (the show creates its own WebView + // on the shared store) and touches none of the main-confined state. + guard Thread.isMainThread else { + Logger.common(message: "[WebView] Prewarm: borrowWarmWebView() called off the main thread — ignoring", + level: .error, category: .webViewInAppMessages) + return nil + } + // Latch even when there is nothing to hand out: a show is starting, and a + // resource prewarm kicked off after this point would compete with it. + hasBeenBorrowed = true + guard let webView = warmWebView else { return nil } + // Presentation is serialized upstream, but that flag has known races — never let + // a second show steal the instance out of an on-screen in-app. + guard !isLentToShow else { return nil } + // Never hand out an instance mid-navigation: the show's load would land on a + // half-committed document and its didFinish can fire before the page's module + // scripts evaluate. Park it instead; the show creates a fresh WKWebView on the + // same shared store (pays process spin-up, keeps the HTTP cache). + guard !webView.isLoading else { + webView.stopLoading() + webView.loadHTMLString(Self.blankPage, baseURL: nil) + Logger.common(message: "[WebView] Prewarm: instance is mid-navigation at borrow — parking it, the show gets a fresh WebView", + level: .info, category: .webViewInAppMessages) + return nil + } + // The instance belongs to the show from here; the blank load tears down the + // prewarm page so its JS can't compete with the show for bandwidth. The bridge's + // staleness filter ignores both leftovers' callbacks. + webView.stopLoading() + webView.loadHTMLString(Self.blankPage, baseURL: nil) + webView.removeFromSuperview() + isLentToShow = true + return webView + } + + func parkWarmWebView() { + DispatchQueue.main.async { + guard let webView = self.warmWebView else { + self.isLentToShow = false + return + } + // Only park an instance no live show is presenting (a newer show may have + // borrowed it before this teardown arrived). + guard webView.superview == nil else { return } + self.isLentToShow = false + // Fully detach the finished show: WKUserContentController retains its script + // handlers, so the previous show's bridge would otherwise stay on the + // parked instance until the next show replaces it. + if #available(iOS 14.0, *) { + webView.configuration.userContentController.removeAllScriptMessageHandlers() + } else { + webView.configuration.userContentController.removeScriptMessageHandler(forName: Constants.WebViewBridgeJS.handlerName) + } + webView.loadHTMLString(Self.blankPage, baseURL: nil) + } + } + + func rememberObservedHosts(_ hosts: [String]) { + guard let configuration = persistenceStorage.configuration else { return } + learnedHostsStore.remember(hosts, endpoint: configuration.endpoint) + } + + // MARK: Resource prewarm + + /// Two loads on the warm instance, both under the shows' cache partition (`baseUrl` + /// from the config's webview layer): a preconnect page (DNS+TCP+TLS to every known + /// host), then the real content page with the official prewarm parameters on its + /// baseURL — a runtime that knows the contract downloads its bundles straight into + /// the HTTP cache; an older runtime degrades to a plain page warm. + private func startResourcePrewarm(layers: [WebviewContentBackgroundLayerDTO]) { + guard let configuration = persistenceStorage.configuration, + let (baseURL, contentURL) = InAppWebViewPrewarmPlanner.prewarmSource(for: layers) else { return } + + let hosts = InAppWebViewPrewarmPlanner.preconnectHosts( + layers: layers, + apiDomain: configuration.domain, + learnedHosts: learnedHostsStore.hosts(endpoint: configuration.endpoint) + ) + let endpoint = configuration.endpoint + let deviceUUID = persistenceStorage.deviceUUID ?? "" + + DispatchQueue.main.async { + guard !self.hasBeenBorrowed, !self.resourcePrewarmLatched else { return } + self.resourcePrewarmLatched = true + if self.warmWebView == nil { + self.warmWebView = self.makeWebView() + self.warmWebView?.navigationDelegate = self.prewarmNavigationPolicy + } + if !hosts.isEmpty { + self.prewarmNavigationPolicy.allow(baseURL) + self.warmWebView?.loadHTMLString(InAppWebViewPrewarmPlanner.preconnectHTML(hosts: hosts), baseURL: baseURL) + Logger.common(message: "[WebView] Prewarm: preconnect to \(hosts.joined(separator: ",")) under \(baseURL.absoluteString)", + level: .info, category: .webViewInAppMessages) + } + self.fetchHTML(contentURL) { html in + DispatchQueue.main.async { [weak self] in + self?.loadPrewarmContentPage(html: html, baseURL: baseURL, endpoint: endpoint, deviceUUID: deviceUUID) + } + } + } + } + + private func loadPrewarmContentPage(html: String?, baseURL: URL, endpoint: String, deviceUUID: String) { + guard let html else { + // No retry by design (the latch stands): this launch degrades to preconnect-only. + Logger.common(message: "[WebView] Prewarm: content page fetch failed, launch degrades to preconnect-only", + level: .info, category: .webViewInAppMessages) + return + } + // The instance can be gone by now — borrowed by a show, or dropped by a + // config-driven release that landed while the fetch was in flight. Either way there + // is nothing to warm, and logging success would misreport it. + guard !hasBeenBorrowed, let warmWebView else { return } + let prewarmBaseURL = InAppWebViewPrewarmPlanner.prewarmContentBaseURL( + from: baseURL, endpoint: endpoint, deviceUUID: deviceUUID + ) + prewarmNavigationPolicy.allow(prewarmBaseURL) + warmWebView.loadHTMLString(html, baseURL: prewarmBaseURL) + Logger.common(message: "[WebView] Prewarm: content page under \(prewarmBaseURL.absoluteString), endpoint \(endpoint)", + level: .info, category: .webViewInAppMessages) + } + + private enum ReleaseReason: String { + case featureToggleOff = "the prewarm feature toggle is off" + case noWebviewInApps = "no webview in-apps in config" + } + + /// Most host apps have no webview in-apps: once the config proves that, drop the + /// stage-1 instance so they don't keep an idle web-content process. The prewarm does + /// not restart within this launch — a show then simply creates its own WebView. + private func releaseUnusedWarmWebView(reason: ReleaseReason) { + DispatchQueue.main.async { + // Latch before the guard: even with nothing to drop yet, a stage-1 hop that + // lands after this release must not start a prewarm the config just killed. + self.resourcePrewarmLatched = true + guard !self.hasBeenBorrowed, let webView = self.warmWebView else { return } + webView.stopLoading() + self.warmWebView = nil + Logger.common(message: "[WebView] Prewarm: \(reason.rawValue) — releasing the warm instance", + level: .info, category: .webViewInAppMessages) + } + } + + // The empty-page skeleton is exactly a preconnect page with no hosts — reuse the + // planner so the parked/teardown markup can never drift from the preconnect markup. + private static let blankPage = InAppWebViewPrewarmPlanner.preconnectHTML(hosts: []) + + /// One transport with the shows' fetch (`InAppWebViewHTMLFetcher`): the prewarm must + /// fill exactly the cache the shows read. + private static func defaultFetchHTML(url: URL, completion: @escaping (String?) -> Void) { + let (session, request) = InAppWebViewHTMLFetcher.sessionAndRequest(for: url) + session.dataTask(with: request) { data, response, _ in + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode), + let data, let html = String(data: data, encoding: .utf8) else { + completion(nil) + return + } + completion(html) + }.resume() + } + + private static func defaultLoadCachedConfig() -> ConfigResponse? { + InAppConfigurationRepository().fetchDecodedConfigFromCache() + } +} diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift index c82394899..d0e11f029 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/TransparentView.swift @@ -16,17 +16,29 @@ final class TransparentView: UIView { weak var delegate: WebVCDelegate? weak var webViewAction: WebViewAction? - private var facade: InappWebViewFacadeProtocol? + var facade: InappWebViewFacadeProtocol? private var quizInitTimeoutWorkItem: DispatchWorkItem? private var params: [String: JSONValue]? private var operation: (name: String, body: String)? private let userAgent: String private let inAppId: String + private let tags: [String: String]? private var lastReadyCheckedUrl: String? - private var isReadyCheckInFlight = false + private var readyChecker: WebViewReadyChecker? + /// True when the page finished loading but the JS bridge never appeared within the + /// ready-check budget — lets the init timeout report the accurate failure category. + private(set) var readyCheckDidGiveUp = false + /// True once the page has sent `init`. After that the init timeout is cancelled, so a + /// later ready-check give-up (a post-load navigation dropped the bridge) has no other + /// closing authority — it must close the show itself. + private var hasReceivedInit = false + private var hasCapturedObservedHosts = false private lazy var localStateStorage: WebViewLocalStateStorageProtocol = DI.injectOrFail(WebViewLocalStateStorageProtocol.self) private lazy var permissionHandlerRegistry = DI.injectOrFail(PermissionHandlerRegistryProtocol.self) private lazy var hapticService: HapticServiceProtocol = DI.injectOrFail(HapticServiceProtocol.self) + lazy var featureToggleManager: FeatureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + lazy var databaseRepository: DatabaseRepositoryProtocol = DI.injectOrFail(DatabaseRepositoryProtocol.self) + lazy var eventRepository: EventRepository = DI.injectOrFail(EventRepository.self) private var isMotionServiceInitialized = false private lazy var motionService: MotionServiceProtocol = { isMotionServiceInitialized = true @@ -37,11 +49,12 @@ final class TransparentView: UIView { return service }() - init(frame: CGRect, params: [String: JSONValue], userAgent: String, operation: (name: String, body: String)?, inAppId: String) { + init(frame: CGRect, params: [String: JSONValue], userAgent: String, operation: (name: String, body: String)?, inAppId: String, tags: [String: String]?) { self.params = params self.operation = operation self.userAgent = userAgent self.inAppId = inAppId + self.tags = tags super.init(frame: frame) commonInit() } @@ -51,6 +64,7 @@ final class TransparentView: UIView { self.operation = nil self.userAgent = "" self.inAppId = "" + self.tags = nil super.init(frame: frame) commonInit() } @@ -60,11 +74,13 @@ final class TransparentView: UIView { self.operation = nil self.userAgent = "" self.inAppId = "" + self.tags = nil super.init(coder: coder) commonInit() } deinit { + readyChecker?.cancel() if isMotionServiceInitialized { motionService.stopMonitoring() } Logger.common(message: "[WebView] Deinit TransparentView", category: .webViewInAppMessages) } @@ -102,6 +118,19 @@ final class TransparentView: UIView { facade?.cleanWebView() } + /// Persists the hosts this show's resources actually came from so the next launch's + /// prewarm can preconnect to them. Called from `viewWillDisappear`, which every + /// dismissal path (close action, dim-tap, timeout) goes through while the page is still + /// alive; the once-flag is a cheap guard against a repeated disappear. + func captureObservedResourceHosts() { + guard !hasCapturedObservedHosts else { return } + hasCapturedObservedHosts = true + facade?.evaluateJavaScript(InAppWebViewPrewarmPlanner.observedHostsScript) { result in + guard case .success(let value) = result, let hosts = value as? [String] else { return } + DI.injectOrFail(InAppWebViewPrewarmServiceProtocol.self).rememberObservedHosts(hosts) + } + } + func cancelTimeoutTimer() { quizInitTimeoutWorkItem?.cancel() quizInitTimeoutWorkItem = nil @@ -174,6 +203,7 @@ extension TransparentView: WebBridgeMessageDelegate { if isMotionServiceInitialized { motionService.stopMonitoring() } webViewAction?.onClose() case .`init`: + hasReceivedInit = true quizInitTimeoutWorkItem?.cancel() hapticService.prepare() webViewAction?.onInit() @@ -234,43 +264,49 @@ extension TransparentView: WebBridgeNavigationDelegate { Logger.common(message: "[WebView] WKNavigationDelegate: start loading URL \(url?.absoluteString ?? "unknown")", category: .webViewInAppMessages) // Reset per-navigation checks (e.g. redirects / re-loads). lastReadyCheckedUrl = nil - isReadyCheckInFlight = false + readyCheckDidGiveUp = false + readyChecker?.cancel() + readyChecker = nil } - + func webBridge(_ bridge: MindboxWebBridge, didFinishNavigation url: URL?) { let urlString = url?.absoluteString ?? "unknown" Logger.common(message: "[WebView] WKNavigationDelegate: Upload completed \(urlString)", category: .webViewInAppMessages) // Avoid duplicate checks on multiple didFinish calls for the same URL. - guard !isReadyCheckInFlight else { return } guard lastReadyCheckedUrl != urlString else { return } lastReadyCheckedUrl = urlString - isReadyCheckInFlight = true - let script = Constants.WebViewBridgeJS.bridgeFunctionReadyCheck - facade?.evaluateJavaScript(script) { [weak self] result in + readyChecker?.cancel() + let checker = WebViewReadyChecker(evaluate: { [weak self] script, completion in + // Teardown: abandon the poll silently, exactly like cancel(). + guard let facade = self?.facade else { return } + facade.evaluateJavaScript(script, completion: completion) + }) + readyChecker = checker + checker.run(onReady: { + Logger.common( + message: "[WebView] JS ready check for URL \(urlString): true", + category: .webViewInAppMessages + ) + }, onGiveUp: { [weak self] lastFailure in guard let self else { return } - self.isReadyCheckInFlight = false - - switch result { - case .success(let anyValue): - let hasReady = (anyValue as? Bool) ?? false - Logger.common( - message: "[WebView] JS ready check for URL \(urlString): \(hasReady)", - category: .webViewInAppMessages - ) - if !hasReady { - self.delegate?.closeJSReadyMissingWebViewVC(reason: "window.bridgeMessagesHandlers.emit is missing for URL \(urlString)") - } - - case .failure(let error): - Logger.common( - message: "[WebView] JS ready check failed for URL \(urlString). Error: \(error.localizedDescription)", - category: .webViewInAppMessages - ) - self.delegate?.closeJSReadyMissingWebViewVC(reason: "evaluateJavaScript error for URL \(urlString): \(error.localizedDescription)") + self.readyCheckDidGiveUp = true + Logger.common( + message: "[WebView] JS ready check gave up for URL \(urlString): \(lastFailure)", + category: .webViewInAppMessages + ) + // Before `init` the 7s timeout owns closing: its budget can expire while a slow + // page is still booting the bridge, and the window is invisible until `init` + // anyway; the flag above lets that timeout report the real category. After + // `init` the timeout is already cancelled, so a give-up here (a post-load + // navigation dropped the bridge) is the only signal left — close now, else the + // in-app stays up with a dead bridge. The give-up flag makes this report + // webview_presentation_failed, matching the pre-refactor behavior. + if self.hasReceivedInit { + self.delegate?.closeTimeoutWebViewVC() } - } + }) } func webBridge(_ bridge: MindboxWebBridge, didFailProvisionalNavigation url: URL?, error: any Error) { @@ -322,18 +358,27 @@ extension TransparentView: WebBridgeNavigationDelegate { extension TransparentView { - private func extractOperationParams(from message: BridgeMessage) -> (name: String, body: String)? { + private func extractOperationParams(from message: BridgeMessage) -> (name: String, body: JSONValue)? { guard case .string(let str) = message.payload, let data = str.data(using: .utf8), let dict = try? JSONDecoder().decode([String: JSONValue].self, from: data), case .string(let operation) = dict["operation"], - let body = dict["body"], - let bodyData = try? JSONEncoder().encode(body), - let bodyString = String(data: bodyData, encoding: .utf8) else { + !operation.isEmpty, + let body = dict["body"] else { return nil } - return (operation, bodyString) + return (operation, body) + } + + private func mergedOperationBodyString(_ body: JSONValue) -> String? { + let gatedTags = featureToggleManager.gatedTags(tags) + let mergedBody = JSONValue.mergingInAppTags(gatedTags, into: body) + + guard let bodyData = try? JSONEncoder().encode(mergedBody) else { + return nil + } + return String(data: bodyData, encoding: .utf8) } private func sendBridgeSuccess(action: String, id: UUID) { @@ -353,15 +398,15 @@ extension TransparentView { } private func handleAsyncOperation(message: BridgeMessage) { - guard let params = extractOperationParams(from: message) else { - sendBridgeError("Invalid payload: missing or empty operation", action: message.action, id: message.id) + guard let params = extractOperationParams(from: message), + let bodyString = mergedOperationBodyString(params.body) else { + sendBridgeError("Invalid payload: could not parse operation/body or encode the operation body", action: message.action, id: message.id) return } - let customEvent = CustomEvent(name: params.name, payload: params.body) + let customEvent = CustomEvent(name: params.name, payload: bodyString) let event = Event(type: .customEvent, body: BodyEncoder(encodable: customEvent).body) - let databaseRepository = DI.injectOrFail(DatabaseRepositoryProtocol.self) do { try databaseRepository.create(event: event) Logger.common(message: "[WebView] asyncOperation '\(params.name)' queued", level: .info, category: .webViewInAppMessages) @@ -375,14 +420,14 @@ extension TransparentView { } private func handleSyncOperation(message: BridgeMessage) { - guard let params = extractOperationParams(from: message) else { - sendBridgeError("Invalid payload: missing or empty operation", action: message.action, id: message.id) + guard let params = extractOperationParams(from: message), + let bodyString = mergedOperationBodyString(params.body) else { + sendBridgeError("Invalid payload: could not parse operation/body or encode the operation body", action: message.action, id: message.id) return } - let customEvent = CustomEvent(name: params.name, payload: params.body) + let customEvent = CustomEvent(name: params.name, payload: bodyString) let event = Event(type: .syncEvent, body: BodyEncoder(encodable: customEvent).body) - let eventRepository = DI.injectOrFail(EventRepository.self) Logger.common(message: "[WebView] syncOperation '\(params.name)' sending", level: .info, category: .webViewInAppMessages) @@ -441,7 +486,7 @@ extension TransparentView { return BridgeMessage( type: .error, action: action, - payload: .string(error.createJSON()), + payload: .string(error.createDataJSON()), id: id ) } diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/WebViewController.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/WebViewController.swift index 7327025c4..882f90e71 100644 --- a/Mindbox/InAppMessages/Presentation/Views/WebView/WebViewController.swift +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/WebViewController.swift @@ -12,7 +12,6 @@ protocol WebVCDelegate: AnyObject { func closeTapWebViewVC() func closeTimeoutWebViewVC() func closeLoadFailedWebViewVC(reason: String) - func closeJSReadyMissingWebViewVC(reason: String) } final class WebViewController: UIViewController, InappViewControllerProtocol { @@ -35,6 +34,7 @@ final class WebViewController: UIViewController, InappViewControllerProtocol { private let id: String private let imagesDict: [String: UIImage] private let operation: (name: String, body: String)? + private let tags: [String: String]? private let onPresented: () -> Void private let onCloseInApp: () -> Void @@ -62,12 +62,14 @@ final class WebViewController: UIViewController, InappViewControllerProtocol { onCloseInApp: @escaping () -> Void, onError: @escaping (InAppPresentationError) -> Void, windowProvider: @escaping () -> UIWindow? = WebViewController.defaultWindowProvider, - operation: (name: String, body: String)? + operation: (name: String, body: String)?, + tags: [String: String]? ) { self.model = model self.id = id self.imagesDict = imagesDict self.operation = operation + self.tags = tags self.onPresented = onPresented self.onCloseInApp = onCloseInApp self.onError = onError @@ -84,6 +86,8 @@ final class WebViewController: UIViewController, InappViewControllerProtocol { NotificationCenter.default.removeObserver(self) Logger.common(message: "[WebView] Deinit WebViewVC", category: .webViewInAppMessages) transparentWebView?.cleanUp() + // Stop the closed in-app's JS from running hidden on the parked warm instance. + DI.injectOrFail(InAppWebViewPrewarmServiceProtocol.self).parkWarmWebView() } private func setupWebView() { @@ -96,7 +100,7 @@ final class WebViewController: UIViewController, InappViewControllerProtocol { switch layer { case .webview(let webviewLayer): - let webView = TransparentView(frame: .zero, params: webviewLayer.params, userAgent: createUserAgent(), operation: operation, inAppId: id) + let webView = TransparentView(frame: .zero, params: webviewLayer.params, userAgent: createUserAgent(), operation: operation, inAppId: id, tags: tags) view.addSubview(webView) setupConstraints(for: webView, in: view) @@ -149,6 +153,13 @@ final class WebViewController: UIViewController, InappViewControllerProtocol { becomeFirstResponder() } + override func viewWillDisappear(_ animated: Bool) { + // Every dismissal path (close action, dim-tap, timeout) goes through here while + // the page is still alive — the last moment the learned-hosts capture can run. + transparentWebView?.captureObservedResourceHosts() + super.viewWillDisappear(animated) + } + override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { if motion == .motionShake { transparentWebView?.handleSystemShake() @@ -198,13 +209,7 @@ final class WebViewController: UIViewController, InappViewControllerProtocol { } private func createUserAgent() -> String { - let utilitiesFetcher = DI.injectOrFail(UtilitiesFetcher.self) - - let sdkVersion = utilitiesFetcher.sdkVersion ?? "unknown" - let appVersion = utilitiesFetcher.appVerson ?? "unknown" - let appName = utilitiesFetcher.hostApplicationName ?? "unknown" - - return "mindbox.sdk/\(sdkVersion) (\(DeviceModelHelper.os) \(DeviceModelHelper.iOSVersion); \(DeviceModelHelper.model)) \(appName)/\(appVersion)" + SDKUserAgent.build() } } @@ -217,26 +222,25 @@ extension WebViewController: WebVCDelegate { func closeTimeoutWebViewVC() { Logger.common(message: "[WebView] WebViewVC closeTimeoutOrErrorWebViewVC", category: .webViewInAppMessages) reportErrorAndClose( - .webviewLoadFailed("[WebView] WebView initialization timeout for in-app id \(id).") + Self.timeoutError(readyCheckGaveUp: transparentWebView?.readyCheckDidGiveUp == true, inAppId: id) ) } + /// The init timeout is the single closing authority, but monitoring must still tell + /// "the page loaded and its JS bridge never appeared" (a content defect) apart from + /// "the page never finished loading" (a load defect). + static func timeoutError(readyCheckGaveUp: Bool, inAppId: String) -> InAppPresentationError { + readyCheckGaveUp + ? .webviewPresentationFailed("[WebView] JS bridge missing after page load (init timeout) for in-app id \(inAppId).") + : .webviewLoadFailed("[WebView] WebView initialization timeout for in-app id \(inAppId).") + } + func closeLoadFailedWebViewVC(reason: String) { Logger.common(message: "[WebView] WebViewVC closeLoadFailedWebViewVC. Reason: \(reason)", category: .webViewInAppMessages) reportErrorAndClose( .webviewLoadFailed(reason) ) } - - func closeJSReadyMissingWebViewVC(reason: String) { - Logger.common( - message: "[WebView] WebViewVC closeJSReadyMissingWebViewVC. Reason: \(reason)", - category: .webViewInAppMessages - ) - reportErrorAndClose( - .webviewPresentationFailed(reason) - ) - } } extension WebViewController: WebViewAction { diff --git a/Mindbox/InAppMessages/Presentation/Views/WebView/WebViewReadyChecker.swift b/Mindbox/InAppMessages/Presentation/Views/WebView/WebViewReadyChecker.swift new file mode 100644 index 000000000..7623a117b --- /dev/null +++ b/Mindbox/InAppMessages/Presentation/Views/WebView/WebViewReadyChecker.swift @@ -0,0 +1,70 @@ +// +// WebViewReadyChecker.swift +// Mindbox +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +/// Polls the page for the JS bridge instead of deciding on a single didFinish-time probe: +/// module scripts can finish evaluating a beat after the navigation's load event (reused +/// prewarmed WebView, slow devices), so one early `false` must not fail a healthy in-app. +final class WebViewReadyChecker { + typealias Evaluate = (_ script: String, _ completion: @escaping (Result) -> Void) -> Void + typealias Schedule = (_ delay: TimeInterval, _ work: @escaping () -> Void) -> Void + + static let maxAttempts = 8 + static let retryDelay: TimeInterval = 0.15 + + private let evaluate: Evaluate + private let schedule: Schedule + private var isCancelled = false + + init(evaluate: @escaping Evaluate, + schedule: @escaping Schedule = { delay, work in + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work) + }) { + self.evaluate = evaluate + self.schedule = schedule + } + + func run(onReady: @escaping () -> Void, onGiveUp: @escaping (_ lastFailure: String) -> Void) { + attempt(1, onReady: onReady, onGiveUp: onGiveUp) + } + + /// Abandons the poll without calling either completion — the caller's new navigation + /// (or teardown) owns readiness from here. + func cancel() { + isCancelled = true + } + + private func attempt(_ number: Int, onReady: @escaping () -> Void, onGiveUp: @escaping (String) -> Void) { + guard !isCancelled else { return } + evaluate(Constants.WebViewBridgeJS.bridgeFunctionReadyCheck) { [weak self] result in + guard let self, !self.isCancelled else { return } + + let failure: String + switch result { + case .success(let anyValue) where (anyValue as? Bool) == true: + onReady() + return + case .success: + failure = "window.bridgeMessagesHandlers.emit is missing" + case .failure(let error): + // Transient during navigation churn (a page mid-teardown rejects + // evaluation) — retried on the same budget as a plain `false`. + failure = "evaluateJavaScript error: \(error.localizedDescription)" + } + + guard number < Self.maxAttempts else { + onGiveUp(failure) + return + } + self.schedule(Self.retryDelay) { [weak self] in + self?.attempt(number + 1, onReady: onReady, onGiveUp: onGiveUp) + } + } + } +} diff --git a/Mindbox/Mindbox.swift b/Mindbox/Mindbox.swift index 1b65b0c42..eb0efd727 100644 --- a/Mindbox/Mindbox.swift +++ b/Mindbox/Mindbox.swift @@ -43,7 +43,9 @@ public class Mindbox: NSObject { private var sessionTemporaryStorage: SessionTemporaryStorage? private var trackVisitManager: TrackVisitCommonTrackProtocol? - private let queue = DispatchQueue(label: "com.Mindbox.initialization", attributes: .concurrent) + /// Serial queue for off-main event creation/persistence so public operation calls return + /// without blocking the caller (main) thread. Serial preserves event ordering. + private let eventQueue = DispatchQueue(label: "com.Mindbox.eventQueue") var coreController: CoreController? @@ -81,6 +83,9 @@ public class Mindbox: NSObject { */ public func initialization(configuration: MBConfiguration) { coreController?.initialization(configuration: configuration) + // Prewarm stage 1: warm the web-content process as early as possible so the first + // webview show doesn't pay process startup. Safe here — assembly() has built DI. + DI.injectOrFail(InAppWebViewPrewarmServiceProtocol.self).prewarmProcess() } private var observeTokens: [UUID] = [] @@ -128,10 +133,17 @@ public class Mindbox: NSObject { let token = UUID() persistenceStorage?.onDidChange = { [weak self] in guard let self = self else { return } - self.observeSemaphore.lock { - guard let value = value(), let index = self.observeTokens.firstIndex(of: token) else { return } + // Resolve under the lock, invoke the host completion AFTER unlocking: the + // non-recursive semaphore would self-deadlock if the callback re-enters + // getDeviceUUID/getAPNSToken. Delivery stays synchronous on the notifying + // thread - long-standing public contract (encoded in MindboxTests). + let resolved: String? = self.observeSemaphore.lock { + guard let value = value(), let index = self.observeTokens.firstIndex(of: token) else { return nil } self.observeTokens.remove(at: index) - completion(value) + return value + } + if let resolved { + completion(resolved) } } observeTokens.append(token) @@ -250,16 +262,7 @@ public class Mindbox: NSObject { */ public func pushClicked(uniqueKey: String, buttonUniqueKey: String? = nil) { - guard let tracker = DI.inject(ClickNotificationManager.self) else { - return - } - - do { - try tracker.track(uniqueKey: uniqueKey, buttonUniqueKey: buttonUniqueKey) - Logger.common(message: "Track Click", level: .info, category: .notification) - } catch { - Logger.common(message: "Track UNNotificationResponse failed with error: \(error)", level: .error, category: .notification) - } + enqueueClickTracking { try $0.track(uniqueKey: uniqueKey, buttonUniqueKey: buttonUniqueKey) } } /** @@ -270,21 +273,12 @@ public class Mindbox: NSObject { - operationBody: Provided `OperationBodyRequestBase` payload to send. */ public func executeAsyncOperation(operationSystemName: String, operationBody: T) { - guard operationSystemName.operationNameIsValid else { - Logger.common(message: "Invalid operation name: \(operationSystemName)", level: .error, category: .notification) - return - } + guard validateOperationName(operationSystemName) else { return } + // Encode on the caller: operation bodies are mutable classes, so the snapshot + // must be taken before the queue hop or it races host mutations. Only the + // immutable JSON string crosses to the queue. let operationBodyJSON = BodyEncoder(encodable: operationBody).body - let customEvent = CustomEvent(name: operationSystemName, payload: operationBodyJSON) - - let event = Event(type: .customEvent, body: BodyEncoder(encodable: customEvent).body) - sendCustomEventInapps(operationSystemName, jsonString: operationBodyJSON) - do { - try databaseRepository?.create(event: event) - Logger.common(message: "Track executeAsyncOperation", level: .info, category: .notification) - } catch { - Logger.common(message: "Track executeAsyncOperation failed with error: \(error)", level: .error, category: .notification) - } + enqueueAsyncEvent(operationSystemName: operationSystemName, payloadJSON: operationBodyJSON) } /** @@ -295,24 +289,8 @@ public class Mindbox: NSObject { - json: String which contains JSON to send. */ public func executeAsyncOperation(operationSystemName: String, json: String) { - guard operationSystemName.operationNameIsValid else { - Logger.common(message: "Invalid operation name: \(operationSystemName)", level: .error, category: .notification) - return - } - guard let jsonData = json.data(using: .utf8), - (try? JSONSerialization.jsonObject(with: jsonData)) != nil else { - Logger.common(message: "Operation body is not valid JSON", level: .error, category: .notification) - return - } - let customEvent = CustomEvent(name: operationSystemName, payload: json) - let event = Event(type: .customEvent, body: BodyEncoder(encodable: customEvent).body) - sendCustomEventInapps(operationSystemName, jsonString: json) - do { - try databaseRepository?.create(event: event) - Logger.common(message: "Track executeAsyncOperation", level: .info, category: .notification) - } catch { - Logger.common(message: "Track executeAsyncOperation failed with error: \(error)", level: .error, category: .notification) - } + guard validateOperationName(operationSystemName) else { return } + enqueueAsyncEvent(operationSystemName: operationSystemName, payloadJSON: json, validatePayloadAsJSON: true) } /** @@ -328,17 +306,13 @@ public class Mindbox: NSObject { operationBody: T, completion: @escaping (Result) -> Void ) where T: OperationBodyRequestType { - guard operationSystemName.operationNameIsValid else { - Logger.common(message: "Invalid operation name: \(operationSystemName)", level: .error, category: .notification) + guard validateOperationName(operationSystemName) else { + failSyncOperation(reason: "Invalid operation name: \(operationSystemName)", completion: completion) return } + // Caller-side encode: same mutable-body snapshot contract as executeAsyncOperation. let operationBodyJSON = BodyEncoder(encodable: operationBody).body - let customEvent = CustomEvent(name: operationSystemName, payload: operationBodyJSON) - let event = Event(type: .syncEvent, body: BodyEncoder(encodable: customEvent).body) - let eventRepository = DI.injectOrFail(EventRepository.self) - eventRepository.send(type: OperationResponse.self, event: event, completion: completion) - sendCustomEventInapps(operationSystemName, jsonString: operationBodyJSON) - Logger.common(message: "Track executeSyncOperation", level: .info, category: .notification) + enqueueSyncEvent(operationSystemName: operationSystemName, payloadJSON: operationBodyJSON, completion: completion) } /** @@ -354,21 +328,11 @@ public class Mindbox: NSObject { json: String, completion: @escaping (Result) -> Void ) { - guard operationSystemName.operationNameIsValid else { - Logger.common(message: "Invalid operation name: \(operationSystemName)", level: .error, category: .notification) - return - } - guard let jsonData = json.data(using: .utf8), - (try? JSONSerialization.jsonObject(with: jsonData)) != nil else { - Logger.common(message: "Operation body is not valid JSON", level: .error, category: .notification) + guard validateOperationName(operationSystemName) else { + failSyncOperation(reason: "Invalid operation name: \(operationSystemName)", completion: completion) return } - let customEvent = CustomEvent(name: operationSystemName, payload: json) - let event = Event(type: .syncEvent, body: BodyEncoder(encodable: customEvent).body) - let eventRepository = DI.injectOrFail(EventRepository.self) - eventRepository.send(type: OperationResponse.self, event: event, completion: completion) - sendCustomEventInapps(operationSystemName, jsonString: json) - Logger.common(message: "Track executeSyncOperation", level: .info, category: .notification) + enqueueSyncEvent(operationSystemName: operationSystemName, payloadJSON: json, validatePayloadAsJSON: true, completion: completion) } /** @@ -388,17 +352,13 @@ public class Mindbox: NSObject { customResponseType: P.Type, completion: @escaping (Result) -> Void ) where T: OperationBodyRequestType, P: OperationResponseType { - guard operationSystemName.operationNameIsValid else { - Logger.common(message: "Invalid operation name: \(operationSystemName)", level: .error, category: .notification) + guard validateOperationName(operationSystemName) else { + failSyncOperation(reason: "Invalid operation name: \(operationSystemName)", completion: completion) return } + // Caller-side encode: same mutable-body snapshot contract as executeAsyncOperation. let operationBodyJSON = BodyEncoder(encodable: operationBody).body - let customEvent = CustomEvent(name: operationSystemName, payload: operationBodyJSON) - let event = Event(type: .syncEvent, body: BodyEncoder(encodable: customEvent).body) - let eventRepository = DI.injectOrFail(EventRepository.self) - eventRepository.send(type: P.self, event: event, completion: completion) - sendCustomEventInapps(operationSystemName, jsonString: operationBodyJSON) - Logger.common(message: "Track executeSyncOperation", level: .info, category: .notification) + enqueueSyncEvent(operationSystemName: operationSystemName, payloadJSON: operationBodyJSON, completion: completion) } /** @@ -414,20 +374,107 @@ public class Mindbox: NSObject { */ @available(*, deprecated, message: "Use `executeAsyncOperation(operationSystemName: String, operationBody: T)` instead.") public func executeAsyncOperation(operationSystemName: String, operationBody: T) { - guard operationSystemName.operationNameIsValid else { - Logger.common(message: "Invalid operation name: \(operationSystemName)", level: .error, category: .notification) + guard validateOperationName(operationSystemName) else { return } + // Caller-side encode: same mutable-body snapshot contract as the generic overload. + let operationBodyJSON = BodyEncoder(encodable: operationBody).body + enqueueAsyncEvent(operationSystemName: operationSystemName, payloadJSON: operationBodyJSON) + } + + // MARK: - Operations pipeline + + /// Shared tail of the pushClicked overloads: resolve the tracker on the caller, + /// persist the click off-main on eventQueue. + private func enqueueClickTracking(_ track: @escaping (ClickNotificationManager) throws -> Void) { + guard let tracker = DI.inject(ClickNotificationManager.self) else { + Logger.common(message: "Track Click dropped: ClickNotificationManager is nil", level: .error, category: .notification) return } - let operationBodyJSON = BodyEncoder(encodable: operationBody).body - let customEvent = CustomEvent(name: operationSystemName, payload: operationBodyJSON) - let event = Event(type: .customEvent, body: BodyEncoder(encodable: customEvent).body) - sendCustomEventInapps(operationSystemName, jsonString: operationBodyJSON) - do { - try databaseRepository?.create(event: event) - Logger.common(message: "Track executeAsyncOperation", level: .info, category: .notification) - } catch { - Logger.common(message: "Track executeAsyncOperation failed with error: \(error)", level: .error, category: .notification) + eventQueue.async { + do { + try track(tracker) + Logger.common(message: "Track Click", level: .info, category: .notification) + } catch { + Logger.common(message: "Track UNNotificationResponse failed with error: \(error)", level: .error, category: .notification) + } + } + } + + /// Shared validation guard of every operation overload; logs the drop reason once. + private func validateOperationName(_ operationSystemName: String) -> Bool { + guard OperationNameValidator.isValid(operationSystemName) else { + Logger.common(message: "Invalid operation name: \(operationSystemName)", level: .error, category: .notification) + return false } + return true + } + + /// Delivers a `.validationError` failure on the main thread for sync operation overloads + /// that detect an invalid input before reaching the event queue. + private func failSyncOperation

( + reason: String, + location: String = "operationSystemName", + completion: @escaping (Result) -> Void + ) { + let error = MindboxError.validationError(ValidationError( + status: .validationError, + validationMessages: [ValidationMessage(message: reason, location: location)] + )) + DispatchQueue.main.async { completion(.failure(error)) } + } + + /// Off-main tail of the executeAsyncOperation overloads: build the event on eventQueue + /// and persist it for guaranteed delivery. `payloadJSON` must be an immutable snapshot + /// taken on the caller; host-provided JSON strings are validated here, off-main. + private func enqueueAsyncEvent(operationSystemName: String, payloadJSON: String, validatePayloadAsJSON: Bool = false) { + eventQueue.async { [self] in + if validatePayloadAsJSON, !Self.isValidJSON(payloadJSON) { + Logger.common(message: "Operation body is not valid JSON", level: .error, category: .notification) + return + } + let customEvent = CustomEvent(name: operationSystemName, payload: payloadJSON) + let event = Event(type: .customEvent, body: BodyEncoder(encodable: customEvent).body) + self.sendCustomEventInapps(operationSystemName, jsonString: payloadJSON) + guard let databaseRepository = self.databaseRepository else { + Logger.common(message: "Track executeAsyncOperation dropped: databaseRepository is nil", level: .error, category: .notification) + return + } + do { + try databaseRepository.create(event: event) + Logger.common(message: "Track executeAsyncOperation", level: .info, category: .notification) + } catch { + Logger.common(message: "Track executeAsyncOperation failed with error: \(error)", level: .error, category: .notification) + } + } + } + + /// Off-main tail of the executeSyncOperation overloads: build the sync event on + /// eventQueue and hand it to EventRepository, which delivers `completion` on main. + /// Invalid JSON fails the operation with a `.validationError` delivered on main, so the + /// completion contract holds on every path. The repository is resolved at call time on purpose. + private func enqueueSyncEvent( + operationSystemName: String, + payloadJSON: String, + validatePayloadAsJSON: Bool = false, + completion: @escaping (Result) -> Void + ) { + let eventRepository = DI.injectOrFail(EventRepository.self) + eventQueue.async { [self] in + if validatePayloadAsJSON, !Self.isValidJSON(payloadJSON) { + Logger.common(message: "Operation body is not valid JSON", level: .error, category: .notification) + self.failSyncOperation(reason: "Operation body is not valid JSON", location: "operationBody", completion: completion) + return + } + let customEvent = CustomEvent(name: operationSystemName, payload: payloadJSON) + let event = Event(type: .syncEvent, body: BodyEncoder(encodable: customEvent).body) + eventRepository.send(type: P.self, event: event, completion: completion) + self.sendCustomEventInapps(operationSystemName, jsonString: payloadJSON) + Logger.common(message: "Track executeSyncOperation", level: .info, category: .notification) + } + } + + private static func isValidJSON(_ json: String) -> Bool { + guard let jsonData = json.data(using: .utf8) else { return false } + return (try? JSONSerialization.jsonObject(with: jsonData)) != nil } /** @@ -438,15 +485,7 @@ public class Mindbox: NSObject { */ public func pushClicked(response: UNNotificationResponse) { - guard let tracker = DI.inject(ClickNotificationManager.self) else { - return - } - do { - try tracker.track(response: response) - Logger.common(message: "Track Click", level: .info, category: .notification) - } catch { - Logger.common(message: "Track UNNotificationResponse failed with error: \(error)", level: .error, category: .notification) - } + enqueueClickTracking { try $0.track(response: response) } } /** @@ -458,8 +497,13 @@ public class Mindbox: NSObject { */ public func track(_ type: TrackVisitType) { guard let trackVisitManager = trackVisitManager else { + Logger.common(message: "Track Visit dropped: trackVisitManager is nil", level: .error, category: .visit) return } + // Deliberately NOT deferred to eventQueue: handlePush/handleUniversalLink set + // skipNextDirectTrackVisit, which trackDirect consumes on controllerQueue. The + // flag write must happen-before that dispatch, or a queued track(.push) races it: + // duplicate direct visit now, the next legitimate one wrongly skipped. do { try trackVisitManager.track(type) } catch { @@ -476,8 +520,10 @@ public class Mindbox: NSObject { */ public func track(data: TrackVisitData) { guard let trackVisitManager = trackVisitManager else { + Logger.common(message: "Track Visit dropped: trackVisitManager is nil", level: .error, category: .visit) return } + // Synchronous on purpose: same skipNextDirectTrackVisit contract as track(_:) above. do { try trackVisitManager.track(data: data) } catch { @@ -578,6 +624,7 @@ public class Mindbox: NSObject { } private func sendCustomEventInapps(_ operationSystemName: String, jsonString: String?) { + // Reached only from the eventQueue operation blocks - i.e. off the main thread. guard let inappMessageEventSender = DI.inject(InappMessageEventSender.self) else { return } diff --git a/Mindbox/Model/Common/MBDate.swift b/Mindbox/Model/Common/MBDate.swift index c4fc4cc19..da9dbc7b9 100644 --- a/Mindbox/Model/Common/MBDate.swift +++ b/Mindbox/Model/Common/MBDate.swift @@ -28,7 +28,8 @@ public final class DateTime: MBDate { } override func decodeWithFormat(_ rawString: String) -> Date? { - return Date.fromISO8601(rawString) + return rawString.toDate(withFormat: .utc) + ?? rawString.toDate(withFormat: .utcWithMillis) } } diff --git a/Mindbox/Model/MindboxError/MindboxError.swift b/Mindbox/Model/MindboxError/MindboxError.swift index 33db0f4c2..cf64d0d13 100644 --- a/Mindbox/Model/MindboxError/MindboxError.swift +++ b/Mindbox/Model/MindboxError/MindboxError.swift @@ -128,59 +128,68 @@ public extension MindboxError { guard let errorData = try? JSONEncoder().encode(self), let errorString = String(data: errorData, encoding: .utf8) else { - return - """ - { - type: "InternalError", - data: { - errroKey: "\(self.data.errorKey ?? "null")", - errroName: "JSON encoding error", - errorMessage: "Unable to convert Data to JSON", - } - } - """ + return #"{"type":"InternalError","data":{"errorKey":"\#(self.data.errorKey ?? "null")","errorName":"JSON encoding error","errorMessage":"Unable to convert Data to JSON"}}"# + } + return errorString + } + + func convertDataToString() -> String { + guard + let errorData = try? JSONEncoder().encode(data), + let errorString = String(data: errorData, encoding: .utf8) else { + return #"{"errorMessage":"Unable to convert Data to JSON"}"# } return errorString } } func createJSON() -> String { + makeErrorJSON().convertToString() + } + + /// Data-only JSON without the `{type, data}` envelope — the WebView JS-bridge + /// `onError` contract. `createJSON()` must keep the envelope: RN/Flutter dispatch on it. + internal func createDataJSON() -> String { + makeErrorJSON().convertDataToString() + } + + private func makeErrorJSON() -> MindboxErrorJSON { switch self { case .validationError(let error): return MindboxErrorJSON(status: error.status, - validationMessages: error.validationMessages).convertToString() + validationMessages: error.validationMessages) case .protocolError(let error): return MindboxErrorJSON(status: error.status, errorMessage: error.errorMessage, errorId: error.errorId ?? "", - httpStatusCode: error.httpStatusCode).convertToString() + httpStatusCode: error.httpStatusCode) case .serverError(let error): return MindboxErrorJSON(status: error.status, errorMessage: error.errorMessage, errorId: error.errorId ?? "", - httpStatusCode: error.httpStatusCode).convertToString() + httpStatusCode: error.httpStatusCode) case .internalError(let error): return MindboxErrorJSON(errorKey: error.errorKey, errorName: error.reason ?? "", - errorMessage: error.description).convertToString() + errorMessage: error.description) case .invalidResponse(let response): if let httpResponse = response as? HTTPURLResponse { let httpStatusCode = String(httpResponse.statusCode) let errorMessage = httpResponse.description return MindboxErrorJSON(httpStatusCode: httpStatusCode, - errorMessage: errorMessage).convertToString() + errorMessage: errorMessage) } else { return MindboxErrorJSON(httpStatusCode: "null", - errorMessage: "Connection error").convertToString() + errorMessage: "Connection error") } case .connectionError: return MindboxErrorJSON(httpStatusCode: "null", - errorMessage: "Connection error").convertToString() + errorMessage: "Connection error") case .unknown(let error): return MindboxErrorJSON(errorKey: "unknown", errorName: "", - errorMessage: error.localizedDescription).convertToString() + errorMessage: error.localizedDescription) } } } diff --git a/Mindbox/Network/MBNetworkFetcher.swift b/Mindbox/Network/MBNetworkFetcher.swift index 4356118a5..2e258a578 100644 --- a/Mindbox/Network/MBNetworkFetcher.swift +++ b/Mindbox/Network/MBNetworkFetcher.swift @@ -27,10 +27,8 @@ class MBNetworkFetcher: NetworkFetcher { private static func makeSession(utilitiesFetcher: UtilitiesFetcher) -> URLSession { let sessionConfiguration: URLSessionConfiguration = .default - let sdkVersion = utilitiesFetcher.sdkVersion ?? "unknow" - let appVersion = utilitiesFetcher.appVerson ?? "unknow" - let appName = utilitiesFetcher.hostApplicationName ?? "unknow" - let userAgent: String = "mindbox.sdk/\(sdkVersion) (\(DeviceModelHelper.os) \(DeviceModelHelper.iOSVersion); \(DeviceModelHelper.model)) \(appName)/\(appVersion)" + let sdkVersion = utilitiesFetcher.sdkVersion ?? "unknown" + let userAgent = SDKUserAgent.build(utilitiesFetcher: utilitiesFetcher) sessionConfiguration.httpAdditionalHeaders = [ "Mindbox-Integration": "iOS-SDK", "Mindbox-Integration-Version": sdkVersion, diff --git a/Mindbox/NetworkRepository/Event/MBEventRepository.swift b/Mindbox/NetworkRepository/Event/MBEventRepository.swift index 5efe99689..17d712edb 100644 --- a/Mindbox/NetworkRepository/Event/MBEventRepository.swift +++ b/Mindbox/NetworkRepository/Event/MBEventRepository.swift @@ -46,12 +46,19 @@ class MBEventRepository: EventRepository { } func send(type: T.Type, event: Event, completion: @escaping (Result) -> Void) where T: Decodable { + // Public contract: the completion is ALWAYS delivered on the main thread - every + // path, including the early validation errors below, goes through `deliver`. + let deliver: (Result) -> Void = { result in + DispatchQueue.main.async { + completion(result) + } + } guard let configuration = persistenceStorage.configuration else { let error = MindboxError(.init( errorKey: .invalidConfiguration, reason: "Configuration is not set" )) - completion(.failure(error)) + deliver(.failure(error)) return } guard let deviceUUID = persistenceStorage.deviceUUID else { @@ -59,7 +66,7 @@ class MBEventRepository: EventRepository { errorKey: .invalidConfiguration, reason: "DeviceUUID is not set" )) - completion(.failure(error)) + deliver(.failure(error)) return } let wrapper = EventWrapper( @@ -68,16 +75,7 @@ class MBEventRepository: EventRepository { deviceUUID: deviceUUID ) let route = makeRoute(wrapper: wrapper) - fetcher.request(type: type, route: route, completion: { result in - DispatchQueue.main.async { - switch result { - case let .failure(error): - completion(.failure(error)) - case let .success(response): - completion(.success(response)) - } - } - }) + fetcher.request(type: type, route: route, completion: deliver) } func sendRaw(event: Event, completion: @escaping (Result) -> Void) { diff --git a/Mindbox/PersistenceStorage/MBPersistenceStorage.swift b/Mindbox/PersistenceStorage/MBPersistenceStorage.swift index d35f5a39a..357ca885f 100644 --- a/Mindbox/PersistenceStorage/MBPersistenceStorage.swift +++ b/Mindbox/PersistenceStorage/MBPersistenceStorage.swift @@ -16,29 +16,27 @@ class MBPersistenceStorage: PersistenceStorage { // MARK: - Dependency static var defaults: UserDefaults = .standard - private let dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .full - dateFormatter.timeStyle = .full - return dateFormatter - }() - // MARK: - Property + // Installed state is the presence of the persisted installation date string, + // never its parseability. Decoupling from parsing keeps `isInstalled` stable + // across region/locale and 12h↔24h time-format changes, which would otherwise + // break a localized date string and route an existing install through the + // fresh-install path. var isInstalled: Bool { - installationDate != nil + installationDateString != nil } var installationDate: Date? { get { if let dateString = installationDateString { - return dateFormatter.date(from: dateString) + return dateString.toDate(withFormat: .utc) } else { return nil } } set { if let date = newValue { - installationDateString = dateFormatter.string(from: date) + installationDateString = date.toString(withFormat: .utc) } else { installationDateString = nil } @@ -48,14 +46,14 @@ class MBPersistenceStorage: PersistenceStorage { var firstInitializationDateTime: Date? { get { if let dateString = firstInitializationDateTimeString { - return dateFormatter.date(from: dateString) + return dateString.toDate(withFormat: .utc) } else { return nil } } set { if let date = newValue { - firstInitializationDateTimeString = dateFormatter.string(from: date) + firstInitializationDateTimeString = date.toString(withFormat: .utc) } else { firstInitializationDateTimeString = nil } @@ -65,14 +63,14 @@ class MBPersistenceStorage: PersistenceStorage { var apnsTokenSaveDate: Date? { get { if let dateString = apnsTokenSaveDateString { - return dateFormatter.date(from: dateString) + return dateString.toDate(withFormat: .utc) } else { return nil } } set { if let date = newValue { - apnsTokenSaveDateString = dateFormatter.string(from: date) + apnsTokenSaveDateString = date.toString(withFormat: .utc) } else { apnsTokenSaveDateString = nil } @@ -82,7 +80,7 @@ class MBPersistenceStorage: PersistenceStorage { var lastInfoUpdateDate: Date? { get { if let dateString = lastInfoUpdateDateString { - return dateFormatter.date(from: dateString) + return dateString.toDate(withFormat: .utc) } else { return nil } @@ -90,7 +88,7 @@ class MBPersistenceStorage: PersistenceStorage { set { if let date = newValue { - lastInfoUpdateDateString = dateFormatter.string(from: date) + lastInfoUpdateDateString = date.toString(withFormat: .utc) } else { lastInfoUpdateDateString = nil } @@ -100,14 +98,14 @@ class MBPersistenceStorage: PersistenceStorage { var deprecatedEventsRemoveDate: Date? { get { if let dateString = deprecatedEventsRemoveDateString { - return dateFormatter.date(from: dateString) + return dateString.toDate(withFormat: .utc) } else { return nil } } set { if let date = newValue { - deprecatedEventsRemoveDateString = dateFormatter.string(from: date) + deprecatedEventsRemoveDateString = date.toString(withFormat: .utc) } else { deprecatedEventsRemoveDateString = nil } @@ -133,14 +131,14 @@ class MBPersistenceStorage: PersistenceStorage { var configDownloadDate: Date? { get { if let dateString = configDownloadDateString { - return dateFormatter.date(from: dateString) + return dateString.toDate(withFormat: .utc) } else { return nil } } set { if let date = newValue { - configDownloadDateString = dateFormatter.string(from: date) + configDownloadDateString = date.toString(withFormat: .utc) } else { configDownloadDateString = nil } @@ -268,6 +266,9 @@ class MBPersistenceStorage: PersistenceStorage { @UserDefaultsWrapper(key: .webViewLocalStateVersion, defaultValue: nil) var webViewLocalStateVersion: Int? + @UserDefaultsWrapper(key: .webViewLearnedHosts, defaultValue: nil) + var webViewLearnedHosts: [String: [String]]? + @UserDefaultsWrapper(key: .operationsDomainFromConfig, defaultValue: nil) var operationsDomainFromConfig: String? { didSet { @@ -313,6 +314,7 @@ extension MBPersistenceStorage { case applicationInfoUpdateVersion = "MBPersistenceStorage-applicationInfoUpdatedVersion" case applicationInstanceId = "MBPersistenceStorage-applicationInstanceId" case webViewLocalStateVersion = "MBPersistenceStorage-webViewLocalStateVersion" + case webViewLearnedHosts = "MBPersistenceStorage-webViewLearnedHosts" case operationsDomainFromConfig = "MBPersistenceStorage-operationsDomainFromConfig" // MARK: - Deprecated Keys @@ -353,3 +355,52 @@ fileprivate extension UserDefaults { return object(forKey: key) != nil } } + +// MARK: - Install-state probing across stores + +extension MBPersistenceStorage { + + /// Backing key for `isInstalled`, exposed so the reporter can probe a specific store. + static var installationDataKey: String { + UserDefaultsWrapper.Key.installationData.rawValue + } + + static func isInstalled(in defaults: UserDefaults) -> Bool { + defaults.object(forKey: installationDataKey) != nil + } +} + +/// Reports (issue #705 follow-up) a fallback-then-recovery fingerprint: install state in BOTH the +/// `.standard` fallback and the App Group suite. Read-only by design (cleanup deferred to a future +/// migration). Runs after re-registration, so it fires on the recovery launch itself and on every +/// cold start while the fingerprint persists. +struct AppGroupStorageTransitionReporter { + + private let localDefaults: UserDefaults + private let sharedDefaults: UserDefaults? + + /// In fallback the active store *is* `.standard`, so there's nothing to compare. + init(activeDefaults: UserDefaults = MBPersistenceStorage.defaults, + localDefaults: UserDefaults = .standard) { + self.localDefaults = localDefaults + self.sharedDefaults = (activeDefaults === localDefaults) ? nil : activeDefaults + } + + @discardableResult + func reportIfNeeded() -> Bool { + guard let sharedDefaults else { return false } + guard MBPersistenceStorage.isInstalled(in: localDefaults), + MBPersistenceStorage.isInstalled(in: sharedDefaults) else { return false } + + Logger.common( + message: "[Storage] Install state found in BOTH the local fallback store and the " + + "App Group suite. The device ran in local-storage fallback (App Group " + + "unavailable) and the App Group has since become available, so the install was " + + "re-registered on the shared container (deviceUUID stays stable, re-derived from IDFA/IDFV; " + + "in-app caps/counters reset).", + level: .info, + category: .general + ) + return true + } +} diff --git a/Mindbox/PersistenceStorage/PersistenceStorage.swift b/Mindbox/PersistenceStorage/PersistenceStorage.swift index df7d067a8..2539745f7 100644 --- a/Mindbox/PersistenceStorage/PersistenceStorage.swift +++ b/Mindbox/PersistenceStorage/PersistenceStorage.swift @@ -75,6 +75,10 @@ protocol PersistenceStorage: AnyObject { /// Version of the WebView localState, managed by JS via localState.init var webViewLocalStateVersion: Int? { get set } + /// Resource hosts observed by webview in-app shows, keyed by endpoint — feeds the next + /// launch's preconnect. See `InAppWebViewLearnedHostsStore`. + var webViewLearnedHosts: [String: [String]]? { get set } + // Reset functions func softReset() @@ -130,5 +134,6 @@ extension PersistenceStorage { operationsDomainFromConfig = nil applicationInstanceId = nil applicationInfoUpdateVersion = nil + webViewLearnedHosts = nil } } diff --git a/Mindbox/Utilities/Migrations/ImplementationOfMigrations/DateFormatMigration.swift b/Mindbox/Utilities/Migrations/ImplementationOfMigrations/DateFormatMigration.swift new file mode 100644 index 000000000..f8aed242d --- /dev/null +++ b/Mindbox/Utilities/Migrations/ImplementationOfMigrations/DateFormatMigration.swift @@ -0,0 +1,151 @@ +// +// DateFormatMigration.swift +// Mindbox +// +// Created by Akylbek Utekeshev on 04.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import MindboxLogger + +/// Converts the persisted date strings written by the previous localized +/// formatter (`dateStyle = .full`, `timeStyle = .full`) into the canonical +/// fixed-pattern `.utc` representation used after the formatter unification. +/// +/// - Important: Ordered first in `MigrationManager`'s migration chain (via the lowest +/// `version`), before any date-consuming migration (e.g. `FirstInitializationDateTimeMigration`, +/// which reads `installationDate` as a parsed `Date`). Without this conversion +/// an upgraded user's date strings stay unparseable by `.utc`, so the parsed +/// `Date` values read as `nil`. `isInstalled` itself is independent of parsing +/// (it checks the presence of the stored string), so it is unaffected — which is +/// why gating the whole chain behind the `isInstalled` guard is safe for upgraded users. +final class DateFormatMigration: MigrationProtocol { + + private let defaults = MBPersistenceStorage.defaults + + /// Raw UserDefaults keys of the six persisted dates, derived from the single source of truth + /// (`MBPersistenceStorage.UserDefaultsWrapper.Key`) so they cannot drift from the stored + /// identifiers. The `` argument is irrelevant: `Key` does not depend on the wrapper's + /// generic parameter. + private let dateKeys: [String] = { + let keys: [MBPersistenceStorage.UserDefaultsWrapper.Key] = [ + .installationData, + .firstInitializationDateTime, + .apnsTokenSaveDate, + .configDownloadDate, + .lastInfoUpdateTime, + .deprecatedEventsRemoveDate + ] + return keys.map(\.rawValue) + }() + + /// Replicas of the removed `MBPersistenceStorage` formatter (`.full`/`.full`), used only + /// to read legacy values written by that formatter on this device — never to write new + /// data (new values go through `.toString(withFormat: .utc)`). + /// + /// A single `.full`/`.full` formatter follows the device's current 12h/24h setting, so a + /// value written under the *other* setting won't parse (the original re-installation bug). + /// We therefore try several candidates that keep the device language/region — so the + /// weekday, month, the "at" literal and the zone name match how the value was written — + /// but pin the hour cycle: + /// - the device locale as-is (matches values written under the current setting); + /// - the device locale forced to 24-hour (`h23`); + /// - the device locale forced to 12-hour (`h12`). + /// The first candidate that parses wins; the instant is then re-stored as `.utc` (24h/UTC), + /// so legacy 12h values are rescued and normalized. + private lazy var legacyFormatters: [DateFormatter] = { + func make(_ identifier: String) -> DateFormatter { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: identifier) + formatter.dateStyle = .full + formatter.timeStyle = .full + return formatter + } + + let base = Locale.current.identifier + // Rebuild the identifier with the hour cycle forced. iOS encodes the device's 12h/24h + // setting straight into `Locale.current` as an `@hours=…` keyword, so naively appending a + // second `hours=` would produce a duplicate (`…@hours=h12;hours=h23`) that ICU resolves to + // the *first* occurrence — silently defeating the override and leaving every candidate on + // the device's own hour cycle. We therefore strip any pre-existing hour-cycle keyword + // (`hours`/`hc`) before adding ours, while preserving the rest (language, region, calendar…). + func identifier(forcingHourCycle hourCycle: String) -> String { + let parts = base.split(separator: "@", maxSplits: 1, omittingEmptySubsequences: false) + let languageAndRegion = String(parts[0]) + var keywords: [(key: String, value: String)] = [] + if parts.count == 2 { + for keyword in parts[1].split(separator: ";") { + let pair = keyword.split(separator: "=", maxSplits: 1) + guard pair.count == 2 else { continue } + keywords.append((String(pair[0]), String(pair[1]))) + } + } + keywords.removeAll { $0.key == "hours" || $0.key == "hc" } + keywords.append(("hours", hourCycle)) + let keywordString = keywords.map { "\($0.key)=\($0.value)" }.joined(separator: ";") + return languageAndRegion + "@" + keywordString + } + + return [ + make(base), + make(identifier(forcingHourCycle: "h23")), + make(identifier(forcingHourCycle: "h12")) + ] + }() + + /// Parses a legacy `.full`/`.full` string with the first candidate formatter that succeeds. + /// Hour cycles don't cross-contaminate: a 24h string (`13:45:45`) is rejected by the 12h + /// formatter and vice versa, so a successful parse always reflects the value's real time. + private func legacyDate(from raw: String) -> Date? { + for formatter in legacyFormatters { + if let date = formatter.date(from: raw) { + return date + } + } + return nil + } + + var description: String { + "Migration converts persisted dates from the legacy localized format to fixed-pattern UTC." + } + + var isNeeded: Bool { + dateKeys.contains { key in + guard let raw = defaults.string(forKey: key) else { return false } + return raw.toDate(withFormat: .utc) == nil && legacyDate(from: raw) != nil + } + } + + /// Negative so the ascending sort in `MigrationManager` places this ahead of the historical + /// chain (`0…`), guaranteeing date strings are normalized before any date-consuming migration + /// (e.g. `FirstInitializationDateTimeMigration`) reads them. + var version: Int { + -1 + } + + func run() throws { + Logger.common(message: "[Migrations] Started DateFormatMigration — scanning \(dateKeys.count) persisted date key(s) for legacy values", level: .info, category: .migration) + + var convertedCount = 0 + for key in dateKeys { + guard let raw = defaults.string(forKey: key) else { continue } + // Already migrated (or natively written in the new format) — leave untouched. + guard raw.toDate(withFormat: .utc) == nil else { + Logger.common(message: "[Migrations] Skip DateFormatMigration key '\(key)' — already in .utc format: '\(raw)'", level: .debug, category: .migration) + continue + } + // Unparseable by any legacy candidate (e.g. the device language changed) — best-effort skip. + guard let date = legacyDate(from: raw) else { + Logger.common(message: "[Migrations] Skip DateFormatMigration key '\(key)' — value not parseable by any legacy formatter: '\(raw)'", level: .error, category: .migration) + continue + } + let newValue = date.toString(withFormat: .utc) + defaults.set(newValue, forKey: key) + convertedCount += 1 + Logger.common(message: "[Migrations] DateFormatMigration converted '\(key)': '\(raw)' → '\(newValue)'", level: .info, category: .migration) + } + + Logger.common(message: "[Migrations] Finished DateFormatMigration — converted \(convertedCount) of \(dateKeys.count) key(s)", level: .info, category: .migration) + } +} diff --git a/Mindbox/Utilities/Migrations/MigrationManager/MigrationManager.swift b/Mindbox/Utilities/Migrations/MigrationManager/MigrationManager.swift index ce99f1fa0..6a13b7c8d 100644 --- a/Mindbox/Utilities/Migrations/MigrationManager/MigrationManager.swift +++ b/Mindbox/Utilities/Migrations/MigrationManager/MigrationManager.swift @@ -51,6 +51,7 @@ final class MigrationManager { self.localSdkVersionCode = Constants.Migration.sdkVersionCode self.migrations = [ + DateFormatMigration(), ShownInAppsIDsMigration(), ShownInAppsDictionaryMigration(), RemoveBackgroundTaskDataMigration(), diff --git a/Mindbox/Utilities/SDKUserAgent.swift b/Mindbox/Utilities/SDKUserAgent.swift new file mode 100644 index 000000000..5fc00e697 --- /dev/null +++ b/Mindbox/Utilities/SDKUserAgent.swift @@ -0,0 +1,23 @@ +// +// SDKUserAgent.swift +// Mindbox +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +/// One User-Agent for every SDK surface — API requests and WebView `applicationName` alike. +/// The backend slices traffic by this string, so transports must not drift apart. All +/// inputs are static per app run, which also keeps a prewarmed WebView indistinguishable +/// from a per-show one. +enum SDKUserAgent { + static func build(utilitiesFetcher: UtilitiesFetcher = DI.injectOrFail(UtilitiesFetcher.self)) -> String { + let sdkVersion = utilitiesFetcher.sdkVersion ?? "unknown" + let appVersion = utilitiesFetcher.appVerson ?? "unknown" + let appName = utilitiesFetcher.hostApplicationName ?? "unknown" + + return "mindbox.sdk/\(sdkVersion) (\(DeviceModelHelper.os) \(DeviceModelHelper.iOSVersion); \(DeviceModelHelper.model)) \(appName)/\(appVersion)" + } +} diff --git a/Mindbox/Utilities/String+Regex.swift b/Mindbox/Utilities/String+Regex.swift index 364bc2994..ca5dcca67 100644 --- a/Mindbox/Utilities/String+Regex.swift +++ b/Mindbox/Utilities/String+Regex.swift @@ -9,12 +9,6 @@ import Foundation extension String { - var operationNameIsValid: Bool { - let range = NSRange(location: 0, length: self.utf16.count) - let regex = try? NSRegularExpression(pattern: "^[A-Za-z0-9\\-\\.]+$") - return regex?.firstMatch(in: self, options: [], range: range) != nil - } - func parseTimeSpanToMillis() throws -> Int64 { let regex = try NSRegularExpression(pattern: "^(-)?((\\d+)\\.)?([01]?\\d|2[0-3]):([0-5]?\\d):([0-5]?\\d)(\\.(\\d{1,7}))?$") let matches = regex.matches(in: self, range: NSRange(self.startIndex..., in: self)) diff --git a/Mindbox/Utilities/UtilitiesFetcher/MBUtilitiesFetcher.swift b/Mindbox/Utilities/UtilitiesFetcher/MBUtilitiesFetcher.swift index 0e33119d5..a54a83cad 100644 --- a/Mindbox/Utilities/UtilitiesFetcher/MBUtilitiesFetcher.swift +++ b/Mindbox/Utilities/UtilitiesFetcher/MBUtilitiesFetcher.swift @@ -29,20 +29,25 @@ class MBUtilitiesFetcher: UtilitiesFetcher { return bundle }() + /// Identifier of the shared App Group container for the SDK's persistent storage + /// (events database + `UserDefaults` suite). + /// + /// Returns `""` when the host bundle id is missing or the container is unavailable, + /// so the SDK falls back to local storage instead of crashing the host (issue #705). + /// Must never trap — not even in Debug: Debug builds on device farms routinely lack a + /// configured App Group, the exact scenario #705 is about. Surfaced as a `.fault` log. var applicationGroupIdentifier: String { guard let hostApplicationName = hostApplicationName else { - fatalError("CFBundleShortVersionString not found for host app") + Logger.common(message: "[MBUtilitiesFetcher] Host application bundle identifier is unavailable", level: .fault, category: .general) + return "" } let identifier = "group.cloud.Mindbox.\(hostApplicationName)" - let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) - guard url != nil else { - #if targetEnvironment(simulator) - return "" - #else - let message = "AppGroup for \(hostApplicationName) not found. Add AppGroup with value: \(identifier)" + guard FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) != nil else { + let message = "App Group '\(identifier)' container is unavailable. " + + "Enable the App Group capability with this exact value on every target (app + extensions). " + + "See https://developers.mindbox.ru/docs/ios-sdk-initialization" Logger.common(message: message, level: .fault, category: .general) - fatalError(message) - #endif + return "" } return identifier } diff --git a/Mindbox/Validators/OperationNameValidator.swift b/Mindbox/Validators/OperationNameValidator.swift new file mode 100644 index 000000000..5078d4a68 --- /dev/null +++ b/Mindbox/Validators/OperationNameValidator.swift @@ -0,0 +1,30 @@ +// +// OperationNameValidator.swift +// Mindbox +// +// Created by Sergei Semko on 08.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +/// Validates Mindbox operation system names: non-empty, only ASCII letters, digits, +/// `-` and `.`. Regex-free scalar scan - no allocations, no per-call regex compile. +/// +/// Intentionally stricter than the legacy `^[A-Za-z0-9\-\.]+$` regex for names with +/// a TRAILING line terminator ("op\n" etc.): ICU `$` matched before a final line +/// terminator, so those were accepted; this scan rejects them. Pinned by +/// `OperationNameValidatorTests`. +enum OperationNameValidator { + static func isValid(_ name: String) -> Bool { + guard !name.isEmpty else { return false } + return name.unicodeScalars.allSatisfy { scalar in + switch scalar { + case "A"..."Z", "a"..."z", "0"..."9", "-", ".": + return true + default: + return false + } + } + } +} diff --git a/MindboxLogger.podspec b/MindboxLogger.podspec index 593198c3a..af7bcd82c 100644 --- a/MindboxLogger.podspec +++ b/MindboxLogger.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "MindboxLogger" - spec.version = "2.15.1" + spec.version = "2.15.2" spec.summary = "SDK for utilities to work with Mindbox" spec.description = "-" spec.homepage = "https://github.com/mindbox-cloud/ios-sdk" diff --git a/MindboxLogger/Shared/Extensions/Date+Extension.swift b/MindboxLogger/Shared/Extensions/Date+Extension.swift index 72174678a..68ac9117b 100644 --- a/MindboxLogger/Shared/Extensions/Date+Extension.swift +++ b/MindboxLogger/Shared/Extensions/Date+Extension.swift @@ -14,28 +14,34 @@ public extension Date { } func toFullString() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - return dateFormatter.string(from: self as Date) + return Date.fullFormatter.string(from: self as Date) } - static var dateFormatter: DateFormatter { + // Built once and reused: a fully-configured DateFormatter is thread-safe for conversion + // (iOS 7+), so there is no need to recreate it on every call. + static let dateFormatter: DateFormatter = { let formatter = DateFormatter() - formatter.dateFormat = "hh:mm:ss.SSSS" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "HH:mm:ss.SSSS" return formatter - } + }() + + private static let fullFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + return formatter + }() func toString(withFormat format: DateFormat) -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = format.value - dateFormatter.timeZone = TimeZone(identifier: "UTC") - return dateFormatter.string(from: self) + return format.string(from: self) } } public extension TimeInterval { private static let readableDateTimeFormatter: DateFormatter = { let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" return formatter }() diff --git a/MindboxLogger/Shared/Extensions/FileManager+Extensions.swift b/MindboxLogger/Shared/Extensions/FileManager+Extensions.swift index 408c51be0..4381117bc 100644 --- a/MindboxLogger/Shared/Extensions/FileManager+Extensions.swift +++ b/MindboxLogger/Shared/Extensions/FileManager+Extensions.swift @@ -9,9 +9,28 @@ import Foundation extension FileManager { - static func storeURL(for appGroup: String, databaseName: String) -> URL { + + /// Error thrown when a shared App Group container cannot be resolved. + enum StoreURLError: LocalizedError, Equatable { + /// The App Group container for `appGroup` is unavailable — typically a missing, + /// misconfigured, or unprovisioned App Group capability. (On a clean Simulator + /// install the container has also been seen to resolve late, though that isn't + /// documented.) + case containerUnavailable(appGroup: String) + + var errorDescription: String? { + switch self { + case .containerUnavailable(let appGroup): + return "App Group container '\(appGroup)' is unavailable. " + + "Set up your AppGroup correctly — it must be the same for all your targets. " + + "Read the documentation: https://developers.mindbox.ru/docs/ios-sdk-initialization" + } + } + } + + static func storeURL(for appGroup: String, databaseName: String) throws -> URL { guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { - fatalError("Container couldn't be created, please set up your AppGroup correctly. It must be the same for all your targets. Read the documentation developers.mindbox.ru/docs/ios-sdk-initialization") + throw StoreURLError.containerUnavailable(appGroup: appGroup) } return fileContainer.appendingPathComponent("\(databaseName).sqlite") diff --git a/MindboxLogger/Shared/Extensions/MBLoggerUtilitiesFetcher.swift b/MindboxLogger/Shared/Extensions/MBLoggerUtilitiesFetcher.swift index f793d5e0d..49cd2ee7a 100644 --- a/MindboxLogger/Shared/Extensions/MBLoggerUtilitiesFetcher.swift +++ b/MindboxLogger/Shared/Extensions/MBLoggerUtilitiesFetcher.swift @@ -16,19 +16,19 @@ class MBLoggerUtilitiesFetcher { return bundle }() - var applicationGroupIdentifier: String { + /// Identifier of the shared App Group container the logger persists its database in, + /// or `nil` when none is available (missing/misconfigured/unprovisioned capability). + /// + /// `nil` makes `LoggerDatabaseLoader` fall back to the app's local caches store, so the + /// logger keeps working instead of being disabled. Must never trap — the SDK must not + /// bring down its host over an unavailable container, on simulator or device (issue #705). + var applicationGroupIdentifier: String? { guard let hostApplicationName = hostApplicationName else { - fatalError("CFBundleShortVersionString not found for host app") + return nil } let identifier = "group.cloud.Mindbox.\(hostApplicationName)" - let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) - guard url != nil else { - #if targetEnvironment(simulator) - return "" - #else - let message = "AppGroup for \(hostApplicationName) not found. Add AppGroup with value: \(identifier)" - fatalError(message) - #endif + guard FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) != nil else { + return nil } return identifier diff --git a/MindboxLogger/Shared/Extensions/String+Extensions.swift b/MindboxLogger/Shared/Extensions/String+Extensions.swift index ecbcb0461..75bba2841 100644 --- a/MindboxLogger/Shared/Extensions/String+Extensions.swift +++ b/MindboxLogger/Shared/Extensions/String+Extensions.swift @@ -7,21 +7,53 @@ import Foundation -public enum DateFormat: String { +public enum DateFormat: String, CaseIterable { case api = "yyyy-MM-dd'T'HH:mm:ss" case utc = "yyyy-MM-dd'T'HH:mm:ss'Z'" + case utcWithMillis = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" var value: String { return self.rawValue } + + // Fixed-pattern formatter: POSIX locale + explicit UTC timezone, 24h `HH`. + private static func makeFormatter(for format: DateFormat) -> DateFormatter { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = format.rawValue + formatter.timeZone = TimeZone(identifier: "UTC") + return formatter + } + + // The set of formats is fixed and tiny, so every formatter is built once, up front. + // `static let` is initialized exactly once (thread-safe), the dictionary is then immutable, + // and each fully-configured `DateFormatter` is safe for concurrent string/date conversion on + // iOS 7+ (min target iOS 12). No lock needed. + private static let formatters: [DateFormat: DateFormatter] = { + var dict = [DateFormat: DateFormatter]() + for format in DateFormat.allCases { + dict[format] = makeFormatter(for: format) + } + return dict + }() + + // Falls back to building on demand instead of force-unwrapping: the dictionary always has an + // entry for every case (built from `allCases`), so the fallback is unreachable in practice. + private var formatter: DateFormatter { + Self.formatters[self] ?? Self.makeFormatter(for: self) + } + + func string(from date: Date) -> String { + formatter.string(from: date) + } + + func date(from string: String) -> Date? { + formatter.date(from: string) + } } public extension String { func toDate(withFormat format: DateFormat) -> Date? { - let dateFormatterGet = DateFormatter() - dateFormatterGet.dateFormat = format.value - dateFormatterGet.timeZone = TimeZone(identifier: "UTC") - - return dateFormatterGet.date(from: self) + return format.date(from: self) } } diff --git a/MindboxLogger/Shared/LoggerRepository/LoggerDatabaseLoader.swift b/MindboxLogger/Shared/LoggerRepository/LoggerDatabaseLoader.swift index dc6b75a98..76ee32644 100644 --- a/MindboxLogger/Shared/LoggerRepository/LoggerDatabaseLoader.swift +++ b/MindboxLogger/Shared/LoggerRepository/LoggerDatabaseLoader.swift @@ -174,7 +174,7 @@ final class LoggerDatabaseLoader: LoggerDatabaseLoading { return explicitURL } if let applicationGroupId = configuration.applicationGroupId { - return FileManager.storeURL(for: applicationGroupId, databaseName: configuration.modelName) + return try FileManager.storeURL(for: applicationGroupId, databaseName: configuration.modelName) } let cachesDirectory = try FileManager.default.url( for: .cachesDirectory, diff --git a/MindboxLoggerTests/DateAndFormatExtensionTests.swift b/MindboxLoggerTests/DateAndFormatExtensionTests.swift new file mode 100644 index 000000000..c554540e0 --- /dev/null +++ b/MindboxLoggerTests/DateAndFormatExtensionTests.swift @@ -0,0 +1,48 @@ +// +// DateAndFormatExtensionTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +/// `Date.toString()` / `toFullString()` and `TimeInterval.asReadableDateTime` use the +/// device-local time zone, so these assert on the literal shape rather than an exact +/// instant. `DateFormatTests` already covers the time-zone-pinned `DateFormat` parsing. +@Suite("Date / TimeInterval / DateFormat helpers", .tags(.dateFormatting)) +struct DateAndFormatExtensionTests { + + private let fixedDate = Date(timeIntervalSince1970: 1_747_017_155) + + @Test("Date.toString() matches HH:mm:ss.SSSS") + func toString() { + let string = fixedDate.toString() + #expect(string.range(of: #"^\d{2}:\d{2}:\d{2}\.\d{4}$"#, options: .regularExpression) != nil, + "unexpected: \(string)") + } + + @Test("Date.toFullString() matches yyyy-MM-dd'T'HH:mm:ssZ") + func toFullString() { + let string = fixedDate.toFullString() + #expect(string.range(of: #"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{4}$"#, options: .regularExpression) != nil, + "unexpected: \(string)") + } + + @Test("TimeInterval.asReadableDateTime matches yyyy-MM-dd HH:mm:ss.SSS") + func asReadableDateTime() { + let string = TimeInterval(1_747_017_155).asReadableDateTime + #expect(string.range(of: #"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$"#, options: .regularExpression) != nil, + "unexpected: \(string)") + } + + @Test("DateFormat.value exposes the literal pattern for every case") + func dateFormatValue() { + #expect(DateFormat.allCases.count == 3) + #expect(DateFormat.api.value == "yyyy-MM-dd'T'HH:mm:ss") + #expect(DateFormat.utc.value == "yyyy-MM-dd'T'HH:mm:ss'Z'") + #expect(DateFormat.utcWithMillis.value == "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX") + } +} diff --git a/MindboxLoggerTests/DateFormatTests.swift b/MindboxLoggerTests/DateFormatTests.swift new file mode 100644 index 000000000..15f1e7942 --- /dev/null +++ b/MindboxLoggerTests/DateFormatTests.swift @@ -0,0 +1,64 @@ +// +// DateFormatTests.swift +// MindboxTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import Testing +@testable import MindboxLogger + +@Suite("DateFormat ISO-8601 primitive", .tags(.dateFormatting)) +struct DateFormatTests { + + private let fixedDate = Date(timeIntervalSince1970: 1_747_017_155) // 2025-05-12T02:32:35Z + + @Test("Date.toString(withFormat: .utc) produces literal yyyy-MM-dd'T'HH:mm:ss'Z'") + func utcFormatLiteral() { + #expect(fixedDate.toString(withFormat: .utc) == "2025-05-12T02:32:35Z") + } + + @Test("Date.toString(withFormat: .api) produces literal yyyy-MM-dd'T'HH:mm:ss") + func apiFormatLiteral() { + #expect(fixedDate.toString(withFormat: .api) == "2025-05-12T02:32:35") + } + + @Test("String.toDate(withFormat: .utc) parses canonical UTC literal") + func utcParseRoundTrip() { + let parsed = "2025-05-12T02:32:35Z".toDate(withFormat: .utc) + #expect(parsed == fixedDate) + } + + @Test("String.toDate(withFormat: .utcWithMillis) parses millisecond-precision payloads") + func millisParse() { + let parsed = "2025-05-12T02:32:35.123Z".toDate(withFormat: .utcWithMillis) + #expect(parsed != nil) + let expected = Date(timeIntervalSince1970: 1_747_017_155.123) + if let parsed { + #expect(abs(parsed.timeIntervalSince(expected)) < 0.001) + } + } + + @Test("Formatter does not silently switch to 12h pattern under 12-hour preference") + func twelveHourPreferenceDoesNotLeakIntoOutput() { + // QA1480: when the user's region preference is 12h, DateFormatter rewrites + // HH:mm:ss into h:mm:ss a unless locale is en_US_POSIX. We assert the + // literal still matches the contract — no AM/PM, no whitespace, leading zero. + let serialized = fixedDate.toString(withFormat: .utc) + #expect(!serialized.contains("AM")) + #expect(!serialized.contains("PM")) + #expect(!serialized.contains(" ")) + #expect(serialized.hasSuffix("Z")) + #expect(serialized.count == 20) + } + + @Test("UTC and millis round-trip via DateTime decoder fallback") + func roundTripFallbackChain() { + let withMillis = "2025-05-12T02:32:35.000Z" + let plain = "2025-05-12T02:32:35Z" + #expect(plain.toDate(withFormat: .utc) != nil) + #expect(withMillis.toDate(withFormat: .utc) == nil) + #expect(withMillis.toDate(withFormat: .utcWithMillis) != nil) + } +} diff --git a/MindboxLoggerTests/FileManagerStoreURLTests.swift b/MindboxLoggerTests/FileManagerStoreURLTests.swift new file mode 100644 index 000000000..65a918c46 --- /dev/null +++ b/MindboxLoggerTests/FileManagerStoreURLTests.swift @@ -0,0 +1,71 @@ +// +// FileManagerStoreURLTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +import CoreData +@testable import MindboxLogger + +/// Regression coverage for App Group store-URL resolution (issue #705). +/// +/// Previously `FileManager.storeURL(for:databaseName:)` called `fatalError` when +/// the shared container was unavailable, which crashed the host straight through +/// the logger's `do/catch`. Now: +/// - `storeURL` *throws* for an unresolvable explicit group (defensive), and +/// - when no App Group is available `MBLoggerUtilitiesFetcher` returns `nil`, so +/// `LoggerDatabaseLoader` falls back to the app's local (caches) store and the +/// logger keeps working — just not in the shared container — instead of +/// crashing or disabling. +/// +/// Note on the empty-string trigger: on the iOS Simulator +/// `containerURL(forSecurityApplicationGroupIdentifier:)` vends a container for any +/// *non-empty* identifier, so only the empty string deterministically yields a +/// `nil` container in a unit test. +@Suite("FileManager.storeURL App Group resolution", .tags(.storage, .storageState)) +struct FileManagerStoreURLTests { + + @Test("storeURL throws .containerUnavailable instead of crashing when the container is unavailable") + func throwsWhenContainerUnavailable() { + #expect(throws: FileManager.StoreURLError.containerUnavailable(appGroup: "")) { + try FileManager.storeURL(for: "", databaseName: "CDLogMessage") + } + } + + @Test("Thrown error carries a localized, actionable description naming the group") + func errorDescriptionNamesTheGroup() throws { + let group = "group.cloud.Mindbox.NonExistent.AppGroup.For.Repro" + let error = FileManager.StoreURLError.containerUnavailable(appGroup: group) + + let description = try #require(error.errorDescription) + #expect(description.contains(group)) + #expect(description.localizedCaseInsensitiveContains("unavailable")) + } + + @Test("Loader falls back to local storage and stays enabled when no App Group is available") + func loaderFallsBackToLocalStorageWhenNoAppGroup() throws { + // Production path when MBLoggerUtilitiesFetcher reports no shared container + // (returns nil): the loader must resolve a local store and keep the logger + // working — not throw, not disable. + let config = LoggerDatabaseLoaderConfig( + modelName: "CDLogMessage", + applicationGroupId: nil, + storeURL: nil, + descriptions: nil + ) + let loader = LoggerDatabaseLoader(config) + // No teardown: this resolves to the real Caches store that + // `MBLoggerCoreDataManager.shared` uses, so destroying it would race sibling suites. + let (container, _) = try loader.loadContainer() + + let stores = container.persistentStoreCoordinator.persistentStores + #expect(!stores.isEmpty) + + let storeURL = try #require(stores.first?.url) + #expect(storeURL.lastPathComponent == "CDLogMessage.sqlite") + #expect(storeURL.path.contains("Caches")) + } +} diff --git a/MindboxLoggerTests/LogPrimitivesTests.swift b/MindboxLoggerTests/LogPrimitivesTests.swift new file mode 100644 index 000000000..5b71e62b3 --- /dev/null +++ b/MindboxLoggerTests/LogPrimitivesTests.swift @@ -0,0 +1,55 @@ +// +// LogPrimitivesTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +@Suite("Log primitives: category, level, writer, message", .tags(.loggingAPI)) +struct LogPrimitivesTests { + + @Test("Every LogCategory exposes a non-empty emoji") + func categoryEmoji() { + #expect(LogCategory.allCases.count == 13) + for category in LogCategory.allCases { + #expect(!category.emoji.isEmpty, "\(category) has no emoji") + } + #expect(LogCategory.general.emoji == "🤖") + #expect(LogCategory.network.emoji == "📡") + } + + @Test("LogLevel emoji, raw values and ordering") + func logLevel() { + #expect(LogLevel.allCases.count == 6) + #expect(LogLevel.none.emoji.isEmpty) + for level in LogLevel.allCases where level != .none { + #expect(!level.emoji.isEmpty, "\(level) has no emoji") + } + + #expect(LogLevel.debug.rawValue == 0) + #expect(LogLevel.none.rawValue == 5) + + #expect(LogLevel.debug < LogLevel.error) + #expect(LogLevel.fault < LogLevel.none) + #expect(!(LogLevel.error < LogLevel.debug)) + } + + @Test("OSLogWriter.writeMessage maps every level to an OSLogType without trapping") + func osLogWriter() { + let writer = OSLogWriter(subsystem: "cloud.Mindbox.UnitTest", category: "Test") + for level in LogLevel.allCases { + writer.writeMessage("message at \(level)", logLevel: level) + } + } + + @Test("LogMessage.description prefixes the UTC timestamp") + func logMessageDescription() { + let date = Date(timeIntervalSince1970: 1_747_017_155) // 2025-05-12T02:32:35Z + let message = LogMessage(timestamp: date, message: "payload") + #expect(message.description == "2025-05-12T02:32:35Z | payload") + } +} diff --git a/MindboxLoggerTests/LogStoreTrimmerTests.swift b/MindboxLoggerTests/LogStoreTrimmerTests.swift new file mode 100644 index 000000000..eb8327d19 --- /dev/null +++ b/MindboxLoggerTests/LogStoreTrimmerTests.swift @@ -0,0 +1,148 @@ +// +// LogStoreTrimmerTests.swift +// MindboxLoggerTests +// +// Created by Sergei Semko on 9/11/25. +// Copyright © 2025 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +@Suite("LogStoreTrimmer policy", .tags(.storage, .trimming)) +struct LogStoreTrimmerTests { + + // MARK: - Fakes + + private final class StubMeasurer: DatabaseSizeMeasuring { + var size: Int + var calls: Int = 0 + init(size: Int) { self.size = size } + func sizeKB() -> Int { calls += 1; return size } + } + + private func makeConfig( + limit: Int = 128, + lowWater: Double = 0.85, + min: Double = 0.05, + max: Double = 0.50, + cooldown: TimeInterval = 10 + ) -> LoggerDBConfig { + LoggerDBConfig( + dbSizeLimitKB: limit, + lowWaterRatio: lowWater, + minDeleteFraction: min, + maxDeleteFraction: max, + batchSize: 15, + writesPerTrimCheck: 5, + trimCooldownSec: cooldown + ) + } + + private func makeTrimmer( + size: Int, + config: LoggerDBConfig? = nil, + start: Date = Date(timeIntervalSince1970: 0) + ) -> (LogStoreTrimmer, StubMeasurer, ManualClock) { + let cfg = config ?? makeConfig() + let measurer = StubMeasurer(size: size) + let clock = ManualClock(start) + let trimmer = LogStoreTrimmer(config: cfg, sizeMeasurer: measurer, clock: clock) + return (trimmer, measurer, clock) + } + + // MARK: - computeTrimFraction + + @Test("Returns nil at or below the limit") + func computeTrimFractionReturnsNilBelowOrEqualLimit() { + let (trimmer, _, _) = makeTrimmer(size: 0, config: makeConfig(limit: 100)) + #expect(trimmer.computeTrimFraction(sizeKB: 100, limitKB: 100) == nil) + #expect(trimmer.computeTrimFraction(sizeKB: 99, limitKB: 100) == nil) + } + + @Test("Clamps to the minimum fraction just over the limit") + func computeTrimFractionRespectsMin() { + // limit=100, lowWater=0.98 -> target=98; size=101 -> raw ≈ 0.0297 < min(0.05) -> 0.05. + let (trimmer, _, _) = makeTrimmer(size: 0, config: makeConfig(limit: 100, lowWater: 0.98, min: 0.05, max: 0.5)) + let fraction = trimmer.computeTrimFraction(sizeKB: 101, limitKB: 100) + #expect(fraction != nil) + #expect(abs((fraction ?? 0) - 0.05) < 1e-9) + } + + @Test("Passes through a raw fraction inside [min, max]") + func computeTrimFractionPassesThrough() { + let (trimmer, _, _) = makeTrimmer(size: 0, config: makeConfig(limit: 100, lowWater: 0.8, min: 0.05, max: 0.5)) + #expect(trimmer.computeTrimFraction(sizeKB: 100, limitKB: 100) == nil) + // raw ≈ (101-80)/101 ≈ 0.2079 + let fraction = trimmer.computeTrimFraction(sizeKB: 101, limitKB: 100) + #expect(abs((fraction ?? 0) - 0.2079) < 1e-3) + } + + @Test("Caps at the maximum fraction when way over the limit") + func computeTrimFractionCapsAtMax() { + let (trimmer, _, _) = makeTrimmer(size: 0, config: makeConfig(limit: 100, lowWater: 0.8, min: 0.05, max: 0.5)) + let fraction = trimmer.computeTrimFraction(sizeKB: 10_000, limitKB: 100) + #expect(abs((fraction ?? 0) - 0.5) < 1e-9) + } + + // MARK: - maybeTrim + + @Test("Uses the precomputed size and never calls the measurer") + func maybeTrimUsesPrecomputedSize() { + let (trimmer, measurer, _) = makeTrimmer(size: 10_000) + var received: Double? + _ = trimmer.maybeTrim(precomputedSizeKB: 129) { received = $0 } + #expect(measurer.calls == 0) + #expect(received != nil) + } + + @Test("Deletes when over the limit, then honours the cooldown window") + func maybeTrimCallsDeleteAndSetsCooldown() { + let cfg = makeConfig(limit: 100, lowWater: 0.8, min: 0.05, max: 0.5, cooldown: 10) + let (trimmer, measurer, clock) = makeTrimmer(size: 120, config: cfg) + + var calls = 0 + _ = trimmer.maybeTrim(precomputedSizeKB: nil) { _ in calls += 1 } + #expect(measurer.calls == 1) + #expect(calls == 1) + + // Under cooldown — must not trim. + _ = trimmer.maybeTrim(precomputedSizeKB: nil) { _ in Issue.record("must not trim under cooldown") } + #expect(calls == 1) + + // 9s in — still under the 10s cooldown. + clock.advance(9) + _ = trimmer.maybeTrim(precomputedSizeKB: nil) { _ in Issue.record("must not trim under cooldown") } + #expect(calls == 1) + + // At 10s the cooldown has elapsed. + clock.advance(1) + _ = trimmer.maybeTrim(precomputedSizeKB: nil) { _ in calls += 1 } + #expect(calls == 2) + } + + @Test("Rethrows the error raised by the delete closure") + func maybeTrimRethrows() { + let (trimmer, _, _) = makeTrimmer(size: 10_000) + enum E: Error { case boom } + #expect(throws: E.self) { + try trimmer.maybeTrim(precomputedSizeKB: nil) { _ in throw E.boom } + } + } + + @Test("resetCooldown allows trimming again immediately") + func resetCooldownAllowsTrimAgain() { + let (trimmer, _, _) = makeTrimmer(size: 10_000) + var count = 0 + _ = trimmer.maybeTrim { _ in count += 1 } + #expect(count == 1) + + _ = trimmer.maybeTrim { _ in count += 1 } // still under cooldown + #expect(count == 1) + + trimmer.resetCooldown() + _ = trimmer.maybeTrim { _ in count += 1 } + #expect(count == 2) + } +} diff --git a/MindboxLoggerTests/LoggerDatabaseLoaderTests.swift b/MindboxLoggerTests/LoggerDatabaseLoaderTests.swift new file mode 100644 index 000000000..8a5779e74 --- /dev/null +++ b/MindboxLoggerTests/LoggerDatabaseLoaderTests.swift @@ -0,0 +1,138 @@ +// +// LoggerDatabaseLoaderTests.swift +// MindboxLoggerTests +// +// Created by Sergei Semko on 9/12/25. +// Copyright © 2025 Mindbox. All rights reserved. +// + +import Testing +import Foundation +import CoreData +@testable import MindboxLogger + +@Suite("LoggerDatabaseLoader", .tags(.storage, .storageState)) +struct LoggerDatabaseLoaderTests { + + // MARK: - Helpers + + private func tmpURL(_ name: String) -> URL { + URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("MB-Loader-\(name)-\(UUID().uuidString).sqlite") + } + + private func sqliteHeader(at url: URL) -> String? { + (try? Data(contentsOf: url).prefix(15)).flatMap { String(data: $0, encoding: .ascii) } + } + + @available(iOS 15.0, *) + @Test("Default description: creates a valid SQLite store and a working background context") + func loadContainerSuccessDefaultDescription() throws { + let url = tmpURL("Success") + let cfg = LoggerDatabaseLoaderConfig(modelName: "CDLogMessage", applicationGroupId: nil, + storeURL: url, descriptions: nil) + let loader = LoggerDatabaseLoader(cfg) + + let (container, ctx) = try loader.loadContainer() + + #expect(FileManager.default.fileExists(atPath: url.path)) + #expect(sqliteHeader(at: url) == "SQLite format 3") + + try ctx.performAndWait { + let entity = try #require(NSEntityDescription.entity(forEntityName: "CDLogMessage", in: ctx)) + let obj = NSManagedObject(entity: entity, insertInto: ctx) + obj.setValue("test", forKey: "message") + obj.setValue(Date(), forKey: "timestamp") + try ctx.save() + } + + #expect(container.persistentStoreDescriptions.first?.url == url) + #expect(container.persistentStoreDescriptions.count == 1) + #expect(container.persistentStoreDescriptions[0].shouldAddStoreAsynchronously == false) + } + + @available(iOS 15.0, *) + @Test("Auto-recreates the store when the existing file is corrupted") + func loadContainerAutoRecreatesOnCorruptedStore() throws { + let url = tmpURL("Corrupted") + + // A broken file + sidecars so the first load attempt fails. + try "NOT A SQLITE DB".data(using: .utf8)!.write(to: url, options: .atomic) + try "WAL".data(using: .utf8)!.write(to: URL(fileURLWithPath: url.path + "-wal")) + try "SHM".data(using: .utf8)!.write(to: URL(fileURLWithPath: url.path + "-shm")) + + let cfg = LoggerDatabaseLoaderConfig(modelName: "CDLogMessage", applicationGroupId: nil, + storeURL: url, descriptions: nil) + let loader = LoggerDatabaseLoader(cfg) + + // First loadStores fails -> catch destroys the store -> successful reload. + let (_, ctx) = try loader.loadContainer() + #expect(sqliteHeader(at: url) == "SQLite format 3") + + try ctx.performAndWait { + let entity = try #require(NSEntityDescription.entity(forEntityName: "CDLogMessage", in: ctx)) + let obj = NSManagedObject(entity: entity, insertInto: ctx) + obj.setValue("after-recreate", forKey: "message") + obj.setValue(Date(), forKey: "timestamp") + try ctx.save() + } + } + + @Test("Honours an explicit store description URL") + func loadContainerUsesExplicitDescriptionURL() throws { + let url = tmpURL("Explicit") + let desc = NSPersistentStoreDescription(url: url) + desc.type = NSSQLiteStoreType + desc.shouldAddStoreAsynchronously = false + + let cfg = LoggerDatabaseLoaderConfig(modelName: "CDLogMessage", applicationGroupId: nil, + storeURL: nil, descriptions: [desc]) + let loader = LoggerDatabaseLoader(cfg) + + let (container, _) = try loader.loadContainer() + + #expect(container.persistentStoreDescriptions.count == 1) + #expect(container.persistentStoreDescriptions.first?.url == url) + #expect(FileManager.default.fileExists(atPath: url.path)) + } + + @Test("Throws .modelNotFound for an unknown model name") + func loadContainerThrowsWhenModelNotFound() { + let cfg = LoggerDatabaseLoaderConfig(modelName: "ModelThatDoesNotExist", applicationGroupId: nil, + storeURL: nil, descriptions: nil) + let loader = LoggerDatabaseLoader(cfg) + + let error = #expect(throws: LoggerDatabaseLoaderError.self) { + try loader.loadContainer() + } + guard case .modelNotFound(let name)? = error else { + Issue.record("Unexpected error: \(String(describing: error))") + return + } + #expect(name == "ModelThatDoesNotExist") + } + + @Test("destroyIfExists removes the store and its -wal/-shm sidecars") + func destroyIfExistsRemovesStoreAndSidecars() throws { + let url = tmpURL("DestroyMe") + let cfg = LoggerDatabaseLoaderConfig(modelName: "CDLogMessage", applicationGroupId: nil, + storeURL: url, descriptions: nil) + let loader = LoggerDatabaseLoader(cfg) + + // Create a store and release all references so the file is not held by Core Data. + try autoreleasepool { + _ = try loader.loadContainer() + } + + let fm = FileManager.default + #expect(fm.fileExists(atPath: url.path)) + _ = fm.createFile(atPath: url.path + "-wal", contents: Data()) + _ = fm.createFile(atPath: url.path + "-shm", contents: Data()) + + try loader.destroyIfExists() + + #expect(fm.fileExists(atPath: url.path) == false) + #expect(fm.fileExists(atPath: url.path + "-wal") == false) + #expect(fm.fileExists(atPath: url.path + "-shm") == false) + } +} diff --git a/MindboxLoggerTests/LoggerPersistenceInternalsTests.swift b/MindboxLoggerTests/LoggerPersistenceInternalsTests.swift new file mode 100644 index 000000000..b7d18d4b8 --- /dev/null +++ b/MindboxLoggerTests/LoggerPersistenceInternalsTests.swift @@ -0,0 +1,51 @@ +// +// LoggerPersistenceInternalsTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +import CoreData +@testable import MindboxLogger + +@Suite("Logger persistence internals", .tags(.storage, .storageState)) +struct LoggerPersistenceInternalsTests { + + @Test("LoggerDatabaseLoaderError.modelNotFound has an actionable description") + func loaderErrorDescription() throws { + let error = LoggerDatabaseLoaderError.modelNotFound(modelName: "CDLogMessage") + let description = try #require(error.errorDescription) + #expect(description.contains("CDLogMessage.momd")) + #expect(description.localizedCaseInsensitiveContains("not found")) + } + + #if DEBUG + @Test("MBLoggerCoreDataManager debug introspection hooks are wired") + func debugIntrospection() { + let manager = MBLoggerCoreDataManager.makeIsolated() + MBLoggerCoreDataManager.waitUntilReady(manager) + + #expect(manager.debugStorageState == .enabled) + #expect(manager.debugLogBufferCount == 0) + #expect(manager.debugLogBufferCapacity >= manager.debugBatchSize) + + // Setters actually persist the value: flip to a distinct value, assert, restore. + let originalState = manager.debugStorageState + manager.debugStorageState = .disabled + #expect(manager.debugStorageState == .disabled) + manager.debugStorageState = originalState + #expect(manager.debugStorageState == originalState) + + let originalContext = manager.debugContext + manager.debugContext = nil + #expect(manager.debugContext == nil) + manager.debugContext = originalContext + #expect(manager.debugContext === originalContext) + + manager.debugWriteBufferToCD() + MBLoggerCoreDataManager.drainQueue(manager) + } + #endif +} diff --git a/MindboxLoggerTests/LoggerStaticAPITests.swift b/MindboxLoggerTests/LoggerStaticAPITests.swift new file mode 100644 index 000000000..a8309be5a --- /dev/null +++ b/MindboxLoggerTests/LoggerStaticAPITests.swift @@ -0,0 +1,81 @@ +// +// LoggerStaticAPITests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +/// `Logger`'s static API is a fire-and-forget logging facade: the methods return +/// `Void` and hand a formatted string to `MBLogger.shared`, so there is no value to +/// assert on. These tests drive every input branch of the message builders; the +/// behavioural contract under test is that each shape is formatted and dispatched +/// without trapping. +@Suite("Logger static API", .tags(.loggingAPI)) +struct LoggerStaticAPITests { + + @Test("error(LoggerErrorModel) traverses the optional description/status/statusCode branches") + func errorLoggerModel() { + Logger.error(LoggerErrorModel(errorType: .server, description: "desc", status: "Failed", statusCode: 500)) + Logger.error(LoggerErrorModel(errorType: .validation)) // no description/status/statusCode + Logger.error(LoggerErrorModel(errorType: .connection, status: "x")) // status only + Logger.error(LoggerErrorModel(errorType: .unknown, statusCode: 1)) // statusCode only + } + + @Test("network(request:) renders method, headers and a UTF-8 body") + func network() { + var request = URLRequest(url: URL(string: "https://api.mindbox.ru/v3/operations?endpoint=test&device=1")!) + request.httpMethod = "POST" + request.allHTTPHeaderFields = ["Authorization": "secret", "Accept": "application/json"] + request.httpBody = #"{"hello":"world"}"#.data(using: .utf8) + Logger.network(request: request, httpAdditionalHeaders: ["X-Extra": "1"]) + + // Minimal request: no method, headers or body. + Logger.network(request: URLRequest(url: URL(string: "https://api.mindbox.ru")!)) + } + + @Test("network(request:) tolerates a non-UTF8 body") + func networkNonUTF8Body() { + var request = URLRequest(url: URL(string: "https://api.mindbox.ru/x")!) + request.httpMethod = "PUT" + request.httpBody = Data([0xFF, 0xFE, 0xFA]) // not valid UTF-8 -> "Can't render body" + Logger.network(request: request) + } + + @Test("response(data:response:error:) renders success, error and empty variants") + func response() { + let url = URL(string: "https://api.mindbox.ru/v3/operations?x=1")! + let http = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)! + + Logger.response(data: #"{"status":"Success"}"#.data(using: .utf8), response: http, error: nil) + Logger.response(data: nil, response: http, error: NSError(domain: "net", code: -1)) // bumps level to .error + Logger.response(data: nil, response: nil, error: nil) // everything nil + Logger.response(data: Data([0x00, 0x01]), response: http, error: nil) // non-JSON body skipped + } + + @Test("common(message:) logs with and without an explicit subsystem") + func common() { + Logger.common(message: "plain message") + Logger.common(message: "with subsystem", level: .info, category: .database, subsystem: "cloud.Mindbox.Test") + } + + @Test("deprecated error(MindboxError) traverses every MindboxError case") + @available(*, deprecated, message: "Intentionally exercises the deprecated Logger.error(_:MindboxError) overload") + func deprecatedErrorMindboxError() { + let proto = ProtocolError(status: .protocolError, errorMessage: "m", httpStatusCode: 500, errorId: "id") + let url = URL(string: "https://api.mindbox.ru")! + + Logger.error(MindboxError.validationError(ValidationError(status: .validationError, validationMessages: []))) + Logger.error(MindboxError.protocolError(proto)) + Logger.error(MindboxError.serverError(proto)) + Logger.error(MindboxError.internalError(InternalError(errorKey: .general, rawError: NSError(domain: "d", code: 1)))) + Logger.error(MindboxError.internalError(InternalError(errorKey: "no-raw-error"))) + Logger.error(MindboxError.invalidResponse(HTTPURLResponse(url: url, statusCode: 500, httpVersion: nil, headerFields: nil)!)) + Logger.error(MindboxError.invalidResponse(nil)) // guard let e else { return } + Logger.error(MindboxError.connectionError) + Logger.error(MindboxError.unknown(NSError(domain: "d", code: 2))) + } +} diff --git a/MindboxLoggerTests/MBLoggerCoreDataManagerTests.swift b/MindboxLoggerTests/MBLoggerCoreDataManagerTests.swift new file mode 100644 index 000000000..5681fd48e --- /dev/null +++ b/MindboxLoggerTests/MBLoggerCoreDataManagerTests.swift @@ -0,0 +1,392 @@ +// +// MBLoggerCoreDataManagerTests.swift +// MindboxLoggerTests +// +// Created by Akylbek Utekeshev on 15.02.2023. +// Copyright © 2023 Mikhail Barilov. All rights reserved. +// + +import Testing +import Foundation +import UIKit +@preconcurrency @testable import MindboxLogger + +/// Serialized so the global `UIApplication` background/foreground notifications these +/// tests post don't interleave *within this suite* (the posts flip `writesImmediately` +/// on every live manager). Note: `.serialized` orders only this suite, not parallel ones. +@Suite("MBLoggerCoreDataManager", .tags(.storage, .storageState), .serialized) +struct MBLoggerCoreDataManagerTests { + + let manager: MBLoggerCoreDataManager + let batchSizeConstant: Int + + init() { + manager = MBLoggerCoreDataManager.makeIsolated() + MBLoggerCoreDataManager.waitUntilReady(manager) + try? manager.deleteAll() + MBLoggerCoreDataManager.drainQueue(manager) + batchSizeConstant = manager.debugBatchSize + } + + // MARK: - CRUD + + @Test("A full batch is flushed and queryable by period") + func createWithBatch() async throws { + let message = "Test message" + let timestamp = Date() + + await create(manager, message: message, timestamp: timestamp) + // Fill the rest of the batch with far-future timestamps so only the first is in range. + let base = Date() + for index in 1.. flush + + let first = try manager.getFirstLog() + #expect(first?.message == "Test message 1") + #expect(first?.timestamp == t1) + } + + @Test("getLastLog returns the newest record") + func fetchLastLog() async throws { + let t1 = Date().addingTimeInterval(-60) + let t2 = Date().addingTimeInterval(-30) + let t3 = Date() + + await create(manager, message: "Test message 1", timestamp: t1) + await create(manager, message: "Test message 2", timestamp: t2) + await createRemaining(basedOn: 3, strategy: .reverseDefault) + await create(manager, message: "Test message 3", timestamp: t3) + + let last = try manager.getLastLog() + #expect(last?.message == "Test message 3") + #expect(last?.timestamp == t3) + } + + @Test("fetchPeriod returns the records inside the window, sorted ascending") + func fetchPeriod() async throws { + let t1 = Date().addingTimeInterval(-60) + let t2 = Date().addingTimeInterval(-30) + let t3 = Date() + + await createRemaining(strategy: .reverse(interval: 100)) // far in the past + await create(manager, message: "Test message 1", timestamp: t1) + await create(manager, message: "Test message 2", timestamp: t2) + await create(manager, message: "Test message 3", timestamp: t3) + await createRemaining(basedOn: 3, strategy: .sequentialDefault) // near future + + let result = try manager.fetchPeriod(t1, t2) + #expect(result.count == 2) + #expect(result[0].message == "Test message 1") + #expect(result[0].timestamp == t1) + #expect(result[1].message == "Test message 2") + #expect(result[1].timestamp == t2) + } + + // MARK: - Background / foreground state + + @Test("didEnterBackground turns on immediate writes (background task is .invalid in tests)") + func enterBackgroundEnablesImmediateWrite() async { + #expect(await onQueue { self.manager.debugWritesImmediately } == false) + + await createMessages(range: 1...(batchSizeConstant / 2), strategy: .sequentialDefault) + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + + #expect(await onQueue { self.manager.debugWritesImmediately } == true) + } + + @Test("flushBufferInBackground persists the buffer and enables immediate writes") + func flushBufferInBackground() async throws { + #expect(await onQueue { self.manager.debugWritesImmediately } == false) + + let half = batchSizeConstant / 2 + await createMessages(range: 1...half, strategy: .sequentialDefault) + + manager.debugFlushBufferInBackground() + await drain(manager) + + let last = try manager.getLastLog() + #expect(last?.message == "Log: \(half)") + #expect(await onQueue { self.manager.debugWritesImmediately } == true) + } + + @Test("The immediate-write flag toggles with background/foreground transitions") + func flagTogglesOnApplicationStateChanges() async { + #expect(await onQueue { self.manager.debugWritesImmediately } == false) + + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + #expect(await onQueue { self.manager.debugWritesImmediately } == true) + + NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) + #expect(await onQueue { self.manager.debugWritesImmediately } == false) + + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + #expect(await onQueue { self.manager.debugWritesImmediately } == true) + } + + @Test("Single-log mode persists each message immediately") + func singleLogModeWritesEachMessageImmediately() async throws { + manager.setImmediateWrite(true) + await drain(manager) + + let count = Int.random(in: 1..= m.debugBatchSize) + } + + @Test("Bootstrap disables storage when the loader fails") + func bootstrapDisabledWhenLoaderFails() { + let failing = MBLoggerCoreDataManager(debug: true, config: .default, loader: AlwaysFailLoader()) + MBLoggerCoreDataManager.waitUntilReady(failing) + + #expect(failing.storageState == .disabled) + #expect(failing.debugHasPersistentStore == false) + #expect(failing.debugContext == nil) + #expect(failing.debugIsStoreLoaded == false) + } +} + +// MARK: - Async helpers + +private extension MBLoggerCoreDataManagerTests { + + enum TimeStrategy { + case none + case sequential(interval: TimeInterval) + case reverse(interval: TimeInterval) + + static let sequentialDefault = TimeStrategy.sequential(interval: 1) + static let reverseDefault = TimeStrategy.reverse(interval: 1) + + func timestamp(baseDate: Date, index: Int) -> Date { + switch self { + case .none: return baseDate + case .sequential(let interval): return baseDate.addingTimeInterval(Double(index) * interval) + case .reverse(let interval): return baseDate.addingTimeInterval(Double(index) * -interval) + } + } + } + + /// Awaits a single `create` completion. + func create(_ m: MBLoggerCoreDataManager, message: String, timestamp: Date) async { + await withCheckedContinuation { (continuation: CheckedContinuation) in + m.create(message: message, timestamp: timestamp) { continuation.resume() } + } + } + + /// Awaits the manager's serial queue draining. + func drain(_ m: MBLoggerCoreDataManager) async { + await withCheckedContinuation { (continuation: CheckedContinuation) in + m.debugSerialQueue.async { continuation.resume() } + } + } + + /// Reads a value on the manager's serial queue (matches the production access pattern). + func onQueue(_ body: @escaping () -> T) async -> T { + await withCheckedContinuation { (continuation: CheckedContinuation) in + manager.debugSerialQueue.async { continuation.resume(returning: body()) } + } + } + + func createMessages(range: R, strategy: TimeStrategy = .none) async where R.Bound == Int { + let baseDate = Date() + for index in range.relative(to: 0.. .debug else { return } + MBLogger.shared.log(level: .debug, message: "below threshold", + date: Date(), category: .general, subsystem: "cloud.Mindbox") + } + + @Test("Public log entry point runs without trapping") + func publicLogDoesNotTrap() { + MBLogger.shared.log(level: .debug, message: "public entry point") + } +} diff --git a/MindboxLoggerTests/MBLoggerUtilitiesFetcherTests.swift b/MindboxLoggerTests/MBLoggerUtilitiesFetcherTests.swift new file mode 100644 index 000000000..37d5e55ae --- /dev/null +++ b/MindboxLoggerTests/MBLoggerUtilitiesFetcherTests.swift @@ -0,0 +1,28 @@ +// +// MBLoggerUtilitiesFetcherTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +/// Regression coverage for issue #705: `MBLoggerUtilitiesFetcher.applicationGroupIdentifier` +/// must NEVER trap. It used to `fatalError` when the shared container was unavailable; it now +/// returns `nil` so `LoggerDatabaseLoader` falls back to the app's local store. +@Suite("MBLoggerUtilitiesFetcher App Group resolution") +struct MBLoggerUtilitiesFetcherTests { + + /// #705 core invariant: the getter must resolve WITHOUT trapping. It used to `fatalError` + /// when the shared container was unavailable; a `fatalError` would tear down the test + /// runner, so this test reaching its assertion at all proves the trap is gone. + @Test + func resolvesWithoutTrapping() { + let id = MBLoggerUtilitiesFetcher().applicationGroupIdentifier + if let id { + #expect(id.hasPrefix("group.cloud.Mindbox.")) + } + } +} diff --git a/MindboxLoggerTests/MindboxErrorModelsTests.swift b/MindboxLoggerTests/MindboxErrorModelsTests.swift new file mode 100644 index 000000000..9fec02fd5 --- /dev/null +++ b/MindboxLoggerTests/MindboxErrorModelsTests.swift @@ -0,0 +1,115 @@ +// +// MindboxErrorModelsTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +@Suite("Error & status model types", .tags(.errorHandling)) +struct MindboxErrorModelsTests { + + // MARK: - ProtocolError + + @Test("ProtocolError.description includes the errorId when present") + func protocolErrorDescriptionWithId() { + let error = ProtocolError(status: .protocolError, errorMessage: "msg", httpStatusCode: 418, errorId: "uuid-1") + let description = error.description + #expect(description.contains("Status code: 418")) + #expect(description.contains("Message: msg")) + #expect(description.contains("ErrorID: uuid-1")) + } + + @Test("ProtocolError.description omits the errorId when nil") + func protocolErrorDescriptionWithoutId() { + let error = ProtocolError(status: .protocolError, errorMessage: "msg", httpStatusCode: 400, errorId: nil) + #expect(!error.description.contains("ErrorID")) + } + + @Test("ProtocolError decodes from JSON, including the optional errorId") + func protocolErrorDecodes() throws { + let withId = #"{"httpStatusCode":500,"status":"ProtocolError","errorMessage":"oops","errorId":"id-9"}"# + let decoded = try JSONDecoder().decode(ProtocolError.self, from: Data(withId.utf8)) + #expect(decoded.httpStatusCode == 500) + #expect(decoded.status == .protocolError) + #expect(decoded.errorMessage == "oops") + #expect(decoded.errorId == "id-9") + + let withoutId = #"{"httpStatusCode":404,"status":"ProtocolError","errorMessage":"nope"}"# + let noId = try JSONDecoder().decode(ProtocolError.self, from: Data(withoutId.utf8)) + #expect(noId.errorId == nil) + } + + // MARK: - ValidationError + + @Test("ValidationError.description joins field messages and tolerates nils") + func validationErrorDescription() { + let error = ValidationError(status: .validationError, validationMessages: [ + ValidationMessage(message: "Required", location: "email"), + ValidationMessage(message: nil, location: nil), + ]) + let description = error.description + #expect(description.contains("Field email error. Message: Required")) + #expect(description.contains("Field no location error. Message: no message")) + #expect(description.contains(";\n")) + } + + @Test("ValidationError round-trips through Codable") + func validationErrorCodable() throws { + let json = #"{"status":"ValidationError","validationMessages":[{"message":"m","location":"l"}]}"# + let decoded = try JSONDecoder().decode(ValidationError.self, from: Data(json.utf8)) + #expect(decoded.status == .validationError) + #expect(decoded.validationMessages.count == 1) + #expect(decoded.validationMessages.first?.location == "l") + #expect(decoded.validationMessages.first?.message == "m") + } + + // MARK: - Status + + @Test(arguments: zip( + [Status.success, .validationError, .protocolError, .internalServerError, .transactionAlreadyProcessed, .unknown], + ["Success", "ValidationError", "ProtocolError", "InternalServerError", "TransactionAlreadyProcessed", "unknown"])) + func statusRawValue(_ status: Status, _ raw: String) { + #expect(status.rawValue == raw) + } + + // MARK: - LoggerErrorModel / LoggerErrorType + + @Test("LoggerErrorModel stores all fields") + func loggerErrorModel() { + let model = LoggerErrorModel(errorType: .server, description: "d", status: "s", statusCode: 500, errorKey: "k") + #expect(model.errorType == .server) + #expect(model.description == "d") + #expect(model.status == "s") + #expect(model.statusCode == 500) + #expect(model.errorKey == "k") + } + + @Test(arguments: zip( + [LoggerErrorType.validation, .protocol, .server, .internal, .invalid, .connection, .unknown], + ["validation", "protocol", "server", "internal", "invalid", "connection", "unknown"])) + func loggerErrorTypeRawValue(_ type: LoggerErrorType, _ raw: String) { + #expect(type.rawValue == raw) + } + + // MARK: - SDKLogsStatus + + @Test("SDKLogsStatus.value renders each case") + func sdkLogsStatusValue() { + #expect(SDKLogsStatus.ok.value == "OK") + #expect(SDKLogsStatus.noData.value == "No data found") + #expect(SDKLogsStatus.elderLog(date: "2025").value == "No data found. The elder log has date: 2025") + #expect(SDKLogsStatus.latestLog(date: "2026").value == "No data found. The latest log has date: 2026") + #expect(SDKLogsStatus.largeSize.value == "The requested log size is too large") + } + + @Test("SDKLogsStatus is Equatable on its associated values") + func sdkLogsStatusEquatable() { + #expect(SDKLogsStatus.elderLog(date: "a") == .elderLog(date: "a")) + #expect(SDKLogsStatus.elderLog(date: "a") != .elderLog(date: "b")) + #expect(SDKLogsStatus.ok != .noData) + } +} diff --git a/MindboxLoggerTests/MindboxErrorTests.swift b/MindboxLoggerTests/MindboxErrorTests.swift new file mode 100644 index 000000000..c8910ac44 --- /dev/null +++ b/MindboxLoggerTests/MindboxErrorTests.swift @@ -0,0 +1,174 @@ +// +// MindboxErrorTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +@Suite("MindboxError, InternalError & ErrorKey", .tags(.errorHandling)) +struct MindboxErrorTests { + + // MARK: - Fixtures + + /// Minimal `CodingKey` for building `DecodingError` values with a coding path. + private struct CK: CodingKey { + let stringValue: String + let intValue: Int? + init(stringValue: String) { self.stringValue = stringValue; self.intValue = nil } + init(intValue: Int) { self.stringValue = "\(intValue)"; self.intValue = intValue } + } + + private func makeProtocolError(errorId: String? = "err-id") -> ProtocolError { + ProtocolError(status: .protocolError, errorMessage: "boom", httpStatusCode: 500, errorId: errorId) + } + + private func makeValidationError() -> ValidationError { + ValidationError(status: .validationError, + validationMessages: [ValidationMessage(message: "msg", location: "loc")]) + } + + private func makeHTTPResponse(_ code: Int) -> HTTPURLResponse { + HTTPURLResponse(url: URL(string: "https://api.mindbox.ru/x")!, + statusCode: code, httpVersion: "HTTP/1.1", headerFields: nil)! + } + + /// `createJSON()` must always emit parseable JSON; returns the decoded envelope. + private func envelope(_ error: MindboxError) throws -> [String: Any] { + let data = try #require(error.createJSON().data(using: .utf8)) + return try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any]) + } + + // MARK: - errorDescription + + @Test("errorDescription routes every case to the right underlying message") + func errorDescription() { + let validation = makeValidationError() + #expect(MindboxError.validationError(validation).errorDescription == validation.description) + + let proto = makeProtocolError() + #expect(MindboxError.protocolError(proto).errorDescription == proto.description) + #expect(MindboxError.serverError(proto).errorDescription == proto.description) + + let internalErr = InternalError(errorKey: .general, reason: "r") + #expect(MindboxError.internalError(internalErr).errorDescription == internalErr.description) + + let underlying = NSError(domain: "d", code: 7) + #expect(MindboxError.unknown(underlying).errorDescription == underlying.localizedDescription) + + #expect(MindboxError.invalidResponse(makeHTTPResponse(404)).errorDescription?.contains("An invalid response") == true) + #expect(MindboxError.invalidResponse(nil).errorDescription?.contains("No response") == true) + #expect(MindboxError.connectionError.errorDescription?.contains("internet connection") == true) + } + + // MARK: - failureReason + + @Test("failureReason covers every case") + func failureReason() { + #expect(MindboxError.serverError(makeProtocolError()).failureReason == "boom") + #expect(MindboxError.internalError(InternalError(errorKey: .general, reason: "rr")).failureReason == "rr") + #expect(MindboxError.validationError(makeValidationError()).failureReason == "Validation error") + #expect(MindboxError.protocolError(makeProtocolError()).failureReason == "boom") + #expect(MindboxError.unknown(NSError(domain: "d", code: 1)).failureReason == "Unknown error") + #expect(MindboxError.invalidResponse(nil).failureReason == "Invalid response") + #expect(MindboxError.connectionError.failureReason == "Connection error") + } + + // MARK: - errorKey + + @Test("errorKey is only present for internalError") + func errorKey() { + #expect(MindboxError.internalError(InternalError(errorKey: "K")).errorKey == "K") + #expect(MindboxError.connectionError.errorKey == nil) + #expect(MindboxError.protocolError(makeProtocolError()).errorKey == nil) + } + + // MARK: - init + + @Test("init(_:) wraps an InternalError") + func initWrapsInternalError() { + let error = MindboxError(InternalError(errorKey: "wrapped")) + #expect(error.errorKey == "wrapped") + guard case .internalError = error else { + Issue.record("expected .internalError, got \(error)") + return + } + } + + // MARK: - createJSON + + @Test("createJSON emits valid JSON with the correct envelope type for every case") + func createJSON() throws { + #expect(try envelope(.validationError(makeValidationError()))["type"] as? String == "MindboxError") + #expect(try envelope(.protocolError(makeProtocolError(errorId: nil)))["type"] as? String == "MindboxError") + #expect(try envelope(.serverError(makeProtocolError()))["type"] as? String == "MindboxError") + #expect(try envelope(.serverError(makeProtocolError(errorId: nil)))["type"] as? String == "MindboxError") + #expect(try envelope(.internalError(InternalError(errorKey: "k")))["type"] as? String == "InternalError") + #expect(try envelope(.internalError(InternalError(errorKey: .general, reason: "r")))["type"] as? String == "InternalError") + #expect(try envelope(.invalidResponse(makeHTTPResponse(500)))["type"] as? String == "NetworkError") + + let plainResponse = URLResponse(url: URL(string: "https://x")!, mimeType: nil, + expectedContentLength: 0, textEncodingName: nil) + #expect(try envelope(.invalidResponse(plainResponse))["type"] as? String == "NetworkError") + #expect(try envelope(.invalidResponse(nil))["type"] as? String == "NetworkError") + #expect(try envelope(.connectionError)["type"] as? String == "NetworkError") + #expect(try envelope(.unknown(NSError(domain: "d", code: 1)))["type"] as? String == "InternalError") + } + + // MARK: - InternalError.description + + @Test("InternalError.description renders DecodingError variants") + func internalErrorDescriptionDecodingErrors() { + let typeMismatch = InternalError( + errorKey: .parsing, + rawError: DecodingError.typeMismatch(Int.self, .init(codingPath: [CK(stringValue: "field")], + debugDescription: "d"))) + #expect(typeMismatch.description.contains("Type Mismatch: key \"field\"")) + + let valueNotFound = InternalError( + errorKey: .parsing, + rawError: DecodingError.valueNotFound(String.self, .init(codingPath: [], debugDescription: "d"))) + #expect(valueNotFound.description.contains("Value Not Found")) + + let keyNotFound = InternalError( + errorKey: .parsing, + rawError: DecodingError.keyNotFound(CK(stringValue: "k"), .init(codingPath: [], debugDescription: "d"))) + #expect(keyNotFound.description.contains("Key Not Found")) + + let dataCorrupted = InternalError( + errorKey: .parsing, + rawError: DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "d"))) + #expect(dataCorrupted.description.contains("Data Corrupted")) + } + + @Test("InternalError.description renders non-decoding errors and optional fields") + func internalErrorDescriptionFields() { + let nonDecoding = InternalError(errorKey: .general, rawError: NSError(domain: "x", code: 1)) + #expect(nonDecoding.description.contains("Error description:")) + + let withStatus = InternalError(errorKey: "k", statusCode: 503) + #expect(withStatus.description.contains("Status code: 503")) + + let withReason = InternalError(errorKey: .general, reason: "why", suggestion: "do this") + #expect(withReason.description.contains("Reason: why")) + #expect(withReason.description.contains("Suggestion: do this")) + + let bare = InternalError(errorKey: "only-key") + #expect(bare.description.contains("Error Key: only-key")) + #expect(!bare.description.contains("Status code")) + #expect(!bare.description.contains("Reason")) + } + + // MARK: - ErrorKey + + @Test(arguments: zip( + [ErrorKey.general, .parsing, .invalidConfiguration, .unknownStatusKey, .serverError, .invalidAccess, .validation], + ["Error_general", "Error_parsing", "Invalid_Configuration", "Error_unknown_status_key", + "Server_error", "Invalid_Access", "Error_validation"])) + func errorKeyRawValue(_ key: ErrorKey, _ raw: String) { + #expect(key.rawValue == raw) + } +} diff --git a/MindboxTests/MindboxLogger/Mocks/IsolatedMBLoggerCoreDataManager.swift b/MindboxLoggerTests/Mocks/IsolatedMBLoggerCoreDataManager.swift similarity index 100% rename from MindboxTests/MindboxLogger/Mocks/IsolatedMBLoggerCoreDataManager.swift rename to MindboxLoggerTests/Mocks/IsolatedMBLoggerCoreDataManager.swift diff --git a/MindboxTests/MindboxLogger/Mocks/TestContainers.swift b/MindboxLoggerTests/Mocks/TestContainers.swift similarity index 100% rename from MindboxTests/MindboxLogger/Mocks/TestContainers.swift rename to MindboxLoggerTests/Mocks/TestContainers.swift diff --git a/MindboxLoggerTests/NSManagedObjectContextExtensionTests.swift b/MindboxLoggerTests/NSManagedObjectContextExtensionTests.swift new file mode 100644 index 000000000..6bd1a24c8 --- /dev/null +++ b/MindboxLoggerTests/NSManagedObjectContextExtensionTests.swift @@ -0,0 +1,51 @@ +// +// NSManagedObjectContextExtensionTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +import CoreData +@testable import MindboxLogger + +@Suite("NSManagedObjectContext perform helpers", .tags(.storage)) +struct NSManagedObjectContextExtensionTests { + + private struct TestError: Error {} + + private func makeContext() -> NSManagedObjectContext { + NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + } + + @Test("executePerformAndWait returns the block result") + func executeReturnsValue() { + let context = makeContext() + let result = context.executePerformAndWait { 21 + 21 } + #expect(result == 42) + } + + @Test("executePerformAndWait rethrows the block error") + func executeRethrows() { + let context = makeContext() + #expect(throws: TestError.self) { + try context.executePerformAndWait { throw TestError() } + } + } + + @Test("mindboxPerformAndWait returns the block result through the helper") + func mindboxReturnsValue() { + let context = makeContext() + let result = context.mindboxPerformAndWait { "ok" } + #expect(result == "ok") + } + + @Test("mindboxPerformAndWait rethrows the block error through the rescue path") + func mindboxRethrows() { + let context = makeContext() + #expect(throws: TestError.self) { + try context.mindboxPerformAndWait { throw TestError() } + } + } +} diff --git a/MindboxLoggerTests/Tag+Extensions.swift b/MindboxLoggerTests/Tag+Extensions.swift new file mode 100644 index 000000000..c3fef826e --- /dev/null +++ b/MindboxLoggerTests/Tag+Extensions.swift @@ -0,0 +1,28 @@ +// +// Tag+Extensions.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import Testing + +extension Tag { + /// Date / time-interval formatting helpers. + @Tag static var dateFormatting: Self + /// The logging facade itself: `Logger` / `MBLogger` entry points, `OSLogWriter`, + /// log levels, categories and `LogMessage` rendering. + @Tag static var loggingAPI: Self + /// Error & decodable model types: `MindboxError`, `ProtocolError`, + /// `ValidationError`, `LoggerErrorModel`, `UnknownDecodable`, `Status`. + @Tag static var errorHandling: Self + /// Core Data persistence stack: the CRUD manager, database loader, persistent + /// container, SQLite size measurer, context helper and store-URL resolution. + @Tag static var storage: Self + /// Enable / disable & graceful-degradation behaviour: bootstrap state, + /// App Group fallback, and storage-state introspection. + @Tag static var storageState: Self + /// Log retention / size-limit trimming policy. + @Tag static var trimming: Self +} diff --git a/MindboxLoggerTests/URLExtensionTests.swift b/MindboxLoggerTests/URLExtensionTests.swift new file mode 100644 index 000000000..0399f46fb --- /dev/null +++ b/MindboxLoggerTests/URLExtensionTests.swift @@ -0,0 +1,34 @@ +// +// URLExtensionTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +@Suite("URL file attributes", .tags(.storage)) +struct URLExtensionTests { + + @Test("fileSize and attributes reflect an existing file") + func existingFile() throws { + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("mb-url-\(UUID().uuidString).bin") + try Data(repeating: 0xAB, count: 1234).write(to: url) + defer { try? FileManager.default.removeItem(at: url) } + + #expect(url.fileSize == 1234) + let attributes = try #require(url.attributes) + #expect(attributes[.size] != nil) + } + + @Test("fileSize is zero and attributes nil for a missing file") + func missingFile() { + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("mb-missing-\(UUID().uuidString).bin") + #expect(url.fileSize == 0) + #expect(url.attributes == nil) + } +} diff --git a/MindboxLoggerTests/UnknownDecodableTests.swift b/MindboxLoggerTests/UnknownDecodableTests.swift new file mode 100644 index 000000000..a78201aaa --- /dev/null +++ b/MindboxLoggerTests/UnknownDecodableTests.swift @@ -0,0 +1,63 @@ +// +// UnknownDecodableTests.swift +// MindboxLoggerTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxLogger + +/// Exercises the `UnknownDecodable` default initializer for `RawRepresentable` +/// string enums through both a type that provides an `unknown` fallback (`Status`) +/// and one that does not (`NoFallback`), to hit every branch. +@Suite("UnknownDecodable RawRepresentable conformance", .tags(.errorHandling)) +struct UnknownDecodableTests { + + private struct StatusWrapper: Decodable { let value: Status } + + private enum NoFallback: String, UnknownCodable { + case alpha + case beta + } + private struct NoFallbackWrapper: Decodable { let value: NoFallback } + + private func decodeStatus(_ jsonValue: String) throws -> Status { + try JSONDecoder().decode(StatusWrapper.self, from: Data(#"{"value":\#(jsonValue)}"#.utf8)).value + } + + private func decodeNoFallback(_ jsonValue: String) throws -> NoFallback { + try JSONDecoder().decode(NoFallbackWrapper.self, from: Data(#"{"value":\#(jsonValue)}"#.utf8)).value + } + + @Test("A known raw value decodes to its case") + func knownValue() throws { + #expect(try decodeStatus("\"Success\"") == .success) + #expect(try decodeNoFallback("\"alpha\"") == .alpha) + } + + @Test("An unknown raw value falls back to .unknown when the type provides one") + func unknownValueWithFallback() throws { + #expect(try decodeStatus("\"DefinitelyNotAStatus\"") == .unknown) + } + + @Test("A non-string payload falls back to .unknown when the type provides one") + func wrongTypeWithFallback() throws { + #expect(try decodeStatus("123") == .unknown) + } + + @Test("An unknown raw value throws .unknownValue when the type has no fallback") + func unknownValueNoFallback() { + #expect(throws: UnknownDecodableError.self) { + _ = try decodeNoFallback("\"gamma\"") + } + } + + @Test("A non-string payload rethrows the decoding error when the type has no fallback") + func wrongTypeNoFallback() { + #expect(throws: DecodingError.self) { + _ = try decodeNoFallback("123") + } + } +} diff --git a/MindboxNotifications.podspec b/MindboxNotifications.podspec index a3e68cef2..546412def 100644 --- a/MindboxNotifications.podspec +++ b/MindboxNotifications.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "MindboxNotifications" - spec.version = "2.15.1" + spec.version = "2.15.2" spec.summary = "SDK for integration notifications with Mindbox" spec.description = "This library allows you to integrate notifications and transfer them to Mindbox Marketing Cloud" spec.homepage = "https://github.com/mindbox-cloud/ios-sdk" diff --git a/MindboxNotificationsTests/ImageFormatTests.swift b/MindboxNotificationsTests/ImageFormatTests.swift new file mode 100644 index 000000000..ccec19846 --- /dev/null +++ b/MindboxNotificationsTests/ImageFormatTests.swift @@ -0,0 +1,56 @@ +// +// ImageFormatTests.swift +// MindboxNotificationsTests +// +// Created by Sergei Semko on 6/16/26. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxNotifications + +@Suite("ImageFormat", .tags(.notifications, .imageFormat)) +struct ImageFormatTests { + + @Test("Detects the format from the leading magic byte", arguments: [ + UInt8(0x89): ImageFormat.png, + 0xFF: .jpg, + 0x47: .gif + ]) + func detectsFormatFromFirstByte(firstByte: UInt8, expected: ImageFormat) { + let data = Data([firstByte, 0x00, 0x01]) + #expect(ImageFormat(data) == expected) + #expect(ImageFormat.get(from: data) == expected) + } + + @Test("Returns nil for an unrecognized leading byte", arguments: [ + UInt8(0x00), 0x42, 0x7F, 0xAB, 0xCC + ]) + func returnsNilForUnknownByte(firstByte: UInt8) { + let data = Data([firstByte, 0x10]) + #expect(ImageFormat(data) == nil) + #expect(ImageFormat.get(from: data) == nil) + } + + @Test("Returns nil for empty data") + func returnsNilForEmptyData() { + #expect(ImageFormat(Data()) == nil) + #expect(ImageFormat.get(from: Data()) == nil) + } + + @Test("Only the first byte determines the format") + func onlyFirstByteMatters() { + #expect(ImageFormat(Data([0x89, 0xFF, 0x47, 0x00])) == .png) + } + + @Test("extension equals the raw value", arguments: [ + ImageFormat.png: "png", + .jpg: "jpg", + .gif: "gif" + ]) + func extensionMatchesRawValue(format: ImageFormat, expected: String) { + #expect(format.extension == expected) + #expect(format.rawValue == expected) + } +} diff --git a/MindboxNotificationsTests/Info.plist b/MindboxNotificationsTests/Info.plist deleted file mode 100644 index 64d65ca49..000000000 --- a/MindboxNotificationsTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/MindboxNotificationsTests/MindboxNotificationContentTests.swift b/MindboxNotificationsTests/MindboxNotificationContentTests.swift index 2607e6609..53e41a6e3 100644 --- a/MindboxNotificationsTests/MindboxNotificationContentTests.swift +++ b/MindboxNotificationsTests/MindboxNotificationContentTests.swift @@ -6,138 +6,81 @@ // Copyright © 2024 Mindbox. All rights reserved. // -import XCTest +import Testing +import Foundation +import UIKit +import UserNotifications @testable import MindboxNotifications -// swiftlint:disable force_unwrapping force_try - -@available(iOS 13.0, *) -final class MindboxNotificationContentTests: XCTestCase { - - var service: MindboxNotificationService! // MindboxNotificationContentProtocol - var mockViewController: UIViewController! - var mockExtensionContext: NSExtensionContext! - var mockNotificationRequest: UNNotificationRequest! - - override func setUp() { - super.setUp() - service = MindboxNotificationService() - mockViewController = UIViewController() - mockExtensionContext = MockExtensionContext() - - let aps: [AnyHashable: Any] = [ - "mutable-content": 1, - "alert": [ - "title": "Test title", - "body": "Test description" - ], - "content-available": 1, - "sound": "default" - ] - - let userInfo: [AnyHashable: Any] = [ - "clickUrl": "https://mindbox.ru/", - "payload": "{\n \"payload\": \"data\"\n}", - "uniqueKey": "4cccb64d-ba46-41eb-9699-3a706f2b910b", - "imageUrl": "https://mobpush-images.mindbox.ru/Mpush-test/63/5933f4cd-47e3-4317-9237-bc5aad291aa9.png", - "buttons": [ - [ - "url": "https://developers.mindbox.ru/docs/mindbox-sdk", - "text": "Documentation", - "uniqueKey": "1b112bcd-5eae-4914-8842-d77198466466" - ], - [ - "url": "https://google.com", - "text": "Button #1", - "uniqueKey": "cff05f38-6df4-4a10-9859-ea3bf0a65068" - ] - ], - "aps": aps - ] - - let content = UNMutableNotificationContent() - content.userInfo = userInfo - content.title = "Test title" - content.body = "Test description" - - let image = UIImage(systemName: "star")! - let imageData = image.pngData()! - let tempDirectory = FileManager.default.temporaryDirectory - let imageFileURL = tempDirectory.appendingPathComponent("testImage.png") - - try! imageData.write(to: imageFileURL) - let notificationAttachment = try! UNNotificationAttachment(identifier: "identifier", url: imageFileURL, options: nil) - - content.attachments.append(notificationAttachment) - - mockNotificationRequest = UNNotificationRequest(identifier: "test", content: content, trigger: nil) - } - - override func tearDown() { - service = nil - mockViewController = nil - mockExtensionContext = nil - mockNotificationRequest = nil - super.tearDown() - } +@MainActor +@Suite("MindboxNotificationService (NotificationContent extension)", .tags(.notifications, .notificationContent)) +struct MindboxNotificationContentTests { - func testDidReceiveFromMindboxNotificationContentProtocol() { - let mockNotification = MockUNNotification(request: mockNotificationRequest) + let service = MindboxNotificationService() + let viewController = UIViewController() - XCTAssertFalse(mockNotification.request.content.attachments.isEmpty) + @Test("didReceive wires up the view controller, image view and button actions") + func didReceiveBuildsContentImageAndActions() throws { + let content = NotificationTestFixtures.makeContent( + userInfo: NotificationTestFixtures.currentFormatUserInfo(), + title: "Test title", + body: "Test description" + ) + content.attachments = [try NotificationTestFixtures.makeImageAttachment()] + let request = UNNotificationRequest(identifier: "test", content: content, trigger: nil) + let notification = MockUNNotification(request: request) + let context = MockExtensionContext() - service.didReceive(notification: mockNotification, - viewController: mockViewController, - extensionContext: mockExtensionContext) + #expect(!notification.request.content.attachments.isEmpty) - XCTAssertEqual(service.viewController, mockViewController) - XCTAssertEqual(service.context, mockExtensionContext) + service.didReceive(notification: notification, viewController: viewController, extensionContext: context) - XCTAssertFalse(service.context!.notificationActions.isEmpty) - XCTAssertEqual(service.context!.notificationActions.count, 2) + #expect(service.viewController === viewController) + #expect(service.context === context) - let actionTitles = service.context!.notificationActions.map { $0.title } - XCTAssertTrue(actionTitles.contains("Documentation")) - XCTAssertTrue(actionTitles.contains("Button #1")) + #expect(service.context?.notificationActions.count == 2) + let actionTitles = service.context?.notificationActions.map { $0.title } + #expect(actionTitles?.contains("Documentation") == true) + #expect(actionTitles?.contains("Button #1") == true) - let imageView = mockViewController.view.subviews.first { $0 is UIImageView } as? UIImageView - XCTAssertNotNil(imageView, "The UIImageView should be added to the ViewController") + let imageView = viewController.view.subviews.first { $0 is UIImageView } + #expect(imageView != nil, "The UIImageView should be added to the view controller") } -} -@available(iOS 12.0, *) -fileprivate class MockExtensionContext: NSExtensionContext { - var actions: [UNNotificationAction] = [] - - override var notificationActions: [UNNotificationAction] { - get { - return actions - } - set { - actions = newValue - } - } -} + @Test("didReceive creates actions but no image view when there is no attachment") + func didReceiveButtonsNoAttachment() { + let request = NotificationTestFixtures.makeRequest(userInfo: NotificationTestFixtures.currentFormatUserInfo()) + let notification = MockUNNotification(request: request) + let context = MockExtensionContext() -@available(iOS 12.0, *) -fileprivate class MockUNNotification: UNNotification { - private let mockRequest: UNNotificationRequest + service.didReceive(notification: notification, viewController: viewController, extensionContext: context) - init(request: UNNotificationRequest) { - self.mockRequest = request + #expect(service.context?.notificationActions.count == 2) + let hasImageView = viewController.view.subviews.contains { $0 is UIImageView } + #expect(!hasImageView) + } - let data = try! NSKeyedArchiver.archivedData(withRootObject: request, requiringSecureCoding: true) + @Test("didReceive creates no actions when the payload has no buttons") + func didReceiveNoButtons() { + let request = NotificationTestFixtures.makeRequest( + userInfo: NotificationTestFixtures.currentFormatUserInfo(includeButtons: false) + ) + let notification = MockUNNotification(request: request) + let context = MockExtensionContext() - let coder = try! NSKeyedUnarchiver(forReadingFrom: data) + service.didReceive(notification: notification, viewController: viewController, extensionContext: context) - super.init(coder: coder)! + #expect(service.context?.notificationActions.isEmpty == true) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + @Test("didReceive without an extension context creates no actions and does not crash") + func didReceiveNilContext() { + let request = NotificationTestFixtures.makeRequest(userInfo: NotificationTestFixtures.currentFormatUserInfo()) + let notification = MockUNNotification(request: request) + + service.didReceive(notification: notification, viewController: viewController, extensionContext: nil) - override var request: UNNotificationRequest { - return mockRequest + #expect(service.context == nil) + #expect(service.viewController === viewController) } } diff --git a/MindboxNotificationsTests/MindboxNotificationServiceTests.swift b/MindboxNotificationsTests/MindboxNotificationServiceTests.swift index 4b191b517..07774fe1f 100644 --- a/MindboxNotificationsTests/MindboxNotificationServiceTests.swift +++ b/MindboxNotificationsTests/MindboxNotificationServiceTests.swift @@ -6,97 +6,152 @@ // Copyright © 2024 Mindbox. All rights reserved. // -import XCTest +import Testing +import Foundation +import UserNotifications @testable import MindboxNotifications -// swiftlint:disable force_unwrapping - -final class MindboxNotificationServiceTests: XCTestCase { - - var service: MindboxNotificationServiceProtocol! - var mockNotificationRequest: UNNotificationRequest! - - override func setUp() { - super.setUp() - service = MindboxNotificationService() - - let aps: [AnyHashable: Any] = [ - "mutable-content": 1, - "alert": [ - "title": "Test title", - "body": "Test description" - ], - "content-available": 1, - "sound": "default" - ] - - let userInfo: [AnyHashable: Any] = [ - "clickUrl": "https://mindbox.ru/", - "payload": "{\n \"payload\": \"data\"\n}", - "uniqueKey": "4cccb64d-ba46-41eb-9699-3a706f2b910b", - "imageUrl": "https://mobpush-images.mindbox.ru/Mpush-test/63/5933f4cd-47e3-4317-9237-bc5aad291aa9.png", - "buttons": [ - [ - "url": "https://developers.mindbox.ru/docs/mindbox-sdk", - "text": "Documentation", - "uniqueKey": "1b112bcd-5eae-4914-8842-d77198466466" - ], - [ - "url": "https://google.com", - "text": "Button #1", - "uniqueKey": "cff05f38-6df4-4a10-9859-ea3bf0a65068" - ] - ], - "aps": aps - ] - - let content = UNMutableNotificationContent() - content.userInfo = userInfo - content.title = "Test title" - content.body = "Test description" - mockNotificationRequest = UNNotificationRequest(identifier: "test", content: content, trigger: nil) +// Serialized: the download tests register a process-global stub `URLProtocol` and share its +// static state, so they must not run concurrently with each other. +@Suite("MindboxNotificationService (NotificationService extension)", .tags(.notifications, .notificationService), .serialized) +struct MindboxNotificationServiceTests { + + let service = MindboxNotificationService() + + // MARK: - didReceive(_:withContentHandler:) + + @Test("didReceive serves the image via a stubbed URL protocol, sets the category and invokes the handler") + func didReceiveDownloadsImageAndCallsHandler() async { + StubURLProtocol.reset() + StubURLProtocol.stubResponseData = NotificationTestFixtures.tinyPNGData() + URLProtocol.registerClass(StubURLProtocol.self) + defer { + URLProtocol.unregisterClass(StubURLProtocol.self) + StubURLProtocol.reset() + } + + let request = NotificationTestFixtures.makeRequest( + userInfo: NotificationTestFixtures.currentFormatUserInfo(), + title: "Test title", + body: "Test description" + ) + + let received: UNNotificationContent = await withCheckedContinuation { continuation in + service.didReceive(request) { content in + continuation.resume(returning: content) + } + } + + #expect(StubURLProtocol.handledRequestCount > 0) // served from the stub, never the real network + #expect(service.contentHandler != nil) + #expect(service.bestAttemptContent != nil) + #expect(service.bestAttemptContent?.userInfo["uniqueKey"] as? String == NotificationTestFixtures.uniqueKey) + #expect(received.title == "Test title") + #expect(received.body == "Test description") + #expect(received.categoryIdentifier == Constants.categoryIdentifier) + #expect(service.bestAttemptContent?.attachments.count == 1) } - override func tearDown() { - service = nil - mockNotificationRequest = nil - super.tearDown() + @Test("didReceive still delivers (no attachment) when the image download fails") + func didReceiveDeliversWhenDownloadFails() async { + StubURLProtocol.reset() + StubURLProtocol.stubError = URLError(.timedOut) + URLProtocol.registerClass(StubURLProtocol.self) + defer { + URLProtocol.unregisterClass(StubURLProtocol.self) + StubURLProtocol.reset() + } + + let request = NotificationTestFixtures.makeRequest( + userInfo: NotificationTestFixtures.currentFormatUserInfo(), + title: "Test title", + body: "Test description" + ) + + let received: UNNotificationContent = await withCheckedContinuation { continuation in + service.didReceive(request) { continuation.resume(returning: $0) } + } + + #expect(StubURLProtocol.handledRequestCount > 0) + #expect(received.categoryIdentifier == Constants.categoryIdentifier) + #expect(received.attachments.isEmpty) } - func testDidReceiveWithContentHandler() { - let expectation = self.expectation(description: "Content Handler Called") - var receivedContent: UNNotificationContent? + @Test("didReceive delivers without an attachment when the data is not a recognized image") + func didReceiveDeliversForUnrecognizedImageData() async { + StubURLProtocol.reset() + StubURLProtocol.stubResponseData = Data([0x00, 0x01, 0x02, 0x03]) // unknown magic byte -> ImageFormat is nil + URLProtocol.registerClass(StubURLProtocol.self) + defer { + URLProtocol.unregisterClass(StubURLProtocol.self) + StubURLProtocol.reset() + } + + let request = NotificationTestFixtures.makeRequest( + userInfo: NotificationTestFixtures.currentFormatUserInfo(), + title: "Test title", + body: "Test description" + ) - service.didReceive(mockNotificationRequest) { content in - receivedContent = content - expectation.fulfill() + let received: UNNotificationContent = await withCheckedContinuation { continuation in + service.didReceive(request) { continuation.resume(returning: $0) } } - waitForExpectations(timeout: 1, handler: nil) - XCTAssertNotNil(service.contentHandler) - XCTAssertNotNil(service.bestAttemptContent) - XCTAssertEqual(service.bestAttemptContent?.userInfo["uniqueKey"] as? String, "4cccb64d-ba46-41eb-9699-3a706f2b910b") - XCTAssertNotNil(receivedContent) + #expect(received.categoryIdentifier == Constants.categoryIdentifier) + #expect(received.attachments.isEmpty) + } + + @Test("didReceive without an image URL delivers immediately and without an attachment") + func didReceiveWithoutImageProceedsImmediately() async { + let request = NotificationTestFixtures.makeRequest( + userInfo: NotificationTestFixtures.currentFormatUserInfo(includeImageUrl: false), + title: "No image", + body: "Body" + ) + + let received: UNNotificationContent = await withCheckedContinuation { continuation in + service.didReceive(request) { content in + continuation.resume(returning: content) + } + } - XCTAssertEqual(service.bestAttemptContent?.title, "Test title") - XCTAssertEqual(service.bestAttemptContent?.body, "Test description") - XCTAssertFalse(service.bestAttemptContent!.attachments.isEmpty) - XCTAssertTrue(service.bestAttemptContent!.attachments.count == 1) + #expect(received.categoryIdentifier == Constants.categoryIdentifier) + #expect(received.attachments.isEmpty) + #expect(service.bestAttemptContent?.title == "No image") } - func testServiceExtensionTimeWillExpire_CallsProceedFinalStage() { - let expectation = self.expectation(description: "Content Handler Called") - var receivedContent: UNNotificationContent? + // MARK: - serviceExtensionTimeWillExpire() - service.didReceive(mockNotificationRequest) { content in - receivedContent = content - expectation.fulfill() - } + @Test("serviceExtensionTimeWillExpire delivers the stored best-attempt content") + func serviceExtensionTimeWillExpireDelivers() { + let content = NotificationTestFixtures.makeContent(userInfo: [:], title: "t", body: "b") + service.bestAttemptContent = content + + var received: UNNotificationContent? + service.contentHandler = { received = $0 } + + service.serviceExtensionTimeWillExpire() + + #expect(received != nil) + #expect(received?.categoryIdentifier == Constants.categoryIdentifier) + } + + @Test("serviceExtensionTimeWillExpire is a no-op without best-attempt content") + func serviceExtensionTimeWillExpireNoContent() { + var called = false + service.contentHandler = { _ in called = true } service.serviceExtensionTimeWillExpire() - waitForExpectations(timeout: 1, handler: nil) - XCTAssertNotNil(receivedContent) - XCTAssertEqual(receivedContent?.categoryIdentifier, Constants.categoryIdentifier) + #expect(!called) + } + + // MARK: - pushDelivered(_:) + + @Test("pushDelivered runs without producing best-attempt content") + func pushDeliveredRuns() { + let request = NotificationTestFixtures.makeRequest(userInfo: NotificationTestFixtures.currentFormatUserInfo()) + service.pushDelivered(request) + #expect(service.bestAttemptContent == nil) } } diff --git a/MindboxNotificationsTests/MindboxPushNotificationTests.swift b/MindboxNotificationsTests/MindboxPushNotificationTests.swift new file mode 100644 index 000000000..0c6a6d7c9 --- /dev/null +++ b/MindboxNotificationsTests/MindboxPushNotificationTests.swift @@ -0,0 +1,100 @@ +// +// MindboxPushNotificationTests.swift +// MindboxNotificationsTests +// +// Created by Sergei Semko on 6/16/26. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxNotifications + +/// Covers the public `MindboxPushNotificationProtocol` surface on `MindboxNotificationService` +/// and `MBPushNotification` decoding. +@Suite("MindboxPushNotification public API", .tags(.notifications, .pushParsing)) +struct MindboxPushNotificationTests { + + let service = MindboxNotificationService() + + // MARK: - isMindboxPush + + @Test("isMindboxPush is true for a current-format Mindbox push") + func isMindboxPushTrueForCurrent() { + #expect(service.isMindboxPush(userInfo: NotificationTestFixtures.currentFormatUserInfo())) + } + + @Test("isMindboxPush is true for a legacy-format Mindbox push") + func isMindboxPushTrueForLegacy() { + #expect(service.isMindboxPush(userInfo: NotificationTestFixtures.legacyFormatUserInfo())) + } + + @Test("isMindboxPush is false for a non-Mindbox push") + func isMindboxPushFalseForPlain() { + #expect(!service.isMindboxPush(userInfo: ["aps": ["alert": ["body": "plain"]]])) + } + + @Test("isMindboxPush is false when no validator is configured") + func isMindboxPushFalseWithoutValidator() { + service.pushValidator = nil + #expect(!service.isMindboxPush(userInfo: NotificationTestFixtures.currentFormatUserInfo())) + } + + // MARK: - getMindboxPushData + + @Test("getMindboxPushData returns parsed data for a Mindbox push") + func getMindboxPushDataReturnsModel() throws { + let data = try #require(service.getMindboxPushData(userInfo: NotificationTestFixtures.currentFormatUserInfo())) + #expect(data.clickUrl == NotificationTestFixtures.clickUrl) + #expect(data.uniqueKey == NotificationTestFixtures.uniqueKey) + #expect(data.aps?.alert?.body == "Test description") + } + + @Test("getMindboxPushData returns nil for a non-Mindbox push") + func getMindboxPushDataNilForPlain() { + #expect(service.getMindboxPushData(userInfo: ["aps": ["alert": ["body": "plain"]]]) == nil) + } + + // MARK: - MBPushNotification Codable + + @Test("MBPushNotification decodes nested aps keys with custom coding keys") + func mbPushNotificationDecodes() throws { + let json = """ + { + "aps": { + "alert": { "title": "T", "body": "B" }, + "sound": "default", + "mutable-content": 1, + "content-available": 1 + }, + "clickUrl": "https://mindbox.ru", + "imageUrl": "https://img", + "payload": "p", + "uniqueKey": "uk", + "buttons": [ { "text": "ok", "url": "https://a", "uniqueKey": "bk" } ] + } + """ + let model = try JSONDecoder().decode(MBPushNotification.self, from: Data(json.utf8)) + #expect(model.clickUrl == "https://mindbox.ru") + #expect(model.imageUrl == "https://img") + #expect(model.payload == "p") + #expect(model.uniqueKey == "uk") + #expect(model.aps?.alert?.title == "T") + #expect(model.aps?.alert?.body == "B") + #expect(model.aps?.sound == "default") + #expect(model.aps?.mutableContent == 1) + #expect(model.aps?.contentAvailable == 1) + #expect(model.buttons?.count == 1) + #expect(model.buttons?.first?.text == "ok") + #expect(model.buttons?.first?.url == "https://a") + #expect(model.buttons?.first?.uniqueKey == "bk") + } + + @Test("MBPushNotification tolerates absent optional fields") + func mbPushNotificationDecodesMinimal() throws { + let model = try JSONDecoder().decode(MBPushNotification.self, from: Data("{}".utf8)) + #expect(model.aps == nil) + #expect(model.clickUrl == nil) + #expect(model.buttons == nil) + } +} diff --git a/MindboxNotificationsTests/NotificationTestFixtures.swift b/MindboxNotificationsTests/NotificationTestFixtures.swift new file mode 100644 index 000000000..0fcbf17c5 --- /dev/null +++ b/MindboxNotificationsTests/NotificationTestFixtures.swift @@ -0,0 +1,188 @@ +// +// NotificationTestFixtures.swift +// MindboxNotificationsTests +// +// Created by Sergei Semko on 6/16/26. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import UIKit +import UserNotifications +@testable import MindboxNotifications + +enum NotificationTestFixtures { + + static let clickUrl = "https://mindbox.ru/" + static let uniqueKey = "4cccb64d-ba46-41eb-9699-3a706f2b910b" + static let imageUrl = "https://mobpush-images.mindbox.ru/Mpush-test/63/5933f4cd-47e3-4317-9237-bc5aad291aa9.png" + static let payloadString = "{\n \"payload\": \"data\"\n}" + + static let firstButtonKey = "1b112bcd-5eae-4914-8842-d77198466466" + static let secondButtonKey = "cff05f38-6df4-4a10-9859-ea3bf0a65068" + + static let buttons: [[String: Any]] = [ + [ + "url": "https://developers.mindbox.ru/docs/mindbox-sdk", + "text": "Documentation", + "uniqueKey": firstButtonKey + ], + [ + "url": "https://google.com", + "text": "Button #1", + "uniqueKey": secondButtonKey + ] + ] + + static func currentFormatUserInfo( + includeBody: Bool = true, + includeClickUrl: Bool = true, + includeButtons: Bool = true, + includeImageUrl: Bool = true + ) -> [AnyHashable: Any] { + var alert: [String: Any] = ["title": "Test title"] + if includeBody { alert["body"] = "Test description" } + + let aps: [String: Any] = [ + "mutable-content": 1, + "alert": alert, + "content-available": 1, + "sound": "default" + ] + + var userInfo: [AnyHashable: Any] = [ + "payload": payloadString, + "uniqueKey": uniqueKey, + "aps": aps + ] + if includeClickUrl { userInfo["clickUrl"] = clickUrl } + if includeButtons { userInfo["buttons"] = buttons } + if includeImageUrl { userInfo["imageUrl"] = imageUrl } + return userInfo + } + + static func legacyFormatUserInfo( + includeBody: Bool = true, + includeClickUrl: Bool = true, + includeButtons: Bool = true + ) -> [AnyHashable: Any] { + var alert: [String: Any] = ["title": "Test title"] + if includeBody { alert["body"] = "Test description" } + + var aps: [String: Any] = [ + "mutable-content": 1, + "alert": alert, + "content-available": 1, + "sound": "default", + "uniqueKey": uniqueKey, + "imageUrl": imageUrl, + "payload": payloadString + ] + if includeClickUrl { aps["clickUrl"] = clickUrl } + if includeButtons { aps["buttons"] = buttons } + return ["aps": aps] + } + + static func makeContent( + userInfo: [AnyHashable: Any], + title: String? = nil, + body: String? = nil + ) -> UNMutableNotificationContent { + let content = UNMutableNotificationContent() + content.userInfo = userInfo + if let title { content.title = title } + if let body { content.body = body } + return content + } + + static func makeRequest( + userInfo: [AnyHashable: Any], + title: String? = nil, + body: String? = nil + ) -> UNNotificationRequest { + let content = makeContent(userInfo: userInfo, title: title, body: body) + return UNNotificationRequest(identifier: "test", content: content, trigger: nil) + } + + static func tinyPNGData() -> Data { + UIGraphicsImageRenderer(size: CGSize(width: 1, height: 1)).pngData { _ in } + } + + static func makeImageAttachment() throws -> UNNotificationAttachment { + let fileURL = FileManager.default.temporaryDirectory + .appendingPathComponent("testImage_\(UUID().uuidString).png") + try tinyPNGData().write(to: fileURL) + return try UNNotificationAttachment(identifier: "identifier", url: fileURL, options: nil) + } +} + +// MARK: - Test doubles + +final class MockExtensionContext: NSExtensionContext { + private var actions: [UNNotificationAction] = [] + + override var notificationActions: [UNNotificationAction] { + get { actions } + set { actions = newValue } + } +} + +// `UNNotification` has no public initializer, so build one by round-tripping through secure coding. +// swiftlint:disable force_unwrapping force_try +final class MockUNNotification: UNNotification { + private let mockRequest: UNNotificationRequest + + init(request: UNNotificationRequest) { + self.mockRequest = request + let data = try! NSKeyedArchiver.archivedData(withRootObject: request, requiringSecureCoding: true) + let coder = try! NSKeyedUnarchiver(forReadingFrom: data) + super.init(coder: coder)! + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var request: UNNotificationRequest { + mockRequest + } +} +// swiftlint:enable force_unwrapping force_try + +// MARK: - URLProtocol stub + +/// Intercepts `URLSession.shared` requests so the notification-service download path runs +/// against a local image instead of a real network fetch. Serves `stubResponseData` for +/// every request while registered, and counts how many it handled. +class StubURLProtocol: URLProtocol { + nonisolated(unsafe) static var stubResponseData: Data? + nonisolated(unsafe) static var stubError: Error? + nonisolated(unsafe) static var handledRequestCount = 0 + + static func reset() { + stubResponseData = nil + stubError = nil + handledRequestCount = 0 + } + + override class func canInit(with request: URLRequest) -> Bool { true } + override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } + + override func startLoading() { + StubURLProtocol.handledRequestCount += 1 + if let error = StubURLProtocol.stubError { + client?.urlProtocol(self, didFailWithError: error) + return + } + if let url = request.url, + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) { + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + } + if let data = StubURLProtocol.stubResponseData { + client?.urlProtocol(self, didLoad: data) + } + client?.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() {} +} diff --git a/MindboxNotificationsTests/PushNotificationParsingTests.swift b/MindboxNotificationsTests/PushNotificationParsingTests.swift new file mode 100644 index 000000000..f87131f0d --- /dev/null +++ b/MindboxNotificationsTests/PushNotificationParsingTests.swift @@ -0,0 +1,157 @@ +// +// PushNotificationParsingTests.swift +// MindboxNotificationsTests +// +// Created by Sergei Semko on 6/16/26. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import MindboxNotifications + +/// Exercises the push-format detection/parsing pipeline: +/// `NotificationStrategyFactory` → `LegacyFormatStrategy`/`CurrentFormatStrategy` +/// → `NotificationFormatter` → `MindboxPushValidator`. +@Suite("Push notification parsing pipeline", .tags(.notifications, .pushParsing)) +struct PushNotificationParsingTests { + + // MARK: - NotificationStrategyFactory + + @Test("Factory selects the legacy strategy when aps carries clickUrl and uniqueKey") + func factoryPicksLegacyStrategy() { + let strategy = NotificationStrategyFactory.strategy(for: NotificationTestFixtures.legacyFormatUserInfo()) + #expect(strategy is LegacyFormatStrategy) + } + + @Test("Factory selects the current strategy for a top-level payload") + func factoryPicksCurrentStrategy() { + let strategy = NotificationStrategyFactory.strategy(for: NotificationTestFixtures.currentFormatUserInfo()) + #expect(strategy is CurrentFormatStrategy) + } + + @Test("Factory falls back to the current strategy without both legacy markers") + func factoryFallsBackToCurrent() { + let missingUniqueKey: [AnyHashable: Any] = ["aps": ["clickUrl": "https://mindbox.ru"]] + let missingClickUrl: [AnyHashable: Any] = ["aps": ["uniqueKey": "abc"]] + let neither: [AnyHashable: Any] = ["aps": ["alert": ["body": "hi"]]] + let noAps: [AnyHashable: Any] = ["foo": "bar"] + + #expect(NotificationStrategyFactory.strategy(for: missingUniqueKey) is CurrentFormatStrategy) + #expect(NotificationStrategyFactory.strategy(for: missingClickUrl) is CurrentFormatStrategy) + #expect(NotificationStrategyFactory.strategy(for: neither) is CurrentFormatStrategy) + #expect(NotificationStrategyFactory.strategy(for: noAps) is CurrentFormatStrategy) + } + + // MARK: - LegacyFormatStrategy + + @Test("Legacy strategy maps a full payload into MBPushNotification") + func legacyStrategyParsesFullPayload() throws { + let result = try #require(LegacyFormatStrategy().handle(userInfo: NotificationTestFixtures.legacyFormatUserInfo())) + #expect(result.clickUrl == NotificationTestFixtures.clickUrl) + #expect(result.uniqueKey == NotificationTestFixtures.uniqueKey) + #expect(result.imageUrl == NotificationTestFixtures.imageUrl) + #expect(result.payload == NotificationTestFixtures.payloadString) + #expect(result.aps?.alert?.title == "Test title") + #expect(result.aps?.alert?.body == "Test description") + #expect(result.aps?.sound == "default") + #expect(result.aps?.mutableContent == 1) + #expect(result.aps?.contentAvailable == 1) + #expect(result.buttons?.count == 2) + #expect(result.buttons?.first?.uniqueKey == NotificationTestFixtures.firstButtonKey) + #expect(result.buttons?.first?.text == "Documentation") + } + + @Test("Legacy strategy returns nil when the alert body is missing") + func legacyStrategyRequiresBody() { + #expect(LegacyFormatStrategy().handle(userInfo: NotificationTestFixtures.legacyFormatUserInfo(includeBody: false)) == nil) + } + + @Test("Legacy strategy returns nil when clickUrl is missing") + func legacyStrategyRequiresClickUrl() { + #expect(LegacyFormatStrategy().handle(userInfo: NotificationTestFixtures.legacyFormatUserInfo(includeClickUrl: false)) == nil) + } + + @Test("Legacy strategy rejects a current-format (top-level) payload") + func legacyStrategyRejectsCurrentFormat() { + #expect(LegacyFormatStrategy().handle(userInfo: NotificationTestFixtures.currentFormatUserInfo()) == nil) + } + + @Test("Legacy strategy drops malformed buttons but keeps the notification") + func legacyStrategyDropsMalformedButtons() throws { + let aps: [String: Any] = [ + "alert": ["title": "t", "body": "b"], + "clickUrl": "https://mindbox.ru", + "uniqueKey": "key", + "buttons": [ + ["text": "ok", "url": "https://a", "uniqueKey": "k1"], + ["text": "missing url", "uniqueKey": "k2"] + ] + ] + let result = try #require(LegacyFormatStrategy().handle(userInfo: ["aps": aps])) + #expect(result.buttons?.count == 1) + #expect(result.buttons?.first?.uniqueKey == "k1") + } + + @Test("Legacy strategy yields nil buttons when the buttons array is absent") + func legacyStrategyNilButtonsWhenAbsent() throws { + let result = try #require(LegacyFormatStrategy().handle(userInfo: NotificationTestFixtures.legacyFormatUserInfo(includeButtons: false))) + #expect(result.buttons == nil) + } + + // MARK: - CurrentFormatStrategy + + @Test("Current strategy decodes a top-level payload") + func currentStrategyParsesPayload() throws { + let result = try #require(CurrentFormatStrategy().handle(userInfo: NotificationTestFixtures.currentFormatUserInfo())) + #expect(result.clickUrl == NotificationTestFixtures.clickUrl) + #expect(result.uniqueKey == NotificationTestFixtures.uniqueKey) + #expect(result.imageUrl == NotificationTestFixtures.imageUrl) + #expect(result.aps?.alert?.title == "Test title") + #expect(result.aps?.alert?.body == "Test description") + #expect(result.buttons?.count == 2) + } + + @Test("Current strategy returns nil when clickUrl is missing") + func currentStrategyRequiresClickUrl() { + #expect(CurrentFormatStrategy().handle(userInfo: NotificationTestFixtures.currentFormatUserInfo(includeClickUrl: false)) == nil) + } + + @Test("Current strategy returns nil when the alert body is missing") + func currentStrategyRequiresBody() { + #expect(CurrentFormatStrategy().handle(userInfo: NotificationTestFixtures.currentFormatUserInfo(includeBody: false)) == nil) + } + + // MARK: - NotificationFormatter + + @Test("Formatter parses a current-format push") + func formatterParsesCurrent() throws { + let result = try #require(NotificationFormatter.formatNotification(NotificationTestFixtures.currentFormatUserInfo())) + #expect(result.clickUrl == NotificationTestFixtures.clickUrl) + } + + @Test("Formatter parses a legacy-format push") + func formatterParsesLegacy() throws { + let result = try #require(NotificationFormatter.formatNotification(NotificationTestFixtures.legacyFormatUserInfo())) + #expect(result.uniqueKey == NotificationTestFixtures.uniqueKey) + } + + @Test("Formatter returns nil for a non-Mindbox payload") + func formatterReturnsNilForPlainPush() { + #expect(NotificationFormatter.formatNotification(["aps": ["alert": ["body": "Just a plain push"]]]) == nil) + } + + // MARK: - MindboxPushValidator + + @Test("Validator accepts current- and legacy-format Mindbox pushes") + func validatorAcceptsMindboxPushes() { + let validator = MindboxPushValidator() + #expect(validator.isValid(item: NotificationTestFixtures.currentFormatUserInfo())) + #expect(validator.isValid(item: NotificationTestFixtures.legacyFormatUserInfo())) + } + + @Test("Validator rejects a non-Mindbox push") + func validatorRejectsPlainPush() { + #expect(!MindboxPushValidator().isValid(item: ["aps": ["alert": ["body": "plain"]]])) + } +} diff --git a/MindboxNotificationsTests/SharedInternalMethodsTests.swift b/MindboxNotificationsTests/SharedInternalMethodsTests.swift index a420cb7ac..943818dcd 100644 --- a/MindboxNotificationsTests/SharedInternalMethodsTests.swift +++ b/MindboxNotificationsTests/SharedInternalMethodsTests.swift @@ -6,131 +6,74 @@ // Copyright © 2024 Mindbox. All rights reserved. // -import XCTest +import Testing +import Foundation +import UserNotifications @testable import MindboxNotifications -final class SharedInternalMethodsTests: XCTestCase { - - var service: MindboxNotificationService! - var mockNotificationRequest: UNNotificationRequest! - - override func setUp() { - super.setUp() - service = MindboxNotificationService() - - let aps: [AnyHashable: Any] = [ - "mutable-content": 1, - "alert": [ - "title": "Test title", - "body": "Test description" - ], - "content-available": 1, - "sound": "default" - ] - - let userInfo: [AnyHashable: Any] = [ - "clickUrl": "https://mindbox.ru/", - "payload": "{\n \"payload\": \"data\"\n}", - "uniqueKey": "4cccb64d-ba46-41eb-9699-3a706f2b910b", - "imageUrl": "https://mobpush-images.mindbox.ru/Mpush-test/63/5933f4cd-47e3-4317-9237-bc5aad291aa9.png", - "buttons": [ - [ - "url": "https://developers.mindbox.ru/docs/mindbox-sdk", - "text": "Documentation", - "uniqueKey": "1b112bcd-5eae-4914-8842-d77198466466" - ], - [ - "url": "https://google.com", - "text": "Button #1", - "uniqueKey": "cff05f38-6df4-4a10-9859-ea3bf0a65068" - ] - ], - "aps": aps - ] - - let content = UNMutableNotificationContent() - content.userInfo = userInfo - mockNotificationRequest = UNNotificationRequest(identifier: "test", content: content, trigger: nil) - } +@Suite("SharedInternalMethods", .tags(.notifications, .pushParsing)) +struct SharedInternalMethodsTests { + + let service = MindboxNotificationService() + + // MARK: - parse(request:) - override func tearDown() { - service = nil - mockNotificationRequest = nil - super.tearDown() + @Test("parse maps a current-format request into a Payload") + func parseCurrentFormat() throws { + let request = NotificationTestFixtures.makeRequest(userInfo: NotificationTestFixtures.currentFormatUserInfo()) + let payload = try #require(service.parse(request: request)) + + let button = try #require(payload.withButton) + #expect(button.buttons?.first?.uniqueKey == NotificationTestFixtures.firstButtonKey) + #expect(button.buttons?.first?.text == "Documentation") + #expect(button.buttons?.last?.uniqueKey == NotificationTestFixtures.secondButtonKey) + #expect(button.buttons?.last?.text == "Button #1") + #expect(payload.withImageURL?.imageUrl == NotificationTestFixtures.imageUrl) } - func testParseToPayload() { - let payload = service.parse(request: mockNotificationRequest) - XCTAssertNotNil(payload) - XCTAssertNotNil(payload?.withButton) - XCTAssertEqual(payload?.withButton?.buttons?.first?.uniqueKey, "1b112bcd-5eae-4914-8842-d77198466466") - XCTAssertEqual(payload?.withButton?.buttons?.first?.text, "Documentation") - XCTAssertEqual(payload?.withButton?.buttons?.last?.uniqueKey, "cff05f38-6df4-4a10-9859-ea3bf0a65068") - XCTAssertEqual(payload?.withButton?.buttons?.last?.text, "Button #1") - XCTAssertNotNil(payload?.withImageURL) - XCTAssertEqual(payload?.withImageURL?.imageUrl, "https://mobpush-images.mindbox.ru/Mpush-test/63/5933f4cd-47e3-4317-9237-bc5aad291aa9.png") + @Test("Payload.Button.debugDescription includes the unique key") + func payloadButtonDebugDescription() throws { + let request = NotificationTestFixtures.makeRequest(userInfo: NotificationTestFixtures.currentFormatUserInfo()) + let button = try #require(service.parse(request: request)?.withButton) + #expect(button.debugDescription == "uniqueKey: \(button.uniqueKey)") } - func testGetUserInfo() { - let result = service.getUserInfo(from: mockNotificationRequest) - XCTAssertNotNil(result) - XCTAssertEqual(result?["uniqueKey"] as? String, "4cccb64d-ba46-41eb-9699-3a706f2b910b") - XCTAssertEqual(result?["clickUrl"] as? String, "https://mindbox.ru/") - - let apsResult = result?["aps"] as? [AnyHashable: Any] - XCTAssertNotNil(apsResult) - XCTAssertEqual(apsResult?["mutable-content"] as? Int, 1) - XCTAssertEqual(apsResult?["content-available"] as? Int, 1) - XCTAssertEqual(apsResult?["sound"] as? String, "default") - - let alertResult = apsResult?["alert"] as? [String: String] - XCTAssertNotNil(alertResult) - XCTAssertEqual(alertResult?["title"], "Test title") - XCTAssertEqual(alertResult?["body"], "Test description") + // parse(request:)'s nil paths are unreachable here: getUserInfo never returns nil, and the + // only JSONSerialization failures throw an ObjC exception that `try?` can't catch (it crashes). + + // MARK: - getUserInfo(from:) + + @Test("getUserInfo returns the outer payload for the current format") + func getUserInfoCurrentFormat() throws { + let request = NotificationTestFixtures.makeRequest(userInfo: NotificationTestFixtures.currentFormatUserInfo()) + let result = try #require(service.getUserInfo(from: request)) + + #expect(result["uniqueKey"] as? String == NotificationTestFixtures.uniqueKey) + #expect(result["clickUrl"] as? String == NotificationTestFixtures.clickUrl) + + let aps = try #require(result["aps"] as? [AnyHashable: Any]) + #expect(aps["sound"] as? String == "default") + #expect(aps["mutable-content"] as? Int == 1) + #expect(aps["content-available"] as? Int == 1) + + let alert = try #require(aps["alert"] as? [String: String]) + #expect(alert["title"] == "Test title") + #expect(alert["body"] == "Test description") } - func testGetUserInfoLegacyPushFormat() { - let aps: [AnyHashable: Any] = [ - "aps": [ - "mutable-content": 1, - "alert": [ - "title": "Test title", - "body": "Test description" - ], - "content-available": 1, - "sound": "default", - "clickUrl": "https://mindbox.ru/", - "payload": "{\n \"payload\": \"data\"\n}", - "uniqueKey": "4cccb64d-ba46-41eb-9699-3a706f2b910b", - "imageUrl": "https://mobpush-images.mindbox.ru/Mpush-test/63/5933f4cd-47e3-4317-9237-bc5aad291aa9.png", - "buttons": [ - [ - "url": "https://developers.mindbox.ru/docs/mindbox-sdk", - "text": "Documentation", - "uniqueKey": "1b112bcd-5eae-4914-8842-d77198466466" - ] - ] - ] - ] - - let userInfo: [AnyHashable: Any] = aps - - let content = UNMutableNotificationContent() - content.userInfo = userInfo - let request = UNNotificationRequest(identifier: "test", content: content, trigger: nil) - - let result = service.getUserInfo(from: request) - XCTAssertNotNil(result) - XCTAssertEqual(result?["uniqueKey"] as? String, "4cccb64d-ba46-41eb-9699-3a706f2b910b") - XCTAssertEqual(result?["clickUrl"] as? String, "https://mindbox.ru/") - - XCTAssertEqual(result?["mutable-content"] as? Int, 1) - XCTAssertEqual(result?["content-available"] as? Int, 1) - XCTAssertEqual(result?["sound"] as? String, "default") - - let alertResult = result?["alert"] as? [String: String] - XCTAssertNotNil(alertResult) - XCTAssertEqual(alertResult?["title"], "Test title") - XCTAssertEqual(alertResult?["body"], "Test description") + @Test("getUserInfo unwraps the aps dictionary for the legacy format") + func getUserInfoLegacyFormat() throws { + let request = NotificationTestFixtures.makeRequest(userInfo: NotificationTestFixtures.legacyFormatUserInfo()) + let result = try #require(service.getUserInfo(from: request)) + + #expect(result["uniqueKey"] as? String == NotificationTestFixtures.uniqueKey) + #expect(result["clickUrl"] as? String == NotificationTestFixtures.clickUrl) + #expect(result["sound"] as? String == "default") + #expect(result["mutable-content"] as? Int == 1) + #expect(result["content-available"] as? Int == 1) + + let alert = try #require(result["alert"] as? [String: String]) + #expect(alert["title"] == "Test title") + #expect(alert["body"] == "Test description") } } diff --git a/MindboxNotificationsTests/Tag+Extensions.swift b/MindboxNotificationsTests/Tag+Extensions.swift new file mode 100644 index 000000000..a77f06856 --- /dev/null +++ b/MindboxNotificationsTests/Tag+Extensions.swift @@ -0,0 +1,30 @@ +// +// Tag+Extensions.swift +// MindboxNotificationsTests +// +// Created by Sergei Semko on 6/16/26. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import Testing + +extension Tag { + /// Umbrella tag applied to every MindboxNotifications test. + @Tag static var notifications: Self + + // MARK: Subject areas + + /// Push-format detection, decoding and validation: strategy factory, legacy/current + /// strategies, formatter, validator, payload parsing and `MBPushNotification`. + @Tag static var pushParsing: Self + + /// Image magic-byte format detection (`ImageFormat`). + @Tag static var imageFormat: Self + + /// Notification Service Extension delivery flow (`didReceive(_:withContentHandler:)`, expiry, push delivered). + @Tag static var notificationService: Self + + /// Notification Content Extension UI flow (`didReceive(notification:…)`, actions, image view). + @Tag static var notificationContent: Self +} diff --git a/MindboxTests/ConfigParsing/Settings/FeatureTogglesConfigParsingTests.swift b/MindboxTests/ConfigParsing/Settings/FeatureTogglesConfigParsingTests.swift index 0cc2562f3..c59be923c 100644 --- a/MindboxTests/ConfigParsing/Settings/FeatureTogglesConfigParsingTests.swift +++ b/MindboxTests/ConfigParsing/Settings/FeatureTogglesConfigParsingTests.swift @@ -23,6 +23,9 @@ fileprivate enum FeatureTogglesConfig: String, Configurable { case settingsFeatureTogglesShouldSendInAppShowErrorMissing = "SettingsFeatureTogglesShouldSendInAppShowErrorMissing" // Missing `shouldSendInAppShowError` case settingsFeatureTogglesShouldSendInAppShowErrorTypeError = "SettingsFeatureTogglesShouldSendInAppShowErrorTypeError" // Type of `shouldSendInAppShowError` is String instead of Bool case settingsFeatureTogglesShouldSendInAppShowErrorFalse = "SettingsFeatureTogglesShouldSendInAppShowErrorFalse" // `shouldSendInAppShowError` is false + + case settingsFeatureTogglesShouldSendInAppTagsFalse = "SettingsFeatureTogglesShouldSendInAppTagsFalse" // `shouldSendInAppTags` is false + case settingsFeatureTogglesShouldSendInAppTagsTypeError = "SettingsFeatureTogglesShouldSendInAppTagsTypeError" // Type of `shouldSendInAppTags` is String instead of Bool } final class FeatureTogglesConfigParsingTests: XCTestCase { @@ -36,7 +39,7 @@ final class FeatureTogglesConfigParsingTests: XCTestCase { XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") XCTAssertEqual(config.featureToggles?.shouldSendInAppShowError, true, "shouldSendInAppShowError must be true") - let featureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + let featureToggleManager = FeatureToggleManager() featureToggleManager.applyFeatureToggles(config.featureToggles) XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppShowError)) } @@ -48,7 +51,7 @@ final class FeatureTogglesConfigParsingTests: XCTestCase { XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") XCTAssertEqual(config.featureToggles?.shouldSendInAppShowError, false, "shouldSendInAppShowError must be false") - let featureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + let featureToggleManager = FeatureToggleManager() featureToggleManager.applyFeatureToggles(config.featureToggles) XCTAssertFalse(featureToggleManager.isFeatureEnabled(.shouldSendInAppShowError)) } @@ -59,7 +62,7 @@ final class FeatureTogglesConfigParsingTests: XCTestCase { XCTAssertNil(config.featureToggles, "FeatureToggles must be `nil` if the key `featureToggles` is not found") - let featureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + let featureToggleManager = FeatureToggleManager() featureToggleManager.applyFeatureToggles(config.featureToggles) XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppShowError)) } @@ -70,7 +73,7 @@ final class FeatureTogglesConfigParsingTests: XCTestCase { XCTAssertNil(config.featureToggles, "FeatureToggles must be `nil` if the type of `featureToggles` is not a `Settings.FeatureToggles`") - let featureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + let featureToggleManager = FeatureToggleManager() featureToggleManager.applyFeatureToggles(config.featureToggles) XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppShowError)) } @@ -81,7 +84,7 @@ final class FeatureTogglesConfigParsingTests: XCTestCase { XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") - let featureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + let featureToggleManager = FeatureToggleManager() featureToggleManager.applyFeatureToggles(config.featureToggles) XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppShowError)) } @@ -91,9 +94,57 @@ final class FeatureTogglesConfigParsingTests: XCTestCase { let config = try! FeatureTogglesConfig.settingsFeatureTogglesShouldSendInAppShowErrorTypeError.getConfig() XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") - - let featureToggleManager = DI.injectOrFail(FeatureToggleManager.self) + + let featureToggleManager = FeatureToggleManager() featureToggleManager.applyFeatureToggles(config.featureToggles) XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppShowError)) } + + // MARK: - FeatureToggles: MobileSdkShouldSendInAppTags + + func test_SettingsConfig_withFeatureTogglesTagsTrue_shouldParseSuccessfully() { + // `shouldSendInAppTags` is true + let config = try! FeatureTogglesConfig.configWithSettings.getConfig() + + XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") + XCTAssertEqual(config.featureToggles?.shouldSendInAppTags, true, "shouldSendInAppTags must be true") + + let featureToggleManager = FeatureToggleManager() + featureToggleManager.applyFeatureToggles(config.featureToggles) + XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppTags)) + } + + func test_SettingsConfig_withFeatureTogglesTagsFalse_shouldParseSuccessfully() { + // `shouldSendInAppTags` is false + let config = try! FeatureTogglesConfig.settingsFeatureTogglesShouldSendInAppTagsFalse.getConfig() + + XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") + XCTAssertEqual(config.featureToggles?.shouldSendInAppTags, false, "shouldSendInAppTags must be false") + + let featureToggleManager = FeatureToggleManager() + featureToggleManager.applyFeatureToggles(config.featureToggles) + XCTAssertFalse(featureToggleManager.isFeatureEnabled(.shouldSendInAppTags)) + } + + func test_SettingsConfig_withFeatureTogglesMissingShouldSendInAppTags_shouldDefaultToTrue() { + // Missing `shouldSendInAppTags` + let config = try! FeatureTogglesConfig.settingsFeatureTogglesShouldSendInAppShowErrorMissing.getConfig() + + XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") + + let featureToggleManager = FeatureToggleManager() + featureToggleManager.applyFeatureToggles(config.featureToggles) + XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppTags)) + } + + func test_SettingsConfig_withFeatureTogglesShouldSendInAppTagsTypeError_shouldDefaultToTrue() { + // Type of `shouldSendInAppTags` is String instead of Bool + let config = try! FeatureTogglesConfig.settingsFeatureTogglesShouldSendInAppTagsTypeError.getConfig() + + XCTAssertNotNil(config.featureToggles, "FeatureToggles must be successfully parsed") + + let featureToggleManager = FeatureToggleManager() + featureToggleManager.applyFeatureToggles(config.featureToggles) + XCTAssertTrue(featureToggleManager.isFeatureEnabled(.shouldSendInAppTags)) + } } diff --git a/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/FeatureTogglesError/SettingsFeatureTogglesShouldSendInAppTagsFalse.json b/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/FeatureTogglesError/SettingsFeatureTogglesShouldSendInAppTagsFalse.json new file mode 100644 index 000000000..3a4ab014e --- /dev/null +++ b/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/FeatureTogglesError/SettingsFeatureTogglesShouldSendInAppTagsFalse.json @@ -0,0 +1,28 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ProductView" + }, + "viewCategory": { + "systemName": "CategoryView" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "0.00:00:10" + }, + "slidingExpiration": { + "config": "0.00:00:10", + "pushTokenKeepalive": "0.00:00:10" + }, + "inapp": { + "maxInappsPerSession": 1, + "maxInappsPerDay": 1, + "minIntervalBetweenShows": "0.00:00:10" + }, + "featureToggles": { + "MobileSdkShouldSendInAppTags": false + } +} diff --git a/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/FeatureTogglesError/SettingsFeatureTogglesShouldSendInAppTagsTypeError.json b/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/FeatureTogglesError/SettingsFeatureTogglesShouldSendInAppTagsTypeError.json new file mode 100644 index 000000000..d0c9d874c --- /dev/null +++ b/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/FeatureTogglesError/SettingsFeatureTogglesShouldSendInAppTagsTypeError.json @@ -0,0 +1,28 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ProductView" + }, + "viewCategory": { + "systemName": "CategoryView" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "0.00:00:10" + }, + "slidingExpiration": { + "config": "0.00:00:10", + "pushTokenKeepalive": "0.00:00:10" + }, + "inapp": { + "maxInappsPerSession": 1, + "maxInappsPerDay": 1, + "minIntervalBetweenShows": "0.00:00:10" + }, + "featureToggles": { + "MobileSdkShouldSendInAppTags": "yes" + } +} diff --git a/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/SettingsConfig.json b/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/SettingsConfig.json index 67893c3ad..c7c514662 100644 --- a/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/SettingsConfig.json +++ b/MindboxTests/ConfigParsing/stubs/Settings/SettingsJsonStubs/SettingsConfig.json @@ -23,7 +23,8 @@ "minIntervalBetweenShows": "0.00:00:10" }, "featureToggles": { - "MobileSdkShouldSendInAppShowError": true + "MobileSdkShouldSendInAppShowError": true, + "MobileSdkShouldSendInAppTags": true }, "baseAddresses": { "operations": "anonymizer-demo-api-regular.mindbox.ru" diff --git a/MindboxTests/DI/AppGroupUnavailableTests.swift b/MindboxTests/DI/AppGroupUnavailableTests.swift new file mode 100644 index 000000000..2740848c6 --- /dev/null +++ b/MindboxTests/DI/AppGroupUnavailableTests.swift @@ -0,0 +1,148 @@ +// +// AppGroupUnavailableTests.swift +// MindboxTests +// +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +import CoreData +import MindboxLogger +@testable import Mindbox + +/// #705 core-SDK fallback: an unavailable App Group must degrade to local storage, not crash. +/// Serialized — the cases mutate global `MBInject` / `MBPersistenceStorage.defaults` / `MBPersistentContainer`. +@Suite("App Group unavailable — core SDK fallback", .serialized) +struct AppGroupUnavailableTests { + + /// Stub fetcher reporting an unavailable App Group (`""`), independent of the simulator's containers. + private struct EmptyAppGroupUtilitiesFetcher: UtilitiesFetcher { + var appVerson: String? { "1.0.0" } + var sdkVersion: String? { "test" } + var hostApplicationName: String? { "cloud.Mindbox.MindboxTests" } + var applicationGroupIdentifier: String { "" } + func getDeviceUUID(completion: @escaping (String) -> Void) { completion(UUID().uuidString) } + } + + /// #705: the getter used to `fatalError` on an unavailable container — a trap would tear down + /// the runner, so simply reaching the assertion proves it's gone. + @Test + func coreFetcherResolvesWithoutTrapping() { + let id = MBUtilitiesFetcher().applicationGroupIdentifier + #expect(id.isEmpty || id.hasPrefix("group.cloud.Mindbox.")) + } + + @Test + func persistenceStorageFallsBackToStandardWhenAppGroupEmpty() { + // All three are global (and `MBPersistenceStorage(defaults:)` writes the static `.defaults`); + // save/restore so this minimal container can't leak into later `.test`-mode tests. + let savedBuilder = MBInject.buildTestContainer + let savedMode = MBInject.mode + let savedDefaults = MBPersistenceStorage.defaults + defer { + MBInject.buildTestContainer = savedBuilder + MBInject.mode = savedMode + MBPersistenceStorage.defaults = savedDefaults + } + + MBInject.buildTestContainer = { + let container = MBContainer() + container.register(UtilitiesFetcher.self) { EmptyAppGroupUtilitiesFetcher() } + return container.registerReplaceableUtilities() + } + MBInject.mode = .test + + let storage = DI.injectOrFail(PersistenceStorage.self) + #expect(storage is MBPersistenceStorage) + #expect(MBPersistenceStorage.defaults === UserDefaults.standard) + } + + /// Unavailable App Group (nil id from the logger path, "" from the core fetcher) → the events + /// store's directory must fall through to the app-local default rather than crash. Covers both + /// `MBPersistentContainer.defaultDirectoryURL()` fallbacks: the nil guard and `containerURL("") ?? super`. + @Test(arguments: [nil, ""] as [String?]) + func eventsStoreFallsBackToLocalDirectory(groupId: String?) { + let saved = MBPersistentContainer.applicationGroupIdentifier + defer { MBPersistentContainer.applicationGroupIdentifier = saved } + + MBPersistentContainer.applicationGroupIdentifier = groupId + #expect(MBPersistentContainer.defaultDirectoryURL() == NSPersistentContainer.defaultDirectoryURL()) + } +} + +/// Storage-transition reporter (#705 follow-up): reports only when install state is in BOTH stores; read-only. +@Suite("App Group storage-transition reporter") +struct AppGroupStorageTransitionReporterTests { + + private func makeStore(_ name: String, installed: Bool) -> UserDefaults { + let defaults = UserDefaults(suiteName: name)! + defaults.removePersistentDomain(forName: name) + if installed { + defaults.set("23.05.2026 10:00:00", forKey: MBPersistenceStorage.installationDataKey) + } + return defaults + } + + @Test("Reports only when install state is in BOTH stores, and never mutates either store", + arguments: [ + (active: true, local: true, expectReport: true), + (active: false, local: true, expectReport: false), + (active: true, local: false, expectReport: false), + (active: false, local: false, expectReport: false), + ]) + func reportsOnlyWhenInstalledInBothStores(active: Bool, local: Bool, expectReport: Bool) { + let activeName = "test.appgroup.active.\(active).\(local)" + let localName = "test.appgroup.local.\(active).\(local)" + let activeStore = makeStore(activeName, installed: active) + let localStore = makeStore(localName, installed: local) + defer { + activeStore.removePersistentDomain(forName: activeName) + localStore.removePersistentDomain(forName: localName) + } + + let reporter = AppGroupStorageTransitionReporter(activeDefaults: activeStore, localDefaults: localStore) + + #expect(reporter.reportIfNeeded() == expectReport) + #expect(MBPersistenceStorage.isInstalled(in: localStore) == local) // read-only: stores unchanged + #expect(MBPersistenceStorage.isInstalled(in: activeStore) == active) + #expect(reporter.reportIfNeeded() == expectReport) // not one-shot: re-fires while the fingerprint persists + } + + @Test("No report in local fallback (App Group unavailable → active store IS the local store)") + func noReportInFallback() { + let name = "test.appgroup.fallback" + let store = makeStore(name, installed: true) + defer { store.removePersistentDomain(forName: name) } + + let didReport = AppGroupStorageTransitionReporter(activeDefaults: store, localDefaults: store) + .reportIfNeeded() + + #expect(didReport == false) + #expect(MBPersistenceStorage.isInstalled(in: store) == true) + } + + /// The reporter relies on the App Group suite NOT reading through to `.standard`; otherwise + /// `isInstalled(in: suite)` would be true whenever `.standard` holds the marker and it would + /// mis-fire. A separately-registered suite is its own read domain, so this holds — the real + /// provisioned-App-Group case was confirmed on device; the runner exercises the same isolation. + @Test("A separate UserDefaults suite does not read through to .standard's install marker") + func suiteDoesNotReadThroughToStandard() { + let key = MBPersistenceStorage.installationDataKey + let group = "group.cloud.Mindbox.ReadThroughProbe" + let suite = UserDefaults(suiteName: group)! + + let savedStandard = UserDefaults.standard.object(forKey: key) + defer { + if let savedStandard { UserDefaults.standard.set(savedStandard, forKey: key) } + else { UserDefaults.standard.removeObject(forKey: key) } + suite.removePersistentDomain(forName: group) + } + + suite.removePersistentDomain(forName: group) + UserDefaults.standard.set("23.05.2026 10:00:00", forKey: key) // marker only in .standard + + #expect(MBPersistenceStorage.isInstalled(in: .standard) == true) + #expect(MBPersistenceStorage.isInstalled(in: suite) == false) + } +} diff --git a/MindboxTests/DI/MBContainerConcurrencyTests.swift b/MindboxTests/DI/MBContainerConcurrencyTests.swift new file mode 100644 index 000000000..b1dba2f5c --- /dev/null +++ b/MindboxTests/DI/MBContainerConcurrencyTests.swift @@ -0,0 +1,49 @@ +// +// MBContainerConcurrencyTests.swift +// MindboxTests +// +// Created by Sergei Semko on 08.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import Mindbox + +@Suite("MBContainer thread safety", .tags(.dependencyInjection)) +struct MBContainerConcurrencyTests { + + private final class Service {} + + /// A `.container`-scoped singleton must be minted exactly once even under concurrent + /// resolves — the recursive lock serializes the check-create-store. Without the lock, + /// racing resolves each see an empty cache and run the factory more than once. + @Test + func concurrentResolveMintsOneInstance() { + let container = MBContainer() + let counterLock = NSLock() + var factoryCalls = 0 + + container.register(Service.self, scope: .container) { + // Widen the check-create-store window so a missing lock reliably races. + Thread.sleep(forTimeInterval: 0.02) + counterLock.lock() + factoryCalls += 1 + counterLock.unlock() + return Service() + } + + let resultsLock = NSLock() + var identifiers = Set() + DispatchQueue.concurrentPerform(iterations: 32) { _ in + if let service = container.resolve(Service.self) { + resultsLock.lock() + identifiers.insert(ObjectIdentifier(service)) + resultsLock.unlock() + } + } + + #expect(factoryCalls == 1) + #expect(identifiers.count == 1) + } +} diff --git a/MindboxTests/Extensions/Tag+Extensions.swift b/MindboxTests/Extensions/Tag+Extensions.swift index f9381ded6..27c57670c 100644 --- a/MindboxTests/Extensions/Tag+Extensions.swift +++ b/MindboxTests/Extensions/Tag+Extensions.swift @@ -27,4 +27,7 @@ extension Tag { @Tag static var trackVisit: Self @Tag static var operationsRouting: Self @Tag static var mbConfiguration: Self + @Tag static var inAppTags: Self + @Tag static var userAgent: Self + @Tag static var dependencyInjection: Self } diff --git a/MindboxTests/Helpers/PollUntil.swift b/MindboxTests/Helpers/PollUntil.swift new file mode 100644 index 000000000..3010cf3a2 --- /dev/null +++ b/MindboxTests/Helpers/PollUntil.swift @@ -0,0 +1,32 @@ +// +// PollUntil.swift +// MindboxTests +// +// Created by Sergei Semko on 11.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation + +/// Polls `value` every `pollInterval` until `condition` accepts it or `deadline` +/// passes, and returns the last polled value either way: on timeout the caller's +/// own assertion fails with the real final state instead of hanging the test. +/// The deadline is only an upper bound for the genuine-failure case, sized for +/// slow CI machines - a passing test returns on the first satisfied poll. +/// Task cancellation also ends the poll: a cancelled `Task.sleep` throws +/// immediately, so without the explicit check the loop would busy-spin. +func pollUntil( + deadline: TimeInterval = 10, + pollInterval: TimeInterval = 0.02, + value: () throws -> T, + condition: (T) -> Bool +) async rethrows -> T { + let start = Date() + while true { + let current = try value() + if condition(current) { return current } + if Date().timeIntervalSince(start) > deadline { return current } + try? await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000)) + if Task.isCancelled { return current } + } +} diff --git a/MindboxTests/InApp/Tests/InAppConfigResponseTests/ConfigJsonStubs/ConfigTargetings/Tags-FailedTargeting.json b/MindboxTests/InApp/Tests/InAppConfigResponseTests/ConfigJsonStubs/ConfigTargetings/Tags-FailedTargeting.json new file mode 100644 index 000000000..dbe165814 --- /dev/null +++ b/MindboxTests/InApp/Tests/InAppConfigResponseTests/ConfigJsonStubs/ConfigTargetings/Tags-FailedTargeting.json @@ -0,0 +1,197 @@ +{ + "settings": { + "operations": { + "viewProduct": { + "systemName": "viewProduct" + } + } + }, + "inapps": [ + { + "id": "1", + "sdkVersion": { + "min": 8, + "max": null + }, + "targeting": { + "nodes": [ + { + "kind": "positive", + "segmentationInternalId": "42", + "segmentationExternalId": "79ff415d-5bcf-45c8-ba78-bbf559083b5b", + "segmentExternalId": "79ff415d-5bcf-45c8-ba78-bbf559083b5b", + "$type": "segment" + } + ], + "$type": "and" + }, + "form": { + "variants": [ + { + "content": { + "background": { + "layers": [ + { + "action": { + "intentPayload": "", + "value": "", + "$type": "redirectUrl" + }, + "source": { + "value": "https://personalization-web-stable.mindbox.ru/user-media/29836/e17b389b8fcd5819c15933de78398dcb65769ae44483d4ac6807781e2bf7781d.png", + "$type": "url" + }, + "$type": "image" + } + ] + }, + "position": { + "margin": { + "kind": "dp", + "top": 0.0, + "right": 20.0, + "left": 20.0, + "bottom": 0.0 + }, + "gravity": { + "horizontal": "center", + "vertical": "top" + } + }, + "elements": [] + }, + "imageUrl": "", + "redirectUrl": "", + "intentPayload": "", + "$type": "snackbar" + } + ] + }, + "tags": { + "templateType": "Popup", + "abTestVariant": "control" + } + }, + { + "id": "2", + "sdkVersion": { + "min": 8, + "max": null + }, + "targeting": { + "nodes": [ + { + "$type": "true" + } + ], + "$type": "and" + }, + "form": { + "variants": [ + { + "content": { + "background": { + "layers": [ + { + "action": { + "intentPayload": "", + "value": "", + "$type": "redirectUrl" + }, + "source": { + "value": "https://personalization-web-stable.mindbox.ru/user-media/29836/e17b389b8fcd5819c15933de78398dcb65769ae44483d4ac6807781e2bf7781d.png", + "$type": "url" + }, + "$type": "image" + } + ] + }, + "position": { + "margin": { + "kind": "dp", + "top": 0.0, + "right": 20.0, + "left": 20.0, + "bottom": 0.0 + }, + "gravity": { + "horizontal": "center", + "vertical": "top" + } + }, + "elements": [] + }, + "imageUrl": "", + "redirectUrl": "", + "intentPayload": "", + "$type": "snackbar" + } + ] + }, + "tags": { + "templateType": "Snackbar" + } + }, + { + "id": "3", + "sdkVersion": { + "min": 8, + "max": null + }, + "targeting": { + "nodes": [ + { + "kind": "positive", + "segmentationInternalId": "42", + "segmentationExternalId": "79ff415d-5bcf-45c8-ba78-bbf559083b5b", + "segmentExternalId": "79ff415d-5bcf-45c8-ba78-bbf559083b5b", + "$type": "segment" + } + ], + "$type": "and" + }, + "form": { + "variants": [ + { + "content": { + "background": { + "layers": [ + { + "action": { + "intentPayload": "", + "value": "", + "$type": "redirectUrl" + }, + "source": { + "value": "https://personalization-web-stable.mindbox.ru/user-media/29836/e17b389b8fcd5819c15933de78398dcb65769ae44483d4ac6807781e2bf7781d.png", + "$type": "url" + }, + "$type": "image" + } + ] + }, + "position": { + "margin": { + "kind": "dp", + "top": 0.0, + "right": 20.0, + "left": 20.0, + "bottom": 0.0 + }, + "gravity": { + "horizontal": "center", + "vertical": "top" + } + }, + "elements": [] + }, + "imageUrl": "", + "redirectUrl": "", + "intentPayload": "", + "$type": "snackbar" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappMapperTests.swift b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappMapperTests.swift index d47164df3..6c99b2005 100644 --- a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappMapperTests.swift +++ b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappMapperTests.swift @@ -26,6 +26,7 @@ fileprivate enum InappTargetingConfig: String, Configurable { case fortyFourTargeting = "44-Targeting" case fortyFiveTargeting = "45-Targeting" case fortySixTargeting = "46-Targeting" + case tagsFailedTargeting = "Tags-FailedTargeting" } // MARK: - Suite @@ -58,6 +59,7 @@ struct InappRemainingTargetingTests { self.mockDataFacade = mock self.mockDataFacade.cleanTargetingArray() self.mockDataFacade.cleanImageDownloadFailures() + self.mockDataFacade.cleanCollectedTargetingFailureIds() self.mapper = DI.injectOrFail(InappMapperProtocol.self) self.persistenceStorage = DI.injectOrFail(PersistenceStorage.self) @@ -104,16 +106,12 @@ struct InappRemainingTargetingTests { expectedCount: Int, timeout: TimeInterval = 5 ) async { - let deadline = Date().addingTimeInterval(timeout) - - while Date() < deadline { - if mockDataFacade.targetingArray.count >= expectedCount { - return - } - try? await Task.sleep(nanoseconds: 50_000_000) // 50 ms + let final = await pollUntil(deadline: timeout, pollInterval: 0.05, + value: { mockDataFacade.targetingArray }, + condition: { $0.count >= expectedCount }) + if final.count < expectedCount { + Issue.record("Timed out waiting for targetingArray to reach count \(expectedCount). Current: \(final)") } - - Issue.record("Timed out waiting for targetingArray to reach count \(expectedCount). Current: \(mockDataFacade.targetingArray)") } // MARK: - Tests @@ -158,6 +156,31 @@ struct InappRemainingTargetingTests { assertTargetingEquals(ids: ["1", "2"]) } + @Test("Failed targeting collects tags only for failed in-apps that have them", .tags(.remainingTargeting, .inAppTags)) + func failedTargeting_collectsTagsOnlyForFailedInappsWithTags() async throws { + let config = try InappTargetingConfig.tagsFailedTargeting.getConfig() + + await handleInapps(event: nil, config: config) + + // In-apps "1" (with tags) and "3" (without tags) fail segment targeting, "2" passes. + #expect(mockDataFacade.collectedTargetingFailureIds == [Set(["1", "3"])]) + #expect(mockDataFacade.collectedTagsByInappId == [ + ["1": ["templateType": "Popup", "abTestVariant": "control"]] + ]) + } + + @Test("Shown in-app propagates its tags into trackTargeting and downloadImage", .tags(.remainingTargeting, .inAppTags)) + func shownInapp_propagatesTagsToTrackTargetingAndDownloadImage() async throws { + let config = try InappTargetingConfig.tagsFailedTargeting.getConfig() + + await handleInapps(event: nil, config: config) + + assertTargetingShows(id: "2") + let trackedTags = mockDataFacade.trackTargetingCalls.first(where: { $0.id == "2" })?.tags + #expect(trackedTags == ["templateType": "Snackbar"]) + #expect(mockDataFacade.downloadImageTags["2"] == ["templateType": "Snackbar"]) + } + @Test("Image download error adds in-app show failure", .tags(.remainingTargeting)) func imageDownloadError_addsFailure() async throws { let config = try InappTargetingConfig.oneTargeting.getConfig() diff --git a/MindboxTests/InApp/Tests/InAppMessagesTrackerTests.swift b/MindboxTests/InApp/Tests/InAppMessagesTrackerTests.swift new file mode 100644 index 000000000..842525003 --- /dev/null +++ b/MindboxTests/InApp/Tests/InAppMessagesTrackerTests.swift @@ -0,0 +1,114 @@ +// +// InAppMessagesTrackerTests.swift +// MindboxTests +// +// Created by Akylbek Utekeshev on 01.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +@testable import Mindbox + +@Suite("InAppMessagesTracker tags gating tests") +final class InAppMessagesTrackerTests { + private struct DecodedBody: Decodable { + let inappId: String + let timeToDisplay: String? + let tags: [String: String]? + } + + private let databaseRepository = InAppMessagesTrackerDatabaseRepositoryMock() + private let featureToggleManager = FeatureToggleManager() + private lazy var tracker = InAppMessagesTracker(databaseRepository: databaseRepository, featureToggleManager: featureToggleManager) + + private func decodedBody() -> DecodedBody? { + guard let event = databaseRepository.createdEvents.first else { return nil } + return BodyDecoder(decodable: event.body)?.body + } + + /// Returns the top-level keys of the encoded event body. Used to assert that the + /// `tags` key is entirely absent (not merely `null`), which is what the contract requires. + private func bodyKeys() -> Set? { + guard let event = databaseRepository.createdEvents.first, + let data = event.body.data(using: .utf8), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return nil + } + return Set(object.keys) + } + + @Test("trackView includes tags when the feature is enabled", .tags(.inAppTags)) + func trackViewIncludesTagsWhenEnabled() throws { + try tracker.trackView(id: "inapp-1", timeToDisplay: "150", tags: ["templateType": "Popup"]) + #expect(decodedBody()?.tags == ["templateType": "Popup"]) + } + + @Test("trackView omits tags when the feature is disabled", .tags(.inAppTags)) + func trackViewOmitsTagsWhenDisabled() throws { + applyTagsToggle(enabled: false) + try tracker.trackView(id: "inapp-1", timeToDisplay: "150", tags: ["templateType": "Popup"]) + #expect(bodyKeys()?.contains("tags") == false) + } + + @Test("trackClick includes tags when the feature is enabled", .tags(.inAppTags)) + func trackClickIncludesTagsWhenEnabled() throws { + try tracker.trackClick(id: "inapp-2", tags: ["templateType": "Snackbar"]) + #expect(decodedBody()?.tags == ["templateType": "Snackbar"]) + } + + @Test("trackClick omits tags when the feature is disabled", .tags(.inAppTags)) + func trackClickOmitsTagsWhenDisabled() throws { + applyTagsToggle(enabled: false) + try tracker.trackClick(id: "inapp-2", tags: ["templateType": "Snackbar"]) + #expect(bodyKeys()?.contains("tags") == false) + } + + @Test("trackTargeting includes tags when the feature is enabled", .tags(.inAppTags)) + func trackTargetingIncludesTagsWhenEnabled() throws { + try tracker.trackTargeting(id: "inapp-3", tags: ["templateType": "Modal"]) + #expect(decodedBody()?.tags == ["templateType": "Modal"]) + } + + @Test("trackTargeting omits tags when the feature is disabled", .tags(.inAppTags)) + func trackTargetingOmitsTagsWhenDisabled() throws { + applyTagsToggle(enabled: false) + try tracker.trackTargeting(id: "inapp-3", tags: ["templateType": "Modal"]) + #expect(bodyKeys()?.contains("tags") == false) + } + + @Test("trackClick omits tags when nil tags are passed", .tags(.inAppTags)) + func trackClickOmitsNilTags() throws { + try tracker.trackClick(id: "inapp-4", tags: nil) + #expect(bodyKeys()?.contains("tags") == false) + } + + private func applyTagsToggle(enabled: Bool) { + featureToggleManager.applyFeatureToggles( + Settings.FeatureToggles(shouldSendInAppShowError: nil, shouldSendInAppTags: enabled, shouldPrewarmInAppWebView: nil, shouldCacheInAppWebView: nil) + ) + } +} + +private final class InAppMessagesTrackerDatabaseRepositoryMock: DatabaseRepositoryProtocol { + var limit: Int = 0 + var lifeLimitDate: Date? + var deprecatedLimit: Int = 0 + var onObjectsDidChange: (() -> Void)? + private(set) var createdEvents: [Event] = [] + + func create(event: Event) throws { + createdEvents.append(event) + } + + func readEvent(by transactionId: String) throws -> Event? { + createdEvents.first(where: { $0.transactionId == transactionId }) + } + + func update(event: Event) throws {} + func delete(event: Event) throws {} + func query(fetchLimit: Int, retryDeadline: TimeInterval) throws -> [Event] { [] } + func removeDeprecatedEventsIfNeeded() throws {} + func countDeprecatedEvents() throws -> Int { 0 } + func erase() throws { createdEvents.removeAll() } + func countEvents() throws -> Int { createdEvents.count } +} diff --git a/MindboxTests/InApp/Tests/InAppTagsGatingTests.swift b/MindboxTests/InApp/Tests/InAppTagsGatingTests.swift new file mode 100644 index 000000000..1b8f7d012 --- /dev/null +++ b/MindboxTests/InApp/Tests/InAppTagsGatingTests.swift @@ -0,0 +1,39 @@ +// +// InAppTagsGatingTests.swift +// MindboxTests +// +// Created by Akylbek Utekeshev on 01.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +@testable import Mindbox + +@Suite("gatedTags pure function tests") +struct InAppTagsGatingTests { + + @Test("Returns tags unchanged when feature is enabled and tags are non-empty", .tags(.inAppTags)) + func returnsTagsWhenEnabled() { + let tags: [String: String]? = ["templateType": "Popup"] + #expect(FeatureToggleManager.gatedTags(tags, isEnabled: true) == tags) + } + + @Test("Returns nil when feature is disabled, even with non-empty tags", .tags(.inAppTags)) + func returnsNilWhenDisabled() { + let tags: [String: String]? = ["templateType": "Popup"] + #expect(FeatureToggleManager.gatedTags(tags, isEnabled: false) == nil) + } + + @Test("Returns nil when tags are nil, regardless of feature state", .tags(.inAppTags)) + func returnsNilWhenTagsNil() { + let tags: [String: String]? = nil + #expect(FeatureToggleManager.gatedTags(tags, isEnabled: true) == nil) + #expect(FeatureToggleManager.gatedTags(tags, isEnabled: false) == nil) + } + + @Test("Returns nil when tags are empty, even when feature is enabled", .tags(.inAppTags)) + func returnsNilWhenTagsEmpty() { + let tags: [String: String]? = [:] + #expect(FeatureToggleManager.gatedTags(tags, isEnabled: true) == nil) + } +} diff --git a/MindboxTests/InApp/Tests/InappConfigurationDataFacade/InappConfigurationDataFacadeTests.swift b/MindboxTests/InApp/Tests/InappConfigurationDataFacade/InappConfigurationDataFacadeTests.swift index 4d13a7241..53ee99361 100644 --- a/MindboxTests/InApp/Tests/InappConfigurationDataFacade/InappConfigurationDataFacadeTests.swift +++ b/MindboxTests/InApp/Tests/InappConfigurationDataFacade/InappConfigurationDataFacadeTests.swift @@ -43,10 +43,10 @@ final class MockSegmentationService: SegmentationServiceProtocol { } final class MockInappShowFailureManager: InappShowFailureManagerProtocol { - private(set) var failures: [(inappId: String, reason: InAppShowFailureReason, details: String?)] = [] + private(set) var failures: [(inappId: String, reason: InAppShowFailureReason, details: String?, tags: [String: String]?)] = [] - func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?) { - failures.append((inappId: inappId, reason: reason, details: details)) + func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?, tags: [String: String]?) { + failures.append((inappId: inappId, reason: reason, details: details, tags: tags)) } func clearFailures() { @@ -193,13 +193,34 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { mockSegmentation.stubError = .serverError(.init(status: .internalServerError, errorMessage: "Internal Server error", httpStatusCode: 500)) dataFacade.fetchProductSegmentationIfNeeded(products: products) - dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-2", "inapp-other"]) + dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-2", "inapp-other"], tagsByInappId: [:]) XCTAssertEqual(mockFailureManager.failures.count, 1) XCTAssertEqual(Set(mockFailureManager.failures.map { $0.inappId }), Set(["inapp-2"])) XCTAssertTrue(mockFailureManager.failures.allSatisfy { $0.reason == .productSegmentRequestFailed }) } + func test_collectTargetingFailures_propagatesTagsByInappId() { + SessionTemporaryStorage.shared.viewProductOperation = "App.ViewProduct".lowercased() + let model = decodeInAppOperationJSONModel(from: """ + { "viewProduct": { "product": { "ids": { "website": "100" } } } } + """ + ) + let products = model!.viewProduct!.product + dataFacade.targetingChecker.event = ApplicationEvent(name: "App.ViewProduct", model: model) + dataFacade.targetingChecker.context.productSegmentInapps = ["inapp-tagged"] + mockSegmentation.stubError = .serverError(.init(status: .internalServerError, errorMessage: "Internal Server error", httpStatusCode: 500)) + + dataFacade.fetchProductSegmentationIfNeeded(products: products) + dataFacade.collectTargetingFailures( + forFailedTargetingInappIds: ["inapp-tagged"], + tagsByInappId: ["inapp-tagged": ["templateType": "Popup"]] + ) + + XCTAssertEqual(mockFailureManager.failures.count, 1) + XCTAssertEqual(mockFailureManager.failures.first?.tags, ["templateType": "Popup"]) + } + func test_doNotAddFailure_whenProductSegmentationReturnsNonServerError() { SessionTemporaryStorage.shared.viewProductOperation = "App.ViewProduct".lowercased() let model = decodeInAppOperationJSONModel(from: """ @@ -212,7 +233,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { mockSegmentation.stubError = .protocolError(.init(status: .protocolError, errorMessage: "Bad request", httpStatusCode: 400)) dataFacade.fetchProductSegmentationIfNeeded(products: products) - dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-1"]) + dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-1"], tagsByInappId: [:]) XCTAssertTrue(mockFailureManager.failures.isEmpty) } @@ -228,7 +249,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { } waitForExpectations(timeout: 1) - dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment-2", "inapp-other"]) + dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment-2", "inapp-other"], tagsByInappId: [:]) XCTAssertEqual(mockFailureManager.failures.count, 1) XCTAssertEqual(Set(mockFailureManager.failures.map { $0.inappId }), Set(["inapp-segment-2"])) @@ -282,7 +303,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { firstExpectation.fulfill() } waitForExpectations(timeout: 1) - facade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment"]) + facade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment"], tagsByInappId: [:]) shouldReturnFailure = false let secondExpectation = expectation(description: "second fetch dependencies") @@ -290,7 +311,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { secondExpectation.fulfill() } waitForExpectations(timeout: 1) - facade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment"]) + facade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment"], tagsByInappId: [:]) XCTAssertEqual(requestCallCount, 1) XCTAssertEqual(localFailureManager.failures.count, 2) @@ -309,7 +330,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { } waitForExpectations(timeout: 1) - dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment"]) + dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-segment"], tagsByInappId: [:]) XCTAssertTrue(mockFailureManager.failures.isEmpty) } @@ -326,7 +347,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { } waitForExpectations(timeout: 1) - dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-geo", "inapp-other"]) + dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-geo", "inapp-other"], tagsByInappId: [:]) XCTAssertEqual(mockFailureManager.failures.count, 1) XCTAssertEqual(mockFailureManager.failures.first?.inappId, "inapp-geo") @@ -344,7 +365,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { firstExpectation.fulfill() } waitForExpectations(timeout: 1) - dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-geo"]) + dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-geo"], tagsByInappId: [:]) networkFetcher?.error = nil let secondExpectation = expectation(description: "second fetch dependencies") @@ -352,7 +373,7 @@ final class InAppConfigurationDataFacadeTests: XCTestCase { secondExpectation.fulfill() } waitForExpectations(timeout: 1) - dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-geo"]) + dataFacade.collectTargetingFailures(forFailedTargetingInappIds: ["inapp-geo"], tagsByInappId: [:]) XCTAssertEqual(mockFailureManager.failures.count, 2) XCTAssertTrue(mockFailureManager.failures.allSatisfy { $0.reason == .geoRequestFailed }) diff --git a/MindboxTests/InApp/Tests/InappScheduleManagerTests.swift b/MindboxTests/InApp/Tests/InappScheduleManagerTests.swift index 8bcc3ac71..e3e92bb32 100644 --- a/MindboxTests/InApp/Tests/InappScheduleManagerTests.swift +++ b/MindboxTests/InApp/Tests/InappScheduleManagerTests.swift @@ -39,31 +39,31 @@ struct InappScheduleManagerTests { // MARK: - No delay - @Test("In-app without delay is scheduled and presented immediately", .tags(.inAppSchedule)) + @Test("In-app without delay is presented exactly once and the queue is cleaned up", .tags(.inAppSchedule)) func scheduleInapp_noDelay_schedulesCorrectly() { #expect(scheduleManager.inappsByPresentationTime.isEmpty) + #expect(scheduleManager.getDelay(nil) == 0) let inapp = createInAppFormData(id: "1", isPriority: false, delayTime: nil) scheduleManager.scheduleInApp(inapp, processingDuration: 0) + // delayTime == nil ⇒ delay 0, so the production DispatchSourceTimer is armed + // for `.now()` and can fire and present on its own the instant the host app is + // foregrounded, racing any "scheduled but not yet shown" inspection. That + // intermediate state is not observable here and contradicts the no-delay + // contract, so we assert only the deterministic end state. If the timer has not + // already presented (e.g. host app backgrounded), drive it manually — this is + // idempotent because showEligibleInapp removes the entry under `queue`, so the + // in-app is presented at most once whichever path wins. var presentationTime: TimeInterval? - scheduleManager.queue.sync { - #expect(self.scheduleManager.inappsByPresentationTime.count == 1) presentationTime = self.scheduleManager.inappsByPresentationTime.keys.first - - let storedInapp = self.scheduleManager.inappsByPresentationTime.values.first?.first?.inapp - #expect(storedInapp?.inAppId == inapp.inAppId) - #expect(self.presentationManagerMock.presentCallsCount == 0) } - guard let time = presentationTime else { - Issue.record("Expected presentationTime to be set") - return + if let presentationTime { + scheduleManager.showEligibleInapp(presentationTime) } - scheduleManager.showEligibleInapp(time) - scheduleManager.queue.sync { #expect(self.presentationManagerMock.presentCallsCount == 1) #expect(self.presentationManagerMock.receivedInAppUIModel?.inAppId == inapp.inAppId) @@ -212,23 +212,22 @@ struct InappScheduleManagerTests { let inapp = createInAppFormData(id: "1", isPriority: false, delayTime: "invalid_time") scheduleManager.scheduleInApp(inapp, processingDuration: 0) + // An invalid delay string falls back to 0, so the timer can auto-fire and + // present immediately, racing inspection — see scheduleInapp_noDelay_schedulesCorrectly. + // Drive presentation only if the timer has not already done so. var presentationTime: TimeInterval? - scheduleManager.queue.sync { - #expect(self.scheduleManager.inappsByPresentationTime.count == 1) presentationTime = self.scheduleManager.inappsByPresentationTime.keys.first } - guard let time = presentationTime else { - Issue.record("Expected presentationTime to be set") - return + if let presentationTime { + scheduleManager.showEligibleInapp(presentationTime) } - scheduleManager.showEligibleInapp(time) - scheduleManager.queue.sync { #expect(self.presentationManagerMock.presentCallsCount == 1) #expect(self.presentationManagerMock.receivedInAppUIModel?.inAppId == inapp.inAppId) + #expect(self.scheduleManager.inappsByPresentationTime.isEmpty) } } @@ -239,23 +238,22 @@ struct InappScheduleManagerTests { let inapp = createInAppFormData(id: "1", isPriority: false, delayTime: "00:00:00") scheduleManager.scheduleInApp(inapp, processingDuration: 0) + // Zero delay ⇒ the timer can auto-fire and present immediately, racing + // inspection — see scheduleInapp_noDelay_schedulesCorrectly. Drive presentation + // only if the timer has not already done so. var presentationTime: TimeInterval? - scheduleManager.queue.sync { - #expect(self.scheduleManager.inappsByPresentationTime.count == 1) presentationTime = self.scheduleManager.inappsByPresentationTime.keys.first } - guard let time = presentationTime else { - Issue.record("Expected presentationTime to be set") - return + if let presentationTime { + scheduleManager.showEligibleInapp(presentationTime) } - scheduleManager.showEligibleInapp(time) - scheduleManager.queue.sync { #expect(self.presentationManagerMock.presentCallsCount == 1) #expect(self.presentationManagerMock.receivedInAppUIModel?.inAppId == inapp.inAppId) + #expect(self.scheduleManager.inappsByPresentationTime.isEmpty) } } @@ -366,6 +364,17 @@ struct InappScheduleManagerTests { #expect(failureManagerMock.sendFailuresCallCount == 1) } + @Test("In-app error callback propagates the in-app tags into the show failure", .tags(.inAppSchedule, .inAppTags)) + func presentInapp_onError_propagatesTags() { + let tags = ["templateType": "Modal"] + let inapp = createInAppFormData(id: "error-tags-id", isPriority: false, delayTime: nil, tags: tags) + + scheduleManager.presentInapp(inapp, stopwatch: ForegroundStopwatch()) + presentationManagerMock.receivedOnError?(.failedToLoadWindow) + + #expect(failureManagerMock.addFailureCalls.first?.tags == tags) + } + @Test("In-app error callback maps error to show failure payload", .tags(.inAppSchedule)) func presentInapp_onError_mapsToFailureReasonAndDetails() { let cases: [(InAppPresentationError, InAppShowFailureReason, String)] = [ @@ -420,7 +429,7 @@ struct InappScheduleManagerTests { // MARK: - Helpers - private func createInAppFormData(id: String, isPriority: Bool, delayTime: String?) -> InAppFormData { + private func createInAppFormData(id: String, isPriority: Bool, delayTime: String?, tags: [String: String]? = nil) -> InAppFormData { let modalVariant = ModalFormVariant(content: createMockContent()) let content: MindboxFormVariant = .modal(modalVariant) let onceFrequency = OnceFrequency(kind: .session) @@ -433,7 +442,8 @@ struct InappScheduleManagerTests { imagesDict: [:], firstImageValue: "", content: content, - frequency: frequency + frequency: frequency, + tags: tags ) } @@ -463,6 +473,7 @@ final class InappShowFailureManagerMock: InappShowFailureManagerProtocol { let inappId: String let reason: InAppShowFailureReason let details: String? + let tags: [String: String]? } private(set) var addFailureCallCount = 0 @@ -470,9 +481,9 @@ final class InappShowFailureManagerMock: InappShowFailureManagerProtocol { private(set) var sendFailuresCallCount = 0 private(set) var addFailureCalls: [AddFailureCall] = [] - func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?) { + func addFailure(inappId: String, reason: InAppShowFailureReason, details: String?, tags: [String: String]?) { addFailureCallCount += 1 - addFailureCalls.append(AddFailureCall(inappId: inappId, reason: reason, details: details)) + addFailureCalls.append(AddFailureCall(inappId: inappId, reason: reason, details: details, tags: tags)) } func clearFailures() { diff --git a/MindboxTests/InApp/Tests/InappShowFailureManagerTests.swift b/MindboxTests/InApp/Tests/InappShowFailureManagerTests.swift index 0e670d3c7..37e60b190 100644 --- a/MindboxTests/InApp/Tests/InappShowFailureManagerTests.swift +++ b/MindboxTests/InApp/Tests/InappShowFailureManagerTests.swift @@ -9,6 +9,7 @@ import XCTest import UIKit @testable import Mindbox +@testable import MindboxLogger final class InappShowFailureManagerTests: XCTestCase { private var databaseRepository: InappShowFailureDatabaseRepositoryMock! @@ -36,7 +37,8 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-1", reason: .presentationFailed, - details: "No window available" + details: "No window available", + tags: nil ) manager.sendFailures() @@ -56,7 +58,8 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-2", reason: .unknownError, - details: nil + details: nil, + tags: nil ) manager.sendFailures() @@ -65,19 +68,30 @@ final class InappShowFailureManagerTests: XCTestCase { let event = try XCTUnwrap(databaseRepository.createdEvents.first) let failure = try XCTUnwrap(decodeFailures(from: event)?.first) XCTAssertFalse(failure.dateTimeUtc.isEmpty) - XCTAssertNotNil(makeUTCFormatter().date(from: failure.dateTimeUtc)) + XCTAssertNotNil(failure.dateTimeUtc.toDate(withFormat: .utc)) + + let dateTimeUtc = failure.dateTimeUtc + XCTAssertEqual(dateTimeUtc.count, 20) + XCTAssertTrue(dateTimeUtc.hasSuffix("Z")) + XCTAssertFalse(dateTimeUtc.contains("AM")) + XCTAssertFalse(dateTimeUtc.contains("PM")) + XCTAssertFalse(dateTimeUtc.contains(" ")) + let pattern = #"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"# + XCTAssertNotNil(dateTimeUtc.range(of: pattern, options: .regularExpression)) } func testAddFailure_duplicateInappId_isIgnored() throws { manager.addFailure( inappId: "inapp-duplicate", reason: .imageDownloadFailed, - details: "first" + details: "first", + tags: nil ) manager.addFailure( inappId: "inapp-duplicate", reason: .unknownError, - details: "second" + details: "second", + tags: nil ) manager.sendFailures() @@ -94,17 +108,20 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-priority", reason: .productSegmentRequestFailed, - details: "product" + details: "product", + tags: nil ) manager.addFailure( inappId: "inapp-priority", reason: .geoRequestFailed, - details: "geo" + details: "geo", + tags: nil ) manager.addFailure( inappId: "inapp-priority", reason: .customerSegmentRequestFailed, - details: "segment" + details: "segment", + tags: nil ) manager.sendFailures() @@ -121,17 +138,20 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-priority-no-downgrade", reason: .customerSegmentRequestFailed, - details: "segment" + details: "segment", + tags: nil ) manager.addFailure( inappId: "inapp-priority-no-downgrade", reason: .geoRequestFailed, - details: "geo" + details: "geo", + tags: nil ) manager.addFailure( inappId: "inapp-priority-no-downgrade", reason: .productSegmentRequestFailed, - details: "product" + details: "product", + tags: nil ) manager.sendFailures() @@ -148,7 +168,8 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-clear", reason: .presentationFailed, - details: "clear me" + details: "clear me", + tags: nil ) manager.clearFailures() manager.sendFailures() @@ -160,7 +181,8 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-send-success", reason: .presentationFailed, - details: nil + details: nil, + tags: nil ) manager.sendFailures() @@ -173,7 +195,8 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-retry", reason: .unknownError, - details: "will retry" + details: "will retry", + tags: nil ) databaseRepository.createError = InappShowFailureRepositoryError.createFailed @@ -191,7 +214,8 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-add-disabled", reason: .presentationFailed, - details: "should be ignored" + details: "should be ignored", + tags: nil ) applyFeatureToggle(shouldSendInAppShowError: true) @@ -203,7 +227,7 @@ final class InappShowFailureManagerTests: XCTestCase { func testAddFailure_errorDetailsBelowLimit_isNotTruncated() throws { let details = String(repeating: "a", count: InappShowFailureManager.errorDetailsLimit - 1) - manager.addFailure(inappId: "inapp-below-limit", reason: .unknownError, details: details) + manager.addFailure(inappId: "inapp-below-limit", reason: .unknownError, details: details, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -216,7 +240,7 @@ final class InappShowFailureManagerTests: XCTestCase { func testAddFailure_errorDetailsAtLimit_isNotTruncated() throws { let details = String(repeating: "b", count: InappShowFailureManager.errorDetailsLimit) - manager.addFailure(inappId: "inapp-at-limit", reason: .unknownError, details: details) + manager.addFailure(inappId: "inapp-at-limit", reason: .unknownError, details: details, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -230,7 +254,7 @@ final class InappShowFailureManagerTests: XCTestCase { let limit = InappShowFailureManager.errorDetailsLimit let details = String(repeating: "c", count: limit + 500) - manager.addFailure(inappId: "inapp-above-limit", reason: .unknownError, details: details) + manager.addFailure(inappId: "inapp-above-limit", reason: .unknownError, details: details, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -241,7 +265,7 @@ final class InappShowFailureManagerTests: XCTestCase { } func testAddFailure_errorDetailsNil_remainsNil() throws { - manager.addFailure(inappId: "inapp-nil-details", reason: .unknownError, details: nil) + manager.addFailure(inappId: "inapp-nil-details", reason: .unknownError, details: nil, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -251,7 +275,7 @@ final class InappShowFailureManagerTests: XCTestCase { } func testAddFailure_errorDetailsEmpty_remainsEmpty() throws { - manager.addFailure(inappId: "inapp-empty-details", reason: .unknownError, details: "") + manager.addFailure(inappId: "inapp-empty-details", reason: .unknownError, details: "", tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -265,7 +289,7 @@ final class InappShowFailureManagerTests: XCTestCase { // Cyrillic 'а' is 2 bytes in UTF-8: total = 2 * limit bytes. let details = String(repeating: "а", count: limit) - manager.addFailure(inappId: "inapp-multibyte", reason: .unknownError, details: details) + manager.addFailure(inappId: "inapp-multibyte", reason: .unknownError, details: details, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -283,7 +307,7 @@ final class InappShowFailureManagerTests: XCTestCase { // Cyrillic 'ё' is 2 bytes — appending it would overflow by 1 byte. let details = asciiPrefix + "ё" - manager.addFailure(inappId: "inapp-no-split", reason: .unknownError, details: details) + manager.addFailure(inappId: "inapp-no-split", reason: .unknownError, details: details, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -301,7 +325,7 @@ final class InappShowFailureManagerTests: XCTestCase { let asciiPrefix = String(repeating: "y", count: limit - 2) let details = asciiPrefix + "🙂" - manager.addFailure(inappId: "inapp-emoji", reason: .unknownError, details: details) + manager.addFailure(inappId: "inapp-emoji", reason: .unknownError, details: details, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -317,8 +341,8 @@ final class InappShowFailureManagerTests: XCTestCase { let limit = InappShowFailureManager.errorDetailsLimit let longDetails = String(repeating: "d", count: limit + 200) - manager.addFailure(inappId: "inapp-priority-truncate", reason: .productSegmentRequestFailed, details: "short") - manager.addFailure(inappId: "inapp-priority-truncate", reason: .customerSegmentRequestFailed, details: longDetails) + manager.addFailure(inappId: "inapp-priority-truncate", reason: .productSegmentRequestFailed, details: "short", tags: nil) + manager.addFailure(inappId: "inapp-priority-truncate", reason: .customerSegmentRequestFailed, details: longDetails, tags: nil) manager.sendFailures() assertCreatedEventsCountEventually(1) @@ -333,7 +357,8 @@ final class InappShowFailureManagerTests: XCTestCase { manager.addFailure( inappId: "inapp-toggle-disabled", reason: .presentationFailed, - details: "disabled" + details: "disabled", + tags: nil ) applyFeatureToggle(shouldSendInAppShowError: false) @@ -348,6 +373,60 @@ final class InappShowFailureManagerTests: XCTestCase { let failure = try XCTUnwrap(decodeFailures(from: event)?.first) XCTAssertEqual(failure.inappId, "inapp-toggle-disabled") } + + func testAddFailure_includesTags_whenTagsFeatureEnabled() throws { + manager.addFailure( + inappId: "inapp-tags-enabled", + reason: .presentationFailed, + details: nil, + tags: ["templateType": "Popup"] + ) + manager.sendFailures() + + assertCreatedEventsCountEventually(1) + let event = try XCTUnwrap(databaseRepository.createdEvents.first) + let failure = try XCTUnwrap(decodeFailures(from: event)?.first) + XCTAssertEqual(failure.tags, ["templateType": "Popup"]) + } + + func testAddFailure_omitsTags_whenTagsFeatureDisabled() throws { + applyTagsFeatureToggle(shouldSendInAppTags: false) + + manager.addFailure( + inappId: "inapp-tags-disabled", + reason: .presentationFailed, + details: nil, + tags: ["templateType": "Popup"] + ) + manager.sendFailures() + + assertCreatedEventsCountEventually(1) + let event = try XCTUnwrap(databaseRepository.createdEvents.first) + let failure = try XCTUnwrap(decodeFailures(from: event)?.first) + XCTAssertNil(failure.tags) + } + + func testAddFailure_priorityReplacement_alsoReplacesTags() throws { + manager.addFailure( + inappId: "inapp-priority-tags", + reason: .productSegmentRequestFailed, + details: "product", + tags: ["templateType": "First"] + ) + manager.addFailure( + inappId: "inapp-priority-tags", + reason: .customerSegmentRequestFailed, + details: "segment", + tags: ["templateType": "Second"] + ) + manager.sendFailures() + + assertCreatedEventsCountEventually(1) + let event = try XCTUnwrap(databaseRepository.createdEvents.first) + let failure = try XCTUnwrap(decodeFailures(from: event)?.first) + XCTAssertEqual(failure.failureReason, .customerSegmentRequestFailed) + XCTAssertEqual(failure.tags, ["templateType": "Second"]) + } } private extension InappShowFailureManagerTests { @@ -359,14 +438,6 @@ private extension InappShowFailureManagerTests { BodyDecoder(decodable: event.body)?.body.failures } - func makeUTCFormatter() -> DateFormatter { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" - return formatter - } - func applyFeatureToggle(shouldSendInAppShowError: Bool) { let settingsJSON = """ { @@ -380,6 +451,12 @@ private extension InappShowFailureManagerTests { featureToggleManager.applyFeatureToggles(settings?.featureToggles) } + func applyTagsFeatureToggle(shouldSendInAppTags: Bool) { + featureToggleManager.applyFeatureToggles( + Settings.FeatureToggles(shouldSendInAppShowError: nil, shouldSendInAppTags: shouldSendInAppTags, shouldPrewarmInAppWebView: nil, shouldCacheInAppWebView: nil) + ) + } + func assertCreatedEventsCountEventually( _ expectedCount: Int, timeout: TimeInterval = 1, @@ -711,7 +788,8 @@ final class WebViewControllerWindowProviderTests: XCTestCase { onCloseInApp: {}, onError: { _ in }, windowProvider: { window }, - operation: nil + operation: nil, + tags: nil ) sut.onInit() @@ -732,7 +810,7 @@ final class WebViewControllerWindowProviderTests: XCTestCase { private final class InAppMessagesTrackerMock: InAppMessagesTrackerProtocol { func trackView(id: String, timeToDisplay: String?, tags: [String: String]?) throws {} - func trackClick(id: String) throws {} + func trackClick(id: String, tags: [String: String]?) throws {} } private final class PresentationStrategyMock: PresentationStrategyProtocol { diff --git a/MindboxTests/InApp/Tests/ModalViewControllerLayoutTests.swift b/MindboxTests/InApp/Tests/ModalViewControllerLayoutTests.swift new file mode 100644 index 000000000..c0e5ba0ed --- /dev/null +++ b/MindboxTests/InApp/Tests/ModalViewControllerLayoutTests.swift @@ -0,0 +1,185 @@ +// +// ModalViewControllerLayoutTests.swift +// MindboxTests +// +// Created by Claude on 17.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +import UIKit +@testable import Mindbox + +/// Regression tests for the `ModalViewController` layout fix. +/// +/// `viewDidLayoutSubviews()` used to rebuild the elements on *every* layout pass, +/// which caused two bugs: +/// 1. an infinite layout loop (`addSubview` + constraint activation re-dirtied the +/// layout, re-triggering `viewDidLayoutSubviews`); +/// 2. unbounded growth of the `elements` array (old views were removed from the +/// superview but never dropped from `self.elements`, while `setupElements` +/// kept appending). +/// +/// The fix caches the inapp content size in `lastElementsLayoutSize` and only rebuilds +/// when it changes, and `setupElements()` now clears `elements` before re-adding. +@MainActor +@Suite("ModalViewController layout regression") +struct ModalViewControllerLayoutTests { + + // MARK: - Fixtures + + /// Modal config with one image background layer (source value `imageKey`) and one + /// close-button element. + private func makeModel(imageKey: String) throws -> ModalFormVariant { + let json = """ + { + "content": { + "background": { + "layers": [ + { + "$type": "image", + "action": { "$type": "redirectUrl", "intentPayload": "payload", "value": "https://example.com" }, + "source": { "$type": "url", "value": "\(imageKey)" } + } + ] + }, + "elements": [ + { + "$type": "closeButton", + "color": "#FFFFFF", + "lineWidth": 2, + "size": { "kind": "dp", "width": 24, "height": 24 }, + "position": { "margin": { "kind": "proportion", "top": 0.02, "right": 0.02, "left": 0.02, "bottom": 0.02 } } + } + ] + } + } + """ + let data = Data(json.utf8) + return try JSONDecoder().decode(ModalFormVariant.self, from: data) + } + + private func makeImage() -> UIImage { + let size = CGSize(width: 1, height: 1) + let renderer = UIGraphicsImageRenderer(size: size) + return renderer.image { context in + UIColor.red.setFill() + context.fill(CGRect(origin: .zero, size: size)) + } + } + + private func makeController( + model: ModalFormVariant, + imagesDict: [String: UIImage] + ) -> ModalViewController { + ModalViewController( + model: model, + id: "test-id", + imagesDict: imagesDict, + onPresented: {}, + onTapAction: { _, _ in }, + onClose: {} + ) + } + + /// Hosts the controller in a window of the given size and forces a real layout pass, + /// so the `InAppImageOnlyView` gets a non-zero frame. + private func host(_ vc: UIViewController, size: CGSize) -> UIWindow { + let window = UIWindow(frame: CGRect(origin: .zero, size: size)) + window.rootViewController = vc + window.makeKeyAndVisible() + window.layoutIfNeeded() + return window + } + + // MARK: - Tests + + /// Repeated layout passes at the same size must not grow the `elements` array + /// (guards against both the infinite loop and the array-growth bug). + @Test("No element growth across repeated layout passes at a stable size") + func noGrowthOnStableSize() throws { + let key = "image-key" + let vc = makeController(model: try makeModel(imageKey: key), imagesDict: [key: makeImage()]) + let window = host(vc, size: CGSize(width: 320, height: 568)) + defer { window.isHidden = true } + + // The first real layout builds exactly the close button. + #expect(vc.elements.count == 1) + + // Simulate many extra layout passes at the same size. + for _ in 0..<20 { + vc.viewDidLayoutSubviews() + } + vc.view.setNeedsLayout() + vc.view.layoutIfNeeded() + + #expect(vc.elements.count == 1, "elements array must not grow on stable-size layout passes") + } + + /// Root-cause guard for the infinite-loop bug: at a stable size, `setupElements()` + /// must NOT run again, so the element view instance stays identical across passes. + /// + /// This is stricter than `noGrowthOnStableSize`: because `setupElements()` now does + /// `removeAll()` + recreate, the count would stay 1 even if the size guard were + /// removed and the loop returned. Object identity catches a re-run that count cannot. + @Test("Stable size does not re-run setupElements (element instance is reused)") + func setupElementsNoGrowthOnStableSize() throws { + let key = "image-key" + let vc = makeController(model: try makeModel(imageKey: key), imagesDict: [key: makeImage()]) + let window = host(vc, size: CGSize(width: 320, height: 568)) + defer { window.isHidden = true } + + let initialElement = try #require(vc.elements.first) + + for _ in 0..<20 { + vc.viewDidLayoutSubviews() + #expect(vc.elements.first === initialElement, + "setupElements must not growth at a stable size — the size guard broke the layout loop") + } + } + + /// A genuine size change must rebuild the elements: the count stays correct (not + /// doubled) and the previous element view is removed from the hierarchy. + @Test("Elements are rebuilt — not duplicated — when the content size changes") + func rebuildOnSizeChange() throws { + let key = "image-key" + let vc = makeController(model: try makeModel(imageKey: key), imagesDict: [key: makeImage()]) + let window = host(vc, size: CGSize(width: 320, height: 568)) + defer { window.isHidden = true } + + #expect(vc.elements.count == 1) + let firstElement = try #require(vc.elements.first) + #expect(firstElement.superview != nil) + + // Change the window size so the InAppImageOnlyView frame (and thus content size) changes. + window.frame = CGRect(x: 0, y: 0, width: 414, height: 896) + vc.view.frame = window.bounds + window.setNeedsLayout() + window.layoutIfNeeded() + + #expect(vc.elements.count == 1, "size change must rebuild, not append") + let secondElement = try #require(vc.elements.first) + #expect(secondElement !== firstElement, "a new element view should be created on rebuild") + #expect(firstElement.superview == nil, "the previous element view must be removed from the superview") + } + + /// When there is no `InAppImageOnlyView` in `layers` (here: the image is missing from + /// `imagesDict`, so no layer view is created), layout must not crash and must not + /// build any elements. + @Test("No InAppImageOnlyView: no elements built, no crash") + func noInappViewMeansNoElements() throws { + let vc = makeController(model: try makeModel(imageKey: "image-key"), imagesDict: [:]) + let window = host(vc, size: CGSize(width: 320, height: 568)) + defer { window.isHidden = true } + + #expect(vc.layers.isEmpty) + #expect(vc.elements.isEmpty) + + // Extra passes must stay safe. + vc.viewDidLayoutSubviews() + vc.view.setNeedsLayout() + vc.view.layoutIfNeeded() + #expect(vc.elements.isEmpty) + } +} diff --git a/MindboxTests/InApp/Tests/TimeToDisplayBackgroundTests.swift b/MindboxTests/InApp/Tests/TimeToDisplayBackgroundTests.swift index 11334aa3e..db503e812 100644 --- a/MindboxTests/InApp/Tests/TimeToDisplayBackgroundTests.swift +++ b/MindboxTests/InApp/Tests/TimeToDisplayBackgroundTests.swift @@ -150,5 +150,5 @@ final class InAppMessagesTrackerSpyMock: InAppMessagesTrackerProtocol { lastTimeToDisplay = timeToDisplay } - func trackClick(id: String) throws {} + func trackClick(id: String, tags: [String: String]?) throws {} } diff --git a/MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift b/MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift index bd245386e..ba406fa24 100644 --- a/MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift +++ b/MindboxTests/InApp/Tests/TransparentViewSyncOperationResponseTests.swift @@ -118,43 +118,126 @@ struct TransparentViewSyncOperationResponseTests { } } - // MARK: - Failure (.connectionError) → .error with createJSON payload + // MARK: - Failure payloads: data contents only, no {type, data} envelope (MOBILE-197) - @Test("Connection failure becomes .error with MindboxError.createJSON payload") - func connectionError_becomesError() { + private func decodedErrorPayload(_ outgoing: BridgeMessage) throws -> [String: Any] { + #expect(outgoing.type == .error) + var jsonString: String? + if case .string(let value) = outgoing.payload { jsonString = value } + let json = try #require(jsonString, "Expected .string payload, got \(String(describing: outgoing.payload))") + let data = try #require(json.data(using: .utf8)) + let object = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any]) + #expect(object["type"] == nil, "Payload must not carry the {type, data} envelope") + #expect(object["data"] == nil, "Payload must not carry the {type, data} envelope") + return object + } + + @Test("Protocol error payload is the data contents: status, errorMessage, httpStatusCode, errorId") + func protocolError_payloadIsDataContentsOnly() throws { + let pe = ProtocolError(status: .protocolError, errorMessage: "Operation Test not found", httpStatusCode: 400, errorId: "error-id-1") + let outgoing = TransparentView.makeSyncOperationResponse( + result: .failure(.protocolError(pe)), + action: action, + id: requestId + ) + + let payload = try decodedErrorPayload(outgoing) + #expect(payload["status"] as? String == "ProtocolError") + #expect(payload["errorMessage"] as? String == "Operation Test not found") + #expect(payload["httpStatusCode"] as? String == "400") + #expect(payload["errorId"] as? String == "error-id-1") + } + + @Test("Server error payload is the data contents with InternalServerError status") + func serverError_payloadIsDataContentsOnly() throws { + let pe = ProtocolError(status: .internalServerError, errorMessage: "Something went wrong", httpStatusCode: 500) + let outgoing = TransparentView.makeSyncOperationResponse( + result: .failure(.serverError(pe)), + action: action, + id: requestId + ) + + let payload = try decodedErrorPayload(outgoing) + #expect(payload["status"] as? String == "InternalServerError") + #expect(payload["errorMessage"] as? String == "Something went wrong") + #expect(payload["httpStatusCode"] as? String == "500") + } + + @Test("Connection failure payload is the data contents without the NetworkError envelope") + func connectionError_payloadIsDataContentsOnly() throws { let outgoing = TransparentView.makeSyncOperationResponse( result: .failure(.connectionError), action: action, id: requestId ) - #expect(outgoing.type == .error) - if case .string(let value) = outgoing.payload { - #expect(value.contains("NetworkError"), "createJSON for connectionError produces a NetworkError envelope") - #expect(value.contains("Connection error")) - } else { - Issue.record("Expected .string payload") - } + let payload = try decodedErrorPayload(outgoing) + #expect(payload["httpStatusCode"] as? String == "null") + #expect(payload["errorMessage"] as? String == "Connection error") + } + + @Test("Validation error payload is the data contents with validationMessages") + func validationError_payloadIsDataContentsOnly() throws { + let ve = ValidationError( + status: .validationError, + validationMessages: [ValidationMessage(message: "Invalid email", location: "/customer/email")] + ) + let outgoing = TransparentView.makeSyncOperationResponse( + result: .failure(.validationError(ve)), + action: action, + id: requestId + ) + + let payload = try decodedErrorPayload(outgoing) + #expect(payload["status"] as? String == "ValidationError") + let messages = try #require(payload["validationMessages"] as? [[String: Any]]) + #expect(messages.count == 1) + #expect(messages.first?["message"] as? String == "Invalid email") + #expect(messages.first?["location"] as? String == "/customer/email") } - // MARK: - Failure (.protocolError) → .error with createJSON payload + @Test("Internal error payload is the data contents with errorKey") + func internalError_payloadIsDataContentsOnly() throws { + let outgoing = TransparentView.makeSyncOperationResponse( + result: .failure(.internalError(InternalError(errorKey: .parsing, reason: "Broken body"))), + action: action, + id: requestId + ) + + let payload = try decodedErrorPayload(outgoing) + #expect(payload["errorKey"] as? String == "Error_parsing") + #expect(payload["errorName"] as? String == "Broken body") + } + + @Test("Invalid response payload is the data contents with httpStatusCode from the response") + func invalidResponse_payloadIsDataContentsOnly() throws { + let url = try #require(URL(string: "https://api.mindbox.ru/v3/operations/sync")) + let httpResponse = try #require(HTTPURLResponse(url: url, statusCode: 403, httpVersion: nil, headerFields: nil)) - @Test("Protocol error becomes .error with MindboxError.createJSON payload") - func protocolError_becomesError() { - let pe = ProtocolError(status: .protocolError, errorMessage: "Bad", httpStatusCode: 400) let outgoing = TransparentView.makeSyncOperationResponse( - result: .failure(.protocolError(pe)), + result: .failure(.invalidResponse(httpResponse)), action: action, id: requestId ) - #expect(outgoing.type == .error) - if case .string(let value) = outgoing.payload { - #expect(value.contains("MindboxError")) - #expect(value.contains("ProtocolError")) - } else { - Issue.record("Expected .string payload") - } + let payload = try decodedErrorPayload(outgoing) + #expect(payload["httpStatusCode"] as? String == "403") + #expect((payload["errorMessage"] as? String)?.isEmpty == false) + } + + @Test("Unknown error payload is the data contents with errorKey 'unknown'") + func unknownError_payloadIsDataContentsOnly() throws { + let underlying = NSError(domain: "test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Something exploded"]) + + let outgoing = TransparentView.makeSyncOperationResponse( + result: .failure(.unknown(underlying)), + action: action, + id: requestId + ) + + let payload = try decodedErrorPayload(outgoing) + #expect(payload["errorKey"] as? String == "unknown") + #expect(payload["errorMessage"] as? String == "Something exploded") } // MARK: - id and action propagated diff --git a/MindboxTests/InApp/Tests/WebView/InAppWebViewCacheTests.swift b/MindboxTests/InApp/Tests/WebView/InAppWebViewCacheTests.swift new file mode 100644 index 000000000..8770daa52 --- /dev/null +++ b/MindboxTests/InApp/Tests/WebView/InAppWebViewCacheTests.swift @@ -0,0 +1,99 @@ +// +// InAppWebViewCacheTests.swift +// MindboxTests +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import Testing +import WebKit +@testable import Mindbox + +@Suite("InApp WebView data store", .tags(.webView)) +@MainActor +struct InAppWebViewDataStoreTests { + + /// Evaluates the lazily-initialized store here rather than first in a host app: a mangled + /// identifier literal traps in CI. The properties are the ones the cache design rests on — + /// one live instance (shared in-memory session), persistence (cache survives relaunch), + /// isolation from the host app's default store, and the exact identifier value: changing + /// it orphans every install's cached store, so it must never change unnoticed. + @Test + func sharedIsOneIsolatedPersistentStore() { + let store = InAppWebViewDataStore.shared() + #expect(store === InAppWebViewDataStore.shared()) + // iOS 17+: isolated persistent store. Below 17 the store IS .default() by product + // decision (some disk cache beats none), so isolation is only asserted here. + if #available(iOS 17.0, *) { + #expect(store !== WKWebsiteDataStore.default()) + #expect(store.isPersistent) + // UUIDv5 of "cloud.Mindbox.InAppWebViewDataStore" — see InAppWebViewDataStore. + #expect(store.identifier == UUID(uuidString: "9E350642-BB9F-5D4C-9981-94FFD93C2B57")) + } + } + + /// Kill-switch semantics: only an explicit `false` disables; an absent key, absent + /// section, or missing/unreadable config (nil from the repository) means enabled. + @Test + func cacheToggleResolvesFromCachedConfig() throws { + func config(_ json: String) throws -> ConfigResponse { + try JSONDecoder().decode(ConfigResponse.self, from: Data(json.utf8)) + } + + #expect(InAppWebViewDataStore.isCacheEnabled(in: nil)) + #expect(InAppWebViewDataStore.isCacheEnabled(in: try config(#"{"settings":{}}"#))) + #expect(InAppWebViewDataStore.isCacheEnabled(in: try config(#"{"settings":{"featureToggles":{}}}"#))) + #expect(InAppWebViewDataStore.isCacheEnabled( + in: try config(#"{"settings":{"featureToggles":{"MobileSdkShouldCacheInAppWebView":true}}}"#) + )) + #expect(!InAppWebViewDataStore.isCacheEnabled( + in: try config(#"{"settings":{"featureToggles":{"MobileSdkShouldCacheInAppWebView":false}}}"#) + )) + } + + @Test + func webViewTogglesDecodeFromFeatureTogglesSection() throws { + let json = #"{"settings":{"featureToggles":{"MobileSdkShouldPrewarmInAppWebView":false,"MobileSdkShouldCacheInAppWebView":true}}}"# + let config = try JSONDecoder().decode(ConfigResponse.self, from: Data(json.utf8)) + + let toggles = try #require(config.settings?.featureToggles) + #expect(toggles.shouldPrewarmInAppWebView == false) + #expect(toggles.shouldCacheInAppWebView == true) + } +} + +@Suite("InApp WebView HTML fetcher", .tags(.webView)) +struct InAppWebViewHTMLFetcherTests { + + /// In the test process the cache toggle latches enabled (no cached config on disk), + /// so this pins the caching path: revalidation, no host cookie jar, one stable session. + @Test + func cachingPathIsCookieLessRevalidatingAndStable() throws { + let url = try #require(URL(string: "https://inapp.local/popup")) + let (session, request) = InAppWebViewHTMLFetcher.sessionAndRequest(for: url) + + #expect(request.cachePolicy == .reloadRevalidatingCacheData) + #expect(session !== URLSession.shared) + #expect(session.configuration.httpShouldSetCookies == false) + #expect(session.configuration.httpCookieStorage == nil) + #expect(session.configuration.urlCache != nil) + #expect(session.configuration.urlCache !== URLCache.shared) + #expect(InAppWebViewHTMLFetcher.sessionAndRequest(for: url).session === session) + } +} + +@Suite("InApp WebView factory", .tags(.webView)) +@MainActor +struct InAppWebViewFactoryTests { + + @Test + func factoryConfiguresTheSharedStoreAndUserAgent() { + let webView = InAppWebViewFactory.make(userAgent: "test-ua") + #expect(webView.configuration.websiteDataStore === InAppWebViewDataStore.shared()) + #expect(webView.configuration.applicationNameForUserAgent == "test-ua") + #expect(webView.configuration.allowsInlineMediaPlayback) + #expect(InAppWebViewFactory.make().configuration.applicationNameForUserAgent == SDKUserAgent.build()) + } +} diff --git a/MindboxTests/InApp/Tests/WebView/JSONValueTagsMergeTests.swift b/MindboxTests/InApp/Tests/WebView/JSONValueTagsMergeTests.swift new file mode 100644 index 000000000..8fe28c0f1 --- /dev/null +++ b/MindboxTests/InApp/Tests/WebView/JSONValueTagsMergeTests.swift @@ -0,0 +1,93 @@ +// +// JSONValueTagsMergeTests.swift +// MindboxTests +// +// Created by Akylbek Utekeshev on 01.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +@_spi(Internal) @testable import Mindbox + +@Suite("JSONValue.mergingInAppTags tests") +struct JSONValueTagsMergeTests { + + @Test("Sets tags directly when body has no tags key", .tags(.inAppTags, .webView)) + func setsTagsWhenAbsent() { + let body: JSONValue = .object(["viewProduct": .string("value")]) + let merged = JSONValue.mergingInAppTags(["templateType": "Popup"], into: body) + + guard case .object(let dict) = merged else { + Issue.record("Expected merged body to be an object") + return + } + #expect(dict["tags"] == .object(["templateType": .string("Popup")])) + #expect(dict["viewProduct"] == .string("value")) + } + + @Test("Sets tags directly when existing tags value is null", .tags(.inAppTags, .webView)) + func setsTagsWhenNull() { + let body: JSONValue = .object(["tags": .null]) + let merged = JSONValue.mergingInAppTags(["templateType": "Popup"], into: body) + + guard case .object(let dict) = merged else { + Issue.record("Expected merged body to be an object") + return + } + #expect(dict["tags"] == .object(["templateType": .string("Popup")])) + } + + @Test("Adds only missing keys when tags already an object, client keys win", .tags(.inAppTags, .webView)) + func mergesMissingKeysOnly() { + let body: JSONValue = .object(["tags": .object(["campaign": .string("client-campaign")])]) + let merged = JSONValue.mergingInAppTags(["templateType": "Popup", "campaign": "server-campaign"], into: body) + + guard case .object(let dict) = merged, + case .object(let tags) = dict["tags"] else { + Issue.record("Expected merged body's tags to be an object") + return + } + #expect(tags["campaign"] == .string("client-campaign")) + #expect(tags["templateType"] == .string("Popup")) + } + + @Test("Leaves body untouched when existing tags value is not an object", .tags(.inAppTags, .webView)) + func leavesNonObjectTagsUntouched() { + let body: JSONValue = .object(["tags": .string("client-string")]) + let merged = JSONValue.mergingInAppTags(["templateType": "Popup"], into: body) + + #expect(merged == body) + } + + @Test("Leaves array-valued tags untouched", .tags(.inAppTags, .webView)) + func leavesArrayTagsUntouched() { + let body: JSONValue = .object(["tags": .array([.string("a")])]) + let merged = JSONValue.mergingInAppTags(["templateType": "Popup"], into: body) + + #expect(merged == body) + } + + @Test("Returns body unchanged when tags are nil", .tags(.inAppTags, .webView)) + func returnsBodyUnchangedWhenTagsNil() { + let body: JSONValue = .object(["viewProduct": .string("value")]) + let merged = JSONValue.mergingInAppTags(nil, into: body) + + #expect(merged == body) + } + + @Test("Returns body unchanged when tags are empty", .tags(.inAppTags, .webView)) + func returnsBodyUnchangedWhenTagsEmpty() { + let body: JSONValue = .object(["viewProduct": .string("value")]) + let merged = JSONValue.mergingInAppTags([:], into: body) + + #expect(merged == body) + } + + @Test("Returns body unchanged when body root is not an object", .tags(.inAppTags, .webView)) + func returnsBodyUnchangedWhenRootIsNotObject() { + let body: JSONValue = .array([.string("a")]) + let merged = JSONValue.mergingInAppTags(["templateType": "Popup"], into: body) + + #expect(merged == body) + } +} diff --git a/MindboxTests/InApp/Tests/WebView/MindboxWebBridgeTests.swift b/MindboxTests/InApp/Tests/WebView/MindboxWebBridgeTests.swift new file mode 100644 index 000000000..ef3cfbf65 --- /dev/null +++ b/MindboxTests/InApp/Tests/WebView/MindboxWebBridgeTests.swift @@ -0,0 +1,266 @@ +// +// MindboxWebBridgeTests.swift +// MindboxTests +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import WebKit +@_spi(Internal) @testable import Mindbox + +/// A reused (pre-warmed) WebView delivers navigation callbacks from previous owners' +/// loads; leftovers must never reach the show's delegate. +@Suite("MindboxWebBridge navigation staleness", .tags(.webView)) +@MainActor +struct MindboxWebBridgeStalenessTests { + + private final class DelegateSpy: WebBridgeNavigationDelegate { + private(set) var startCount = 0 + private(set) var finishCount = 0 + private(set) var failCount = 0 + private(set) var finishURLs: [URL?] = [] + + func webBridge(_ bridge: MindboxWebBridge, didStartProvisionalNavigation url: URL?) { startCount += 1 } + func webBridge(_ bridge: MindboxWebBridge, didFinishNavigation url: URL?) { + finishCount += 1 + finishURLs.append(url) + } + func webBridge(_ bridge: MindboxWebBridge, didFailProvisionalNavigation url: URL?, error: Error) { failCount += 1 } + func webBridge(_ bridge: MindboxWebBridge, decidePolicyFor url: URL?, navigationType: WKNavigationType, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + decisionHandler(.allow) + } + } + + private let webView: WKWebView + private let bridge: MindboxWebBridge + private let spy: DelegateSpy + // Source of real, distinct WKNavigation objects; separate from `webView` so its async + // delegate callbacks can never reach the bridge under test. + private let navigationFactory = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + + init() { + webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + bridge = MindboxWebBridge(webView: webView) + spy = DelegateSpy() + bridge.navigationDelegate = spy + } + + private func makeNavigation() -> WKNavigation { + // swiftlint:disable:next force_unwrapping + navigationFactory.loadHTMLString("", baseURL: nil)! + } + + @Test("Before the show's own load every navigation callback is stale") + func everythingIsStaleBeforeContentLoad() { + bridge.webView(webView, didStartProvisionalNavigation: makeNavigation()) + bridge.webView(webView, didFinish: makeNavigation()) + + #expect(spy.startCount == 0) + #expect(spy.finishCount == 0) + } + + @Test("Only the expected navigation's callbacks reach the delegate") + func strangerNavigationsAreFiltered() { + let expected = makeNavigation() + bridge.expectContentNavigation(expected) + + let stranger = makeNavigation() + bridge.webView(webView, didStartProvisionalNavigation: stranger) + bridge.webView(webView, didFinish: stranger) + #expect(spy.startCount == 0) + #expect(spy.finishCount == 0) + + bridge.webView(webView, didStartProvisionalNavigation: expected) + bridge.webView(webView, didFinish: expected) + #expect(spy.startCount == 1) + #expect(spy.finishCount == 1) + } + + @Test("After the expected navigation finished the filter opens for page-initiated navigations") + func filterOpensAfterExpectedFinish() { + let expected = makeNavigation() + bridge.expectContentNavigation(expected) + bridge.webView(webView, didFinish: expected) + #expect(spy.finishCount == 1) + + bridge.webView(webView, didFinish: makeNavigation()) + #expect(spy.finishCount == 2) + } + + @Test("A nil expected navigation (loadHTMLString returned nil) fails open, not closed") + func nilExpectedNavigationFailsOpen() { + bridge.expectContentNavigation(nil) + + bridge.webView(webView, didFinish: makeNavigation()) + + #expect(spy.finishCount == 1) + } + + @Test("A nil callback navigation fails open, mirroring the nil-expected case") + func nilCallbackNavigationFailsOpen() { + bridge.expectContentNavigation(makeNavigation()) + + bridge.webView(webView, didFailProvisionalNavigation: nil, withError: NSError(domain: "test", code: 2)) + #expect(spy.failCount == 1) + + bridge.webView(webView, didFinish: nil) + #expect(spy.finishCount == 1) + } + + @Test("Stale failure callbacks never close the show") + func staleFailuresAreFiltered() { + bridge.expectContentNavigation(makeNavigation()) + + bridge.webView(webView, didFailProvisionalNavigation: makeNavigation(), + withError: NSError(domain: "test", code: 1)) + + #expect(spy.failCount == 0) + } + + @Test("Stale non-provisional failures are filtered too") + func staleDidFailIsFiltered() { + bridge.expectContentNavigation(makeNavigation()) + + bridge.webView(webView, didFail: makeNavigation(), withError: NSError(domain: "test", code: 3)) + + #expect(spy.failCount == 0) + } + + @Test("The expected navigation's non-provisional failure reaches the delegate") + func expectedDidFailReachesDelegate() { + let expected = makeNavigation() + bridge.expectContentNavigation(expected) + + bridge.webView(webView, didFail: expected, withError: NSError(domain: "test", code: 4)) + + #expect(spy.failCount == 1) + } + + @Test("Only the show's own finish reports the content URL; page navigations report their real URL") + func finishReportsPerNavigationURL() { + let contentURL = URL(string: "https://content.mindbox.ru/index.html") + bridge.updateContentURL(contentURL) + let expected = makeNavigation() + bridge.expectContentNavigation(expected) + + bridge.webView(webView, didFinish: expected) + // The unloaded test instance's real URL is nil — distinct from contentURL. + bridge.webView(webView, didFinish: makeNavigation()) + + #expect(spy.finishURLs.count == 2) + #expect(spy.finishURLs.first == contentURL) + #expect(spy.finishURLs.last == URL?.none) + } +} + +/// Script messages have their own gate: until the show's own document commits, a reused +/// WebView's previous document (e.g. a dying prewarm page) can still post to the freshly +/// attached handler — answering it could present a page that must never be shown. +@Suite("MindboxWebBridge message gate", .tags(.webView)) +@MainActor +struct MindboxWebBridgeMessageGateTests { + + private final class MessageSpy: WebBridgeMessageDelegate { + private(set) var received: [BridgeMessage] = [] + func webBridge(_ bridge: MindboxWebBridge, didReceiveBridgeMessage message: BridgeMessage) { + received.append(message) + } + } + + /// WKScriptMessage's real initializer is WebKit-internal; the bridge only reads + /// `name` and `body`. + private final class FakeScriptMessage: WKScriptMessage { + private let fakeName: String + private let fakeBody: Any + + init(name: String, body: Any) { + self.fakeName = name + self.fakeBody = body + super.init() + } + + override var name: String { fakeName } + override var body: Any { fakeBody } + } + + private let webView: WKWebView + private let bridge: MindboxWebBridge + private let spy: MessageSpy + private let navigationFactory = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + + init() { + webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) + bridge = MindboxWebBridge(webView: webView) + spy = MessageSpy() + bridge.messageDelegate = spy + } + + private func makeNavigation() -> WKNavigation { + // swiftlint:disable:next force_unwrapping + navigationFactory.loadHTMLString("", baseURL: nil)! + } + + private func postValidMessage() throws { + let message = try #require(BridgeMessage(type: .request, action: "log", payload: "hi")) + let body = try #require(message.jsonString()) + bridge.userContentController( + webView.configuration.userContentController, + didReceive: FakeScriptMessage(name: Constants.WebViewBridgeJS.handlerName, body: body) + ) + } + + @Test("Messages are dropped until the show's own navigation commits") + func messagesGatedUntilExpectedCommit() throws { + try postValidMessage() + #expect(spy.received.isEmpty) + + let expected = makeNavigation() + bridge.expectContentNavigation(expected) + + try postValidMessage() + #expect(spy.received.isEmpty) + + // A stale (stranger) commit must not open the gate. + bridge.webView(webView, didCommit: makeNavigation()) + try postValidMessage() + #expect(spy.received.isEmpty) + + bridge.webView(webView, didCommit: expected) + try postValidMessage() + #expect(spy.received.count == 1) + } + + @Test("A nil expected navigation opens the gate at the first commit (fail-open)") + func nilExpectedNavigationGateOpensOnCommit() throws { + bridge.expectContentNavigation(nil) + + try postValidMessage() + #expect(spy.received.isEmpty) + + bridge.webView(webView, didCommit: makeNavigation()) + try postValidMessage() + #expect(spy.received.count == 1) + } + + @Test("A reload re-arms the gate until the new document commits") + func reloadRearmsGate() throws { + let first = makeNavigation() + bridge.expectContentNavigation(first) + bridge.webView(webView, didCommit: first) + bridge.webView(webView, didFinish: first) + try postValidMessage() + #expect(spy.received.count == 1) + + let reload = makeNavigation() + bridge.expectContentNavigation(reload) + try postValidMessage() + #expect(spy.received.count == 1) + + bridge.webView(webView, didCommit: reload) + try postValidMessage() + #expect(spy.received.count == 2) + } +} diff --git a/MindboxTests/InApp/Tests/WebView/TransparentViewJSBridgeTests.swift b/MindboxTests/InApp/Tests/WebView/TransparentViewJSBridgeTests.swift new file mode 100644 index 000000000..ee3f10a82 --- /dev/null +++ b/MindboxTests/InApp/Tests/WebView/TransparentViewJSBridgeTests.swift @@ -0,0 +1,210 @@ +// +// TransparentViewJSBridgeTests.swift +// MindboxTests +// +// Created by Akylbek Utekeshev on 08.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import UIKit +import WebKit +@_spi(Internal) @testable import Mindbox + +@MainActor +@Suite("TransparentView JS-bridge operation tags tests") +final class TransparentViewJSBridgeTests { + + private let featureToggleManager = FeatureToggleManager() + private let databaseRepository = DatabaseRepositorySpy() + private let eventRepository = EventRepositorySpy() + private let facade = WebViewFacadeSpy() + + init() { + TestConfiguration.configure() + } + + // MARK: - asyncOperation + + @Test("asyncOperation merges in-app tags into the operation body when the feature is enabled", .tags(.inAppTags, .webView)) + func asyncOperationMergesTagsWhenEnabled() throws { + let view = makeView(tags: ["templateType": "Popup"]) + send(.asyncOperation, payload: #"{"operation":"Test.Operation","body":{"field":"value"}}"#, to: view) + + let customEvent = try #require(queuedCustomEvent()) + #expect(customEvent.name == "Test.Operation") + + let body = try #require(decodedPayload(of: customEvent)) + #expect(body["field"] == .string("value")) + #expect(body["tags"] == .object(["templateType": .string("Popup")])) + #expect(facade.sentMessages.last?.type == .response) + } + + @Test("asyncOperation omits tags from the operation body when the feature is disabled", .tags(.inAppTags, .webView)) + func asyncOperationOmitsTagsWhenDisabled() throws { + applyTagsToggle(enabled: false) + let view = makeView(tags: ["templateType": "Popup"]) + send(.asyncOperation, payload: #"{"operation":"Test.Operation","body":{"field":"value"}}"#, to: view) + + let customEvent = try #require(queuedCustomEvent()) + let body = try #require(decodedPayload(of: customEvent)) + #expect(body["field"] == .string("value")) + #expect(body.keys.contains("tags") == false) + } + + @Test("asyncOperation keeps client-provided tag values on key collision", .tags(.inAppTags, .webView)) + func asyncOperationKeepsClientTagsOnCollision() throws { + let view = makeView(tags: ["templateType": "Popup", "source": "server"]) + send(.asyncOperation, payload: #"{"operation":"Test.Operation","body":{"tags":{"templateType":"client"}}}"#, to: view) + + let customEvent = try #require(queuedCustomEvent()) + let body = try #require(decodedPayload(of: customEvent)) + #expect(body["tags"] == .object([ + "templateType": .string("client"), + "source": .string("server") + ])) + } + + @Test("asyncOperation without a body responds with a bridge error and queues nothing", .tags(.inAppTags, .webView)) + func asyncOperationWithoutBodySendsBridgeError() throws { + let view = makeView(tags: ["templateType": "Popup"]) + send(.asyncOperation, payload: #"{"operation":"Test.Operation"}"#, to: view) + + #expect(databaseRepository.createdEvents.isEmpty) + #expect(facade.sentMessages.last?.type == .error) + } + + @Test("asyncOperation with an empty operation name responds with a bridge error and queues nothing", .tags(.inAppTags, .webView)) + func asyncOperationWithEmptyNameSendsBridgeError() throws { + let view = makeView(tags: ["templateType": "Popup"]) + send(.asyncOperation, payload: #"{"operation":"","body":{"field":"value"}}"#, to: view) + + #expect(databaseRepository.createdEvents.isEmpty) + #expect(facade.sentMessages.last?.type == .error) + } + + // MARK: - syncOperation + + @Test("syncOperation merges in-app tags into the operation body when the feature is enabled", .tags(.inAppTags, .webView)) + func syncOperationMergesTagsWhenEnabled() throws { + let view = makeView(tags: ["templateType": "Snackbar"]) + send(.syncOperation, payload: #"{"operation":"Test.Sync","body":{"field":"value"}}"#, to: view) + + let event = try #require(eventRepository.sentRawEvents.first) + let customEvent = try #require(BodyDecoder(decodable: event.body)?.body) + #expect(customEvent.name == "Test.Sync") + + let body = try #require(decodedPayload(of: customEvent)) + #expect(body["field"] == .string("value")) + #expect(body["tags"] == .object(["templateType": .string("Snackbar")])) + } + + @Test("syncOperation omits tags from the operation body when the feature is disabled", .tags(.inAppTags, .webView)) + func syncOperationOmitsTagsWhenDisabled() throws { + applyTagsToggle(enabled: false) + let view = makeView(tags: ["templateType": "Snackbar"]) + send(.syncOperation, payload: #"{"operation":"Test.Sync","body":{"field":"value"}}"#, to: view) + + let event = try #require(eventRepository.sentRawEvents.first) + let customEvent = try #require(BodyDecoder(decodable: event.body)?.body) + let body = try #require(decodedPayload(of: customEvent)) + #expect(body.keys.contains("tags") == false) + } + + // MARK: - Helpers + + private func makeView(tags: [String: String]?) -> TransparentView { + let view = TransparentView( + frame: .zero, + params: [:], + userAgent: "", + operation: nil, + inAppId: "inapp-1", + tags: tags + ) + view.facade = facade + view.featureToggleManager = featureToggleManager + view.databaseRepository = databaseRepository + view.eventRepository = eventRepository + return view + } + + private func send(_ action: BridgeMessage.Action, payload: String, to view: TransparentView) { + let bridge = MindboxWebBridge(webView: WKWebView()) + let message = BridgeMessage(type: .request, action: action, payload: .string(payload)) + view.webBridge(bridge, didReceiveBridgeMessage: message) + } + + private func queuedCustomEvent() -> CustomEvent? { + guard let event = databaseRepository.createdEvents.first else { return nil } + return BodyDecoder(decodable: event.body)?.body + } + + private func decodedPayload(of customEvent: CustomEvent) -> [String: JSONValue]? { + guard let data = customEvent.payload.data(using: .utf8) else { return nil } + return try? JSONDecoder().decode([String: JSONValue].self, from: data) + } + + private func applyTagsToggle(enabled: Bool) { + featureToggleManager.applyFeatureToggles( + Settings.FeatureToggles(shouldSendInAppShowError: nil, shouldSendInAppTags: enabled, shouldPrewarmInAppWebView: nil, shouldCacheInAppWebView: nil) + ) + } +} + +// MARK: - Spies + +private final class WebViewFacadeSpy: InappWebViewFacadeProtocol { + private(set) var sentMessages: [BridgeMessage] = [] + + func makeView() -> UIView { UIView() } + func loadHTML(baseUrl: String, contentUrl: String, onFailure: @escaping () -> Void) {} + func applyViewSettings(scrollViewDelegate: UIScrollViewDelegate?) {} + func cleanWebView() {} + func sendReadyEvent(id: UUID) {} + func sendToJS(_ message: BridgeMessage) { sentMessages.append(message) } + func evaluateJavaScript(_ script: String, completion: @escaping (Result) -> Void) {} + func setBridgeMessageDelegate(_ delegate: WebBridgeMessageDelegate?) {} + func setNavigationDelegate(_ delegate: WebBridgeNavigationDelegate?) {} +} + +private final class DatabaseRepositorySpy: DatabaseRepositoryProtocol { + var limit: Int = 0 + var lifeLimitDate: Date? + var deprecatedLimit: Int = 0 + var onObjectsDidChange: (() -> Void)? + private(set) var createdEvents: [Event] = [] + + func create(event: Event) throws { + createdEvents.append(event) + } + + func readEvent(by transactionId: String) throws -> Event? { + createdEvents.first(where: { $0.transactionId == transactionId }) + } + + func update(event: Event) throws {} + func delete(event: Event) throws {} + func query(fetchLimit: Int, retryDeadline: TimeInterval) throws -> [Event] { [] } + func removeDeprecatedEventsIfNeeded() throws {} + func countDeprecatedEvents() throws -> Int { 0 } + func erase() throws { createdEvents.removeAll() } + func countEvents() throws -> Int { createdEvents.count } +} + +private final class EventRepositorySpy: EventRepository { + private(set) var sentRawEvents: [Event] = [] + + func send(event: Event, completion: @escaping (Result) -> Void) { + completion(.success(())) + } + + func send(type: T.Type, event: Event, completion: @escaping (Result) -> Void) where T: Decodable {} + + func sendRaw(event: Event, completion: @escaping (Result) -> Void) { + sentRawEvents.append(event) + completion(.success(Data(#"{"status":"Success"}"#.utf8))) + } + + func cancelAllRequests() {} +} diff --git a/MindboxTests/InApp/Tests/WebView/WebViewReadyCheckerTests.swift b/MindboxTests/InApp/Tests/WebView/WebViewReadyCheckerTests.swift new file mode 100644 index 000000000..b72654d72 --- /dev/null +++ b/MindboxTests/InApp/Tests/WebView/WebViewReadyCheckerTests.swift @@ -0,0 +1,111 @@ +// +// WebViewReadyCheckerTests.swift +// MindboxTests +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import Mindbox + +@Suite("WebView ready check retry", .tags(.webView)) +struct WebViewReadyCheckerTests { + + /// Scripted evaluate answers + manual control over the retry schedule. + private final class Harness { + var answers: [Result] + private(set) var evaluateCount = 0 + private(set) var pendingWork: [() -> Void] = [] + + init(answers: [Result]) { + self.answers = answers + } + + func makeChecker() -> WebViewReadyChecker { + WebViewReadyChecker( + evaluate: { [self] _, completion in + evaluateCount += 1 + completion(answers.removeFirst()) + }, + schedule: { [self] _, work in pendingWork.append(work) } + ) + } + + func runPending() { + let work = pendingWork + pendingWork = [] + work.forEach { $0() } + } + } + + @Test("An immediately ready page passes on the first attempt") + func readyFirstAttempt() { + let harness = Harness(answers: [.success(true)]) + var readyCount = 0 + + harness.makeChecker().run(onReady: { readyCount += 1 }, onGiveUp: { _ in Issue.record("unexpected give-up") }) + + #expect(readyCount == 1) + #expect(harness.evaluateCount == 1) + #expect(harness.pendingWork.isEmpty) + } + + @Test("A module evaluating after didFinish passes on a retry instead of failing the show") + func readyAfterRetries() { + let harness = Harness(answers: [.success(false), .success(false), .success(true)]) + var readyCount = 0 + + let checker = harness.makeChecker() + checker.run(onReady: { readyCount += 1 }, onGiveUp: { _ in Issue.record("unexpected give-up") }) + harness.runPending() + harness.runPending() + + withExtendedLifetime(checker) {} + #expect(readyCount == 1) + #expect(harness.evaluateCount == 3) + } + + @Test("Evaluation errors are retried like a plain false") + func evaluationErrorRetries() { + let error = NSError(domain: "test", code: 1) + let harness = Harness(answers: [.failure(error), .success(true)]) + var readyCount = 0 + + let checker = harness.makeChecker() + checker.run(onReady: { readyCount += 1 }, onGiveUp: { _ in Issue.record("unexpected give-up") }) + harness.runPending() + + withExtendedLifetime(checker) {} + #expect(readyCount == 1) + } + + @Test("A page that never boots gives up only after the full retry budget") + func givesUpAfterBudget() { + let harness = Harness(answers: Array(repeating: .success(false), count: WebViewReadyChecker.maxAttempts)) + var giveUpFailure: String? + + let checker = harness.makeChecker() + checker.run(onReady: { Issue.record("unexpected ready") }, onGiveUp: { giveUpFailure = $0 }) + while !harness.pendingWork.isEmpty { + harness.runPending() + } + + withExtendedLifetime(checker) {} + #expect(harness.evaluateCount == WebViewReadyChecker.maxAttempts) + #expect(giveUpFailure == "window.bridgeMessagesHandlers.emit is missing") + } + + @Test("Cancel abandons the poll without ever resolving") + func cancelAbandonsSilently() { + let harness = Harness(answers: [.success(false), .success(true)]) + let checker = harness.makeChecker() + + checker.run(onReady: { Issue.record("resolved after cancel") }, onGiveUp: { _ in Issue.record("resolved after cancel") }) + checker.cancel() + harness.runPending() + + #expect(harness.evaluateCount == 1) + } +} diff --git a/MindboxTests/InApp/Tests/WebView/WebViewTimeoutErrorTests.swift b/MindboxTests/InApp/Tests/WebView/WebViewTimeoutErrorTests.swift new file mode 100644 index 000000000..c423c16fd --- /dev/null +++ b/MindboxTests/InApp/Tests/WebView/WebViewTimeoutErrorTests.swift @@ -0,0 +1,28 @@ +// +// WebViewTimeoutErrorTests.swift +// MindboxTests +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +@testable import Mindbox + +@Suite("WebView timeout error category", .tags(.webView)) +struct WebViewTimeoutErrorTests { + + /// The 7s init timeout stays the single closing authority, but monitoring must tell + /// "page loaded, JS bridge never appeared" apart from "page never finished loading". + @Test + func timeoutDistinguishesBridgeMissingFromLoadFailure() { + guard case .webviewPresentationFailed = WebViewController.timeoutError(readyCheckGaveUp: true, inAppId: "x") else { + Issue.record("expected webviewPresentationFailed when the ready check gave up") + return + } + guard case .webviewLoadFailed = WebViewController.timeoutError(readyCheckGaveUp: false, inAppId: "x") else { + Issue.record("expected webviewLoadFailed when the page never loaded") + return + } + } +} diff --git a/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewLearnedHostsStoreTests.swift b/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewLearnedHostsStoreTests.swift new file mode 100644 index 000000000..9a2f3aa34 --- /dev/null +++ b/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewLearnedHostsStoreTests.swift @@ -0,0 +1,97 @@ +// +// InAppWebViewLearnedHostsStoreTests.swift +// MindboxTests +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import Testing +@testable import Mindbox + +@Suite("InApp WebView learned hosts store", .tags(.webView)) +struct InAppWebViewLearnedHostsStoreTests { + + private let storage = MockPersistenceStorage() + private var store: InAppWebViewLearnedHostsStore { + InAppWebViewLearnedHostsStore(persistenceStorage: storage) + } + + @Test("Observed hosts merge in order without duplicates") + func mergesWithoutDuplicates() { + store.remember(["a.example", "b.example"], endpoint: "Endpoint") + store.remember(["b.example", "c.example", ""], endpoint: "Endpoint") + + #expect(store.hosts(endpoint: "Endpoint") == ["a.example", "b.example", "c.example"]) + } + + @Test("The oldest hosts are dropped beyond the cap") + func capsAtMaximumDroppingOldest() { + let cap = InAppWebViewLearnedHostsStore.maxHosts + + store.remember((0..", + "bad host with spaces", + "https://full.url/path", + "host/with/path", + "evil.example\">", + "a&b.example", + "" + ], + endpoint: "Endpoint" + ) + + #expect(store.hosts(endpoint: "Endpoint") == ["cdn.ok.example"]) + } + + @Test("Re-remembering only known hosts does not rewrite storage") + func noOpWriteWhenNothingNew() { + store.remember(["a.example", "b.example"], endpoint: "Endpoint") + let writesAfterFirst = storage.webViewLearnedHostsWriteCount + + // Every observed host is already known — the steady state after the first shows. + // The persisted value is unchanged either way, so the write count is the only + // observable proof the redundant UserDefaults write is skipped. + store.remember(["a.example", "b.example"], endpoint: "Endpoint") + #expect(storage.webViewLearnedHostsWriteCount == writesAfterFirst) + + // A genuinely new host still writes. + store.remember(["c.example"], endpoint: "Endpoint") + #expect(storage.webViewLearnedHostsWriteCount == writesAfterFirst + 1) + #expect(store.hosts(endpoint: "Endpoint") == ["a.example", "b.example", "c.example"]) + } + + @Test("Hosts are scoped per endpoint") + func endpointsAreIsolated() { + store.remember(["a.example"], endpoint: "First") + store.remember(["b.example"], endpoint: "Second") + + #expect(store.hosts(endpoint: "First") == ["a.example"]) + #expect(store.hosts(endpoint: "Second") == ["b.example"]) + } +} diff --git a/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewPrewarmPlannerTests.swift b/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewPrewarmPlannerTests.swift new file mode 100644 index 000000000..712028b3d --- /dev/null +++ b/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewPrewarmPlannerTests.swift @@ -0,0 +1,218 @@ +// +// InAppWebViewPrewarmPlannerTests.swift +// MindboxTests +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import Testing +@testable import Mindbox + +@Suite("InApp WebView prewarm planning", .tags(.webView)) +struct InAppWebViewPrewarmPlannerTests { + + private func webviewLayer( + baseUrl: String? = "https://inapp.local/popup", + contentUrl: String? = "https://mobile-static.mindbox.ru/stable/inapps/webview/content/index.html" + ) -> WebviewContentBackgroundLayerDTO { + WebviewContentBackgroundLayerDTO(baseUrl: baseUrl, contentUrl: contentUrl, params: nil) + } + + // MARK: Prewarm source (partition baseURL + contentURL from one layer) + + @Test("Prewarm source is taken from the first fully showable layer") + func prewarmSourcePicksFirstValid() throws { + let layers = [ + webviewLayer(baseUrl: nil), + webviewLayer(baseUrl: "https://inapp.local/popup"), + webviewLayer(baseUrl: "https://other.example/popup") + ] + let source = try #require(InAppWebViewPrewarmPlanner.prewarmSource(for: layers)) + #expect(source.baseURL.absoluteString == "https://inapp.local/popup") + } + + @Test("No layer with a usable baseUrl yields no prewarm source", arguments: [ + [] as [String?], + [nil], + [""] + ]) + func prewarmSourceMissing(baseUrls: [String?]) { + let layers = baseUrls.map { webviewLayer(baseUrl: $0) } + #expect(InAppWebViewPrewarmPlanner.prewarmSource(for: layers) == nil) + } + + @Test("Host-less baseUrls and layers without contentUrl don't donate a prewarm source") + func prewarmSourceRequiresHostAndContent() throws { + let hostless = webviewLayer(baseUrl: "popup") + let noContent = webviewLayer(contentUrl: nil) + let valid = webviewLayer() + + let source = try #require(InAppWebViewPrewarmPlanner.prewarmSource(for: [hostless, noContent, valid])) + #expect(source.baseURL.host == "inapp.local") + #expect(InAppWebViewPrewarmPlanner.prewarmSource(for: [hostless, noContent]) == nil) + } + + @Test("Non-https layers never donate a prewarm source") + func prewarmSourceRequiresHttps() throws { + let httpBase = webviewLayer(baseUrl: "http://insecure.local/popup") + let httpContent = webviewLayer(contentUrl: "http://cdn.example/index.html") + let valid = webviewLayer() + + #expect(InAppWebViewPrewarmPlanner.prewarmSource(for: [httpBase, httpContent]) == nil) + let source = try #require(InAppWebViewPrewarmPlanner.prewarmSource(for: [httpBase, httpContent, valid])) + #expect(source.baseURL.host == "inapp.local") + } + + @Test("Base and content URLs always come from the same layer — never mixed across layers") + func prewarmSourceNeverMixesLayers() throws { + let broken = webviewLayer(baseUrl: "not a url", contentUrl: "https://cdn.a/index.html") + let valid = webviewLayer(baseUrl: "https://inapp.local/popup", contentUrl: "https://cdn.b/index.html") + + let source = try #require(InAppWebViewPrewarmPlanner.prewarmSource(for: [broken, valid])) + #expect(source.baseURL.host == "inapp.local") + #expect(source.contentURL.absoluteString == "https://cdn.b/index.html") + } + + // MARK: Preconnect hosts + + @Test("Hosts are deduplicated, merged with API domain and learned hosts, and sorted") + func preconnectHostsMergesAllSources() { + let layers = [ + webviewLayer(contentUrl: "https://mobile-static.mindbox.ru/a/index.html"), + webviewLayer(contentUrl: "https://mobile-static.mindbox.ru/b/index.html") + ] + let hosts = InAppWebViewPrewarmPlanner.preconnectHosts( + layers: layers, + apiDomain: "api.mindbox.ru", + learnedHosts: ["web-static.mindbox.ru", "api.mindbox.ru"] + ) + #expect(hosts == ["api.mindbox.ru", "mobile-static.mindbox.ru", "web-static.mindbox.ru"]) + } + + @Test("Invalid content URLs and an absent API domain contribute nothing") + func preconnectHostsSkipsUnusableSources() { + let layers = [webviewLayer(contentUrl: nil), webviewLayer(contentUrl: "")] + let hosts = InAppWebViewPrewarmPlanner.preconnectHosts(layers: layers, apiDomain: nil, learnedHosts: []) + #expect(hosts.isEmpty) + + let emptyDomain = InAppWebViewPrewarmPlanner.preconnectHosts(layers: layers, apiDomain: "", learnedHosts: []) + #expect(emptyDomain.isEmpty) + } + + // MARK: Preconnect page + + @Test("Preconnect page hints every host and downloads nothing") + func preconnectHTMLContainsHints() { + let html = InAppWebViewPrewarmPlanner.preconnectHTML(hosts: ["a.example", "b.example"]) + #expect(html.contains("")) + #expect(html.contains("")) + #expect(!html.contains("", "spaced host.ru"] + ) + #expect(html.contains("https://ok.example")) + #expect(!html.contains(" ConfigResponse { + let bundle = Bundle(for: MindboxTests.self) + let url = try #require(bundle.url(forResource: name, withExtension: "json")) + return try JSONDecoder().decode(ConfigResponse.self, from: Data(contentsOf: url)) +} diff --git a/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewPrewarmServiceTests.swift b/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewPrewarmServiceTests.swift new file mode 100644 index 000000000..2255803b9 --- /dev/null +++ b/MindboxTests/InApp/Tests/WebViewPrewarmTests/InAppWebViewPrewarmServiceTests.swift @@ -0,0 +1,408 @@ +// +// InAppWebViewPrewarmServiceTests.swift +// MindboxTests +// +// Created by Sergei Semko on 06.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Foundation +import Testing +import WebKit +@testable import Mindbox + +@MainActor +@Suite("InApp WebView prewarm service guards", .tags(.webView)) +struct InAppWebViewPrewarmServiceTests { + + private final class SpyWebView: WKWebView { + private(set) var loadedHTMLCount = 0 + private(set) var loadedBaseURLs: [URL?] = [] + private(set) var stopLoadingCount = 0 + var stubbedIsLoading = false + + override var isLoading: Bool { stubbedIsLoading } + + override func loadHTMLString(_ string: String, baseURL: URL?) -> WKNavigation? { + loadedHTMLCount += 1 + loadedBaseURLs.append(baseURL) + return nil // spy only — keep WebKit from actually navigating in unit tests + } + + override func stopLoading() { + stopLoadingCount += 1 + } + } + + private let spy: SpyWebView + private let service: InAppWebViewPrewarmService + + init() throws { + self = try Self.init(cachedConfig: nil) + } + + private init(cachedConfig: ConfigResponse?) throws { + let spy = SpyWebView(frame: .zero, configuration: WKWebViewConfiguration()) + self.spy = spy + + let storage = MockPersistenceStorage() + storage.configuration = try MBConfiguration(endpoint: "Test.Endpoint", domain: "api.mindbox.ru") + storage.deviceUUID = "test-device-uuid" + + service = InAppWebViewPrewarmService( + persistenceStorage: storage, + makeWebView: { spy }, + fetchHTML: { _, completion in completion("content") }, + loadCachedConfig: { cachedConfig } + ) + } + + /// The service hops its mutations through `DispatchQueue.main.async`; one more hop + /// behind them guarantees they have run. + private func drainMainQueue() async { + await withCheckedContinuation { (continuation: CheckedContinuation) in + DispatchQueue.main.async { continuation.resume() } + } + } + + /// The cached-config head start hops through a background queue first — poll for its + /// main-queue effects instead of assuming scheduling order. + private func waitUntil(_ condition: @autoclosure () -> Bool) async throws { + for _ in 0..<100 where !condition() { + try await Task.sleep(nanoseconds: 20_000_000) + } + #expect(condition()) + } + + @Test("Without a cached webview config nothing is created at init") + func initWithoutCachedConfigCreatesNothing() async { + service.prewarmProcess() + service.prewarmProcess() + await drainMainQueue() + await drainMainQueue() + + #expect(spy.loadedHTMLCount == 0) + #expect(service.borrowWarmWebView() == nil) + } + + @Test("A cached config with webview in-apps starts the resource prewarm at init") + func headStartRunsFromCachedConfig() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + + suite.service.prewarmProcess() + + // preconnect + content page (the instance is created lazily by the prewarm itself) + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + } + + @Test("Resource prewarm runs once: preconnect page, then the content page with the prewarm params") + func resourcePrewarmRunsOnce() async throws { + let config = try loadPrewarmTestConfig("InAppWebviewValid") + service.prewarmResources(for: config) + service.prewarmResources(for: config) + await drainMainQueue() + await drainMainQueue() + + #expect(spy.loadedHTMLCount == 2) // one preconnect + one content page + + let contentBaseURL = try #require(spy.loadedBaseURLs.last ?? nil) + #expect(contentBaseURL.host == "inapp.local") + let query = try #require(URLComponents(url: contentBaseURL, resolvingAgainstBaseURL: false)?.queryItems) + #expect(query.contains(URLQueryItem(name: "prewarm", value: "1"))) + #expect(query.contains(URLQueryItem(name: "endpointId", value: "Test.Endpoint"))) + #expect(query.contains(URLQueryItem(name: "deviceUuid", value: "test-device-uuid"))) + + // The prewarm injects nothing into the page: user scripts persist across + // navigations and would leak into the borrowed instance's shows. + #expect(spy.configuration.userContentController.userScripts.isEmpty) + } + + @Test("The prewarm instance is pinned by the navigation policy until a show takes over") + func prewarmArmsNavigationPolicy() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + + #expect(suite.spy.navigationDelegate is InAppWebViewPrewarmNavigationPolicy) + } + + @Test("Borrow stops in-flight loading, tears the prewarm page down, and keeps the instance") + func borrowStopsLoadingAndKeepsInstance() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + + let borrowed = suite.service.borrowWarmWebView() + + #expect(borrowed === suite.spy) + #expect(suite.spy.stopLoadingCount == 1) + #expect(suite.spy.loadedHTMLCount == 3) // preconnect + content + the hard-kill blank at borrow + + // While lent to a live show the instance must never be stolen by a second borrow. + #expect(suite.service.borrowWarmWebView() == nil) + + suite.service.parkWarmWebView() + await suite.drainMainQueue() + #expect(suite.service.borrowWarmWebView() === suite.spy) + } + + @Test("Borrow refuses an instance whose prewarm navigation hasn't settled and parks it for the next show") + func borrowRefusesMidNavigationInstance() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + suite.spy.stubbedIsLoading = true + + // A mid-navigation document must never reach a show: its didFinish can fire + // before module scripts evaluate. + #expect(suite.service.borrowWarmWebView() == nil) + #expect(suite.spy.stopLoadingCount == 1) + #expect(suite.spy.loadedHTMLCount == 3) // head start (2) + park blank + + suite.spy.stubbedIsLoading = false + #expect(suite.service.borrowWarmWebView() === suite.spy) + } + + @Test("Resource prewarm never navigates a borrowed instance") + func resourcePrewarmSkippedAfterBorrow() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + _ = suite.service.borrowWarmWebView() + + suite.service.prewarmResources(for: try loadPrewarmTestConfig("InAppWebviewValid")) + await suite.drainMainQueue() + await suite.drainMainQueue() + + #expect(suite.spy.loadedHTMLCount == 3) // head start + borrow hard-kill; nothing more + } + + @Test("A show starting before any prewarm blocks later resource prewarms") + func showBeforePrewarmBlocksLaterPrewarm() async throws { + #expect(service.borrowWarmWebView() == nil) + + service.prewarmResources(for: try loadPrewarmTestConfig("InAppWebviewValid")) + await drainMainQueue() + await drainMainQueue() + + #expect(spy.loadedHTMLCount == 0) + } + + @Test("A config without webview in-apps releases the unused warm instance") + func noWebviewConfigReleasesInstance() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + + suite.service.prewarmResources(for: ConfigResponse()) + await suite.drainMainQueue() + + // An in-flight prewarm navigation must not keep burning bandwidth if anything + // briefly retains the dropped instance. + #expect(suite.spy.stopLoadingCount == 1) + #expect(suite.service.borrowWarmWebView() == nil) + } + + @Test("Parking an idle borrowed instance loads a blank page to stop hidden JS") + func parkingLoadsBlankPage() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + _ = suite.service.borrowWarmWebView() + + suite.service.parkWarmWebView() + await suite.drainMainQueue() + + #expect(suite.spy.loadedHTMLCount == 4) // head start (2) + borrow hard-kill + park blank + } + + @Test("An off-main borrow is refused without touching the warm instance") + func offMainBorrowRefusedSafely() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + + // Deliberate off-main access — exactly what the guard under test refuses. + nonisolated(unsafe) let service = suite.service + let refused = await withCheckedContinuation { (continuation: CheckedContinuation) in + DispatchQueue.global().async { + continuation.resume(returning: service.borrowWarmWebView() == nil) + } + } + + #expect(refused) + // Main-confined state untouched: the instance still serves a main-thread borrow. + #expect(suite.service.borrowWarmWebView() === suite.spy) + } + + @Test("A memory warning frees the parked instance; the disk cache keeps the prewarm benefit") + func memoryWarningFreesParkedInstance() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + + NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) + await suite.drainMainQueue() + + #expect(suite.spy.stopLoadingCount == 1) + // The next show creates its own WKWebView on the shared store instead. + #expect(suite.service.borrowWarmWebView() == nil) + } + + @Test("A memory warning never touches an instance lent to a live show") + func memoryWarningSparesLentInstance() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + suite.service.prewarmProcess() + try await suite.waitUntil(suite.spy.loadedHTMLCount == 2) + _ = suite.service.borrowWarmWebView() + + NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) + await suite.drainMainQueue() + + suite.service.parkWarmWebView() + await suite.drainMainQueue() + #expect(suite.service.borrowWarmWebView() === suite.spy) + } + + @Test("Observed hosts are persisted under the configuration endpoint") + func rememberObservedHostsPersistsUnderEndpoint() throws { + let storage = MockPersistenceStorage() + storage.configuration = try MBConfiguration(endpoint: "Test.Endpoint", domain: "api.mindbox.ru") + let service = InAppWebViewPrewarmService( + persistenceStorage: storage, + makeWebView: { SpyWebView(frame: .zero, configuration: WKWebViewConfiguration()) }, + fetchHTML: { _, completion in completion(nil) }, + loadCachedConfig: { nil } + ) + + service.rememberObservedHosts(["a.example", "b.example"]) + + #expect(storage.webViewLearnedHosts?["Test.Endpoint"] == ["a.example", "b.example"]) + } + + @Test("A content-page fetch failure degrades to preconnect-only") + func contentFetchFailureLeavesPreconnectOnly() async throws { + let storage = MockPersistenceStorage() + storage.configuration = try MBConfiguration(endpoint: "Test.Endpoint", domain: "api.mindbox.ru") + let spy = SpyWebView(frame: .zero, configuration: WKWebViewConfiguration()) + let service = InAppWebViewPrewarmService( + persistenceStorage: storage, + makeWebView: { spy }, + fetchHTML: { _, completion in completion(nil) }, + loadCachedConfig: { nil } + ) + + service.prewarmResources(for: try loadPrewarmTestConfig("InAppWebviewValid")) + await drainMainQueue() + await drainMainQueue() + + // Preconnect page loaded; the failed content fetch adds nothing. + #expect(spy.loadedHTMLCount == 1) + } + + // MARK: Feature toggle + + /// The valid webview config with an explicit `featureToggles` section. + private static func config(prewarmToggle: Bool?) throws -> ConfigResponse { + let base = try loadPrewarmTestConfig("InAppWebviewValid") + let toggles = Settings.FeatureToggles( + shouldSendInAppShowError: nil, + shouldSendInAppTags: nil, + shouldPrewarmInAppWebView: prewarmToggle, + shouldCacheInAppWebView: nil + ) + let settings = Settings( + operations: nil, ttl: nil, slidingExpiration: nil, + inapp: nil, featureToggles: toggles, baseAddresses: nil + ) + return ConfigResponse(inapps: base.inapps, settings: settings) + } + + @Test("Prewarm toggle off in the cached config skips the head start") + func prewarmToggleOffSkipsHeadStart() async throws { + let suite = try Self.init(cachedConfig: Self.config(prewarmToggle: false)) + + suite.service.prewarmProcess() + try await Task.sleep(nanoseconds: 100_000_000) + await suite.drainMainQueue() + + #expect(suite.spy.loadedHTMLCount == 0) + #expect(suite.service.borrowWarmWebView() == nil) + } + + @Test("Prewarm toggle off in the fresh config releases the stage-1 instance") + func prewarmToggleOffReleasesStageOneInstance() async throws { + service.prewarmResources(for: try loadPrewarmTestConfig("InAppWebviewValid")) + await drainMainQueue() + #expect(spy.loadedHTMLCount == 2) + + service.prewarmResources(for: try Self.config(prewarmToggle: false)) + await drainMainQueue() + + #expect(spy.stopLoadingCount >= 1) + #expect(service.borrowWarmWebView() == nil) + } + + @Test("A present section without the key, or an explicit true, keeps the prewarm on", + arguments: [nil, true] as [Bool?]) + func prewarmToggleAbsentOrTrueKeepsFeatureOn(_ toggle: Bool?) async throws { + service.prewarmResources(for: try Self.config(prewarmToggle: toggle)) + await drainMainQueue() + + #expect(spy.loadedHTMLCount == 2) + } + + @Test("A toggle-off release that beats the slow stage-1 hop still kills the head start") + func releaseBeforeStageOneBlocksHeadStart() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + + suite.service.prewarmResources(for: try Self.config(prewarmToggle: false)) + await suite.drainMainQueue() + + suite.service.prewarmProcess() + try await Task.sleep(nanoseconds: 100_000_000) + await suite.drainMainQueue() + + #expect(suite.spy.loadedHTMLCount == 0) + #expect(suite.service.borrowWarmWebView() == nil) + } + + @Test("A no-layers release also blocks a late stage-1 hop") + func noLayersReleaseBlocksLateStageOne() async throws { + let suite = try Self.init(cachedConfig: loadPrewarmTestConfig("InAppWebviewValid")) + + suite.service.prewarmResources(for: ConfigResponse()) + await suite.drainMainQueue() + + suite.service.prewarmProcess() + try await Task.sleep(nanoseconds: 100_000_000) + await suite.drainMainQueue() + + #expect(suite.spy.loadedHTMLCount == 0) + } +} + +/// A hidden prewarm WebView must never be able to wander off to arbitrary URLs. +@Suite("InApp WebView prewarm navigation policy", .tags(.webView)) +@MainActor +struct InAppWebViewPrewarmNavigationPolicyTests { + + private let policy = InAppWebViewPrewarmNavigationPolicy() + + @Test("SDK-issued documents are allowed; anything else on the main frame is cancelled") + func pinsMainFrameToIssuedDocuments() throws { + let issued = try #require(URL(string: "https://inapp.local/popup?prewarm=1")) + policy.allow(issued) + + #expect(policy.decision(for: issued, targetIsMainFrame: true) == .allow) + #expect(policy.decision(for: URL(string: "https://evil.example/landing"), targetIsMainFrame: true) == .cancel) + // The park/borrow blank load is always ours. + #expect(policy.decision(for: URL(string: "about:blank"), targetIsMainFrame: true) == .allow) + } + + @Test("Subframes stay the page's own business; a nil target frame (window.open) is pinned") + func subframesAllowedNilTargetPinned() { + #expect(policy.decision(for: URL(string: "https://ads.example/frame"), targetIsMainFrame: false) == .allow) + #expect(policy.decision(for: URL(string: "https://popup.example/win"), targetIsMainFrame: nil) == .cancel) + } +} diff --git a/MindboxTests/InitializationTests/DeviceUUIDInitializationTests.swift b/MindboxTests/InitializationTests/DeviceUUIDInitializationTests.swift new file mode 100644 index 000000000..3b576cba8 --- /dev/null +++ b/MindboxTests/InitializationTests/DeviceUUIDInitializationTests.swift @@ -0,0 +1,138 @@ +// +// DeviceUUIDInitializationTests.swift +// MindboxTests +// +// Created by Mindbox on 08.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +@testable import Mindbox + +// MARK: - deviceUUID / applicationInstanceId behavior on initialization + +@Suite(.serialized) +@MainActor +struct DeviceUUIDInitializationTests { + + private let storage: PersistenceStorage + private let coreController: CoreController + private let controllerQueue: DispatchQueue + + init() { + TestConfiguration.configure() + storage = DI.injectOrFail(PersistenceStorage.self) + storage.reset() + coreController = DI.injectOrFail(CoreController.self) + controllerQueue = coreController.controllerQueue + let databaseRepository = DI.injectOrFail(DatabaseRepositoryProtocol.self) + try? databaseRepository.erase() + } + + @Test("Honest reinstall: empty storage generates a deviceUUID and an applicationInstanceId") + func honestReinstallGeneratesNewIdentifiers() async throws { + storage.reset() + #expect(storage.deviceUUID == nil) + #expect(storage.installationDate == nil) + #expect(storage.applicationInstanceId == nil) + + let configuration = try MBConfiguration(plistName: "TestConfig1") + coreController.initialization(configuration: configuration) + await waitForInitializationFinished() + + let deviceUUID = try #require( + storage.deviceUUID, + "A genuine first installation must generate a deviceUUID." + ) + #expect(!deviceUUID.isEmpty, "Generated deviceUUID must not be empty.") + + let instanceId = try #require( + storage.applicationInstanceId, + "A genuine first installation must generate an applicationInstanceId." + ) + #expect(!instanceId.isEmpty, "Generated applicationInstanceId must not be empty.") + + cleanup() + } + + @Test("Persisted deviceUUID is reused on install, not regenerated") + func persistedDeviceUUIDIsReused() async throws { + storage.reset() + // Simulate a locale-driven re-installation: `isInstalled` became false (installationDate + // absent), yet a deviceUUID from a previous installation is still persisted. The fix must + // reuse that deviceUUID instead of generating a new one. + let persistedUUID = "00000000-0000-0000-0000-0000000000AA" + storage.deviceUUID = persistedUUID + #expect(storage.installationDate == nil) + #expect(storage.isInstalled == false) + + let configuration = try MBConfiguration(plistName: "TestConfig1") + coreController.initialization(configuration: configuration) + await waitForInitializationFinished() + + #expect( + storage.deviceUUID == persistedUUID, + "primaryInitialization must reuse the persisted deviceUUID, not regenerate it." + ) + + cleanup() + } + + @Test("Reinstall over an existing installation keeps deviceUUID and refreshes instanceId") + func reinstallKeepsDeviceUUIDRefreshesInstanceId() async throws { + let configuration = try MBConfiguration(plistName: "TestConfig1") + coreController.initialization(configuration: configuration) + await waitForInitializationFinished() + + let originalUUID = try #require( + storage.deviceUUID, + "deviceUUID must be set after the first installation." + ) + let originalInstanceId = try #require( + storage.applicationInstanceId, + "applicationInstanceId must be set after the first installation." + ) + #expect(storage.isInstalled, "Storage must be installed after the first initialization.") + + // Re-initialize with a configuration whose endpoint differs: changedState becomes `.rest`, + // so repeatInitialization runs install() again over the existing installation. + let changedConfiguration = try MBConfiguration( + endpoint: "app-with-hub-IOS-reinstall", + domain: "api.mindbox.ru" + ) + coreController.initialization(configuration: changedConfiguration) + await waitForInitializationFinished() + + #expect( + storage.deviceUUID == originalUUID, + "deviceUUID must stay stable across a reinstall over an existing installation." + ) + let newInstanceId = try #require( + storage.applicationInstanceId, + "applicationInstanceId must be present after reinstall." + ) + #expect( + newInstanceId != originalInstanceId, + "install() must generate a fresh applicationInstanceId on reinstall." + ) + + cleanup() + } + + // MARK: - Helpers + + private func waitForInitializationFinished() async { + await withCheckedContinuation { continuation in + controllerQueue.async { + continuation.resume() + } + } + } + + private func cleanup() { + storage.reset() + storage.userVisitCount = 0 + SessionTemporaryStorage.shared.erase() + SessionTemporaryStorage.shared.isInstalledFromPersistenceStorageBeforeInitSDK = false + } +} diff --git a/MindboxTests/InitializationTests/MBPersistenceStorageDateTests.swift b/MindboxTests/InitializationTests/MBPersistenceStorageDateTests.swift new file mode 100644 index 000000000..04830fcbb --- /dev/null +++ b/MindboxTests/InitializationTests/MBPersistenceStorageDateTests.swift @@ -0,0 +1,83 @@ +// +// MBPersistenceStorageDateTests.swift +// MindboxTests +// +// Created by Mindbox on 08.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import Mindbox + +// MARK: - Real MBPersistenceStorage: installed state + date round-trips + +@Suite(.serialized) +struct MBPersistenceStorageDateTests { + + private let storage: PersistenceStorage + + init() { + storage = DI.injectOrFail(PersistenceStorage.self) + storage.reset() + } + + // MARK: - isInstalled + + @Test("isInstalled is false when no installation-date string is stored") + func isInstalledFalseWithoutString() { + #expect(storage.installationDate == nil) + #expect(storage.isInstalled == false, "isInstalled must be false when nothing is persisted.") + } + + @Test("isInstalled becomes true once an installationDate is stored, false again after clearing") + func isInstalledTracksStringPresence() { + storage.installationDate = Date(timeIntervalSince1970: 1_700_000_000) + #expect(storage.isInstalled, "isInstalled must be true once an installationDate is persisted.") + + storage.installationDate = nil + #expect(storage.isInstalled == false, "isInstalled must be false after the installationDate is cleared.") + } + + // MARK: - Round-trip for all six UTC-backed date properties + + @Test("All six UTC date properties round-trip setter → getter at second precision") + func allDatesRoundTrip() { + // (name, setter, getter) for every Date property backed by a `.utc` string in production. + let cases: [(name: String, set: (Date?) -> Void, get: () -> Date?)] = [ + ("installationDate", + { storage.installationDate = $0 }, { storage.installationDate }), + ("firstInitializationDateTime", + { storage.firstInitializationDateTime = $0 }, { storage.firstInitializationDateTime }), + ("apnsTokenSaveDate", + { storage.apnsTokenSaveDate = $0 }, { storage.apnsTokenSaveDate }), + ("lastInfoUpdateDate", + { storage.lastInfoUpdateDate = $0 }, { storage.lastInfoUpdateDate }), + ("deprecatedEventsRemoveDate", + { storage.deprecatedEventsRemoveDate = $0 }, { storage.deprecatedEventsRemoveDate }), + ("configDownloadDate", + { storage.configDownloadDate = $0 }, { storage.configDownloadDate }), + ] + + // Whole-second timestamp: the `.utc` format ("yyyy-MM-dd'T'HH:mm:ss'Z'") has no sub-second + // component, so a round-trip is exact only at second precision. + let expectedEpoch = 1_700_000_000 + + for testCase in cases { + testCase.set(Date(timeIntervalSince1970: TimeInterval(expectedEpoch))) + let readBack = testCase.get() + + let epoch = readBack.map { Int($0.timeIntervalSince1970) } + #expect( + epoch == expectedEpoch, + "\(testCase.name) must round-trip through storage. Expected \(expectedEpoch), got \(String(describing: epoch))." + ) + + testCase.set(nil) + #expect( + testCase.get() == nil, + "\(testCase.name) must be nil after being cleared." + ) + } + } +} diff --git a/MindboxTests/MigrationsTests/DateFormatMigrationTests.swift b/MindboxTests/MigrationsTests/DateFormatMigrationTests.swift new file mode 100644 index 000000000..f81af13f8 --- /dev/null +++ b/MindboxTests/MigrationsTests/DateFormatMigrationTests.swift @@ -0,0 +1,240 @@ +// +// DateFormatMigrationTests.swift +// MindboxTests +// +// Created by Akylbek Utekeshev on 04.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import Mindbox +import MindboxLogger + +// A class-based suite so `deinit` can restore the global `MBPersistenceStorage.defaults` +// after every test. `.serialized` because the tests mutate that shared global. +@Suite(.serialized) +final class DateFormatMigrationTests { + + private let migration: MigrationProtocol + private let userDefaults: UserDefaults + private let userDefaultsSuiteName = "DateFormatMigrationTests" + private let originalDefaults: UserDefaults + + private let installationKey = "MBPersistenceStorage-installationData" + private let firstInitKey = "MBPersistenceStorage-firstInitializationDateTime" + private let apnsKey = "MBPersistenceStorage-apnsTokenSaveDate" + private let configKey = "MBPersistenceStorage-configDownloadDate" + private let lastInfoKey = "MBPersistenceStorage-lastInfoUpdateTime" + private let deprecatedKey = "MBPersistenceStorage-deprecatedEventsRemoveDate" + + private var allKeys: [String] { + [installationKey, firstInitKey, apnsKey, configKey, lastInfoKey, deprecatedKey] + } + + init() { + originalDefaults = MBPersistenceStorage.defaults + userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)! + userDefaults.removePersistentDomain(forName: userDefaultsSuiteName) + MBPersistenceStorage.defaults = userDefaults + migration = DateFormatMigration() + } + + deinit { + userDefaults.removePersistentDomain(forName: userDefaultsSuiteName) + // Restore the global so this suite never leaks its private suite into other tests. + MBPersistenceStorage.defaults = originalDefaults + } + + // MARK: - Helpers + + /// Produces a string in the legacy format (replica of the removed MBPersistenceStorage + /// formatter) under the device's current 12h/24h setting. + private func legacyString(from date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .full + formatter.timeStyle = .full + return formatter.string(from: date) + } + + /// Independent oracle for the hour-cycle rescue: builds a legacy `.full`/`.full` string with the + /// hour cycle pinned through the structured `Locale.Components` API — a *different* mechanism than + /// the `@hours=` identifier `DateFormatMigration` assembles itself. The base stays `Locale.current` + /// so only the hour cycle differs from the migration's candidates (the date layout still matches). + /// If the migration's hour-cycle keyword were wrong, this independently-built opposite-cycle string + /// would fail to parse — so the test proves the rescue works, not just that both sides build the + /// same identifier. + @available(iOS 16.0, *) + private func independentLegacyString(from date: Date, hourCycle: Locale.HourCycle) -> String { + var components = Locale.Components(locale: .current) + components.hourCycle = hourCycle + let formatter = DateFormatter() + formatter.locale = Locale(components: components) + formatter.dateStyle = .full + formatter.timeStyle = .full + return formatter.string(from: date) + } + + /// Whole-second instant so legacy (`.full`) and `.utc` representations round-trip exactly. + private var fixedDate: Date { + Date(timeIntervalSince1970: 1_700_000_000) + } +} + +// MARK: - Scenarios + +extension DateFormatMigrationTests { + + @Test("isNeeded is true when a legacy value is present") + func isNeededTrueWhenLegacyValuePresent() { + userDefaults.set(legacyString(from: fixedDate), forKey: installationKey) + #expect(migration.isNeeded) + } + + @Test("isNeeded is false and run() is a no-op when there are no values") + func isNeededFalseWhenNoValues() throws { + #expect(migration.isNeeded == false) + try migration.run() + } + + @Test("run() converts a legacy installation date and keeps isInstalled stable") + func runConvertsLegacyInstallationDateAndKeepsIsInstalledStable() throws { + userDefaults.set(legacyString(from: fixedDate), forKey: installationKey) + + // Before migration: the legacy string is unparseable by the new `.utc` reader, + // so the parsed `installationDate` is nil — but `isInstalled` stays true because + // it depends on the stored string's presence, not on parsing it. + let storageBefore = MBPersistenceStorage(defaults: userDefaults) + #expect(storageBefore.installationDate == nil) + #expect(storageBefore.isInstalled) + + try migration.run() + + // After migration: the date value parses again, and `isInstalled` is unchanged. + let storageAfter = MBPersistenceStorage(defaults: userDefaults) + #expect(storageAfter.installationDate != nil) + #expect(storageAfter.isInstalled) + #expect( + storageAfter.installationDate.map { Int($0.timeIntervalSince1970) } + == Int(fixedDate.timeIntervalSince1970) + ) + #expect(migration.isNeeded == false) + } + + /// The original re-installation bug: a value written under 12h must still convert when the + /// migration runs (even if the device is now in 24h). The 12h candidate rescues it. + @available(iOS 16.0, *) + @Test("run() converts a legacy installation date written in 12h format") + func runConvertsLegacyInstallationDateWrittenIn12hFormat() throws { + userDefaults.set( + independentLegacyString(from: fixedDate, hourCycle: .oneToTwelve), + forKey: installationKey + ) + + #expect(migration.isNeeded, "A 12h legacy value must be recognized as needing migration.") + try migration.run() + + let raw = userDefaults.string(forKey: installationKey) + #expect( + raw?.toDate(withFormat: .utc).map { Int($0.timeIntervalSince1970) } + == Int(fixedDate.timeIntervalSince1970), + "12h legacy installation date must be converted to the correct .utc instant." + ) + #expect(migration.isNeeded == false) + } + + /// Symmetric to the 12h case: a value written under 24h converts as well. + @available(iOS 16.0, *) + @Test("run() converts a legacy installation date written in 24h format") + func runConvertsLegacyInstallationDateWrittenIn24hFormat() throws { + userDefaults.set( + independentLegacyString(from: fixedDate, hourCycle: .zeroToTwentyThree), + forKey: installationKey + ) + + #expect(migration.isNeeded, "A 24h legacy value must be recognized as needing migration.") + try migration.run() + + let raw = userDefaults.string(forKey: installationKey) + #expect( + raw?.toDate(withFormat: .utc).map { Int($0.timeIntervalSince1970) } + == Int(fixedDate.timeIntervalSince1970), + "24h legacy installation date must be converted to the correct .utc instant." + ) + #expect(migration.isNeeded == false) + } + + @Test("run() converts all six persisted date keys") + func runConvertsAllSixKeys() throws { + allKeys.forEach { userDefaults.set(legacyString(from: fixedDate), forKey: $0) } + + try migration.run() + + for key in allKeys { + let raw = userDefaults.string(forKey: key) + #expect(raw?.toDate(withFormat: .utc) != nil, "Key \(key) was not converted to .utc") + } + #expect(migration.isNeeded == false) + } + + @Test("run() leaves an already-.utc value untouched") + func runLeavesAlreadyUtcValueUntouched() throws { + let utc = fixedDate.toString(withFormat: .utc) + userDefaults.set(utc, forKey: installationKey) + + #expect(migration.isNeeded == false) + try migration.run() + + #expect(userDefaults.string(forKey: installationKey) == utc) + } + + @Test("run() leaves an unparseable value untouched") + func runLeavesUnparseableValueUntouched() throws { + let garbage = "definitely not a date" + userDefaults.set(garbage, forKey: installationKey) + + #expect(migration.isNeeded == false) + try migration.run() + + #expect(userDefaults.string(forKey: installationKey) == garbage) + } + + /// Regression: an installed user must stay installed even when the stored installation + /// date string cannot be parsed (e.g. a region / 12h↔24h time-format change made a + /// legacy localized string unreadable). `isInstalled` depends on the string's presence, + /// not on parsing it, so it stays true without any migration having run. + @Test("isInstalled stays true for an unparseable installation date, without migration") + func isInstalledTrueForUnparseableInstallationDateWithoutMigration() { + userDefaults.set("definitely not a parseable date", forKey: installationKey) + + let storage = MBPersistenceStorage(defaults: userDefaults) + + #expect(storage.installationDate == nil) + #expect(storage.isInstalled) + } + + /// Ordering contract: `DateFormatMigration` must sort ahead of any date-consuming migration + /// in `MigrationManager`'s ascending-by-`version` chain. If this regresses (e.g. a renumber), + /// `FirstInitializationDateTimeMigration` would read an unparseable legacy `installationDate` + /// as `nil` and silently skip — the original upgrade bug. + @Test("version sorts before any date-consuming migration") + func versionSortsBeforeDateConsumingMigration() { + #expect( + DateFormatMigration().version < FirstInitializationDateTimeMigration().version, + "DateFormatMigration must run before FirstInitializationDateTimeMigration." + ) + } + + @Test("run() is idempotent when called twice") + func runIdempotentWhenCalledTwice() throws { + userDefaults.set(legacyString(from: fixedDate), forKey: installationKey) + + try migration.run() + let afterFirstRun = userDefaults.string(forKey: installationKey) + #expect(migration.isNeeded == false) + + try migration.run() + #expect(userDefaults.string(forKey: installationKey) == afterFirstRun) + #expect(migration.isNeeded == false) + } +} diff --git a/MindboxTests/MindboxLogger/LogStoreTrimmerTests.swift b/MindboxTests/MindboxLogger/LogStoreTrimmerTests.swift deleted file mode 100644 index 5432d2fbc..000000000 --- a/MindboxTests/MindboxLogger/LogStoreTrimmerTests.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// LogStoreTrimmerTests.swift -// MindboxTests -// -// Created by Sergei Semko on 9/11/25. -// Copyright © 2025 Mindbox. All rights reserved. -// - -import XCTest -@testable import MindboxLogger - -final class LogStoreTrimmerTests: XCTestCase { - - // MARK: - Fakes - - private final class StubMeasurer: DatabaseSizeMeasuring { - var size: Int - var calls: Int = 0 - init(size: Int) { self.size = size } - func sizeKB() -> Int { calls += 1; return size } - } - - private func makeConfig( - limit: Int = 128, - lowWater: Double = 0.85, - min: Double = 0.05, - max: Double = 0.50, - cooldown: TimeInterval = 10 - ) -> LoggerDBConfig { - LoggerDBConfig( - dbSizeLimitKB: limit, - lowWaterRatio: lowWater, - minDeleteFraction: min, - maxDeleteFraction: max, - batchSize: 15, - writesPerTrimCheck: 5, - trimCooldownSec: cooldown - ) - } - - private func makeTrimmer( - size: Int, - config: LoggerDBConfig? = nil, - start: Date = Date(timeIntervalSince1970: 0) - ) -> (LogStoreTrimmer, StubMeasurer, ManualClock) { - let cfg = config ?? makeConfig() - let measurer = StubMeasurer(size: size) - let clock = ManualClock(start) - let trimmer = LogStoreTrimmer(config: cfg, sizeMeasurer: measurer, clock: clock) - return (trimmer, measurer, clock) - } - - // MARK: - computeTrimFraction - - func testComputeTrimFraction_ReturnsNil_WhenBelowOrEqualLimit() { - let cfg = makeConfig(limit: 100) - let (trimmer, _, _) = makeTrimmer(size: 0, config: cfg) - XCTAssertNil(trimmer.computeTrimFraction(sizeKB: 100, limitKB: 100)) - XCTAssertNil(trimmer.computeTrimFraction(sizeKB: 99, limitKB: 100)) - } - - func testComputeTrimFraction_RespectsMin_WhenSlightlyOverLimit() { - // For min to work, you need: - // 1) to be SLIGHTLY above the limit (size > limit) so as not to get nil; - // 2) to have raw < min. raw = (size - target) / size; target = limit * lowWater. - // Let's take limit=100, lowWater=0.98 → target=98. - // When size=101: raw ≈ (101-98)/101 ≈ 0.0297 < min(0.05) → result = 0.05. - let cfg = makeConfig(limit: 100, lowWater: 0.98, min: 0.05, max: 0.5) - let (trimmer, _, _) = makeTrimmer(size: 0, config: cfg) - let f = trimmer.computeTrimFraction(sizeKB: 101, limitKB: 100) - XCTAssertNotNil(f) - XCTAssertEqual(f!, 0.05, accuracy: 1e-9) - } - - func testComputeTrimFraction_PassesThrough_WhenWithinMinMax() { - // limit=100, lowWater=0.8 → target=80, size=100 → equal to limit → nil - let cfg = makeConfig(limit: 100, lowWater: 0.8, min: 0.05, max: 0.5) - let (trimmer, _, _) = makeTrimmer(size: 0, config: cfg) - - let f = trimmer.computeTrimFraction(sizeKB: 100, limitKB: 100) - XCTAssertNil(f) - - // Slightly above the limit: raw ≈ (101-80)/101 ≈ 0.2079 → between min and max - let f2 = trimmer.computeTrimFraction(sizeKB: 101, limitKB: 100) - XCTAssertEqual(f2!, 0.2079, accuracy: 1e-3) - } - - func testComputeTrimFraction_CapsAtMax_WhenWayOverLimit() { - let cfg = makeConfig(limit: 100, lowWater: 0.8, min: 0.05, max: 0.5) - let (trimmer, _, _) = makeTrimmer(size: 0, config: cfg) - let f = trimmer.computeTrimFraction(sizeKB: 10_000, limitKB: 100) - XCTAssertEqual(f!, 0.5, accuracy: 1e-9) - } - - // MARK: - maybeTrim - - func testMaybeTrim_UsesPrecomputedSize_DoesNotCallMeasurer() { - let (trimmer, measurer, _) = makeTrimmer(size: 10_000) - var received: Double? - - _ = trimmer.maybeTrim(precomputedSizeKB: 129) { fraction in - received = fraction - } - XCTAssertEqual(measurer.calls, 0) // the meter was not used - XCTAssertNotNil(received) // the trim happened - } - - func testMaybeTrim_CallsDelete_WhenOverLimit_AndSetsCooldown() { - let cfg = makeConfig(limit: 100, lowWater: 0.8, min: 0.05, max: 0.5, cooldown: 10) - let (trimmer, measurer, clock) = makeTrimmer(size: 120, config: cfg, start: Date(timeIntervalSince1970: 0)) - - var calls = 0 - var fractions: [Double] = [] - - _ = trimmer.maybeTrim(precomputedSizeKB: nil) { f in - calls += 1 - fractions.append(f) - } - XCTAssertEqual(measurer.calls, 1) - XCTAssertEqual(calls, 1) - XCTAssertFalse(fractions.isEmpty) - - // Repeated call before the cooldown expires — should not trim - _ = trimmer.maybeTrim(precomputedSizeKB: nil) { _ in - XCTFail("Should not be called under cooldown") - } - XCTAssertEqual(calls, 1) - - // We advance the clock by 9 seconds — still cooldown. - clock.advance(9) - _ = trimmer.maybeTrim(precomputedSizeKB: nil) { _ in - XCTFail("Should not be called under cooldown") - } - XCTAssertEqual(calls, 1) - - // At the 10-second mark, the cooldown ended. - clock.advance(1) - _ = trimmer.maybeTrim(precomputedSizeKB: nil) { f in - calls += 1 - fractions.append(f) - } - XCTAssertEqual(calls, 2) - } - - func testMaybeTrim_RethrowsErrorFromDelete() { - let (trimmer, _, _) = makeTrimmer(size: 10_000) - enum E: Error { case boom } - - XCTAssertThrowsError(try trimmer.maybeTrim(precomputedSizeKB: nil) { _ in - throw E.boom - }) { error in - guard case E.boom = error else { return XCTFail("Wrong error") } - } - } - - func testResetCooldown_AllowsTrimAgainImmediately() { - let (trimmer, _, _) = makeTrimmer(size: 10_000) - - var count = 0 - _ = trimmer.maybeTrim { _ in count += 1 } - XCTAssertEqual(count, 1) - - // immediate repeat — should not work - _ = trimmer.maybeTrim { _ in count += 1 } - XCTAssertEqual(count, 1) - - // reset the cooldown — you can do it again - trimmer.resetCooldown() - _ = trimmer.maybeTrim { _ in count += 1 } - XCTAssertEqual(count, 2) - } -} diff --git a/MindboxTests/MindboxLogger/LoggerDatabaseLoaderTests.swift b/MindboxTests/MindboxLogger/LoggerDatabaseLoaderTests.swift deleted file mode 100644 index f1417ffcf..000000000 --- a/MindboxTests/MindboxLogger/LoggerDatabaseLoaderTests.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// LoggerDatabaseLoaderTests.swift -// MindboxTests -// -// Created by Sergei Semko on 9/12/25. -// Copyright © 2025 Mindbox. All rights reserved. -// - -import XCTest -import CoreData -@testable import MindboxLogger - -@available(iOS 15.0, *) -final class LoggerDatabaseLoaderTests: XCTestCase { - - // MARK: - Helpers - - private func tmpURL(_ name: String) -> URL { - URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("MB-Loader-\(name)-\(UUID().uuidString).sqlite") - } - - private func sqliteHeader(at url: URL) -> String? { - (try? Data(contentsOf: url).prefix(15)).flatMap { String(data: $0, encoding: .ascii) } - } - - func test_loadContainer_success_defaultDescription_createsStoreAndContext() throws { - let url = tmpURL("Success") - let cfg = LoggerDatabaseLoaderConfig( - modelName: "CDLogMessage", - applicationGroupId: nil, - storeURL: url, - descriptions: nil - ) - let loader = LoggerDatabaseLoader(cfg) - - let (container, ctx) = try loader.loadContainer() - - // The file has been created and is valid SQLite. - XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) - XCTAssertEqual(sqliteHeader(at: url), "SQLite format 3") - - // Background context working — trying the simplest recording - try ctx.performAndWait { - let entity = NSEntityDescription.entity(forEntityName: "CDLogMessage", in: ctx)! - let obj = NSManagedObject(entity: entity, insertInto: ctx) - obj.setValue("test", forKey: "message") - obj.setValue(Date(), forKey: "timestamp") - try ctx.save() - } - - // The container did indeed use defaultDescription(for: url) - XCTAssertEqual(container.persistentStoreDescriptions.first?.url, url) - XCTAssertEqual(container.persistentStoreDescriptions.count, 1) - XCTAssertFalse(container.persistentStoreDescriptions[0].shouldAddStoreAsynchronously) - } - - func test_loadContainer_autoRecreates_onCorruptedStore() throws { - let url = tmpURL("Corrupted") - - // We create a “broken” file and sidecars so that the first download fails. - try "NOT A SQLITE DB".data(using: .utf8)!.write(to: url, options: .atomic) - try "WAL".data(using: .utf8)!.write(to: URL(fileURLWithPath: url.path + "-wal")) - try "SHM".data(using: .utf8)!.write(to: URL(fileURLWithPath: url.path + "-shm")) - - let cfg = LoggerDatabaseLoaderConfig( - modelName: "CDLogMessage", - applicationGroupId: nil, - storeURL: url, - descriptions: nil - ) - let loader = LoggerDatabaseLoader(cfg) - - // The first loadStores attempt should fail → catch: destroyStore(...) → successful reload - let (_, ctx) = try loader.loadContainer() - - XCTAssertEqual(sqliteHeader(at: url), "SQLite format 3") - - // And you can write - try ctx.performAndWait { - let entity = NSEntityDescription.entity(forEntityName: "CDLogMessage", in: ctx)! - let obj = NSManagedObject(entity: entity, insertInto: ctx) - obj.setValue("after-recreate", forKey: "message") - obj.setValue(Date(), forKey: "timestamp") - try ctx.save() - } - } - - func test_loadContainer_usesExplicitDescriptionURL() throws { - let url = tmpURL("Explicit") - let desc = NSPersistentStoreDescription(url: url) - desc.type = NSSQLiteStoreType - desc.shouldAddStoreAsynchronously = false - - let cfg = LoggerDatabaseLoaderConfig( - modelName: "CDLogMessage", - applicationGroupId: nil, - storeURL: nil, - descriptions: [desc] - ) - let loader = LoggerDatabaseLoader(cfg) - - let (container, _) = try loader.loadContainer() - - XCTAssertEqual(container.persistentStoreDescriptions.count, 1) - XCTAssertEqual(container.persistentStoreDescriptions.first?.url, url) - XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) - } - - func test_loadContainer_throwsWhenModelNotFound() { - let cfg = LoggerDatabaseLoaderConfig( - modelName: "ModelThatDoesNotExist", - applicationGroupId: nil, - storeURL: nil, - descriptions: nil - ) - let loader = LoggerDatabaseLoader(cfg) - - XCTAssertThrowsError(try loader.loadContainer()) { error in - guard case LoggerDatabaseLoaderError.modelNotFound(let name) = error else { - return XCTFail("Unexpected error: \(error)") - } - XCTAssertEqual(name, "ModelThatDoesNotExist") - } - } - - func test_destroyIfExists_removesStoreAndSidecars() throws { - let url = tmpURL("DestroyMe") - let cfg = LoggerDatabaseLoaderConfig( - modelName: "CDLogMessage", - applicationGroupId: nil, - storeURL: url, - descriptions: nil - ) - let loader = LoggerDatabaseLoader(cfg) - - // Create a store and immediately release all references (so that the file is not occupied by Core Data). - try autoreleasepool { - _ = try loader.loadContainer() - } - - let fm = FileManager.default - XCTAssertTrue(fm.fileExists(atPath: url.path)) - _ = fm.createFile(atPath: url.path + "-wal", contents: Data()) - _ = fm.createFile(atPath: url.path + "-shm", contents: Data()) - - try loader.destroyIfExists() - - XCTAssertFalse(fm.fileExists(atPath: url.path)) - XCTAssertFalse(fm.fileExists(atPath: url.path + "-wal")) - XCTAssertFalse(fm.fileExists(atPath: url.path + "-shm")) - } -} diff --git a/MindboxTests/MindboxLogger/MBLoggerCoreDataManagerTests.swift b/MindboxTests/MindboxLogger/MBLoggerCoreDataManagerTests.swift deleted file mode 100644 index 9ebbbb0ca..000000000 --- a/MindboxTests/MindboxLogger/MBLoggerCoreDataManagerTests.swift +++ /dev/null @@ -1,500 +0,0 @@ -// -// MBLoggerCoreDataManagerTests.swift -// MindboxTests -// -// Created by Akylbek Utekeshev on 15.02.2023. -// Copyright © 2023 Mikhail Barilov. All rights reserved. -// - -import XCTest -@testable import MindboxLogger -@testable import Mindbox - -final class MBLoggerCoreDataManagerTests: XCTestCase { - - private var batchSizeConstant: Int! - var manager: MBLoggerCoreDataManager! - - override func setUpWithError() throws { - try super.setUpWithError() - manager = MBLoggerCoreDataManager.makeIsolated() - MBLoggerCoreDataManager.waitUntilReady(manager) - - try? manager.deleteAll() - MBLoggerCoreDataManager.drainQueue(manager) - - batchSizeConstant = manager.debugBatchSize - } - - override func tearDown() { - manager = nil - super.tearDown() - } - - func test_measure_create_one_batch() throws { - measure { - let exp = expectation(description: "Fetch created log") - exp.expectedFulfillmentCount = batchSizeConstant - createMessages(range: 1...batchSizeConstant) { _, _ in exp.fulfill() } - wait(for: [exp]) - } - } - - func test_measure_create_1_000() throws { - let logsCount = 1_000 - measure { - let exp = expectation(description: "Fetch created log") - exp.expectedFulfillmentCount = logsCount - createMessages(range: 0.. Void)? - ) { - let timestamp: Date - - switch timeStrategy { - case .none: - timestamp = baseDate - case .sequential(let interval): - timestamp = baseDate.addingTimeInterval(Double(index) * interval) - case .reverse(let interval): - timestamp = baseDate.addingTimeInterval(Double(index) * -interval) - case .custom(let date, let interval): - timestamp = date.addingTimeInterval(Double(index) * interval) - } - - let message = message ?? "Log: \(index)" - manager.create(message: message, timestamp: timestamp) { - completion?(index, timestamp) - } - } - - func createRemainingMessages( - basedOn countOfManuallyCreatedMessages: Int = 0, - timeStrategy: TimeStrategy = .none, - completion: ((_ index: Int, _ timestamp: Date) -> Void)? = nil - ) { - let remainsBatchSize = batchSizeConstant - countOfManuallyCreatedMessages - let baseDate = Date() - - for i in 1...remainsBatchSize { - createMessageWithIndex(index: i, - baseDate: baseDate, - timeStrategy: timeStrategy, - completion: completion) - } - } - - func createMessages( - message: String? = nil, - range: R, - timeStrategy: TimeStrategy = .none, - completion: ((_ index: Int, _ timestamp: Date) -> Void)? = nil - ) where R.Bound == Int { - let baseDate = Date() - - let resolvedRange = range.relative(to: 0.. [CustomEvent] { + try await pollUntil(value: { try fetchCustomEvents(named: name) }, + condition: { $0.count >= count }) + } + + /// Custom events named `name`, in the repository's own send order + /// (fetchRequestForSend sorts by retry/enqueue timestamp) — i.e. exactly the + /// order GuaranteedDeliveryManager would deliver them in. + private func fetchCustomEvents(named name: String) throws -> [CustomEvent] { + try databaseRepository + .query(fetchLimit: 200, retryDeadline: 60) + .filter { $0.type == .customEvent } + .compactMap { try? JSONDecoder().decode(CustomEvent.self, from: Data($0.body.utf8)) } + .filter { $0.name == name } + } + + /// Thread-safe one-shot value holder for asserting on completion callbacks. + /// Polled with a deadline instead of awaiting a continuation, so a regression + /// that never calls the completion FAILS the test instead of hanging it. + private final class ResultBox: @unchecked Sendable { + private let lock = NSLock() + private var stored: T? + func set(_ value: T) { lock.lock(); stored = value; lock.unlock() } + var value: T? { lock.lock(); defer { lock.unlock() }; return stored } + } + + private func waitForValue(in box: ResultBox) async -> T? { + await pollUntil(value: { box.value }, condition: { $0 != nil }) + } + + // MARK: - Ordering + + @Test("Burst of async operations is persisted in call order") + func eventOrderMatchesCallOrder() async throws { + struct Payload: Decodable { let i: Int } + let total = 30 + for i in 0..() + Mindbox.shared.executeSyncOperation(operationSystemName: "Test.Sync", json: "{}") { _ in + box.set(Thread.isMainThread) + } + let deliveredOnMain = await waitForValue(in: box) + #expect(deliveredOnMain == true, "completion not delivered (nil) or delivered off main (false)") + } + + // MARK: - pushClicked + + @Test("pushClicked persists the click event for guaranteed delivery") + func pushClickPersisted() async throws { + Mindbox.shared.pushClicked(uniqueKey: "test-push-unique-key") + + let clicks = try await pollUntil( + value: { try databaseRepository.query(fetchLimit: 200, retryDeadline: 60).filter { $0.type == .trackClick } }, + condition: { !$0.isEmpty }) + #expect(clicks.count == 1) + #expect(clicks.first?.body.contains("test-push-unique-key") == true) + } + + // MARK: - track() synchronicity + + // track(_:) must finish its work before returning: handlePush/handleUniversalLink + // set skipNextDirectTrackVisit, which the didBecomeActive-driven trackDirect + // consumes on controllerQueue. If track() is ever deferred to a queue "for + // symmetry" with the operation methods, that flag write starts racing trackDirect + // — this test trips then. lastTrackVisit is written in the same synchronous chain + // as the (private) flag, so it stands in for it. + @Test("track(.universalLink) applies its effects before returning (stays synchronous)") + func trackAppliesEffectsSynchronously() { + SessionTemporaryStorage.shared.erase() + let activity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + activity.webpageURL = URL(string: "https://test-site.s.mindbox.ru") + + Mindbox.shared.track(.universalLink(activity)) + + // Asserted immediately, with no waiting: the effect must already be visible + // the moment the call returns. + #expect(SessionTemporaryStorage.shared.lastTrackVisit?.source == .link) + } + + @Test("executeSyncOperation early errors (unconfigured SDK) are delivered on main too") + func syncEarlyErrorArrivesOnMain() async { + // configuration is nil after reset() in init — the "Configuration is not set" path. + let box = ResultBox<(onMain: Bool, isFailure: Bool)>() + Mindbox.shared.executeSyncOperation(operationSystemName: "Test.Sync", json: "{}") { result in + if case .failure = result { + box.set((Thread.isMainThread, true)) + } else { + box.set((Thread.isMainThread, false)) + } + } + let result = await waitForValue(in: box) + #expect(result?.onMain == true) + #expect(result?.isFailure == true) + } + + @Test("executeSyncOperation with invalid name delivers a .validationError on the main thread") + func syncInvalidNameDeliversValidationErrorOnMain() async { + // configuration is nil after reset() in init, but name validation fires before + // enqueueSyncEvent is called — so the completion must arrive regardless. + let box = ResultBox<(onMain: Bool, isValidationError: Bool)>() + Mindbox.shared.executeSyncOperation(operationSystemName: "плохое имя", json: "{}") { result in + if case .failure(.validationError(let error)) = result { + box.set((Thread.isMainThread, error.validationMessages.first?.location == "operationSystemName")) + } else { + box.set((Thread.isMainThread, false)) + } + } + let result = await waitForValue(in: box) + #expect(result?.onMain == true, "completion not delivered (nil) or delivered off main") + #expect(result?.isValidationError == true, "invalid name must deliver a .validationError located on operationSystemName") + } + + @Test("executeSyncOperation with invalid JSON delivers a .validationError on the main thread") + func syncInvalidJSONDeliversValidationErrorOnMain() async throws { + // Configure the SDK so the only path to a failure is the invalid-JSON branch, not + // the "Configuration is not set" early error. This pins the regression where invalid + // JSON dropped the operation without ever invoking `completion`, hanging the caller. + persistenceStorage.configuration = try MBConfiguration(endpoint: "test-endpoint", domain: "api.mindbox.ru") + persistenceStorage.deviceUUID = "00000000-0000-0000-0000-000000000001" + + let box = ResultBox<(onMain: Bool, isValidationError: Bool)>() + Mindbox.shared.executeSyncOperation(operationSystemName: "Test.Sync", json: "not json at all") { result in + if case .failure(.validationError(let error)) = result { + box.set((Thread.isMainThread, error.validationMessages.first?.location == "operationBody")) + } else { + box.set((Thread.isMainThread, false)) + } + } + let result = await waitForValue(in: box) + #expect(result?.onMain == true, "completion not delivered (nil) or delivered off main") + #expect(result?.isValidationError == true, "invalid JSON must deliver a .validationError located on operationBody") + } +} diff --git a/MindboxTests/MindboxTests.swift b/MindboxTests/MindboxTests.swift index aced083ea..0a8acc4ce 100644 --- a/MindboxTests/MindboxTests.swift +++ b/MindboxTests/MindboxTests.swift @@ -189,10 +189,34 @@ class MindboxTests: XCTestCase { XCTAssertEqual(secondCountApnsToken, 1) } - func testOperationNameValidity() { - XCTAssertTrue("TEST.-".operationNameIsValid) - XCTAssertFalse("тест".operationNameIsValid) - XCTAssertFalse("TESт".operationNameIsValid) + // Functional validator cases live in OperationNameValidatorTests (Swift Testing). + + // Regression (MOBILE-208): observe() used to invoke the host completion while + // holding observeSemaphore, so re-entering getDeviceUUID/getAPNSToken from inside + // a completion deadlocked the controller queue. Now the completion runs after the + // lock is released; on the old code this test fails by expectation timeout. + func testObserveCompletionMayReenterPublicAPIWithoutDeadlock() { + let deviceUUIDDelivered = expectation(description: "deviceUUID delivered") + let apnsTokenDelivered = expectation(description: "apns token delivered") + + Mindbox.shared.getDeviceUUID { _ in + // Re-entrant call BEFORE fulfilling: under the old lock-held delivery this + // wedged right here, so the expectation below never fired. + Mindbox.shared.getAPNSToken { _ in + apnsTokenDelivered.fulfill() + } + deviceUUIDDelivered.fulfill() + } + + let configuration = try! MBConfiguration(plistName: "TestConfig1") + Mindbox.shared.initialization(configuration: configuration) + waitForInitializationFinished() + wait(for: [deviceUUIDDelivered], timeout: 10) + + let tokenData = Data(ConstantsForTests.token.utf8) + Mindbox.shared.apnsTokenUpdate(deviceToken: tokenData) + waitForInitializationFinished() + wait(for: [apnsTokenDelivered], timeout: 10) } } diff --git a/MindboxTests/Mock/MockInAppConfigurationDataFacade.swift b/MindboxTests/Mock/MockInAppConfigurationDataFacade.swift index cd62be7ec..4574d153f 100644 --- a/MindboxTests/Mock/MockInAppConfigurationDataFacade.swift +++ b/MindboxTests/Mock/MockInAppConfigurationDataFacade.swift @@ -20,9 +20,12 @@ class MockInAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { public var showArray: [String] = [] public var targetingArray: [String] = [] + public var trackTargetingCalls: [(id: String?, tags: [String: String]?)] = [] public var downloadImageError: MindboxError? public var imageDownloadFailures: [(inappId: String, details: String?)] = [] + public var downloadImageTags: [String: [String: String]?] = [:] public var collectedTargetingFailureIds: [Set] = [] + public var collectedTagsByInappId: [[String: [String: String]]] = [] init(segmentationService: SegmentationServiceProtocol, targetingChecker: InAppTargetingCheckerProtocol, @@ -42,7 +45,8 @@ class MockInAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { completion() } - func downloadImage(withUrl url: String, inappId: String, completion: @escaping (Result) -> Void) { + func downloadImage(withUrl url: String, inappId: String, tags: [String: String]?, completion: @escaping (Result) -> Void) { + downloadImageTags[inappId] = tags if let downloadImageError { switch downloadImageError { case .serverError, .protocolError, .unknown: @@ -62,11 +66,13 @@ class MockInAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { } } - func collectTargetingFailures(forFailedTargetingInappIds failedTargetingInappIds: Set) { + func collectTargetingFailures(forFailedTargetingInappIds failedTargetingInappIds: Set, tagsByInappId: [String: [String: String]]) { collectedTargetingFailureIds.append(failedTargetingInappIds) + collectedTagsByInappId.append(tagsByInappId) } - - func trackTargeting(id: String?) { + + func trackTargeting(id: String?, tags: [String: String]?) { + trackTargetingCalls.append((id: id, tags: tags)) if let id = id { if showArray.isEmpty { showArray.append(id) @@ -78,13 +84,16 @@ class MockInAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { func cleanTargetingArray() { targetingArray = [] + trackTargetingCalls = [] } - + func cleanImageDownloadFailures() { imageDownloadFailures = [] + downloadImageTags = [:] } func cleanCollectedTargetingFailureIds() { collectedTargetingFailureIds = [] + collectedTagsByInappId = [] } } diff --git a/MindboxTests/Mock/MockPersistenceStorage.swift b/MindboxTests/Mock/MockPersistenceStorage.swift index f7e587146..800e4e950 100644 --- a/MindboxTests/Mock/MockPersistenceStorage.swift +++ b/MindboxTests/Mock/MockPersistenceStorage.swift @@ -28,8 +28,10 @@ class MockPersistenceStorage: PersistenceStorage { } } + // Mirror production `MBPersistenceStorage`: installed state is the presence of a persisted + // installation-date marker, never its parseability — not `installationDate != nil` directly. var isInstalled: Bool { - installationDate != nil + installationDateString != nil } var apnsToken: String? { @@ -62,8 +64,14 @@ class MockPersistenceStorage: PersistenceStorage { } } + // Presence marker for `isInstalled`, mirroring production's installation-date string. The Date + // itself is stored as-is (no `.utc` flooring) so the mock keeps full precision like its other + // date properties — only `isInstalled` is derived from the marker's presence. + private var installationDateString: String? + var installationDate: Date? { didSet { + installationDateString = installationDate?.toString(withFormat: .utc) onDidChange?() } } @@ -124,6 +132,13 @@ class MockPersistenceStorage: PersistenceStorage { var webViewLocalStateVersion: Int? + // Counts writes so a no-op-write regression (rewriting an unchanged dictionary) is + // observable — the persisted value alone can't distinguish "written again" from "unchanged". + var webViewLearnedHostsWriteCount = 0 + var webViewLearnedHosts: [String: [String]]? { + didSet { webViewLearnedHostsWriteCount += 1 } + } + var operationsDomainFromConfig: String? { didSet { onDidChange?() diff --git a/MindboxTests/Network/MBEventRepositorySendRawTests.swift b/MindboxTests/Network/MBEventRepositorySendRawTests.swift index 667050606..4f22816b4 100644 --- a/MindboxTests/Network/MBEventRepositorySendRawTests.swift +++ b/MindboxTests/Network/MBEventRepositorySendRawTests.swift @@ -7,15 +7,17 @@ import Testing import Foundation @testable import Mindbox -@Suite("MBEventRepository.sendRaw") +@Suite("MBEventRepository") struct MBEventRepositorySendRawTests { // MARK: - Test doubles private final class FakeNetworkFetcher: NetworkFetcher, @unchecked Sendable { var requestRawResult: Result = .success(Data()) + var requestTypedRawResponse: Result = .success(Data(#"{"status":"Success"}"#.utf8)) private(set) var capturedRoute: Route? private(set) var requestRawCallCount = 0 + private(set) var requestTypedCallCount = 0 func request( type: T.Type, @@ -23,7 +25,15 @@ struct MBEventRepositorySendRawTests { needBaseResponse: Bool, completion: @escaping ((Result) -> Void) ) where T: Decodable { - // not used by sendRaw + requestTypedCallCount += 1 + capturedRoute = route + switch requestTypedRawResponse { + case .success(let data): + // A fixture/type mismatch is a test bug - crash loudly rather than mask it. + completion(.success(try! JSONDecoder().decode(T.self, from: data))) + case .failure(let error): + completion(.failure(error)) + } } func request(route: Route, completion: @escaping ((Result) -> Void)) { @@ -208,4 +218,66 @@ struct MBEventRepositorySendRawTests { #expect(isMain) } + + // MARK: - send: completion always on the main queue (MOBILE-208 contract) + + @Test("send network-path completion is delivered on the main queue") + func sendTyped_completion_onMainQueue() async throws { + let fetcher = FakeNetworkFetcher() + let repo = MBEventRepository(fetcher: fetcher, persistenceStorage: try makeStorage()) + + let outcome: (isMain: Bool, isSuccess: Bool) = await withCheckedContinuation { cont in + repo.send(type: OperationResponse.self, event: makeSyncEvent()) { result in + if case .success = result { + cont.resume(returning: (Thread.isMainThread, true)) + } else { + cont.resume(returning: (Thread.isMainThread, false)) + } + } + } + + #expect(outcome.isMain) + #expect(outcome.isSuccess) + #expect(fetcher.requestTypedCallCount == 1) + } + + @Test("send early error (missing configuration) is delivered on the main queue") + func sendTyped_missingConfiguration_errorOnMainQueue() async throws { + let fetcher = FakeNetworkFetcher() + let repo = MBEventRepository(fetcher: fetcher, persistenceStorage: try makeStorage(configured: false)) + + let outcome: (isMain: Bool, errorKey: String?) = await withCheckedContinuation { cont in + repo.send(type: OperationResponse.self, event: makeSyncEvent()) { result in + guard case .failure(.internalError(let ie)) = result else { + cont.resume(returning: (Thread.isMainThread, nil)) + return + } + cont.resume(returning: (Thread.isMainThread, ie.errorKey)) + } + } + + #expect(outcome.isMain) + #expect(outcome.errorKey == ErrorKey.invalidConfiguration.rawValue) + #expect(fetcher.requestTypedCallCount == 0, "Fetcher must not be called when configuration is missing") + } + + @Test("send early error (missing deviceUUID) is delivered on the main queue") + func sendTyped_missingDeviceUUID_errorOnMainQueue() async throws { + let fetcher = FakeNetworkFetcher() + let repo = MBEventRepository(fetcher: fetcher, persistenceStorage: try makeStorage(deviceUUID: nil)) + + let outcome: (isMain: Bool, errorKey: String?) = await withCheckedContinuation { cont in + repo.send(type: OperationResponse.self, event: makeSyncEvent()) { result in + guard case .failure(.internalError(let ie)) = result else { + cont.resume(returning: (Thread.isMainThread, nil)) + return + } + cont.resume(returning: (Thread.isMainThread, ie.errorKey)) + } + } + + #expect(outcome.isMain) + #expect(outcome.errorKey == ErrorKey.invalidConfiguration.rawValue) + #expect(fetcher.requestTypedCallCount == 0, "Fetcher must not be called when deviceUUID is missing") + } } diff --git a/MindboxTests/Network/SDKUserAgentTests.swift b/MindboxTests/Network/SDKUserAgentTests.swift new file mode 100644 index 000000000..34209d1af --- /dev/null +++ b/MindboxTests/Network/SDKUserAgentTests.swift @@ -0,0 +1,49 @@ +// +// SDKUserAgentTests.swift +// MindboxTests +// +// Created by Sergei Semko on 08.07.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +import Foundation +@testable import Mindbox + +@Suite("SDK User-Agent builder", .tags(.userAgent)) +struct SDKUserAgentTests { + + private struct StubUtilitiesFetcher: UtilitiesFetcher { + var sdkVersion: String? + var appVerson: String? + var hostApplicationName: String? + var applicationGroupIdentifier = "group.stub" + func getDeviceUUID(completion: @escaping (String) -> Void) { completion("") } + } + + /// Pins the UA layout `mindbox.sdk/ ( ; ) /` so a + /// refactor can't reorder the segments: the backend slices traffic by this string, and + /// the network layer and the WebView share this single builder. + @Test + func buildKeepsSegmentOrderAndFormat() { + let fetcher = StubUtilitiesFetcher(sdkVersion: "9.9.9", appVerson: "1.2.3", hostApplicationName: "com.test.app") + let ua = SDKUserAgent.build(utilitiesFetcher: fetcher) + + // sdk token first, app/appVer last — a reordering breaks the prefix/suffix. + #expect(ua.hasPrefix("mindbox.sdk/9.9.9 (")) + #expect(ua.hasSuffix(") com.test.app/1.2.3")) + // The device segment "(os ver; model)" sits between them. + #expect(ua.contains("; ")) + } + + /// Missing bundle metadata falls back to "unknown" in every slot — not the legacy + /// "unknow" typo, and not an empty token. + @Test + func buildFallsBackToUnknown() { + let fetcher = StubUtilitiesFetcher(sdkVersion: nil, appVerson: nil, hostApplicationName: nil) + let ua = SDKUserAgent.build(utilitiesFetcher: fetcher) + + #expect(ua.hasPrefix("mindbox.sdk/unknown (")) + #expect(ua.hasSuffix(") unknown/unknown")) + } +} diff --git a/MindboxTests/TrackVisitManager/TrackVisitManagerTests.swift b/MindboxTests/TrackVisitManager/TrackVisitManagerTests.swift index 6b11536e1..053638927 100644 --- a/MindboxTests/TrackVisitManager/TrackVisitManagerTests.swift +++ b/MindboxTests/TrackVisitManager/TrackVisitManagerTests.swift @@ -41,7 +41,7 @@ struct TrackVisitManagerTests { @Test("trackDirect sends direct event with source=direct and sets lastTrackVisit") func trackDirectSendsEvent() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.trackDirect() @@ -58,7 +58,7 @@ struct TrackVisitManagerTests { @Test("trackForeground sends event with source=nil and does not modify lastTrackVisit") func trackForegroundDoesNotOverwriteLastTrackVisit() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() // Simulate a prior universal link track visit try sut.track(.universalLink(makeUserActivity())) @@ -75,7 +75,7 @@ struct TrackVisitManagerTests { @Test("trackForeground does not affect skipNextDirectTrackVisit flag") func trackForegroundDoesNotAffectSkipFlag() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.trackForeground() try sut.trackDirect() @@ -88,7 +88,7 @@ struct TrackVisitManagerTests { @Test("trackDirect is skipped after universal link") func trackDirectSkippedAfterUniversalLink() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.track(.universalLink(makeUserActivity())) try sut.trackDirect() @@ -101,7 +101,7 @@ struct TrackVisitManagerTests { @Test("universal link event contains source=link and requestUrl") func universalLinkEventBody() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() let url = "https://test-site.s.mindbox.ru/some/path" try sut.track(.universalLink(makeUserActivity(url: url))) @@ -117,7 +117,7 @@ struct TrackVisitManagerTests { @Test("skip flag resets after one skip — second trackDirect sends direct") func skipFlagResetsAfterSkip() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.track(.universalLink(makeUserActivity())) try sut.trackDirect() // skipped @@ -134,7 +134,7 @@ struct TrackVisitManagerTests { @Test("second universal link resets flag — only one direct is skipped") func multipleUniversalLinks() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.track(.universalLink(makeUserActivity())) try sut.track(.universalLink(makeUserActivity(url: "https://test-site.g.mindbox.ru"))) @@ -151,7 +151,7 @@ struct TrackVisitManagerTests { @Test("trackForeground between universal link and trackDirect does not consume skip flag") func keepaliveBetweenLinkAndDirect() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.track(.universalLink(makeUserActivity())) try sut.trackForeground() // keepalive — should not consume flag @@ -182,7 +182,7 @@ struct TrackVisitManagerTests { @Test("trackDirect sends event when no push or link preceded it") func trackDirectWithoutPrecedingPushOrLink() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.trackDirect() try sut.trackDirect() @@ -196,7 +196,7 @@ struct TrackVisitManagerTests { @Test("track launch with nil options does not create event") func trackLaunchNilOptions() throws { - let (sut, dbSpy, sessionSpy) = makeSUT() + let (sut, dbSpy, _) = makeSUT() try sut.track(.launch(nil)) diff --git a/MindboxTests/Validators/OperationNameValidatorTests.swift b/MindboxTests/Validators/OperationNameValidatorTests.swift new file mode 100644 index 000000000..728fdce4a --- /dev/null +++ b/MindboxTests/Validators/OperationNameValidatorTests.swift @@ -0,0 +1,87 @@ +// +// OperationNameValidatorTests.swift +// MindboxTests +// +// Created by Sergei Semko on 11.06.2026. +// Copyright © 2026 Mindbox. All rights reserved. +// + +import Testing +@testable import Mindbox + +/// Functional contract of the operation-name validator: only ASCII letters, +/// digits, `-` and `.` are allowed, nothing else — including the trailing +/// line terminators the legacy NSRegularExpression check used to let through +/// (ICU `$` matches before a final terminator). +@Suite("OperationNameValidator", .tags(.customOperation)) +struct OperationNameValidatorTests { + + @Test("Accepts names made of the documented charset", arguments: [ + "ViewProduct", + "Website.ProductView", + "Order.Create.Online", + "app.event.name-123", + "TEST.-", + // single characters from each allowed class + "a", "z", "A", "Z", "0", "9", "-", ".", + // the charset imposes no structure - leading/trailing/only separators are fine + ".op", "-op", "op.", "op-", "...", "---", + "123", + String(repeating: "a", count: 256) + ]) + func accepts(_ name: String) { + #expect(OperationNameValidator.isValid(name)) + } + + @Test("Rejects anything outside the charset", arguments: [ + "", + " ", + "has space", + "op name", + "тест", + "TESт", + "op_name", + "emoji😀", + "op\u{0}name", + "\nop", + "op\nname", + "op\n\n" + ]) + func rejects(_ name: String) { + #expect(!OperationNameValidator.isValid(name)) + } + + // ASCII neighbors of the allowed ranges - the exact switch-case boundaries: + // '@'/'[' surround A-Z, '`'/'{' surround a-z, '/'/':' surround 0-9, + // ','/'+' sit next to '-' and '.'. + @Test("Rejects characters adjacent to the allowed ASCII ranges", arguments: [ + "op@", "op[", "op`", "op{", "op/", "op:", "op,", "op+" + ]) + func rejectsRangeBoundaryNeighbors(_ name: String) { + #expect(!OperationNameValidator.isValid(name)) + } + + // Unicode traps: lookalikes and invisible characters must not pass. + @Test("Rejects unicode lookalikes and invisible characters", arguments: [ + "Op", // fullwidth latin (U+FF2F U+FF50) + "123", // fullwidth digits + "évent", // precomposed e-acute + "e\u{0301}vent", // combining acute accent + "op\u{00A0}name", // no-break space + "op\tname", // tab + "op\u{200B}name" // zero-width space + ]) + func rejectsUnicodeLookalikes(_ name: String) { + #expect(!OperationNameValidator.isValid(name)) + } + + // Intentionally stricter than the legacy regex: a single TRAILING line + // terminator was accepted by `^...$` (ICU quirk) and such operations were + // actually sent. The scalar scan enforces the documented charset. + @Test("Rejects names with a trailing line terminator", arguments: [ + "op\n", "op\r", "op\r\n", "op\u{0085}", "op\u{2028}", "op\u{2029}" + ]) + func rejectsTrailingLineTerminator(_ name: String) { + #expect(!OperationNameValidator.isValid(name)) + } +} diff --git a/SDKVersionProvider/SDKVersionConfig.xcconfig b/SDKVersionProvider/SDKVersionConfig.xcconfig index c491fb202..d166653e1 100644 --- a/SDKVersionProvider/SDKVersionConfig.xcconfig +++ b/SDKVersionProvider/SDKVersionConfig.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 2.15.1 +MARKETING_VERSION = 2.15.2 diff --git a/SDKVersionProvider/SDKVersionProvider.swift b/SDKVersionProvider/SDKVersionProvider.swift index 618f478a1..7f33b151d 100644 --- a/SDKVersionProvider/SDKVersionProvider.swift +++ b/SDKVersionProvider/SDKVersionProvider.swift @@ -8,6 +8,6 @@ import Foundation public class SDKVersionProvider { - public static let sdkVersion = "2.15.1" + public static let sdkVersion = "2.15.2" } diff --git a/TestPlans/AddressSanitizer.xctestplan b/TestPlans/AddressSanitizer.xctestplan new file mode 100644 index 000000000..098d3ec1e --- /dev/null +++ b/TestPlans/AddressSanitizer.xctestplan @@ -0,0 +1,54 @@ +{ + "configurations" : [ + { + "id" : "0FB280C1-5967-4CD8-9C2B-54A8DCAF407E", + "name" : "Address Sanitizer", + "options" : { + "addressSanitizer" : { + "detectStackUseAfterReturn" : true, + "enabled" : true + }, + "undefinedBehaviorSanitizerEnabled" : true + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "environmentVariableEntries" : [ + { + "key" : "OS_ACTIVITY_MODE", + "value" : "disable" + }, + { + "key" : "IDEPreferLogStreaming", + "value" : "YES" + } + ], + "uiTestingScreenshotsLifetime" : "keepNever" + }, + "testTargets" : [ + { + "parallelizable" : false, + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "313B233825ADEA0F00A1CB72", + "name" : "MindboxTests" + } + }, + { + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "3333C19F2681D3CF00B60D84", + "name" : "MindboxNotificationsTests" + } + }, + { + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "A17853C429AF7E950072578F", + "name" : "MindboxLoggerTests" + } + } + ], + "version" : 1 +} diff --git a/TestPlans/Mindbox.xctestplan b/TestPlans/Mindbox.xctestplan new file mode 100644 index 000000000..e23d59c53 --- /dev/null +++ b/TestPlans/Mindbox.xctestplan @@ -0,0 +1,72 @@ +{ + "configurations" : [ + { + "id" : "5A458544-9C9E-4B2F-B5E9-8D2451CDE5B3", + "name" : "Unit Tests", + "options" : { + "defaultTestExecutionTimeAllowance" : 60, + "testTimeoutsEnabled" : true + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "313B232F25ADEA0F00A1CB72", + "name" : "Mindbox" + }, + { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "3333C1972681D3CF00B60D84", + "name" : "MindboxNotifications" + }, + { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "A17853BD29AF7E940072578F", + "name" : "MindboxLogger" + } + ] + }, + "defaultTestExecutionTimeAllowance" : 60, + "environmentVariableEntries" : [ + { + "key" : "OS_ACTIVITY_MODE", + "value" : "disable" + }, + { + "key" : "IDEPreferLogStreaming", + "value" : "YES" + } + ], + "performanceAntipatternCheckerEnabled" : true, + "testTimeoutsEnabled" : true, + "uiTestingScreenshotsLifetime" : "keepNever" + }, + "testTargets" : [ + { + "parallelizable" : false, + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "313B233825ADEA0F00A1CB72", + "name" : "MindboxTests" + } + }, + { + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "3333C19F2681D3CF00B60D84", + "name" : "MindboxNotificationsTests" + } + }, + { + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "A17853C429AF7E950072578F", + "name" : "MindboxLoggerTests" + } + } + ], + "version" : 1 +} diff --git a/TestPlans/ThreadSanitizer.xctestplan b/TestPlans/ThreadSanitizer.xctestplan new file mode 100644 index 000000000..8a690443c --- /dev/null +++ b/TestPlans/ThreadSanitizer.xctestplan @@ -0,0 +1,51 @@ +{ + "configurations" : [ + { + "id" : "231EBBB5-8FB3-466D-8D04-C57EA83E5808", + "name" : "Thread Sanitizer", + "options" : { + "threadSanitizerEnabled" : true, + "undefinedBehaviorSanitizerEnabled" : true + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "environmentVariableEntries" : [ + { + "key" : "OS_ACTIVITY_MODE", + "value" : "disable" + }, + { + "key" : "IDEPreferLogStreaming", + "value" : "YES" + } + ], + "uiTestingScreenshotsLifetime" : "keepNever" + }, + "testTargets" : [ + { + "parallelizable" : false, + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "313B233825ADEA0F00A1CB72", + "name" : "MindboxTests" + } + }, + { + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "3333C19F2681D3CF00B60D84", + "name" : "MindboxNotificationsTests" + } + }, + { + "target" : { + "containerPath" : "container:Mindbox.xcodeproj", + "identifier" : "A17853C429AF7E950072578F", + "name" : "MindboxLoggerTests" + } + } + ], + "version" : 1 +} diff --git a/fastlane/Appfile.swift b/fastlane/Appfile.swift deleted file mode 100644 index b8d462e21..000000000 --- a/fastlane/Appfile.swift +++ /dev/null @@ -1,7 +0,0 @@ -var appIdentifier: String { return "[[APP_IDENTIFIER]]" } // The bundle identifier of your app -var appleID: String { return "[[APPLE_ID]]" } // Your Apple email address - - - -// For more information about the Appfile, see: -// https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 000000000..de9160697 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,72 @@ +PROJECT = "Mindbox.xcodeproj" + +desc "Build for testing" +lane :buildLane do + scan( + project: PROJECT, + scheme: "Mindbox", + xcodebuild_formatter: "", + derived_data_path: "derivedData", + build_for_testing: true, + xcargs: "CI=true" + ) +end + +desc "Run unit tests" +lane :unitTestLane do + scan( + project: PROJECT, + scheme: "Mindbox", + prelaunch_simulator: true, + testplan: "Mindbox", + clean: true, + output_directory: "test_output", + output_types: "junit", + xcodebuild_formatter: "xcbeautify", + result_bundle: true, + disable_concurrent_testing: true, + test_without_building: false, + xcargs: "CI=true" + ) +end + +# MARK: - Sanitizer lanes (nightly scaffolding — intentionally NOT wired into CI yet) +# Each runs the full unit-test set under a dedicated sanitizer test plan. +# They are expected to stay red until the data races ThreadSanitizer surfaced +# are fixed, so keep them off the PR/develop pipeline and run on a schedule. + +desc "Run tests under ThreadSanitizer (nightly; not wired to CI yet)" +lane :threadSanitizerLane do + scan( + project: PROJECT, + scheme: "Mindbox", + prelaunch_simulator: true, + testplan: "ThreadSanitizer", + clean: true, + output_directory: "test_output_tsan", + output_types: "junit", + xcodebuild_formatter: "xcbeautify", + result_bundle: true, + disable_concurrent_testing: true, + test_without_building: false, + xcargs: "CI=true" + ) +end + +desc "Run tests under AddressSanitizer (nightly; not wired to CI yet)" +lane :addressSanitizerLane do + scan( + project: PROJECT, + scheme: "Mindbox", + prelaunch_simulator: true, + testplan: "AddressSanitizer", + clean: true, + output_directory: "test_output_asan", + output_types: "junit", + xcodebuild_formatter: "xcbeautify", + result_bundle: true, + disable_concurrent_testing: true, + test_without_building: false, + xcargs: "CI=true" + ) +end diff --git a/fastlane/Fastfile.swift b/fastlane/Fastfile.swift deleted file mode 100644 index fc6d2948b..000000000 --- a/fastlane/Fastfile.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation - -class Fastfile: LaneFile { - private let project = "Mindbox.xcodeproj" - - func buildLane() { - desc("Build for testing") - scan( - project: .userDefined(project), - scheme: "Mindbox", - xcodebuildFormatter: "", - derivedDataPath: "derivedData", - buildForTesting: .userDefined(true), - xcargs: "CI=true" - ) - } - - func unitTestLane() { - desc("Run unit tests") - scan(project: .userDefined(project), - scheme: "Mindbox", - prelaunchSimulator: .userDefined(true), - onlyTesting: ["MindboxTests"], - clean: true, - xcodebuildFormatter: "xcbeautify", - disableConcurrentTesting: true, - testWithoutBuilding: .userDefined(false), - xcargs: "CI=true" - ) - } -} diff --git a/fastlane/swift/Actions.swift b/fastlane/swift/Actions.swift deleted file mode 100644 index f47e92f6a..000000000 --- a/fastlane/swift/Actions.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Actions.swift -// Copyright (c) 2026 FastlaneTools - -// This autogenerated file will be overwritten or replaced when running "fastlane generate_swift" -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.56] diff --git a/fastlane/swift/Appfile.swift b/fastlane/swift/Appfile.swift deleted file mode 100644 index 2c9ed4c8b..000000000 --- a/fastlane/swift/Appfile.swift +++ /dev/null @@ -1,15 +0,0 @@ -// Appfile.swift -// Copyright (c) 2021 FastlaneTools - -var appIdentifier: String { return "" } // The bundle identifier of your app -var appleID: String { return "" } // Your Apple email address - -var teamID: String { return "" } // Developer Portal Team ID -var itcTeam: String? { return nil } // App Store Connect Team ID (may be nil if no team) - -// you can even provide different app identifiers, Apple IDs and team names per lane: -// More information: https://docs.fastlane.tools/advanced/#appfile - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.1] diff --git a/fastlane/swift/ArgumentProcessor.swift b/fastlane/swift/ArgumentProcessor.swift deleted file mode 100644 index 01681e8ce..000000000 --- a/fastlane/swift/ArgumentProcessor.swift +++ /dev/null @@ -1,89 +0,0 @@ -// ArgumentProcessor.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -struct ArgumentProcessor { - let args: [RunnerArgument] - let currentLane: String - let commandTimeout: Int - let port: UInt32 - - init(args: [String]) { - // Dump the first arg which is the program name - let fastlaneArgs = stride(from: 1, to: args.count - 1, by: 2).map { - RunnerArgument(name: args[$0], value: args[$0 + 1]) - } - self.args = fastlaneArgs - - let fastlaneArgsMinusLanes = fastlaneArgs.filter { arg in - arg.name.lowercased() != "lane" - } - - let potentialLogMode = fastlaneArgsMinusLanes.filter { arg in - arg.name.lowercased() == "logmode" - } - - port = UInt32(fastlaneArgsMinusLanes.first(where: { $0.name == "swiftServerPort" })?.value ?? "") ?? 2000 - - // Configure logMode since we might need to use it before we finish parsing - if let logModeArg = potentialLogMode.first { - let logModeString = logModeArg.value - Logger.logMode = Logger.LogMode(logMode: logModeString) - } - - let lanes = self.args.filter { arg in - arg.name.lowercased() == "lane" - } - verbose(message: lanes.description) - - guard lanes.count == 1 else { - let message = "You must have exactly one lane specified as an arg, here's what I got: \(lanes)" - log(message: message) - fatalError(message) - } - - let lane = lanes.first! - currentLane = lane.value - - // User might have configured a timeout for the socket connection - let potentialTimeout = fastlaneArgsMinusLanes.filter { arg in - arg.name.lowercased() == "timeoutseconds" - } - - if let logModeArg = potentialLogMode.first { - let logModeString = logModeArg.value - Logger.logMode = Logger.LogMode(logMode: logModeString) - } - - if let timeoutArg = potentialTimeout.first { - let timeoutString = timeoutArg.value - commandTimeout = (timeoutString as NSString).integerValue - } else { - commandTimeout = SocketClient.defaultCommandTimeoutSeconds - } - } - - func laneParameters() -> [String: String] { - let laneParametersArgs = args.filter { arg in - let lowercasedName = arg.name.lowercased() - return lowercasedName != "timeoutseconds" && lowercasedName != "lane" && lowercasedName != "logmode" - } - var laneParameters = [String: String]() - for arg in laneParametersArgs { - laneParameters[arg.name] = arg.value - } - return laneParameters - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/Atomic.swift b/fastlane/swift/Atomic.swift deleted file mode 100644 index 45d113571..000000000 --- a/fastlane/swift/Atomic.swift +++ /dev/null @@ -1,150 +0,0 @@ -// Atomic.swift -// Copyright (c) 2022 FastlaneTools - -import Foundation - -protocol DictionaryProtocol: class { - associatedtype Key: Hashable - associatedtype Value - - subscript(_: Key) -> Value? { get set } - - @discardableResult - func removeValue(forKey key: Key) -> Value? - - func get(_ key: Key) -> Value? - func set(_ key: Key, value: Value?) -} - -extension DictionaryProtocol { - subscript(_ key: Key) -> Value? { - get { - get(key) - } - set { - set(key, value: newValue) - } - } -} - -protocol LockProtocol: DictionaryProtocol { - associatedtype Lock - - var _lock: Lock { get set } - - func lock() - func unlock() -} - -protocol AnyLock {} - -extension UnsafeMutablePointer: AnyLock { - @available(macOS, deprecated: 10.12) - static func make() -> Self where Pointee == OSSpinLock { - let spin = UnsafeMutablePointer.allocate(capacity: 1) - spin.initialize(to: OS_SPINLOCK_INIT) - return spin - } - - @available(macOS, introduced: 10.12) - static func make() -> Self where Pointee == os_unfair_lock { - let unfairLock = UnsafeMutablePointer.allocate(capacity: 1) - unfairLock.initialize(to: os_unfair_lock()) - return unfairLock - } - - @available(macOS, deprecated: 10.12) - static func lock(_ lock: Self) where Pointee == OSSpinLock { - OSSpinLockLock(lock) - } - - @available(macOS, deprecated: 10.12) - static func unlock(_ lock: Self) where Pointee == OSSpinLock { - OSSpinLockUnlock(lock) - } - - @available(macOS, introduced: 10.12) - static func lock(_ lock: Self) where Pointee == os_unfair_lock { - os_unfair_lock_lock(lock) - } - - @available(macOS, introduced: 10.12) - static func unlock(_ lock: Self) where Pointee == os_unfair_lock { - os_unfair_lock_unlock(lock) - } -} - -// MARK: - Classes - -class AtomicDictionary: LockProtocol { - typealias Lock = AnyLock - - var _lock: Lock - - private var storage: [Key: Value] = [:] - - init(_ lock: Lock) { - _lock = lock - } - - @discardableResult - func removeValue(forKey key: Key) -> Value? { - lock() - defer { unlock() } - return storage.removeValue(forKey: key) - } - - func get(_ key: Key) -> Value? { - lock() - defer { unlock() } - return storage[key] - } - - func set(_ key: Key, value: Value?) { - lock() - defer { unlock() } - storage[key] = value - } - - func lock() { - fatalError() - } - - func unlock() { - fatalError() - } -} - -@available(macOS, introduced: 10.12) -final class UnfairAtomicDictionary: AtomicDictionary { - typealias Lock = UnsafeMutablePointer - - init() { - super.init(Lock.make()) - } - - override func lock() { - Lock.lock(_lock as! Lock) - } - - override func unlock() { - Lock.unlock(_lock as! Lock) - } -} - -@available(macOS, deprecated: 10.12) -final class OSSPinAtomicDictionary: AtomicDictionary { - typealias Lock = UnsafeMutablePointer - - init() { - super.init(Lock.make()) - } - - override func lock() { - Lock.lock(_lock as! Lock) - } - - override func unlock() { - Lock.unlock(_lock as! Lock) - } -} diff --git a/fastlane/swift/ControlCommand.swift b/fastlane/swift/ControlCommand.swift deleted file mode 100644 index e9fc88169..000000000 --- a/fastlane/swift/ControlCommand.swift +++ /dev/null @@ -1,75 +0,0 @@ -// ControlCommand.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -struct ControlCommand: RubyCommandable { - static let commandKey = "command" - var type: CommandType { - return .control - } - - enum ShutdownCommandType { - static let userMessageKey: String = "userMessage" - - enum CancelReason { - static let reasonKey: String = "reason" - case clientError - case serverError - - var reasonText: String { - switch self { - case .clientError: - return "clientError" - case .serverError: - return "serverError" - } - } - } - - case done - case cancel(cancelReason: CancelReason) - - var token: String { - switch self { - case .done: - return "done" - case .cancel: - return "cancelFastlaneRun" - } - } - } - - let message: String? - let id: String = UUID().uuidString - let shutdownCommandType: ShutdownCommandType - var commandJson: String { - var jsonDictionary: [String: Any] = [ControlCommand.commandKey: shutdownCommandType.token] - - if let message = message { - jsonDictionary[ShutdownCommandType.userMessageKey] = message - } - if case let .cancel(reason) = shutdownCommandType { - jsonDictionary[ShutdownCommandType.CancelReason.reasonKey] = reason.reasonText - } - - let jsonData = try! JSONSerialization.data(withJSONObject: jsonDictionary, options: []) - return String(data: jsonData, encoding: .utf8)! - } - - init(commandType: ShutdownCommandType, message: String? = nil) { - shutdownCommandType = commandType - self.message = message - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/Deliverfile.swift b/fastlane/swift/Deliverfile.swift deleted file mode 100644 index a2d668a51..000000000 --- a/fastlane/swift/Deliverfile.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Deliverfile.swift -// Copyright (c) 2021 FastlaneTools - -// This class is automatically included in FastlaneRunner during build - -// This autogenerated file will be overwritten or replaced during build time, or when you initialize `deliver` -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -public class Deliverfile: DeliverfileProtocol { - // If you want to enable `deliver`, run `fastlane deliver init` - // After, this file will be replaced with a custom implementation that contains values you supplied - // during the `init` process, and you won't see this message -} - -// Generated with fastlane 2.178.0 diff --git a/fastlane/swift/DeliverfileProtocol.swift b/fastlane/swift/DeliverfileProtocol.swift deleted file mode 100644 index ed5102a85..000000000 --- a/fastlane/swift/DeliverfileProtocol.swift +++ /dev/null @@ -1,500 +0,0 @@ -// DeliverfileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -public protocol DeliverfileProtocol: AnyObject { - /// Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - var apiKeyPath: String? { get } - - /// Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - var apiKey: [String: Any]? { get } - - /// Your Apple ID Username - var username: String? { get } - - /// The bundle identifier of your app - var appIdentifier: String? { get } - - /// The version that should be edited or created - var appVersion: String? { get } - - /// Path to your ipa file - var ipa: String? { get } - - /// Path to your pkg file - var pkg: String? { get } - - /// If set the given build number (already uploaded to iTC) will be used instead of the current built one - var buildNumber: String? { get } - - /// The platform to use (optional) - var platform: String { get } - - /// Modify live metadata, this option disables ipa upload and screenshot upload - var editLive: Bool { get } - - /// Force usage of live version rather than edit version - var useLiveVersion: Bool { get } - - /// Path to the folder containing the metadata files - var metadataPath: String? { get } - - /// Path to the folder containing the screenshots - var screenshotsPath: String? { get } - - /// Path to the folder containing localized App Preview videos - var appPreviewsPath: String? { get } - - /// Time code for the App Preview still frame written as hour:minute:second:centisecond (e.g. 00:00:00:01) - var previewFrameTimeCode: String { get } - - /// Clear all previously uploaded App Preview videos before uploading the new ones - var overwritePreviewVideos: Bool { get } - - /// Skip uploading an ipa or pkg to App Store Connect - var skipBinaryUpload: Bool { get } - - /// Don't upload the screenshots - var skipScreenshots: Bool { get } - - /// Don't upload the metadata (e.g. title, description). This will still upload screenshots - var skipMetadata: Bool { get } - - /// Don’t create or update the app version that is being prepared for submission - var skipAppVersionUpdate: Bool { get } - - /// Skip verification of HTML preview file - var force: Bool { get } - - /// Clear all previously uploaded screenshots before uploading the new ones - var overwriteScreenshots: Bool { get } - - /// Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing - var screenshotProcessingTimeout: Int { get } - - /// Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well - var syncScreenshots: Bool { get } - - /// Submit the new version for Review after uploading everything - var submitForReview: Bool { get } - - /// Verifies archive with App Store Connect without uploading - var verifyOnly: Bool { get } - - /// Rejects the previously submitted build if it's in a state where it's possible - var rejectIfPossible: Bool { get } - - /// After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow - var versionCheckWaitRetryLimit: Int { get } - - /// Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) - var automaticRelease: Bool? { get } - - /// Date in milliseconds for automatically releasing on pending approval (Cannot be used together with `automatic_release`) - var autoReleaseDate: Int? { get } - - /// Enable the phased release feature of iTC - var phasedRelease: Bool { get } - - /// Reset the summary rating when you release a new version of the application - var resetRatings: Bool { get } - - /// The price tier of this application - var priceTier: Int? { get } - - /// Path to the app rating's config - var appRatingConfigPath: String? { get } - - /// Extra information for the submission (e.g. compliance specifications) - var submissionInformation: [String: Any]? { get } - - /// The ID of your App Store Connect team if you're in multiple teams - var teamId: String? { get } - - /// The name of your App Store Connect team if you're in multiple teams - var teamName: String? { get } - - /// The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! - var devPortalTeamId: String? { get } - - /// The name of your Developer Portal team if you're in multiple teams - var devPortalTeamName: String? { get } - - /// The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column - var itcProvider: String? { get } - - /// The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication - var providerPublicId: String? { get } - - /// Run precheck before submitting to app review - var runPrecheckBeforeSubmit: Bool { get } - - /// The default precheck rule level unless otherwise configured - var precheckDefaultRuleLevel: String { get } - - /// **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - An array of localized metadata items to upload individually by language so that errors can be identified. E.g. ['name', 'keywords', 'description']. Note: slow - var individualMetadataItems: [String]? { get } - - /// **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the app icon - var appIcon: String? { get } - - /// **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the Apple Watch app icon - var appleWatchAppIcon: String? { get } - - /// Metadata: The copyright notice - var copyright: String? { get } - - /// Metadata: The english name of the primary category (e.g. `Business`, `Books`) - var primaryCategory: String? { get } - - /// Metadata: The english name of the secondary category (e.g. `Business`, `Books`) - var secondaryCategory: String? { get } - - /// Metadata: The english name of the primary first sub category (e.g. `Educational`, `Puzzle`) - var primaryFirstSubCategory: String? { get } - - /// Metadata: The english name of the primary second sub category (e.g. `Educational`, `Puzzle`) - var primarySecondSubCategory: String? { get } - - /// Metadata: The english name of the secondary first sub category (e.g. `Educational`, `Puzzle`) - var secondaryFirstSubCategory: String? { get } - - /// Metadata: The english name of the secondary second sub category (e.g. `Educational`, `Puzzle`) - var secondarySecondSubCategory: String? { get } - - /// **DEPRECATED!** This is no longer used by App Store Connect - Metadata: A hash containing the trade representative contact information - var tradeRepresentativeContactInformation: [String: Any]? { get } - - /// Metadata: A hash containing the review information - var appReviewInformation: [String: Any]? { get } - - /// Metadata: Path to the app review attachment file - var appReviewAttachmentFile: String? { get } - - /// Metadata: The localised app description - var description: [String: Any]? { get } - - /// Metadata: The localised app name - var name: [String: Any]? { get } - - /// Metadata: The localised app subtitle - var subtitle: [String: Any]? { get } - - /// Metadata: An array of localised keywords - var keywords: [String: Any]? { get } - - /// Metadata: An array of localised promotional texts - var promotionalText: [String: Any]? { get } - - /// Metadata: Localised release notes for this version - var releaseNotes: [String: Any]? { get } - - /// Metadata: Localised privacy url - var privacyUrl: [String: Any]? { get } - - /// Metadata: Localised Apple TV privacy policy text - var appleTvPrivacyPolicy: [String: Any]? { get } - - /// Metadata: Localised support url - var supportUrl: [String: Any]? { get } - - /// Metadata: Localised marketing url - var marketingUrl: [String: Any]? { get } - - /// Metadata: List of languages to activate - var languages: [String]? { get } - - /// Ignore errors when invalid languages are found in metadata and screenshot directories - var ignoreLanguageDirectoryValidation: Bool { get } - - /// Should precheck check in-app purchases? - var precheckIncludeInAppPurchases: Bool { get } - - /// The (spaceship) app ID of the app you want to use/modify - var app: Int? { get } -} - -public extension DeliverfileProtocol { - var apiKeyPath: String? { - return nil - } - - var apiKey: [String: Any]? { - return nil - } - - var username: String? { - return nil - } - - var appIdentifier: String? { - return nil - } - - var appVersion: String? { - return nil - } - - var ipa: String? { - return nil - } - - var pkg: String? { - return nil - } - - var buildNumber: String? { - return nil - } - - var platform: String { - return "ios" - } - - var editLive: Bool { - return false - } - - var useLiveVersion: Bool { - return false - } - - var metadataPath: String? { - return nil - } - - var screenshotsPath: String? { - return nil - } - - var appPreviewsPath: String? { - return nil - } - - var previewFrameTimeCode: String { - return "00:00:05:00" - } - - var overwritePreviewVideos: Bool { - return false - } - - var skipBinaryUpload: Bool { - return false - } - - var skipScreenshots: Bool { - return false - } - - var skipMetadata: Bool { - return false - } - - var skipAppVersionUpdate: Bool { - return false - } - - var force: Bool { - return false - } - - var overwriteScreenshots: Bool { - return false - } - - var screenshotProcessingTimeout: Int { - return 3600 - } - - var syncScreenshots: Bool { - return false - } - - var submitForReview: Bool { - return false - } - - var verifyOnly: Bool { - return false - } - - var rejectIfPossible: Bool { - return false - } - - var versionCheckWaitRetryLimit: Int { - return 7 - } - - var automaticRelease: Bool? { - return nil - } - - var autoReleaseDate: Int? { - return nil - } - - var phasedRelease: Bool { - return false - } - - var resetRatings: Bool { - return false - } - - var priceTier: Int? { - return nil - } - - var appRatingConfigPath: String? { - return nil - } - - var submissionInformation: [String: Any]? { - return nil - } - - var teamId: String? { - return nil - } - - var teamName: String? { - return nil - } - - var devPortalTeamId: String? { - return nil - } - - var devPortalTeamName: String? { - return nil - } - - var itcProvider: String? { - return nil - } - - var providerPublicId: String? { - return nil - } - - var runPrecheckBeforeSubmit: Bool { - return true - } - - var precheckDefaultRuleLevel: String { - return "warn" - } - - var individualMetadataItems: [String]? { - return nil - } - - var appIcon: String? { - return nil - } - - var appleWatchAppIcon: String? { - return nil - } - - var copyright: String? { - return nil - } - - var primaryCategory: String? { - return nil - } - - var secondaryCategory: String? { - return nil - } - - var primaryFirstSubCategory: String? { - return nil - } - - var primarySecondSubCategory: String? { - return nil - } - - var secondaryFirstSubCategory: String? { - return nil - } - - var secondarySecondSubCategory: String? { - return nil - } - - var tradeRepresentativeContactInformation: [String: Any]? { - return nil - } - - var appReviewInformation: [String: Any]? { - return nil - } - - var appReviewAttachmentFile: String? { - return nil - } - - var description: [String: Any]? { - return nil - } - - var name: [String: Any]? { - return nil - } - - var subtitle: [String: Any]? { - return nil - } - - var keywords: [String: Any]? { - return nil - } - - var promotionalText: [String: Any]? { - return nil - } - - var releaseNotes: [String: Any]? { - return nil - } - - var privacyUrl: [String: Any]? { - return nil - } - - var appleTvPrivacyPolicy: [String: Any]? { - return nil - } - - var supportUrl: [String: Any]? { - return nil - } - - var marketingUrl: [String: Any]? { - return nil - } - - var languages: [String]? { - return nil - } - - var ignoreLanguageDirectoryValidation: Bool { - return false - } - - var precheckIncludeInAppPurchases: Bool { - return true - } - - var app: Int? { - return nil - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.145] diff --git a/fastlane/swift/Fastfile.swift b/fastlane/swift/Fastfile.swift deleted file mode 100644 index 7393b6a87..000000000 --- a/fastlane/swift/Fastfile.swift +++ /dev/null @@ -1,16 +0,0 @@ -// This class is automatically included in FastlaneRunner during build -// If you have a custom Fastfile.swift, this file will be replaced by it -// Don't modify this file unless you are familiar with how fastlane's swift code generation works -// *** This file will be overwritten or replaced during build time *** - -import Foundation - -open class Fastfile: LaneFile { - override public init() { - super.init() - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.1] diff --git a/fastlane/swift/Fastlane.swift b/fastlane/swift/Fastlane.swift deleted file mode 100644 index f157a4958..000000000 --- a/fastlane/swift/Fastlane.swift +++ /dev/null @@ -1,13979 +0,0 @@ -// Fastlane.swift -// Copyright (c) 2026 FastlaneTools - -import Foundation - -/** - Run ADB Actions - - - parameters: - - serial: Android serial of the device to use for this command - - command: All commands you want to pass to the adb command, e.g. `kill-server` - - adbPath: The path to your `adb` binary (can be left blank if the ANDROID_SDK_ROOT, ANDROID_HOME or ANDROID_SDK environment variable is set) - - - returns: The output of the adb command - - see adb --help for more details - */ -@discardableResult public func adb(serial: String = "", - command: OptionalConfigValue = .fastlaneDefault(nil), - adbPath: String = "adb") -> String -{ - let serialArg = RubyCommand.Argument(name: "serial", value: serial, type: nil) - let commandArg = command.asRubyArgument(name: "command", type: nil) - let adbPathArg = RubyCommand.Argument(name: "adb_path", value: adbPath, type: nil) - let array: [RubyCommand.Argument?] = [serialArg, - commandArg, - adbPathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "adb", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Get an array of Connected android device serials - - - parameter adbPath: The path to your `adb` binary (can be left blank if the ANDROID_SDK_ROOT environment variable is set) - - - returns: Returns an array of all currently connected android devices. Example: [] - - Fetches device list via adb, e.g. run an adb command on all connected devices. - */ -public func adbDevices(adbPath: String = "adb") { - let adbPathArg = RubyCommand.Argument(name: "adb_path", value: adbPath, type: nil) - let array: [RubyCommand.Argument?] = [adbPathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "adb_devices", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Modify the default list of supported platforms - - - parameter platforms: The optional extra platforms to support - */ -public func addExtraPlatforms(platforms: [String] = []) { - let platformsArg = RubyCommand.Argument(name: "platforms", value: platforms, type: nil) - let array: [RubyCommand.Argument?] = [platformsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "add_extra_platforms", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will add an annotated git tag to the current branch - - - parameters: - - tag: Define your own tag text. This will replace all other parameters - - grouping: Is used to keep your tags organised under one 'folder' - - includesLane: Whether the current lane should be included in the tag and message composition, e.g. '//' - - prefix: Anything you want to put in front of the version number (e.g. 'v') - - postfix: Anything you want to put at the end of the version number (e.g. '-RC1') - - buildNumber: The build number. Defaults to the result of increment_build_number if you're using it - - message: The tag message. Defaults to the tag's name - - commit: The commit or object where the tag will be set. Defaults to the current HEAD - - force: Force adding the tag - - sign: Make a GPG-signed tag, using the default e-mail address's key - - This will automatically tag your build with the following format: `//`, where:| - | - >- `grouping` is just to keep your tags organised under one 'folder', defaults to 'builds'| - - `lane` is the name of the current fastlane lane, if chosen to be included via 'includes_lane' option, which defaults to 'true'| - - `prefix` is anything you want to stick in front of the version number, e.g. 'v'| - - `postfix` is anything you want to stick at the end of the version number, e.g. '-RC1'| - - `build_number` is the build number, which defaults to the value emitted by the `increment_build_number` action| - >| - For example, for build 1234 in the 'appstore' lane, it will tag the commit with `builds/appstore/1234`. - */ -public func addGitTag(tag: OptionalConfigValue = .fastlaneDefault(nil), - grouping: String = "builds", - includesLane: OptionalConfigValue = .fastlaneDefault(true), - prefix: String = "", - postfix: String = "", - buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - message: OptionalConfigValue = .fastlaneDefault(nil), - commit: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - sign: OptionalConfigValue = .fastlaneDefault(false)) -{ - let tagArg = tag.asRubyArgument(name: "tag", type: nil) - let groupingArg = RubyCommand.Argument(name: "grouping", value: grouping, type: nil) - let includesLaneArg = includesLane.asRubyArgument(name: "includes_lane", type: nil) - let prefixArg = RubyCommand.Argument(name: "prefix", value: prefix, type: nil) - let postfixArg = RubyCommand.Argument(name: "postfix", value: postfix, type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let messageArg = message.asRubyArgument(name: "message", type: nil) - let commitArg = commit.asRubyArgument(name: "commit", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let signArg = sign.asRubyArgument(name: "sign", type: nil) - let array: [RubyCommand.Argument?] = [tagArg, - groupingArg, - includesLaneArg, - prefixArg, - postfixArg, - buildNumberArg, - messageArg, - commitArg, - forceArg, - signArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "add_git_tag", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Returns the current build_number of either live or edit version - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - initialBuildNumber: sets the build number to given value if no build (upload) is in current train - - appIdentifier: The bundle identifier of your app - - username: Your Apple ID Username - - teamId: The ID of your App Store Connect team if you're in multiple teams - - live: Query the live version (ready-for-sale) - - version: The version number whose latest build number we want - - platform: The platform to use (optional) - - teamName: The name of your App Store Connect team if you're in multiple teams - - Returns the current build number of either the live or testflight version - it is useful for getting the build_number of the current or ready-for-sale app version, and it also works on non-live testflight version. - If you need to handle more build-trains please see `latest_testflight_build_number`. - */ -public func appStoreBuildNumber(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - initialBuildNumber: String, - appIdentifier: String, - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - live: OptionalConfigValue = .fastlaneDefault(true), - version: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - teamName: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let initialBuildNumberArg = RubyCommand.Argument(name: "initial_build_number", value: initialBuildNumber, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let liveArg = live.asRubyArgument(name: "live", type: nil) - let versionArg = version.asRubyArgument(name: "version", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - initialBuildNumberArg, - appIdentifierArg, - usernameArg, - teamIdArg, - liveArg, - versionArg, - platformArg, - teamNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "app_store_build_number", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Load the App Store Connect API token to use in other fastlane tools and actions - - - parameters: - - keyId: The key ID - - issuerId: The issuer ID. It should be nil if the key is individual API key - - keyFilepath: The path to the key p8 file - - keyContent: The content of the key p8 file - - isKeyContentBase64: Whether :key_content is Base64 encoded or not - - duration: The token session duration - - inHouse: Is App Store or Enterprise (in house) team? App Store Connect API cannot determine this on its own (yet) - - setSpaceshipToken: Authorizes all Spaceship::ConnectAPI requests by automatically setting Spaceship::ConnectAPI.token - - Load the App Store Connect API token to use in other fastlane tools and actions - */ -public func appStoreConnectApiKey(keyId: String, - issuerId: OptionalConfigValue = .fastlaneDefault(nil), - keyFilepath: OptionalConfigValue = .fastlaneDefault(nil), - keyContent: OptionalConfigValue = .fastlaneDefault(nil), - isKeyContentBase64: OptionalConfigValue = .fastlaneDefault(false), - duration: Int = 500, - inHouse: OptionalConfigValue = .fastlaneDefault(false), - setSpaceshipToken: OptionalConfigValue = .fastlaneDefault(true)) -{ - let keyIdArg = RubyCommand.Argument(name: "key_id", value: keyId, type: nil) - let issuerIdArg = issuerId.asRubyArgument(name: "issuer_id", type: nil) - let keyFilepathArg = keyFilepath.asRubyArgument(name: "key_filepath", type: nil) - let keyContentArg = keyContent.asRubyArgument(name: "key_content", type: nil) - let isKeyContentBase64Arg = isKeyContentBase64.asRubyArgument(name: "is_key_content_base64", type: nil) - let durationArg = RubyCommand.Argument(name: "duration", value: duration, type: nil) - let inHouseArg = inHouse.asRubyArgument(name: "in_house", type: nil) - let setSpaceshipTokenArg = setSpaceshipToken.asRubyArgument(name: "set_spaceship_token", type: nil) - let array: [RubyCommand.Argument?] = [keyIdArg, - issuerIdArg, - keyFilepathArg, - keyContentArg, - isKeyContentBase64Arg, - durationArg, - inHouseArg, - setSpaceshipTokenArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "app_store_connect_api_key", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload your app to [Appaloosa Store](https://www.appaloosa-store.com/) - - - parameters: - - binary: Binary path. Optional for ipa if you use the `ipa` or `xcodebuild` action - - apiToken: Your API token - - storeId: Your Store id - - groupIds: Your app is limited to special users? Give us the group ids - - screenshots: Add some screenshots application to your store or hit [enter] - - locale: Select the folder locale for your screenshots - - device: Select the device format for your screenshots - - description: Your app description - - changelog: Your app changelog - - Appaloosa is a private mobile application store. This action offers a quick deployment on the platform. - You can create an account, push to your existing account, or manage your user groups. - We accept iOS and Android applications. - */ -public func appaloosa(binary: String, - apiToken: String, - storeId: String, - groupIds: String = "", - screenshots: String, - locale: String = "en-US", - device: OptionalConfigValue = .fastlaneDefault(nil), - description: OptionalConfigValue = .fastlaneDefault(nil), - changelog: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let binaryArg = RubyCommand.Argument(name: "binary", value: binary, type: nil) - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let storeIdArg = RubyCommand.Argument(name: "store_id", value: storeId, type: nil) - let groupIdsArg = RubyCommand.Argument(name: "group_ids", value: groupIds, type: nil) - let screenshotsArg = RubyCommand.Argument(name: "screenshots", value: screenshots, type: nil) - let localeArg = RubyCommand.Argument(name: "locale", value: locale, type: nil) - let deviceArg = device.asRubyArgument(name: "device", type: nil) - let descriptionArg = description.asRubyArgument(name: "description", type: nil) - let changelogArg = changelog.asRubyArgument(name: "changelog", type: nil) - let array: [RubyCommand.Argument?] = [binaryArg, - apiTokenArg, - storeIdArg, - groupIdsArg, - screenshotsArg, - localeArg, - deviceArg, - descriptionArg, - changelogArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "appaloosa", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload your app to [Appetize.io](https://appetize.io/) to stream it in browser - - - parameters: - - apiHost: Appetize API host - - apiToken: Appetize.io API Token - - url: URL from which the ipa file can be fetched. Alternative to :path - - platform: Platform. Either `ios` or `android` - - path: Path to zipped build on the local filesystem. Either this or `url` must be specified - - publicKey: If not provided, a new app will be created. If provided, the existing build will be overwritten - - note: Notes you wish to add to the uploaded app - - timeout: The number of seconds to wait until automatically ending the session due to user inactivity. Must be 30, 60, 90, 120, 180, 300, 600, 1800, 3600 or 7200. Default is 120 - - If you provide a `public_key`, this will overwrite an existing application. If you want to have this build as a new app version, you shouldn't provide this value. - - To integrate appetize into your GitHub workflow check out the [device_grid guide](https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/actions/device_grid/README.md). - */ -public func appetize(apiHost: String = "api.appetize.io", - apiToken: String, - url: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - path: OptionalConfigValue = .fastlaneDefault(nil), - publicKey: OptionalConfigValue = .fastlaneDefault(nil), - note: OptionalConfigValue = .fastlaneDefault(nil), - timeout: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let apiHostArg = RubyCommand.Argument(name: "api_host", value: apiHost, type: nil) - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let urlArg = url.asRubyArgument(name: "url", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let pathArg = path.asRubyArgument(name: "path", type: nil) - let publicKeyArg = publicKey.asRubyArgument(name: "public_key", type: nil) - let noteArg = note.asRubyArgument(name: "note", type: nil) - let timeoutArg = timeout.asRubyArgument(name: "timeout", type: nil) - let array: [RubyCommand.Argument?] = [apiHostArg, - apiTokenArg, - urlArg, - platformArg, - pathArg, - publicKeyArg, - noteArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "appetize", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generate an URL for appetize simulator - - - parameters: - - publicKey: Public key of the app you wish to update - - baseUrl: Base URL of Appetize service - - device: Device type: iphone4s, iphone5s, iphone6, iphone6plus, ipadair, iphone6s, iphone6splus, ipadair2, nexus5, nexus7 or nexus9 - - scale: Scale of the simulator - - orientation: Device orientation - - language: Device language in ISO 639-1 language code, e.g. 'de' - - color: Color of the device - - launchUrl: Specify a deep link to open when your app is launched - - osVersion: The operating system version on which to run your app, e.g. 10.3, 8.0 - - params: Specify params value to be passed to Appetize - - proxy: Specify a HTTP proxy to be passed to Appetize - - - returns: The URL to preview the iPhone app - - Check out the [device_grid guide](https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/actions/device_grid/README.md) for more information - */ -public func appetizeViewingUrlGenerator(publicKey: String, - baseUrl: String = "https://appetize.io/embed", - device: String = "iphone5s", - scale: OptionalConfigValue = .fastlaneDefault(nil), - orientation: String = "portrait", - language: OptionalConfigValue = .fastlaneDefault(nil), - color: String = "black", - launchUrl: OptionalConfigValue = .fastlaneDefault(nil), - osVersion: OptionalConfigValue = .fastlaneDefault(nil), - params: OptionalConfigValue = .fastlaneDefault(nil), - proxy: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let publicKeyArg = RubyCommand.Argument(name: "public_key", value: publicKey, type: nil) - let baseUrlArg = RubyCommand.Argument(name: "base_url", value: baseUrl, type: nil) - let deviceArg = RubyCommand.Argument(name: "device", value: device, type: nil) - let scaleArg = scale.asRubyArgument(name: "scale", type: nil) - let orientationArg = RubyCommand.Argument(name: "orientation", value: orientation, type: nil) - let languageArg = language.asRubyArgument(name: "language", type: nil) - let colorArg = RubyCommand.Argument(name: "color", value: color, type: nil) - let launchUrlArg = launchUrl.asRubyArgument(name: "launch_url", type: nil) - let osVersionArg = osVersion.asRubyArgument(name: "os_version", type: nil) - let paramsArg = params.asRubyArgument(name: "params", type: nil) - let proxyArg = proxy.asRubyArgument(name: "proxy", type: nil) - let array: [RubyCommand.Argument?] = [publicKeyArg, - baseUrlArg, - deviceArg, - scaleArg, - orientationArg, - languageArg, - colorArg, - launchUrlArg, - osVersionArg, - paramsArg, - proxyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "appetize_viewing_url_generator", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Run UI test by Appium with RSpec - - - parameters: - - platform: Appium platform name - - specPath: Path to Appium spec directory - - appPath: Path to Appium target app file - - invokeAppiumServer: Use local Appium server with invoke automatically - - host: Hostname of Appium server - - port: HTTP port of Appium server - - appiumPath: Path to Appium executable - - caps: Hash of caps for Appium::Driver - - appiumLib: Hash of appium_lib for Appium::Driver - */ -public func appium(platform: String, - specPath: String, - appPath: String, - invokeAppiumServer: OptionalConfigValue = .fastlaneDefault(true), - host: String = "0.0.0.0", - port: Int = 4723, - appiumPath: OptionalConfigValue = .fastlaneDefault(nil), - caps: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appiumLib: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil)) -{ - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let specPathArg = RubyCommand.Argument(name: "spec_path", value: specPath, type: nil) - let appPathArg = RubyCommand.Argument(name: "app_path", value: appPath, type: nil) - let invokeAppiumServerArg = invokeAppiumServer.asRubyArgument(name: "invoke_appium_server", type: nil) - let hostArg = RubyCommand.Argument(name: "host", value: host, type: nil) - let portArg = RubyCommand.Argument(name: "port", value: port, type: nil) - let appiumPathArg = appiumPath.asRubyArgument(name: "appium_path", type: nil) - let capsArg = caps.asRubyArgument(name: "caps", type: nil) - let appiumLibArg = appiumLib.asRubyArgument(name: "appium_lib", type: nil) - let array: [RubyCommand.Argument?] = [platformArg, - specPathArg, - appPathArg, - invokeAppiumServerArg, - hostArg, - portArg, - appiumPathArg, - capsArg, - appiumLibArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "appium", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generate Apple-like source code documentation from the source code - - - parameters: - - input: Path(s) to source file directories or individual source files. Accepts a single path or an array of paths - - output: Output path - - templates: Template files path - - docsetInstallPath: DocSet installation path - - include: Include static doc(s) at path - - ignore: Ignore given path - - excludeOutput: Exclude given path from output - - indexDesc: File including main index description - - projectName: Project name - - projectVersion: Project version - - projectCompany: Project company - - companyId: Company UTI (i.e. reverse DNS name) - - createHtml: Create HTML - - createDocset: Create documentation set - - installDocset: Install documentation set to Xcode - - publishDocset: Prepare DocSet for publishing - - noCreateDocset: Create HTML and skip creating a DocSet - - htmlAnchors: The html anchor format to use in DocSet HTML - - cleanOutput: Remove contents of output path before starting - - docsetBundleId: DocSet bundle identifier - - docsetBundleName: DocSet bundle name - - docsetDesc: DocSet description - - docsetCopyright: DocSet copyright message - - docsetFeedName: DocSet feed name - - docsetFeedUrl: DocSet feed URL - - docsetFeedFormats: DocSet feed formats. Separated by a comma [atom,xml] - - docsetPackageUrl: DocSet package (.xar) URL - - docsetFallbackUrl: DocSet fallback URL - - docsetPublisherId: DocSet publisher identifier - - docsetPublisherName: DocSet publisher name - - docsetMinXcodeVersion: DocSet min. Xcode version - - docsetPlatformFamily: DocSet platform family - - docsetCertIssuer: DocSet certificate issuer - - docsetCertSigner: DocSet certificate signer - - docsetBundleFilename: DocSet bundle filename - - docsetAtomFilename: DocSet atom feed filename - - docsetXmlFilename: DocSet xml feed filename - - docsetPackageFilename: DocSet package (.xar,.tgz) filename - - options: Documentation generation options - - crossrefFormat: Cross reference template regex - - exitThreshold: Exit code threshold below which 0 is returned - - docsSectionTitle: Title of the documentation section (defaults to "Programming Guides" - - warnings: Documentation generation warnings - - logformat: Log format [0-3] - - verbose: Log verbosity level [0-6,xcode] - - Runs `appledoc [OPTIONS] ` for the project - */ -public func appledoc(input: [String], - output: OptionalConfigValue = .fastlaneDefault(nil), - templates: OptionalConfigValue = .fastlaneDefault(nil), - docsetInstallPath: OptionalConfigValue = .fastlaneDefault(nil), - include: OptionalConfigValue = .fastlaneDefault(nil), - ignore: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - excludeOutput: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - indexDesc: OptionalConfigValue = .fastlaneDefault(nil), - projectName: String, - projectVersion: OptionalConfigValue = .fastlaneDefault(nil), - projectCompany: String, - companyId: OptionalConfigValue = .fastlaneDefault(nil), - createHtml: OptionalConfigValue = .fastlaneDefault(false), - createDocset: OptionalConfigValue = .fastlaneDefault(false), - installDocset: OptionalConfigValue = .fastlaneDefault(false), - publishDocset: OptionalConfigValue = .fastlaneDefault(false), - noCreateDocset: OptionalConfigValue = .fastlaneDefault(false), - htmlAnchors: OptionalConfigValue = .fastlaneDefault(nil), - cleanOutput: OptionalConfigValue = .fastlaneDefault(false), - docsetBundleId: OptionalConfigValue = .fastlaneDefault(nil), - docsetBundleName: OptionalConfigValue = .fastlaneDefault(nil), - docsetDesc: OptionalConfigValue = .fastlaneDefault(nil), - docsetCopyright: OptionalConfigValue = .fastlaneDefault(nil), - docsetFeedName: OptionalConfigValue = .fastlaneDefault(nil), - docsetFeedUrl: OptionalConfigValue = .fastlaneDefault(nil), - docsetFeedFormats: OptionalConfigValue = .fastlaneDefault(nil), - docsetPackageUrl: OptionalConfigValue = .fastlaneDefault(nil), - docsetFallbackUrl: OptionalConfigValue = .fastlaneDefault(nil), - docsetPublisherId: OptionalConfigValue = .fastlaneDefault(nil), - docsetPublisherName: OptionalConfigValue = .fastlaneDefault(nil), - docsetMinXcodeVersion: OptionalConfigValue = .fastlaneDefault(nil), - docsetPlatformFamily: OptionalConfigValue = .fastlaneDefault(nil), - docsetCertIssuer: OptionalConfigValue = .fastlaneDefault(nil), - docsetCertSigner: OptionalConfigValue = .fastlaneDefault(nil), - docsetBundleFilename: OptionalConfigValue = .fastlaneDefault(nil), - docsetAtomFilename: OptionalConfigValue = .fastlaneDefault(nil), - docsetXmlFilename: OptionalConfigValue = .fastlaneDefault(nil), - docsetPackageFilename: OptionalConfigValue = .fastlaneDefault(nil), - options: OptionalConfigValue = .fastlaneDefault(nil), - crossrefFormat: OptionalConfigValue = .fastlaneDefault(nil), - exitThreshold: Int = 2, - docsSectionTitle: OptionalConfigValue = .fastlaneDefault(nil), - warnings: OptionalConfigValue = .fastlaneDefault(nil), - logformat: OptionalConfigValue = .fastlaneDefault(nil), - verbose: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let inputArg = RubyCommand.Argument(name: "input", value: input, type: nil) - let outputArg = output.asRubyArgument(name: "output", type: nil) - let templatesArg = templates.asRubyArgument(name: "templates", type: nil) - let docsetInstallPathArg = docsetInstallPath.asRubyArgument(name: "docset_install_path", type: nil) - let includeArg = include.asRubyArgument(name: "include", type: nil) - let ignoreArg = ignore.asRubyArgument(name: "ignore", type: nil) - let excludeOutputArg = excludeOutput.asRubyArgument(name: "exclude_output", type: nil) - let indexDescArg = indexDesc.asRubyArgument(name: "index_desc", type: nil) - let projectNameArg = RubyCommand.Argument(name: "project_name", value: projectName, type: nil) - let projectVersionArg = projectVersion.asRubyArgument(name: "project_version", type: nil) - let projectCompanyArg = RubyCommand.Argument(name: "project_company", value: projectCompany, type: nil) - let companyIdArg = companyId.asRubyArgument(name: "company_id", type: nil) - let createHtmlArg = createHtml.asRubyArgument(name: "create_html", type: nil) - let createDocsetArg = createDocset.asRubyArgument(name: "create_docset", type: nil) - let installDocsetArg = installDocset.asRubyArgument(name: "install_docset", type: nil) - let publishDocsetArg = publishDocset.asRubyArgument(name: "publish_docset", type: nil) - let noCreateDocsetArg = noCreateDocset.asRubyArgument(name: "no_create_docset", type: nil) - let htmlAnchorsArg = htmlAnchors.asRubyArgument(name: "html_anchors", type: nil) - let cleanOutputArg = cleanOutput.asRubyArgument(name: "clean_output", type: nil) - let docsetBundleIdArg = docsetBundleId.asRubyArgument(name: "docset_bundle_id", type: nil) - let docsetBundleNameArg = docsetBundleName.asRubyArgument(name: "docset_bundle_name", type: nil) - let docsetDescArg = docsetDesc.asRubyArgument(name: "docset_desc", type: nil) - let docsetCopyrightArg = docsetCopyright.asRubyArgument(name: "docset_copyright", type: nil) - let docsetFeedNameArg = docsetFeedName.asRubyArgument(name: "docset_feed_name", type: nil) - let docsetFeedUrlArg = docsetFeedUrl.asRubyArgument(name: "docset_feed_url", type: nil) - let docsetFeedFormatsArg = docsetFeedFormats.asRubyArgument(name: "docset_feed_formats", type: nil) - let docsetPackageUrlArg = docsetPackageUrl.asRubyArgument(name: "docset_package_url", type: nil) - let docsetFallbackUrlArg = docsetFallbackUrl.asRubyArgument(name: "docset_fallback_url", type: nil) - let docsetPublisherIdArg = docsetPublisherId.asRubyArgument(name: "docset_publisher_id", type: nil) - let docsetPublisherNameArg = docsetPublisherName.asRubyArgument(name: "docset_publisher_name", type: nil) - let docsetMinXcodeVersionArg = docsetMinXcodeVersion.asRubyArgument(name: "docset_min_xcode_version", type: nil) - let docsetPlatformFamilyArg = docsetPlatformFamily.asRubyArgument(name: "docset_platform_family", type: nil) - let docsetCertIssuerArg = docsetCertIssuer.asRubyArgument(name: "docset_cert_issuer", type: nil) - let docsetCertSignerArg = docsetCertSigner.asRubyArgument(name: "docset_cert_signer", type: nil) - let docsetBundleFilenameArg = docsetBundleFilename.asRubyArgument(name: "docset_bundle_filename", type: nil) - let docsetAtomFilenameArg = docsetAtomFilename.asRubyArgument(name: "docset_atom_filename", type: nil) - let docsetXmlFilenameArg = docsetXmlFilename.asRubyArgument(name: "docset_xml_filename", type: nil) - let docsetPackageFilenameArg = docsetPackageFilename.asRubyArgument(name: "docset_package_filename", type: nil) - let optionsArg = options.asRubyArgument(name: "options", type: nil) - let crossrefFormatArg = crossrefFormat.asRubyArgument(name: "crossref_format", type: nil) - let exitThresholdArg = RubyCommand.Argument(name: "exit_threshold", value: exitThreshold, type: nil) - let docsSectionTitleArg = docsSectionTitle.asRubyArgument(name: "docs_section_title", type: nil) - let warningsArg = warnings.asRubyArgument(name: "warnings", type: nil) - let logformatArg = logformat.asRubyArgument(name: "logformat", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let array: [RubyCommand.Argument?] = [inputArg, - outputArg, - templatesArg, - docsetInstallPathArg, - includeArg, - ignoreArg, - excludeOutputArg, - indexDescArg, - projectNameArg, - projectVersionArg, - projectCompanyArg, - companyIdArg, - createHtmlArg, - createDocsetArg, - installDocsetArg, - publishDocsetArg, - noCreateDocsetArg, - htmlAnchorsArg, - cleanOutputArg, - docsetBundleIdArg, - docsetBundleNameArg, - docsetDescArg, - docsetCopyrightArg, - docsetFeedNameArg, - docsetFeedUrlArg, - docsetFeedFormatsArg, - docsetPackageUrlArg, - docsetFallbackUrlArg, - docsetPublisherIdArg, - docsetPublisherNameArg, - docsetMinXcodeVersionArg, - docsetPlatformFamilyArg, - docsetCertIssuerArg, - docsetCertSignerArg, - docsetBundleFilenameArg, - docsetAtomFilenameArg, - docsetXmlFilenameArg, - docsetPackageFilenameArg, - optionsArg, - crossrefFormatArg, - exitThresholdArg, - docsSectionTitleArg, - warningsArg, - logformatArg, - verboseArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "appledoc", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `upload_to_app_store` action - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - appIdentifier: The bundle identifier of your app - - appVersion: The version that should be edited or created - - ipa: Path to your ipa file - - pkg: Path to your pkg file - - buildNumber: If set the given build number (already uploaded to iTC) will be used instead of the current built one - - platform: The platform to use (optional) - - editLive: Modify live metadata, this option disables ipa upload and screenshot upload - - useLiveVersion: Force usage of live version rather than edit version - - metadataPath: Path to the folder containing the metadata files - - screenshotsPath: Path to the folder containing the screenshots - - appPreviewsPath: Path to the folder containing localized App Preview videos - - previewFrameTimeCode: Time code for the App Preview still frame written as hour:minute:second:centisecond (e.g. 00:00:00:01) - - overwritePreviewVideos: Clear all previously uploaded App Preview videos before uploading the new ones - - skipBinaryUpload: Skip uploading an ipa or pkg to App Store Connect - - skipScreenshots: Don't upload the screenshots - - skipMetadata: Don't upload the metadata (e.g. title, description). This will still upload screenshots - - skipAppVersionUpdate: Don’t create or update the app version that is being prepared for submission - - force: Skip verification of HTML preview file - - overwriteScreenshots: Clear all previously uploaded screenshots before uploading the new ones - - screenshotProcessingTimeout: Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing - - syncScreenshots: Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well - - submitForReview: Submit the new version for Review after uploading everything - - verifyOnly: Verifies archive with App Store Connect without uploading - - rejectIfPossible: Rejects the previously submitted build if it's in a state where it's possible - - versionCheckWaitRetryLimit: After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow - - automaticRelease: Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) - - autoReleaseDate: Date in milliseconds for automatically releasing on pending approval (Cannot be used together with `automatic_release`) - - phasedRelease: Enable the phased release feature of iTC - - resetRatings: Reset the summary rating when you release a new version of the application - - priceTier: The price tier of this application - - appRatingConfigPath: Path to the app rating's config - - submissionInformation: Extra information for the submission (e.g. compliance specifications) - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - devPortalTeamId: The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! - - devPortalTeamName: The name of your Developer Portal team if you're in multiple teams - - itcProvider: The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column - - providerPublicId: The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication - - runPrecheckBeforeSubmit: Run precheck before submitting to app review - - precheckDefaultRuleLevel: The default precheck rule level unless otherwise configured - - individualMetadataItems: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - An array of localized metadata items to upload individually by language so that errors can be identified. E.g. ['name', 'keywords', 'description']. Note: slow - - appIcon: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the app icon - - appleWatchAppIcon: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the Apple Watch app icon - - copyright: Metadata: The copyright notice - - primaryCategory: Metadata: The english name of the primary category (e.g. `Business`, `Books`) - - secondaryCategory: Metadata: The english name of the secondary category (e.g. `Business`, `Books`) - - primaryFirstSubCategory: Metadata: The english name of the primary first sub category (e.g. `Educational`, `Puzzle`) - - primarySecondSubCategory: Metadata: The english name of the primary second sub category (e.g. `Educational`, `Puzzle`) - - secondaryFirstSubCategory: Metadata: The english name of the secondary first sub category (e.g. `Educational`, `Puzzle`) - - secondarySecondSubCategory: Metadata: The english name of the secondary second sub category (e.g. `Educational`, `Puzzle`) - - tradeRepresentativeContactInformation: **DEPRECATED!** This is no longer used by App Store Connect - Metadata: A hash containing the trade representative contact information - - appReviewInformation: Metadata: A hash containing the review information - - appReviewAttachmentFile: Metadata: Path to the app review attachment file - - description: Metadata: The localised app description - - name: Metadata: The localised app name - - subtitle: Metadata: The localised app subtitle - - keywords: Metadata: An array of localised keywords - - promotionalText: Metadata: An array of localised promotional texts - - releaseNotes: Metadata: Localised release notes for this version - - privacyUrl: Metadata: Localised privacy url - - appleTvPrivacyPolicy: Metadata: Localised Apple TV privacy policy text - - supportUrl: Metadata: Localised support url - - marketingUrl: Metadata: Localised marketing url - - languages: Metadata: List of languages to activate - - ignoreLanguageDirectoryValidation: Ignore errors when invalid languages are found in metadata and screenshot directories - - precheckIncludeInAppPurchases: Should precheck check in-app purchases? - - app: The (spaceship) app ID of the app you want to use/modify - - Using _upload_to_app_store_ after _build_app_ and _capture_screenshots_ will automatically upload the latest ipa and screenshots with no other configuration. - - If you don't want to verify an HTML preview for App Store builds, use the `:force` option. - This is useful when running _fastlane_ on your Continuous Integration server: - `_upload_to_app_store_(force: true)` - If your account is on multiple teams and you need to tell the transporter which provider to use, you can set `:itc_provider` or `:provider_public_id`. - */ -public func appstore(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - appVersion: OptionalConfigValue = .fastlaneDefault(nil), - ipa: OptionalConfigValue = .fastlaneDefault(nil), - pkg: OptionalConfigValue = .fastlaneDefault(nil), - buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - editLive: OptionalConfigValue = .fastlaneDefault(false), - useLiveVersion: OptionalConfigValue = .fastlaneDefault(false), - metadataPath: OptionalConfigValue = .fastlaneDefault(nil), - screenshotsPath: OptionalConfigValue = .fastlaneDefault(nil), - appPreviewsPath: OptionalConfigValue = .fastlaneDefault(nil), - previewFrameTimeCode: String = "00:00:05:00", - overwritePreviewVideos: OptionalConfigValue = .fastlaneDefault(false), - skipBinaryUpload: OptionalConfigValue = .fastlaneDefault(false), - skipScreenshots: OptionalConfigValue = .fastlaneDefault(false), - skipMetadata: OptionalConfigValue = .fastlaneDefault(false), - skipAppVersionUpdate: OptionalConfigValue = .fastlaneDefault(false), - force: OptionalConfigValue = .fastlaneDefault(false), - overwriteScreenshots: OptionalConfigValue = .fastlaneDefault(false), - screenshotProcessingTimeout: Int = 3600, - syncScreenshots: OptionalConfigValue = .fastlaneDefault(false), - submitForReview: OptionalConfigValue = .fastlaneDefault(false), - verifyOnly: OptionalConfigValue = .fastlaneDefault(false), - rejectIfPossible: OptionalConfigValue = .fastlaneDefault(false), - versionCheckWaitRetryLimit: Int = 7, - automaticRelease: OptionalConfigValue = .fastlaneDefault(nil), - autoReleaseDate: OptionalConfigValue = .fastlaneDefault(nil), - phasedRelease: OptionalConfigValue = .fastlaneDefault(false), - resetRatings: OptionalConfigValue = .fastlaneDefault(false), - priceTier: OptionalConfigValue = .fastlaneDefault(nil), - appRatingConfigPath: OptionalConfigValue = .fastlaneDefault(nil), - submissionInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - devPortalTeamId: OptionalConfigValue = .fastlaneDefault(nil), - devPortalTeamName: OptionalConfigValue = .fastlaneDefault(nil), - itcProvider: OptionalConfigValue = .fastlaneDefault(nil), - providerPublicId: OptionalConfigValue = .fastlaneDefault(nil), - runPrecheckBeforeSubmit: OptionalConfigValue = .fastlaneDefault(true), - precheckDefaultRuleLevel: String = "warn", - individualMetadataItems: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - appIcon: OptionalConfigValue = .fastlaneDefault(nil), - appleWatchAppIcon: OptionalConfigValue = .fastlaneDefault(nil), - copyright: OptionalConfigValue = .fastlaneDefault(nil), - primaryCategory: OptionalConfigValue = .fastlaneDefault(nil), - secondaryCategory: OptionalConfigValue = .fastlaneDefault(nil), - primaryFirstSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - primarySecondSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - secondaryFirstSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - secondarySecondSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - tradeRepresentativeContactInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appReviewInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appReviewAttachmentFile: OptionalConfigValue = .fastlaneDefault(nil), - description: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - name: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - subtitle: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - keywords: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - promotionalText: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - releaseNotes: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - privacyUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appleTvPrivacyPolicy: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - supportUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - marketingUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - languages: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - ignoreLanguageDirectoryValidation: OptionalConfigValue = .fastlaneDefault(false), - precheckIncludeInAppPurchases: OptionalConfigValue = .fastlaneDefault(true), - app: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let pkgArg = pkg.asRubyArgument(name: "pkg", type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let editLiveArg = editLive.asRubyArgument(name: "edit_live", type: nil) - let useLiveVersionArg = useLiveVersion.asRubyArgument(name: "use_live_version", type: nil) - let metadataPathArg = metadataPath.asRubyArgument(name: "metadata_path", type: nil) - let screenshotsPathArg = screenshotsPath.asRubyArgument(name: "screenshots_path", type: nil) - let appPreviewsPathArg = appPreviewsPath.asRubyArgument(name: "app_previews_path", type: nil) - let previewFrameTimeCodeArg = RubyCommand.Argument(name: "preview_frame_time_code", value: previewFrameTimeCode, type: nil) - let overwritePreviewVideosArg = overwritePreviewVideos.asRubyArgument(name: "overwrite_preview_videos", type: nil) - let skipBinaryUploadArg = skipBinaryUpload.asRubyArgument(name: "skip_binary_upload", type: nil) - let skipScreenshotsArg = skipScreenshots.asRubyArgument(name: "skip_screenshots", type: nil) - let skipMetadataArg = skipMetadata.asRubyArgument(name: "skip_metadata", type: nil) - let skipAppVersionUpdateArg = skipAppVersionUpdate.asRubyArgument(name: "skip_app_version_update", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let overwriteScreenshotsArg = overwriteScreenshots.asRubyArgument(name: "overwrite_screenshots", type: nil) - let screenshotProcessingTimeoutArg = RubyCommand.Argument(name: "screenshot_processing_timeout", value: screenshotProcessingTimeout, type: nil) - let syncScreenshotsArg = syncScreenshots.asRubyArgument(name: "sync_screenshots", type: nil) - let submitForReviewArg = submitForReview.asRubyArgument(name: "submit_for_review", type: nil) - let verifyOnlyArg = verifyOnly.asRubyArgument(name: "verify_only", type: nil) - let rejectIfPossibleArg = rejectIfPossible.asRubyArgument(name: "reject_if_possible", type: nil) - let versionCheckWaitRetryLimitArg = RubyCommand.Argument(name: "version_check_wait_retry_limit", value: versionCheckWaitRetryLimit, type: nil) - let automaticReleaseArg = automaticRelease.asRubyArgument(name: "automatic_release", type: nil) - let autoReleaseDateArg = autoReleaseDate.asRubyArgument(name: "auto_release_date", type: nil) - let phasedReleaseArg = phasedRelease.asRubyArgument(name: "phased_release", type: nil) - let resetRatingsArg = resetRatings.asRubyArgument(name: "reset_ratings", type: nil) - let priceTierArg = priceTier.asRubyArgument(name: "price_tier", type: nil) - let appRatingConfigPathArg = appRatingConfigPath.asRubyArgument(name: "app_rating_config_path", type: nil) - let submissionInformationArg = submissionInformation.asRubyArgument(name: "submission_information", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let devPortalTeamIdArg = devPortalTeamId.asRubyArgument(name: "dev_portal_team_id", type: nil) - let devPortalTeamNameArg = devPortalTeamName.asRubyArgument(name: "dev_portal_team_name", type: nil) - let itcProviderArg = itcProvider.asRubyArgument(name: "itc_provider", type: nil) - let providerPublicIdArg = providerPublicId.asRubyArgument(name: "provider_public_id", type: nil) - let runPrecheckBeforeSubmitArg = runPrecheckBeforeSubmit.asRubyArgument(name: "run_precheck_before_submit", type: nil) - let precheckDefaultRuleLevelArg = RubyCommand.Argument(name: "precheck_default_rule_level", value: precheckDefaultRuleLevel, type: nil) - let individualMetadataItemsArg = individualMetadataItems.asRubyArgument(name: "individual_metadata_items", type: nil) - let appIconArg = appIcon.asRubyArgument(name: "app_icon", type: nil) - let appleWatchAppIconArg = appleWatchAppIcon.asRubyArgument(name: "apple_watch_app_icon", type: nil) - let copyrightArg = copyright.asRubyArgument(name: "copyright", type: nil) - let primaryCategoryArg = primaryCategory.asRubyArgument(name: "primary_category", type: nil) - let secondaryCategoryArg = secondaryCategory.asRubyArgument(name: "secondary_category", type: nil) - let primaryFirstSubCategoryArg = primaryFirstSubCategory.asRubyArgument(name: "primary_first_sub_category", type: nil) - let primarySecondSubCategoryArg = primarySecondSubCategory.asRubyArgument(name: "primary_second_sub_category", type: nil) - let secondaryFirstSubCategoryArg = secondaryFirstSubCategory.asRubyArgument(name: "secondary_first_sub_category", type: nil) - let secondarySecondSubCategoryArg = secondarySecondSubCategory.asRubyArgument(name: "secondary_second_sub_category", type: nil) - let tradeRepresentativeContactInformationArg = tradeRepresentativeContactInformation.asRubyArgument(name: "trade_representative_contact_information", type: nil) - let appReviewInformationArg = appReviewInformation.asRubyArgument(name: "app_review_information", type: nil) - let appReviewAttachmentFileArg = appReviewAttachmentFile.asRubyArgument(name: "app_review_attachment_file", type: nil) - let descriptionArg = description.asRubyArgument(name: "description", type: nil) - let nameArg = name.asRubyArgument(name: "name", type: nil) - let subtitleArg = subtitle.asRubyArgument(name: "subtitle", type: nil) - let keywordsArg = keywords.asRubyArgument(name: "keywords", type: nil) - let promotionalTextArg = promotionalText.asRubyArgument(name: "promotional_text", type: nil) - let releaseNotesArg = releaseNotes.asRubyArgument(name: "release_notes", type: nil) - let privacyUrlArg = privacyUrl.asRubyArgument(name: "privacy_url", type: nil) - let appleTvPrivacyPolicyArg = appleTvPrivacyPolicy.asRubyArgument(name: "apple_tv_privacy_policy", type: nil) - let supportUrlArg = supportUrl.asRubyArgument(name: "support_url", type: nil) - let marketingUrlArg = marketingUrl.asRubyArgument(name: "marketing_url", type: nil) - let languagesArg = languages.asRubyArgument(name: "languages", type: nil) - let ignoreLanguageDirectoryValidationArg = ignoreLanguageDirectoryValidation.asRubyArgument(name: "ignore_language_directory_validation", type: nil) - let precheckIncludeInAppPurchasesArg = precheckIncludeInAppPurchases.asRubyArgument(name: "precheck_include_in_app_purchases", type: nil) - let appArg = app.asRubyArgument(name: "app", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - usernameArg, - appIdentifierArg, - appVersionArg, - ipaArg, - pkgArg, - buildNumberArg, - platformArg, - editLiveArg, - useLiveVersionArg, - metadataPathArg, - screenshotsPathArg, - appPreviewsPathArg, - previewFrameTimeCodeArg, - overwritePreviewVideosArg, - skipBinaryUploadArg, - skipScreenshotsArg, - skipMetadataArg, - skipAppVersionUpdateArg, - forceArg, - overwriteScreenshotsArg, - screenshotProcessingTimeoutArg, - syncScreenshotsArg, - submitForReviewArg, - verifyOnlyArg, - rejectIfPossibleArg, - versionCheckWaitRetryLimitArg, - automaticReleaseArg, - autoReleaseDateArg, - phasedReleaseArg, - resetRatingsArg, - priceTierArg, - appRatingConfigPathArg, - submissionInformationArg, - teamIdArg, - teamNameArg, - devPortalTeamIdArg, - devPortalTeamNameArg, - itcProviderArg, - providerPublicIdArg, - runPrecheckBeforeSubmitArg, - precheckDefaultRuleLevelArg, - individualMetadataItemsArg, - appIconArg, - appleWatchAppIconArg, - copyrightArg, - primaryCategoryArg, - secondaryCategoryArg, - primaryFirstSubCategoryArg, - primarySecondSubCategoryArg, - secondaryFirstSubCategoryArg, - secondarySecondSubCategoryArg, - tradeRepresentativeContactInformationArg, - appReviewInformationArg, - appReviewAttachmentFileArg, - descriptionArg, - nameArg, - subtitleArg, - keywordsArg, - promotionalTextArg, - releaseNotesArg, - privacyUrlArg, - appleTvPrivacyPolicyArg, - supportUrlArg, - marketingUrlArg, - languagesArg, - ignoreLanguageDirectoryValidationArg, - precheckIncludeInAppPurchasesArg, - appArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "appstore", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload dSYM file to [Apteligent (Crittercism)](http://www.apteligent.com/) - - - parameters: - - dsym: dSYM.zip file to upload to Apteligent - - appId: Apteligent App ID key e.g. 569f5c87cb99e10e00c7xxxx - - apiKey: Apteligent App API key e.g. IXPQIi8yCbHaLliqzRoo065tH0lxxxxx - */ -public func apteligent(dsym: OptionalConfigValue = .fastlaneDefault(nil), - appId: String, - apiKey: String) -{ - let dsymArg = dsym.asRubyArgument(name: "dsym", type: nil) - let appIdArg = RubyCommand.Argument(name: "app_id", value: appId, type: nil) - let apiKeyArg = RubyCommand.Argument(name: "api_key", value: apiKey, type: nil) - let array: [RubyCommand.Argument?] = [dsymArg, - appIdArg, - apiKeyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "apteligent", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This action uploads an artifact to artifactory - - - parameters: - - file: File to be uploaded to artifactory - - repo: Artifactory repo to put the file in - - repoPath: Path to deploy within the repo, including filename - - endpoint: Artifactory endpoint - - username: Artifactory username - - password: Artifactory password - - apiKey: Artifactory API key - - properties: Artifact properties hash - - sslPemFile: Location of pem file to use for ssl verification - - sslVerify: Verify SSL - - proxyUsername: Proxy username - - proxyPassword: Proxy password - - proxyAddress: Proxy address - - proxyPort: Proxy port - - readTimeout: Read timeout - - Connect to the artifactory server using either a username/password or an api_key - */ -public func artifactory(file: String, - repo: String, - repoPath: String, - endpoint: String, - username: OptionalConfigValue = .fastlaneDefault(nil), - password: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue = .fastlaneDefault(nil), - properties: [String: Any] = [:], - sslPemFile: OptionalConfigValue = .fastlaneDefault(nil), - sslVerify: OptionalConfigValue = .fastlaneDefault(true), - proxyUsername: OptionalConfigValue = .fastlaneDefault(nil), - proxyPassword: OptionalConfigValue = .fastlaneDefault(nil), - proxyAddress: OptionalConfigValue = .fastlaneDefault(nil), - proxyPort: OptionalConfigValue = .fastlaneDefault(nil), - readTimeout: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let fileArg = RubyCommand.Argument(name: "file", value: file, type: nil) - let repoArg = RubyCommand.Argument(name: "repo", value: repo, type: nil) - let repoPathArg = RubyCommand.Argument(name: "repo_path", value: repoPath, type: nil) - let endpointArg = RubyCommand.Argument(name: "endpoint", value: endpoint, type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let passwordArg = password.asRubyArgument(name: "password", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let propertiesArg = RubyCommand.Argument(name: "properties", value: properties, type: nil) - let sslPemFileArg = sslPemFile.asRubyArgument(name: "ssl_pem_file", type: nil) - let sslVerifyArg = sslVerify.asRubyArgument(name: "ssl_verify", type: nil) - let proxyUsernameArg = proxyUsername.asRubyArgument(name: "proxy_username", type: nil) - let proxyPasswordArg = proxyPassword.asRubyArgument(name: "proxy_password", type: nil) - let proxyAddressArg = proxyAddress.asRubyArgument(name: "proxy_address", type: nil) - let proxyPortArg = proxyPort.asRubyArgument(name: "proxy_port", type: nil) - let readTimeoutArg = readTimeout.asRubyArgument(name: "read_timeout", type: nil) - let array: [RubyCommand.Argument?] = [fileArg, - repoArg, - repoPathArg, - endpointArg, - usernameArg, - passwordArg, - apiKeyArg, - propertiesArg, - sslPemFileArg, - sslVerifyArg, - proxyUsernameArg, - proxyPasswordArg, - proxyAddressArg, - proxyPortArg, - readTimeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "artifactory", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Configures Xcode's Codesigning options - - - parameters: - - path: Path to your Xcode project - - useAutomaticSigning: Defines if project should use automatic signing - - teamId: Team ID, is used when upgrading project - - targets: Specify targets you want to toggle the signing mech. (default to all targets) - - codeSignIdentity: Code signing identity type (iPhone Developer, iPhone Distribution) - - profileName: Provisioning profile name to use for code signing - - profileUuid: Provisioning profile UUID to use for code signing - - bundleIdentifier: Application Product Bundle Identifier - - - returns: The current status (boolean) of codesigning after modification - - Configures Xcode's Codesigning options of all targets in the project - */ -public func automaticCodeSigning(path: String, - useAutomaticSigning: OptionalConfigValue = .fastlaneDefault(false), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - targets: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - codeSignIdentity: OptionalConfigValue = .fastlaneDefault(nil), - profileName: OptionalConfigValue = .fastlaneDefault(nil), - profileUuid: OptionalConfigValue = .fastlaneDefault(nil), - bundleIdentifier: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let useAutomaticSigningArg = useAutomaticSigning.asRubyArgument(name: "use_automatic_signing", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let targetsArg = targets.asRubyArgument(name: "targets", type: nil) - let codeSignIdentityArg = codeSignIdentity.asRubyArgument(name: "code_sign_identity", type: nil) - let profileNameArg = profileName.asRubyArgument(name: "profile_name", type: nil) - let profileUuidArg = profileUuid.asRubyArgument(name: "profile_uuid", type: nil) - let bundleIdentifierArg = bundleIdentifier.asRubyArgument(name: "bundle_identifier", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - useAutomaticSigningArg, - teamIdArg, - targetsArg, - codeSignIdentityArg, - profileNameArg, - profileUuidArg, - bundleIdentifierArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "automatic_code_signing", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This action backs up your file to "[path].back" - - - parameter path: Path to the file you want to backup - */ -public func backupFile(path: String) { - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let array: [RubyCommand.Argument?] = [pathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "backup_file", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Save your [zipped] xcarchive elsewhere from default path - - - parameters: - - xcarchive: Path to your xcarchive file. Optional if you use the `xcodebuild` action - - destination: Where your archive will be placed - - zip: Enable compression of the archive - - zipFilename: Filename of the compressed archive. Will be appended by `.xcarchive.zip`. Default value is the output xcarchive filename - - versioned: Create a versioned (date and app version) subfolder where to put the archive - */ -public func backupXcarchive(xcarchive: String, - destination: String, - zip: OptionalConfigValue = .fastlaneDefault(true), - zipFilename: OptionalConfigValue = .fastlaneDefault(nil), - versioned: OptionalConfigValue = .fastlaneDefault(true)) -{ - let xcarchiveArg = RubyCommand.Argument(name: "xcarchive", value: xcarchive, type: nil) - let destinationArg = RubyCommand.Argument(name: "destination", value: destination, type: nil) - let zipArg = zip.asRubyArgument(name: "zip", type: nil) - let zipFilenameArg = zipFilename.asRubyArgument(name: "zip_filename", type: nil) - let versionedArg = versioned.asRubyArgument(name: "versioned", type: nil) - let array: [RubyCommand.Argument?] = [xcarchiveArg, - destinationArg, - zipArg, - zipFilenameArg, - versionedArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "backup_xcarchive", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Automatically add a badge to your app icon - - - parameters: - - dark: Adds a dark flavored badge on top of your icon - - custom: Add your custom overlay/badge image - - noBadge: Hides the beta badge - - shield: Add a shield to your app icon from shields.io - - alpha: Adds and alpha badge instead of the default beta one - - path: Sets the root path to look for AppIcons - - shieldIoTimeout: Set custom duration for the timeout of the shields.io request in seconds - - glob: Glob pattern for finding image files - - alphaChannel: Keeps/adds an alpha channel to the icon (useful for android icons) - - shieldGravity: Position of shield on icon. Default: North - Choices include: NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast - - shieldNoResize: Shield image will no longer be resized to aspect fill the full icon. Instead it will only be shrunk to not exceed the icon graphic - - Please use the [badge plugin](https://github.com/HazAT/fastlane-plugin-badge) instead. - This action will add a light/dark badge onto your app icon. - You can also provide your custom badge/overlay or add a shield for more customization. - More info: [https://github.com/HazAT/badge](https://github.com/HazAT/badge) - **Note**: If you want to reset the badge back to default, you can use `sh 'git checkout -- /Assets.xcassets/'`. - */ -public func badge(dark: OptionalConfigValue = .fastlaneDefault(nil), - custom: OptionalConfigValue = .fastlaneDefault(nil), - noBadge: OptionalConfigValue = .fastlaneDefault(nil), - shield: OptionalConfigValue = .fastlaneDefault(nil), - alpha: OptionalConfigValue = .fastlaneDefault(nil), - path: String = ".", - shieldIoTimeout: OptionalConfigValue = .fastlaneDefault(nil), - glob: OptionalConfigValue = .fastlaneDefault(nil), - alphaChannel: OptionalConfigValue = .fastlaneDefault(nil), - shieldGravity: OptionalConfigValue = .fastlaneDefault(nil), - shieldNoResize: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let darkArg = dark.asRubyArgument(name: "dark", type: nil) - let customArg = custom.asRubyArgument(name: "custom", type: nil) - let noBadgeArg = noBadge.asRubyArgument(name: "no_badge", type: nil) - let shieldArg = shield.asRubyArgument(name: "shield", type: nil) - let alphaArg = alpha.asRubyArgument(name: "alpha", type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let shieldIoTimeoutArg = shieldIoTimeout.asRubyArgument(name: "shield_io_timeout", type: nil) - let globArg = glob.asRubyArgument(name: "glob", type: nil) - let alphaChannelArg = alphaChannel.asRubyArgument(name: "alpha_channel", type: nil) - let shieldGravityArg = shieldGravity.asRubyArgument(name: "shield_gravity", type: nil) - let shieldNoResizeArg = shieldNoResize.asRubyArgument(name: "shield_no_resize", type: nil) - let array: [RubyCommand.Argument?] = [darkArg, - customArg, - noBadgeArg, - shieldArg, - alphaArg, - pathArg, - shieldIoTimeoutArg, - globArg, - alphaChannelArg, - shieldGravityArg, - shieldNoResizeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "badge", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generate and upload an ipa file to appetize.io - - - parameters: - - xcodebuild: Parameters that are passed to the xcodebuild action - - scheme: The scheme to build. Can also be passed using the `xcodebuild` parameter - - apiToken: Appetize.io API Token - - publicKey: If not provided, a new app will be created. If provided, the existing build will be overwritten - - note: Notes you wish to add to the uploaded app - - timeout: The number of seconds to wait until automatically ending the session due to user inactivity. Must be 30, 60, 90, 120, 180, 300, 600, 1800, 3600 or 7200. Default is 120 - - This should be called from danger. - More information in the [device_grid guide](https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/actions/device_grid/README.md). - */ -public func buildAndUploadToAppetize(xcodebuild: [String: Any] = [:], - scheme: OptionalConfigValue = .fastlaneDefault(nil), - apiToken: String, - publicKey: OptionalConfigValue = .fastlaneDefault(nil), - note: OptionalConfigValue = .fastlaneDefault(nil), - timeout: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let xcodebuildArg = RubyCommand.Argument(name: "xcodebuild", value: xcodebuild, type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let publicKeyArg = publicKey.asRubyArgument(name: "public_key", type: nil) - let noteArg = note.asRubyArgument(name: "note", type: nil) - let timeoutArg = timeout.asRubyArgument(name: "timeout", type: nil) - let array: [RubyCommand.Argument?] = [xcodebuildArg, - schemeArg, - apiTokenArg, - publicKeyArg, - noteArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "build_and_upload_to_appetize", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `gradle` action - - - parameters: - - task: The gradle task you want to execute, e.g. `assemble`, `bundle` or `test`. For tasks such as `assembleMyFlavorRelease` you should use gradle(task: 'assemble', flavor: 'Myflavor', build_type: 'Release') - - flavor: The flavor that you want the task for, e.g. `MyFlavor`. If you are running the `assemble` task in a multi-flavor project, and you rely on Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] then you must specify a flavor here or else this value will be undefined - - buildType: The build type that you want the task for, e.g. `Release`. Useful for some tasks such as `assemble` - - tasks: The multiple gradle tasks that you want to execute, e.g. `[assembleDebug, bundleDebug]` - - flags: All parameter flags you want to pass to the gradle command, e.g. `--exitcode --xml file.xml` - - projectDir: The root directory of the gradle project - - gradlePath: The path to your `gradlew`. If you specify a relative path, it is assumed to be relative to the `project_dir` - - properties: Gradle properties to be exposed to the gradle script - - systemProperties: Gradle system properties to be exposed to the gradle script - - serial: Android serial, which device should be used for this command - - printCommand: Control whether the generated Gradle command is printed as output before running it (true/false) - - printCommandOutput: Control whether the output produced by given Gradle command is printed while running (true/false) - - - returns: The output of running the gradle task - - Run `./gradlew tasks` to get a list of all available gradle tasks for your project - */ -public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefault(nil), - flavor: OptionalConfigValue = .fastlaneDefault(nil), - buildType: OptionalConfigValue = .fastlaneDefault(nil), - tasks: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - flags: OptionalConfigValue = .fastlaneDefault(nil), - projectDir: String = ".", - gradlePath: OptionalConfigValue = .fastlaneDefault(nil), - properties: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - systemProperties: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - serial: String = "", - printCommand: OptionalConfigValue = .fastlaneDefault(true), - printCommandOutput: OptionalConfigValue = .fastlaneDefault(true)) -{ - let taskArg = task.asRubyArgument(name: "task", type: nil) - let flavorArg = flavor.asRubyArgument(name: "flavor", type: nil) - let buildTypeArg = buildType.asRubyArgument(name: "build_type", type: nil) - let tasksArg = tasks.asRubyArgument(name: "tasks", type: nil) - let flagsArg = flags.asRubyArgument(name: "flags", type: nil) - let projectDirArg = RubyCommand.Argument(name: "project_dir", value: projectDir, type: nil) - let gradlePathArg = gradlePath.asRubyArgument(name: "gradle_path", type: nil) - let propertiesArg = properties.asRubyArgument(name: "properties", type: nil) - let systemPropertiesArg = systemProperties.asRubyArgument(name: "system_properties", type: nil) - let serialArg = RubyCommand.Argument(name: "serial", value: serial, type: nil) - let printCommandArg = printCommand.asRubyArgument(name: "print_command", type: nil) - let printCommandOutputArg = printCommandOutput.asRubyArgument(name: "print_command_output", type: nil) - let array: [RubyCommand.Argument?] = [taskArg, - flavorArg, - buildTypeArg, - tasksArg, - flagsArg, - projectDirArg, - gradlePathArg, - propertiesArg, - systemPropertiesArg, - serialArg, - printCommandArg, - printCommandOutputArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "build_android_app", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Easily build and sign your app (via _gym_) - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - scheme: The project's scheme. Make sure it's marked as `Shared` - - clean: Should the project be cleaned before building it? - - outputDirectory: The directory in which the ipa file should be stored in - - outputName: The name of the resulting ipa file - - appName: App name to use in logfile name - - configuration: The configuration to use when building the app. Defaults to 'Release' - - silent: Hide all information that's not necessary while building - - codesigningIdentity: The name of the code signing identity to use. It has to match the name exactly. e.g. 'iPhone Distribution: SunApps GmbH' - - skipPackageIpa: Should we skip packaging the ipa? - - skipPackagePkg: Should we skip packaging the pkg? - - includeSymbols: Should the ipa file include symbols? - - includeBitcode: Should the ipa file include bitcode? - - exportMethod: Method used to export the archive. Valid values are: app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application - - exportOptions: Path to an export options plist or a hash with export options. Use 'xcodebuild -help' to print the full set of available options - - exportXcargs: Pass additional arguments to xcodebuild for the package phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - skipBuildArchive: Export ipa from previously built xcarchive. Uses archive_path as source - - skipArchive: After building, don't archive, effectively not including -archivePath param - - skipCodesigning: Build without codesigning - - catalystPlatform: Platform to build when using a Catalyst enabled app. Valid values are: ios, macos - - installerCertName: Full name of 3rd Party Mac Developer Installer or Developer ID Installer certificate. Example: `3rd Party Mac Developer Installer: Your Company (ABC1234XWYZ)` - - buildPath: The directory in which the archive should be stored in - - archivePath: The path to the created archive - - derivedDataPath: The directory where built products and other derived data will go - - resultBundle: Should an Xcode result bundle be generated in the output directory - - resultBundlePath: Path to the result bundle directory to create. Ignored if `result_bundle` if false - - buildlogPath: The directory where to store the build log - - sdk: The SDK that should be used for building the application - - toolchain: The toolchain that should be used for building the application (e.g. com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a) - - destination: Use a custom destination for building the app - - exportTeamId: Optional: Sometimes you need to specify a team id when exporting the ipa file - - xcargs: Pass additional arguments to xcodebuild for the build phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - buildTimingSummary: Create a build timing summary - - disableXcpretty: **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Disable xcpretty formatting of build output - - xcprettyTestFormat: Use the test (RSpec style) format for build output - - xcprettyFormatter: A custom xcpretty formatter to use - - xcprettyReportJunit: Have xcpretty create a JUnit-style XML report at the provided path - - xcprettyReportHtml: Have xcpretty create a simple HTML report at the provided path - - xcprettyReportJson: Have xcpretty create a JSON compilation database at the provided path - - xcprettyUtf: Have xcpretty use unicode encoding when reporting builds - - analyzeBuildTime: Analyze the project build time and store the output in 'culprits.txt' file - - skipProfileDetection: Do not try to build a profile mapping from the xcodeproj. Match or a manually provided mapping should be used - - xcodebuildCommand: Allows for override of the default `xcodebuild` command - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - useSystemScm: Lets xcodebuild use system's scm configuration - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - generateAppstoreInfo: Generate AppStoreInfo.plist using swinfo for app-store exports - - - returns: The absolute path to the generated ipa file - - More information: https://fastlane.tools/gym - */ -@discardableResult public func buildApp(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - clean: OptionalConfigValue = .fastlaneDefault(false), - outputDirectory: String = ".", - outputName: OptionalConfigValue = .fastlaneDefault(nil), - appName: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - silent: OptionalConfigValue = .fastlaneDefault(false), - codesigningIdentity: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageIpa: OptionalConfigValue = .fastlaneDefault(false), - skipPackagePkg: OptionalConfigValue = .fastlaneDefault(false), - includeSymbols: OptionalConfigValue = .fastlaneDefault(nil), - includeBitcode: OptionalConfigValue = .fastlaneDefault(nil), - exportMethod: OptionalConfigValue = .fastlaneDefault(nil), - exportOptions: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - exportXcargs: OptionalConfigValue = .fastlaneDefault(nil), - skipBuildArchive: OptionalConfigValue = .fastlaneDefault(nil), - skipArchive: OptionalConfigValue = .fastlaneDefault(nil), - skipCodesigning: OptionalConfigValue = .fastlaneDefault(nil), - catalystPlatform: OptionalConfigValue = .fastlaneDefault(nil), - installerCertName: OptionalConfigValue = .fastlaneDefault(nil), - buildPath: OptionalConfigValue = .fastlaneDefault(nil), - archivePath: OptionalConfigValue = .fastlaneDefault(nil), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(nil), - resultBundle: OptionalConfigValue = .fastlaneDefault(false), - resultBundlePath: OptionalConfigValue = .fastlaneDefault(nil), - buildlogPath: String = "~/Library/Logs/gym", - sdk: OptionalConfigValue = .fastlaneDefault(nil), - toolchain: OptionalConfigValue = .fastlaneDefault(nil), - destination: OptionalConfigValue = .fastlaneDefault(nil), - exportTeamId: OptionalConfigValue = .fastlaneDefault(nil), - xcargs: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(nil), - xcodebuildFormatter: String = "xcbeautify", - buildTimingSummary: OptionalConfigValue = .fastlaneDefault(false), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyTestFormat: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyFormatter: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportJunit: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportHtml: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportJson: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyUtf: OptionalConfigValue = .fastlaneDefault(nil), - analyzeBuildTime: OptionalConfigValue = .fastlaneDefault(nil), - skipProfileDetection: OptionalConfigValue = .fastlaneDefault(false), - xcodebuildCommand: String = "xcodebuild", - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), - packageCachePath: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(false), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil), - generateAppstoreInfo: OptionalConfigValue = .fastlaneDefault(false)) -> String -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputNameArg = outputName.asRubyArgument(name: "output_name", type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let silentArg = silent.asRubyArgument(name: "silent", type: nil) - let codesigningIdentityArg = codesigningIdentity.asRubyArgument(name: "codesigning_identity", type: nil) - let skipPackageIpaArg = skipPackageIpa.asRubyArgument(name: "skip_package_ipa", type: nil) - let skipPackagePkgArg = skipPackagePkg.asRubyArgument(name: "skip_package_pkg", type: nil) - let includeSymbolsArg = includeSymbols.asRubyArgument(name: "include_symbols", type: nil) - let includeBitcodeArg = includeBitcode.asRubyArgument(name: "include_bitcode", type: nil) - let exportMethodArg = exportMethod.asRubyArgument(name: "export_method", type: nil) - let exportOptionsArg = exportOptions.asRubyArgument(name: "export_options", type: nil) - let exportXcargsArg = exportXcargs.asRubyArgument(name: "export_xcargs", type: nil) - let skipBuildArchiveArg = skipBuildArchive.asRubyArgument(name: "skip_build_archive", type: nil) - let skipArchiveArg = skipArchive.asRubyArgument(name: "skip_archive", type: nil) - let skipCodesigningArg = skipCodesigning.asRubyArgument(name: "skip_codesigning", type: nil) - let catalystPlatformArg = catalystPlatform.asRubyArgument(name: "catalyst_platform", type: nil) - let installerCertNameArg = installerCertName.asRubyArgument(name: "installer_cert_name", type: nil) - let buildPathArg = buildPath.asRubyArgument(name: "build_path", type: nil) - let archivePathArg = archivePath.asRubyArgument(name: "archive_path", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let resultBundlePathArg = resultBundlePath.asRubyArgument(name: "result_bundle_path", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let toolchainArg = toolchain.asRubyArgument(name: "toolchain", type: nil) - let destinationArg = destination.asRubyArgument(name: "destination", type: nil) - let exportTeamIdArg = exportTeamId.asRubyArgument(name: "export_team_id", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let buildTimingSummaryArg = buildTimingSummary.asRubyArgument(name: "build_timing_summary", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let xcprettyTestFormatArg = xcprettyTestFormat.asRubyArgument(name: "xcpretty_test_format", type: nil) - let xcprettyFormatterArg = xcprettyFormatter.asRubyArgument(name: "xcpretty_formatter", type: nil) - let xcprettyReportJunitArg = xcprettyReportJunit.asRubyArgument(name: "xcpretty_report_junit", type: nil) - let xcprettyReportHtmlArg = xcprettyReportHtml.asRubyArgument(name: "xcpretty_report_html", type: nil) - let xcprettyReportJsonArg = xcprettyReportJson.asRubyArgument(name: "xcpretty_report_json", type: nil) - let xcprettyUtfArg = xcprettyUtf.asRubyArgument(name: "xcpretty_utf", type: nil) - let analyzeBuildTimeArg = analyzeBuildTime.asRubyArgument(name: "analyze_build_time", type: nil) - let skipProfileDetectionArg = skipProfileDetection.asRubyArgument(name: "skip_profile_detection", type: nil) - let xcodebuildCommandArg = RubyCommand.Argument(name: "xcodebuild_command", value: xcodebuildCommand, type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let generateAppstoreInfoArg = generateAppstoreInfo.asRubyArgument(name: "generate_appstore_info", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - schemeArg, - cleanArg, - outputDirectoryArg, - outputNameArg, - appNameArg, - configurationArg, - silentArg, - codesigningIdentityArg, - skipPackageIpaArg, - skipPackagePkgArg, - includeSymbolsArg, - includeBitcodeArg, - exportMethodArg, - exportOptionsArg, - exportXcargsArg, - skipBuildArchiveArg, - skipArchiveArg, - skipCodesigningArg, - catalystPlatformArg, - installerCertNameArg, - buildPathArg, - archivePathArg, - derivedDataPathArg, - resultBundleArg, - resultBundlePathArg, - buildlogPathArg, - sdkArg, - toolchainArg, - destinationArg, - exportTeamIdArg, - xcargsArg, - xcconfigArg, - suppressXcodeOutputArg, - xcodebuildFormatterArg, - buildTimingSummaryArg, - disableXcprettyArg, - xcprettyTestFormatArg, - xcprettyFormatterArg, - xcprettyReportJunitArg, - xcprettyReportHtmlArg, - xcprettyReportJsonArg, - xcprettyUtfArg, - analyzeBuildTimeArg, - skipProfileDetectionArg, - xcodebuildCommandArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - useSystemScmArg, - packageAuthorizationProviderArg, - generateAppstoreInfoArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "build_app", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Alias for the `build_app` action but only for iOS - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - scheme: The project's scheme. Make sure it's marked as `Shared` - - clean: Should the project be cleaned before building it? - - outputDirectory: The directory in which the ipa file should be stored in - - outputName: The name of the resulting ipa file - - appName: App name to use in logfile name - - configuration: The configuration to use when building the app. Defaults to 'Release' - - silent: Hide all information that's not necessary while building - - codesigningIdentity: The name of the code signing identity to use. It has to match the name exactly. e.g. 'iPhone Distribution: SunApps GmbH' - - skipPackageIpa: Should we skip packaging the ipa? - - includeSymbols: Should the ipa file include symbols? - - includeBitcode: Should the ipa file include bitcode? - - exportMethod: Method used to export the archive. Valid values are: app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application - - exportOptions: Path to an export options plist or a hash with export options. Use 'xcodebuild -help' to print the full set of available options - - exportXcargs: Pass additional arguments to xcodebuild for the package phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - skipBuildArchive: Export ipa from previously built xcarchive. Uses archive_path as source - - skipArchive: After building, don't archive, effectively not including -archivePath param - - skipCodesigning: Build without codesigning - - buildPath: The directory in which the archive should be stored in - - archivePath: The path to the created archive - - derivedDataPath: The directory where built products and other derived data will go - - resultBundle: Should an Xcode result bundle be generated in the output directory - - resultBundlePath: Path to the result bundle directory to create. Ignored if `result_bundle` if false - - buildlogPath: The directory where to store the build log - - sdk: The SDK that should be used for building the application - - toolchain: The toolchain that should be used for building the application (e.g. com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a) - - destination: Use a custom destination for building the app - - exportTeamId: Optional: Sometimes you need to specify a team id when exporting the ipa file - - xcargs: Pass additional arguments to xcodebuild for the build phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - buildTimingSummary: Create a build timing summary - - disableXcpretty: **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Disable xcpretty formatting of build output - - xcprettyTestFormat: Use the test (RSpec style) format for build output - - xcprettyFormatter: A custom xcpretty formatter to use - - xcprettyReportJunit: Have xcpretty create a JUnit-style XML report at the provided path - - xcprettyReportHtml: Have xcpretty create a simple HTML report at the provided path - - xcprettyReportJson: Have xcpretty create a JSON compilation database at the provided path - - xcprettyUtf: Have xcpretty use unicode encoding when reporting builds - - analyzeBuildTime: Analyze the project build time and store the output in 'culprits.txt' file - - skipProfileDetection: Do not try to build a profile mapping from the xcodeproj. Match or a manually provided mapping should be used - - xcodebuildCommand: Allows for override of the default `xcodebuild` command - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - useSystemScm: Lets xcodebuild use system's scm configuration - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - generateAppstoreInfo: Generate AppStoreInfo.plist using swinfo for app-store exports - - - returns: The absolute path to the generated ipa file - - More information: https://fastlane.tools/gym - */ -@discardableResult public func buildIosApp(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - clean: OptionalConfigValue = .fastlaneDefault(false), - outputDirectory: String = ".", - outputName: OptionalConfigValue = .fastlaneDefault(nil), - appName: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - silent: OptionalConfigValue = .fastlaneDefault(false), - codesigningIdentity: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageIpa: OptionalConfigValue = .fastlaneDefault(false), - includeSymbols: OptionalConfigValue = .fastlaneDefault(nil), - includeBitcode: OptionalConfigValue = .fastlaneDefault(nil), - exportMethod: OptionalConfigValue = .fastlaneDefault(nil), - exportOptions: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - exportXcargs: OptionalConfigValue = .fastlaneDefault(nil), - skipBuildArchive: OptionalConfigValue = .fastlaneDefault(nil), - skipArchive: OptionalConfigValue = .fastlaneDefault(nil), - skipCodesigning: OptionalConfigValue = .fastlaneDefault(nil), - buildPath: OptionalConfigValue = .fastlaneDefault(nil), - archivePath: OptionalConfigValue = .fastlaneDefault(nil), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(nil), - resultBundle: OptionalConfigValue = .fastlaneDefault(false), - resultBundlePath: OptionalConfigValue = .fastlaneDefault(nil), - buildlogPath: String = "~/Library/Logs/gym", - sdk: OptionalConfigValue = .fastlaneDefault(nil), - toolchain: OptionalConfigValue = .fastlaneDefault(nil), - destination: OptionalConfigValue = .fastlaneDefault(nil), - exportTeamId: OptionalConfigValue = .fastlaneDefault(nil), - xcargs: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(nil), - xcodebuildFormatter: String = "xcbeautify", - buildTimingSummary: OptionalConfigValue = .fastlaneDefault(false), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyTestFormat: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyFormatter: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportJunit: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportHtml: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportJson: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyUtf: OptionalConfigValue = .fastlaneDefault(nil), - analyzeBuildTime: OptionalConfigValue = .fastlaneDefault(nil), - skipProfileDetection: OptionalConfigValue = .fastlaneDefault(false), - xcodebuildCommand: String = "xcodebuild", - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), - packageCachePath: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(false), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil), - generateAppstoreInfo: OptionalConfigValue = .fastlaneDefault(false)) -> String -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputNameArg = outputName.asRubyArgument(name: "output_name", type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let silentArg = silent.asRubyArgument(name: "silent", type: nil) - let codesigningIdentityArg = codesigningIdentity.asRubyArgument(name: "codesigning_identity", type: nil) - let skipPackageIpaArg = skipPackageIpa.asRubyArgument(name: "skip_package_ipa", type: nil) - let includeSymbolsArg = includeSymbols.asRubyArgument(name: "include_symbols", type: nil) - let includeBitcodeArg = includeBitcode.asRubyArgument(name: "include_bitcode", type: nil) - let exportMethodArg = exportMethod.asRubyArgument(name: "export_method", type: nil) - let exportOptionsArg = exportOptions.asRubyArgument(name: "export_options", type: nil) - let exportXcargsArg = exportXcargs.asRubyArgument(name: "export_xcargs", type: nil) - let skipBuildArchiveArg = skipBuildArchive.asRubyArgument(name: "skip_build_archive", type: nil) - let skipArchiveArg = skipArchive.asRubyArgument(name: "skip_archive", type: nil) - let skipCodesigningArg = skipCodesigning.asRubyArgument(name: "skip_codesigning", type: nil) - let buildPathArg = buildPath.asRubyArgument(name: "build_path", type: nil) - let archivePathArg = archivePath.asRubyArgument(name: "archive_path", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let resultBundlePathArg = resultBundlePath.asRubyArgument(name: "result_bundle_path", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let toolchainArg = toolchain.asRubyArgument(name: "toolchain", type: nil) - let destinationArg = destination.asRubyArgument(name: "destination", type: nil) - let exportTeamIdArg = exportTeamId.asRubyArgument(name: "export_team_id", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let buildTimingSummaryArg = buildTimingSummary.asRubyArgument(name: "build_timing_summary", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let xcprettyTestFormatArg = xcprettyTestFormat.asRubyArgument(name: "xcpretty_test_format", type: nil) - let xcprettyFormatterArg = xcprettyFormatter.asRubyArgument(name: "xcpretty_formatter", type: nil) - let xcprettyReportJunitArg = xcprettyReportJunit.asRubyArgument(name: "xcpretty_report_junit", type: nil) - let xcprettyReportHtmlArg = xcprettyReportHtml.asRubyArgument(name: "xcpretty_report_html", type: nil) - let xcprettyReportJsonArg = xcprettyReportJson.asRubyArgument(name: "xcpretty_report_json", type: nil) - let xcprettyUtfArg = xcprettyUtf.asRubyArgument(name: "xcpretty_utf", type: nil) - let analyzeBuildTimeArg = analyzeBuildTime.asRubyArgument(name: "analyze_build_time", type: nil) - let skipProfileDetectionArg = skipProfileDetection.asRubyArgument(name: "skip_profile_detection", type: nil) - let xcodebuildCommandArg = RubyCommand.Argument(name: "xcodebuild_command", value: xcodebuildCommand, type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let generateAppstoreInfoArg = generateAppstoreInfo.asRubyArgument(name: "generate_appstore_info", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - schemeArg, - cleanArg, - outputDirectoryArg, - outputNameArg, - appNameArg, - configurationArg, - silentArg, - codesigningIdentityArg, - skipPackageIpaArg, - includeSymbolsArg, - includeBitcodeArg, - exportMethodArg, - exportOptionsArg, - exportXcargsArg, - skipBuildArchiveArg, - skipArchiveArg, - skipCodesigningArg, - buildPathArg, - archivePathArg, - derivedDataPathArg, - resultBundleArg, - resultBundlePathArg, - buildlogPathArg, - sdkArg, - toolchainArg, - destinationArg, - exportTeamIdArg, - xcargsArg, - xcconfigArg, - suppressXcodeOutputArg, - xcodebuildFormatterArg, - buildTimingSummaryArg, - disableXcprettyArg, - xcprettyTestFormatArg, - xcprettyFormatterArg, - xcprettyReportJunitArg, - xcprettyReportHtmlArg, - xcprettyReportJsonArg, - xcprettyUtfArg, - analyzeBuildTimeArg, - skipProfileDetectionArg, - xcodebuildCommandArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - useSystemScmArg, - packageAuthorizationProviderArg, - generateAppstoreInfoArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "build_ios_app", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Alias for the `build_app` action but only for macOS - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - scheme: The project's scheme. Make sure it's marked as `Shared` - - clean: Should the project be cleaned before building it? - - outputDirectory: The directory in which the ipa file should be stored in - - outputName: The name of the resulting ipa file - - appName: App name to use in logfile name - - configuration: The configuration to use when building the app. Defaults to 'Release' - - silent: Hide all information that's not necessary while building - - codesigningIdentity: The name of the code signing identity to use. It has to match the name exactly. e.g. 'iPhone Distribution: SunApps GmbH' - - skipPackagePkg: Should we skip packaging the pkg? - - includeSymbols: Should the ipa file include symbols? - - includeBitcode: Should the ipa file include bitcode? - - exportMethod: Method used to export the archive. Valid values are: app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application - - exportOptions: Path to an export options plist or a hash with export options. Use 'xcodebuild -help' to print the full set of available options - - exportXcargs: Pass additional arguments to xcodebuild for the package phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - skipBuildArchive: Export ipa from previously built xcarchive. Uses archive_path as source - - skipArchive: After building, don't archive, effectively not including -archivePath param - - skipCodesigning: Build without codesigning - - installerCertName: Full name of 3rd Party Mac Developer Installer or Developer ID Installer certificate. Example: `3rd Party Mac Developer Installer: Your Company (ABC1234XWYZ)` - - buildPath: The directory in which the archive should be stored in - - archivePath: The path to the created archive - - derivedDataPath: The directory where built products and other derived data will go - - resultBundle: Should an Xcode result bundle be generated in the output directory - - resultBundlePath: Path to the result bundle directory to create. Ignored if `result_bundle` if false - - buildlogPath: The directory where to store the build log - - sdk: The SDK that should be used for building the application - - toolchain: The toolchain that should be used for building the application (e.g. com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a) - - destination: Use a custom destination for building the app - - exportTeamId: Optional: Sometimes you need to specify a team id when exporting the ipa file - - xcargs: Pass additional arguments to xcodebuild for the build phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - buildTimingSummary: Create a build timing summary - - disableXcpretty: **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Disable xcpretty formatting of build output - - xcprettyTestFormat: Use the test (RSpec style) format for build output - - xcprettyFormatter: A custom xcpretty formatter to use - - xcprettyReportJunit: Have xcpretty create a JUnit-style XML report at the provided path - - xcprettyReportHtml: Have xcpretty create a simple HTML report at the provided path - - xcprettyReportJson: Have xcpretty create a JSON compilation database at the provided path - - xcprettyUtf: Have xcpretty use unicode encoding when reporting builds - - analyzeBuildTime: Analyze the project build time and store the output in 'culprits.txt' file - - skipProfileDetection: Do not try to build a profile mapping from the xcodeproj. Match or a manually provided mapping should be used - - xcodebuildCommand: Allows for override of the default `xcodebuild` command - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - useSystemScm: Lets xcodebuild use system's scm configuration - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - generateAppstoreInfo: Generate AppStoreInfo.plist using swinfo for app-store exports - - - returns: The absolute path to the generated ipa file - - More information: https://fastlane.tools/gym - */ -@discardableResult public func buildMacApp(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - clean: OptionalConfigValue = .fastlaneDefault(false), - outputDirectory: String = ".", - outputName: OptionalConfigValue = .fastlaneDefault(nil), - appName: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - silent: OptionalConfigValue = .fastlaneDefault(false), - codesigningIdentity: OptionalConfigValue = .fastlaneDefault(nil), - skipPackagePkg: OptionalConfigValue = .fastlaneDefault(false), - includeSymbols: OptionalConfigValue = .fastlaneDefault(nil), - includeBitcode: OptionalConfigValue = .fastlaneDefault(nil), - exportMethod: OptionalConfigValue = .fastlaneDefault(nil), - exportOptions: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - exportXcargs: OptionalConfigValue = .fastlaneDefault(nil), - skipBuildArchive: OptionalConfigValue = .fastlaneDefault(nil), - skipArchive: OptionalConfigValue = .fastlaneDefault(nil), - skipCodesigning: OptionalConfigValue = .fastlaneDefault(nil), - installerCertName: OptionalConfigValue = .fastlaneDefault(nil), - buildPath: OptionalConfigValue = .fastlaneDefault(nil), - archivePath: OptionalConfigValue = .fastlaneDefault(nil), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(nil), - resultBundle: OptionalConfigValue = .fastlaneDefault(false), - resultBundlePath: OptionalConfigValue = .fastlaneDefault(nil), - buildlogPath: String = "~/Library/Logs/gym", - sdk: OptionalConfigValue = .fastlaneDefault(nil), - toolchain: OptionalConfigValue = .fastlaneDefault(nil), - destination: OptionalConfigValue = .fastlaneDefault(nil), - exportTeamId: OptionalConfigValue = .fastlaneDefault(nil), - xcargs: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(nil), - xcodebuildFormatter: String = "xcbeautify", - buildTimingSummary: OptionalConfigValue = .fastlaneDefault(false), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyTestFormat: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyFormatter: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportJunit: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportHtml: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyReportJson: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyUtf: OptionalConfigValue = .fastlaneDefault(nil), - analyzeBuildTime: OptionalConfigValue = .fastlaneDefault(nil), - skipProfileDetection: OptionalConfigValue = .fastlaneDefault(false), - xcodebuildCommand: String = "xcodebuild", - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), - packageCachePath: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(false), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil), - generateAppstoreInfo: OptionalConfigValue = .fastlaneDefault(false)) -> String -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputNameArg = outputName.asRubyArgument(name: "output_name", type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let silentArg = silent.asRubyArgument(name: "silent", type: nil) - let codesigningIdentityArg = codesigningIdentity.asRubyArgument(name: "codesigning_identity", type: nil) - let skipPackagePkgArg = skipPackagePkg.asRubyArgument(name: "skip_package_pkg", type: nil) - let includeSymbolsArg = includeSymbols.asRubyArgument(name: "include_symbols", type: nil) - let includeBitcodeArg = includeBitcode.asRubyArgument(name: "include_bitcode", type: nil) - let exportMethodArg = exportMethod.asRubyArgument(name: "export_method", type: nil) - let exportOptionsArg = exportOptions.asRubyArgument(name: "export_options", type: nil) - let exportXcargsArg = exportXcargs.asRubyArgument(name: "export_xcargs", type: nil) - let skipBuildArchiveArg = skipBuildArchive.asRubyArgument(name: "skip_build_archive", type: nil) - let skipArchiveArg = skipArchive.asRubyArgument(name: "skip_archive", type: nil) - let skipCodesigningArg = skipCodesigning.asRubyArgument(name: "skip_codesigning", type: nil) - let installerCertNameArg = installerCertName.asRubyArgument(name: "installer_cert_name", type: nil) - let buildPathArg = buildPath.asRubyArgument(name: "build_path", type: nil) - let archivePathArg = archivePath.asRubyArgument(name: "archive_path", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let resultBundlePathArg = resultBundlePath.asRubyArgument(name: "result_bundle_path", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let toolchainArg = toolchain.asRubyArgument(name: "toolchain", type: nil) - let destinationArg = destination.asRubyArgument(name: "destination", type: nil) - let exportTeamIdArg = exportTeamId.asRubyArgument(name: "export_team_id", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let buildTimingSummaryArg = buildTimingSummary.asRubyArgument(name: "build_timing_summary", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let xcprettyTestFormatArg = xcprettyTestFormat.asRubyArgument(name: "xcpretty_test_format", type: nil) - let xcprettyFormatterArg = xcprettyFormatter.asRubyArgument(name: "xcpretty_formatter", type: nil) - let xcprettyReportJunitArg = xcprettyReportJunit.asRubyArgument(name: "xcpretty_report_junit", type: nil) - let xcprettyReportHtmlArg = xcprettyReportHtml.asRubyArgument(name: "xcpretty_report_html", type: nil) - let xcprettyReportJsonArg = xcprettyReportJson.asRubyArgument(name: "xcpretty_report_json", type: nil) - let xcprettyUtfArg = xcprettyUtf.asRubyArgument(name: "xcpretty_utf", type: nil) - let analyzeBuildTimeArg = analyzeBuildTime.asRubyArgument(name: "analyze_build_time", type: nil) - let skipProfileDetectionArg = skipProfileDetection.asRubyArgument(name: "skip_profile_detection", type: nil) - let xcodebuildCommandArg = RubyCommand.Argument(name: "xcodebuild_command", value: xcodebuildCommand, type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let generateAppstoreInfoArg = generateAppstoreInfo.asRubyArgument(name: "generate_appstore_info", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - schemeArg, - cleanArg, - outputDirectoryArg, - outputNameArg, - appNameArg, - configurationArg, - silentArg, - codesigningIdentityArg, - skipPackagePkgArg, - includeSymbolsArg, - includeBitcodeArg, - exportMethodArg, - exportOptionsArg, - exportXcargsArg, - skipBuildArchiveArg, - skipArchiveArg, - skipCodesigningArg, - installerCertNameArg, - buildPathArg, - archivePathArg, - derivedDataPathArg, - resultBundleArg, - resultBundlePathArg, - buildlogPathArg, - sdkArg, - toolchainArg, - destinationArg, - exportTeamIdArg, - xcargsArg, - xcconfigArg, - suppressXcodeOutputArg, - xcodebuildFormatterArg, - buildTimingSummaryArg, - disableXcprettyArg, - xcprettyTestFormatArg, - xcprettyFormatterArg, - xcprettyReportJunitArg, - xcprettyReportHtmlArg, - xcprettyReportJsonArg, - xcprettyUtfArg, - analyzeBuildTimeArg, - skipProfileDetectionArg, - xcodebuildCommandArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - useSystemScmArg, - packageAuthorizationProviderArg, - generateAppstoreInfoArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "build_mac_app", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - This action runs `bundle install` (if available) - - - parameters: - - binstubs: Generate bin stubs for bundled gems to ./bin - - clean: Run bundle clean automatically after install - - fullIndex: Use the rubygems modern index instead of the API endpoint - - gemfile: Use the specified gemfile instead of Gemfile - - jobs: Install gems using parallel workers - - local: Do not attempt to fetch gems remotely and use the gem cache instead - - deployment: Install using defaults tuned for deployment and CI environments - - noCache: Don't update the existing gem cache - - noPrune: Don't remove stale gems from the cache - - path: Specify a different path than the system default ($BUNDLE_PATH or $GEM_HOME). Bundler will remember this value for future installs on this machine - - system: Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application - - quiet: Only output warnings and errors - - retry: Retry network and git requests that have failed - - shebang: Specify a different shebang executable name than the default (usually 'ruby') - - standalone: Make a bundle that can work without the Bundler runtime - - trustPolicy: Sets level of security when dealing with signed gems. Accepts `LowSecurity`, `MediumSecurity` and `HighSecurity` as values - - without: Exclude gems that are part of the specified named group - - with: Include gems that are part of the specified named group - - frozen: Don't allow the Gemfile.lock to be updated after install - - redownload: Force download every gem, even if the required versions are already available locally - */ -public func bundleInstall(binstubs: OptionalConfigValue = .fastlaneDefault(nil), - clean: OptionalConfigValue = .fastlaneDefault(false), - fullIndex: OptionalConfigValue = .fastlaneDefault(false), - gemfile: OptionalConfigValue = .fastlaneDefault(nil), - jobs: OptionalConfigValue = .fastlaneDefault(nil), - local: OptionalConfigValue = .fastlaneDefault(false), - deployment: OptionalConfigValue = .fastlaneDefault(false), - noCache: OptionalConfigValue = .fastlaneDefault(false), - noPrune: OptionalConfigValue = .fastlaneDefault(false), - path: OptionalConfigValue = .fastlaneDefault(nil), - system: OptionalConfigValue = .fastlaneDefault(false), - quiet: OptionalConfigValue = .fastlaneDefault(false), - retry: OptionalConfigValue = .fastlaneDefault(nil), - shebang: OptionalConfigValue = .fastlaneDefault(nil), - standalone: OptionalConfigValue = .fastlaneDefault(nil), - trustPolicy: OptionalConfigValue = .fastlaneDefault(nil), - without: OptionalConfigValue = .fastlaneDefault(nil), - with: OptionalConfigValue = .fastlaneDefault(nil), - frozen: OptionalConfigValue = .fastlaneDefault(false), - redownload: OptionalConfigValue = .fastlaneDefault(false)) -{ - let binstubsArg = binstubs.asRubyArgument(name: "binstubs", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let fullIndexArg = fullIndex.asRubyArgument(name: "full_index", type: nil) - let gemfileArg = gemfile.asRubyArgument(name: "gemfile", type: nil) - let jobsArg = jobs.asRubyArgument(name: "jobs", type: nil) - let localArg = local.asRubyArgument(name: "local", type: nil) - let deploymentArg = deployment.asRubyArgument(name: "deployment", type: nil) - let noCacheArg = noCache.asRubyArgument(name: "no_cache", type: nil) - let noPruneArg = noPrune.asRubyArgument(name: "no_prune", type: nil) - let pathArg = path.asRubyArgument(name: "path", type: nil) - let systemArg = system.asRubyArgument(name: "system", type: nil) - let quietArg = quiet.asRubyArgument(name: "quiet", type: nil) - let retryArg = retry.asRubyArgument(name: "retry", type: nil) - let shebangArg = shebang.asRubyArgument(name: "shebang", type: nil) - let standaloneArg = standalone.asRubyArgument(name: "standalone", type: nil) - let trustPolicyArg = trustPolicy.asRubyArgument(name: "trust_policy", type: nil) - let withoutArg = without.asRubyArgument(name: "without", type: nil) - let withArg = with.asRubyArgument(name: "with", type: nil) - let frozenArg = frozen.asRubyArgument(name: "frozen", type: nil) - let redownloadArg = redownload.asRubyArgument(name: "redownload", type: nil) - let array: [RubyCommand.Argument?] = [binstubsArg, - cleanArg, - fullIndexArg, - gemfileArg, - jobsArg, - localArg, - deploymentArg, - noCacheArg, - noPruneArg, - pathArg, - systemArg, - quietArg, - retryArg, - shebangArg, - standaloneArg, - trustPolicyArg, - withoutArg, - withArg, - frozenArg, - redownloadArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "bundle_install", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Automated localized screenshots of your Android app (via _screengrab_) - - - parameters: - - androidHome: Path to the root of your Android SDK installation, e.g. ~/tools/android-sdk-macosx - - buildToolsVersion: **DEPRECATED!** The Android build tools version to use, e.g. '23.0.2' - - locales: A list of locales which should be used - - clearPreviousScreenshots: Enabling this option will automatically clear previously generated screenshots before running screengrab - - outputDirectory: The directory where to store the screenshots - - skipOpenSummary: Don't open the summary after running _screengrab_ - - appPackageName: The package name of the app under test (e.g. com.yourcompany.yourapp) - - testsPackageName: The package name of the tests bundle (e.g. com.yourcompany.yourapp.test) - - useTestsInPackages: Only run tests in these Java packages - - useTestsInClasses: Only run tests in these Java classes - - launchArguments: Additional launch arguments - - testInstrumentationRunner: The fully qualified class name of your test instrumentation runner - - endingLocale: **DEPRECATED!** Return the device to this locale after running tests - - useAdbRoot: **DEPRECATED!** Restarts the adb daemon using `adb root` to allow access to screenshots directories on device. Use if getting 'Permission denied' errors - - appApkPath: The path to the APK for the app under test - - testsApkPath: The path to the APK for the tests bundle - - specificDevice: Use the device or emulator with the given serial number or qualifier - - deviceType: Type of device used for screenshots. Matches Google Play Types (phone, sevenInch, tenInch, tv, wear) - - exitOnTestFailure: Whether or not to exit Screengrab on test failure. Exiting on failure will not copy screenshots to local machine nor open screenshots summary - - reinstallApp: Enabling this option will automatically uninstall the application before running it - - useTimestampSuffix: Add timestamp suffix to screenshot filename - - adbHost: Configure the host used by adb to connect, allows running on remote devices farm - */ -public func captureAndroidScreenshots(androidHome: OptionalConfigValue = .fastlaneDefault(nil), - buildToolsVersion: OptionalConfigValue = .fastlaneDefault(nil), - locales: [String] = ["en-US"], - clearPreviousScreenshots: OptionalConfigValue = .fastlaneDefault(false), - outputDirectory: String = "fastlane/metadata/android", - skipOpenSummary: OptionalConfigValue = .fastlaneDefault(false), - appPackageName: String, - testsPackageName: OptionalConfigValue = .fastlaneDefault(nil), - useTestsInPackages: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - useTestsInClasses: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - launchArguments: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - testInstrumentationRunner: String = "androidx.test.runner.AndroidJUnitRunner", - endingLocale: String = "en-US", - useAdbRoot: OptionalConfigValue = .fastlaneDefault(false), - appApkPath: OptionalConfigValue = .fastlaneDefault(nil), - testsApkPath: OptionalConfigValue = .fastlaneDefault(nil), - specificDevice: OptionalConfigValue = .fastlaneDefault(nil), - deviceType: String = "phone", - exitOnTestFailure: OptionalConfigValue = .fastlaneDefault(true), - reinstallApp: OptionalConfigValue = .fastlaneDefault(false), - useTimestampSuffix: OptionalConfigValue = .fastlaneDefault(true), - adbHost: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let androidHomeArg = androidHome.asRubyArgument(name: "android_home", type: nil) - let buildToolsVersionArg = buildToolsVersion.asRubyArgument(name: "build_tools_version", type: nil) - let localesArg = RubyCommand.Argument(name: "locales", value: locales, type: nil) - let clearPreviousScreenshotsArg = clearPreviousScreenshots.asRubyArgument(name: "clear_previous_screenshots", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let skipOpenSummaryArg = skipOpenSummary.asRubyArgument(name: "skip_open_summary", type: nil) - let appPackageNameArg = RubyCommand.Argument(name: "app_package_name", value: appPackageName, type: nil) - let testsPackageNameArg = testsPackageName.asRubyArgument(name: "tests_package_name", type: nil) - let useTestsInPackagesArg = useTestsInPackages.asRubyArgument(name: "use_tests_in_packages", type: nil) - let useTestsInClassesArg = useTestsInClasses.asRubyArgument(name: "use_tests_in_classes", type: nil) - let launchArgumentsArg = launchArguments.asRubyArgument(name: "launch_arguments", type: nil) - let testInstrumentationRunnerArg = RubyCommand.Argument(name: "test_instrumentation_runner", value: testInstrumentationRunner, type: nil) - let endingLocaleArg = RubyCommand.Argument(name: "ending_locale", value: endingLocale, type: nil) - let useAdbRootArg = useAdbRoot.asRubyArgument(name: "use_adb_root", type: nil) - let appApkPathArg = appApkPath.asRubyArgument(name: "app_apk_path", type: nil) - let testsApkPathArg = testsApkPath.asRubyArgument(name: "tests_apk_path", type: nil) - let specificDeviceArg = specificDevice.asRubyArgument(name: "specific_device", type: nil) - let deviceTypeArg = RubyCommand.Argument(name: "device_type", value: deviceType, type: nil) - let exitOnTestFailureArg = exitOnTestFailure.asRubyArgument(name: "exit_on_test_failure", type: nil) - let reinstallAppArg = reinstallApp.asRubyArgument(name: "reinstall_app", type: nil) - let useTimestampSuffixArg = useTimestampSuffix.asRubyArgument(name: "use_timestamp_suffix", type: nil) - let adbHostArg = adbHost.asRubyArgument(name: "adb_host", type: nil) - let array: [RubyCommand.Argument?] = [androidHomeArg, - buildToolsVersionArg, - localesArg, - clearPreviousScreenshotsArg, - outputDirectoryArg, - skipOpenSummaryArg, - appPackageNameArg, - testsPackageNameArg, - useTestsInPackagesArg, - useTestsInClassesArg, - launchArgumentsArg, - testInstrumentationRunnerArg, - endingLocaleArg, - useAdbRootArg, - appApkPathArg, - testsApkPathArg, - specificDeviceArg, - deviceTypeArg, - exitOnTestFailureArg, - reinstallAppArg, - useTimestampSuffixArg, - adbHostArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "capture_android_screenshots", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generate new localized screenshots on multiple devices (via _snapshot_) - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - xcargs: Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - devices: A list of devices you want to take the screenshots from - - languages: A list of languages which should be used - - launchArguments: A list of launch arguments which should be used - - outputDirectory: The directory where to store the screenshots - - outputSimulatorLogs: If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory - - iosVersion: By default, the latest version should be used automatically. If you want to change it, do it here - - skipOpenSummary: Don't open the HTML summary after running _snapshot_ - - skipHelperVersionCheck: Do not check for most recent SnapshotHelper code - - clearPreviousScreenshots: Enabling this option will automatically clear previously generated screenshots before running snapshot - - reinstallApp: Enabling this option will automatically uninstall the application before running it - - eraseSimulator: Enabling this option will automatically erase the simulator before running the application - - headless: Enabling this option will prevent displaying the simulator window - - overrideStatusBar: Enabling this option will automatically override the status bar to show 9:41 AM, full battery, and full reception (Adjust 'SNAPSHOT_SIMULATOR_WAIT_FOR_BOOT_TIMEOUT' environment variable if override status bar is not working. Might be because simulator is not fully booted. Defaults to 10 seconds) - - overrideStatusBarArguments: Fully customize the status bar by setting each option here. Requires `override_status_bar` to be set to `true`. See `xcrun simctl status_bar --help` - - localizeSimulator: Enabling this option will configure the Simulator's system language - - darkMode: Enabling this option will configure the Simulator to be in dark mode (false for light, true for dark) - - appIdentifier: The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) - - addPhotos: A list of photos that should be added to the simulator before running the application - - addVideos: A list of videos that should be added to the simulator before running the application - - htmlTemplate: A path to screenshots.html template - - buildlogPath: The directory where to store the build log - - clean: Should the project be cleaned before building it? - - testWithoutBuilding: Test without building, requires a derived data path - - configuration: The configuration to use when building the app. Defaults to 'Release' - - sdk: The SDK that should be used for building the application - - scheme: The scheme you want to use, this must be the scheme for the UI Tests - - numberOfRetries: The number of times a test can fail before snapshot should stop retrying - - stopAfterFirstError: Should snapshot stop immediately after the tests completely failed on one device? - - derivedDataPath: The directory where build products and other derived data will go - - resultBundle: Should an Xcode result bundle be generated in the output directory - - testTargetName: The name of the target you want to test (if you desire to override the Target Application from Xcode) - - namespaceLogFiles: Separate the log files per device and per language - - concurrentSimulators: Take snapshots on multiple simulators concurrently. Note: This option is only applicable when running against Xcode 9 - - disableSlideToType: Disable the simulator from showing the 'Slide to type' prompt - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - testplan: The testplan associated with the scheme that should be used for testing - - onlyTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to run - - skipTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to skip - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - xcprettyArgs: **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Additional xcpretty arguments - - disableXcpretty: Disable xcpretty formatting of build - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - useSystemScm: Lets xcodebuild use system's scm configuration - */ -public func captureIosScreenshots(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - xcargs: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - devices: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - languages: [String] = ["en-US"], - launchArguments: [String] = [""], - outputDirectory: String = "screenshots", - outputSimulatorLogs: OptionalConfigValue = .fastlaneDefault(false), - iosVersion: OptionalConfigValue = .fastlaneDefault(nil), - skipOpenSummary: OptionalConfigValue = .fastlaneDefault(false), - skipHelperVersionCheck: OptionalConfigValue = .fastlaneDefault(false), - clearPreviousScreenshots: OptionalConfigValue = .fastlaneDefault(false), - reinstallApp: OptionalConfigValue = .fastlaneDefault(false), - eraseSimulator: OptionalConfigValue = .fastlaneDefault(false), - headless: OptionalConfigValue = .fastlaneDefault(true), - overrideStatusBar: OptionalConfigValue = .fastlaneDefault(false), - overrideStatusBarArguments: OptionalConfigValue = .fastlaneDefault(nil), - localizeSimulator: OptionalConfigValue = .fastlaneDefault(false), - darkMode: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - addPhotos: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - addVideos: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - htmlTemplate: OptionalConfigValue = .fastlaneDefault(nil), - buildlogPath: String = "~/Library/Logs/snapshot", - clean: OptionalConfigValue = .fastlaneDefault(false), - testWithoutBuilding: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - sdk: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - numberOfRetries: Int = 1, - stopAfterFirstError: OptionalConfigValue = .fastlaneDefault(false), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(nil), - resultBundle: OptionalConfigValue = .fastlaneDefault(false), - testTargetName: OptionalConfigValue = .fastlaneDefault(nil), - namespaceLogFiles: Any? = nil, - concurrentSimulators: OptionalConfigValue = .fastlaneDefault(true), - disableSlideToType: OptionalConfigValue = .fastlaneDefault(false), - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), - packageCachePath: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(false), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil), - testplan: OptionalConfigValue = .fastlaneDefault(nil), - onlyTesting: Any? = nil, - skipTesting: Any? = nil, - xcodebuildFormatter: String = "xcbeautify", - xcprettyArgs: OptionalConfigValue = .fastlaneDefault(nil), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(nil), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(nil), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false)) -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let devicesArg = devices.asRubyArgument(name: "devices", type: nil) - let languagesArg = RubyCommand.Argument(name: "languages", value: languages, type: nil) - let launchArgumentsArg = RubyCommand.Argument(name: "launch_arguments", value: launchArguments, type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputSimulatorLogsArg = outputSimulatorLogs.asRubyArgument(name: "output_simulator_logs", type: nil) - let iosVersionArg = iosVersion.asRubyArgument(name: "ios_version", type: nil) - let skipOpenSummaryArg = skipOpenSummary.asRubyArgument(name: "skip_open_summary", type: nil) - let skipHelperVersionCheckArg = skipHelperVersionCheck.asRubyArgument(name: "skip_helper_version_check", type: nil) - let clearPreviousScreenshotsArg = clearPreviousScreenshots.asRubyArgument(name: "clear_previous_screenshots", type: nil) - let reinstallAppArg = reinstallApp.asRubyArgument(name: "reinstall_app", type: nil) - let eraseSimulatorArg = eraseSimulator.asRubyArgument(name: "erase_simulator", type: nil) - let headlessArg = headless.asRubyArgument(name: "headless", type: nil) - let overrideStatusBarArg = overrideStatusBar.asRubyArgument(name: "override_status_bar", type: nil) - let overrideStatusBarArgumentsArg = overrideStatusBarArguments.asRubyArgument(name: "override_status_bar_arguments", type: nil) - let localizeSimulatorArg = localizeSimulator.asRubyArgument(name: "localize_simulator", type: nil) - let darkModeArg = darkMode.asRubyArgument(name: "dark_mode", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let addPhotosArg = addPhotos.asRubyArgument(name: "add_photos", type: nil) - let addVideosArg = addVideos.asRubyArgument(name: "add_videos", type: nil) - let htmlTemplateArg = htmlTemplate.asRubyArgument(name: "html_template", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let testWithoutBuildingArg = testWithoutBuilding.asRubyArgument(name: "test_without_building", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let numberOfRetriesArg = RubyCommand.Argument(name: "number_of_retries", value: numberOfRetries, type: nil) - let stopAfterFirstErrorArg = stopAfterFirstError.asRubyArgument(name: "stop_after_first_error", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let testTargetNameArg = testTargetName.asRubyArgument(name: "test_target_name", type: nil) - let namespaceLogFilesArg = RubyCommand.Argument(name: "namespace_log_files", value: namespaceLogFiles, type: nil) - let concurrentSimulatorsArg = concurrentSimulators.asRubyArgument(name: "concurrent_simulators", type: nil) - let disableSlideToTypeArg = disableSlideToType.asRubyArgument(name: "disable_slide_to_type", type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) - let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) - let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let xcprettyArgsArg = xcprettyArgs.asRubyArgument(name: "xcpretty_args", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - xcargsArg, - xcconfigArg, - devicesArg, - languagesArg, - launchArgumentsArg, - outputDirectoryArg, - outputSimulatorLogsArg, - iosVersionArg, - skipOpenSummaryArg, - skipHelperVersionCheckArg, - clearPreviousScreenshotsArg, - reinstallAppArg, - eraseSimulatorArg, - headlessArg, - overrideStatusBarArg, - overrideStatusBarArgumentsArg, - localizeSimulatorArg, - darkModeArg, - appIdentifierArg, - addPhotosArg, - addVideosArg, - htmlTemplateArg, - buildlogPathArg, - cleanArg, - testWithoutBuildingArg, - configurationArg, - sdkArg, - schemeArg, - numberOfRetriesArg, - stopAfterFirstErrorArg, - derivedDataPathArg, - resultBundleArg, - testTargetNameArg, - namespaceLogFilesArg, - concurrentSimulatorsArg, - disableSlideToTypeArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - packageAuthorizationProviderArg, - testplanArg, - onlyTestingArg, - skipTestingArg, - xcodebuildFormatterArg, - xcprettyArgsArg, - disableXcprettyArg, - suppressXcodeOutputArg, - useSystemScmArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "capture_ios_screenshots", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `capture_ios_screenshots` action - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - xcargs: Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - devices: A list of devices you want to take the screenshots from - - languages: A list of languages which should be used - - launchArguments: A list of launch arguments which should be used - - outputDirectory: The directory where to store the screenshots - - outputSimulatorLogs: If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory - - iosVersion: By default, the latest version should be used automatically. If you want to change it, do it here - - skipOpenSummary: Don't open the HTML summary after running _snapshot_ - - skipHelperVersionCheck: Do not check for most recent SnapshotHelper code - - clearPreviousScreenshots: Enabling this option will automatically clear previously generated screenshots before running snapshot - - reinstallApp: Enabling this option will automatically uninstall the application before running it - - eraseSimulator: Enabling this option will automatically erase the simulator before running the application - - headless: Enabling this option will prevent displaying the simulator window - - overrideStatusBar: Enabling this option will automatically override the status bar to show 9:41 AM, full battery, and full reception (Adjust 'SNAPSHOT_SIMULATOR_WAIT_FOR_BOOT_TIMEOUT' environment variable if override status bar is not working. Might be because simulator is not fully booted. Defaults to 10 seconds) - - overrideStatusBarArguments: Fully customize the status bar by setting each option here. Requires `override_status_bar` to be set to `true`. See `xcrun simctl status_bar --help` - - localizeSimulator: Enabling this option will configure the Simulator's system language - - darkMode: Enabling this option will configure the Simulator to be in dark mode (false for light, true for dark) - - appIdentifier: The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) - - addPhotos: A list of photos that should be added to the simulator before running the application - - addVideos: A list of videos that should be added to the simulator before running the application - - htmlTemplate: A path to screenshots.html template - - buildlogPath: The directory where to store the build log - - clean: Should the project be cleaned before building it? - - testWithoutBuilding: Test without building, requires a derived data path - - configuration: The configuration to use when building the app. Defaults to 'Release' - - sdk: The SDK that should be used for building the application - - scheme: The scheme you want to use, this must be the scheme for the UI Tests - - numberOfRetries: The number of times a test can fail before snapshot should stop retrying - - stopAfterFirstError: Should snapshot stop immediately after the tests completely failed on one device? - - derivedDataPath: The directory where build products and other derived data will go - - resultBundle: Should an Xcode result bundle be generated in the output directory - - testTargetName: The name of the target you want to test (if you desire to override the Target Application from Xcode) - - namespaceLogFiles: Separate the log files per device and per language - - concurrentSimulators: Take snapshots on multiple simulators concurrently. Note: This option is only applicable when running against Xcode 9 - - disableSlideToType: Disable the simulator from showing the 'Slide to type' prompt - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - testplan: The testplan associated with the scheme that should be used for testing - - onlyTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to run - - skipTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to skip - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - xcprettyArgs: **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Additional xcpretty arguments - - disableXcpretty: Disable xcpretty formatting of build - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - useSystemScm: Lets xcodebuild use system's scm configuration - */ -public func captureScreenshots(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - xcargs: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - devices: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - languages: [String] = ["en-US"], - launchArguments: [String] = [""], - outputDirectory: String = "screenshots", - outputSimulatorLogs: OptionalConfigValue = .fastlaneDefault(false), - iosVersion: OptionalConfigValue = .fastlaneDefault(nil), - skipOpenSummary: OptionalConfigValue = .fastlaneDefault(false), - skipHelperVersionCheck: OptionalConfigValue = .fastlaneDefault(false), - clearPreviousScreenshots: OptionalConfigValue = .fastlaneDefault(false), - reinstallApp: OptionalConfigValue = .fastlaneDefault(false), - eraseSimulator: OptionalConfigValue = .fastlaneDefault(false), - headless: OptionalConfigValue = .fastlaneDefault(true), - overrideStatusBar: OptionalConfigValue = .fastlaneDefault(false), - overrideStatusBarArguments: OptionalConfigValue = .fastlaneDefault(nil), - localizeSimulator: OptionalConfigValue = .fastlaneDefault(false), - darkMode: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - addPhotos: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - addVideos: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - htmlTemplate: OptionalConfigValue = .fastlaneDefault(nil), - buildlogPath: String = "~/Library/Logs/snapshot", - clean: OptionalConfigValue = .fastlaneDefault(false), - testWithoutBuilding: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - sdk: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - numberOfRetries: Int = 1, - stopAfterFirstError: OptionalConfigValue = .fastlaneDefault(false), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(nil), - resultBundle: OptionalConfigValue = .fastlaneDefault(false), - testTargetName: OptionalConfigValue = .fastlaneDefault(nil), - namespaceLogFiles: Any? = nil, - concurrentSimulators: OptionalConfigValue = .fastlaneDefault(true), - disableSlideToType: OptionalConfigValue = .fastlaneDefault(false), - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), - packageCachePath: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(false), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil), - testplan: OptionalConfigValue = .fastlaneDefault(nil), - onlyTesting: Any? = nil, - skipTesting: Any? = nil, - xcodebuildFormatter: String = "xcbeautify", - xcprettyArgs: OptionalConfigValue = .fastlaneDefault(nil), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(nil), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(nil), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false)) -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let devicesArg = devices.asRubyArgument(name: "devices", type: nil) - let languagesArg = RubyCommand.Argument(name: "languages", value: languages, type: nil) - let launchArgumentsArg = RubyCommand.Argument(name: "launch_arguments", value: launchArguments, type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputSimulatorLogsArg = outputSimulatorLogs.asRubyArgument(name: "output_simulator_logs", type: nil) - let iosVersionArg = iosVersion.asRubyArgument(name: "ios_version", type: nil) - let skipOpenSummaryArg = skipOpenSummary.asRubyArgument(name: "skip_open_summary", type: nil) - let skipHelperVersionCheckArg = skipHelperVersionCheck.asRubyArgument(name: "skip_helper_version_check", type: nil) - let clearPreviousScreenshotsArg = clearPreviousScreenshots.asRubyArgument(name: "clear_previous_screenshots", type: nil) - let reinstallAppArg = reinstallApp.asRubyArgument(name: "reinstall_app", type: nil) - let eraseSimulatorArg = eraseSimulator.asRubyArgument(name: "erase_simulator", type: nil) - let headlessArg = headless.asRubyArgument(name: "headless", type: nil) - let overrideStatusBarArg = overrideStatusBar.asRubyArgument(name: "override_status_bar", type: nil) - let overrideStatusBarArgumentsArg = overrideStatusBarArguments.asRubyArgument(name: "override_status_bar_arguments", type: nil) - let localizeSimulatorArg = localizeSimulator.asRubyArgument(name: "localize_simulator", type: nil) - let darkModeArg = darkMode.asRubyArgument(name: "dark_mode", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let addPhotosArg = addPhotos.asRubyArgument(name: "add_photos", type: nil) - let addVideosArg = addVideos.asRubyArgument(name: "add_videos", type: nil) - let htmlTemplateArg = htmlTemplate.asRubyArgument(name: "html_template", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let testWithoutBuildingArg = testWithoutBuilding.asRubyArgument(name: "test_without_building", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let numberOfRetriesArg = RubyCommand.Argument(name: "number_of_retries", value: numberOfRetries, type: nil) - let stopAfterFirstErrorArg = stopAfterFirstError.asRubyArgument(name: "stop_after_first_error", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let testTargetNameArg = testTargetName.asRubyArgument(name: "test_target_name", type: nil) - let namespaceLogFilesArg = RubyCommand.Argument(name: "namespace_log_files", value: namespaceLogFiles, type: nil) - let concurrentSimulatorsArg = concurrentSimulators.asRubyArgument(name: "concurrent_simulators", type: nil) - let disableSlideToTypeArg = disableSlideToType.asRubyArgument(name: "disable_slide_to_type", type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) - let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) - let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let xcprettyArgsArg = xcprettyArgs.asRubyArgument(name: "xcpretty_args", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - xcargsArg, - xcconfigArg, - devicesArg, - languagesArg, - launchArgumentsArg, - outputDirectoryArg, - outputSimulatorLogsArg, - iosVersionArg, - skipOpenSummaryArg, - skipHelperVersionCheckArg, - clearPreviousScreenshotsArg, - reinstallAppArg, - eraseSimulatorArg, - headlessArg, - overrideStatusBarArg, - overrideStatusBarArgumentsArg, - localizeSimulatorArg, - darkModeArg, - appIdentifierArg, - addPhotosArg, - addVideosArg, - htmlTemplateArg, - buildlogPathArg, - cleanArg, - testWithoutBuildingArg, - configurationArg, - sdkArg, - schemeArg, - numberOfRetriesArg, - stopAfterFirstErrorArg, - derivedDataPathArg, - resultBundleArg, - testTargetNameArg, - namespaceLogFilesArg, - concurrentSimulatorsArg, - disableSlideToTypeArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - packageAuthorizationProviderArg, - testplanArg, - onlyTestingArg, - skipTestingArg, - xcodebuildFormatterArg, - xcprettyArgsArg, - disableXcprettyArg, - suppressXcodeOutputArg, - useSystemScmArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "capture_screenshots", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs `carthage` for your project - - - parameters: - - command: Carthage command (one of: build, bootstrap, update, archive) - - dependencies: Carthage dependencies to update, build or bootstrap - - useSsh: Use SSH for downloading GitHub repositories - - useSubmodules: Add dependencies as Git submodules - - useNetrc: Use .netrc for downloading frameworks - - useBinaries: Check out dependency repositories even when prebuilt frameworks exist - - noCheckout: When bootstrapping Carthage do not checkout - - noBuild: When bootstrapping Carthage do not build - - noSkipCurrent: Don't skip building the Carthage project (in addition to its dependencies) - - derivedData: Use derived data folder at path - - verbose: Print xcodebuild output inline - - platform: Define which platform to build for - - cacheBuilds: By default Carthage will rebuild a dependency regardless of whether it's the same resolved version as before. Passing the --cache-builds will cause carthage to avoid rebuilding a dependency if it can - - frameworks: Framework name or names to archive, could be applied only along with the archive command - - output: Output name for the archive, could be applied only along with the archive command. Use following format *.framework.zip - - configuration: Define which build configuration to use when building - - toolchain: Define which xcodebuild toolchain to use when building - - projectDirectory: Define the directory containing the Carthage project - - newResolver: Use new resolver when resolving dependency graph - - logPath: Path to the xcode build output - - useXcframeworks: Create xcframework bundles instead of one framework per platform (requires Xcode 12+) - - archive: Archive built frameworks from the current project - - executable: Path to the `carthage` executable on your machine - */ -public func carthage(command: String = "bootstrap", - dependencies: [String] = [], - useSsh: OptionalConfigValue = .fastlaneDefault(nil), - useSubmodules: OptionalConfigValue = .fastlaneDefault(nil), - useNetrc: OptionalConfigValue = .fastlaneDefault(nil), - useBinaries: OptionalConfigValue = .fastlaneDefault(nil), - noCheckout: OptionalConfigValue = .fastlaneDefault(nil), - noBuild: OptionalConfigValue = .fastlaneDefault(nil), - noSkipCurrent: OptionalConfigValue = .fastlaneDefault(nil), - derivedData: OptionalConfigValue = .fastlaneDefault(nil), - verbose: OptionalConfigValue = .fastlaneDefault(nil), - platform: OptionalConfigValue = .fastlaneDefault(nil), - cacheBuilds: OptionalConfigValue = .fastlaneDefault(false), - frameworks: [String] = [], - output: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - toolchain: OptionalConfigValue = .fastlaneDefault(nil), - projectDirectory: OptionalConfigValue = .fastlaneDefault(nil), - newResolver: OptionalConfigValue = .fastlaneDefault(nil), - logPath: OptionalConfigValue = .fastlaneDefault(nil), - useXcframeworks: OptionalConfigValue = .fastlaneDefault(false), - archive: OptionalConfigValue = .fastlaneDefault(false), - executable: String = "carthage") -{ - let commandArg = RubyCommand.Argument(name: "command", value: command, type: nil) - let dependenciesArg = RubyCommand.Argument(name: "dependencies", value: dependencies, type: nil) - let useSshArg = useSsh.asRubyArgument(name: "use_ssh", type: nil) - let useSubmodulesArg = useSubmodules.asRubyArgument(name: "use_submodules", type: nil) - let useNetrcArg = useNetrc.asRubyArgument(name: "use_netrc", type: nil) - let useBinariesArg = useBinaries.asRubyArgument(name: "use_binaries", type: nil) - let noCheckoutArg = noCheckout.asRubyArgument(name: "no_checkout", type: nil) - let noBuildArg = noBuild.asRubyArgument(name: "no_build", type: nil) - let noSkipCurrentArg = noSkipCurrent.asRubyArgument(name: "no_skip_current", type: nil) - let derivedDataArg = derivedData.asRubyArgument(name: "derived_data", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let platformArg = platform.asRubyArgument(name: "platform", type: nil) - let cacheBuildsArg = cacheBuilds.asRubyArgument(name: "cache_builds", type: nil) - let frameworksArg = RubyCommand.Argument(name: "frameworks", value: frameworks, type: nil) - let outputArg = output.asRubyArgument(name: "output", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let toolchainArg = toolchain.asRubyArgument(name: "toolchain", type: nil) - let projectDirectoryArg = projectDirectory.asRubyArgument(name: "project_directory", type: nil) - let newResolverArg = newResolver.asRubyArgument(name: "new_resolver", type: nil) - let logPathArg = logPath.asRubyArgument(name: "log_path", type: nil) - let useXcframeworksArg = useXcframeworks.asRubyArgument(name: "use_xcframeworks", type: nil) - let archiveArg = archive.asRubyArgument(name: "archive", type: nil) - let executableArg = RubyCommand.Argument(name: "executable", value: executable, type: nil) - let array: [RubyCommand.Argument?] = [commandArg, - dependenciesArg, - useSshArg, - useSubmodulesArg, - useNetrcArg, - useBinariesArg, - noCheckoutArg, - noBuildArg, - noSkipCurrentArg, - derivedDataArg, - verboseArg, - platformArg, - cacheBuildsArg, - frameworksArg, - outputArg, - configurationArg, - toolchainArg, - projectDirectoryArg, - newResolverArg, - logPathArg, - useXcframeworksArg, - archiveArg, - executableArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "carthage", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `get_certificates` action - - - parameters: - - development: Create a development certificate instead of a distribution one - - type: Create specific certificate type (takes precedence over :development) - - force: Create a certificate even if an existing certificate exists - - generateAppleCerts: Create a certificate type for Xcode 11 and later (Apple Development or Apple Distribution) - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - filename: The filename of certificate to store - - outputPath: The path to a directory in which all certificates and private keys should be stored - - keychainPath: Path to a custom keychain - - keychainPassword: This might be required the first time you access certificates on a new mac. For the login/default keychain this is your macOS account password - - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing - - platform: Set the provisioning profile's platform (ios, macos, tvos) - - **Important**: It is recommended to use [match](https://docs.fastlane.tools/actions/match/) according to the [codesigning.guide](https://codesigning.guide) for generating and maintaining your certificates. Use _cert_ directly only if you want full control over what's going on and know more about codesigning. - Use this action to download the latest code signing identity. - */ -public func cert(development: OptionalConfigValue = .fastlaneDefault(false), - type: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - generateAppleCerts: OptionalConfigValue = .fastlaneDefault(true), - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - filename: OptionalConfigValue = .fastlaneDefault(nil), - outputPath: String = ".", - keychainPath: OptionalConfigValue = .fastlaneDefault(nil), - keychainPassword: OptionalConfigValue = .fastlaneDefault(nil), - skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(false), - platform: String = "ios") -{ - let developmentArg = development.asRubyArgument(name: "development", type: nil) - let typeArg = type.asRubyArgument(name: "type", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let generateAppleCertsArg = generateAppleCerts.asRubyArgument(name: "generate_apple_certs", type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let filenameArg = filename.asRubyArgument(name: "filename", type: nil) - let outputPathArg = RubyCommand.Argument(name: "output_path", value: outputPath, type: nil) - let keychainPathArg = keychainPath.asRubyArgument(name: "keychain_path", type: nil) - let keychainPasswordArg = keychainPassword.asRubyArgument(name: "keychain_password", type: nil) - let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let array: [RubyCommand.Argument?] = [developmentArg, - typeArg, - forceArg, - generateAppleCertsArg, - apiKeyPathArg, - apiKeyArg, - usernameArg, - teamIdArg, - teamNameArg, - filenameArg, - outputPathArg, - keychainPathArg, - keychainPasswordArg, - skipSetPartitionListArg, - platformArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "cert", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Collect git commit messages into a changelog - - - parameters: - - between: Array containing two Git revision values between which to collect messages, you mustn't use it with :commits_count key at the same time - - commitsCount: Number of commits to include in changelog, you mustn't use it with :between key at the same time - - path: Path of the git repository - - pretty: The format applied to each commit while generating the collected value - - dateFormat: The date format applied to each commit while generating the collected value - - ancestryPath: Whether or not to use ancestry-path param - - tagMatchPattern: A glob(7) pattern to match against when finding the last git tag - - matchLightweightTag: Whether or not to match a lightweight tag when searching for the last one - - quiet: Whether or not to disable changelog output - - includeMerges: **DEPRECATED!** Use `:merge_commit_filtering` instead - Whether or not to include any commits that are merges - - mergeCommitFiltering: Controls inclusion of merge commits when collecting the changelog. Valid values: 'include_merges', 'exclude_merges', 'only_include_merges' - - appPath: Scopes the changelog to a specific subdirectory of the repository - - - returns: Returns a String containing your formatted git commits - - By default, messages will be collected back to the last tag, but the range can be controlled - */ -@discardableResult public func changelogFromGitCommits(between: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - commitsCount: OptionalConfigValue = .fastlaneDefault(nil), - path: String = "./", - pretty: String = "%B", - dateFormat: OptionalConfigValue = .fastlaneDefault(nil), - ancestryPath: OptionalConfigValue = .fastlaneDefault(false), - tagMatchPattern: OptionalConfigValue = .fastlaneDefault(nil), - matchLightweightTag: OptionalConfigValue = .fastlaneDefault(true), - quiet: OptionalConfigValue = .fastlaneDefault(false), - includeMerges: OptionalConfigValue = .fastlaneDefault(nil), - mergeCommitFiltering: String = "include_merges", - appPath: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let betweenArg = between.asRubyArgument(name: "between", type: nil) - let commitsCountArg = commitsCount.asRubyArgument(name: "commits_count", type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let prettyArg = RubyCommand.Argument(name: "pretty", value: pretty, type: nil) - let dateFormatArg = dateFormat.asRubyArgument(name: "date_format", type: nil) - let ancestryPathArg = ancestryPath.asRubyArgument(name: "ancestry_path", type: nil) - let tagMatchPatternArg = tagMatchPattern.asRubyArgument(name: "tag_match_pattern", type: nil) - let matchLightweightTagArg = matchLightweightTag.asRubyArgument(name: "match_lightweight_tag", type: nil) - let quietArg = quiet.asRubyArgument(name: "quiet", type: nil) - let includeMergesArg = includeMerges.asRubyArgument(name: "include_merges", type: nil) - let mergeCommitFilteringArg = RubyCommand.Argument(name: "merge_commit_filtering", value: mergeCommitFiltering, type: nil) - let appPathArg = appPath.asRubyArgument(name: "app_path", type: nil) - let array: [RubyCommand.Argument?] = [betweenArg, - commitsCountArg, - pathArg, - prettyArg, - dateFormatArg, - ancestryPathArg, - tagMatchPatternArg, - matchLightweightTagArg, - quietArg, - includeMergesArg, - mergeCommitFilteringArg, - appPathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "changelog_from_git_commits", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Send a success/error message to [ChatWork](https://go.chatwork.com/) - - - parameters: - - apiToken: ChatWork API Token - - message: The message to post on ChatWork - - roomid: The room ID - - success: Was this build successful? (true/false) - - Information on how to obtain an API token: [http://developer.chatwork.com/ja/authenticate.html](http://developer.chatwork.com/ja/authenticate.html) - */ -public func chatwork(apiToken: String, - message: String, - roomid: Int, - success: OptionalConfigValue = .fastlaneDefault(true)) -{ - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let roomidArg = RubyCommand.Argument(name: "roomid", value: roomid, type: nil) - let successArg = success.asRubyArgument(name: "success", type: nil) - let array: [RubyCommand.Argument?] = [apiTokenArg, - messageArg, - roomidArg, - successArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "chatwork", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Check your app's metadata before you submit your app to review (via _precheck_) - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - appIdentifier: The bundle identifier of your app - - username: Your Apple ID Username - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - platform: The platform to use (optional) - - defaultRuleLevel: The default rule level unless otherwise configured - - includeInAppPurchases: Should check in-app purchases? - - useLive: Should force check live app? - - negativeAppleSentiment: mentioning  in a way that could be considered negative - - placeholderText: using placeholder text (e.g.:"lorem ipsum", "text here", etc...) - - otherPlatforms: mentioning other platforms, like Android or Blackberry - - futureFunctionality: mentioning features or content that is not currently available in your app - - testWords: using text indicating this release is a test - - curseWords: including words that might be considered objectionable - - freeStuffInIap: using text indicating that your IAP is free - - customText: mentioning any of the user-specified words passed to custom_text(data: [words]) - - copyrightDate: using a copyright date that is any different from this current year, or missing a date - - unreachableUrls: unreachable URLs in app metadata - - - returns: true if precheck passes, else, false - - More information: https://fastlane.tools/precheck - */ -@discardableResult public func checkAppStoreMetadata(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appIdentifier: String, - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - defaultRuleLevel: String = "error", - includeInAppPurchases: OptionalConfigValue = .fastlaneDefault(true), - useLive: OptionalConfigValue = .fastlaneDefault(false), - negativeAppleSentiment: Any? = nil, - placeholderText: Any? = nil, - otherPlatforms: Any? = nil, - futureFunctionality: Any? = nil, - testWords: Any? = nil, - curseWords: Any? = nil, - freeStuffInIap: Any? = nil, - customText: Any? = nil, - copyrightDate: Any? = nil, - unreachableUrls: Any? = nil) -> Bool -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let defaultRuleLevelArg = RubyCommand.Argument(name: "default_rule_level", value: defaultRuleLevel, type: nil) - let includeInAppPurchasesArg = includeInAppPurchases.asRubyArgument(name: "include_in_app_purchases", type: nil) - let useLiveArg = useLive.asRubyArgument(name: "use_live", type: nil) - let negativeAppleSentimentArg = RubyCommand.Argument(name: "negative_apple_sentiment", value: negativeAppleSentiment, type: nil) - let placeholderTextArg = RubyCommand.Argument(name: "placeholder_text", value: placeholderText, type: nil) - let otherPlatformsArg = RubyCommand.Argument(name: "other_platforms", value: otherPlatforms, type: nil) - let futureFunctionalityArg = RubyCommand.Argument(name: "future_functionality", value: futureFunctionality, type: nil) - let testWordsArg = RubyCommand.Argument(name: "test_words", value: testWords, type: nil) - let curseWordsArg = RubyCommand.Argument(name: "curse_words", value: curseWords, type: nil) - let freeStuffInIapArg = RubyCommand.Argument(name: "free_stuff_in_iap", value: freeStuffInIap, type: nil) - let customTextArg = RubyCommand.Argument(name: "custom_text", value: customText, type: nil) - let copyrightDateArg = RubyCommand.Argument(name: "copyright_date", value: copyrightDate, type: nil) - let unreachableUrlsArg = RubyCommand.Argument(name: "unreachable_urls", value: unreachableUrls, type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - appIdentifierArg, - usernameArg, - teamIdArg, - teamNameArg, - platformArg, - defaultRuleLevelArg, - includeInAppPurchasesArg, - useLiveArg, - negativeAppleSentimentArg, - placeholderTextArg, - otherPlatformsArg, - futureFunctionalityArg, - testWordsArg, - curseWordsArg, - freeStuffInIapArg, - customTextArg, - copyrightDateArg, - unreachableUrlsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "check_app_store_metadata", className: nil, args: args) - return parseBool(fromString: runner.executeCommand(command)) -} - -/** - Deletes files created as result of running gym, cert, sigh or download_dsyms - - - parameter excludePattern: Exclude all files from clearing that match the given Regex pattern: e.g. '.*.mobileprovision' - - This action deletes the files that get created in your repo as a result of running the _gym_ and _sigh_ commands. It doesn't delete the `fastlane/report.xml` though, this is probably more suited for the .gitignore. - - Useful if you quickly want to send out a test build by dropping down to the command line and typing something like `fastlane beta`, without leaving your repo in a messy state afterwards. - */ -public func cleanBuildArtifacts(excludePattern: OptionalConfigValue = .fastlaneDefault(nil)) { - let excludePatternArg = excludePattern.asRubyArgument(name: "exclude_pattern", type: nil) - let array: [RubyCommand.Argument?] = [excludePatternArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "clean_build_artifacts", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Remove the cache for pods - - - parameters: - - name: Pod name to be removed from cache - - noAnsi: Show output without ANSI codes - - verbose: Show more debugging information - - silent: Show nothing - - allowRoot: Allows CocoaPods to run as root - */ -public func cleanCocoapodsCache(name: OptionalConfigValue = .fastlaneDefault(nil), - noAnsi: OptionalConfigValue = .fastlaneDefault(false), - verbose: OptionalConfigValue = .fastlaneDefault(false), - silent: OptionalConfigValue = .fastlaneDefault(false), - allowRoot: OptionalConfigValue = .fastlaneDefault(false)) -{ - let nameArg = name.asRubyArgument(name: "name", type: nil) - let noAnsiArg = noAnsi.asRubyArgument(name: "no_ansi", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let silentArg = silent.asRubyArgument(name: "silent", type: nil) - let allowRootArg = allowRoot.asRubyArgument(name: "allow_root", type: nil) - let array: [RubyCommand.Argument?] = [nameArg, - noAnsiArg, - verboseArg, - silentArg, - allowRootArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "clean_cocoapods_cache", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Deletes the Xcode Derived Data - - - parameter derivedDataPath: Custom path for derivedData - - Deletes the Derived Data from path set on Xcode or a supplied path - */ -public func clearDerivedData(derivedDataPath: String = "~/Library/Developer/Xcode/DerivedData") { - let derivedDataPathArg = RubyCommand.Argument(name: "derived_data_path", value: derivedDataPath, type: nil) - let array: [RubyCommand.Argument?] = [derivedDataPathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "clear_derived_data", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Copies a given string into the clipboard. Works only on macOS - - - parameter value: The string that should be copied into the clipboard - */ -public func clipboard(value: String) { - let valueArg = RubyCommand.Argument(name: "value", value: value, type: nil) - let array: [RubyCommand.Argument?] = [valueArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "clipboard", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generates a Code Count that can be read by Jenkins (xml format) - - - parameters: - - binaryPath: Where the cloc binary lives on your system (full path including 'cloc') - - excludeDir: Comma separated list of directories to exclude - - outputDirectory: Where to put the generated report file - - sourceDirectory: Where to look for the source code (relative to the project root folder) - - xml: Should we generate an XML File (if false, it will generate a plain text file)? - - This action will run cloc to generate a SLOC report that the Jenkins SLOCCount plugin can read. - See [https://wiki.jenkins-ci.org/display/JENKINS/SLOCCount+Plugin](https://wiki.jenkins-ci.org/display/JENKINS/SLOCCount+Plugin) and [https://github.com/AlDanial/cloc](https://github.com/AlDanial/cloc) for more information. - */ -public func cloc(binaryPath: String = "/usr/local/bin/cloc", - excludeDir: OptionalConfigValue = .fastlaneDefault(nil), - outputDirectory: String = "build", - sourceDirectory: String = "", - xml: OptionalConfigValue = .fastlaneDefault(true)) -{ - let binaryPathArg = RubyCommand.Argument(name: "binary_path", value: binaryPath, type: nil) - let excludeDirArg = excludeDir.asRubyArgument(name: "exclude_dir", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let sourceDirectoryArg = RubyCommand.Argument(name: "source_directory", value: sourceDirectory, type: nil) - let xmlArg = xml.asRubyArgument(name: "xml", type: nil) - let array: [RubyCommand.Argument?] = [binaryPathArg, - excludeDirArg, - outputDirectoryArg, - sourceDirectoryArg, - xmlArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "cloc", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs `pod install` for the project - - - parameters: - - repoUpdate: Add `--repo-update` flag to `pod install` command - - cleanInstall: Execute a full pod installation ignoring the content of the project cache - - silent: Execute command without logging output - - verbose: Show more debugging information - - ansi: Show output with ANSI codes - - useBundleExec: Use bundle exec when there is a Gemfile presented - - podfile: Explicitly specify the path to the Cocoapods' Podfile. You can either set it to the Podfile's path or to the folder containing the Podfile file - - errorCallback: A callback invoked with the command output if there is a non-zero exit status - - tryRepoUpdateOnError: Retry with --repo-update if action was finished with error - - deployment: Disallow any changes to the Podfile or the Podfile.lock during installation - - allowRoot: Allows CocoaPods to run as root - - clean: **DEPRECATED!** (Option renamed as clean_install) Remove SCM directories - - integrate: **DEPRECATED!** (Option removed from cocoapods) Integrate the Pods libraries into the Xcode project(s) - - If you use [CocoaPods](http://cocoapods.org) you can use the `cocoapods` integration to run `pod install` before building your app. - */ -public func cocoapods(repoUpdate: OptionalConfigValue = .fastlaneDefault(false), - cleanInstall: OptionalConfigValue = .fastlaneDefault(false), - silent: OptionalConfigValue = .fastlaneDefault(false), - verbose: OptionalConfigValue = .fastlaneDefault(false), - ansi: OptionalConfigValue = .fastlaneDefault(true), - useBundleExec: OptionalConfigValue = .fastlaneDefault(true), - podfile: OptionalConfigValue = .fastlaneDefault(nil), - errorCallback: ((String) -> Void)? = nil, - tryRepoUpdateOnError: OptionalConfigValue = .fastlaneDefault(false), - deployment: OptionalConfigValue = .fastlaneDefault(false), - allowRoot: OptionalConfigValue = .fastlaneDefault(false), - clean: OptionalConfigValue = .fastlaneDefault(true), - integrate: OptionalConfigValue = .fastlaneDefault(true)) -{ - let repoUpdateArg = repoUpdate.asRubyArgument(name: "repo_update", type: nil) - let cleanInstallArg = cleanInstall.asRubyArgument(name: "clean_install", type: nil) - let silentArg = silent.asRubyArgument(name: "silent", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let ansiArg = ansi.asRubyArgument(name: "ansi", type: nil) - let useBundleExecArg = useBundleExec.asRubyArgument(name: "use_bundle_exec", type: nil) - let podfileArg = podfile.asRubyArgument(name: "podfile", type: nil) - let errorCallbackArg = RubyCommand.Argument(name: "error_callback", value: errorCallback, type: .stringClosure) - let tryRepoUpdateOnErrorArg = tryRepoUpdateOnError.asRubyArgument(name: "try_repo_update_on_error", type: nil) - let deploymentArg = deployment.asRubyArgument(name: "deployment", type: nil) - let allowRootArg = allowRoot.asRubyArgument(name: "allow_root", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let integrateArg = integrate.asRubyArgument(name: "integrate", type: nil) - let array: [RubyCommand.Argument?] = [repoUpdateArg, - cleanInstallArg, - silentArg, - verboseArg, - ansiArg, - useBundleExecArg, - podfileArg, - errorCallbackArg, - tryRepoUpdateOnErrorArg, - deploymentArg, - allowRootArg, - cleanArg, - integrateArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "cocoapods", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will commit a file directly on GitHub via the API - - - parameters: - - repositoryName: The path to your repo, e.g. 'fastlane/fastlane' - - serverUrl: The server url. e.g. 'https://your.internal.github.host/api/v3' (Default: 'https://api.github.com') - - apiToken: Personal API Token for GitHub - generate one at https://github.com/settings/tokens - - apiBearer: Use a Bearer authorization token. Usually generated by GitHub Apps, e.g. GitHub Actions GITHUB_TOKEN environment variable - - branch: The branch that the file should be committed on (default: master) - - path: The relative path to your file from project root e.g. assets/my_app.xcarchive - - message: The commit message. Defaults to the file name - - secure: Optionally disable secure requests (ssl_verify_peer) - - - returns: A hash containing all relevant information for this commit - Access things like 'html_url', 'sha', 'message' - - Commits a file directly to GitHub. You must provide your GitHub Personal token (get one from [https://github.com/settings/tokens/new](https://github.com/settings/tokens/new)), the repository name and the relative file path from the root git project. - Out parameters provide the commit sha created, which can be used for later usage for examples such as releases, the direct download link and the full response JSON. - Documentation: [https://developer.github.com/v3/repos/contents/#create-a-file](https://developer.github.com/v3/repos/contents/#create-a-file). - */ -@discardableResult public func commitGithubFile(repositoryName: String, - serverUrl: String = "https://api.github.com", - apiToken: OptionalConfigValue = .fastlaneDefault(nil), - apiBearer: OptionalConfigValue = .fastlaneDefault(nil), - branch: String = "master", - path: String, - message: OptionalConfigValue = .fastlaneDefault(nil), - secure: OptionalConfigValue = .fastlaneDefault(true)) -> [String: String] -{ - let repositoryNameArg = RubyCommand.Argument(name: "repository_name", value: repositoryName, type: nil) - let serverUrlArg = RubyCommand.Argument(name: "server_url", value: serverUrl, type: nil) - let apiTokenArg = apiToken.asRubyArgument(name: "api_token", type: nil) - let apiBearerArg = apiBearer.asRubyArgument(name: "api_bearer", type: nil) - let branchArg = RubyCommand.Argument(name: "branch", value: branch, type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let messageArg = message.asRubyArgument(name: "message", type: nil) - let secureArg = secure.asRubyArgument(name: "secure", type: nil) - let array: [RubyCommand.Argument?] = [repositoryNameArg, - serverUrlArg, - apiTokenArg, - apiBearerArg, - branchArg, - pathArg, - messageArg, - secureArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "commit_github_file", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Creates a 'Version Bump' commit. Run after `increment_build_number` - - - parameters: - - message: The commit message when committing the version bump - - xcodeproj: The path to your project file (Not the workspace). If you have only one, this is optional - - force: Forces the commit, even if other files than the ones containing the version number have been modified - - settings: Include Settings.bundle/Root.plist with version bump - - ignore: A regular expression used to filter matched plist files to be modified - - include: A list of extra files to be included in the version bump (string array or comma-separated string) - - noVerify: Whether or not to use --no-verify - - This action will create a 'Version Bump' commit in your repo. Useful in conjunction with `increment_build_number`. - It checks the repo to make sure that only the relevant files have changed. These are the files that `increment_build_number` (`agvtool`) touches:| - | - >- All `.plist` files| - - The `.xcodeproj/project.pbxproj` file| - >| - Then commits those files to the repo. - Customize the message with the `:message` option. It defaults to 'Version Bump'. - If you have other uncommitted changes in your repo, this action will fail. If you started off in a clean repo, and used the _ipa_ and or _sigh_ actions, then you can use the [clean_build_artifacts](https://docs.fastlane.tools/actions/clean_build_artifacts/) action to clean those temporary files up before running this action. - */ -public func commitVersionBump(message: OptionalConfigValue = .fastlaneDefault(nil), - xcodeproj: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - settings: OptionalConfigValue = .fastlaneDefault(false), - ignore: OptionalConfigValue = .fastlaneDefault(nil), - include: [String] = [], - noVerify: OptionalConfigValue = .fastlaneDefault(false)) -{ - let messageArg = message.asRubyArgument(name: "message", type: nil) - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let settingsArg = settings.asRubyArgument(name: "settings", type: nil) - let ignoreArg = ignore.asRubyArgument(name: "ignore", type: nil) - let includeArg = RubyCommand.Argument(name: "include", value: include, type: nil) - let noVerifyArg = noVerify.asRubyArgument(name: "no_verify", type: nil) - let array: [RubyCommand.Argument?] = [messageArg, - xcodeprojArg, - forceArg, - settingsArg, - ignoreArg, - includeArg, - noVerifyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "commit_version_bump", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Copy and save your build artifacts (useful when you use reset_git_repo) - - - parameters: - - keepOriginal: Set this to false if you want move, rather than copy, the found artifacts - - targetPath: The directory in which you want your artifacts placed - - artifacts: An array of file patterns of the files/folders you want to preserve - - failOnMissing: Fail when a source file isn't found - - This action copies artifacts to a target directory. It's useful if you have a CI that will pick up these artifacts and attach them to the build. Useful e.g. for storing your `.ipa`s, `.dSYM.zip`s, `.mobileprovision`s, `.cert`s. - Make sure your `:target_path` is ignored from git, and if you use `reset_git_repo`, make sure the artifacts are added to the exclude list. - */ -public func copyArtifacts(keepOriginal: OptionalConfigValue = .fastlaneDefault(true), - targetPath: String = "artifacts", - artifacts: [String] = [], - failOnMissing: OptionalConfigValue = .fastlaneDefault(false)) -{ - let keepOriginalArg = keepOriginal.asRubyArgument(name: "keep_original", type: nil) - let targetPathArg = RubyCommand.Argument(name: "target_path", value: targetPath, type: nil) - let artifactsArg = RubyCommand.Argument(name: "artifacts", value: artifacts, type: nil) - let failOnMissingArg = failOnMissing.asRubyArgument(name: "fail_on_missing", type: nil) - let array: [RubyCommand.Argument?] = [keepOriginalArg, - targetPathArg, - artifactsArg, - failOnMissingArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "copy_artifacts", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Create Managed Google Play Apps - - - parameters: - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - developerAccountId: The ID of your Google Play Console account. Can be obtained from the URL when you log in (`https://play.google.com/apps/publish/?account=...` or when you 'Obtain private app publishing rights' (https://developers.google.com/android/work/play/custom-app-api/get-started#retrieve_the_developer_account_id) - - apk: Path to the APK file to upload - - appTitle: App Title - - language: Default app language (e.g. 'en_US') - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - timeout: Timeout for read, open, and send (in seconds) - - Create new apps on Managed Google Play. - */ -public func createAppOnManagedPlayStore(jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - developerAccountId: String, - apk: String, - appTitle: String, - language: String = "en_US", - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 300) -{ - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let developerAccountIdArg = RubyCommand.Argument(name: "developer_account_id", value: developerAccountId, type: nil) - let apkArg = RubyCommand.Argument(name: "apk", value: apk, type: nil) - let appTitleArg = RubyCommand.Argument(name: "app_title", value: appTitle, type: nil) - let languageArg = RubyCommand.Argument(name: "language", value: language, type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let array: [RubyCommand.Argument?] = [jsonKeyArg, - jsonKeyDataArg, - developerAccountIdArg, - apkArg, - appTitleArg, - languageArg, - rootUrlArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "create_app_on_managed_play_store", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Creates the given application on iTC and the Dev Portal (via _produce_) - - - parameters: - - username: Your Apple ID Username - - appIdentifier: App Identifier (Bundle ID, e.g. com.krausefx.app) - - bundleIdentifierSuffix: App Identifier Suffix (Ignored if App Identifier does not end with .*) - - appName: App Name - - appVersion: Initial version number (e.g. '1.0') - - sku: SKU Number (e.g. '1234') - - platform: The platform to use (optional) - - platforms: The platforms to use (optional) - - language: Primary Language (e.g. 'en-US', 'fr-FR') - - companyName: The name of your company. It's used to set company name on App Store Connect team's app pages. Only required if it's the first app you create - - skipItc: Skip the creation of the app on App Store Connect - - itcUsers: Array of App Store Connect users. If provided, you can limit access to this newly created app for users with the App Manager, Developer, Marketer or Sales roles - - enabledFeatures: **DEPRECATED!** Please use `enable_services` instead - Array with Spaceship App Services - - enableServices: Array with Spaceship App Services (e.g. access_wifi: (on|off), app_attest: (on|off), app_group: (on|off), apple_pay: (on|off), associated_domains: (on|off), auto_fill_credential: (on|off), class_kit: (on|off), declared_age_range: (on|off), icloud: (legacy|cloudkit), custom_network_protocol: (on|off), data_protection: (complete|unlessopen|untilfirstauth), extended_virtual_address_space: (on|off), family_controls: (on|off), file_provider_testing_mode: (on|off), fonts: (on|off), game_center: (ios|mac), health_kit: (on|off), hls_interstitial_preview: (on|off), home_kit: (on|off), hotspot: (on|off), in_app_purchase: (on|off), inter_app_audio: (on|off), low_latency_hls: (on|off), managed_associated_domains: (on|off), maps: (on|off), multipath: (on|off), network_extension: (on|off), nfc_tag_reading: (on|off), personal_vpn: (on|off), passbook: (on|off), push_notification: (on|off), sign_in_with_apple: (on), siri_kit: (on|off), system_extension: (on|off), user_management: (on|off), vpn_configuration: (on|off), wallet: (on|off), wireless_accessory: (on|off), car_play_audio_app: (on|off), car_play_messaging_app: (on|off), car_play_navigation_app: (on|off), car_play_voip_calling_app: (on|off), critical_alerts: (on|off), hotspot_helper: (on|off), driver_kit: (on|off), driver_kit_endpoint_security: (on|off), driver_kit_family_hid_device: (on|off), driver_kit_family_networking: (on|off), driver_kit_family_serial: (on|off), driver_kit_hid_event_service: (on|off), driver_kit_transport_hid: (on|off), multitasking_camera_access: (on|off), sf_universal_link_api: (on|off), vp9_decoder: (on|off), music_kit: (on|off), shazam_kit: (on|off), communication_notifications: (on|off), group_activities: (on|off), health_kit_estimate_recalibration: (on|off), time_sensitive_notifications: (on|off)) - - skipDevcenter: Skip the creation of the app on the Apple Developer Portal - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - itcTeamId: The ID of your App Store Connect team if you're in multiple teams - - itcTeamName: The name of your App Store Connect team if you're in multiple teams - - Create new apps on App Store Connect and Apple Developer Portal via _produce_. - If the app already exists, `create_app_online` will not do anything. - For more information about _produce_, visit its documentation page: [https://docs.fastlane.tools/actions/produce/](https://docs.fastlane.tools/actions/produce/). - */ -public func createAppOnline(username: String, - appIdentifier: String, - bundleIdentifierSuffix: OptionalConfigValue = .fastlaneDefault(nil), - appName: String, - appVersion: OptionalConfigValue = .fastlaneDefault(nil), - sku: String, - platform: String = "ios", - platforms: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - language: String = "English", - companyName: OptionalConfigValue = .fastlaneDefault(nil), - skipItc: OptionalConfigValue = .fastlaneDefault(false), - itcUsers: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - enabledFeatures: [String: Any] = [:], - enableServices: [String: Any] = [:], - skipDevcenter: OptionalConfigValue = .fastlaneDefault(false), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - itcTeamId: Any? = nil, - itcTeamName: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let bundleIdentifierSuffixArg = bundleIdentifierSuffix.asRubyArgument(name: "bundle_identifier_suffix", type: nil) - let appNameArg = RubyCommand.Argument(name: "app_name", value: appName, type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let skuArg = RubyCommand.Argument(name: "sku", value: sku, type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let platformsArg = platforms.asRubyArgument(name: "platforms", type: nil) - let languageArg = RubyCommand.Argument(name: "language", value: language, type: nil) - let companyNameArg = companyName.asRubyArgument(name: "company_name", type: nil) - let skipItcArg = skipItc.asRubyArgument(name: "skip_itc", type: nil) - let itcUsersArg = itcUsers.asRubyArgument(name: "itc_users", type: nil) - let enabledFeaturesArg = RubyCommand.Argument(name: "enabled_features", value: enabledFeatures, type: nil) - let enableServicesArg = RubyCommand.Argument(name: "enable_services", value: enableServices, type: nil) - let skipDevcenterArg = skipDevcenter.asRubyArgument(name: "skip_devcenter", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let itcTeamIdArg = RubyCommand.Argument(name: "itc_team_id", value: itcTeamId, type: nil) - let itcTeamNameArg = itcTeamName.asRubyArgument(name: "itc_team_name", type: nil) - let array: [RubyCommand.Argument?] = [usernameArg, - appIdentifierArg, - bundleIdentifierSuffixArg, - appNameArg, - appVersionArg, - skuArg, - platformArg, - platformsArg, - languageArg, - companyNameArg, - skipItcArg, - itcUsersArg, - enabledFeaturesArg, - enableServicesArg, - skipDevcenterArg, - teamIdArg, - teamNameArg, - itcTeamIdArg, - itcTeamNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "create_app_online", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Create a new Keychain - - - parameters: - - name: Keychain name - - path: Path to keychain - - password: Password for the keychain - - defaultKeychain: Should the newly created Keychain be the new system default keychain - - unlock: Unlock keychain after create - - timeout: timeout interval in seconds. Set `0` if you want to specify "no time-out" - - lockWhenSleeps: Lock keychain when the system sleeps - - lockAfterTimeout: Lock keychain after timeout interval - - addToSearchList: Add keychain to search list - - requireCreate: Fail the action if the Keychain already exists - */ -public func createKeychain(name: OptionalConfigValue = .fastlaneDefault(nil), - path: OptionalConfigValue = .fastlaneDefault(nil), - password: String, - defaultKeychain: OptionalConfigValue = .fastlaneDefault(false), - unlock: OptionalConfigValue = .fastlaneDefault(false), - timeout: Int = 300, - lockWhenSleeps: OptionalConfigValue = .fastlaneDefault(false), - lockAfterTimeout: OptionalConfigValue = .fastlaneDefault(false), - addToSearchList: OptionalConfigValue = .fastlaneDefault(true), - requireCreate: OptionalConfigValue = .fastlaneDefault(false)) -{ - let nameArg = name.asRubyArgument(name: "name", type: nil) - let pathArg = path.asRubyArgument(name: "path", type: nil) - let passwordArg = RubyCommand.Argument(name: "password", value: password, type: nil) - let defaultKeychainArg = defaultKeychain.asRubyArgument(name: "default_keychain", type: nil) - let unlockArg = unlock.asRubyArgument(name: "unlock", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let lockWhenSleepsArg = lockWhenSleeps.asRubyArgument(name: "lock_when_sleeps", type: nil) - let lockAfterTimeoutArg = lockAfterTimeout.asRubyArgument(name: "lock_after_timeout", type: nil) - let addToSearchListArg = addToSearchList.asRubyArgument(name: "add_to_search_list", type: nil) - let requireCreateArg = requireCreate.asRubyArgument(name: "require_create", type: nil) - let array: [RubyCommand.Argument?] = [nameArg, - pathArg, - passwordArg, - defaultKeychainArg, - unlockArg, - timeoutArg, - lockWhenSleepsArg, - lockAfterTimeoutArg, - addToSearchListArg, - requireCreateArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "create_keychain", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will create a new pull request on GitHub - - - parameters: - - apiToken: Personal API Token for GitHub - generate one at https://github.com/settings/tokens - - apiBearer: Use a Bearer authorization token. Usually generated by GitHub Apps, e.g. GitHub Actions GITHUB_TOKEN environment variable - - repo: The name of the repository you want to submit the pull request to - - title: The title of the pull request - - body: The contents of the pull request - - draft: Indicates whether the pull request is a draft - - labels: The labels for the pull request - - milestone: The milestone ID (Integer) for the pull request - - head: The name of the branch where your changes are implemented (defaults to the current branch name) - - base: The name of the branch you want your changes pulled into (defaults to `master`) - - apiUrl: The URL of GitHub API - used when the Enterprise (default to `https://api.github.com`) - - assignees: The assignees for the pull request - - reviewers: The reviewers (slug) for the pull request - - teamReviewers: The team reviewers (slug) for the pull request - - - returns: The pull request URL when successful - */ -public func createPullRequest(apiToken: OptionalConfigValue = .fastlaneDefault(nil), - apiBearer: OptionalConfigValue = .fastlaneDefault(nil), - repo: String, - title: String, - body: OptionalConfigValue = .fastlaneDefault(nil), - draft: OptionalConfigValue = .fastlaneDefault(nil), - labels: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - milestone: OptionalConfigValue = .fastlaneDefault(nil), - head: String = "master", - base: String = "master", - apiUrl: String = "https://api.github.com", - assignees: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - reviewers: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - teamReviewers: OptionalConfigValue<[String]?> = .fastlaneDefault(nil)) -{ - let apiTokenArg = apiToken.asRubyArgument(name: "api_token", type: nil) - let apiBearerArg = apiBearer.asRubyArgument(name: "api_bearer", type: nil) - let repoArg = RubyCommand.Argument(name: "repo", value: repo, type: nil) - let titleArg = RubyCommand.Argument(name: "title", value: title, type: nil) - let bodyArg = body.asRubyArgument(name: "body", type: nil) - let draftArg = draft.asRubyArgument(name: "draft", type: nil) - let labelsArg = labels.asRubyArgument(name: "labels", type: nil) - let milestoneArg = milestone.asRubyArgument(name: "milestone", type: nil) - let headArg = RubyCommand.Argument(name: "head", value: head, type: nil) - let baseArg = RubyCommand.Argument(name: "base", value: base, type: nil) - let apiUrlArg = RubyCommand.Argument(name: "api_url", value: apiUrl, type: nil) - let assigneesArg = assignees.asRubyArgument(name: "assignees", type: nil) - let reviewersArg = reviewers.asRubyArgument(name: "reviewers", type: nil) - let teamReviewersArg = teamReviewers.asRubyArgument(name: "team_reviewers", type: nil) - let array: [RubyCommand.Argument?] = [apiTokenArg, - apiBearerArg, - repoArg, - titleArg, - bodyArg, - draftArg, - labelsArg, - milestoneArg, - headArg, - baseArg, - apiUrlArg, - assigneesArg, - reviewersArg, - teamReviewersArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "create_pull_request", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Package multiple build configs of a library/framework into a single xcframework - - - parameters: - - frameworks: Frameworks (without dSYMs) to add to the target xcframework - - frameworksWithDsyms: Frameworks (with dSYMs) to add to the target xcframework - - libraries: Libraries (without headers or dSYMs) to add to the target xcframework - - librariesWithHeadersOrDsyms: Libraries (with headers or dSYMs) to add to the target xcframework - - output: The path to write the xcframework to - - allowInternalDistribution: Specifies that the created xcframework contains information not suitable for public distribution - - Utility for packaging multiple build configurations of a given library - or framework into a single xcframework. - - If you want to package several frameworks just provide one of: - - * An array containing the list of frameworks using the :frameworks parameter - (if they have no associated dSYMs): - ['FrameworkA.framework', 'FrameworkB.framework'] - - * A hash containing the list of frameworks with their dSYMs using the - :frameworks_with_dsyms parameter: - { - 'FrameworkA.framework' => {}, - 'FrameworkB.framework' => { dsyms: 'FrameworkB.framework.dSYM' } - } - - If you want to package several libraries just provide one of: - - * An array containing the list of libraries using the :libraries parameter - (if they have no associated headers or dSYMs): - ['LibraryA.so', 'LibraryB.so'] - - * A hash containing the list of libraries with their headers and dSYMs - using the :libraries_with_headers_or_dsyms parameter: - { - 'LibraryA.so' => { dsyms: 'libraryA.so.dSYM' }, - 'LibraryB.so' => { headers: 'headers' } - } - - Finally specify the location of the xcframework to be generated using the :output - parameter. - - */ -public func createXcframework(frameworks: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - frameworksWithDsyms: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - libraries: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - librariesWithHeadersOrDsyms: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - output: String, - allowInternalDistribution: OptionalConfigValue = .fastlaneDefault(false)) -{ - let frameworksArg = frameworks.asRubyArgument(name: "frameworks", type: nil) - let frameworksWithDsymsArg = frameworksWithDsyms.asRubyArgument(name: "frameworks_with_dsyms", type: nil) - let librariesArg = libraries.asRubyArgument(name: "libraries", type: nil) - let librariesWithHeadersOrDsymsArg = librariesWithHeadersOrDsyms.asRubyArgument(name: "libraries_with_headers_or_dsyms", type: nil) - let outputArg = RubyCommand.Argument(name: "output", value: output, type: nil) - let allowInternalDistributionArg = allowInternalDistribution.asRubyArgument(name: "allow_internal_distribution", type: nil) - let array: [RubyCommand.Argument?] = [frameworksArg, - frameworksWithDsymsArg, - librariesArg, - librariesWithHeadersOrDsymsArg, - outputArg, - allowInternalDistributionArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "create_xcframework", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs `danger` for the project - - - parameters: - - useBundleExec: Use bundle exec when there is a Gemfile presented - - verbose: Show more debugging information - - dangerId: The identifier of this Danger instance - - dangerfile: The location of your Dangerfile - - githubApiToken: GitHub API token for danger - - githubEnterpriseHost: GitHub host URL for GitHub Enterprise - - githubEnterpriseApiBaseUrl: GitHub API base URL for GitHub Enterprise - - failOnErrors: Should always fail the build process, defaults to false - - newComment: Makes Danger post a new comment instead of editing its previous one - - removePreviousComments: Makes Danger remove all previous comment and create a new one in the end of the list - - base: A branch/tag/commit to use as the base of the diff. [master|dev|stable] - - head: A branch/tag/commit to use as the head. [master|dev|stable] - - pr: Run danger on a specific pull request. e.g. "https://github.com/danger/danger/pull/518" - - failIfNoPr: Fail Danger execution if no PR is found - - Formalize your Pull Request etiquette. - More information: [https://github.com/danger/danger](https://github.com/danger/danger). - */ -public func danger(useBundleExec: OptionalConfigValue = .fastlaneDefault(true), - verbose: OptionalConfigValue = .fastlaneDefault(false), - dangerId: OptionalConfigValue = .fastlaneDefault(nil), - dangerfile: OptionalConfigValue = .fastlaneDefault(nil), - githubApiToken: OptionalConfigValue = .fastlaneDefault(nil), - githubEnterpriseHost: OptionalConfigValue = .fastlaneDefault(nil), - githubEnterpriseApiBaseUrl: OptionalConfigValue = .fastlaneDefault(nil), - failOnErrors: OptionalConfigValue = .fastlaneDefault(false), - newComment: OptionalConfigValue = .fastlaneDefault(false), - removePreviousComments: OptionalConfigValue = .fastlaneDefault(false), - base: OptionalConfigValue = .fastlaneDefault(nil), - head: OptionalConfigValue = .fastlaneDefault(nil), - pr: OptionalConfigValue = .fastlaneDefault(nil), - failIfNoPr: OptionalConfigValue = .fastlaneDefault(false)) -{ - let useBundleExecArg = useBundleExec.asRubyArgument(name: "use_bundle_exec", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let dangerIdArg = dangerId.asRubyArgument(name: "danger_id", type: nil) - let dangerfileArg = dangerfile.asRubyArgument(name: "dangerfile", type: nil) - let githubApiTokenArg = githubApiToken.asRubyArgument(name: "github_api_token", type: nil) - let githubEnterpriseHostArg = githubEnterpriseHost.asRubyArgument(name: "github_enterprise_host", type: nil) - let githubEnterpriseApiBaseUrlArg = githubEnterpriseApiBaseUrl.asRubyArgument(name: "github_enterprise_api_base_url", type: nil) - let failOnErrorsArg = failOnErrors.asRubyArgument(name: "fail_on_errors", type: nil) - let newCommentArg = newComment.asRubyArgument(name: "new_comment", type: nil) - let removePreviousCommentsArg = removePreviousComments.asRubyArgument(name: "remove_previous_comments", type: nil) - let baseArg = base.asRubyArgument(name: "base", type: nil) - let headArg = head.asRubyArgument(name: "head", type: nil) - let prArg = pr.asRubyArgument(name: "pr", type: nil) - let failIfNoPrArg = failIfNoPr.asRubyArgument(name: "fail_if_no_pr", type: nil) - let array: [RubyCommand.Argument?] = [useBundleExecArg, - verboseArg, - dangerIdArg, - dangerfileArg, - githubApiTokenArg, - githubEnterpriseHostArg, - githubEnterpriseApiBaseUrlArg, - failOnErrorsArg, - newCommentArg, - removePreviousCommentsArg, - baseArg, - headArg, - prArg, - failIfNoPrArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "danger", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Print out an overview of the lane context values - */ -public func debug() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "debug", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Defines a default platform to not have to specify the platform - */ -public func defaultPlatform() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "default_platform", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Delete keychains and remove them from the search list - - - parameters: - - name: Keychain name - - keychainPath: Keychain path - - Keychains can be deleted after being created with `create_keychain` - */ -public func deleteKeychain(name: OptionalConfigValue = .fastlaneDefault(nil), - keychainPath: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let nameArg = name.asRubyArgument(name: "name", type: nil) - let keychainPathArg = keychainPath.asRubyArgument(name: "keychain_path", type: nil) - let array: [RubyCommand.Argument?] = [nameArg, - keychainPathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "delete_keychain", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `upload_to_app_store` action - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - appIdentifier: The bundle identifier of your app - - appVersion: The version that should be edited or created - - ipa: Path to your ipa file - - pkg: Path to your pkg file - - buildNumber: If set the given build number (already uploaded to iTC) will be used instead of the current built one - - platform: The platform to use (optional) - - editLive: Modify live metadata, this option disables ipa upload and screenshot upload - - useLiveVersion: Force usage of live version rather than edit version - - metadataPath: Path to the folder containing the metadata files - - screenshotsPath: Path to the folder containing the screenshots - - appPreviewsPath: Path to the folder containing localized App Preview videos - - previewFrameTimeCode: Time code for the App Preview still frame written as hour:minute:second:centisecond (e.g. 00:00:00:01) - - overwritePreviewVideos: Clear all previously uploaded App Preview videos before uploading the new ones - - skipBinaryUpload: Skip uploading an ipa or pkg to App Store Connect - - skipScreenshots: Don't upload the screenshots - - skipMetadata: Don't upload the metadata (e.g. title, description). This will still upload screenshots - - skipAppVersionUpdate: Don’t create or update the app version that is being prepared for submission - - force: Skip verification of HTML preview file - - overwriteScreenshots: Clear all previously uploaded screenshots before uploading the new ones - - screenshotProcessingTimeout: Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing - - syncScreenshots: Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well - - submitForReview: Submit the new version for Review after uploading everything - - verifyOnly: Verifies archive with App Store Connect without uploading - - rejectIfPossible: Rejects the previously submitted build if it's in a state where it's possible - - versionCheckWaitRetryLimit: After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow - - automaticRelease: Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) - - autoReleaseDate: Date in milliseconds for automatically releasing on pending approval (Cannot be used together with `automatic_release`) - - phasedRelease: Enable the phased release feature of iTC - - resetRatings: Reset the summary rating when you release a new version of the application - - priceTier: The price tier of this application - - appRatingConfigPath: Path to the app rating's config - - submissionInformation: Extra information for the submission (e.g. compliance specifications) - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - devPortalTeamId: The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! - - devPortalTeamName: The name of your Developer Portal team if you're in multiple teams - - itcProvider: The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column - - providerPublicId: The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication - - runPrecheckBeforeSubmit: Run precheck before submitting to app review - - precheckDefaultRuleLevel: The default precheck rule level unless otherwise configured - - individualMetadataItems: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - An array of localized metadata items to upload individually by language so that errors can be identified. E.g. ['name', 'keywords', 'description']. Note: slow - - appIcon: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the app icon - - appleWatchAppIcon: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the Apple Watch app icon - - copyright: Metadata: The copyright notice - - primaryCategory: Metadata: The english name of the primary category (e.g. `Business`, `Books`) - - secondaryCategory: Metadata: The english name of the secondary category (e.g. `Business`, `Books`) - - primaryFirstSubCategory: Metadata: The english name of the primary first sub category (e.g. `Educational`, `Puzzle`) - - primarySecondSubCategory: Metadata: The english name of the primary second sub category (e.g. `Educational`, `Puzzle`) - - secondaryFirstSubCategory: Metadata: The english name of the secondary first sub category (e.g. `Educational`, `Puzzle`) - - secondarySecondSubCategory: Metadata: The english name of the secondary second sub category (e.g. `Educational`, `Puzzle`) - - tradeRepresentativeContactInformation: **DEPRECATED!** This is no longer used by App Store Connect - Metadata: A hash containing the trade representative contact information - - appReviewInformation: Metadata: A hash containing the review information - - appReviewAttachmentFile: Metadata: Path to the app review attachment file - - description: Metadata: The localised app description - - name: Metadata: The localised app name - - subtitle: Metadata: The localised app subtitle - - keywords: Metadata: An array of localised keywords - - promotionalText: Metadata: An array of localised promotional texts - - releaseNotes: Metadata: Localised release notes for this version - - privacyUrl: Metadata: Localised privacy url - - appleTvPrivacyPolicy: Metadata: Localised Apple TV privacy policy text - - supportUrl: Metadata: Localised support url - - marketingUrl: Metadata: Localised marketing url - - languages: Metadata: List of languages to activate - - ignoreLanguageDirectoryValidation: Ignore errors when invalid languages are found in metadata and screenshot directories - - precheckIncludeInAppPurchases: Should precheck check in-app purchases? - - app: The (spaceship) app ID of the app you want to use/modify - - Using _upload_to_app_store_ after _build_app_ and _capture_screenshots_ will automatically upload the latest ipa and screenshots with no other configuration. - - If you don't want to verify an HTML preview for App Store builds, use the `:force` option. - This is useful when running _fastlane_ on your Continuous Integration server: - `_upload_to_app_store_(force: true)` - If your account is on multiple teams and you need to tell the transporter which provider to use, you can set `:itc_provider` or `:provider_public_id`. - */ -public func deliver(apiKeyPath: OptionalConfigValue = .fastlaneDefault(deliverfile.apiKeyPath), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.apiKey), - username: OptionalConfigValue = .fastlaneDefault(deliverfile.username), - appIdentifier: OptionalConfigValue = .fastlaneDefault(deliverfile.appIdentifier), - appVersion: OptionalConfigValue = .fastlaneDefault(deliverfile.appVersion), - ipa: OptionalConfigValue = .fastlaneDefault(deliverfile.ipa), - pkg: OptionalConfigValue = .fastlaneDefault(deliverfile.pkg), - buildNumber: OptionalConfigValue = .fastlaneDefault(deliverfile.buildNumber), - platform: String = deliverfile.platform, - editLive: OptionalConfigValue = .fastlaneDefault(deliverfile.editLive), - useLiveVersion: OptionalConfigValue = .fastlaneDefault(deliverfile.useLiveVersion), - metadataPath: OptionalConfigValue = .fastlaneDefault(deliverfile.metadataPath), - screenshotsPath: OptionalConfigValue = .fastlaneDefault(deliverfile.screenshotsPath), - appPreviewsPath: OptionalConfigValue = .fastlaneDefault(deliverfile.appPreviewsPath), - previewFrameTimeCode: String = deliverfile.previewFrameTimeCode, - overwritePreviewVideos: OptionalConfigValue = .fastlaneDefault(deliverfile.overwritePreviewVideos), - skipBinaryUpload: OptionalConfigValue = .fastlaneDefault(deliverfile.skipBinaryUpload), - skipScreenshots: OptionalConfigValue = .fastlaneDefault(deliverfile.skipScreenshots), - skipMetadata: OptionalConfigValue = .fastlaneDefault(deliverfile.skipMetadata), - skipAppVersionUpdate: OptionalConfigValue = .fastlaneDefault(deliverfile.skipAppVersionUpdate), - force: OptionalConfigValue = .fastlaneDefault(deliverfile.force), - overwriteScreenshots: OptionalConfigValue = .fastlaneDefault(deliverfile.overwriteScreenshots), - screenshotProcessingTimeout: Int = deliverfile.screenshotProcessingTimeout, - syncScreenshots: OptionalConfigValue = .fastlaneDefault(deliverfile.syncScreenshots), - submitForReview: OptionalConfigValue = .fastlaneDefault(deliverfile.submitForReview), - verifyOnly: OptionalConfigValue = .fastlaneDefault(deliverfile.verifyOnly), - rejectIfPossible: OptionalConfigValue = .fastlaneDefault(deliverfile.rejectIfPossible), - versionCheckWaitRetryLimit: Int = deliverfile.versionCheckWaitRetryLimit, - automaticRelease: OptionalConfigValue = .fastlaneDefault(deliverfile.automaticRelease), - autoReleaseDate: OptionalConfigValue = .fastlaneDefault(deliverfile.autoReleaseDate), - phasedRelease: OptionalConfigValue = .fastlaneDefault(deliverfile.phasedRelease), - resetRatings: OptionalConfigValue = .fastlaneDefault(deliverfile.resetRatings), - priceTier: OptionalConfigValue = .fastlaneDefault(deliverfile.priceTier), - appRatingConfigPath: OptionalConfigValue = .fastlaneDefault(deliverfile.appRatingConfigPath), - submissionInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.submissionInformation), - teamId: OptionalConfigValue = .fastlaneDefault(deliverfile.teamId), - teamName: OptionalConfigValue = .fastlaneDefault(deliverfile.teamName), - devPortalTeamId: OptionalConfigValue = .fastlaneDefault(deliverfile.devPortalTeamId), - devPortalTeamName: OptionalConfigValue = .fastlaneDefault(deliverfile.devPortalTeamName), - itcProvider: OptionalConfigValue = .fastlaneDefault(deliverfile.itcProvider), - providerPublicId: OptionalConfigValue = .fastlaneDefault(deliverfile.providerPublicId), - runPrecheckBeforeSubmit: OptionalConfigValue = .fastlaneDefault(deliverfile.runPrecheckBeforeSubmit), - precheckDefaultRuleLevel: Any = deliverfile.precheckDefaultRuleLevel, - individualMetadataItems: OptionalConfigValue<[String]?> = .fastlaneDefault(deliverfile.individualMetadataItems), - appIcon: OptionalConfigValue = .fastlaneDefault(deliverfile.appIcon), - appleWatchAppIcon: OptionalConfigValue = .fastlaneDefault(deliverfile.appleWatchAppIcon), - copyright: OptionalConfigValue = .fastlaneDefault(deliverfile.copyright), - primaryCategory: OptionalConfigValue = .fastlaneDefault(deliverfile.primaryCategory), - secondaryCategory: OptionalConfigValue = .fastlaneDefault(deliverfile.secondaryCategory), - primaryFirstSubCategory: OptionalConfigValue = .fastlaneDefault(deliverfile.primaryFirstSubCategory), - primarySecondSubCategory: OptionalConfigValue = .fastlaneDefault(deliverfile.primarySecondSubCategory), - secondaryFirstSubCategory: OptionalConfigValue = .fastlaneDefault(deliverfile.secondaryFirstSubCategory), - secondarySecondSubCategory: OptionalConfigValue = .fastlaneDefault(deliverfile.secondarySecondSubCategory), - tradeRepresentativeContactInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.tradeRepresentativeContactInformation), - appReviewInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.appReviewInformation), - appReviewAttachmentFile: OptionalConfigValue = .fastlaneDefault(deliverfile.appReviewAttachmentFile), - description: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.description), - name: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.name), - subtitle: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.subtitle), - keywords: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.keywords), - promotionalText: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.promotionalText), - releaseNotes: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.releaseNotes), - privacyUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.privacyUrl), - appleTvPrivacyPolicy: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.appleTvPrivacyPolicy), - supportUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.supportUrl), - marketingUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(deliverfile.marketingUrl), - languages: OptionalConfigValue<[String]?> = .fastlaneDefault(deliverfile.languages), - ignoreLanguageDirectoryValidation: OptionalConfigValue = .fastlaneDefault(deliverfile.ignoreLanguageDirectoryValidation), - precheckIncludeInAppPurchases: OptionalConfigValue = .fastlaneDefault(deliverfile.precheckIncludeInAppPurchases), - app: OptionalConfigValue = .fastlaneDefault(deliverfile.app)) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let pkgArg = pkg.asRubyArgument(name: "pkg", type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let editLiveArg = editLive.asRubyArgument(name: "edit_live", type: nil) - let useLiveVersionArg = useLiveVersion.asRubyArgument(name: "use_live_version", type: nil) - let metadataPathArg = metadataPath.asRubyArgument(name: "metadata_path", type: nil) - let screenshotsPathArg = screenshotsPath.asRubyArgument(name: "screenshots_path", type: nil) - let appPreviewsPathArg = appPreviewsPath.asRubyArgument(name: "app_previews_path", type: nil) - let previewFrameTimeCodeArg = RubyCommand.Argument(name: "preview_frame_time_code", value: previewFrameTimeCode, type: nil) - let overwritePreviewVideosArg = overwritePreviewVideos.asRubyArgument(name: "overwrite_preview_videos", type: nil) - let skipBinaryUploadArg = skipBinaryUpload.asRubyArgument(name: "skip_binary_upload", type: nil) - let skipScreenshotsArg = skipScreenshots.asRubyArgument(name: "skip_screenshots", type: nil) - let skipMetadataArg = skipMetadata.asRubyArgument(name: "skip_metadata", type: nil) - let skipAppVersionUpdateArg = skipAppVersionUpdate.asRubyArgument(name: "skip_app_version_update", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let overwriteScreenshotsArg = overwriteScreenshots.asRubyArgument(name: "overwrite_screenshots", type: nil) - let screenshotProcessingTimeoutArg = RubyCommand.Argument(name: "screenshot_processing_timeout", value: screenshotProcessingTimeout, type: nil) - let syncScreenshotsArg = syncScreenshots.asRubyArgument(name: "sync_screenshots", type: nil) - let submitForReviewArg = submitForReview.asRubyArgument(name: "submit_for_review", type: nil) - let verifyOnlyArg = verifyOnly.asRubyArgument(name: "verify_only", type: nil) - let rejectIfPossibleArg = rejectIfPossible.asRubyArgument(name: "reject_if_possible", type: nil) - let versionCheckWaitRetryLimitArg = RubyCommand.Argument(name: "version_check_wait_retry_limit", value: versionCheckWaitRetryLimit, type: nil) - let automaticReleaseArg = automaticRelease.asRubyArgument(name: "automatic_release", type: nil) - let autoReleaseDateArg = autoReleaseDate.asRubyArgument(name: "auto_release_date", type: nil) - let phasedReleaseArg = phasedRelease.asRubyArgument(name: "phased_release", type: nil) - let resetRatingsArg = resetRatings.asRubyArgument(name: "reset_ratings", type: nil) - let priceTierArg = priceTier.asRubyArgument(name: "price_tier", type: nil) - let appRatingConfigPathArg = appRatingConfigPath.asRubyArgument(name: "app_rating_config_path", type: nil) - let submissionInformationArg = submissionInformation.asRubyArgument(name: "submission_information", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let devPortalTeamIdArg = devPortalTeamId.asRubyArgument(name: "dev_portal_team_id", type: nil) - let devPortalTeamNameArg = devPortalTeamName.asRubyArgument(name: "dev_portal_team_name", type: nil) - let itcProviderArg = itcProvider.asRubyArgument(name: "itc_provider", type: nil) - let providerPublicIdArg = providerPublicId.asRubyArgument(name: "provider_public_id", type: nil) - let runPrecheckBeforeSubmitArg = runPrecheckBeforeSubmit.asRubyArgument(name: "run_precheck_before_submit", type: nil) - let precheckDefaultRuleLevelArg = RubyCommand.Argument(name: "precheck_default_rule_level", value: precheckDefaultRuleLevel, type: nil) - let individualMetadataItemsArg = individualMetadataItems.asRubyArgument(name: "individual_metadata_items", type: nil) - let appIconArg = appIcon.asRubyArgument(name: "app_icon", type: nil) - let appleWatchAppIconArg = appleWatchAppIcon.asRubyArgument(name: "apple_watch_app_icon", type: nil) - let copyrightArg = copyright.asRubyArgument(name: "copyright", type: nil) - let primaryCategoryArg = primaryCategory.asRubyArgument(name: "primary_category", type: nil) - let secondaryCategoryArg = secondaryCategory.asRubyArgument(name: "secondary_category", type: nil) - let primaryFirstSubCategoryArg = primaryFirstSubCategory.asRubyArgument(name: "primary_first_sub_category", type: nil) - let primarySecondSubCategoryArg = primarySecondSubCategory.asRubyArgument(name: "primary_second_sub_category", type: nil) - let secondaryFirstSubCategoryArg = secondaryFirstSubCategory.asRubyArgument(name: "secondary_first_sub_category", type: nil) - let secondarySecondSubCategoryArg = secondarySecondSubCategory.asRubyArgument(name: "secondary_second_sub_category", type: nil) - let tradeRepresentativeContactInformationArg = tradeRepresentativeContactInformation.asRubyArgument(name: "trade_representative_contact_information", type: nil) - let appReviewInformationArg = appReviewInformation.asRubyArgument(name: "app_review_information", type: nil) - let appReviewAttachmentFileArg = appReviewAttachmentFile.asRubyArgument(name: "app_review_attachment_file", type: nil) - let descriptionArg = description.asRubyArgument(name: "description", type: nil) - let nameArg = name.asRubyArgument(name: "name", type: nil) - let subtitleArg = subtitle.asRubyArgument(name: "subtitle", type: nil) - let keywordsArg = keywords.asRubyArgument(name: "keywords", type: nil) - let promotionalTextArg = promotionalText.asRubyArgument(name: "promotional_text", type: nil) - let releaseNotesArg = releaseNotes.asRubyArgument(name: "release_notes", type: nil) - let privacyUrlArg = privacyUrl.asRubyArgument(name: "privacy_url", type: nil) - let appleTvPrivacyPolicyArg = appleTvPrivacyPolicy.asRubyArgument(name: "apple_tv_privacy_policy", type: nil) - let supportUrlArg = supportUrl.asRubyArgument(name: "support_url", type: nil) - let marketingUrlArg = marketingUrl.asRubyArgument(name: "marketing_url", type: nil) - let languagesArg = languages.asRubyArgument(name: "languages", type: nil) - let ignoreLanguageDirectoryValidationArg = ignoreLanguageDirectoryValidation.asRubyArgument(name: "ignore_language_directory_validation", type: nil) - let precheckIncludeInAppPurchasesArg = precheckIncludeInAppPurchases.asRubyArgument(name: "precheck_include_in_app_purchases", type: nil) - let appArg = app.asRubyArgument(name: "app", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - usernameArg, - appIdentifierArg, - appVersionArg, - ipaArg, - pkgArg, - buildNumberArg, - platformArg, - editLiveArg, - useLiveVersionArg, - metadataPathArg, - screenshotsPathArg, - appPreviewsPathArg, - previewFrameTimeCodeArg, - overwritePreviewVideosArg, - skipBinaryUploadArg, - skipScreenshotsArg, - skipMetadataArg, - skipAppVersionUpdateArg, - forceArg, - overwriteScreenshotsArg, - screenshotProcessingTimeoutArg, - syncScreenshotsArg, - submitForReviewArg, - verifyOnlyArg, - rejectIfPossibleArg, - versionCheckWaitRetryLimitArg, - automaticReleaseArg, - autoReleaseDateArg, - phasedReleaseArg, - resetRatingsArg, - priceTierArg, - appRatingConfigPathArg, - submissionInformationArg, - teamIdArg, - teamNameArg, - devPortalTeamIdArg, - devPortalTeamNameArg, - itcProviderArg, - providerPublicIdArg, - runPrecheckBeforeSubmitArg, - precheckDefaultRuleLevelArg, - individualMetadataItemsArg, - appIconArg, - appleWatchAppIconArg, - copyrightArg, - primaryCategoryArg, - secondaryCategoryArg, - primaryFirstSubCategoryArg, - primarySecondSubCategoryArg, - secondaryFirstSubCategoryArg, - secondarySecondSubCategoryArg, - tradeRepresentativeContactInformationArg, - appReviewInformationArg, - appReviewAttachmentFileArg, - descriptionArg, - nameArg, - subtitleArg, - keywordsArg, - promotionalTextArg, - releaseNotesArg, - privacyUrlArg, - appleTvPrivacyPolicyArg, - supportUrlArg, - marketingUrlArg, - languagesArg, - ignoreLanguageDirectoryValidationArg, - precheckIncludeInAppPurchasesArg, - appArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "deliver", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload a new build to [DeployGate](https://deploygate.com/) - - - parameters: - - apiToken: Deploygate API Token - - user: Target username or organization name - - ipa: Path to your IPA file. Optional if you use the _gym_ or _xcodebuild_ action - - apk: Path to your APK file - - message: Release Notes - - distributionKey: Target Distribution Key - - releaseNote: Release note for distribution page - - disableNotify: Disables Push notification emails - - distributionName: Target Distribution Name - - You can retrieve your username and API token on [your settings page](https://deploygate.com/settings). - More information about the available options can be found in the [DeployGate Push API document](https://deploygate.com/docs/api). - */ -public func deploygate(apiToken: String, - user: String, - ipa: OptionalConfigValue = .fastlaneDefault(nil), - apk: OptionalConfigValue = .fastlaneDefault(nil), - message: String = "No changelog provided", - distributionKey: OptionalConfigValue = .fastlaneDefault(nil), - releaseNote: OptionalConfigValue = .fastlaneDefault(nil), - disableNotify: OptionalConfigValue = .fastlaneDefault(false), - distributionName: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let userArg = RubyCommand.Argument(name: "user", value: user, type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let apkArg = apk.asRubyArgument(name: "apk", type: nil) - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let distributionKeyArg = distributionKey.asRubyArgument(name: "distribution_key", type: nil) - let releaseNoteArg = releaseNote.asRubyArgument(name: "release_note", type: nil) - let disableNotifyArg = disableNotify.asRubyArgument(name: "disable_notify", type: nil) - let distributionNameArg = distributionName.asRubyArgument(name: "distribution_name", type: nil) - let array: [RubyCommand.Argument?] = [apiTokenArg, - userArg, - ipaArg, - apkArg, - messageArg, - distributionKeyArg, - releaseNoteArg, - disableNotifyArg, - distributionNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "deploygate", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Reads in production secrets set in a dotgpg file and puts them in ENV - - - parameter dotgpgFile: Path to your gpg file - - More information about dotgpg can be found at [https://github.com/ConradIrwin/dotgpg](https://github.com/ConradIrwin/dotgpg). - */ -public func dotgpgEnvironment(dotgpgFile: String) { - let dotgpgFileArg = RubyCommand.Argument(name: "dotgpg_file", value: dotgpgFile, type: nil) - let array: [RubyCommand.Argument?] = [dotgpgFileArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "dotgpg_environment", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Download a file from a remote server (e.g. JSON file) - - - parameter url: The URL that should be downloaded - - Specify the URL to download and get the content as a return value. - Automatically parses JSON into a Ruby data structure. - For more advanced networking code, use the Ruby functions instead: [http://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html](http://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html). - */ -public func download(url: String) { - let urlArg = RubyCommand.Argument(name: "url", value: url, type: nil) - let array: [RubyCommand.Argument?] = [urlArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "download", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Download App Privacy Details from an app in App Store Connect - - - parameters: - - username: Your Apple ID Username for App Store Connect - - appIdentifier: The bundle identifier of your app - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - outputJsonPath: Path to the app usage data JSON file generated by interactive questions - - Download App Privacy Details from an app in App Store Connect. For more detail information, view https://docs.fastlane.tools/uploading-app-privacy-details - */ -public func downloadAppPrivacyDetailsFromAppStore(username: String, - appIdentifier: String, - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - outputJsonPath: String = "./fastlane/app_privacy_details.json") -{ - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let outputJsonPathArg = RubyCommand.Argument(name: "output_json_path", value: outputJsonPath, type: nil) - let array: [RubyCommand.Argument?] = [usernameArg, - appIdentifierArg, - teamIdArg, - teamNameArg, - outputJsonPathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "download_app_privacy_details_from_app_store", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Download dSYM files from App Store Connect for Bitcode apps - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#use-return-value-and-pass-in-as-an-option) - - username: Your Apple ID Username for App Store Connect - - appIdentifier: The bundle identifier of your app - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - platform: The app platform for dSYMs you wish to download (ios, xros, appletvos) - - version: The app version for dSYMs you wish to download, pass in 'latest' to download only the latest build's dSYMs or 'live' to download only the live version dSYMs - - buildNumber: The app build_number for dSYMs you wish to download - - minVersion: The minimum app version for dSYMs you wish to download - - afterUploadedDate: The uploaded date after which you wish to download dSYMs - - outputDirectory: Where to save the download dSYMs, defaults to the current path - - waitForDsymProcessing: Wait for dSYMs to process - - waitTimeout: Number of seconds to wait for dSYMs to process - - This action downloads dSYM files from App Store Connect after the ipa gets re-compiled by Apple. Useful if you have Bitcode enabled.| - | - ```ruby| - lane :refresh_dsyms do| - download_dsyms # Download dSYM files from iTC| - upload_symbols_to_crashlytics # Upload them to Crashlytics| - clean_build_artifacts # Delete the local dSYM files| - end| - ```| - >| - */ -public func downloadDsyms(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: String, - appIdentifier: String, - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - version: OptionalConfigValue = .fastlaneDefault(nil), - buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - minVersion: OptionalConfigValue = .fastlaneDefault(nil), - afterUploadedDate: OptionalConfigValue = .fastlaneDefault(nil), - outputDirectory: OptionalConfigValue = .fastlaneDefault(nil), - waitForDsymProcessing: OptionalConfigValue = .fastlaneDefault(false), - waitTimeout: Int = 300) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let versionArg = version.asRubyArgument(name: "version", type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let minVersionArg = minVersion.asRubyArgument(name: "min_version", type: nil) - let afterUploadedDateArg = afterUploadedDate.asRubyArgument(name: "after_uploaded_date", type: nil) - let outputDirectoryArg = outputDirectory.asRubyArgument(name: "output_directory", type: nil) - let waitForDsymProcessingArg = waitForDsymProcessing.asRubyArgument(name: "wait_for_dsym_processing", type: nil) - let waitTimeoutArg = RubyCommand.Argument(name: "wait_timeout", value: waitTimeout, type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - usernameArg, - appIdentifierArg, - teamIdArg, - teamNameArg, - platformArg, - versionArg, - buildNumberArg, - minVersionArg, - afterUploadedDateArg, - outputDirectoryArg, - waitForDsymProcessingArg, - waitTimeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "download_dsyms", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Download metadata and binaries from Google Play (via _supply_) - - - parameters: - - packageName: The package name of the application to use - - versionName: Version name (used when uploading new apks/aabs) - defaults to 'versionName' in build.gradle or AndroidManifest.xml - - track: The track of the application to use. The default available tracks are: production, beta, alpha, internal - - metadataPath: Path to the directory containing the metadata files - - key: **DEPRECATED!** Use `--json_key` instead - The p12 File used to authenticate with Google - - issuer: **DEPRECATED!** Use `--json_key` instead - The issuer of the p12 file (email address of the service account) - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - timeout: Timeout for read, open, and send (in seconds) - - More information: https://docs.fastlane.tools/actions/download_from_play_store/ - */ -public func downloadFromPlayStore(packageName: String, - versionName: OptionalConfigValue = .fastlaneDefault(nil), - track: String = "production", - metadataPath: OptionalConfigValue = .fastlaneDefault(nil), - key: OptionalConfigValue = .fastlaneDefault(nil), - issuer: OptionalConfigValue = .fastlaneDefault(nil), - jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 300) -{ - let packageNameArg = RubyCommand.Argument(name: "package_name", value: packageName, type: nil) - let versionNameArg = versionName.asRubyArgument(name: "version_name", type: nil) - let trackArg = RubyCommand.Argument(name: "track", value: track, type: nil) - let metadataPathArg = metadataPath.asRubyArgument(name: "metadata_path", type: nil) - let keyArg = key.asRubyArgument(name: "key", type: nil) - let issuerArg = issuer.asRubyArgument(name: "issuer", type: nil) - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let array: [RubyCommand.Argument?] = [packageNameArg, - versionNameArg, - trackArg, - metadataPathArg, - keyArg, - issuerArg, - jsonKeyArg, - jsonKeyDataArg, - rootUrlArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "download_from_play_store", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Download the Universal APK of a given version code from the Google Play Console - - - parameters: - - packageName: The package name of the application to use - - versionCode: The versionCode for which to download the generated APK - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - timeout: Timeout for read, open, and send (in seconds) - - destination: The path on disk where to download the Generated Universal APK - - certificateSha256Hash: The SHA256 hash of the signing key for which to download the Universal, Code-Signed APK for. Use 'xx:xx:xx:…' format (32 hex bytes separated by colons), as printed by `keytool -list -keystore `. Only useful to provide if you have multiple signing keys configured on GPC, to specify which generated APK to download - - - returns: The path to the downloaded Universal APK. The action will raise an exception if it failed to find or download the APK in Google Play - - Download the universal APK of a given version code from the Google Play Console. - - This uses _fastlane_ `supply` (and the `AndroidPublisher` Google API) to download the Universal APK - generated by Google after you uploaded an `.aab` bundle to the Play Console. - - See https://developers.google.com/android-publisher/api-ref/rest/v3/generatedapks/list - - */ -public func downloadUniversalApkFromGooglePlay(packageName: String, - versionCode: OptionalConfigValue = .fastlaneDefault(nil), - jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 300, - destination: String, - certificateSha256Hash: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let packageNameArg = RubyCommand.Argument(name: "package_name", value: packageName, type: nil) - let versionCodeArg = versionCode.asRubyArgument(name: "version_code", type: nil) - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let destinationArg = RubyCommand.Argument(name: "destination", value: destination, type: nil) - let certificateSha256HashArg = certificateSha256Hash.asRubyArgument(name: "certificate_sha256_hash", type: nil) - let array: [RubyCommand.Argument?] = [packageNameArg, - versionCodeArg, - jsonKeyArg, - jsonKeyDataArg, - rootUrlArg, - timeoutArg, - destinationArg, - certificateSha256HashArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "download_universal_apk_from_google_play", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Creates a zipped dSYM in the project root from the .xcarchive - - - parameters: - - archivePath: Path to your xcarchive file. Optional if you use the `xcodebuild` action - - dsymPath: Path for generated dsym. Optional, default is your apps root directory - - all: Whether or not all dSYM files are to be included. Optional, default is false in which only your app dSYM is included - - You can manually specify the path to the xcarchive (not needed if you use `xcodebuild`/`xcarchive` to build your archive) - */ -public func dsymZip(archivePath: OptionalConfigValue = .fastlaneDefault(nil), - dsymPath: OptionalConfigValue = .fastlaneDefault(nil), - all: OptionalConfigValue = .fastlaneDefault(false)) -{ - let archivePathArg = archivePath.asRubyArgument(name: "archive_path", type: nil) - let dsymPathArg = dsymPath.asRubyArgument(name: "dsym_path", type: nil) - let allArg = all.asRubyArgument(name: "all", type: nil) - let array: [RubyCommand.Argument?] = [archivePathArg, - dsymPathArg, - allArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "dsym_zip", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `puts` action - - - parameter message: Message to be printed out - */ -public func echo(message: OptionalConfigValue = .fastlaneDefault(nil)) { - let messageArg = message.asRubyArgument(name: "message", type: nil) - let array: [RubyCommand.Argument?] = [messageArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "echo", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Raises an exception if not using `bundle exec` to run fastlane - - This action will check if you are using `bundle exec` to run fastlane. - You can put it into `before_all` to make sure that fastlane is ran using the `bundle exec fastlane` command. - */ -public func ensureBundleExec() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "ensure_bundle_exec", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Raises an exception if the specified env vars are not set - - - parameter envVars: The environment variables names that should be checked - - This action will check if some environment variables are set. - */ -public func ensureEnvVars(envVars: [String]) { - let envVarsArg = RubyCommand.Argument(name: "env_vars", value: envVars, type: nil) - let array: [RubyCommand.Argument?] = [envVarsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ensure_env_vars", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Raises an exception if not on a specific git branch - - - parameter branch: The branch that should be checked for. String that can be either the full name of the branch or a regex e.g. `^feature/.*$` to match - - This action will check if your git repo is checked out to a specific branch. - You may only want to make releases from a specific branch, so `ensure_git_branch` will stop a lane if it was accidentally executed on an incorrect branch. - */ -public func ensureGitBranch(branch: String = "master") { - let branchArg = RubyCommand.Argument(name: "branch", value: branch, type: nil) - let array: [RubyCommand.Argument?] = [branchArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ensure_git_branch", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Raises an exception if there are uncommitted git changes - - - parameters: - - showUncommittedChanges: The flag whether to show uncommitted changes if the repo is dirty - - showDiff: The flag whether to show the git diff if the repo is dirty - - ignored: The handling mode of the ignored files. The available options are: `'traditional'`, `'none'` (default) and `'matching'`. Specifying `'none'` to this parameter is the same as not specifying the parameter at all, which means that no ignored file will be used to check if the repo is dirty or not. Specifying `'traditional'` or `'matching'` causes some ignored files to be used to check if the repo is dirty or not (more info in the official docs: https://git-scm.com/docs/git-status#Documentation/git-status.txt---ignoredltmodegt) - - ignoreFiles: Array of files to ignore - - A sanity check to make sure you are working in a repo that is clean. - Especially useful to put at the beginning of your Fastfile in the `before_all` block, if some of your other actions will touch your filesystem, do things to your git repo, or just as a general reminder to save your work. - Also needed as a prerequisite for some other actions like `reset_git_repo`. - */ -public func ensureGitStatusClean(showUncommittedChanges: OptionalConfigValue = .fastlaneDefault(false), - showDiff: OptionalConfigValue = .fastlaneDefault(false), - ignored: OptionalConfigValue = .fastlaneDefault(nil), - ignoreFiles: OptionalConfigValue<[String]?> = .fastlaneDefault(nil)) -{ - let showUncommittedChangesArg = showUncommittedChanges.asRubyArgument(name: "show_uncommitted_changes", type: nil) - let showDiffArg = showDiff.asRubyArgument(name: "show_diff", type: nil) - let ignoredArg = ignored.asRubyArgument(name: "ignored", type: nil) - let ignoreFilesArg = ignoreFiles.asRubyArgument(name: "ignore_files", type: nil) - let array: [RubyCommand.Argument?] = [showUncommittedChangesArg, - showDiffArg, - ignoredArg, - ignoreFilesArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ensure_git_status_clean", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Ensures the given text is nowhere in the code base - - - parameters: - - text: The text that must not be in the code base - - path: The directory containing all the source files - - extension: The extension that should be searched for - - extensions: An array of file extensions that should be searched for - - exclude: Exclude a certain pattern from the search - - excludeDirs: An array of dirs that should not be included in the search - - You don't want any debug code to slip into production. - This can be used to check if there is any debug code still in your codebase or if you have things like `// TO DO` or similar. - */ -public func ensureNoDebugCode(text: String, - path: String = ".", - extension: OptionalConfigValue = .fastlaneDefault(nil), - extensions: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - exclude: OptionalConfigValue = .fastlaneDefault(nil), - excludeDirs: OptionalConfigValue<[String]?> = .fastlaneDefault(nil)) -{ - let textArg = RubyCommand.Argument(name: "text", value: text, type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let extensionArg = `extension`.asRubyArgument(name: "extension", type: nil) - let extensionsArg = extensions.asRubyArgument(name: "extensions", type: nil) - let excludeArg = exclude.asRubyArgument(name: "exclude", type: nil) - let excludeDirsArg = excludeDirs.asRubyArgument(name: "exclude_dirs", type: nil) - let array: [RubyCommand.Argument?] = [textArg, - pathArg, - extensionArg, - extensionsArg, - excludeArg, - excludeDirsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ensure_no_debug_code", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Ensure the right version of Xcode is used - - - parameters: - - version: Xcode version to verify that is selected - - strict: Should the version be verified strictly (all 3 version numbers), or matching only the given version numbers (i.e. `11.3` == `11.3.x`) - - If building your app requires a specific version of Xcode, you can invoke this command before using gym. - For example, to ensure that a beta version of Xcode is not accidentally selected to build, which would make uploading to TestFlight fail. - You can either manually provide a specific version using `version:` or you make use of the `.xcode-version` file. - Using the `strict` parameter, you can either verify the full set of version numbers strictly (i.e. `11.3.1`) or only a subset of them (i.e. `11.3` or `11`). - */ -public func ensureXcodeVersion(version: OptionalConfigValue = .fastlaneDefault(nil), - strict: OptionalConfigValue = .fastlaneDefault(true)) -{ - let versionArg = version.asRubyArgument(name: "version", type: nil) - let strictArg = strict.asRubyArgument(name: "strict", type: nil) - let array: [RubyCommand.Argument?] = [versionArg, - strictArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ensure_xcode_version", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Sets/gets env vars for Fastlane.swift. Don't use in ruby, use `ENV[key] = val` - - - parameters: - - set: Set the environment variables named - - get: Get the environment variable named - - remove: Remove the environment variable named - */ -@discardableResult public func environmentVariable(set: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - get: OptionalConfigValue = .fastlaneDefault(nil), - remove: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let setArg = set.asRubyArgument(name: "set", type: nil) - let getArg = get.asRubyArgument(name: "get", type: nil) - let removeArg = remove.asRubyArgument(name: "remove", type: nil) - let array: [RubyCommand.Argument?] = [setArg, - getArg, - removeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "environment_variable", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Allows to Generate output files based on ERB templates - - - parameters: - - template: ERB Template File - - destination: Destination file - - placeholders: Placeholders given as a hash - - trimMode: Trim mode applied to the ERB - - Renders an ERB template with `:placeholders` given as a hash via parameter. - If no `:destination` is set, it returns the rendered template as string. - */ -public func erb(template: String, - destination: OptionalConfigValue = .fastlaneDefault(nil), - placeholders: [String: Any] = [:], - trimMode: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let templateArg = RubyCommand.Argument(name: "template", value: template, type: nil) - let destinationArg = destination.asRubyArgument(name: "destination", type: nil) - let placeholdersArg = RubyCommand.Argument(name: "placeholders", value: placeholders, type: nil) - let trimModeArg = trimMode.asRubyArgument(name: "trim_mode", type: nil) - let array: [RubyCommand.Argument?] = [templateArg, - destinationArg, - placeholdersArg, - trimModeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "erb", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `min_fastlane_version` action - - Add this to your `Fastfile` to require a certain version of _fastlane_. - Use it if you use an action that just recently came out and you need it. - */ -public func fastlaneVersion() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "fastlane_version", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Send a message to a [Flock](https://flock.com/) group - - - parameters: - - message: Message text - - token: Token for the Flock incoming webhook - - baseUrl: Base URL of the Flock incoming message webhook - - To obtain the token, create a new [incoming message webhook](https://dev.flock.co/wiki/display/FlockAPI/Incoming+Webhooks) in your Flock admin panel. - */ -public func flock(message: String, - token: String, - baseUrl: String = "https://api.flock.co/hooks/sendMessage") -{ - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let tokenArg = RubyCommand.Argument(name: "token", value: token, type: nil) - let baseUrlArg = RubyCommand.Argument(name: "base_url", value: baseUrl, type: nil) - let array: [RubyCommand.Argument?] = [messageArg, - tokenArg, - baseUrlArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "flock", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Adds device frames around all screenshots (via _frameit_) - - - parameters: - - white: Use white device frames - - silver: Use white device frames. Alias for :white - - roseGold: Use rose gold device frames. Alias for :rose_gold - - gold: Use gold device frames. Alias for :gold - - forceDeviceType: Forces a given device type, useful for Mac screenshots, as their sizes vary - - useLegacyIphone5s: Use iPhone 5s instead of iPhone SE frames - - useLegacyIphone6s: Use iPhone 6s frames instead of iPhone 7 frames - - useLegacyIphone7: Use iPhone 7 frames instead of iPhone 8 frames - - useLegacyIphonex: Use iPhone X instead of iPhone XS frames - - useLegacyIphonexr: Use iPhone XR instead of iPhone 11 frames - - useLegacyIphonexs: Use iPhone XS instead of iPhone 11 Pro frames - - useLegacyIphonexsmax: Use iPhone XS Max instead of iPhone 11 Pro Max frames - - forceOrientationBlock: [Advanced] A block to customize your screenshots' device orientation - - debugMode: Output debug information in framed screenshots - - resume: Resume frameit instead of reprocessing all screenshots - - usePlatform: Choose a platform, the valid options are IOS, ANDROID and ANY (default is either general platform defined in the fastfile or IOS to ensure backward compatibility) - - path: The path to the directory containing the screenshots - - Uses [frameit](https://docs.fastlane.tools/actions/frameit/) to prepare perfect screenshots for the App Store, your website, QA or emails. - You can add background and titles to the framed screenshots as well. - */ -public func frameScreenshots(white: OptionalConfigValue = .fastlaneDefault(nil), - silver: OptionalConfigValue = .fastlaneDefault(nil), - roseGold: OptionalConfigValue = .fastlaneDefault(nil), - gold: OptionalConfigValue = .fastlaneDefault(nil), - forceDeviceType: OptionalConfigValue = .fastlaneDefault(nil), - useLegacyIphone5s: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphone6s: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphone7: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonex: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonexr: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonexs: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonexsmax: OptionalConfigValue = .fastlaneDefault(false), - forceOrientationBlock: ((String) -> Void)? = nil, - debugMode: OptionalConfigValue = .fastlaneDefault(false), - resume: OptionalConfigValue = .fastlaneDefault(false), - usePlatform: String = "IOS", - path: String = "./") -{ - let whiteArg = white.asRubyArgument(name: "white", type: nil) - let silverArg = silver.asRubyArgument(name: "silver", type: nil) - let roseGoldArg = roseGold.asRubyArgument(name: "rose_gold", type: nil) - let goldArg = gold.asRubyArgument(name: "gold", type: nil) - let forceDeviceTypeArg = forceDeviceType.asRubyArgument(name: "force_device_type", type: nil) - let useLegacyIphone5sArg = useLegacyIphone5s.asRubyArgument(name: "use_legacy_iphone5s", type: nil) - let useLegacyIphone6sArg = useLegacyIphone6s.asRubyArgument(name: "use_legacy_iphone6s", type: nil) - let useLegacyIphone7Arg = useLegacyIphone7.asRubyArgument(name: "use_legacy_iphone7", type: nil) - let useLegacyIphonexArg = useLegacyIphonex.asRubyArgument(name: "use_legacy_iphonex", type: nil) - let useLegacyIphonexrArg = useLegacyIphonexr.asRubyArgument(name: "use_legacy_iphonexr", type: nil) - let useLegacyIphonexsArg = useLegacyIphonexs.asRubyArgument(name: "use_legacy_iphonexs", type: nil) - let useLegacyIphonexsmaxArg = useLegacyIphonexsmax.asRubyArgument(name: "use_legacy_iphonexsmax", type: nil) - let forceOrientationBlockArg = RubyCommand.Argument(name: "force_orientation_block", value: forceOrientationBlock, type: .stringClosure) - let debugModeArg = debugMode.asRubyArgument(name: "debug_mode", type: nil) - let resumeArg = resume.asRubyArgument(name: "resume", type: nil) - let usePlatformArg = RubyCommand.Argument(name: "use_platform", value: usePlatform, type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let array: [RubyCommand.Argument?] = [whiteArg, - silverArg, - roseGoldArg, - goldArg, - forceDeviceTypeArg, - useLegacyIphone5sArg, - useLegacyIphone6sArg, - useLegacyIphone7Arg, - useLegacyIphonexArg, - useLegacyIphonexrArg, - useLegacyIphonexsArg, - useLegacyIphonexsmaxArg, - forceOrientationBlockArg, - debugModeArg, - resumeArg, - usePlatformArg, - pathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "frame_screenshots", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `frame_screenshots` action - - - parameters: - - white: Use white device frames - - silver: Use white device frames. Alias for :white - - roseGold: Use rose gold device frames. Alias for :rose_gold - - gold: Use gold device frames. Alias for :gold - - forceDeviceType: Forces a given device type, useful for Mac screenshots, as their sizes vary - - useLegacyIphone5s: Use iPhone 5s instead of iPhone SE frames - - useLegacyIphone6s: Use iPhone 6s frames instead of iPhone 7 frames - - useLegacyIphone7: Use iPhone 7 frames instead of iPhone 8 frames - - useLegacyIphonex: Use iPhone X instead of iPhone XS frames - - useLegacyIphonexr: Use iPhone XR instead of iPhone 11 frames - - useLegacyIphonexs: Use iPhone XS instead of iPhone 11 Pro frames - - useLegacyIphonexsmax: Use iPhone XS Max instead of iPhone 11 Pro Max frames - - forceOrientationBlock: [Advanced] A block to customize your screenshots' device orientation - - debugMode: Output debug information in framed screenshots - - resume: Resume frameit instead of reprocessing all screenshots - - usePlatform: Choose a platform, the valid options are IOS, ANDROID and ANY (default is either general platform defined in the fastfile or IOS to ensure backward compatibility) - - path: The path to the directory containing the screenshots - - Uses [frameit](https://docs.fastlane.tools/actions/frameit/) to prepare perfect screenshots for the App Store, your website, QA or emails. - You can add background and titles to the framed screenshots as well. - */ -public func frameit(white: OptionalConfigValue = .fastlaneDefault(nil), - silver: OptionalConfigValue = .fastlaneDefault(nil), - roseGold: OptionalConfigValue = .fastlaneDefault(nil), - gold: OptionalConfigValue = .fastlaneDefault(nil), - forceDeviceType: OptionalConfigValue = .fastlaneDefault(nil), - useLegacyIphone5s: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphone6s: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphone7: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonex: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonexr: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonexs: OptionalConfigValue = .fastlaneDefault(false), - useLegacyIphonexsmax: OptionalConfigValue = .fastlaneDefault(false), - forceOrientationBlock: ((String) -> Void)? = nil, - debugMode: OptionalConfigValue = .fastlaneDefault(false), - resume: OptionalConfigValue = .fastlaneDefault(false), - usePlatform: String = "IOS", - path: String = "./") -{ - let whiteArg = white.asRubyArgument(name: "white", type: nil) - let silverArg = silver.asRubyArgument(name: "silver", type: nil) - let roseGoldArg = roseGold.asRubyArgument(name: "rose_gold", type: nil) - let goldArg = gold.asRubyArgument(name: "gold", type: nil) - let forceDeviceTypeArg = forceDeviceType.asRubyArgument(name: "force_device_type", type: nil) - let useLegacyIphone5sArg = useLegacyIphone5s.asRubyArgument(name: "use_legacy_iphone5s", type: nil) - let useLegacyIphone6sArg = useLegacyIphone6s.asRubyArgument(name: "use_legacy_iphone6s", type: nil) - let useLegacyIphone7Arg = useLegacyIphone7.asRubyArgument(name: "use_legacy_iphone7", type: nil) - let useLegacyIphonexArg = useLegacyIphonex.asRubyArgument(name: "use_legacy_iphonex", type: nil) - let useLegacyIphonexrArg = useLegacyIphonexr.asRubyArgument(name: "use_legacy_iphonexr", type: nil) - let useLegacyIphonexsArg = useLegacyIphonexs.asRubyArgument(name: "use_legacy_iphonexs", type: nil) - let useLegacyIphonexsmaxArg = useLegacyIphonexsmax.asRubyArgument(name: "use_legacy_iphonexsmax", type: nil) - let forceOrientationBlockArg = RubyCommand.Argument(name: "force_orientation_block", value: forceOrientationBlock, type: .stringClosure) - let debugModeArg = debugMode.asRubyArgument(name: "debug_mode", type: nil) - let resumeArg = resume.asRubyArgument(name: "resume", type: nil) - let usePlatformArg = RubyCommand.Argument(name: "use_platform", value: usePlatform, type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let array: [RubyCommand.Argument?] = [whiteArg, - silverArg, - roseGoldArg, - goldArg, - forceDeviceTypeArg, - useLegacyIphone5sArg, - useLegacyIphone6sArg, - useLegacyIphone7Arg, - useLegacyIphonexArg, - useLegacyIphonexrArg, - useLegacyIphonexsArg, - useLegacyIphonexsmaxArg, - forceOrientationBlockArg, - debugModeArg, - resumeArg, - usePlatformArg, - pathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "frameit", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs test coverage reports for your Xcode project - - Generate summarized code coverage reports using [gcovr](http://gcovr.com/) - */ -public func gcovr() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "gcovr", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Get the build number of your project - - - parameters: - - xcodeproj: optional, you must specify the path to your main Xcode project if it is not in the project root directory - - hideErrorWhenVersioningDisabled: Used during `fastlane init` to hide the error message - - This action will return the current build number set on your project. - You first have to set up your Xcode project, if you haven't done it already: [https://developer.apple.com/library/ios/qa/qa1827/_index.html](https://developer.apple.com/library/ios/qa/qa1827/_index.html). - */ -@discardableResult public func getBuildNumber(xcodeproj: OptionalConfigValue = .fastlaneDefault(nil), - hideErrorWhenVersioningDisabled: OptionalConfigValue = .fastlaneDefault(false)) -> String -{ - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let hideErrorWhenVersioningDisabledArg = hideErrorWhenVersioningDisabled.asRubyArgument(name: "hide_error_when_versioning_disabled", type: nil) - let array: [RubyCommand.Argument?] = [xcodeprojArg, - hideErrorWhenVersioningDisabledArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_build_number", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Get the build number from the current repository - - - parameter useHgRevisionNumber: Use hg revision number instead of hash (ignored for non-hg repos) - - - returns: The build number from the current repository - - This action will get the **build number** according to what the SCM HEAD reports. - Currently supported SCMs are svn (uses root revision), git-svn (uses svn revision), git (uses short hash) and mercurial (uses short hash or revision number). - There is an option, `:use_hg_revision_number`, which allows to use mercurial revision number instead of hash. - */ -public func getBuildNumberRepository(useHgRevisionNumber: OptionalConfigValue = .fastlaneDefault(false)) { - let useHgRevisionNumberArg = useHgRevisionNumber.asRubyArgument(name: "use_hg_revision_number", type: nil) - let array: [RubyCommand.Argument?] = [useHgRevisionNumberArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_build_number_repository", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Create new iOS code signing certificates (via _cert_) - - - parameters: - - development: Create a development certificate instead of a distribution one - - type: Create specific certificate type (takes precedence over :development) - - force: Create a certificate even if an existing certificate exists - - generateAppleCerts: Create a certificate type for Xcode 11 and later (Apple Development or Apple Distribution) - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - filename: The filename of certificate to store - - outputPath: The path to a directory in which all certificates and private keys should be stored - - keychainPath: Path to a custom keychain - - keychainPassword: This might be required the first time you access certificates on a new mac. For the login/default keychain this is your macOS account password - - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing - - platform: Set the provisioning profile's platform (ios, macos, tvos) - - **Important**: It is recommended to use [match](https://docs.fastlane.tools/actions/match/) according to the [codesigning.guide](https://codesigning.guide) for generating and maintaining your certificates. Use _cert_ directly only if you want full control over what's going on and know more about codesigning. - Use this action to download the latest code signing identity. - */ -public func getCertificates(development: OptionalConfigValue = .fastlaneDefault(false), - type: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - generateAppleCerts: OptionalConfigValue = .fastlaneDefault(true), - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - filename: OptionalConfigValue = .fastlaneDefault(nil), - outputPath: String = ".", - keychainPath: OptionalConfigValue = .fastlaneDefault(nil), - keychainPassword: OptionalConfigValue = .fastlaneDefault(nil), - skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(false), - platform: String = "ios") -{ - let developmentArg = development.asRubyArgument(name: "development", type: nil) - let typeArg = type.asRubyArgument(name: "type", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let generateAppleCertsArg = generateAppleCerts.asRubyArgument(name: "generate_apple_certs", type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let filenameArg = filename.asRubyArgument(name: "filename", type: nil) - let outputPathArg = RubyCommand.Argument(name: "output_path", value: outputPath, type: nil) - let keychainPathArg = keychainPath.asRubyArgument(name: "keychain_path", type: nil) - let keychainPasswordArg = keychainPassword.asRubyArgument(name: "keychain_password", type: nil) - let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let array: [RubyCommand.Argument?] = [developmentArg, - typeArg, - forceArg, - generateAppleCertsArg, - apiKeyPathArg, - apiKeyArg, - usernameArg, - teamIdArg, - teamNameArg, - filenameArg, - outputPathArg, - keychainPathArg, - keychainPasswordArg, - skipSetPartitionListArg, - platformArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_certificates", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will verify if a given release version is available on GitHub - - - parameters: - - url: The path to your repo, e.g. 'KrauseFx/fastlane' - - serverUrl: The server url. e.g. 'https://your.github.server/api/v3' (Default: 'https://api.github.com') - - version: The version tag of the release to check - - apiToken: GitHub Personal Token (required for private repositories) - - apiBearer: Use a Bearer authorization token. Usually generated by GitHub Apps, e.g. GitHub Actions GITHUB_TOKEN environment variable - - This will return all information about a release. For example:| - | - ```no-highlight| - {| - "url"=>"https://api.github.com/repos/KrauseFx/fastlane/releases/1537713",| - "assets_url"=>"https://api.github.com/repos/KrauseFx/fastlane/releases/1537713/assets",| - "upload_url"=>"https://uploads.github.com/repos/KrauseFx/fastlane/releases/1537713/assets{?name}",| - "html_url"=>"https://github.com/fastlane/fastlane/releases/tag/1.8.0",| - "id"=>1537713,| - "tag_name"=>"1.8.0",| - "target_commitish"=>"master",| - "name"=>"1.8.0 Switch Lanes & Pass Parameters",| - "draft"=>false,| - "author"=>| - {"login"=>"KrauseFx",| - "id"=>869950,| - "avatar_url"=>"https://avatars.githubusercontent.com/u/869950?v=3",| - "gravatar_id"=>"",| - "url"=>"https://api.github.com/users/KrauseFx",| - "html_url"=>"https://github.com/fastlane",| - "followers_url"=>"https://api.github.com/users/KrauseFx/followers",| - "following_url"=>"https://api.github.com/users/KrauseFx/following{/other_user}",| - "gists_url"=>"https://api.github.com/users/KrauseFx/gists{/gist_id}",| - "starred_url"=>"https://api.github.com/users/KrauseFx/starred{/owner}{/repo}",| - "subscriptions_url"=>"https://api.github.com/users/KrauseFx/subscriptions",| - "organizations_url"=>"https://api.github.com/users/KrauseFx/orgs",| - "repos_url"=>"https://api.github.com/users/KrauseFx/repos",| - "events_url"=>"https://api.github.com/users/KrauseFx/events{/privacy}",| - "received_events_url"=>"https://api.github.com/users/KrauseFx/received_events",| - "type"=>"User",| - "site_admin"=>false},| - "prerelease"=>false,| - "created_at"=>"2015-07-14T23:33:01Z",| - "published_at"=>"2015-07-14T23:44:10Z",| - "assets"=>[],| - "tarball_url"=>"https://api.github.com/repos/KrauseFx/fastlane/tarball/1.8.0",| - "zipball_url"=>"https://api.github.com/repos/KrauseFx/fastlane/zipball/1.8.0",| - "body"=> ...Markdown...| - "This is one of the biggest updates of _fastlane_ yet"| - }| - ```| - >| - */ -public func getGithubRelease(url: String, - serverUrl: String = "https://api.github.com", - version: String, - apiToken: OptionalConfigValue = .fastlaneDefault(nil), - apiBearer: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let urlArg = RubyCommand.Argument(name: "url", value: url, type: nil) - let serverUrlArg = RubyCommand.Argument(name: "server_url", value: serverUrl, type: nil) - let versionArg = RubyCommand.Argument(name: "version", value: version, type: nil) - let apiTokenArg = apiToken.asRubyArgument(name: "api_token", type: nil) - let apiBearerArg = apiBearer.asRubyArgument(name: "api_bearer", type: nil) - let array: [RubyCommand.Argument?] = [urlArg, - serverUrlArg, - versionArg, - apiTokenArg, - apiBearerArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_github_release", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Returns value from Info.plist of your project as native Ruby data structures - - - parameters: - - key: Name of parameter - - path: Path to plist file you want to read - - Get a value from a plist file, which can be used to fetch the app identifier and more information about your app - */ -@discardableResult public func getInfoPlistValue(key: String, - path: String) -> String -{ - let keyArg = RubyCommand.Argument(name: "key", value: key, type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let array: [RubyCommand.Argument?] = [keyArg, - pathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_info_plist_value", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Returns a value from Info.plist inside a .ipa file - - - parameters: - - key: Name of parameter - - ipa: Path to IPA - - - returns: Returns the value in the .ipa's Info.plist corresponding to the passed in Key - - This is useful for introspecting Info.plist files for `.ipa` files that have already been built. - */ -@discardableResult public func getIpaInfoPlistValue(key: String, - ipa: String) -> String -{ - let keyArg = RubyCommand.Argument(name: "key", value: key, type: nil) - let ipaArg = RubyCommand.Argument(name: "ipa", value: ipa, type: nil) - let array: [RubyCommand.Argument?] = [keyArg, - ipaArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_ipa_info_plist_value", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Obtain publishing rights for custom apps on Managed Google Play Store - - - parameters: - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - - returns: An URI to obtain publishing rights for custom apps on Managed Play Store - - If you haven't done so before, start by following the first two steps of Googles ["Get started with custom app publishing"](https://developers.google.com/android/work/play/custom-app-api/get-started) -> ["Preliminary setup"](https://developers.google.com/android/work/play/custom-app-api/get-started#preliminary_setup) instructions: - "[Enable the Google Play Custom App Publishing API](https://developers.google.com/android/work/play/custom-app-api/get-started#enable_the_google_play_custom_app_publishing_api)" and "[Create a service account](https://developers.google.com/android/work/play/custom-app-api/get-started#create_a_service_account)". - You need the "service account's private key file" to continue. - Run the action and supply the "private key file" to it as the `json_key` parameter. The command will output a URL to visit. After logging in you are redirected to a page that outputs your "Developer Account ID" - take note of that, you will need it to be able to use [`create_app_on_managed_play_store`](https://docs.fastlane.tools/actions/create_app_on_managed_play_store/). - */ -public func getManagedPlayStorePublishingRights(jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let array: [RubyCommand.Argument?] = [jsonKeyArg, - jsonKeyDataArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_managed_play_store_publishing_rights", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generates a provisioning profile, saving it in the current folder (via _sigh_) - - - parameters: - - adhoc: Setting this flag will generate AdHoc profiles instead of App Store Profiles - - developerId: Setting this flag will generate Developer ID profiles instead of App Store Profiles - - development: Renew the development certificate instead of the production one - - skipInstall: By default, the certificate will be added to your local machine. Setting this flag will skip this action - - force: Renew provisioning profiles regardless of its state - to automatically add all devices for ad hoc profiles - - includeMacInProfiles: Include Apple Silicon Mac devices in provisioning profiles for iOS/iPadOS apps - - appIdentifier: The bundle identifier of your app - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - provisioningName: The name of the profile that is used on the Apple Developer Portal - - ignoreProfilesWithDifferentName: Use in combination with :provisioning_name - when true only profiles matching this exact name will be downloaded - - outputPath: Directory in which the profile should be stored - - certId: The ID of the code signing certificate to use (e.g. 78ADL6LVAA) - - certOwnerName: The certificate name to use for new profiles, or to renew with. (e.g. "Felix Krause") - - filename: Filename to use for the generated provisioning profile (must include .mobileprovision) - - skipFetchProfiles: Skips the verification of existing profiles which is useful if you have thousands of profiles - - includeAllCertificates: Include all matching certificates in the provisioning profile. Works only for the 'development' provisioning profile type - - skipCertificateVerification: Skips the verification of the certificates for every existing profiles. This will make sure the provisioning profile can be used on the local machine - - platform: Set the provisioning profile's platform (i.e. ios, tvos, macos, catalyst) - - readonly: Only fetch existing profile, don't generate new ones - - templateName: **DEPRECATED!** Removed since May 2025 on App Store Connect API OpenAPI v3.8.0 - Learn more: https://docs.fastlane.tools/actions/match/#managed-capabilities - The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. "Apple Pay Pass Suppression Development") - - failOnNameTaken: Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first - - cachedCertificates: A list of cached certificates - - cachedDevices: A list of cached devices - - cachedBundleIds: A list of cached bundle ids - - cachedProfiles: A list of cached bundle ids - - - returns: The UUID of the profile sigh just fetched/generated - - **Note**: It is recommended to use [match](https://docs.fastlane.tools/actions/match/) according to the [codesigning.guide](https://codesigning.guide) for generating and maintaining your provisioning profiles. Use _sigh_ directly only if you want full control over what's going on and know more about codesigning. - */ -@discardableResult public func getProvisioningProfile(adhoc: OptionalConfigValue = .fastlaneDefault(false), - developerId: OptionalConfigValue = .fastlaneDefault(false), - development: OptionalConfigValue = .fastlaneDefault(false), - skipInstall: OptionalConfigValue = .fastlaneDefault(false), - force: OptionalConfigValue = .fastlaneDefault(false), - includeMacInProfiles: OptionalConfigValue = .fastlaneDefault(false), - appIdentifier: String, - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - provisioningName: OptionalConfigValue = .fastlaneDefault(nil), - ignoreProfilesWithDifferentName: OptionalConfigValue = .fastlaneDefault(false), - outputPath: String = ".", - certId: OptionalConfigValue = .fastlaneDefault(nil), - certOwnerName: OptionalConfigValue = .fastlaneDefault(nil), - filename: OptionalConfigValue = .fastlaneDefault(nil), - skipFetchProfiles: OptionalConfigValue = .fastlaneDefault(false), - includeAllCertificates: OptionalConfigValue = .fastlaneDefault(false), - skipCertificateVerification: OptionalConfigValue = .fastlaneDefault(false), - platform: Any = "ios", - readonly: OptionalConfigValue = .fastlaneDefault(false), - templateName: OptionalConfigValue = .fastlaneDefault(nil), - failOnNameTaken: OptionalConfigValue = .fastlaneDefault(false), - cachedCertificates: Any? = nil, - cachedDevices: Any? = nil, - cachedBundleIds: Any? = nil, - cachedProfiles: Any? = nil) -> String -{ - let adhocArg = adhoc.asRubyArgument(name: "adhoc", type: nil) - let developerIdArg = developerId.asRubyArgument(name: "developer_id", type: nil) - let developmentArg = development.asRubyArgument(name: "development", type: nil) - let skipInstallArg = skipInstall.asRubyArgument(name: "skip_install", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let includeMacInProfilesArg = includeMacInProfiles.asRubyArgument(name: "include_mac_in_profiles", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let provisioningNameArg = provisioningName.asRubyArgument(name: "provisioning_name", type: nil) - let ignoreProfilesWithDifferentNameArg = ignoreProfilesWithDifferentName.asRubyArgument(name: "ignore_profiles_with_different_name", type: nil) - let outputPathArg = RubyCommand.Argument(name: "output_path", value: outputPath, type: nil) - let certIdArg = certId.asRubyArgument(name: "cert_id", type: nil) - let certOwnerNameArg = certOwnerName.asRubyArgument(name: "cert_owner_name", type: nil) - let filenameArg = filename.asRubyArgument(name: "filename", type: nil) - let skipFetchProfilesArg = skipFetchProfiles.asRubyArgument(name: "skip_fetch_profiles", type: nil) - let includeAllCertificatesArg = includeAllCertificates.asRubyArgument(name: "include_all_certificates", type: nil) - let skipCertificateVerificationArg = skipCertificateVerification.asRubyArgument(name: "skip_certificate_verification", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let readonlyArg = readonly.asRubyArgument(name: "readonly", type: nil) - let templateNameArg = templateName.asRubyArgument(name: "template_name", type: nil) - let failOnNameTakenArg = failOnNameTaken.asRubyArgument(name: "fail_on_name_taken", type: nil) - let cachedCertificatesArg = RubyCommand.Argument(name: "cached_certificates", value: cachedCertificates, type: nil) - let cachedDevicesArg = RubyCommand.Argument(name: "cached_devices", value: cachedDevices, type: nil) - let cachedBundleIdsArg = RubyCommand.Argument(name: "cached_bundle_ids", value: cachedBundleIds, type: nil) - let cachedProfilesArg = RubyCommand.Argument(name: "cached_profiles", value: cachedProfiles, type: nil) - let array: [RubyCommand.Argument?] = [adhocArg, - developerIdArg, - developmentArg, - skipInstallArg, - forceArg, - includeMacInProfilesArg, - appIdentifierArg, - apiKeyPathArg, - apiKeyArg, - usernameArg, - teamIdArg, - teamNameArg, - provisioningNameArg, - ignoreProfilesWithDifferentNameArg, - outputPathArg, - certIdArg, - certOwnerNameArg, - filenameArg, - skipFetchProfilesArg, - includeAllCertificatesArg, - skipCertificateVerificationArg, - platformArg, - readonlyArg, - templateNameArg, - failOnNameTakenArg, - cachedCertificatesArg, - cachedDevicesArg, - cachedBundleIdsArg, - cachedProfilesArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_provisioning_profile", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Ensure a valid push profile is active, creating a new one if needed (via _pem_) - - - parameters: - - platform: Set certificate's platform. Used for creation of production & development certificates. Supported platforms: ios, macos - - development: Renew the development push certificate instead of the production one - - websitePush: Create a Website Push certificate - - generateP12: Generate a p12 file additionally to a PEM file - - activeDaysLimit: If the current certificate is active for less than this number of days, generate a new one - - force: Create a new push certificate, even if the current one is active for 30 (or PEM_ACTIVE_DAYS_LIMIT) more days - - savePrivateKey: Set to save the private RSA key - - appIdentifier: The bundle identifier of your app - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - p12Password: The password that is used for your p12 file - - pemName: The file name of the generated .pem file - - outputPath: The path to a directory in which all certificates and private keys should be stored - - newProfile: Block that is called if there is a new profile - - Additionally to the available options, you can also specify a block that only gets executed if a new profile was created. You can use it to upload the new profile to your server. - Use it like this:| - | - ```ruby| - get_push_certificate(| - new_profile: proc do| - # your upload code| - end| - )| - ```| - >| - */ -public func getPushCertificate(platform: String = "ios", - development: OptionalConfigValue = .fastlaneDefault(false), - websitePush: OptionalConfigValue = .fastlaneDefault(false), - generateP12: OptionalConfigValue = .fastlaneDefault(true), - activeDaysLimit: Int = 30, - force: OptionalConfigValue = .fastlaneDefault(false), - savePrivateKey: OptionalConfigValue = .fastlaneDefault(true), - appIdentifier: String, - username: String, - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - p12Password: OptionalConfigValue = .fastlaneDefault(nil), - pemName: OptionalConfigValue = .fastlaneDefault(nil), - outputPath: String = ".", - newProfile: ((String) -> Void)? = nil) -{ - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let developmentArg = development.asRubyArgument(name: "development", type: nil) - let websitePushArg = websitePush.asRubyArgument(name: "website_push", type: nil) - let generateP12Arg = generateP12.asRubyArgument(name: "generate_p12", type: nil) - let activeDaysLimitArg = RubyCommand.Argument(name: "active_days_limit", value: activeDaysLimit, type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let savePrivateKeyArg = savePrivateKey.asRubyArgument(name: "save_private_key", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let p12PasswordArg = p12Password.asRubyArgument(name: "p12_password", type: nil) - let pemNameArg = pemName.asRubyArgument(name: "pem_name", type: nil) - let outputPathArg = RubyCommand.Argument(name: "output_path", value: outputPath, type: nil) - let newProfileArg = RubyCommand.Argument(name: "new_profile", value: newProfile, type: .stringClosure) - let array: [RubyCommand.Argument?] = [platformArg, - developmentArg, - websitePushArg, - generateP12Arg, - activeDaysLimitArg, - forceArg, - savePrivateKeyArg, - appIdentifierArg, - usernameArg, - teamIdArg, - teamNameArg, - p12PasswordArg, - pemNameArg, - outputPathArg, - newProfileArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_push_certificate", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Get the version number of your project - - - parameters: - - xcodeproj: Path to the Xcode project to read version number from, or its containing directory, optional. If omitted, or if a directory is passed instead, it will use the first Xcode project found within the given directory, or the project root directory if none is passed - - target: Target name, optional. Will be needed if you have more than one non-test target to avoid being prompted to select one - - configuration: Configuration name, optional. Will be needed if you have altered the configurations from the default or your version number depends on the configuration selected - - This action will return the current version number set on your project. It first looks in the plist and then for '$(MARKETING_VERSION)' in the build settings. - */ -@discardableResult public func getVersionNumber(xcodeproj: OptionalConfigValue = .fastlaneDefault(nil), - target: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let targetArg = target.asRubyArgument(name: "target", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let array: [RubyCommand.Argument?] = [xcodeprojArg, - targetArg, - configurationArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "get_version_number", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Directly add the given file or all files - - - parameters: - - path: The file(s) and path(s) you want to add - - shellEscape: Shell escapes paths (set to false if using wildcards or manually escaping spaces in :path) - - force: Allow adding otherwise ignored files - - pathspec: **DEPRECATED!** Use `--path` instead - The pathspec you want to add files from - */ -public func gitAdd(path: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - shellEscape: OptionalConfigValue = .fastlaneDefault(true), - force: OptionalConfigValue = .fastlaneDefault(false), - pathspec: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let pathArg = path.asRubyArgument(name: "path", type: nil) - let shellEscapeArg = shellEscape.asRubyArgument(name: "shell_escape", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let pathspecArg = pathspec.asRubyArgument(name: "pathspec", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - shellEscapeArg, - forceArg, - pathspecArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "git_add", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Returns the name of the current git branch, possibly as managed by CI ENV vars - - If no branch could be found, this action will return an empty string. If `FL_GIT_BRANCH_DONT_USE_ENV_VARS` is `true`, it'll ignore CI ENV vars. This is a wrapper for the internal action Actions.git_branch - */ -@discardableResult public func gitBranch() -> String { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "git_branch", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Directly commit the given file with the given message - - - parameters: - - path: The file(s) or directory(ies) you want to commit. You can pass an array of multiple file-paths or fileglobs "*.txt" to commit all matching files. The files already staged but not specified and untracked files won't be committed - - message: The commit message that should be used - - skipGitHooks: Set to true to pass `--no-verify` to git - - allowNothingToCommit: Set to true to allow commit without any git changes in the files you want to commit - */ -public func gitCommit(path: [String], - message: String, - skipGitHooks: OptionalConfigValue = .fastlaneDefault(false), - allowNothingToCommit: OptionalConfigValue = .fastlaneDefault(false)) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let skipGitHooksArg = skipGitHooks.asRubyArgument(name: "skip_git_hooks", type: nil) - let allowNothingToCommitArg = allowNothingToCommit.asRubyArgument(name: "allow_nothing_to_commit", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - messageArg, - skipGitHooksArg, - allowNothingToCommitArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "git_commit", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Executes a simple git pull command - - - parameters: - - onlyTags: Simply pull the tags, and not bring new commits to the current branch from the remote - - rebase: Rebase on top of the remote branch instead of merge - */ -public func gitPull(onlyTags: OptionalConfigValue = .fastlaneDefault(false), - rebase: OptionalConfigValue = .fastlaneDefault(false)) -{ - let onlyTagsArg = onlyTags.asRubyArgument(name: "only_tags", type: nil) - let rebaseArg = rebase.asRubyArgument(name: "rebase", type: nil) - let array: [RubyCommand.Argument?] = [onlyTagsArg, - rebaseArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "git_pull", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Returns the name of the current git remote default branch - - - parameter remoteName: The remote repository to check - - If no default remote branch could be found, this action will return nil. This is a wrapper for the internal action Actions.git_default_remote_branch_name - */ -@discardableResult public func gitRemoteBranch(remoteName: OptionalConfigValue = .fastlaneDefault(nil)) -> String { - let remoteNameArg = remoteName.asRubyArgument(name: "remote_name", type: nil) - let array: [RubyCommand.Argument?] = [remoteNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "git_remote_branch", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Executes a git submodule update command - - - parameters: - - recursive: Should the submodules be updated recursively? - - init: Should the submodules be initiated before update? - */ -public func gitSubmoduleUpdate(recursive: OptionalConfigValue = .fastlaneDefault(false), - init: OptionalConfigValue = .fastlaneDefault(false)) -{ - let recursiveArg = recursive.asRubyArgument(name: "recursive", type: nil) - let initArg = `init`.asRubyArgument(name: "init", type: nil) - let array: [RubyCommand.Argument?] = [recursiveArg, - initArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "git_submodule_update", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Checks if the git tag with the given name exists in the current repo - - - parameters: - - tag: The tag name that should be checked - - remote: Whether to check remote. Defaults to `false` - - remoteName: The remote to check. Defaults to `origin` - - - returns: Boolean value whether the tag exists or not - */ -@discardableResult public func gitTagExists(tag: String, - remote: OptionalConfigValue = .fastlaneDefault(false), - remoteName: String = "origin") -> Bool -{ - let tagArg = RubyCommand.Argument(name: "tag", value: tag, type: nil) - let remoteArg = remote.asRubyArgument(name: "remote", type: nil) - let remoteNameArg = RubyCommand.Argument(name: "remote_name", value: remoteName, type: nil) - let array: [RubyCommand.Argument?] = [tagArg, - remoteArg, - remoteNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "git_tag_exists", className: nil, args: args) - return parseBool(fromString: runner.executeCommand(command)) -} - -/** - Call a GitHub API endpoint and get the resulting JSON response - - - parameters: - - serverUrl: The server url. e.g. 'https://your.internal.github.host/api/v3' (Default: 'https://api.github.com') - - apiToken: Personal API Token for GitHub - generate one at https://github.com/settings/tokens - - apiBearer: Use a Bearer authorization token. Usually generated by GitHub Apps, e.g. GitHub Actions GITHUB_TOKEN environment variable - - httpMethod: The HTTP method. e.g. GET / POST - - body: The request body in JSON or hash format - - rawBody: The request body taken verbatim instead of as JSON, useful for file uploads - - path: The endpoint path. e.g. '/repos/:owner/:repo/readme' - - url: The complete full url - used instead of path. e.g. 'https://uploads.github.com/repos/fastlane...' - - errorHandlers: Optional error handling hash based on status code, or pass '*' to handle all errors - - headers: Optional headers to apply - - secure: Optionally disable secure requests (ssl_verify_peer) - - - returns: A hash including the HTTP status code (:status), the response body (:body), and if valid JSON has been returned the parsed JSON (:json). - - Calls any GitHub API endpoint. You must provide your GitHub Personal token (get one from [https://github.com/settings/tokens/new](https://github.com/settings/tokens/new)). - Out parameters provide the status code and the full response JSON if valid, otherwise the raw response body. - Documentation: [https://developer.github.com/v3](https://developer.github.com/v3). - */ -public func githubApi(serverUrl: String = "https://api.github.com", - apiToken: OptionalConfigValue = .fastlaneDefault(nil), - apiBearer: OptionalConfigValue = .fastlaneDefault(nil), - httpMethod: String = "GET", - body: [String: Any] = [:], - rawBody: OptionalConfigValue = .fastlaneDefault(nil), - path: OptionalConfigValue = .fastlaneDefault(nil), - url: OptionalConfigValue = .fastlaneDefault(nil), - errorHandlers: [String: Any] = [:], - headers: [String: Any] = [:], - secure: OptionalConfigValue = .fastlaneDefault(true)) -{ - let serverUrlArg = RubyCommand.Argument(name: "server_url", value: serverUrl, type: nil) - let apiTokenArg = apiToken.asRubyArgument(name: "api_token", type: nil) - let apiBearerArg = apiBearer.asRubyArgument(name: "api_bearer", type: nil) - let httpMethodArg = RubyCommand.Argument(name: "http_method", value: httpMethod, type: nil) - let bodyArg = RubyCommand.Argument(name: "body", value: body, type: nil) - let rawBodyArg = rawBody.asRubyArgument(name: "raw_body", type: nil) - let pathArg = path.asRubyArgument(name: "path", type: nil) - let urlArg = url.asRubyArgument(name: "url", type: nil) - let errorHandlersArg = RubyCommand.Argument(name: "error_handlers", value: errorHandlers, type: nil) - let headersArg = RubyCommand.Argument(name: "headers", value: headers, type: nil) - let secureArg = secure.asRubyArgument(name: "secure", type: nil) - let array: [RubyCommand.Argument?] = [serverUrlArg, - apiTokenArg, - apiBearerArg, - httpMethodArg, - bodyArg, - rawBodyArg, - pathArg, - urlArg, - errorHandlersArg, - headersArg, - secureArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "github_api", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Retrieves release names for a Google Play track - - - parameters: - - packageName: The package name of the application to use - - track: The track of the application to use. The default available tracks are: production, beta, alpha, internal - - key: **DEPRECATED!** Use `--json_key` instead - The p12 File used to authenticate with Google - - issuer: **DEPRECATED!** Use `--json_key` instead - The issuer of the p12 file (email address of the service account) - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - timeout: Timeout for read, open, and send (in seconds) - - - returns: Array of strings representing the release names for the given Google Play track - - More information: [https://docs.fastlane.tools/actions/supply/](https://docs.fastlane.tools/actions/supply/) - */ -public func googlePlayTrackReleaseNames(packageName: String, - track: String = "production", - key: OptionalConfigValue = .fastlaneDefault(nil), - issuer: OptionalConfigValue = .fastlaneDefault(nil), - jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 300) -{ - let packageNameArg = RubyCommand.Argument(name: "package_name", value: packageName, type: nil) - let trackArg = RubyCommand.Argument(name: "track", value: track, type: nil) - let keyArg = key.asRubyArgument(name: "key", type: nil) - let issuerArg = issuer.asRubyArgument(name: "issuer", type: nil) - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let array: [RubyCommand.Argument?] = [packageNameArg, - trackArg, - keyArg, - issuerArg, - jsonKeyArg, - jsonKeyDataArg, - rootUrlArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "google_play_track_release_names", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Retrieves version codes for a Google Play track - - - parameters: - - packageName: The package name of the application to use - - track: The track of the application to use. The default available tracks are: production, beta, alpha, internal - - key: **DEPRECATED!** Use `--json_key` instead - The p12 File used to authenticate with Google - - issuer: **DEPRECATED!** Use `--json_key` instead - The issuer of the p12 file (email address of the service account) - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - timeout: Timeout for read, open, and send (in seconds) - - - returns: Array of integers representing the version codes for the given Google Play track - - More information: [https://docs.fastlane.tools/actions/supply/](https://docs.fastlane.tools/actions/supply/) - */ -public func googlePlayTrackVersionCodes(packageName: String, - track: String = "production", - key: OptionalConfigValue = .fastlaneDefault(nil), - issuer: OptionalConfigValue = .fastlaneDefault(nil), - jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 300) -{ - let packageNameArg = RubyCommand.Argument(name: "package_name", value: packageName, type: nil) - let trackArg = RubyCommand.Argument(name: "track", value: track, type: nil) - let keyArg = key.asRubyArgument(name: "key", type: nil) - let issuerArg = issuer.asRubyArgument(name: "issuer", type: nil) - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let array: [RubyCommand.Argument?] = [packageNameArg, - trackArg, - keyArg, - issuerArg, - jsonKeyArg, - jsonKeyDataArg, - rootUrlArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "google_play_track_version_codes", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - All gradle related actions, including building and testing your Android app - - - parameters: - - task: The gradle task you want to execute, e.g. `assemble`, `bundle` or `test`. For tasks such as `assembleMyFlavorRelease` you should use gradle(task: 'assemble', flavor: 'Myflavor', build_type: 'Release') - - flavor: The flavor that you want the task for, e.g. `MyFlavor`. If you are running the `assemble` task in a multi-flavor project, and you rely on Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] then you must specify a flavor here or else this value will be undefined - - buildType: The build type that you want the task for, e.g. `Release`. Useful for some tasks such as `assemble` - - tasks: The multiple gradle tasks that you want to execute, e.g. `[assembleDebug, bundleDebug]` - - flags: All parameter flags you want to pass to the gradle command, e.g. `--exitcode --xml file.xml` - - projectDir: The root directory of the gradle project - - gradlePath: The path to your `gradlew`. If you specify a relative path, it is assumed to be relative to the `project_dir` - - properties: Gradle properties to be exposed to the gradle script - - systemProperties: Gradle system properties to be exposed to the gradle script - - serial: Android serial, which device should be used for this command - - printCommand: Control whether the generated Gradle command is printed as output before running it (true/false) - - printCommandOutput: Control whether the output produced by given Gradle command is printed while running (true/false) - - - returns: The output of running the gradle task - - Run `./gradlew tasks` to get a list of all available gradle tasks for your project - */ -public func gradle(task: OptionalConfigValue = .fastlaneDefault(nil), - flavor: OptionalConfigValue = .fastlaneDefault(nil), - buildType: OptionalConfigValue = .fastlaneDefault(nil), - tasks: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - flags: OptionalConfigValue = .fastlaneDefault(nil), - projectDir: String = ".", - gradlePath: OptionalConfigValue = .fastlaneDefault(nil), - properties: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - systemProperties: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - serial: String = "", - printCommand: OptionalConfigValue = .fastlaneDefault(true), - printCommandOutput: OptionalConfigValue = .fastlaneDefault(true)) -{ - let taskArg = task.asRubyArgument(name: "task", type: nil) - let flavorArg = flavor.asRubyArgument(name: "flavor", type: nil) - let buildTypeArg = buildType.asRubyArgument(name: "build_type", type: nil) - let tasksArg = tasks.asRubyArgument(name: "tasks", type: nil) - let flagsArg = flags.asRubyArgument(name: "flags", type: nil) - let projectDirArg = RubyCommand.Argument(name: "project_dir", value: projectDir, type: nil) - let gradlePathArg = gradlePath.asRubyArgument(name: "gradle_path", type: nil) - let propertiesArg = properties.asRubyArgument(name: "properties", type: nil) - let systemPropertiesArg = systemProperties.asRubyArgument(name: "system_properties", type: nil) - let serialArg = RubyCommand.Argument(name: "serial", value: serial, type: nil) - let printCommandArg = printCommand.asRubyArgument(name: "print_command", type: nil) - let printCommandOutputArg = printCommandOutput.asRubyArgument(name: "print_command_output", type: nil) - let array: [RubyCommand.Argument?] = [taskArg, - flavorArg, - buildTypeArg, - tasksArg, - flagsArg, - projectDirArg, - gradlePathArg, - propertiesArg, - systemPropertiesArg, - serialArg, - printCommandArg, - printCommandOutputArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "gradle", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `build_app` action - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - scheme: The project's scheme. Make sure it's marked as `Shared` - - clean: Should the project be cleaned before building it? - - outputDirectory: The directory in which the ipa file should be stored in - - outputName: The name of the resulting ipa file - - appName: App name to use in logfile name - - configuration: The configuration to use when building the app. Defaults to 'Release' - - silent: Hide all information that's not necessary while building - - codesigningIdentity: The name of the code signing identity to use. It has to match the name exactly. e.g. 'iPhone Distribution: SunApps GmbH' - - skipPackageIpa: Should we skip packaging the ipa? - - skipPackagePkg: Should we skip packaging the pkg? - - includeSymbols: Should the ipa file include symbols? - - includeBitcode: Should the ipa file include bitcode? - - exportMethod: Method used to export the archive. Valid values are: app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application - - exportOptions: Path to an export options plist or a hash with export options. Use 'xcodebuild -help' to print the full set of available options - - exportXcargs: Pass additional arguments to xcodebuild for the package phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - skipBuildArchive: Export ipa from previously built xcarchive. Uses archive_path as source - - skipArchive: After building, don't archive, effectively not including -archivePath param - - skipCodesigning: Build without codesigning - - catalystPlatform: Platform to build when using a Catalyst enabled app. Valid values are: ios, macos - - installerCertName: Full name of 3rd Party Mac Developer Installer or Developer ID Installer certificate. Example: `3rd Party Mac Developer Installer: Your Company (ABC1234XWYZ)` - - buildPath: The directory in which the archive should be stored in - - archivePath: The path to the created archive - - derivedDataPath: The directory where built products and other derived data will go - - resultBundle: Should an Xcode result bundle be generated in the output directory - - resultBundlePath: Path to the result bundle directory to create. Ignored if `result_bundle` if false - - buildlogPath: The directory where to store the build log - - sdk: The SDK that should be used for building the application - - toolchain: The toolchain that should be used for building the application (e.g. com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a) - - destination: Use a custom destination for building the app - - exportTeamId: Optional: Sometimes you need to specify a team id when exporting the ipa file - - xcargs: Pass additional arguments to xcodebuild for the build phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - buildTimingSummary: Create a build timing summary - - disableXcpretty: **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Disable xcpretty formatting of build output - - xcprettyTestFormat: Use the test (RSpec style) format for build output - - xcprettyFormatter: A custom xcpretty formatter to use - - xcprettyReportJunit: Have xcpretty create a JUnit-style XML report at the provided path - - xcprettyReportHtml: Have xcpretty create a simple HTML report at the provided path - - xcprettyReportJson: Have xcpretty create a JSON compilation database at the provided path - - xcprettyUtf: Have xcpretty use unicode encoding when reporting builds - - analyzeBuildTime: Analyze the project build time and store the output in 'culprits.txt' file - - skipProfileDetection: Do not try to build a profile mapping from the xcodeproj. Match or a manually provided mapping should be used - - xcodebuildCommand: Allows for override of the default `xcodebuild` command - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - useSystemScm: Lets xcodebuild use system's scm configuration - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - generateAppstoreInfo: Generate AppStoreInfo.plist using swinfo for app-store exports - - - returns: The absolute path to the generated ipa file - - More information: https://fastlane.tools/gym - */ -@discardableResult public func gym(workspace: OptionalConfigValue = .fastlaneDefault(gymfile.workspace), - project: OptionalConfigValue = .fastlaneDefault(gymfile.project), - scheme: OptionalConfigValue = .fastlaneDefault(gymfile.scheme), - clean: OptionalConfigValue = .fastlaneDefault(gymfile.clean), - outputDirectory: String = gymfile.outputDirectory, - outputName: OptionalConfigValue = .fastlaneDefault(gymfile.outputName), - appName: OptionalConfigValue = .fastlaneDefault(gymfile.appName), - configuration: OptionalConfigValue = .fastlaneDefault(gymfile.configuration), - silent: OptionalConfigValue = .fastlaneDefault(gymfile.silent), - codesigningIdentity: OptionalConfigValue = .fastlaneDefault(gymfile.codesigningIdentity), - skipPackageIpa: OptionalConfigValue = .fastlaneDefault(gymfile.skipPackageIpa), - skipPackagePkg: OptionalConfigValue = .fastlaneDefault(gymfile.skipPackagePkg), - includeSymbols: OptionalConfigValue = .fastlaneDefault(gymfile.includeSymbols), - includeBitcode: OptionalConfigValue = .fastlaneDefault(gymfile.includeBitcode), - exportMethod: OptionalConfigValue = .fastlaneDefault(gymfile.exportMethod), - exportOptions: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(gymfile.exportOptions), - exportXcargs: OptionalConfigValue = .fastlaneDefault(gymfile.exportXcargs), - skipBuildArchive: OptionalConfigValue = .fastlaneDefault(gymfile.skipBuildArchive), - skipArchive: OptionalConfigValue = .fastlaneDefault(gymfile.skipArchive), - skipCodesigning: OptionalConfigValue = .fastlaneDefault(gymfile.skipCodesigning), - catalystPlatform: OptionalConfigValue = .fastlaneDefault(gymfile.catalystPlatform), - installerCertName: OptionalConfigValue = .fastlaneDefault(gymfile.installerCertName), - buildPath: OptionalConfigValue = .fastlaneDefault(gymfile.buildPath), - archivePath: OptionalConfigValue = .fastlaneDefault(gymfile.archivePath), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(gymfile.derivedDataPath), - resultBundle: OptionalConfigValue = .fastlaneDefault(gymfile.resultBundle), - resultBundlePath: OptionalConfigValue = .fastlaneDefault(gymfile.resultBundlePath), - buildlogPath: String = gymfile.buildlogPath, - sdk: OptionalConfigValue = .fastlaneDefault(gymfile.sdk), - toolchain: OptionalConfigValue = .fastlaneDefault(gymfile.toolchain), - destination: OptionalConfigValue = .fastlaneDefault(gymfile.destination), - exportTeamId: OptionalConfigValue = .fastlaneDefault(gymfile.exportTeamId), - xcargs: OptionalConfigValue = .fastlaneDefault(gymfile.xcargs), - xcconfig: OptionalConfigValue = .fastlaneDefault(gymfile.xcconfig), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(gymfile.suppressXcodeOutput), - xcodebuildFormatter: String = gymfile.xcodebuildFormatter, - buildTimingSummary: OptionalConfigValue = .fastlaneDefault(gymfile.buildTimingSummary), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(gymfile.disableXcpretty), - xcprettyTestFormat: OptionalConfigValue = .fastlaneDefault(gymfile.xcprettyTestFormat), - xcprettyFormatter: OptionalConfigValue = .fastlaneDefault(gymfile.xcprettyFormatter), - xcprettyReportJunit: OptionalConfigValue = .fastlaneDefault(gymfile.xcprettyReportJunit), - xcprettyReportHtml: OptionalConfigValue = .fastlaneDefault(gymfile.xcprettyReportHtml), - xcprettyReportJson: OptionalConfigValue = .fastlaneDefault(gymfile.xcprettyReportJson), - xcprettyUtf: OptionalConfigValue = .fastlaneDefault(gymfile.xcprettyUtf), - analyzeBuildTime: OptionalConfigValue = .fastlaneDefault(gymfile.analyzeBuildTime), - skipProfileDetection: OptionalConfigValue = .fastlaneDefault(gymfile.skipProfileDetection), - xcodebuildCommand: String = gymfile.xcodebuildCommand, - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(gymfile.clonedSourcePackagesPath), - packageCachePath: OptionalConfigValue = .fastlaneDefault(gymfile.packageCachePath), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(gymfile.skipPackageDependenciesResolution), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(gymfile.disablePackageAutomaticUpdates), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(gymfile.skipPackageRepositoryFetches), - useSystemScm: OptionalConfigValue = .fastlaneDefault(gymfile.useSystemScm), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(gymfile.packageAuthorizationProvider), - generateAppstoreInfo: OptionalConfigValue = .fastlaneDefault(gymfile.generateAppstoreInfo)) -> String -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputNameArg = outputName.asRubyArgument(name: "output_name", type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let silentArg = silent.asRubyArgument(name: "silent", type: nil) - let codesigningIdentityArg = codesigningIdentity.asRubyArgument(name: "codesigning_identity", type: nil) - let skipPackageIpaArg = skipPackageIpa.asRubyArgument(name: "skip_package_ipa", type: nil) - let skipPackagePkgArg = skipPackagePkg.asRubyArgument(name: "skip_package_pkg", type: nil) - let includeSymbolsArg = includeSymbols.asRubyArgument(name: "include_symbols", type: nil) - let includeBitcodeArg = includeBitcode.asRubyArgument(name: "include_bitcode", type: nil) - let exportMethodArg = exportMethod.asRubyArgument(name: "export_method", type: nil) - let exportOptionsArg = exportOptions.asRubyArgument(name: "export_options", type: nil) - let exportXcargsArg = exportXcargs.asRubyArgument(name: "export_xcargs", type: nil) - let skipBuildArchiveArg = skipBuildArchive.asRubyArgument(name: "skip_build_archive", type: nil) - let skipArchiveArg = skipArchive.asRubyArgument(name: "skip_archive", type: nil) - let skipCodesigningArg = skipCodesigning.asRubyArgument(name: "skip_codesigning", type: nil) - let catalystPlatformArg = catalystPlatform.asRubyArgument(name: "catalyst_platform", type: nil) - let installerCertNameArg = installerCertName.asRubyArgument(name: "installer_cert_name", type: nil) - let buildPathArg = buildPath.asRubyArgument(name: "build_path", type: nil) - let archivePathArg = archivePath.asRubyArgument(name: "archive_path", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let resultBundlePathArg = resultBundlePath.asRubyArgument(name: "result_bundle_path", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let toolchainArg = toolchain.asRubyArgument(name: "toolchain", type: nil) - let destinationArg = destination.asRubyArgument(name: "destination", type: nil) - let exportTeamIdArg = exportTeamId.asRubyArgument(name: "export_team_id", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let buildTimingSummaryArg = buildTimingSummary.asRubyArgument(name: "build_timing_summary", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let xcprettyTestFormatArg = xcprettyTestFormat.asRubyArgument(name: "xcpretty_test_format", type: nil) - let xcprettyFormatterArg = xcprettyFormatter.asRubyArgument(name: "xcpretty_formatter", type: nil) - let xcprettyReportJunitArg = xcprettyReportJunit.asRubyArgument(name: "xcpretty_report_junit", type: nil) - let xcprettyReportHtmlArg = xcprettyReportHtml.asRubyArgument(name: "xcpretty_report_html", type: nil) - let xcprettyReportJsonArg = xcprettyReportJson.asRubyArgument(name: "xcpretty_report_json", type: nil) - let xcprettyUtfArg = xcprettyUtf.asRubyArgument(name: "xcpretty_utf", type: nil) - let analyzeBuildTimeArg = analyzeBuildTime.asRubyArgument(name: "analyze_build_time", type: nil) - let skipProfileDetectionArg = skipProfileDetection.asRubyArgument(name: "skip_profile_detection", type: nil) - let xcodebuildCommandArg = RubyCommand.Argument(name: "xcodebuild_command", value: xcodebuildCommand, type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let generateAppstoreInfoArg = generateAppstoreInfo.asRubyArgument(name: "generate_appstore_info", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - schemeArg, - cleanArg, - outputDirectoryArg, - outputNameArg, - appNameArg, - configurationArg, - silentArg, - codesigningIdentityArg, - skipPackageIpaArg, - skipPackagePkgArg, - includeSymbolsArg, - includeBitcodeArg, - exportMethodArg, - exportOptionsArg, - exportXcargsArg, - skipBuildArchiveArg, - skipArchiveArg, - skipCodesigningArg, - catalystPlatformArg, - installerCertNameArg, - buildPathArg, - archivePathArg, - derivedDataPathArg, - resultBundleArg, - resultBundlePathArg, - buildlogPathArg, - sdkArg, - toolchainArg, - destinationArg, - exportTeamIdArg, - xcargsArg, - xcconfigArg, - suppressXcodeOutputArg, - xcodebuildFormatterArg, - buildTimingSummaryArg, - disableXcprettyArg, - xcprettyTestFormatArg, - xcprettyFormatterArg, - xcprettyReportJunitArg, - xcprettyReportHtmlArg, - xcprettyReportJsonArg, - xcprettyUtfArg, - analyzeBuildTimeArg, - skipProfileDetectionArg, - xcodebuildCommandArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - useSystemScmArg, - packageAuthorizationProviderArg, - generateAppstoreInfoArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "gym", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - This will add a hg tag to the current branch - - - parameter tag: Tag to create - */ -public func hgAddTag(tag: String) { - let tagArg = RubyCommand.Argument(name: "tag", value: tag, type: nil) - let array: [RubyCommand.Argument?] = [tagArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "hg_add_tag", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will commit a version bump to the hg repo - - - parameters: - - message: The commit message when committing the version bump - - xcodeproj: The path to your project file (Not the workspace). If you have only one, this is optional - - force: Forces the commit, even if other files than the ones containing the version number have been modified - - testDirtyFiles: A list of dirty files passed in for testing - - testExpectedFiles: A list of expected changed files passed in for testing - - The mercurial equivalent of the [commit_version_bump](https://docs.fastlane.tools/actions/commit_version_bump/) git action. Like the git version, it is useful in conjunction with [`increment_build_number`](https://docs.fastlane.tools/actions/increment_build_number/). - It checks the repo to make sure that only the relevant files have changed, these are the files that `increment_build_number` (`agvtool`) touches:| - | - >- All `.plist` files| - - The `.xcodeproj/project.pbxproj` file| - >| - Then commits those files to the repo. - Customize the message with the `:message` option, defaults to 'Version Bump' - If you have other uncommitted changes in your repo, this action will fail. If you started off in a clean repo, and used the _ipa_ and or _sigh_ actions, then you can use the [clean_build_artifacts](https://docs.fastlane.tools/actions/clean_build_artifacts/) action to clean those temporary files up before running this action. - */ -public func hgCommitVersionBump(message: String = "Version Bump", - xcodeproj: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - testDirtyFiles: String = "file1, file2", - testExpectedFiles: String = "file1, file2") -{ - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let testDirtyFilesArg = RubyCommand.Argument(name: "test_dirty_files", value: testDirtyFiles, type: nil) - let testExpectedFilesArg = RubyCommand.Argument(name: "test_expected_files", value: testExpectedFiles, type: nil) - let array: [RubyCommand.Argument?] = [messageArg, - xcodeprojArg, - forceArg, - testDirtyFilesArg, - testExpectedFilesArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "hg_commit_version_bump", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Raises an exception if there are uncommitted hg changes - - Along the same lines as the [ensure_git_status_clean](https://docs.fastlane.tools/actions/ensure_git_status_clean/) action, this is a sanity check to ensure the working mercurial repo is clean. Especially useful to put at the beginning of your Fastfile in the `before_all` block. - */ -public func hgEnsureCleanStatus() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "hg_ensure_clean_status", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will push changes to the remote hg repository - - - parameters: - - force: Force push to remote - - destination: The destination to push to - - The mercurial equivalent of [push_to_git_remote](https://docs.fastlane.tools/actions/push_to_git_remote/). Pushes your local commits to a remote mercurial repo. Useful when local changes such as adding a version bump commit or adding a tag are part of your lane’s actions. - */ -public func hgPush(force: OptionalConfigValue = .fastlaneDefault(false), - destination: String = "") -{ - let forceArg = force.asRubyArgument(name: "force", type: nil) - let destinationArg = RubyCommand.Argument(name: "destination", value: destination, type: nil) - let array: [RubyCommand.Argument?] = [forceArg, - destinationArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "hg_push", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Refer to [App Center](https://github.com/Microsoft/fastlane-plugin-appcenter/) - - - parameters: - - apk: Path to your APK file - - apiToken: API Token for Hockey Access - - ipa: Path to your IPA file. Optional if you use the _gym_ or _xcodebuild_ action. For Mac zip the .app. For Android provide path to .apk file. In addition you could use this to upload .msi, .zip, .pkg, etc if you use the 'create_update' mechanism - - dsym: Path to your symbols file. For iOS and Mac provide path to app.dSYM.zip. For Android provide path to mappings.txt file - - createUpdate: Set true if you want to create then update your app as opposed to just upload it. You will need the 'public_identifier', 'bundle_version' and 'bundle_short_version' - - notes: Beta Notes - - notify: Notify testers? "1" for yes - - status: Download status: "1" = No user can download; "2" = Available for download (only possible with full-access token) - - createStatus: Download status for initial version creation when create_update is true: "1" = No user can download; "2" = Available for download (only possible with full-access token) - - notesType: Notes type for your :notes, "0" = Textile, "1" = Markdown (default) - - releaseType: Release type of the app: "0" = Beta (default), "1" = Store, "2" = Alpha, "3" = Enterprise - - mandatory: Set to "1" to make this update mandatory - - teams: Comma separated list of team ID numbers to which this build will be restricted - - users: Comma separated list of user ID numbers to which this build will be restricted - - tags: Comma separated list of tags which will receive access to the build - - bundleShortVersion: The bundle_short_version of your application, required when using `create_update` - - bundleVersion: The bundle_version of your application, required when using `create_update` - - publicIdentifier: App id of the app you are targeting, usually you won't need this value. Required, if `upload_dsym_only` set to `true` - - commitSha: The Git commit SHA for this build - - repositoryUrl: The URL of your source repository - - buildServerUrl: The URL of the build job on your build server - - uploadDsymOnly: Flag to upload only the dSYM file to hockey app - - ownerId: ID for the owner of the app - - strategy: Strategy: 'add' = to add the build as a new build even if it has the same build number (default); 'replace' = to replace a build with the same build number - - timeout: Request timeout in seconds - - bypassCdn: Flag to bypass Hockey CDN when it uploads successfully but reports error - - dsaSignature: DSA signature for sparkle updates for macOS - - HockeyApp will be no longer supported and will be transitioned into App Center on November 16, 2019. - Please migrate over to [App Center](https://github.com/Microsoft/fastlane-plugin-appcenter/) - - Symbols will also be uploaded automatically if a `app.dSYM.zip` file is found next to `app.ipa`. In case it is located in a different place you can specify the path explicitly in the `:dsym` parameter. - More information about the available options can be found in the [HockeyApp Docs](http://support.hockeyapp.net/kb/api/api-versions#upload-version). - */ -public func hockey(apk: OptionalConfigValue = .fastlaneDefault(nil), - apiToken: String, - ipa: OptionalConfigValue = .fastlaneDefault(nil), - dsym: OptionalConfigValue = .fastlaneDefault(nil), - createUpdate: OptionalConfigValue = .fastlaneDefault(false), - notes: String = "No changelog given", - notify: String = "1", - status: String = "2", - createStatus: String = "2", - notesType: String = "1", - releaseType: String = "0", - mandatory: String = "0", - teams: OptionalConfigValue = .fastlaneDefault(nil), - users: OptionalConfigValue = .fastlaneDefault(nil), - tags: OptionalConfigValue = .fastlaneDefault(nil), - bundleShortVersion: OptionalConfigValue = .fastlaneDefault(nil), - bundleVersion: OptionalConfigValue = .fastlaneDefault(nil), - publicIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - commitSha: OptionalConfigValue = .fastlaneDefault(nil), - repositoryUrl: OptionalConfigValue = .fastlaneDefault(nil), - buildServerUrl: OptionalConfigValue = .fastlaneDefault(nil), - uploadDsymOnly: OptionalConfigValue = .fastlaneDefault(false), - ownerId: OptionalConfigValue = .fastlaneDefault(nil), - strategy: String = "add", - timeout: OptionalConfigValue = .fastlaneDefault(nil), - bypassCdn: OptionalConfigValue = .fastlaneDefault(false), - dsaSignature: String = "") -{ - let apkArg = apk.asRubyArgument(name: "apk", type: nil) - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let dsymArg = dsym.asRubyArgument(name: "dsym", type: nil) - let createUpdateArg = createUpdate.asRubyArgument(name: "create_update", type: nil) - let notesArg = RubyCommand.Argument(name: "notes", value: notes, type: nil) - let notifyArg = RubyCommand.Argument(name: "notify", value: notify, type: nil) - let statusArg = RubyCommand.Argument(name: "status", value: status, type: nil) - let createStatusArg = RubyCommand.Argument(name: "create_status", value: createStatus, type: nil) - let notesTypeArg = RubyCommand.Argument(name: "notes_type", value: notesType, type: nil) - let releaseTypeArg = RubyCommand.Argument(name: "release_type", value: releaseType, type: nil) - let mandatoryArg = RubyCommand.Argument(name: "mandatory", value: mandatory, type: nil) - let teamsArg = teams.asRubyArgument(name: "teams", type: nil) - let usersArg = users.asRubyArgument(name: "users", type: nil) - let tagsArg = tags.asRubyArgument(name: "tags", type: nil) - let bundleShortVersionArg = bundleShortVersion.asRubyArgument(name: "bundle_short_version", type: nil) - let bundleVersionArg = bundleVersion.asRubyArgument(name: "bundle_version", type: nil) - let publicIdentifierArg = publicIdentifier.asRubyArgument(name: "public_identifier", type: nil) - let commitShaArg = commitSha.asRubyArgument(name: "commit_sha", type: nil) - let repositoryUrlArg = repositoryUrl.asRubyArgument(name: "repository_url", type: nil) - let buildServerUrlArg = buildServerUrl.asRubyArgument(name: "build_server_url", type: nil) - let uploadDsymOnlyArg = uploadDsymOnly.asRubyArgument(name: "upload_dsym_only", type: nil) - let ownerIdArg = ownerId.asRubyArgument(name: "owner_id", type: nil) - let strategyArg = RubyCommand.Argument(name: "strategy", value: strategy, type: nil) - let timeoutArg = timeout.asRubyArgument(name: "timeout", type: nil) - let bypassCdnArg = bypassCdn.asRubyArgument(name: "bypass_cdn", type: nil) - let dsaSignatureArg = RubyCommand.Argument(name: "dsa_signature", value: dsaSignature, type: nil) - let array: [RubyCommand.Argument?] = [apkArg, - apiTokenArg, - ipaArg, - dsymArg, - createUpdateArg, - notesArg, - notifyArg, - statusArg, - createStatusArg, - notesTypeArg, - releaseTypeArg, - mandatoryArg, - teamsArg, - usersArg, - tagsArg, - bundleShortVersionArg, - bundleVersionArg, - publicIdentifierArg, - commitShaArg, - repositoryUrlArg, - buildServerUrlArg, - uploadDsymOnlyArg, - ownerIdArg, - strategyArg, - timeoutArg, - bypassCdnArg, - dsaSignatureArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "hockey", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Connect to the [IFTTT Maker Channel](https://ifttt.com/maker) - - - parameters: - - apiKey: API key - - eventName: The name of the event that will be triggered - - value1: Extra data sent with the event - - value2: Extra data sent with the event - - value3: Extra data sent with the event - - Connect to the IFTTT [Maker Channel](https://ifttt.com/maker). An IFTTT Recipe has two components: a Trigger and an Action. In this case, the Trigger will fire every time the Maker Channel receives a web request (made by this _fastlane_ action) to notify it of an event. The Action can be anything that IFTTT supports: email, SMS, etc. - */ -public func ifttt(apiKey: String, - eventName: String, - value1: OptionalConfigValue = .fastlaneDefault(nil), - value2: OptionalConfigValue = .fastlaneDefault(nil), - value3: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let apiKeyArg = RubyCommand.Argument(name: "api_key", value: apiKey, type: nil) - let eventNameArg = RubyCommand.Argument(name: "event_name", value: eventName, type: nil) - let value1Arg = value1.asRubyArgument(name: "value1", type: nil) - let value2Arg = value2.asRubyArgument(name: "value2", type: nil) - let value3Arg = value3.asRubyArgument(name: "value3", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyArg, - eventNameArg, - value1Arg, - value2Arg, - value3Arg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ifttt", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Import certificate from inputfile into a keychain - - - parameters: - - certificatePath: Path to certificate - - certificatePassword: Certificate password - - certificateFormat: Format of the certificate. Check the '-f' switch from 'security import --help' command - - keychainName: Keychain the items should be imported to - - keychainPath: Path to the Keychain file to which the items should be imported - - keychainPassword: The password for the keychain. Note that for the login keychain this is your user's password - - logOutput: If output should be logged to the console - - Import certificates (and private keys) into the current default keychain. Use the `create_keychain` action to create a new keychain. - */ -public func importCertificate(certificatePath: String, - certificatePassword: OptionalConfigValue = .fastlaneDefault(nil), - certificateFormat: OptionalConfigValue = .fastlaneDefault(nil), - keychainName: String, - keychainPath: OptionalConfigValue = .fastlaneDefault(nil), - keychainPassword: OptionalConfigValue = .fastlaneDefault(nil), - logOutput: OptionalConfigValue = .fastlaneDefault(false)) -{ - let certificatePathArg = RubyCommand.Argument(name: "certificate_path", value: certificatePath, type: nil) - let certificatePasswordArg = certificatePassword.asRubyArgument(name: "certificate_password", type: nil) - let certificateFormatArg = certificateFormat.asRubyArgument(name: "certificate_format", type: nil) - let keychainNameArg = RubyCommand.Argument(name: "keychain_name", value: keychainName, type: nil) - let keychainPathArg = keychainPath.asRubyArgument(name: "keychain_path", type: nil) - let keychainPasswordArg = keychainPassword.asRubyArgument(name: "keychain_password", type: nil) - let logOutputArg = logOutput.asRubyArgument(name: "log_output", type: nil) - let array: [RubyCommand.Argument?] = [certificatePathArg, - certificatePasswordArg, - certificateFormatArg, - keychainNameArg, - keychainPathArg, - keychainPasswordArg, - logOutputArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "import_certificate", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Increment the build number of your project - - - parameters: - - buildNumber: Change to a specific version. When you provide this parameter, Apple Generic Versioning does not have to be enabled - - skipInfoPlist: Don't update Info.plist files when updating the build version - - xcodeproj: optional, you must specify the path to your main Xcode project if it is not in the project root directory - - - returns: The new build number - */ -@discardableResult public func incrementBuildNumber(buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - skipInfoPlist: OptionalConfigValue = .fastlaneDefault(false), - xcodeproj: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let skipInfoPlistArg = skipInfoPlist.asRubyArgument(name: "skip_info_plist", type: nil) - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let array: [RubyCommand.Argument?] = [buildNumberArg, - skipInfoPlistArg, - xcodeprojArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "increment_build_number", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Increment the version number of your project - - - parameters: - - bumpType: The type of this version bump. Available: patch, minor, major - - versionNumber: Change to a specific version. This will replace the bump type value - - xcodeproj: optional, you must specify the path to your main Xcode project if it is not in the project root directory - - - returns: The new version number - - This action will increment the version number. - You first have to set up your Xcode project, if you haven't done it already: [https://developer.apple.com/library/ios/qa/qa1827/_index.html](https://developer.apple.com/library/ios/qa/qa1827/_index.html). - */ -@discardableResult public func incrementVersionNumber(bumpType: String = "bump", - versionNumber: OptionalConfigValue = .fastlaneDefault(nil), - xcodeproj: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let bumpTypeArg = RubyCommand.Argument(name: "bump_type", value: bumpType, type: nil) - let versionNumberArg = versionNumber.asRubyArgument(name: "version_number", type: nil) - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let array: [RubyCommand.Argument?] = [bumpTypeArg, - versionNumberArg, - xcodeprojArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "increment_version_number", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Installs an .ipa file on a connected iOS-device via usb or wifi - - - parameters: - - extra: Extra Command-line arguments passed to ios-deploy - - deviceId: id of the device / if not set defaults to first found device - - skipWifi: Do not search for devices via WiFi - - ipa: The IPA file to put on the device - - Installs the ipa on the device. If no id is given, the first found iOS device will be used. Works via USB or Wi-Fi. This requires `ios-deploy` to be installed. Please have a look at [ios-deploy](https://github.com/ios-control/ios-deploy). To quickly install it, use `brew install ios-deploy` - */ -public func installOnDevice(extra: OptionalConfigValue = .fastlaneDefault(nil), - deviceId: OptionalConfigValue = .fastlaneDefault(nil), - skipWifi: OptionalConfigValue = .fastlaneDefault(nil), - ipa: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let extraArg = extra.asRubyArgument(name: "extra", type: nil) - let deviceIdArg = deviceId.asRubyArgument(name: "device_id", type: nil) - let skipWifiArg = skipWifi.asRubyArgument(name: "skip_wifi", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let array: [RubyCommand.Argument?] = [extraArg, - deviceIdArg, - skipWifiArg, - ipaArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "install_on_device", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Install provisioning profile from path - - - parameter path: Path to provisioning profile - - - returns: The absolute path to the installed provisioning profile - - Install provisioning profile from path for current user - */ -@discardableResult public func installProvisioningProfile(path: String) -> String { - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let array: [RubyCommand.Argument?] = [pathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "install_provisioning_profile", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Install an Xcode plugin for the current user - - - parameters: - - url: URL for Xcode plugin ZIP file - - github: GitHub repository URL for Xcode plugin - */ -public func installXcodePlugin(url: String, - github: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let urlArg = RubyCommand.Argument(name: "url", value: url, type: nil) - let githubArg = github.asRubyArgument(name: "github", type: nil) - let array: [RubyCommand.Argument?] = [urlArg, - githubArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "install_xcode_plugin", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload a new build to [Installr](http://installrapp.com/) - - - parameters: - - apiToken: API Token for Installr Access - - ipa: Path to your IPA file. Optional if you use the _gym_ or _xcodebuild_ action - - notes: Release notes - - notify: Groups to notify (e.g. 'dev,qa') - - add: Groups to add (e.g. 'exec,ops') - */ -public func installr(apiToken: String, - ipa: String, - notes: OptionalConfigValue = .fastlaneDefault(nil), - notify: OptionalConfigValue = .fastlaneDefault(nil), - add: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let ipaArg = RubyCommand.Argument(name: "ipa", value: ipa, type: nil) - let notesArg = notes.asRubyArgument(name: "notes", type: nil) - let notifyArg = notify.asRubyArgument(name: "notify", type: nil) - let addArg = add.asRubyArgument(name: "add", type: nil) - let array: [RubyCommand.Argument?] = [apiTokenArg, - ipaArg, - notesArg, - notifyArg, - addArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "installr", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Easily build and sign your app using shenzhen - - - parameters: - - workspace: WORKSPACE Workspace (.xcworkspace) file to use to build app (automatically detected in current directory) - - project: Project (.xcodeproj) file to use to build app (automatically detected in current directory, overridden by --workspace option, if passed) - - configuration: Configuration used to build - - scheme: Scheme used to build app - - clean: Clean project before building - - archive: Archive project after building - - destination: Build destination. Defaults to current directory - - embed: Sign .ipa file with .mobileprovision - - identity: Identity to be used along with --embed - - sdk: Use SDK as the name or path of the base SDK when building the project - - ipa: Specify the name of the .ipa file to generate (including file extension) - - xcconfig: Use an extra XCCONFIG file to build the app - - xcargs: Pass additional arguments to xcodebuild when building the app. Be sure to quote multiple args - */ -public func ipa(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - clean: OptionalConfigValue = .fastlaneDefault(nil), - archive: OptionalConfigValue = .fastlaneDefault(nil), - destination: OptionalConfigValue = .fastlaneDefault(nil), - embed: OptionalConfigValue = .fastlaneDefault(nil), - identity: OptionalConfigValue = .fastlaneDefault(nil), - sdk: OptionalConfigValue = .fastlaneDefault(nil), - ipa: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - xcargs: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let archiveArg = archive.asRubyArgument(name: "archive", type: nil) - let destinationArg = destination.asRubyArgument(name: "destination", type: nil) - let embedArg = embed.asRubyArgument(name: "embed", type: nil) - let identityArg = identity.asRubyArgument(name: "identity", type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - configurationArg, - schemeArg, - cleanArg, - archiveArg, - destinationArg, - embedArg, - identityArg, - sdkArg, - ipaArg, - xcconfigArg, - xcargsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ipa", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Is the current run being executed on a CI system, like Jenkins or Travis - - The return value of this method is true if fastlane is currently executed on Travis, Jenkins, Circle or a similar CI service - */ -@discardableResult public func isCi() -> Bool { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "is_ci", className: nil, args: args) - return parseBool(fromString: runner.executeCommand(command)) -} - -/** - Generate docs using Jazzy - - - parameters: - - config: Path to jazzy config file - - moduleVersion: Version string to use as part of the default docs title and inside the docset - */ -public func jazzy(config: OptionalConfigValue = .fastlaneDefault(nil), - moduleVersion: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let configArg = config.asRubyArgument(name: "config", type: nil) - let moduleVersionArg = moduleVersion.asRubyArgument(name: "module_version", type: nil) - let array: [RubyCommand.Argument?] = [configArg, - moduleVersionArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "jazzy", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Leave a comment on a Jira ticket - - - parameters: - - url: URL for Jira instance - - contextPath: Appends to the url (ex: "/jira") - - username: Username for Jira instance - - password: Password or API token for Jira - - ticketId: Ticket ID for Jira, i.e. IOS-123 - - commentText: Text to add to the ticket as a comment - - failOnError: Should an error adding the Jira comment cause a failure? - - - returns: A hash containing all relevant information of the Jira comment - Access Jira comment 'id', 'author', 'body', and more - */ -@discardableResult public func jira(url: String, - contextPath: String = "", - username: String, - password: String, - ticketId: String, - commentText: String, - failOnError: OptionalConfigValue = .fastlaneDefault(true)) -> [String: Any] -{ - let urlArg = RubyCommand.Argument(name: "url", value: url, type: nil) - let contextPathArg = RubyCommand.Argument(name: "context_path", value: contextPath, type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let passwordArg = RubyCommand.Argument(name: "password", value: password, type: nil) - let ticketIdArg = RubyCommand.Argument(name: "ticket_id", value: ticketId, type: nil) - let commentTextArg = RubyCommand.Argument(name: "comment_text", value: commentText, type: nil) - let failOnErrorArg = failOnError.asRubyArgument(name: "fail_on_error", type: nil) - let array: [RubyCommand.Argument?] = [urlArg, - contextPathArg, - usernameArg, - passwordArg, - ticketIdArg, - commentTextArg, - failOnErrorArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "jira", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Access lane context values - - Access the fastlane lane context values. - More information about how the lane context works: [https://docs.fastlane.tools/advanced/#lane-context](https://docs.fastlane.tools/advanced/#lane-context). - */ -@discardableResult public func laneContext() -> [String: Any] { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "lane_context", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Return last git commit hash, abbreviated commit hash, commit message and author - - - returns: Returns the following dict: {commit_hash: "commit hash", abbreviated_commit_hash: "abbreviated commit hash" author: "Author", author_email: "author email", message: "commit message"}. Example: {:message=>"message", :author=>"author", :author_email=>"author_email", :commit_hash=>"commit_hash", :abbreviated_commit_hash=>"short_hash"} - */ -@discardableResult public func lastGitCommit() -> [String: String] { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "last_git_commit", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Get the most recent git tag - - - parameter pattern: Pattern to filter tags when looking for last one. Limit tags to ones matching given shell glob. If pattern lacks ?, *, or [, * at the end is implied - - If you are using this action on a **shallow clone**, *the default with some CI systems like Bamboo*, you need to ensure that you have also pulled all the git tags appropriately. Assuming your git repo has the correct remote set you can issue `sh('git fetch --tags')`. - Pattern parameter allows you to filter to a subset of tags. - */ -@discardableResult public func lastGitTag(pattern: OptionalConfigValue = .fastlaneDefault(nil)) -> String { - let patternArg = pattern.asRubyArgument(name: "pattern", type: nil) - let array: [RubyCommand.Argument?] = [patternArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "last_git_tag", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Fetches most recent build number from TestFlight - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - live: Query the live version (ready-for-sale) - - appIdentifier: The bundle identifier of your app - - username: Your Apple ID Username - - version: The version number whose latest build number we want - - platform: The platform to use (optional) - - initialBuildNumber: sets the build number to given value if no build (upload) is in current train - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - - returns: Integer representation of the latest build number uploaded to TestFlight. Example: 2 - - Provides a way to have `increment_build_number` be based on the latest build you uploaded to iTC. - Fetches the most recent build number from TestFlight based on the version number. Provides a way to have `increment_build_number` be based on the latest build you uploaded to iTC. - */ -@discardableResult public func latestTestflightBuildNumber(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - live: OptionalConfigValue = .fastlaneDefault(false), - appIdentifier: String, - username: OptionalConfigValue = .fastlaneDefault(nil), - version: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - initialBuildNumber: Int = 1, - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil)) -> Int -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let liveArg = live.asRubyArgument(name: "live", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let versionArg = version.asRubyArgument(name: "version", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let initialBuildNumberArg = RubyCommand.Argument(name: "initial_build_number", value: initialBuildNumber, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - liveArg, - appIdentifierArg, - usernameArg, - versionArg, - platformArg, - initialBuildNumberArg, - teamIdArg, - teamNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "latest_testflight_build_number", className: nil, args: args) - return parseInt(fromString: runner.executeCommand(command)) -} - -/** - Generates coverage data using lcov - - - parameters: - - projectName: Name of the project - - scheme: Scheme of the project - - arch: The build arch where will search .gcda files - - outputDir: The output directory that coverage data will be stored. If not passed will use coverage_reports as default value - */ -public func lcov(projectName: String, - scheme: String, - arch: String = "i386", - outputDir: String = "coverage_reports") -{ - let projectNameArg = RubyCommand.Argument(name: "project_name", value: projectName, type: nil) - let schemeArg = RubyCommand.Argument(name: "scheme", value: scheme, type: nil) - let archArg = RubyCommand.Argument(name: "arch", value: arch, type: nil) - let outputDirArg = RubyCommand.Argument(name: "output_dir", value: outputDir, type: nil) - let array: [RubyCommand.Argument?] = [projectNameArg, - schemeArg, - archArg, - outputDirArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "lcov", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Send a success/error message to an email group - - - parameters: - - mailgunSandboxDomain: Mailgun sandbox domain postmaster for your mail. Please use postmaster instead - - mailgunSandboxPostmaster: Mailgun sandbox domain postmaster for your mail. Please use postmaster instead - - mailgunApikey: Mailgun apikey for your mail. Please use postmaster instead - - postmaster: Mailgun sandbox domain postmaster for your mail - - apikey: Mailgun apikey for your mail - - to: Destination of your mail - - from: Mailgun sender name - - message: Message of your mail - - subject: Subject of your mail - - success: Was this build successful? (true/false) - - appLink: App Release link - - ciBuildLink: CI Build Link - - templatePath: Mail HTML template - - replyTo: Mail Reply to - - attachment: Mail Attachment filenames, either an array or just one string - - customPlaceholders: Placeholders for template given as a hash - */ -public func mailgun(mailgunSandboxDomain: OptionalConfigValue = .fastlaneDefault(nil), - mailgunSandboxPostmaster: OptionalConfigValue = .fastlaneDefault(nil), - mailgunApikey: OptionalConfigValue = .fastlaneDefault(nil), - postmaster: String, - apikey: String, - to: String, - from: String = "Mailgun Sandbox", - message: String, - subject: String = "fastlane build", - success: OptionalConfigValue = .fastlaneDefault(true), - appLink: String, - ciBuildLink: OptionalConfigValue = .fastlaneDefault(nil), - templatePath: OptionalConfigValue = .fastlaneDefault(nil), - replyTo: OptionalConfigValue = .fastlaneDefault(nil), - attachment: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - customPlaceholders: [String: Any] = [:]) -{ - let mailgunSandboxDomainArg = mailgunSandboxDomain.asRubyArgument(name: "mailgun_sandbox_domain", type: nil) - let mailgunSandboxPostmasterArg = mailgunSandboxPostmaster.asRubyArgument(name: "mailgun_sandbox_postmaster", type: nil) - let mailgunApikeyArg = mailgunApikey.asRubyArgument(name: "mailgun_apikey", type: nil) - let postmasterArg = RubyCommand.Argument(name: "postmaster", value: postmaster, type: nil) - let apikeyArg = RubyCommand.Argument(name: "apikey", value: apikey, type: nil) - let toArg = RubyCommand.Argument(name: "to", value: to, type: nil) - let fromArg = RubyCommand.Argument(name: "from", value: from, type: nil) - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let subjectArg = RubyCommand.Argument(name: "subject", value: subject, type: nil) - let successArg = success.asRubyArgument(name: "success", type: nil) - let appLinkArg = RubyCommand.Argument(name: "app_link", value: appLink, type: nil) - let ciBuildLinkArg = ciBuildLink.asRubyArgument(name: "ci_build_link", type: nil) - let templatePathArg = templatePath.asRubyArgument(name: "template_path", type: nil) - let replyToArg = replyTo.asRubyArgument(name: "reply_to", type: nil) - let attachmentArg = attachment.asRubyArgument(name: "attachment", type: nil) - let customPlaceholdersArg = RubyCommand.Argument(name: "custom_placeholders", value: customPlaceholders, type: nil) - let array: [RubyCommand.Argument?] = [mailgunSandboxDomainArg, - mailgunSandboxPostmasterArg, - mailgunApikeyArg, - postmasterArg, - apikeyArg, - toArg, - fromArg, - messageArg, - subjectArg, - successArg, - appLinkArg, - ciBuildLinkArg, - templatePathArg, - replyToArg, - attachmentArg, - customPlaceholdersArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "mailgun", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generate a changelog using the Changes section from the current Jenkins build - - - parameters: - - fallbackChangelog: Fallback changelog if there is not one on Jenkins, or it couldn't be read - - includeCommitBody: Include the commit body along with the summary - - This is useful when deploying automated builds. The changelog from Jenkins lists all the commit messages since the last build. - */ -public func makeChangelogFromJenkins(fallbackChangelog: String = "", - includeCommitBody: OptionalConfigValue = .fastlaneDefault(true)) -{ - let fallbackChangelogArg = RubyCommand.Argument(name: "fallback_changelog", value: fallbackChangelog, type: nil) - let includeCommitBodyArg = includeCommitBody.asRubyArgument(name: "include_commit_body", type: nil) - let array: [RubyCommand.Argument?] = [fallbackChangelogArg, - includeCommitBodyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "make_changelog_from_jenkins", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `sync_code_signing` action - - - parameters: - - type: Define the profile type, can be appstore, adhoc, development, enterprise, developer_id, mac_installer_distribution, developer_id_installer - - additionalCertTypes: Create additional cert types needed for macOS installers (valid values: mac_installer_distribution, developer_id_installer) - - readonly: Only fetch existing certificates and profiles, don't generate new ones - - generateAppleCerts: Create a certificate type for Xcode 11 and later (Apple Development or Apple Distribution) - - skipProvisioningProfiles: Skip syncing provisioning profiles - - appIdentifier: The bundle identifier(s) of your app (comma-separated string or array of strings) - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - storageMode: Define where you want to store your certificates - - gitUrl: URL to the git repo containing all the certificates - - gitBranch: Specific git branch to use - - gitFullName: git user full name to commit - - gitUserEmail: git user email to commit - - shallowClone: Make a shallow clone of the repository (truncate the history to 1 revision) - - cloneBranchDirectly: Clone just the branch specified, instead of the whole repo. This requires that the branch already exists. Otherwise the command will fail - - gitBasicAuthorization: Use a basic authorization header to access the git repo (e.g.: access via HTTPS, GitHub Actions, etc), usually a string in Base64 - - gitBearerAuthorization: Use a bearer authorization header to access the git repo (e.g.: access to an Azure DevOps repository), usually a string in Base64 - - gitPrivateKey: Use a private key to access the git repo (e.g.: access to GitHub repository via Deploy keys), usually a id_rsa named file or the contents hereof - - googleCloudBucketName: Name of the Google Cloud Storage bucket to use - - googleCloudKeysFile: Path to the gc_keys.json file - - googleCloudProjectId: ID of the Google Cloud project to use for authentication - - skipGoogleCloudAccountConfirmation: Skips confirming to use the system google account - - s3Region: Name of the S3 region - - s3AccessKey: S3 access key - - s3SecretAccessKey: S3 secret access key - - s3SessionToken: S3 session token - - s3Bucket: Name of the S3 bucket - - s3ObjectPrefix: Prefix to be used on all objects uploaded to S3 - - s3SkipEncryption: Skip encryption of all objects uploaded to S3. WARNING: only enable this on S3 buckets with sufficiently restricted permissions and server-side encryption enabled. See https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingEncryption.html - - gitlabProject: GitLab Project Path (i.e. 'gitlab-org/gitlab') - - gitlabHost: GitLab Host (i.e. 'https://gitlab.com') - - jobToken: GitLab CI_JOB_TOKEN - - privateToken: GitLab Access Token - - keychainName: Keychain the items should be imported to - - keychainPassword: This might be required the first time you access certificates on a new mac. For the login/default keychain this is your macOS account password - - force: Renew the provisioning profiles every time you run match - - forceForNewDevices: Renew the provisioning profiles if the device count on the developer portal has changed. Ignored for profile types 'appstore' and 'developer_id' - - includeMacInProfiles: Include Apple Silicon Mac devices in provisioning profiles for iOS/iPadOS apps - - includeAllCertificates: Include all matching certificates in the provisioning profile. Works only for the 'development' provisioning profile type - - certificateId: Select certificate by id. Useful if multiple certificates are stored in one place - - forceForNewCertificates: Renew the provisioning profiles if the certificate count on the developer portal has changed. Works only for the 'development' provisioning profile type. Requires 'include_all_certificates' option to be 'true' - - skipConfirmation: Disables confirmation prompts during nuke, answering them with yes - - safeRemoveCerts: Remove certs from repository during nuke without revoking them on the developer portal - - skipDocs: Skip generation of a README.md for the created git repository - - platform: Set the provisioning profile's platform to work with (i.e. ios, tvos, macos, catalyst) - - deriveCatalystAppIdentifier: Enable this if you have the Mac Catalyst capability enabled and your project was created with Xcode 11.3 or earlier. Prepends 'maccatalyst.' to the app identifier for the provisioning profile mapping - - templateName: **DEPRECATED!** Removed since May 2025 on App Store Connect API OpenAPI v3.8.0 - Learn more: https://docs.fastlane.tools/actions/match/#managed-capabilities - The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. "Apple Pay Pass Suppression Development") - - profileName: A custom name for the provisioning profile. This will replace the default provisioning profile name if specified - - failOnNameTaken: Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first - - skipCertificateMatching: Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action - - outputPath: Path in which to export certificates, key and profile - - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing - - forceLegacyEncryption: Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions - - verbose: Print out extra information and all commands - - More information: https://docs.fastlane.tools/actions/match/ - */ -public func match(type: String = matchfile.type, - additionalCertTypes: OptionalConfigValue<[String]?> = .fastlaneDefault(matchfile.additionalCertTypes), - readonly: OptionalConfigValue = .fastlaneDefault(matchfile.readonly), - generateAppleCerts: OptionalConfigValue = .fastlaneDefault(matchfile.generateAppleCerts), - skipProvisioningProfiles: OptionalConfigValue = .fastlaneDefault(matchfile.skipProvisioningProfiles), - appIdentifier: [String] = matchfile.appIdentifier, - apiKeyPath: OptionalConfigValue = .fastlaneDefault(matchfile.apiKeyPath), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(matchfile.apiKey), - username: OptionalConfigValue = .fastlaneDefault(matchfile.username), - teamId: OptionalConfigValue = .fastlaneDefault(matchfile.teamId), - teamName: OptionalConfigValue = .fastlaneDefault(matchfile.teamName), - storageMode: String = matchfile.storageMode, - gitUrl: String = matchfile.gitUrl, - gitBranch: String = matchfile.gitBranch, - gitFullName: OptionalConfigValue = .fastlaneDefault(matchfile.gitFullName), - gitUserEmail: OptionalConfigValue = .fastlaneDefault(matchfile.gitUserEmail), - shallowClone: OptionalConfigValue = .fastlaneDefault(matchfile.shallowClone), - cloneBranchDirectly: OptionalConfigValue = .fastlaneDefault(matchfile.cloneBranchDirectly), - gitBasicAuthorization: OptionalConfigValue = .fastlaneDefault(matchfile.gitBasicAuthorization), - gitBearerAuthorization: OptionalConfigValue = .fastlaneDefault(matchfile.gitBearerAuthorization), - gitPrivateKey: OptionalConfigValue = .fastlaneDefault(matchfile.gitPrivateKey), - googleCloudBucketName: OptionalConfigValue = .fastlaneDefault(matchfile.googleCloudBucketName), - googleCloudKeysFile: OptionalConfigValue = .fastlaneDefault(matchfile.googleCloudKeysFile), - googleCloudProjectId: OptionalConfigValue = .fastlaneDefault(matchfile.googleCloudProjectId), - skipGoogleCloudAccountConfirmation: OptionalConfigValue = .fastlaneDefault(matchfile.skipGoogleCloudAccountConfirmation), - s3Region: OptionalConfigValue = .fastlaneDefault(matchfile.s3Region), - s3AccessKey: OptionalConfigValue = .fastlaneDefault(matchfile.s3AccessKey), - s3SecretAccessKey: OptionalConfigValue = .fastlaneDefault(matchfile.s3SecretAccessKey), - s3SessionToken: OptionalConfigValue = .fastlaneDefault(matchfile.s3SessionToken), - s3Bucket: OptionalConfigValue = .fastlaneDefault(matchfile.s3Bucket), - s3ObjectPrefix: OptionalConfigValue = .fastlaneDefault(matchfile.s3ObjectPrefix), - s3SkipEncryption: OptionalConfigValue = .fastlaneDefault(matchfile.s3SkipEncryption), - gitlabProject: OptionalConfigValue = .fastlaneDefault(matchfile.gitlabProject), - gitlabHost: String = matchfile.gitlabHost, - jobToken: OptionalConfigValue = .fastlaneDefault(matchfile.jobToken), - privateToken: OptionalConfigValue = .fastlaneDefault(matchfile.privateToken), - keychainName: String = matchfile.keychainName, - keychainPassword: OptionalConfigValue = .fastlaneDefault(matchfile.keychainPassword), - force: OptionalConfigValue = .fastlaneDefault(matchfile.force), - forceForNewDevices: OptionalConfigValue = .fastlaneDefault(matchfile.forceForNewDevices), - includeMacInProfiles: OptionalConfigValue = .fastlaneDefault(matchfile.includeMacInProfiles), - includeAllCertificates: OptionalConfigValue = .fastlaneDefault(matchfile.includeAllCertificates), - certificateId: OptionalConfigValue = .fastlaneDefault(matchfile.certificateId), - forceForNewCertificates: OptionalConfigValue = .fastlaneDefault(matchfile.forceForNewCertificates), - skipConfirmation: OptionalConfigValue = .fastlaneDefault(matchfile.skipConfirmation), - safeRemoveCerts: OptionalConfigValue = .fastlaneDefault(matchfile.safeRemoveCerts), - skipDocs: OptionalConfigValue = .fastlaneDefault(matchfile.skipDocs), - platform: String = matchfile.platform, - deriveCatalystAppIdentifier: OptionalConfigValue = .fastlaneDefault(matchfile.deriveCatalystAppIdentifier), - templateName: OptionalConfigValue = .fastlaneDefault(matchfile.templateName), - profileName: OptionalConfigValue = .fastlaneDefault(matchfile.profileName), - failOnNameTaken: OptionalConfigValue = .fastlaneDefault(matchfile.failOnNameTaken), - skipCertificateMatching: OptionalConfigValue = .fastlaneDefault(matchfile.skipCertificateMatching), - outputPath: OptionalConfigValue = .fastlaneDefault(matchfile.outputPath), - skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(matchfile.skipSetPartitionList), - forceLegacyEncryption: OptionalConfigValue = .fastlaneDefault(matchfile.forceLegacyEncryption), - verbose: OptionalConfigValue = .fastlaneDefault(matchfile.verbose)) -{ - let typeArg = RubyCommand.Argument(name: "type", value: type, type: nil) - let additionalCertTypesArg = additionalCertTypes.asRubyArgument(name: "additional_cert_types", type: nil) - let readonlyArg = readonly.asRubyArgument(name: "readonly", type: nil) - let generateAppleCertsArg = generateAppleCerts.asRubyArgument(name: "generate_apple_certs", type: nil) - let skipProvisioningProfilesArg = skipProvisioningProfiles.asRubyArgument(name: "skip_provisioning_profiles", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let storageModeArg = RubyCommand.Argument(name: "storage_mode", value: storageMode, type: nil) - let gitUrlArg = RubyCommand.Argument(name: "git_url", value: gitUrl, type: nil) - let gitBranchArg = RubyCommand.Argument(name: "git_branch", value: gitBranch, type: nil) - let gitFullNameArg = gitFullName.asRubyArgument(name: "git_full_name", type: nil) - let gitUserEmailArg = gitUserEmail.asRubyArgument(name: "git_user_email", type: nil) - let shallowCloneArg = shallowClone.asRubyArgument(name: "shallow_clone", type: nil) - let cloneBranchDirectlyArg = cloneBranchDirectly.asRubyArgument(name: "clone_branch_directly", type: nil) - let gitBasicAuthorizationArg = gitBasicAuthorization.asRubyArgument(name: "git_basic_authorization", type: nil) - let gitBearerAuthorizationArg = gitBearerAuthorization.asRubyArgument(name: "git_bearer_authorization", type: nil) - let gitPrivateKeyArg = gitPrivateKey.asRubyArgument(name: "git_private_key", type: nil) - let googleCloudBucketNameArg = googleCloudBucketName.asRubyArgument(name: "google_cloud_bucket_name", type: nil) - let googleCloudKeysFileArg = googleCloudKeysFile.asRubyArgument(name: "google_cloud_keys_file", type: nil) - let googleCloudProjectIdArg = googleCloudProjectId.asRubyArgument(name: "google_cloud_project_id", type: nil) - let skipGoogleCloudAccountConfirmationArg = skipGoogleCloudAccountConfirmation.asRubyArgument(name: "skip_google_cloud_account_confirmation", type: nil) - let s3RegionArg = s3Region.asRubyArgument(name: "s3_region", type: nil) - let s3AccessKeyArg = s3AccessKey.asRubyArgument(name: "s3_access_key", type: nil) - let s3SecretAccessKeyArg = s3SecretAccessKey.asRubyArgument(name: "s3_secret_access_key", type: nil) - let s3SessionTokenArg = s3SessionToken.asRubyArgument(name: "s3_session_token", type: nil) - let s3BucketArg = s3Bucket.asRubyArgument(name: "s3_bucket", type: nil) - let s3ObjectPrefixArg = s3ObjectPrefix.asRubyArgument(name: "s3_object_prefix", type: nil) - let s3SkipEncryptionArg = s3SkipEncryption.asRubyArgument(name: "s3_skip_encryption", type: nil) - let gitlabProjectArg = gitlabProject.asRubyArgument(name: "gitlab_project", type: nil) - let gitlabHostArg = RubyCommand.Argument(name: "gitlab_host", value: gitlabHost, type: nil) - let jobTokenArg = jobToken.asRubyArgument(name: "job_token", type: nil) - let privateTokenArg = privateToken.asRubyArgument(name: "private_token", type: nil) - let keychainNameArg = RubyCommand.Argument(name: "keychain_name", value: keychainName, type: nil) - let keychainPasswordArg = keychainPassword.asRubyArgument(name: "keychain_password", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let forceForNewDevicesArg = forceForNewDevices.asRubyArgument(name: "force_for_new_devices", type: nil) - let includeMacInProfilesArg = includeMacInProfiles.asRubyArgument(name: "include_mac_in_profiles", type: nil) - let includeAllCertificatesArg = includeAllCertificates.asRubyArgument(name: "include_all_certificates", type: nil) - let certificateIdArg = certificateId.asRubyArgument(name: "certificate_id", type: nil) - let forceForNewCertificatesArg = forceForNewCertificates.asRubyArgument(name: "force_for_new_certificates", type: nil) - let skipConfirmationArg = skipConfirmation.asRubyArgument(name: "skip_confirmation", type: nil) - let safeRemoveCertsArg = safeRemoveCerts.asRubyArgument(name: "safe_remove_certs", type: nil) - let skipDocsArg = skipDocs.asRubyArgument(name: "skip_docs", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let deriveCatalystAppIdentifierArg = deriveCatalystAppIdentifier.asRubyArgument(name: "derive_catalyst_app_identifier", type: nil) - let templateNameArg = templateName.asRubyArgument(name: "template_name", type: nil) - let profileNameArg = profileName.asRubyArgument(name: "profile_name", type: nil) - let failOnNameTakenArg = failOnNameTaken.asRubyArgument(name: "fail_on_name_taken", type: nil) - let skipCertificateMatchingArg = skipCertificateMatching.asRubyArgument(name: "skip_certificate_matching", type: nil) - let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) - let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) - let forceLegacyEncryptionArg = forceLegacyEncryption.asRubyArgument(name: "force_legacy_encryption", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let array: [RubyCommand.Argument?] = [typeArg, - additionalCertTypesArg, - readonlyArg, - generateAppleCertsArg, - skipProvisioningProfilesArg, - appIdentifierArg, - apiKeyPathArg, - apiKeyArg, - usernameArg, - teamIdArg, - teamNameArg, - storageModeArg, - gitUrlArg, - gitBranchArg, - gitFullNameArg, - gitUserEmailArg, - shallowCloneArg, - cloneBranchDirectlyArg, - gitBasicAuthorizationArg, - gitBearerAuthorizationArg, - gitPrivateKeyArg, - googleCloudBucketNameArg, - googleCloudKeysFileArg, - googleCloudProjectIdArg, - skipGoogleCloudAccountConfirmationArg, - s3RegionArg, - s3AccessKeyArg, - s3SecretAccessKeyArg, - s3SessionTokenArg, - s3BucketArg, - s3ObjectPrefixArg, - s3SkipEncryptionArg, - gitlabProjectArg, - gitlabHostArg, - jobTokenArg, - privateTokenArg, - keychainNameArg, - keychainPasswordArg, - forceArg, - forceForNewDevicesArg, - includeMacInProfilesArg, - includeAllCertificatesArg, - certificateIdArg, - forceForNewCertificatesArg, - skipConfirmationArg, - safeRemoveCertsArg, - skipDocsArg, - platformArg, - deriveCatalystAppIdentifierArg, - templateNameArg, - profileNameArg, - failOnNameTakenArg, - skipCertificateMatchingArg, - outputPathArg, - skipSetPartitionListArg, - forceLegacyEncryptionArg, - verboseArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "match", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Easily nuke your certificate and provisioning profiles (via _match_) - - - parameters: - - type: Define the profile type, can be appstore, adhoc, development, enterprise, developer_id, mac_installer_distribution, developer_id_installer - - additionalCertTypes: Create additional cert types needed for macOS installers (valid values: mac_installer_distribution, developer_id_installer) - - readonly: Only fetch existing certificates and profiles, don't generate new ones - - generateAppleCerts: Create a certificate type for Xcode 11 and later (Apple Development or Apple Distribution) - - skipProvisioningProfiles: Skip syncing provisioning profiles - - appIdentifier: The bundle identifier(s) of your app (comma-separated string or array of strings) - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - storageMode: Define where you want to store your certificates - - gitUrl: URL to the git repo containing all the certificates - - gitBranch: Specific git branch to use - - gitFullName: git user full name to commit - - gitUserEmail: git user email to commit - - shallowClone: Make a shallow clone of the repository (truncate the history to 1 revision) - - cloneBranchDirectly: Clone just the branch specified, instead of the whole repo. This requires that the branch already exists. Otherwise the command will fail - - gitBasicAuthorization: Use a basic authorization header to access the git repo (e.g.: access via HTTPS, GitHub Actions, etc), usually a string in Base64 - - gitBearerAuthorization: Use a bearer authorization header to access the git repo (e.g.: access to an Azure DevOps repository), usually a string in Base64 - - gitPrivateKey: Use a private key to access the git repo (e.g.: access to GitHub repository via Deploy keys), usually a id_rsa named file or the contents hereof - - googleCloudBucketName: Name of the Google Cloud Storage bucket to use - - googleCloudKeysFile: Path to the gc_keys.json file - - googleCloudProjectId: ID of the Google Cloud project to use for authentication - - skipGoogleCloudAccountConfirmation: Skips confirming to use the system google account - - s3Region: Name of the S3 region - - s3AccessKey: S3 access key - - s3SecretAccessKey: S3 secret access key - - s3SessionToken: S3 session token - - s3Bucket: Name of the S3 bucket - - s3ObjectPrefix: Prefix to be used on all objects uploaded to S3 - - s3SkipEncryption: Skip encryption of all objects uploaded to S3. WARNING: only enable this on S3 buckets with sufficiently restricted permissions and server-side encryption enabled. See https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingEncryption.html - - gitlabProject: GitLab Project Path (i.e. 'gitlab-org/gitlab') - - gitlabHost: GitLab Host (i.e. 'https://gitlab.com') - - jobToken: GitLab CI_JOB_TOKEN - - privateToken: GitLab Access Token - - keychainName: Keychain the items should be imported to - - keychainPassword: This might be required the first time you access certificates on a new mac. For the login/default keychain this is your macOS account password - - force: Renew the provisioning profiles every time you run match - - forceForNewDevices: Renew the provisioning profiles if the device count on the developer portal has changed. Ignored for profile types 'appstore' and 'developer_id' - - includeMacInProfiles: Include Apple Silicon Mac devices in provisioning profiles for iOS/iPadOS apps - - includeAllCertificates: Include all matching certificates in the provisioning profile. Works only for the 'development' provisioning profile type - - certificateId: Select certificate by id. Useful if multiple certificates are stored in one place - - forceForNewCertificates: Renew the provisioning profiles if the certificate count on the developer portal has changed. Works only for the 'development' provisioning profile type. Requires 'include_all_certificates' option to be 'true' - - skipConfirmation: Disables confirmation prompts during nuke, answering them with yes - - safeRemoveCerts: Remove certs from repository during nuke without revoking them on the developer portal - - skipDocs: Skip generation of a README.md for the created git repository - - platform: Set the provisioning profile's platform to work with (i.e. ios, tvos, macos, catalyst) - - deriveCatalystAppIdentifier: Enable this if you have the Mac Catalyst capability enabled and your project was created with Xcode 11.3 or earlier. Prepends 'maccatalyst.' to the app identifier for the provisioning profile mapping - - templateName: **DEPRECATED!** Removed since May 2025 on App Store Connect API OpenAPI v3.8.0 - Learn more: https://docs.fastlane.tools/actions/match/#managed-capabilities - The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. "Apple Pay Pass Suppression Development") - - profileName: A custom name for the provisioning profile. This will replace the default provisioning profile name if specified - - failOnNameTaken: Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first - - skipCertificateMatching: Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action - - outputPath: Path in which to export certificates, key and profile - - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing - - forceLegacyEncryption: Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions - - verbose: Print out extra information and all commands - - Use the match_nuke action to revoke your certificates and provisioning profiles. - Don't worry, apps that are already available in the App Store / TestFlight will still work. - Builds distributed via Ad Hoc or Enterprise will be disabled after nuking your account, so you'll have to re-upload a new build. - After clearing your account you'll start from a clean state, and you can run match to generate your certificates and profiles again. - More information: https://docs.fastlane.tools/actions/match/ - */ -public func matchNuke(type: String = "development", - additionalCertTypes: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - readonly: OptionalConfigValue = .fastlaneDefault(false), - generateAppleCerts: OptionalConfigValue = .fastlaneDefault(true), - skipProvisioningProfiles: OptionalConfigValue = .fastlaneDefault(false), - appIdentifier: [String], - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - storageMode: String = "git", - gitUrl: String, - gitBranch: String = "master", - gitFullName: OptionalConfigValue = .fastlaneDefault(nil), - gitUserEmail: OptionalConfigValue = .fastlaneDefault(nil), - shallowClone: OptionalConfigValue = .fastlaneDefault(false), - cloneBranchDirectly: OptionalConfigValue = .fastlaneDefault(false), - gitBasicAuthorization: OptionalConfigValue = .fastlaneDefault(nil), - gitBearerAuthorization: OptionalConfigValue = .fastlaneDefault(nil), - gitPrivateKey: OptionalConfigValue = .fastlaneDefault(nil), - googleCloudBucketName: OptionalConfigValue = .fastlaneDefault(nil), - googleCloudKeysFile: OptionalConfigValue = .fastlaneDefault(nil), - googleCloudProjectId: OptionalConfigValue = .fastlaneDefault(nil), - skipGoogleCloudAccountConfirmation: OptionalConfigValue = .fastlaneDefault(false), - s3Region: OptionalConfigValue = .fastlaneDefault(nil), - s3AccessKey: OptionalConfigValue = .fastlaneDefault(nil), - s3SecretAccessKey: OptionalConfigValue = .fastlaneDefault(nil), - s3SessionToken: OptionalConfigValue = .fastlaneDefault(nil), - s3Bucket: OptionalConfigValue = .fastlaneDefault(nil), - s3ObjectPrefix: OptionalConfigValue = .fastlaneDefault(nil), - s3SkipEncryption: OptionalConfigValue = .fastlaneDefault(false), - gitlabProject: OptionalConfigValue = .fastlaneDefault(nil), - gitlabHost: String = "https://gitlab.com", - jobToken: OptionalConfigValue = .fastlaneDefault(nil), - privateToken: OptionalConfigValue = .fastlaneDefault(nil), - keychainName: String = "login.keychain", - keychainPassword: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - forceForNewDevices: OptionalConfigValue = .fastlaneDefault(false), - includeMacInProfiles: OptionalConfigValue = .fastlaneDefault(false), - includeAllCertificates: OptionalConfigValue = .fastlaneDefault(false), - certificateId: OptionalConfigValue = .fastlaneDefault(nil), - forceForNewCertificates: OptionalConfigValue = .fastlaneDefault(false), - skipConfirmation: OptionalConfigValue = .fastlaneDefault(false), - safeRemoveCerts: OptionalConfigValue = .fastlaneDefault(false), - skipDocs: OptionalConfigValue = .fastlaneDefault(false), - platform: String = "ios", - deriveCatalystAppIdentifier: OptionalConfigValue = .fastlaneDefault(false), - templateName: OptionalConfigValue = .fastlaneDefault(nil), - profileName: OptionalConfigValue = .fastlaneDefault(nil), - failOnNameTaken: OptionalConfigValue = .fastlaneDefault(false), - skipCertificateMatching: OptionalConfigValue = .fastlaneDefault(false), - outputPath: OptionalConfigValue = .fastlaneDefault(nil), - skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(false), - forceLegacyEncryption: OptionalConfigValue = .fastlaneDefault(false), - verbose: OptionalConfigValue = .fastlaneDefault(false)) -{ - let typeArg = RubyCommand.Argument(name: "type", value: type, type: nil) - let additionalCertTypesArg = additionalCertTypes.asRubyArgument(name: "additional_cert_types", type: nil) - let readonlyArg = readonly.asRubyArgument(name: "readonly", type: nil) - let generateAppleCertsArg = generateAppleCerts.asRubyArgument(name: "generate_apple_certs", type: nil) - let skipProvisioningProfilesArg = skipProvisioningProfiles.asRubyArgument(name: "skip_provisioning_profiles", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let storageModeArg = RubyCommand.Argument(name: "storage_mode", value: storageMode, type: nil) - let gitUrlArg = RubyCommand.Argument(name: "git_url", value: gitUrl, type: nil) - let gitBranchArg = RubyCommand.Argument(name: "git_branch", value: gitBranch, type: nil) - let gitFullNameArg = gitFullName.asRubyArgument(name: "git_full_name", type: nil) - let gitUserEmailArg = gitUserEmail.asRubyArgument(name: "git_user_email", type: nil) - let shallowCloneArg = shallowClone.asRubyArgument(name: "shallow_clone", type: nil) - let cloneBranchDirectlyArg = cloneBranchDirectly.asRubyArgument(name: "clone_branch_directly", type: nil) - let gitBasicAuthorizationArg = gitBasicAuthorization.asRubyArgument(name: "git_basic_authorization", type: nil) - let gitBearerAuthorizationArg = gitBearerAuthorization.asRubyArgument(name: "git_bearer_authorization", type: nil) - let gitPrivateKeyArg = gitPrivateKey.asRubyArgument(name: "git_private_key", type: nil) - let googleCloudBucketNameArg = googleCloudBucketName.asRubyArgument(name: "google_cloud_bucket_name", type: nil) - let googleCloudKeysFileArg = googleCloudKeysFile.asRubyArgument(name: "google_cloud_keys_file", type: nil) - let googleCloudProjectIdArg = googleCloudProjectId.asRubyArgument(name: "google_cloud_project_id", type: nil) - let skipGoogleCloudAccountConfirmationArg = skipGoogleCloudAccountConfirmation.asRubyArgument(name: "skip_google_cloud_account_confirmation", type: nil) - let s3RegionArg = s3Region.asRubyArgument(name: "s3_region", type: nil) - let s3AccessKeyArg = s3AccessKey.asRubyArgument(name: "s3_access_key", type: nil) - let s3SecretAccessKeyArg = s3SecretAccessKey.asRubyArgument(name: "s3_secret_access_key", type: nil) - let s3SessionTokenArg = s3SessionToken.asRubyArgument(name: "s3_session_token", type: nil) - let s3BucketArg = s3Bucket.asRubyArgument(name: "s3_bucket", type: nil) - let s3ObjectPrefixArg = s3ObjectPrefix.asRubyArgument(name: "s3_object_prefix", type: nil) - let s3SkipEncryptionArg = s3SkipEncryption.asRubyArgument(name: "s3_skip_encryption", type: nil) - let gitlabProjectArg = gitlabProject.asRubyArgument(name: "gitlab_project", type: nil) - let gitlabHostArg = RubyCommand.Argument(name: "gitlab_host", value: gitlabHost, type: nil) - let jobTokenArg = jobToken.asRubyArgument(name: "job_token", type: nil) - let privateTokenArg = privateToken.asRubyArgument(name: "private_token", type: nil) - let keychainNameArg = RubyCommand.Argument(name: "keychain_name", value: keychainName, type: nil) - let keychainPasswordArg = keychainPassword.asRubyArgument(name: "keychain_password", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let forceForNewDevicesArg = forceForNewDevices.asRubyArgument(name: "force_for_new_devices", type: nil) - let includeMacInProfilesArg = includeMacInProfiles.asRubyArgument(name: "include_mac_in_profiles", type: nil) - let includeAllCertificatesArg = includeAllCertificates.asRubyArgument(name: "include_all_certificates", type: nil) - let certificateIdArg = certificateId.asRubyArgument(name: "certificate_id", type: nil) - let forceForNewCertificatesArg = forceForNewCertificates.asRubyArgument(name: "force_for_new_certificates", type: nil) - let skipConfirmationArg = skipConfirmation.asRubyArgument(name: "skip_confirmation", type: nil) - let safeRemoveCertsArg = safeRemoveCerts.asRubyArgument(name: "safe_remove_certs", type: nil) - let skipDocsArg = skipDocs.asRubyArgument(name: "skip_docs", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let deriveCatalystAppIdentifierArg = deriveCatalystAppIdentifier.asRubyArgument(name: "derive_catalyst_app_identifier", type: nil) - let templateNameArg = templateName.asRubyArgument(name: "template_name", type: nil) - let profileNameArg = profileName.asRubyArgument(name: "profile_name", type: nil) - let failOnNameTakenArg = failOnNameTaken.asRubyArgument(name: "fail_on_name_taken", type: nil) - let skipCertificateMatchingArg = skipCertificateMatching.asRubyArgument(name: "skip_certificate_matching", type: nil) - let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) - let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) - let forceLegacyEncryptionArg = forceLegacyEncryption.asRubyArgument(name: "force_legacy_encryption", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let array: [RubyCommand.Argument?] = [typeArg, - additionalCertTypesArg, - readonlyArg, - generateAppleCertsArg, - skipProvisioningProfilesArg, - appIdentifierArg, - apiKeyPathArg, - apiKeyArg, - usernameArg, - teamIdArg, - teamNameArg, - storageModeArg, - gitUrlArg, - gitBranchArg, - gitFullNameArg, - gitUserEmailArg, - shallowCloneArg, - cloneBranchDirectlyArg, - gitBasicAuthorizationArg, - gitBearerAuthorizationArg, - gitPrivateKeyArg, - googleCloudBucketNameArg, - googleCloudKeysFileArg, - googleCloudProjectIdArg, - skipGoogleCloudAccountConfirmationArg, - s3RegionArg, - s3AccessKeyArg, - s3SecretAccessKeyArg, - s3SessionTokenArg, - s3BucketArg, - s3ObjectPrefixArg, - s3SkipEncryptionArg, - gitlabProjectArg, - gitlabHostArg, - jobTokenArg, - privateTokenArg, - keychainNameArg, - keychainPasswordArg, - forceArg, - forceForNewDevicesArg, - includeMacInProfilesArg, - includeAllCertificatesArg, - certificateIdArg, - forceForNewCertificatesArg, - skipConfirmationArg, - safeRemoveCertsArg, - skipDocsArg, - platformArg, - deriveCatalystAppIdentifierArg, - templateNameArg, - profileNameArg, - failOnNameTakenArg, - skipCertificateMatchingArg, - outputPathArg, - skipSetPartitionListArg, - forceLegacyEncryptionArg, - verboseArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "match_nuke", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Verifies the minimum fastlane version required - - Add this to your `Fastfile` to require a certain version of _fastlane_. - Use it if you use an action that just recently came out and you need it. - */ -public func minFastlaneVersion() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "min_fastlane_version", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Modifies the services of the app created on Developer Portal - - - parameters: - - username: Your Apple ID Username - - appIdentifier: App Identifier (Bundle ID, e.g. com.krausefx.app) - - services: Array with Spaceship App Services (e.g. access_wifi: (on|off)(:on|:off)(true|false), app_attest: (on|off)(:on|:off)(true|false), app_group: (on|off)(:on|:off)(true|false), apple_pay: (on|off)(:on|:off)(true|false), associated_domains: (on|off)(:on|:off)(true|false), auto_fill_credential: (on|off)(:on|:off)(true|false), class_kit: (on|off)(:on|:off)(true|false), declared_age_range: (on|off)(:on|:off)(true|false), icloud: (legacy|cloudkit)(:on|:off)(true|false), custom_network_protocol: (on|off)(:on|:off)(true|false), data_protection: (complete|unlessopen|untilfirstauth)(:on|:off)(true|false), extended_virtual_address_space: (on|off)(:on|:off)(true|false), family_controls: (on|off)(:on|:off)(true|false), file_provider_testing_mode: (on|off)(:on|:off)(true|false), fonts: (on|off)(:on|:off)(true|false), game_center: (ios|mac)(:on|:off)(true|false), health_kit: (on|off)(:on|:off)(true|false), hls_interstitial_preview: (on|off)(:on|:off)(true|false), home_kit: (on|off)(:on|:off)(true|false), hotspot: (on|off)(:on|:off)(true|false), in_app_purchase: (on|off)(:on|:off)(true|false), inter_app_audio: (on|off)(:on|:off)(true|false), low_latency_hls: (on|off)(:on|:off)(true|false), managed_associated_domains: (on|off)(:on|:off)(true|false), maps: (on|off)(:on|:off)(true|false), multipath: (on|off)(:on|:off)(true|false), network_extension: (on|off)(:on|:off)(true|false), nfc_tag_reading: (on|off)(:on|:off)(true|false), personal_vpn: (on|off)(:on|:off)(true|false), passbook: (on|off)(:on|:off)(true|false), push_notification: (on|off)(:on|:off)(true|false), sign_in_with_apple: (on)(:on|:off)(true|false), siri_kit: (on|off)(:on|:off)(true|false), system_extension: (on|off)(:on|:off)(true|false), user_management: (on|off)(:on|:off)(true|false), vpn_configuration: (on|off)(:on|:off)(true|false), wallet: (on|off)(:on|:off)(true|false), wireless_accessory: (on|off)(:on|:off)(true|false), car_play_audio_app: (on|off)(:on|:off)(true|false), car_play_messaging_app: (on|off)(:on|:off)(true|false), car_play_navigation_app: (on|off)(:on|:off)(true|false), car_play_voip_calling_app: (on|off)(:on|:off)(true|false), critical_alerts: (on|off)(:on|:off)(true|false), hotspot_helper: (on|off)(:on|:off)(true|false), driver_kit: (on|off)(:on|:off)(true|false), driver_kit_endpoint_security: (on|off)(:on|:off)(true|false), driver_kit_family_hid_device: (on|off)(:on|:off)(true|false), driver_kit_family_networking: (on|off)(:on|:off)(true|false), driver_kit_family_serial: (on|off)(:on|:off)(true|false), driver_kit_hid_event_service: (on|off)(:on|:off)(true|false), driver_kit_transport_hid: (on|off)(:on|:off)(true|false), multitasking_camera_access: (on|off)(:on|:off)(true|false), sf_universal_link_api: (on|off)(:on|:off)(true|false), vp9_decoder: (on|off)(:on|:off)(true|false), music_kit: (on|off)(:on|:off)(true|false), shazam_kit: (on|off)(:on|:off)(true|false), communication_notifications: (on|off)(:on|:off)(true|false), group_activities: (on|off)(:on|:off)(true|false), health_kit_estimate_recalibration: (on|off)(:on|:off)(true|false), time_sensitive_notifications: (on|off)(:on|:off)(true|false)) - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - The options are the same as `:enable_services` in the [produce action](https://docs.fastlane.tools/actions/produce/#parameters_1) - */ -public func modifyServices(username: String, - appIdentifier: String, - services: [String: Any] = [:], - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let servicesArg = RubyCommand.Argument(name: "services", value: services, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let array: [RubyCommand.Argument?] = [usernameArg, - appIdentifierArg, - servicesArg, - teamIdArg, - teamNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "modify_services", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload a file to [Sonatype Nexus platform](https://www.sonatype.com) - - - parameters: - - file: File to be uploaded to Nexus - - repoId: Nexus repository id e.g. artefacts - - repoGroupId: Nexus repository group id e.g. com.company - - repoProjectName: Nexus repository commandect name. Only letters, digits, underscores(_), hyphens(-), and dots(.) are allowed - - repoProjectVersion: Nexus repository commandect version - - repoClassifier: Nexus repository artifact classifier (optional) - - endpoint: Nexus endpoint e.g. http://nexus:8081 - - mountPath: Nexus mount path (Nexus 3 instances have this configured as empty by default) - - username: Nexus username - - password: Nexus password - - sslVerify: Verify SSL - - nexusVersion: Nexus major version - - verbose: Make detailed output - - proxyUsername: Proxy username - - proxyPassword: Proxy password - - proxyAddress: Proxy address - - proxyPort: Proxy port - */ -public func nexusUpload(file: String, - repoId: String, - repoGroupId: String, - repoProjectName: String, - repoProjectVersion: String, - repoClassifier: OptionalConfigValue = .fastlaneDefault(nil), - endpoint: String, - mountPath: String = "/nexus", - username: String, - password: String, - sslVerify: OptionalConfigValue = .fastlaneDefault(true), - nexusVersion: Int = 2, - verbose: OptionalConfigValue = .fastlaneDefault(false), - proxyUsername: OptionalConfigValue = .fastlaneDefault(nil), - proxyPassword: OptionalConfigValue = .fastlaneDefault(nil), - proxyAddress: OptionalConfigValue = .fastlaneDefault(nil), - proxyPort: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let fileArg = RubyCommand.Argument(name: "file", value: file, type: nil) - let repoIdArg = RubyCommand.Argument(name: "repo_id", value: repoId, type: nil) - let repoGroupIdArg = RubyCommand.Argument(name: "repo_group_id", value: repoGroupId, type: nil) - let repoProjectNameArg = RubyCommand.Argument(name: "repo_project_name", value: repoProjectName, type: nil) - let repoProjectVersionArg = RubyCommand.Argument(name: "repo_project_version", value: repoProjectVersion, type: nil) - let repoClassifierArg = repoClassifier.asRubyArgument(name: "repo_classifier", type: nil) - let endpointArg = RubyCommand.Argument(name: "endpoint", value: endpoint, type: nil) - let mountPathArg = RubyCommand.Argument(name: "mount_path", value: mountPath, type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let passwordArg = RubyCommand.Argument(name: "password", value: password, type: nil) - let sslVerifyArg = sslVerify.asRubyArgument(name: "ssl_verify", type: nil) - let nexusVersionArg = RubyCommand.Argument(name: "nexus_version", value: nexusVersion, type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let proxyUsernameArg = proxyUsername.asRubyArgument(name: "proxy_username", type: nil) - let proxyPasswordArg = proxyPassword.asRubyArgument(name: "proxy_password", type: nil) - let proxyAddressArg = proxyAddress.asRubyArgument(name: "proxy_address", type: nil) - let proxyPortArg = proxyPort.asRubyArgument(name: "proxy_port", type: nil) - let array: [RubyCommand.Argument?] = [fileArg, - repoIdArg, - repoGroupIdArg, - repoProjectNameArg, - repoProjectVersionArg, - repoClassifierArg, - endpointArg, - mountPathArg, - usernameArg, - passwordArg, - sslVerifyArg, - nexusVersionArg, - verboseArg, - proxyUsernameArg, - proxyPasswordArg, - proxyAddressArg, - proxyPortArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "nexus_upload", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Notarizes a macOS app - - - parameters: - - package: Path to package to notarize, e.g. .app bundle or disk image - - useNotarytool: Whether to `xcrun notarytool` or `xcrun altool` - - tryEarlyStapling: Whether to try early stapling while the notarization request is in progress - - skipStapling: Do not staple the notarization ticket to the artifact; useful for single file executables and ZIP archives - - bundleId: Bundle identifier to uniquely identify the package - - username: Apple ID username - - ascProvider: Provider short name for accounts associated with multiple providers - - printLog: Whether to print notarization log file, listing issues on failure and warnings on success - - verbose: Whether to log requests - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - */ -public func notarize(package: String, - useNotarytool: OptionalConfigValue = .fastlaneDefault(true), - tryEarlyStapling: OptionalConfigValue = .fastlaneDefault(false), - skipStapling: OptionalConfigValue = .fastlaneDefault(false), - bundleId: OptionalConfigValue = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - ascProvider: OptionalConfigValue = .fastlaneDefault(nil), - printLog: OptionalConfigValue = .fastlaneDefault(false), - verbose: OptionalConfigValue = .fastlaneDefault(false), - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil)) -{ - let packageArg = RubyCommand.Argument(name: "package", value: package, type: nil) - let useNotarytoolArg = useNotarytool.asRubyArgument(name: "use_notarytool", type: nil) - let tryEarlyStaplingArg = tryEarlyStapling.asRubyArgument(name: "try_early_stapling", type: nil) - let skipStaplingArg = skipStapling.asRubyArgument(name: "skip_stapling", type: nil) - let bundleIdArg = bundleId.asRubyArgument(name: "bundle_id", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let ascProviderArg = ascProvider.asRubyArgument(name: "asc_provider", type: nil) - let printLogArg = printLog.asRubyArgument(name: "print_log", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let array: [RubyCommand.Argument?] = [packageArg, - useNotarytoolArg, - tryEarlyStaplingArg, - skipStaplingArg, - bundleIdArg, - usernameArg, - ascProviderArg, - printLogArg, - verboseArg, - apiKeyPathArg, - apiKeyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "notarize", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Display a macOS notification with custom message and title - - - parameters: - - title: The title to display in the notification - - subtitle: A subtitle to display in the notification - - message: The message to display in the notification - - sound: The name of a sound to play when the notification appears (names are listed in Sound Preferences) - - activate: Bundle identifier of application to be opened when the notification is clicked - - appIcon: The URL of an image to display instead of the application icon (Mavericks+ only) - - contentImage: The URL of an image to display attached to the notification (Mavericks+ only) - - open: URL of the resource to be opened when the notification is clicked - - execute: Shell command to run when the notification is clicked - */ -public func notification(title: String = "fastlane", - subtitle: OptionalConfigValue = .fastlaneDefault(nil), - message: String, - sound: OptionalConfigValue = .fastlaneDefault(nil), - activate: OptionalConfigValue = .fastlaneDefault(nil), - appIcon: OptionalConfigValue = .fastlaneDefault(nil), - contentImage: OptionalConfigValue = .fastlaneDefault(nil), - open: OptionalConfigValue = .fastlaneDefault(nil), - execute: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let titleArg = RubyCommand.Argument(name: "title", value: title, type: nil) - let subtitleArg = subtitle.asRubyArgument(name: "subtitle", type: nil) - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let soundArg = sound.asRubyArgument(name: "sound", type: nil) - let activateArg = activate.asRubyArgument(name: "activate", type: nil) - let appIconArg = appIcon.asRubyArgument(name: "app_icon", type: nil) - let contentImageArg = contentImage.asRubyArgument(name: "content_image", type: nil) - let openArg = open.asRubyArgument(name: "open", type: nil) - let executeArg = execute.asRubyArgument(name: "execute", type: nil) - let array: [RubyCommand.Argument?] = [titleArg, - subtitleArg, - messageArg, - soundArg, - activateArg, - appIconArg, - contentImageArg, - openArg, - executeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "notification", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Shows a macOS notification - use `notification` instead - */ -public func notify() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "notify", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Return the number of commits in current git branch - - - parameter all: Returns number of all commits instead of current branch - - - returns: The total number of all commits in current git branch - - You can use this action to get the number of commits of this branch. This is useful if you want to set the build number to the number of commits. See `fastlane actions number_of_commits` for more details. - */ -@discardableResult public func numberOfCommits(all: OptionalConfigValue = .fastlaneDefault(nil)) -> Int { - let allArg = all.asRubyArgument(name: "all", type: nil) - let array: [RubyCommand.Argument?] = [allArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "number_of_commits", className: nil, args: args) - return parseInt(fromString: runner.executeCommand(command)) -} - -/** - Lints implementation files with OCLint - - - parameters: - - oclintPath: The path to oclint binary - - compileCommands: The json compilation database, use xctool reporter 'json-compilation-database' - - selectReqex: **DEPRECATED!** Use `:select_regex` instead - Select all files matching this reqex - - selectRegex: Select all files matching this regex - - excludeRegex: Exclude all files matching this regex - - reportType: The type of the report (default: html) - - reportPath: The reports file path - - listEnabledRules: List enabled rules - - rc: Override the default behavior of rules - - thresholds: List of rule thresholds to override the default behavior of rules - - enableRules: List of rules to pick explicitly - - disableRules: List of rules to disable - - maxPriority1: The max allowed number of priority 1 violations - - maxPriority2: The max allowed number of priority 2 violations - - maxPriority3: The max allowed number of priority 3 violations - - enableClangStaticAnalyzer: Enable Clang Static Analyzer, and integrate results into OCLint report - - enableGlobalAnalysis: Compile every source, and analyze across global contexts (depends on number of source files, could results in high memory load) - - allowDuplicatedViolations: Allow duplicated violations in the OCLint report - - extraArg: Additional argument to append to the compiler command line - - Run the static analyzer tool [OCLint](http://oclint.org) for your project. You need to have a `compile_commands.json` file in your _fastlane_ directory or pass a path to your file. - */ -public func oclint(oclintPath: String = "oclint", - compileCommands: String = "compile_commands.json", - selectReqex: OptionalConfigValue = .fastlaneDefault(nil), - selectRegex: OptionalConfigValue = .fastlaneDefault(nil), - excludeRegex: OptionalConfigValue = .fastlaneDefault(nil), - reportType: String = "html", - reportPath: OptionalConfigValue = .fastlaneDefault(nil), - listEnabledRules: OptionalConfigValue = .fastlaneDefault(false), - rc: OptionalConfigValue = .fastlaneDefault(nil), - thresholds: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - enableRules: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - disableRules: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - maxPriority1: OptionalConfigValue = .fastlaneDefault(nil), - maxPriority2: OptionalConfigValue = .fastlaneDefault(nil), - maxPriority3: OptionalConfigValue = .fastlaneDefault(nil), - enableClangStaticAnalyzer: OptionalConfigValue = .fastlaneDefault(false), - enableGlobalAnalysis: OptionalConfigValue = .fastlaneDefault(false), - allowDuplicatedViolations: OptionalConfigValue = .fastlaneDefault(false), - extraArg: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let oclintPathArg = RubyCommand.Argument(name: "oclint_path", value: oclintPath, type: nil) - let compileCommandsArg = RubyCommand.Argument(name: "compile_commands", value: compileCommands, type: nil) - let selectReqexArg = selectReqex.asRubyArgument(name: "select_reqex", type: nil) - let selectRegexArg = selectRegex.asRubyArgument(name: "select_regex", type: nil) - let excludeRegexArg = excludeRegex.asRubyArgument(name: "exclude_regex", type: nil) - let reportTypeArg = RubyCommand.Argument(name: "report_type", value: reportType, type: nil) - let reportPathArg = reportPath.asRubyArgument(name: "report_path", type: nil) - let listEnabledRulesArg = listEnabledRules.asRubyArgument(name: "list_enabled_rules", type: nil) - let rcArg = rc.asRubyArgument(name: "rc", type: nil) - let thresholdsArg = thresholds.asRubyArgument(name: "thresholds", type: nil) - let enableRulesArg = enableRules.asRubyArgument(name: "enable_rules", type: nil) - let disableRulesArg = disableRules.asRubyArgument(name: "disable_rules", type: nil) - let maxPriority1Arg = maxPriority1.asRubyArgument(name: "max_priority_1", type: nil) - let maxPriority2Arg = maxPriority2.asRubyArgument(name: "max_priority_2", type: nil) - let maxPriority3Arg = maxPriority3.asRubyArgument(name: "max_priority_3", type: nil) - let enableClangStaticAnalyzerArg = enableClangStaticAnalyzer.asRubyArgument(name: "enable_clang_static_analyzer", type: nil) - let enableGlobalAnalysisArg = enableGlobalAnalysis.asRubyArgument(name: "enable_global_analysis", type: nil) - let allowDuplicatedViolationsArg = allowDuplicatedViolations.asRubyArgument(name: "allow_duplicated_violations", type: nil) - let extraArgArg = extraArg.asRubyArgument(name: "extra_arg", type: nil) - let array: [RubyCommand.Argument?] = [oclintPathArg, - compileCommandsArg, - selectReqexArg, - selectRegexArg, - excludeRegexArg, - reportTypeArg, - reportPathArg, - listEnabledRulesArg, - rcArg, - thresholdsArg, - enableRulesArg, - disableRulesArg, - maxPriority1Arg, - maxPriority2Arg, - maxPriority3Arg, - enableClangStaticAnalyzerArg, - enableGlobalAnalysisArg, - allowDuplicatedViolationsArg, - extraArgArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "oclint", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Create or update a new [OneSignal](https://onesignal.com/) application - - - parameters: - - appId: OneSignal App ID. Setting this updates an existing app - - authToken: OneSignal Authorization Key - - appName: OneSignal App Name. This is required when creating an app (in other words, when `:app_id` is not set, and optional when updating an app - - androidToken: ANDROID GCM KEY - - androidGcmSenderId: GCM SENDER ID - - fcmJson: FCM Service Account JSON File (in .json format) - - apnsP12: APNS P12 File (in .p12 format) - - apnsP12Password: APNS P12 password - - apnsEnv: APNS environment - - organizationId: OneSignal Organization ID - - You can use this action to automatically create or update a OneSignal application. You can also upload a `.p12` with password, a GCM key, or both. - */ -public func onesignal(appId: OptionalConfigValue = .fastlaneDefault(nil), - authToken: String, - appName: OptionalConfigValue = .fastlaneDefault(nil), - androidToken: OptionalConfigValue = .fastlaneDefault(nil), - androidGcmSenderId: OptionalConfigValue = .fastlaneDefault(nil), - fcmJson: OptionalConfigValue = .fastlaneDefault(nil), - apnsP12: OptionalConfigValue = .fastlaneDefault(nil), - apnsP12Password: OptionalConfigValue = .fastlaneDefault(nil), - apnsEnv: String = "production", - organizationId: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let appIdArg = appId.asRubyArgument(name: "app_id", type: nil) - let authTokenArg = RubyCommand.Argument(name: "auth_token", value: authToken, type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let androidTokenArg = androidToken.asRubyArgument(name: "android_token", type: nil) - let androidGcmSenderIdArg = androidGcmSenderId.asRubyArgument(name: "android_gcm_sender_id", type: nil) - let fcmJsonArg = fcmJson.asRubyArgument(name: "fcm_json", type: nil) - let apnsP12Arg = apnsP12.asRubyArgument(name: "apns_p12", type: nil) - let apnsP12PasswordArg = apnsP12Password.asRubyArgument(name: "apns_p12_password", type: nil) - let apnsEnvArg = RubyCommand.Argument(name: "apns_env", value: apnsEnv, type: nil) - let organizationIdArg = organizationId.asRubyArgument(name: "organization_id", type: nil) - let array: [RubyCommand.Argument?] = [appIdArg, - authTokenArg, - appNameArg, - androidTokenArg, - androidGcmSenderIdArg, - fcmJsonArg, - apnsP12Arg, - apnsP12PasswordArg, - apnsEnvArg, - organizationIdArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "onesignal", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will prevent reports from being uploaded when _fastlane_ crashes - - _fastlane_ doesn't have crash reporting anymore. Feel free to remove `opt_out_crash_reporting` from your Fastfile. - */ -public func optOutCrashReporting() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "opt_out_crash_reporting", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will stop uploading the information which actions were run - - By default, _fastlane_ will track what actions are being used. No personal/sensitive information is recorded. - Learn more at [https://docs.fastlane.tools/#metrics](https://docs.fastlane.tools/#metrics). - Add `opt_out_usage` at the top of your Fastfile to disable metrics collection. - */ -public func optOutUsage() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "opt_out_usage", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `get_push_certificate` action - - - parameters: - - platform: Set certificate's platform. Used for creation of production & development certificates. Supported platforms: ios, macos - - development: Renew the development push certificate instead of the production one - - websitePush: Create a Website Push certificate - - generateP12: Generate a p12 file additionally to a PEM file - - activeDaysLimit: If the current certificate is active for less than this number of days, generate a new one - - force: Create a new push certificate, even if the current one is active for 30 (or PEM_ACTIVE_DAYS_LIMIT) more days - - savePrivateKey: Set to save the private RSA key - - appIdentifier: The bundle identifier of your app - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - p12Password: The password that is used for your p12 file - - pemName: The file name of the generated .pem file - - outputPath: The path to a directory in which all certificates and private keys should be stored - - newProfile: Block that is called if there is a new profile - - Additionally to the available options, you can also specify a block that only gets executed if a new profile was created. You can use it to upload the new profile to your server. - Use it like this:| - | - ```ruby| - get_push_certificate(| - new_profile: proc do| - # your upload code| - end| - )| - ```| - >| - */ -public func pem(platform: String = "ios", - development: OptionalConfigValue = .fastlaneDefault(false), - websitePush: OptionalConfigValue = .fastlaneDefault(false), - generateP12: OptionalConfigValue = .fastlaneDefault(true), - activeDaysLimit: Int = 30, - force: OptionalConfigValue = .fastlaneDefault(false), - savePrivateKey: OptionalConfigValue = .fastlaneDefault(true), - appIdentifier: String, - username: String, - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - p12Password: OptionalConfigValue = .fastlaneDefault(nil), - pemName: OptionalConfigValue = .fastlaneDefault(nil), - outputPath: String = ".", - newProfile: ((String) -> Void)? = nil) -{ - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let developmentArg = development.asRubyArgument(name: "development", type: nil) - let websitePushArg = websitePush.asRubyArgument(name: "website_push", type: nil) - let generateP12Arg = generateP12.asRubyArgument(name: "generate_p12", type: nil) - let activeDaysLimitArg = RubyCommand.Argument(name: "active_days_limit", value: activeDaysLimit, type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let savePrivateKeyArg = savePrivateKey.asRubyArgument(name: "save_private_key", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let p12PasswordArg = p12Password.asRubyArgument(name: "p12_password", type: nil) - let pemNameArg = pemName.asRubyArgument(name: "pem_name", type: nil) - let outputPathArg = RubyCommand.Argument(name: "output_path", value: outputPath, type: nil) - let newProfileArg = RubyCommand.Argument(name: "new_profile", value: newProfile, type: .stringClosure) - let array: [RubyCommand.Argument?] = [platformArg, - developmentArg, - websitePushArg, - generateP12Arg, - activeDaysLimitArg, - forceArg, - savePrivateKeyArg, - appIdentifierArg, - usernameArg, - teamIdArg, - teamNameArg, - p12PasswordArg, - pemNameArg, - outputPathArg, - newProfileArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "pem", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `upload_to_testflight` action - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - appIdentifier: The bundle identifier of the app to upload or manage testers (optional) - - appPlatform: The platform to use (optional) - - appleId: Apple ID property in the App Information section in App Store Connect - - ipa: Path to the ipa file to upload - - pkg: Path to your pkg file - - demoAccountRequired: Do you need a demo account when Apple does review? - - betaAppReviewInfo: Beta app review information for contact info and demo account - - localizedAppInfo: Localized beta app test info for description, feedback email, marketing url, and privacy policy - - betaAppDescription: Provide the 'Beta App Description' when uploading a new build - - betaAppFeedbackEmail: Provide the beta app email when uploading a new build - - localizedBuildInfo: Localized beta app test info for what's new - - changelog: Provide the 'What to Test' text when uploading a new build - - skipSubmission: Skip the distributing action of pilot and only upload the ipa file - - skipWaitingForBuildProcessing: If set to true, the `distribute_external` option won't work and no build will be distributed to testers. (You might want to use this option if you are using this action on CI and have to pay for 'minutes used' on your CI plan). If set to `true` and a changelog is provided, it will partially wait for the build to appear on AppStore Connect so the changelog can be set, and skip the remaining processing steps - - updateBuildInfoOnUpload: **DEPRECATED!** Update build info immediately after validation. This is deprecated and will be removed in a future release. App Store Connect no longer supports setting build info until after build processing has completed, which is when build info is updated by default - - distributeOnly: Distribute a previously uploaded build (equivalent to the `fastlane pilot distribute` command) - - usesNonExemptEncryption: Provide the 'Uses Non-Exempt Encryption' for export compliance. This is used if there is 'ITSAppUsesNonExemptEncryption' is not set in the Info.plist - - distributeExternal: Should the build be distributed to external testers? If set to true, use of `groups` option is required - - notifyExternalTesters: Should notify external testers? (Not setting a value will use App Store Connect's default which is to notify) - - appVersion: The version number of the application build to distribute. If the version number is not specified, then the most recent build uploaded to TestFlight will be distributed. If specified, the most recent build for the version number will be distributed - - buildNumber: The build number of the application build to distribute. If the build number is not specified, the most recent build is distributed - - expirePreviousBuilds: Should expire previous builds? - - firstName: The tester's first name - - lastName: The tester's last name - - email: The tester's email - - testersFilePath: Path to a CSV file of testers - - groups: Associate tester to one group or more by group name / group id. E.g. `-g "Team 1","Team 2"` This is required when `distribute_external` option is set to true or when we want to add a tester to one or more external testing groups - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - devPortalTeamId: The short ID of your team in the developer portal, if you're in multiple teams. Different from your iTC team ID! - - itcProvider: The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column - - providerPublicId: The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication - - waitProcessingInterval: Interval in seconds to wait for App Store Connect processing - - waitProcessingTimeoutDuration: Timeout duration in seconds to wait for App Store Connect processing. If set, after exceeding timeout duration, this will `force stop` to wait for App Store Connect processing and exit with exception - - waitForUploadedBuild: **DEPRECATED!** No longer needed with the transition over to the App Store Connect API - Use version info from uploaded ipa file to determine what build to use for distribution. If set to false, latest processing or any latest build will be used - - rejectBuildWaitingForReview: Expire previous if it's 'waiting for review' - - submitBetaReview: Send the build for a beta review - - More details can be found on https://docs.fastlane.tools/actions/pilot/. - This integration will only do the TestFlight upload. - */ -public func pilot(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - appPlatform: OptionalConfigValue = .fastlaneDefault(nil), - appleId: OptionalConfigValue = .fastlaneDefault(nil), - ipa: OptionalConfigValue = .fastlaneDefault(nil), - pkg: OptionalConfigValue = .fastlaneDefault(nil), - demoAccountRequired: OptionalConfigValue = .fastlaneDefault(nil), - betaAppReviewInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - localizedAppInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - betaAppDescription: OptionalConfigValue = .fastlaneDefault(nil), - betaAppFeedbackEmail: OptionalConfigValue = .fastlaneDefault(nil), - localizedBuildInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - changelog: OptionalConfigValue = .fastlaneDefault(nil), - skipSubmission: OptionalConfigValue = .fastlaneDefault(false), - skipWaitingForBuildProcessing: OptionalConfigValue = .fastlaneDefault(false), - updateBuildInfoOnUpload: OptionalConfigValue = .fastlaneDefault(false), - distributeOnly: OptionalConfigValue = .fastlaneDefault(false), - usesNonExemptEncryption: OptionalConfigValue = .fastlaneDefault(false), - distributeExternal: OptionalConfigValue = .fastlaneDefault(false), - notifyExternalTesters: Any? = nil, - appVersion: OptionalConfigValue = .fastlaneDefault(nil), - buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - expirePreviousBuilds: OptionalConfigValue = .fastlaneDefault(false), - firstName: OptionalConfigValue = .fastlaneDefault(nil), - lastName: OptionalConfigValue = .fastlaneDefault(nil), - email: OptionalConfigValue = .fastlaneDefault(nil), - testersFilePath: String = "./testers.csv", - groups: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - teamId: Any? = nil, - teamName: OptionalConfigValue = .fastlaneDefault(nil), - devPortalTeamId: OptionalConfigValue = .fastlaneDefault(nil), - itcProvider: OptionalConfigValue = .fastlaneDefault(nil), - providerPublicId: OptionalConfigValue = .fastlaneDefault(nil), - waitProcessingInterval: Int = 30, - waitProcessingTimeoutDuration: OptionalConfigValue = .fastlaneDefault(nil), - waitForUploadedBuild: OptionalConfigValue = .fastlaneDefault(false), - rejectBuildWaitingForReview: OptionalConfigValue = .fastlaneDefault(false), - submitBetaReview: OptionalConfigValue = .fastlaneDefault(true)) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let appPlatformArg = appPlatform.asRubyArgument(name: "app_platform", type: nil) - let appleIdArg = appleId.asRubyArgument(name: "apple_id", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let pkgArg = pkg.asRubyArgument(name: "pkg", type: nil) - let demoAccountRequiredArg = demoAccountRequired.asRubyArgument(name: "demo_account_required", type: nil) - let betaAppReviewInfoArg = betaAppReviewInfo.asRubyArgument(name: "beta_app_review_info", type: nil) - let localizedAppInfoArg = localizedAppInfo.asRubyArgument(name: "localized_app_info", type: nil) - let betaAppDescriptionArg = betaAppDescription.asRubyArgument(name: "beta_app_description", type: nil) - let betaAppFeedbackEmailArg = betaAppFeedbackEmail.asRubyArgument(name: "beta_app_feedback_email", type: nil) - let localizedBuildInfoArg = localizedBuildInfo.asRubyArgument(name: "localized_build_info", type: nil) - let changelogArg = changelog.asRubyArgument(name: "changelog", type: nil) - let skipSubmissionArg = skipSubmission.asRubyArgument(name: "skip_submission", type: nil) - let skipWaitingForBuildProcessingArg = skipWaitingForBuildProcessing.asRubyArgument(name: "skip_waiting_for_build_processing", type: nil) - let updateBuildInfoOnUploadArg = updateBuildInfoOnUpload.asRubyArgument(name: "update_build_info_on_upload", type: nil) - let distributeOnlyArg = distributeOnly.asRubyArgument(name: "distribute_only", type: nil) - let usesNonExemptEncryptionArg = usesNonExemptEncryption.asRubyArgument(name: "uses_non_exempt_encryption", type: nil) - let distributeExternalArg = distributeExternal.asRubyArgument(name: "distribute_external", type: nil) - let notifyExternalTestersArg = RubyCommand.Argument(name: "notify_external_testers", value: notifyExternalTesters, type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let expirePreviousBuildsArg = expirePreviousBuilds.asRubyArgument(name: "expire_previous_builds", type: nil) - let firstNameArg = firstName.asRubyArgument(name: "first_name", type: nil) - let lastNameArg = lastName.asRubyArgument(name: "last_name", type: nil) - let emailArg = email.asRubyArgument(name: "email", type: nil) - let testersFilePathArg = RubyCommand.Argument(name: "testers_file_path", value: testersFilePath, type: nil) - let groupsArg = groups.asRubyArgument(name: "groups", type: nil) - let teamIdArg = RubyCommand.Argument(name: "team_id", value: teamId, type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let devPortalTeamIdArg = devPortalTeamId.asRubyArgument(name: "dev_portal_team_id", type: nil) - let itcProviderArg = itcProvider.asRubyArgument(name: "itc_provider", type: nil) - let providerPublicIdArg = providerPublicId.asRubyArgument(name: "provider_public_id", type: nil) - let waitProcessingIntervalArg = RubyCommand.Argument(name: "wait_processing_interval", value: waitProcessingInterval, type: nil) - let waitProcessingTimeoutDurationArg = waitProcessingTimeoutDuration.asRubyArgument(name: "wait_processing_timeout_duration", type: nil) - let waitForUploadedBuildArg = waitForUploadedBuild.asRubyArgument(name: "wait_for_uploaded_build", type: nil) - let rejectBuildWaitingForReviewArg = rejectBuildWaitingForReview.asRubyArgument(name: "reject_build_waiting_for_review", type: nil) - let submitBetaReviewArg = submitBetaReview.asRubyArgument(name: "submit_beta_review", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - usernameArg, - appIdentifierArg, - appPlatformArg, - appleIdArg, - ipaArg, - pkgArg, - demoAccountRequiredArg, - betaAppReviewInfoArg, - localizedAppInfoArg, - betaAppDescriptionArg, - betaAppFeedbackEmailArg, - localizedBuildInfoArg, - changelogArg, - skipSubmissionArg, - skipWaitingForBuildProcessingArg, - updateBuildInfoOnUploadArg, - distributeOnlyArg, - usesNonExemptEncryptionArg, - distributeExternalArg, - notifyExternalTestersArg, - appVersionArg, - buildNumberArg, - expirePreviousBuildsArg, - firstNameArg, - lastNameArg, - emailArg, - testersFilePathArg, - groupsArg, - teamIdArg, - teamNameArg, - devPortalTeamIdArg, - itcProviderArg, - providerPublicIdArg, - waitProcessingIntervalArg, - waitProcessingTimeoutDurationArg, - waitForUploadedBuildArg, - rejectBuildWaitingForReviewArg, - submitBetaReviewArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "pilot", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - No description provided - - - parameters: - - outputPath: - - templatePath: - - cachePath: - */ -public func pluginScores(outputPath: String, - templatePath: String, - cachePath: String) -{ - let outputPathArg = RubyCommand.Argument(name: "output_path", value: outputPath, type: nil) - let templatePathArg = RubyCommand.Argument(name: "template_path", value: templatePath, type: nil) - let cachePathArg = RubyCommand.Argument(name: "cache_path", value: cachePath, type: nil) - let array: [RubyCommand.Argument?] = [outputPathArg, - templatePathArg, - cachePathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "plugin_scores", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Pod lib lint - - - parameters: - - useBundleExec: Use bundle exec when there is a Gemfile presented - - podspec: Path of spec to lint - - verbose: Allow output detail in console - - allowWarnings: Allow warnings during pod lint - - sources: The sources of repos you want the pod spec to lint with, separated by commas - - subspec: A specific subspec to lint instead of the entire spec - - includePodspecs: A Glob of additional ancillary podspecs which are used for linting via :path (available since cocoapods >= 1.7) - - externalPodspecs: A Glob of additional ancillary podspecs which are used for linting via :podspec. If there are --include-podspecs, then these are removed from them (available since cocoapods >= 1.7) - - swiftVersion: The SWIFT_VERSION that should be used to lint the spec. This takes precedence over a .swift-version file - - useLibraries: Lint uses static libraries to install the spec - - useModularHeaders: Lint using modular libraries (available since cocoapods >= 1.6) - - failFast: Lint stops on the first failing platform or subspec - - private: Lint skips checks that apply only to public specs - - quick: Lint skips checks that would require to download and build the spec - - noClean: Lint leaves the build directory intact for inspection - - noSubspecs: Lint skips validation of subspecs - - platforms: Lint against specific platforms (defaults to all platforms supported by the podspec). Multiple platforms must be comma-delimited (available since cocoapods >= 1.6) - - skipImportValidation: Lint skips validating that the pod can be imported (available since cocoapods >= 1.3) - - skipTests: Lint skips building and running tests during validation (available since cocoapods >= 1.3) - - analyze: Validate with the Xcode Static Analysis tool (available since cocoapods >= 1.6.1) - - Test the syntax of your Podfile by linting the pod against the files of its directory - */ -public func podLibLint(useBundleExec: OptionalConfigValue = .fastlaneDefault(true), - podspec: OptionalConfigValue = .fastlaneDefault(nil), - verbose: OptionalConfigValue = .fastlaneDefault(nil), - allowWarnings: OptionalConfigValue = .fastlaneDefault(nil), - sources: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - subspec: OptionalConfigValue = .fastlaneDefault(nil), - includePodspecs: OptionalConfigValue = .fastlaneDefault(nil), - externalPodspecs: OptionalConfigValue = .fastlaneDefault(nil), - swiftVersion: OptionalConfigValue = .fastlaneDefault(nil), - useLibraries: OptionalConfigValue = .fastlaneDefault(false), - useModularHeaders: OptionalConfigValue = .fastlaneDefault(false), - failFast: OptionalConfigValue = .fastlaneDefault(false), - private: OptionalConfigValue = .fastlaneDefault(false), - quick: OptionalConfigValue = .fastlaneDefault(false), - noClean: OptionalConfigValue = .fastlaneDefault(false), - noSubspecs: OptionalConfigValue = .fastlaneDefault(false), - platforms: OptionalConfigValue = .fastlaneDefault(nil), - skipImportValidation: OptionalConfigValue = .fastlaneDefault(false), - skipTests: OptionalConfigValue = .fastlaneDefault(false), - analyze: OptionalConfigValue = .fastlaneDefault(false)) -{ - let useBundleExecArg = useBundleExec.asRubyArgument(name: "use_bundle_exec", type: nil) - let podspecArg = podspec.asRubyArgument(name: "podspec", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let allowWarningsArg = allowWarnings.asRubyArgument(name: "allow_warnings", type: nil) - let sourcesArg = sources.asRubyArgument(name: "sources", type: nil) - let subspecArg = subspec.asRubyArgument(name: "subspec", type: nil) - let includePodspecsArg = includePodspecs.asRubyArgument(name: "include_podspecs", type: nil) - let externalPodspecsArg = externalPodspecs.asRubyArgument(name: "external_podspecs", type: nil) - let swiftVersionArg = swiftVersion.asRubyArgument(name: "swift_version", type: nil) - let useLibrariesArg = useLibraries.asRubyArgument(name: "use_libraries", type: nil) - let useModularHeadersArg = useModularHeaders.asRubyArgument(name: "use_modular_headers", type: nil) - let failFastArg = failFast.asRubyArgument(name: "fail_fast", type: nil) - let privateArg = `private`.asRubyArgument(name: "private", type: nil) - let quickArg = quick.asRubyArgument(name: "quick", type: nil) - let noCleanArg = noClean.asRubyArgument(name: "no_clean", type: nil) - let noSubspecsArg = noSubspecs.asRubyArgument(name: "no_subspecs", type: nil) - let platformsArg = platforms.asRubyArgument(name: "platforms", type: nil) - let skipImportValidationArg = skipImportValidation.asRubyArgument(name: "skip_import_validation", type: nil) - let skipTestsArg = skipTests.asRubyArgument(name: "skip_tests", type: nil) - let analyzeArg = analyze.asRubyArgument(name: "analyze", type: nil) - let array: [RubyCommand.Argument?] = [useBundleExecArg, - podspecArg, - verboseArg, - allowWarningsArg, - sourcesArg, - subspecArg, - includePodspecsArg, - externalPodspecsArg, - swiftVersionArg, - useLibrariesArg, - useModularHeadersArg, - failFastArg, - privateArg, - quickArg, - noCleanArg, - noSubspecsArg, - platformsArg, - skipImportValidationArg, - skipTestsArg, - analyzeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "pod_lib_lint", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Push a Podspec to Trunk or a private repository - - - parameters: - - useBundleExec: Use bundle exec when there is a Gemfile presented - - path: The Podspec you want to push - - repo: The repo you want to push. Pushes to Trunk by default - - allowWarnings: Allow warnings during pod push - - useLibraries: Allow lint to use static libraries to install the spec - - sources: The sources of repos you want the pod spec to lint with, separated by commas - - swiftVersion: The SWIFT_VERSION that should be used to lint the spec. This takes precedence over a .swift-version file - - skipImportValidation: Lint skips validating that the pod can be imported - - skipTests: Lint skips building and running tests during validation - - useJson: Convert the podspec to JSON before pushing it to the repo - - verbose: Show more debugging information - - useModularHeaders: Use modular headers option during validation - - synchronous: If validation depends on other recently pushed pods, synchronize - - noOverwrite: Disallow pushing that would overwrite an existing spec - - localOnly: Does not perform the step of pushing REPO to its remote - */ -public func podPush(useBundleExec: OptionalConfigValue = .fastlaneDefault(false), - path: OptionalConfigValue = .fastlaneDefault(nil), - repo: OptionalConfigValue = .fastlaneDefault(nil), - allowWarnings: OptionalConfigValue = .fastlaneDefault(nil), - useLibraries: OptionalConfigValue = .fastlaneDefault(nil), - sources: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - swiftVersion: OptionalConfigValue = .fastlaneDefault(nil), - skipImportValidation: OptionalConfigValue = .fastlaneDefault(nil), - skipTests: OptionalConfigValue = .fastlaneDefault(nil), - useJson: OptionalConfigValue = .fastlaneDefault(nil), - verbose: OptionalConfigValue = .fastlaneDefault(false), - useModularHeaders: OptionalConfigValue = .fastlaneDefault(nil), - synchronous: OptionalConfigValue = .fastlaneDefault(nil), - noOverwrite: OptionalConfigValue = .fastlaneDefault(nil), - localOnly: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let useBundleExecArg = useBundleExec.asRubyArgument(name: "use_bundle_exec", type: nil) - let pathArg = path.asRubyArgument(name: "path", type: nil) - let repoArg = repo.asRubyArgument(name: "repo", type: nil) - let allowWarningsArg = allowWarnings.asRubyArgument(name: "allow_warnings", type: nil) - let useLibrariesArg = useLibraries.asRubyArgument(name: "use_libraries", type: nil) - let sourcesArg = sources.asRubyArgument(name: "sources", type: nil) - let swiftVersionArg = swiftVersion.asRubyArgument(name: "swift_version", type: nil) - let skipImportValidationArg = skipImportValidation.asRubyArgument(name: "skip_import_validation", type: nil) - let skipTestsArg = skipTests.asRubyArgument(name: "skip_tests", type: nil) - let useJsonArg = useJson.asRubyArgument(name: "use_json", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let useModularHeadersArg = useModularHeaders.asRubyArgument(name: "use_modular_headers", type: nil) - let synchronousArg = synchronous.asRubyArgument(name: "synchronous", type: nil) - let noOverwriteArg = noOverwrite.asRubyArgument(name: "no_overwrite", type: nil) - let localOnlyArg = localOnly.asRubyArgument(name: "local_only", type: nil) - let array: [RubyCommand.Argument?] = [useBundleExecArg, - pathArg, - repoArg, - allowWarningsArg, - useLibrariesArg, - sourcesArg, - swiftVersionArg, - skipImportValidationArg, - skipTestsArg, - useJsonArg, - verboseArg, - useModularHeadersArg, - synchronousArg, - noOverwriteArg, - localOnlyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "pod_push", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Creates or updates an item within your Podio app - - - parameters: - - clientId: Client ID for Podio API (see https://developers.podio.com/api-key) - - clientSecret: Client secret for Podio API (see https://developers.podio.com/api-key) - - appId: App ID of the app you intend to authenticate with (see https://developers.podio.com/authentication/app_auth) - - appToken: App token of the app you intend to authenticate with (see https://developers.podio.com/authentication/app_auth) - - identifyingField: String specifying the field key used for identification of an item - - identifyingValue: String uniquely specifying an item within the app - - otherFields: Dictionary of your app fields. Podio supports several field types, see https://developers.podio.com/doc/items - - Use this action to create or update an item within your Podio app (see [https://help.podio.com/hc/en-us/articles/201019278-Creating-apps-](https://help.podio.com/hc/en-us/articles/201019278-Creating-apps-)). - Pass in dictionary with field keys and their values. - Field key is located under `Modify app` -> `Advanced` -> `Developer` -> `External ID` (see [https://developers.podio.com/examples/items](https://developers.podio.com/examples/items)). - */ -public func podioItem(clientId: String, - clientSecret: String, - appId: String, - appToken: String, - identifyingField: String, - identifyingValue: String, - otherFields: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil)) -{ - let clientIdArg = RubyCommand.Argument(name: "client_id", value: clientId, type: nil) - let clientSecretArg = RubyCommand.Argument(name: "client_secret", value: clientSecret, type: nil) - let appIdArg = RubyCommand.Argument(name: "app_id", value: appId, type: nil) - let appTokenArg = RubyCommand.Argument(name: "app_token", value: appToken, type: nil) - let identifyingFieldArg = RubyCommand.Argument(name: "identifying_field", value: identifyingField, type: nil) - let identifyingValueArg = RubyCommand.Argument(name: "identifying_value", value: identifyingValue, type: nil) - let otherFieldsArg = otherFields.asRubyArgument(name: "other_fields", type: nil) - let array: [RubyCommand.Argument?] = [clientIdArg, - clientSecretArg, - appIdArg, - appTokenArg, - identifyingFieldArg, - identifyingValueArg, - otherFieldsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "podio_item", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `check_app_store_metadata` action - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - appIdentifier: The bundle identifier of your app - - username: Your Apple ID Username - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - platform: The platform to use (optional) - - defaultRuleLevel: The default rule level unless otherwise configured - - includeInAppPurchases: Should check in-app purchases? - - useLive: Should force check live app? - - freeStuffInIap: using text indicating that your IAP is free - - - returns: true if precheck passes, else, false - - More information: https://fastlane.tools/precheck - */ -@discardableResult public func precheck(apiKeyPath: OptionalConfigValue = .fastlaneDefault(precheckfile.apiKeyPath), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(precheckfile.apiKey), - appIdentifier: String = precheckfile.appIdentifier, - username: OptionalConfigValue = .fastlaneDefault(precheckfile.username), - teamId: OptionalConfigValue = .fastlaneDefault(precheckfile.teamId), - teamName: OptionalConfigValue = .fastlaneDefault(precheckfile.teamName), - platform: String = precheckfile.platform, - defaultRuleLevel: Any = precheckfile.defaultRuleLevel, - includeInAppPurchases: OptionalConfigValue = .fastlaneDefault(precheckfile.includeInAppPurchases), - useLive: OptionalConfigValue = .fastlaneDefault(precheckfile.useLive), - freeStuffInIap: Any? = precheckfile.freeStuffInIap) -> Bool -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let defaultRuleLevelArg = RubyCommand.Argument(name: "default_rule_level", value: defaultRuleLevel, type: nil) - let includeInAppPurchasesArg = includeInAppPurchases.asRubyArgument(name: "include_in_app_purchases", type: nil) - let useLiveArg = useLive.asRubyArgument(name: "use_live", type: nil) - let freeStuffInIapArg = RubyCommand.Argument(name: "free_stuff_in_iap", value: freeStuffInIap, type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - appIdentifierArg, - usernameArg, - teamIdArg, - teamNameArg, - platformArg, - defaultRuleLevelArg, - includeInAppPurchasesArg, - useLiveArg, - freeStuffInIapArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "precheck", className: nil, args: args) - return parseBool(fromString: runner.executeCommand(command)) -} - -/** - Alias for the `puts` action - - - parameter message: Message to be printed out - */ -public func println(message: OptionalConfigValue = .fastlaneDefault(nil)) { - let messageArg = message.asRubyArgument(name: "message", type: nil) - let array: [RubyCommand.Argument?] = [messageArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "println", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `create_app_online` action - - - parameters: - - username: Your Apple ID Username - - appIdentifier: App Identifier (Bundle ID, e.g. com.krausefx.app) - - bundleIdentifierSuffix: App Identifier Suffix (Ignored if App Identifier does not end with .*) - - appName: App Name - - appVersion: Initial version number (e.g. '1.0') - - sku: SKU Number (e.g. '1234') - - platform: The platform to use (optional) - - platforms: The platforms to use (optional) - - language: Primary Language (e.g. 'en-US', 'fr-FR') - - companyName: The name of your company. It's used to set company name on App Store Connect team's app pages. Only required if it's the first app you create - - skipItc: Skip the creation of the app on App Store Connect - - itcUsers: Array of App Store Connect users. If provided, you can limit access to this newly created app for users with the App Manager, Developer, Marketer or Sales roles - - enabledFeatures: **DEPRECATED!** Please use `enable_services` instead - Array with Spaceship App Services - - enableServices: Array with Spaceship App Services (e.g. access_wifi: (on|off), app_attest: (on|off), app_group: (on|off), apple_pay: (on|off), associated_domains: (on|off), auto_fill_credential: (on|off), class_kit: (on|off), declared_age_range: (on|off), icloud: (legacy|cloudkit), custom_network_protocol: (on|off), data_protection: (complete|unlessopen|untilfirstauth), extended_virtual_address_space: (on|off), family_controls: (on|off), file_provider_testing_mode: (on|off), fonts: (on|off), game_center: (ios|mac), health_kit: (on|off), hls_interstitial_preview: (on|off), home_kit: (on|off), hotspot: (on|off), in_app_purchase: (on|off), inter_app_audio: (on|off), low_latency_hls: (on|off), managed_associated_domains: (on|off), maps: (on|off), multipath: (on|off), network_extension: (on|off), nfc_tag_reading: (on|off), personal_vpn: (on|off), passbook: (on|off), push_notification: (on|off), sign_in_with_apple: (on), siri_kit: (on|off), system_extension: (on|off), user_management: (on|off), vpn_configuration: (on|off), wallet: (on|off), wireless_accessory: (on|off), car_play_audio_app: (on|off), car_play_messaging_app: (on|off), car_play_navigation_app: (on|off), car_play_voip_calling_app: (on|off), critical_alerts: (on|off), hotspot_helper: (on|off), driver_kit: (on|off), driver_kit_endpoint_security: (on|off), driver_kit_family_hid_device: (on|off), driver_kit_family_networking: (on|off), driver_kit_family_serial: (on|off), driver_kit_hid_event_service: (on|off), driver_kit_transport_hid: (on|off), multitasking_camera_access: (on|off), sf_universal_link_api: (on|off), vp9_decoder: (on|off), music_kit: (on|off), shazam_kit: (on|off), communication_notifications: (on|off), group_activities: (on|off), health_kit_estimate_recalibration: (on|off), time_sensitive_notifications: (on|off)) - - skipDevcenter: Skip the creation of the app on the Apple Developer Portal - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - itcTeamId: The ID of your App Store Connect team if you're in multiple teams - - itcTeamName: The name of your App Store Connect team if you're in multiple teams - - Create new apps on App Store Connect and Apple Developer Portal via _produce_. - If the app already exists, `create_app_online` will not do anything. - For more information about _produce_, visit its documentation page: [https://docs.fastlane.tools/actions/produce/](https://docs.fastlane.tools/actions/produce/). - */ -public func produce(username: String, - appIdentifier: String, - bundleIdentifierSuffix: OptionalConfigValue = .fastlaneDefault(nil), - appName: String, - appVersion: OptionalConfigValue = .fastlaneDefault(nil), - sku: String, - platform: String = "ios", - platforms: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - language: String = "English", - companyName: OptionalConfigValue = .fastlaneDefault(nil), - skipItc: OptionalConfigValue = .fastlaneDefault(false), - itcUsers: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - enabledFeatures: [String: Any] = [:], - enableServices: [String: Any] = [:], - skipDevcenter: OptionalConfigValue = .fastlaneDefault(false), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - itcTeamId: Any? = nil, - itcTeamName: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let bundleIdentifierSuffixArg = bundleIdentifierSuffix.asRubyArgument(name: "bundle_identifier_suffix", type: nil) - let appNameArg = RubyCommand.Argument(name: "app_name", value: appName, type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let skuArg = RubyCommand.Argument(name: "sku", value: sku, type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let platformsArg = platforms.asRubyArgument(name: "platforms", type: nil) - let languageArg = RubyCommand.Argument(name: "language", value: language, type: nil) - let companyNameArg = companyName.asRubyArgument(name: "company_name", type: nil) - let skipItcArg = skipItc.asRubyArgument(name: "skip_itc", type: nil) - let itcUsersArg = itcUsers.asRubyArgument(name: "itc_users", type: nil) - let enabledFeaturesArg = RubyCommand.Argument(name: "enabled_features", value: enabledFeatures, type: nil) - let enableServicesArg = RubyCommand.Argument(name: "enable_services", value: enableServices, type: nil) - let skipDevcenterArg = skipDevcenter.asRubyArgument(name: "skip_devcenter", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let itcTeamIdArg = RubyCommand.Argument(name: "itc_team_id", value: itcTeamId, type: nil) - let itcTeamNameArg = itcTeamName.asRubyArgument(name: "itc_team_name", type: nil) - let array: [RubyCommand.Argument?] = [usernameArg, - appIdentifierArg, - bundleIdentifierSuffixArg, - appNameArg, - appVersionArg, - skuArg, - platformArg, - platformsArg, - languageArg, - companyNameArg, - skipItcArg, - itcUsersArg, - enabledFeaturesArg, - enableServicesArg, - skipDevcenterArg, - teamIdArg, - teamNameArg, - itcTeamIdArg, - itcTeamNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "produce", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Ask the user for a value or for confirmation - - - parameters: - - text: The text that will be displayed to the user - - ciInput: The default text that will be used when being executed on a CI service - - boolean: Is that a boolean question (yes/no)? This will add (y/n) at the end - - secureText: Is that a secure text (yes/no)? - - multiLineEndKeyword: Enable multi-line inputs by providing an end text (e.g. 'END') which will stop the user input - - You can use `prompt` to ask the user for a value or to just let the user confirm the next step. - When this is executed on a CI service, the passed `ci_input` value will be returned. - This action also supports multi-line inputs using the `multi_line_end_keyword` option. - */ -@discardableResult public func prompt(text: String = "Please enter some text: ", - ciInput: String = "", - boolean: OptionalConfigValue = .fastlaneDefault(false), - secureText: OptionalConfigValue = .fastlaneDefault(false), - multiLineEndKeyword: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let textArg = RubyCommand.Argument(name: "text", value: text, type: nil) - let ciInputArg = RubyCommand.Argument(name: "ci_input", value: ciInput, type: nil) - let booleanArg = boolean.asRubyArgument(name: "boolean", type: nil) - let secureTextArg = secureText.asRubyArgument(name: "secure_text", type: nil) - let multiLineEndKeywordArg = multiLineEndKeyword.asRubyArgument(name: "multi_line_end_keyword", type: nil) - let array: [RubyCommand.Argument?] = [textArg, - ciInputArg, - booleanArg, - secureTextArg, - multiLineEndKeywordArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "prompt", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Push local tags to the remote - this will only push tags - - - parameters: - - force: Force push to remote - - remote: The remote to push tags to - - tag: The tag to push to remote - - If you only want to push the tags and nothing else, you can use the `push_git_tags` action - */ -public func pushGitTags(force: OptionalConfigValue = .fastlaneDefault(false), - remote: String = "origin", - tag: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let forceArg = force.asRubyArgument(name: "force", type: nil) - let remoteArg = RubyCommand.Argument(name: "remote", value: remote, type: nil) - let tagArg = tag.asRubyArgument(name: "tag", type: nil) - let array: [RubyCommand.Argument?] = [forceArg, - remoteArg, - tagArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "push_git_tags", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Push local changes to the remote branch - - - parameters: - - localBranch: The local branch to push from. Defaults to the current branch - - remoteBranch: The remote branch to push to. Defaults to the local branch - - force: Force push to remote - - forceWithLease: Force push with lease to remote - - tags: Whether tags are pushed to remote - - remote: The remote to push to - - noVerify: Whether or not to use --no-verify - - setUpstream: Whether or not to use --set-upstream - - pushOptions: Array of strings to be passed using the '--push-option' option - - Lets you push your local commits to a remote git repo. Useful if you make local changes such as adding a version bump commit (using `commit_version_bump`) or a git tag (using 'add_git_tag') on a CI server, and you want to push those changes back to your canonical/main repo. - If this is a new branch, use the `set_upstream` option to set the remote branch as upstream. - */ -public func pushToGitRemote(localBranch: OptionalConfigValue = .fastlaneDefault(nil), - remoteBranch: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - forceWithLease: OptionalConfigValue = .fastlaneDefault(false), - tags: OptionalConfigValue = .fastlaneDefault(true), - remote: String = "origin", - noVerify: OptionalConfigValue = .fastlaneDefault(false), - setUpstream: OptionalConfigValue = .fastlaneDefault(false), - pushOptions: [String] = []) -{ - let localBranchArg = localBranch.asRubyArgument(name: "local_branch", type: nil) - let remoteBranchArg = remoteBranch.asRubyArgument(name: "remote_branch", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let forceWithLeaseArg = forceWithLease.asRubyArgument(name: "force_with_lease", type: nil) - let tagsArg = tags.asRubyArgument(name: "tags", type: nil) - let remoteArg = RubyCommand.Argument(name: "remote", value: remote, type: nil) - let noVerifyArg = noVerify.asRubyArgument(name: "no_verify", type: nil) - let setUpstreamArg = setUpstream.asRubyArgument(name: "set_upstream", type: nil) - let pushOptionsArg = RubyCommand.Argument(name: "push_options", value: pushOptions, type: nil) - let array: [RubyCommand.Argument?] = [localBranchArg, - remoteBranchArg, - forceArg, - forceWithLeaseArg, - tagsArg, - remoteArg, - noVerifyArg, - setUpstreamArg, - pushOptionsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "push_to_git_remote", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Prints out the given text - - - parameter message: Message to be printed out - */ -public func puts(message: OptionalConfigValue = .fastlaneDefault(nil)) { - let messageArg = message.asRubyArgument(name: "message", type: nil) - let array: [RubyCommand.Argument?] = [messageArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "puts", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Loads a CocoaPods spec as JSON - - - parameter path: Path to the podspec to be read - - This can be used for only specifying a version string in your podspec - and during your release process you'd read it from the podspec by running `version = read_podspec['version']` at the beginning of your lane. - Loads the specified (or the first found) podspec in the folder as JSON, so that you can inspect its `version`, `files` etc. - This can be useful when basing your release process on the version string only stored in one place - in the podspec. - As one of the first steps you'd read the podspec and its version and the rest of the workflow can use that version string (when e.g. creating a new git tag or a GitHub Release). - */ -@discardableResult public func readPodspec(path: String) -> [String: Any] { - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let array: [RubyCommand.Argument?] = [pathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "read_podspec", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Recreate not shared Xcode project schemes - - - parameter project: The Xcode project - */ -public func recreateSchemes(project: String) { - let projectArg = RubyCommand.Argument(name: "project", value: project, type: nil) - let array: [RubyCommand.Argument?] = [projectArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "recreate_schemes", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Registers a new device to the Apple Dev Portal - - - parameters: - - name: Provide the name of the device to register as - - platform: Provide the platform of the device to register as (ios, mac) - - udid: Provide the UDID of the device to register as - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - username: Optional: Your Apple ID - - This will register an iOS device with the Developer Portal so that you can include it in your provisioning profiles. - This is an optimistic action, in that it will only ever add a device to the member center. If the device has already been registered within the member center, it will be left alone in the member center. - The action will connect to the Apple Developer Portal using the username you specified in your `Appfile` with `apple_id`, but you can override it using the `:username` option. - */ -@discardableResult public func registerDevice(name: String, - platform: String = "ios", - udid: String, - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let nameArg = RubyCommand.Argument(name: "name", value: name, type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let udidArg = RubyCommand.Argument(name: "udid", value: udid, type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let array: [RubyCommand.Argument?] = [nameArg, - platformArg, - udidArg, - apiKeyPathArg, - apiKeyArg, - teamIdArg, - teamNameArg, - usernameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "register_device", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Registers new devices to the Apple Dev Portal - - - parameters: - - devices: A hash of devices, with the name as key and the UDID as value - - devicesFile: Provide a path to a file with the devices to register. For the format of the file see the examples - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - username: Optional: Your Apple ID - - platform: The platform to use (optional) - - This will register iOS/Mac devices with the Developer Portal so that you can include them in your provisioning profiles. - This is an optimistic action, in that it will only ever add new devices to the member center, and never remove devices. If a device which has already been registered within the member center is not passed to this action, it will be left alone in the member center and continue to work. - The action will connect to the Apple Developer Portal using the username you specified in your `Appfile` with `apple_id`, but you can override it using the `username` option, or by setting the env variable `ENV['DELIVER_USER']`. - */ -public func registerDevices(devices: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - devicesFile: OptionalConfigValue = .fastlaneDefault(nil), - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios") -{ - let devicesArg = devices.asRubyArgument(name: "devices", type: nil) - let devicesFileArg = devicesFile.asRubyArgument(name: "devices_file", type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let array: [RubyCommand.Argument?] = [devicesArg, - devicesFileArg, - apiKeyPathArg, - apiKeyArg, - teamIdArg, - teamNameArg, - usernameArg, - platformArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "register_devices", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Resets git repo to a clean state by discarding uncommitted changes - - - parameters: - - files: Array of files the changes should be discarded. If not given, all files will be discarded - - force: Skip verifying of previously clean state of repo. Only recommended in combination with `files` option - - skipClean: Skip 'git clean' to avoid removing untracked files like `.env` - - disregardGitignore: Setting this to true will clean the whole repository, ignoring anything in your local .gitignore. Set this to true if you want the equivalent of a fresh clone, and for all untracked and ignore files to also be removed - - exclude: You can pass a string, or array of, file pattern(s) here which you want to have survive the cleaning process, and remain on disk, e.g. to leave the `artifacts` directory you would specify `exclude: 'artifacts'`. Make sure this pattern is also in your gitignore! See the gitignore documentation for info on patterns - - This action will reset your git repo to a clean state, discarding any uncommitted and untracked changes. Useful in case you need to revert the repo back to a clean state, e.g. after running _fastlane_. - Untracked files like `.env` will also be deleted, unless `:skip_clean` is true. - It's a pretty drastic action so it comes with a sort of safety latch. It will only proceed with the reset if this condition is met:| - | - >- You have called the `ensure_git_status_clean` action prior to calling this action. This ensures that your repo started off in a clean state, so the only things that will get destroyed by this action are files that are created as a byproduct of the fastlane run.| - >| - */ -public func resetGitRepo(files: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - skipClean: OptionalConfigValue = .fastlaneDefault(false), - disregardGitignore: OptionalConfigValue = .fastlaneDefault(true), - exclude: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let filesArg = files.asRubyArgument(name: "files", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let skipCleanArg = skipClean.asRubyArgument(name: "skip_clean", type: nil) - let disregardGitignoreArg = disregardGitignore.asRubyArgument(name: "disregard_gitignore", type: nil) - let excludeArg = exclude.asRubyArgument(name: "exclude", type: nil) - let array: [RubyCommand.Argument?] = [filesArg, - forceArg, - skipCleanArg, - disregardGitignoreArg, - excludeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "reset_git_repo", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Shutdown and reset running simulators - - - parameters: - - ios: **DEPRECATED!** Use `:os_versions` instead - Which OS versions of Simulators you want to reset content and settings, this does not remove/recreate the simulators - - osVersions: Which OS versions of Simulators you want to reset content and settings, this does not remove/recreate the simulators - */ -public func resetSimulatorContents(ios: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - osVersions: OptionalConfigValue<[String]?> = .fastlaneDefault(nil)) -{ - let iosArg = ios.asRubyArgument(name: "ios", type: nil) - let osVersionsArg = osVersions.asRubyArgument(name: "os_versions", type: nil) - let array: [RubyCommand.Argument?] = [iosArg, - osVersionsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "reset_simulator_contents", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Codesign an existing ipa file - - - parameters: - - ipa: Path to the ipa file to resign. Optional if you use the _gym_ or _xcodebuild_ action - - signingIdentity: Code signing identity to use. e.g. `iPhone Distribution: Luka Mirosevic (0123456789)` - - entitlements: Path to the entitlement file to use, e.g. `myApp/MyApp.entitlements` - - provisioningProfile: Path to your provisioning_profile. Optional if you use _sigh_ - - version: Version number to force resigned ipa to use. Updates both `CFBundleShortVersionString` and `CFBundleVersion` values in `Info.plist`. Applies for main app and all nested apps or extensions - - displayName: Display name to force resigned ipa to use - - shortVersion: Short version string to force resigned ipa to use (`CFBundleShortVersionString`) - - bundleVersion: Bundle version to force resigned ipa to use (`CFBundleVersion`) - - bundleId: Set new bundle ID during resign (`CFBundleIdentifier`) - - useAppEntitlements: Extract app bundle codesigning entitlements and combine with entitlements from new provisioning profile - - keychainPath: Provide a path to a keychain file that should be used by `/usr/bin/codesign` - - pagesize: Page size in bytes passed to `/usr/bin/codesign --pagesize` (must be a power of two) - */ -public func resign(ipa: String, - signingIdentity: String, - entitlements: OptionalConfigValue = .fastlaneDefault(nil), - provisioningProfile: String, - version: OptionalConfigValue = .fastlaneDefault(nil), - displayName: OptionalConfigValue = .fastlaneDefault(nil), - shortVersion: OptionalConfigValue = .fastlaneDefault(nil), - bundleVersion: OptionalConfigValue = .fastlaneDefault(nil), - bundleId: OptionalConfigValue = .fastlaneDefault(nil), - useAppEntitlements: OptionalConfigValue = .fastlaneDefault(nil), - keychainPath: OptionalConfigValue = .fastlaneDefault(nil), - pagesize: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let ipaArg = RubyCommand.Argument(name: "ipa", value: ipa, type: nil) - let signingIdentityArg = RubyCommand.Argument(name: "signing_identity", value: signingIdentity, type: nil) - let entitlementsArg = entitlements.asRubyArgument(name: "entitlements", type: nil) - let provisioningProfileArg = RubyCommand.Argument(name: "provisioning_profile", value: provisioningProfile, type: nil) - let versionArg = version.asRubyArgument(name: "version", type: nil) - let displayNameArg = displayName.asRubyArgument(name: "display_name", type: nil) - let shortVersionArg = shortVersion.asRubyArgument(name: "short_version", type: nil) - let bundleVersionArg = bundleVersion.asRubyArgument(name: "bundle_version", type: nil) - let bundleIdArg = bundleId.asRubyArgument(name: "bundle_id", type: nil) - let useAppEntitlementsArg = useAppEntitlements.asRubyArgument(name: "use_app_entitlements", type: nil) - let keychainPathArg = keychainPath.asRubyArgument(name: "keychain_path", type: nil) - let pagesizeArg = pagesize.asRubyArgument(name: "pagesize", type: nil) - let array: [RubyCommand.Argument?] = [ipaArg, - signingIdentityArg, - entitlementsArg, - provisioningProfileArg, - versionArg, - displayNameArg, - shortVersionArg, - bundleVersionArg, - bundleIdArg, - useAppEntitlementsArg, - keychainPathArg, - pagesizeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "resign", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This action restore your file that was backed up with the `backup_file` action - - - parameter path: Original file name you want to restore - */ -public func restoreFile(path: String) { - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let array: [RubyCommand.Argument?] = [pathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "restore_file", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Outputs ascii-art for a rocket 🚀 - - Print an ascii Rocket :rocket:. Useful after using _crashlytics_ or _pilot_ to indicate that your new build has been shipped to outer-space. - */ -@discardableResult public func rocket() -> String { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "rocket", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Run tests using rspec - */ -public func rspec() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "rspec", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Rsync files from :source to :destination - - - parameters: - - extra: Port - - source: source file/folder - - destination: destination file/folder - - A wrapper around `rsync`, which is a tool that lets you synchronize files, including permissions and so on. For a more detailed information about `rsync`, please see [rsync(1) man page](https://linux.die.net/man/1/rsync). - */ -public func rsync(extra: String = "-av", - source: String, - destination: String) -{ - let extraArg = RubyCommand.Argument(name: "extra", value: extra, type: nil) - let sourceArg = RubyCommand.Argument(name: "source", value: source, type: nil) - let destinationArg = RubyCommand.Argument(name: "destination", value: destination, type: nil) - let array: [RubyCommand.Argument?] = [extraArg, - sourceArg, - destinationArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "rsync", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs the code style checks - */ -public func rubocop() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "rubocop", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Verifies the minimum ruby version required - - Add this to your `Fastfile` to require a certain version of _ruby_. - Put it at the top of your `Fastfile` to ensure that _fastlane_ is executed appropriately. - */ -public func rubyVersion() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "ruby_version", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Easily run tests of your iOS app (via _scan_) - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - packagePath: Path to the Swift Package - - scheme: The project's scheme. Make sure it's marked as `Shared` - - device: The name of the simulator type you want to run tests on (e.g. 'iPhone 6' or 'iPhone SE (2nd generation) (14.5)') - - devices: Array of devices to run the tests on (e.g. ['iPhone 6', 'iPad Air', 'iPhone SE (2nd generation) (14.5)']) - - skipDetectDevices: Should skip auto detecting of devices if none were specified - - ensureDevicesFound: Should fail if devices not found - - forceQuitSimulator: Enabling this option will automatically killall Simulator processes before the run - - resetSimulator: Enabling this option will automatically erase the simulator before running the application - - disableSlideToType: Enabling this option will disable the simulator from showing the 'Slide to type' prompt - - prelaunchSimulator: Enabling this option will launch the first simulator prior to calling any xcodebuild command - - reinstallApp: Enabling this option will automatically uninstall the application before running it - - appIdentifier: The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) - - onlyTesting: Array of test identifiers to run. Expected format: TestTarget[/TestSuite[/TestCase]] - - skipTesting: Array of test identifiers to skip. Expected format: TestTarget[/TestSuite[/TestCase]] - - testplan: The testplan associated with the scheme that should be used for testing - - onlyTestConfigurations: Array of strings matching test plan configurations to run - - skipTestConfigurations: Array of strings matching test plan configurations to skip - - xctestrun: Run tests using the provided `.xctestrun` file - - toolchain: The toolchain that should be used for building the application (e.g. `com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a`) - - clean: Should the project be cleaned before building it? - - codeCoverage: Should code coverage be generated? (Xcode 7 and up) - - addressSanitizer: Should the address sanitizer be turned on? - - threadSanitizer: Should the thread sanitizer be turned on? - - openReport: Should the HTML report be opened when tests are completed? - - outputDirectory: The directory in which all reports will be stored - - outputStyle: Define how the output should look like. Valid values are: standard, basic, rspec, or raw (disables xcpretty during xcodebuild) - - outputTypes: Comma separated list of the output types (e.g. html, junit, json-compilation-database) - - outputFiles: Comma separated list of the output files, corresponding to the types provided by :output_types (order should match). If specifying an output type of json-compilation-database with :use_clang_report_name enabled, that option will take precedence - - buildlogPath: The directory where to store the raw log - - includeSimulatorLogs: If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - outputRemoveRetryAttempts: Remove retry attempts from test results table and the JUnit report (if not using xcpretty) - - disableXcpretty: **DEPRECATED!** Use `output_style: 'raw'` instead - Disable xcpretty formatting of build, similar to `output_style='raw'` but this will also skip the test results table - - formatter: **DEPRECATED!** Use 'xcpretty_formatter' instead - A custom xcpretty formatter to use - - xcprettyFormatter: A custom xcpretty formatter to use - - xcprettyArgs: Pass in xcpretty additional command line arguments (e.g. '--test --no-color' or '--tap --no-utf') - - derivedDataPath: The directory where build products and other derived data will go - - shouldZipBuildProducts: Should zip the derived data build products and place in output path? - - outputXctestrun: Should provide additional copy of .xctestrun file (settings.xctestrun) and place in output path? - - resultBundlePath: Custom path for the result bundle, overrides result_bundle - - resultBundle: Should an Xcode result bundle be generated in the output directory - - useClangReportName: Generate the json compilation database with clang naming convention (compile_commands.json) - - parallelTesting: Optionally override the per-target setting in the scheme for running tests in parallel. Equivalent to -parallel-testing-enabled - - concurrentWorkers: Specify the exact number of test runners that will be spawned during parallel testing. Equivalent to -parallel-testing-worker-count - - maxConcurrentSimulators: Constrain the number of simulator devices on which to test concurrently. Equivalent to -maximum-concurrent-test-simulator-destinations - - disableConcurrentTesting: Do not run test bundles in parallel on the specified destinations. Testing will occur on each destination serially. Equivalent to -disable-concurrent-testing - - skipBuild: Should debug build be skipped before test build? - - testWithoutBuilding: Test without building, requires a derived data path - - buildForTesting: Build for testing only, does not run tests - - sdk: The SDK that should be used for building the application - - configuration: The configuration to use when building the app. Defaults to 'Release' - - xcargs: Pass additional arguments to xcodebuild. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - appName: App name to use in slack message and logfile name - - deploymentTargetVersion: Target version of the app being build or tested. Used to filter out simulator version - - slackUrl: Create an Incoming WebHook for your Slack group to post results there - - slackChannel: #channel or @username - - slackMessage: The message included with each message posted to slack - - slackUseWebhookConfiguredUsernameAndIcon: Use webhook's default username and icon settings? (true/false) - - slackUsername: Overrides the webhook's username property if slack_use_webhook_configured_username_and_icon is false - - slackIconUrl: Overrides the webhook's image property if slack_use_webhook_configured_username_and_icon is false - - skipSlack: Don't publish to slack, even when an URL is given - - slackOnlyOnFailure: Only post on Slack if the tests fail - - slackDefaultPayloads: Specifies default payloads to include in Slack messages. For more info visit https://docs.fastlane.tools/actions/slack - - destination: Use only if you're a pro, use the other options instead - - runRosettaSimulator: Adds arch=x86_64 to the xcodebuild 'destination' argument to run simulator in a Rosetta mode - - catalystPlatform: Platform to build when using a Catalyst enabled app. Valid values are: ios, macos - - customReportFileName: **DEPRECATED!** Use `--output_files` instead - Sets custom full report file name when generating a single report - - xcodebuildCommand: Allows for override of the default `xcodebuild` command - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - useSystemScm: Lets xcodebuild use system's scm configuration - - numberOfRetries: The number of times a test can fail - - failBuild: Should this step stop the build if the tests fail? Set this to false if you're using trainer - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - - returns: Outputs hash of results with the following keys: :number_of_tests, :number_of_failures, :number_of_retries, :number_of_tests_excluding_retries, :number_of_failures_excluding_retries - - More information: https://docs.fastlane.tools/actions/scan/ - */ -@discardableResult public func runTests(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - packagePath: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - device: OptionalConfigValue = .fastlaneDefault(nil), - devices: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - skipDetectDevices: OptionalConfigValue = .fastlaneDefault(false), - ensureDevicesFound: OptionalConfigValue = .fastlaneDefault(false), - forceQuitSimulator: OptionalConfigValue = .fastlaneDefault(false), - resetSimulator: OptionalConfigValue = .fastlaneDefault(false), - disableSlideToType: OptionalConfigValue = .fastlaneDefault(true), - prelaunchSimulator: OptionalConfigValue = .fastlaneDefault(nil), - reinstallApp: OptionalConfigValue = .fastlaneDefault(false), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - onlyTesting: Any? = nil, - skipTesting: Any? = nil, - testplan: OptionalConfigValue = .fastlaneDefault(nil), - onlyTestConfigurations: Any? = nil, - skipTestConfigurations: Any? = nil, - xctestrun: OptionalConfigValue = .fastlaneDefault(nil), - toolchain: Any? = nil, - clean: OptionalConfigValue = .fastlaneDefault(false), - codeCoverage: OptionalConfigValue = .fastlaneDefault(nil), - addressSanitizer: OptionalConfigValue = .fastlaneDefault(nil), - threadSanitizer: OptionalConfigValue = .fastlaneDefault(nil), - openReport: OptionalConfigValue = .fastlaneDefault(false), - outputDirectory: String = "./test_output", - outputStyle: OptionalConfigValue = .fastlaneDefault(nil), - outputTypes: String = "html,junit", - outputFiles: OptionalConfigValue = .fastlaneDefault(nil), - buildlogPath: String = "~/Library/Logs/scan", - includeSimulatorLogs: OptionalConfigValue = .fastlaneDefault(false), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(nil), - xcodebuildFormatter: String = "xcbeautify", - outputRemoveRetryAttempts: OptionalConfigValue = .fastlaneDefault(false), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(nil), - formatter: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyFormatter: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyArgs: OptionalConfigValue = .fastlaneDefault(nil), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(nil), - shouldZipBuildProducts: OptionalConfigValue = .fastlaneDefault(false), - outputXctestrun: OptionalConfigValue = .fastlaneDefault(false), - resultBundlePath: OptionalConfigValue = .fastlaneDefault(nil), - resultBundle: OptionalConfigValue = .fastlaneDefault(false), - useClangReportName: OptionalConfigValue = .fastlaneDefault(false), - parallelTesting: OptionalConfigValue = .fastlaneDefault(nil), - concurrentWorkers: OptionalConfigValue = .fastlaneDefault(nil), - maxConcurrentSimulators: OptionalConfigValue = .fastlaneDefault(nil), - disableConcurrentTesting: OptionalConfigValue = .fastlaneDefault(false), - skipBuild: OptionalConfigValue = .fastlaneDefault(false), - testWithoutBuilding: OptionalConfigValue = .fastlaneDefault(nil), - buildForTesting: OptionalConfigValue = .fastlaneDefault(nil), - sdk: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - xcargs: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - appName: OptionalConfigValue = .fastlaneDefault(nil), - deploymentTargetVersion: OptionalConfigValue = .fastlaneDefault(nil), - slackUrl: OptionalConfigValue = .fastlaneDefault(nil), - slackChannel: OptionalConfigValue = .fastlaneDefault(nil), - slackMessage: OptionalConfigValue = .fastlaneDefault(nil), - slackUseWebhookConfiguredUsernameAndIcon: OptionalConfigValue = .fastlaneDefault(false), - slackUsername: String = "fastlane", - slackIconUrl: String = "https://fastlane.tools/assets/img/fastlane_icon.png", - skipSlack: OptionalConfigValue = .fastlaneDefault(false), - slackOnlyOnFailure: OptionalConfigValue = .fastlaneDefault(false), - slackDefaultPayloads: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - destination: Any? = nil, - runRosettaSimulator: OptionalConfigValue = .fastlaneDefault(false), - catalystPlatform: OptionalConfigValue = .fastlaneDefault(nil), - customReportFileName: OptionalConfigValue = .fastlaneDefault(nil), - xcodebuildCommand: String = "env NSUnbufferedIO=YES xcodebuild", - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), - packageCachePath: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(false), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false), - numberOfRetries: Int = 0, - failBuild: OptionalConfigValue = .fastlaneDefault(true), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil)) -> [String: Any] -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let packagePathArg = packagePath.asRubyArgument(name: "package_path", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let deviceArg = device.asRubyArgument(name: "device", type: nil) - let devicesArg = devices.asRubyArgument(name: "devices", type: nil) - let skipDetectDevicesArg = skipDetectDevices.asRubyArgument(name: "skip_detect_devices", type: nil) - let ensureDevicesFoundArg = ensureDevicesFound.asRubyArgument(name: "ensure_devices_found", type: nil) - let forceQuitSimulatorArg = forceQuitSimulator.asRubyArgument(name: "force_quit_simulator", type: nil) - let resetSimulatorArg = resetSimulator.asRubyArgument(name: "reset_simulator", type: nil) - let disableSlideToTypeArg = disableSlideToType.asRubyArgument(name: "disable_slide_to_type", type: nil) - let prelaunchSimulatorArg = prelaunchSimulator.asRubyArgument(name: "prelaunch_simulator", type: nil) - let reinstallAppArg = reinstallApp.asRubyArgument(name: "reinstall_app", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) - let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) - let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) - let onlyTestConfigurationsArg = RubyCommand.Argument(name: "only_test_configurations", value: onlyTestConfigurations, type: nil) - let skipTestConfigurationsArg = RubyCommand.Argument(name: "skip_test_configurations", value: skipTestConfigurations, type: nil) - let xctestrunArg = xctestrun.asRubyArgument(name: "xctestrun", type: nil) - let toolchainArg = RubyCommand.Argument(name: "toolchain", value: toolchain, type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let codeCoverageArg = codeCoverage.asRubyArgument(name: "code_coverage", type: nil) - let addressSanitizerArg = addressSanitizer.asRubyArgument(name: "address_sanitizer", type: nil) - let threadSanitizerArg = threadSanitizer.asRubyArgument(name: "thread_sanitizer", type: nil) - let openReportArg = openReport.asRubyArgument(name: "open_report", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputStyleArg = outputStyle.asRubyArgument(name: "output_style", type: nil) - let outputTypesArg = RubyCommand.Argument(name: "output_types", value: outputTypes, type: nil) - let outputFilesArg = outputFiles.asRubyArgument(name: "output_files", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let includeSimulatorLogsArg = includeSimulatorLogs.asRubyArgument(name: "include_simulator_logs", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let outputRemoveRetryAttemptsArg = outputRemoveRetryAttempts.asRubyArgument(name: "output_remove_retry_attempts", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let formatterArg = formatter.asRubyArgument(name: "formatter", type: nil) - let xcprettyFormatterArg = xcprettyFormatter.asRubyArgument(name: "xcpretty_formatter", type: nil) - let xcprettyArgsArg = xcprettyArgs.asRubyArgument(name: "xcpretty_args", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let shouldZipBuildProductsArg = shouldZipBuildProducts.asRubyArgument(name: "should_zip_build_products", type: nil) - let outputXctestrunArg = outputXctestrun.asRubyArgument(name: "output_xctestrun", type: nil) - let resultBundlePathArg = resultBundlePath.asRubyArgument(name: "result_bundle_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let useClangReportNameArg = useClangReportName.asRubyArgument(name: "use_clang_report_name", type: nil) - let parallelTestingArg = parallelTesting.asRubyArgument(name: "parallel_testing", type: nil) - let concurrentWorkersArg = concurrentWorkers.asRubyArgument(name: "concurrent_workers", type: nil) - let maxConcurrentSimulatorsArg = maxConcurrentSimulators.asRubyArgument(name: "max_concurrent_simulators", type: nil) - let disableConcurrentTestingArg = disableConcurrentTesting.asRubyArgument(name: "disable_concurrent_testing", type: nil) - let skipBuildArg = skipBuild.asRubyArgument(name: "skip_build", type: nil) - let testWithoutBuildingArg = testWithoutBuilding.asRubyArgument(name: "test_without_building", type: nil) - let buildForTestingArg = buildForTesting.asRubyArgument(name: "build_for_testing", type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let deploymentTargetVersionArg = deploymentTargetVersion.asRubyArgument(name: "deployment_target_version", type: nil) - let slackUrlArg = slackUrl.asRubyArgument(name: "slack_url", type: nil) - let slackChannelArg = slackChannel.asRubyArgument(name: "slack_channel", type: nil) - let slackMessageArg = slackMessage.asRubyArgument(name: "slack_message", type: nil) - let slackUseWebhookConfiguredUsernameAndIconArg = slackUseWebhookConfiguredUsernameAndIcon.asRubyArgument(name: "slack_use_webhook_configured_username_and_icon", type: nil) - let slackUsernameArg = RubyCommand.Argument(name: "slack_username", value: slackUsername, type: nil) - let slackIconUrlArg = RubyCommand.Argument(name: "slack_icon_url", value: slackIconUrl, type: nil) - let skipSlackArg = skipSlack.asRubyArgument(name: "skip_slack", type: nil) - let slackOnlyOnFailureArg = slackOnlyOnFailure.asRubyArgument(name: "slack_only_on_failure", type: nil) - let slackDefaultPayloadsArg = slackDefaultPayloads.asRubyArgument(name: "slack_default_payloads", type: nil) - let destinationArg = RubyCommand.Argument(name: "destination", value: destination, type: nil) - let runRosettaSimulatorArg = runRosettaSimulator.asRubyArgument(name: "run_rosetta_simulator", type: nil) - let catalystPlatformArg = catalystPlatform.asRubyArgument(name: "catalyst_platform", type: nil) - let customReportFileNameArg = customReportFileName.asRubyArgument(name: "custom_report_file_name", type: nil) - let xcodebuildCommandArg = RubyCommand.Argument(name: "xcodebuild_command", value: xcodebuildCommand, type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let numberOfRetriesArg = RubyCommand.Argument(name: "number_of_retries", value: numberOfRetries, type: nil) - let failBuildArg = failBuild.asRubyArgument(name: "fail_build", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - packagePathArg, - schemeArg, - deviceArg, - devicesArg, - skipDetectDevicesArg, - ensureDevicesFoundArg, - forceQuitSimulatorArg, - resetSimulatorArg, - disableSlideToTypeArg, - prelaunchSimulatorArg, - reinstallAppArg, - appIdentifierArg, - onlyTestingArg, - skipTestingArg, - testplanArg, - onlyTestConfigurationsArg, - skipTestConfigurationsArg, - xctestrunArg, - toolchainArg, - cleanArg, - codeCoverageArg, - addressSanitizerArg, - threadSanitizerArg, - openReportArg, - outputDirectoryArg, - outputStyleArg, - outputTypesArg, - outputFilesArg, - buildlogPathArg, - includeSimulatorLogsArg, - suppressXcodeOutputArg, - xcodebuildFormatterArg, - outputRemoveRetryAttemptsArg, - disableXcprettyArg, - formatterArg, - xcprettyFormatterArg, - xcprettyArgsArg, - derivedDataPathArg, - shouldZipBuildProductsArg, - outputXctestrunArg, - resultBundlePathArg, - resultBundleArg, - useClangReportNameArg, - parallelTestingArg, - concurrentWorkersArg, - maxConcurrentSimulatorsArg, - disableConcurrentTestingArg, - skipBuildArg, - testWithoutBuildingArg, - buildForTestingArg, - sdkArg, - configurationArg, - xcargsArg, - xcconfigArg, - appNameArg, - deploymentTargetVersionArg, - slackUrlArg, - slackChannelArg, - slackMessageArg, - slackUseWebhookConfiguredUsernameAndIconArg, - slackUsernameArg, - slackIconUrlArg, - skipSlackArg, - slackOnlyOnFailureArg, - slackDefaultPayloadsArg, - destinationArg, - runRosettaSimulatorArg, - catalystPlatformArg, - customReportFileNameArg, - xcodebuildCommandArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - useSystemScmArg, - numberOfRetriesArg, - failBuildArg, - packageAuthorizationProviderArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "run_tests", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Generates a plist file and uploads all to AWS S3 - - - parameters: - - ipa: .ipa file for the build - - dsym: zipped .dsym package for the build - - uploadMetadata: Upload relevant metadata for this build - - plistTemplatePath: plist template path - - plistFileName: uploaded plist filename - - htmlTemplatePath: html erb template path - - htmlFileName: uploaded html filename - - versionTemplatePath: version erb template path - - versionFileName: uploaded version filename - - accessKey: AWS Access Key ID - - secretAccessKey: AWS Secret Access Key - - bucket: AWS bucket name - - region: AWS region (for bucket creation) - - path: S3 'path'. Values from Info.plist will be substituted for keys wrapped in {} - - source: Optional source directory e.g. ./build - - acl: Uploaded object permissions e.g public_read (default), private, public_read_write, authenticated_read - - Upload a new build to Amazon S3 to distribute the build to beta testers. - Works for both Ad Hoc and Enterprise signed applications. This step will generate the necessary HTML, plist, and version files for you. - It is recommended to **not** store the AWS access keys in the `Fastfile`. The uploaded `version.json` file provides an easy way for apps to poll if a new update is available. - */ -public func s3(ipa: OptionalConfigValue = .fastlaneDefault(nil), - dsym: OptionalConfigValue = .fastlaneDefault(nil), - uploadMetadata: OptionalConfigValue = .fastlaneDefault(true), - plistTemplatePath: OptionalConfigValue = .fastlaneDefault(nil), - plistFileName: OptionalConfigValue = .fastlaneDefault(nil), - htmlTemplatePath: OptionalConfigValue = .fastlaneDefault(nil), - htmlFileName: OptionalConfigValue = .fastlaneDefault(nil), - versionTemplatePath: OptionalConfigValue = .fastlaneDefault(nil), - versionFileName: OptionalConfigValue = .fastlaneDefault(nil), - accessKey: OptionalConfigValue = .fastlaneDefault(nil), - secretAccessKey: OptionalConfigValue = .fastlaneDefault(nil), - bucket: OptionalConfigValue = .fastlaneDefault(nil), - region: OptionalConfigValue = .fastlaneDefault(nil), - path: String = "v{CFBundleShortVersionString}_b{CFBundleVersion}/", - source: OptionalConfigValue = .fastlaneDefault(nil), - acl: String = "public_read") -{ - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let dsymArg = dsym.asRubyArgument(name: "dsym", type: nil) - let uploadMetadataArg = uploadMetadata.asRubyArgument(name: "upload_metadata", type: nil) - let plistTemplatePathArg = plistTemplatePath.asRubyArgument(name: "plist_template_path", type: nil) - let plistFileNameArg = plistFileName.asRubyArgument(name: "plist_file_name", type: nil) - let htmlTemplatePathArg = htmlTemplatePath.asRubyArgument(name: "html_template_path", type: nil) - let htmlFileNameArg = htmlFileName.asRubyArgument(name: "html_file_name", type: nil) - let versionTemplatePathArg = versionTemplatePath.asRubyArgument(name: "version_template_path", type: nil) - let versionFileNameArg = versionFileName.asRubyArgument(name: "version_file_name", type: nil) - let accessKeyArg = accessKey.asRubyArgument(name: "access_key", type: nil) - let secretAccessKeyArg = secretAccessKey.asRubyArgument(name: "secret_access_key", type: nil) - let bucketArg = bucket.asRubyArgument(name: "bucket", type: nil) - let regionArg = region.asRubyArgument(name: "region", type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let sourceArg = source.asRubyArgument(name: "source", type: nil) - let aclArg = RubyCommand.Argument(name: "acl", value: acl, type: nil) - let array: [RubyCommand.Argument?] = [ipaArg, - dsymArg, - uploadMetadataArg, - plistTemplatePathArg, - plistFileNameArg, - htmlTemplatePathArg, - htmlFileNameArg, - versionTemplatePathArg, - versionFileNameArg, - accessKeyArg, - secretAccessKeyArg, - bucketArg, - regionArg, - pathArg, - sourceArg, - aclArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "s3", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This action speaks the given text out loud - - - parameters: - - text: Text to be spoken out loud (as string or array of strings) - - mute: If say should be muted with text printed out - */ -public func say(text: [String], - mute: OptionalConfigValue = .fastlaneDefault(false)) -{ - let textArg = RubyCommand.Argument(name: "text", value: text, type: nil) - let muteArg = mute.asRubyArgument(name: "mute", type: nil) - let array: [RubyCommand.Argument?] = [textArg, - muteArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "say", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `run_tests` action - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - packagePath: Path to the Swift Package - - scheme: The project's scheme. Make sure it's marked as `Shared` - - device: The name of the simulator type you want to run tests on (e.g. 'iPhone 6' or 'iPhone SE (2nd generation) (14.5)') - - devices: Array of devices to run the tests on (e.g. ['iPhone 6', 'iPad Air', 'iPhone SE (2nd generation) (14.5)']) - - skipDetectDevices: Should skip auto detecting of devices if none were specified - - ensureDevicesFound: Should fail if devices not found - - forceQuitSimulator: Enabling this option will automatically killall Simulator processes before the run - - resetSimulator: Enabling this option will automatically erase the simulator before running the application - - disableSlideToType: Enabling this option will disable the simulator from showing the 'Slide to type' prompt - - prelaunchSimulator: Enabling this option will launch the first simulator prior to calling any xcodebuild command - - reinstallApp: Enabling this option will automatically uninstall the application before running it - - appIdentifier: The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) - - onlyTesting: Array of test identifiers to run. Expected format: TestTarget[/TestSuite[/TestCase]] - - skipTesting: Array of test identifiers to skip. Expected format: TestTarget[/TestSuite[/TestCase]] - - testplan: The testplan associated with the scheme that should be used for testing - - onlyTestConfigurations: Array of strings matching test plan configurations to run - - skipTestConfigurations: Array of strings matching test plan configurations to skip - - xctestrun: Run tests using the provided `.xctestrun` file - - toolchain: The toolchain that should be used for building the application (e.g. `com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a`) - - clean: Should the project be cleaned before building it? - - codeCoverage: Should code coverage be generated? (Xcode 7 and up) - - addressSanitizer: Should the address sanitizer be turned on? - - threadSanitizer: Should the thread sanitizer be turned on? - - openReport: Should the HTML report be opened when tests are completed? - - outputDirectory: The directory in which all reports will be stored - - outputStyle: Define how the output should look like. Valid values are: standard, basic, rspec, or raw (disables xcpretty during xcodebuild) - - outputTypes: Comma separated list of the output types (e.g. html, junit, json-compilation-database) - - outputFiles: Comma separated list of the output files, corresponding to the types provided by :output_types (order should match). If specifying an output type of json-compilation-database with :use_clang_report_name enabled, that option will take precedence - - buildlogPath: The directory where to store the raw log - - includeSimulatorLogs: If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - outputRemoveRetryAttempts: Remove retry attempts from test results table and the JUnit report (if not using xcpretty) - - disableXcpretty: **DEPRECATED!** Use `output_style: 'raw'` instead - Disable xcpretty formatting of build, similar to `output_style='raw'` but this will also skip the test results table - - formatter: **DEPRECATED!** Use 'xcpretty_formatter' instead - A custom xcpretty formatter to use - - xcprettyFormatter: A custom xcpretty formatter to use - - xcprettyArgs: Pass in xcpretty additional command line arguments (e.g. '--test --no-color' or '--tap --no-utf') - - derivedDataPath: The directory where build products and other derived data will go - - shouldZipBuildProducts: Should zip the derived data build products and place in output path? - - outputXctestrun: Should provide additional copy of .xctestrun file (settings.xctestrun) and place in output path? - - resultBundlePath: Custom path for the result bundle, overrides result_bundle - - resultBundle: Should an Xcode result bundle be generated in the output directory - - useClangReportName: Generate the json compilation database with clang naming convention (compile_commands.json) - - parallelTesting: Optionally override the per-target setting in the scheme for running tests in parallel. Equivalent to -parallel-testing-enabled - - concurrentWorkers: Specify the exact number of test runners that will be spawned during parallel testing. Equivalent to -parallel-testing-worker-count - - maxConcurrentSimulators: Constrain the number of simulator devices on which to test concurrently. Equivalent to -maximum-concurrent-test-simulator-destinations - - disableConcurrentTesting: Do not run test bundles in parallel on the specified destinations. Testing will occur on each destination serially. Equivalent to -disable-concurrent-testing - - skipBuild: Should debug build be skipped before test build? - - testWithoutBuilding: Test without building, requires a derived data path - - buildForTesting: Build for testing only, does not run tests - - sdk: The SDK that should be used for building the application - - configuration: The configuration to use when building the app. Defaults to 'Release' - - xcargs: Pass additional arguments to xcodebuild. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - appName: App name to use in slack message and logfile name - - deploymentTargetVersion: Target version of the app being build or tested. Used to filter out simulator version - - slackUrl: Create an Incoming WebHook for your Slack group to post results there - - slackChannel: #channel or @username - - slackMessage: The message included with each message posted to slack - - slackUseWebhookConfiguredUsernameAndIcon: Use webhook's default username and icon settings? (true/false) - - slackUsername: Overrides the webhook's username property if slack_use_webhook_configured_username_and_icon is false - - slackIconUrl: Overrides the webhook's image property if slack_use_webhook_configured_username_and_icon is false - - skipSlack: Don't publish to slack, even when an URL is given - - slackOnlyOnFailure: Only post on Slack if the tests fail - - slackDefaultPayloads: Specifies default payloads to include in Slack messages. For more info visit https://docs.fastlane.tools/actions/slack - - destination: Use only if you're a pro, use the other options instead - - runRosettaSimulator: Adds arch=x86_64 to the xcodebuild 'destination' argument to run simulator in a Rosetta mode - - catalystPlatform: Platform to build when using a Catalyst enabled app. Valid values are: ios, macos - - customReportFileName: **DEPRECATED!** Use `--output_files` instead - Sets custom full report file name when generating a single report - - xcodebuildCommand: Allows for override of the default `xcodebuild` command - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - useSystemScm: Lets xcodebuild use system's scm configuration - - numberOfRetries: The number of times a test can fail - - failBuild: Should this step stop the build if the tests fail? Set this to false if you're using trainer - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - - returns: Outputs hash of results with the following keys: :number_of_tests, :number_of_failures, :number_of_retries, :number_of_tests_excluding_retries, :number_of_failures_excluding_retries - - More information: https://docs.fastlane.tools/actions/scan/ - */ -@discardableResult public func scan(workspace: OptionalConfigValue = .fastlaneDefault(scanfile.workspace), - project: OptionalConfigValue = .fastlaneDefault(scanfile.project), - packagePath: OptionalConfigValue = .fastlaneDefault(scanfile.packagePath), - scheme: OptionalConfigValue = .fastlaneDefault(scanfile.scheme), - device: OptionalConfigValue = .fastlaneDefault(scanfile.device), - devices: OptionalConfigValue<[String]?> = .fastlaneDefault(scanfile.devices), - skipDetectDevices: OptionalConfigValue = .fastlaneDefault(scanfile.skipDetectDevices), - ensureDevicesFound: OptionalConfigValue = .fastlaneDefault(scanfile.ensureDevicesFound), - forceQuitSimulator: OptionalConfigValue = .fastlaneDefault(scanfile.forceQuitSimulator), - resetSimulator: OptionalConfigValue = .fastlaneDefault(scanfile.resetSimulator), - disableSlideToType: OptionalConfigValue = .fastlaneDefault(scanfile.disableSlideToType), - prelaunchSimulator: OptionalConfigValue = .fastlaneDefault(scanfile.prelaunchSimulator), - reinstallApp: OptionalConfigValue = .fastlaneDefault(scanfile.reinstallApp), - appIdentifier: OptionalConfigValue = .fastlaneDefault(scanfile.appIdentifier), - onlyTesting: Any? = scanfile.onlyTesting, - skipTesting: Any? = scanfile.skipTesting, - testplan: OptionalConfigValue = .fastlaneDefault(scanfile.testplan), - onlyTestConfigurations: Any? = scanfile.onlyTestConfigurations, - skipTestConfigurations: Any? = scanfile.skipTestConfigurations, - xctestrun: OptionalConfigValue = .fastlaneDefault(scanfile.xctestrun), - toolchain: Any? = scanfile.toolchain, - clean: OptionalConfigValue = .fastlaneDefault(scanfile.clean), - codeCoverage: OptionalConfigValue = .fastlaneDefault(scanfile.codeCoverage), - addressSanitizer: OptionalConfigValue = .fastlaneDefault(scanfile.addressSanitizer), - threadSanitizer: OptionalConfigValue = .fastlaneDefault(scanfile.threadSanitizer), - openReport: OptionalConfigValue = .fastlaneDefault(scanfile.openReport), - outputDirectory: String = scanfile.outputDirectory, - outputStyle: OptionalConfigValue = .fastlaneDefault(scanfile.outputStyle), - outputTypes: String = scanfile.outputTypes, - outputFiles: OptionalConfigValue = .fastlaneDefault(scanfile.outputFiles), - buildlogPath: String = scanfile.buildlogPath, - includeSimulatorLogs: OptionalConfigValue = .fastlaneDefault(scanfile.includeSimulatorLogs), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(scanfile.suppressXcodeOutput), - xcodebuildFormatter: String = scanfile.xcodebuildFormatter, - outputRemoveRetryAttempts: OptionalConfigValue = .fastlaneDefault(scanfile.outputRemoveRetryAttempts), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(scanfile.disableXcpretty), - formatter: OptionalConfigValue = .fastlaneDefault(scanfile.formatter), - xcprettyFormatter: OptionalConfigValue = .fastlaneDefault(scanfile.xcprettyFormatter), - xcprettyArgs: OptionalConfigValue = .fastlaneDefault(scanfile.xcprettyArgs), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(scanfile.derivedDataPath), - shouldZipBuildProducts: OptionalConfigValue = .fastlaneDefault(scanfile.shouldZipBuildProducts), - outputXctestrun: OptionalConfigValue = .fastlaneDefault(scanfile.outputXctestrun), - resultBundlePath: OptionalConfigValue = .fastlaneDefault(scanfile.resultBundlePath), - resultBundle: OptionalConfigValue = .fastlaneDefault(scanfile.resultBundle), - useClangReportName: OptionalConfigValue = .fastlaneDefault(scanfile.useClangReportName), - parallelTesting: OptionalConfigValue = .fastlaneDefault(scanfile.parallelTesting), - concurrentWorkers: OptionalConfigValue = .fastlaneDefault(scanfile.concurrentWorkers), - maxConcurrentSimulators: OptionalConfigValue = .fastlaneDefault(scanfile.maxConcurrentSimulators), - disableConcurrentTesting: OptionalConfigValue = .fastlaneDefault(scanfile.disableConcurrentTesting), - skipBuild: OptionalConfigValue = .fastlaneDefault(scanfile.skipBuild), - testWithoutBuilding: OptionalConfigValue = .fastlaneDefault(scanfile.testWithoutBuilding), - buildForTesting: OptionalConfigValue = .fastlaneDefault(scanfile.buildForTesting), - sdk: OptionalConfigValue = .fastlaneDefault(scanfile.sdk), - configuration: OptionalConfigValue = .fastlaneDefault(scanfile.configuration), - xcargs: OptionalConfigValue = .fastlaneDefault(scanfile.xcargs), - xcconfig: OptionalConfigValue = .fastlaneDefault(scanfile.xcconfig), - appName: OptionalConfigValue = .fastlaneDefault(scanfile.appName), - deploymentTargetVersion: OptionalConfigValue = .fastlaneDefault(scanfile.deploymentTargetVersion), - slackUrl: OptionalConfigValue = .fastlaneDefault(scanfile.slackUrl), - slackChannel: OptionalConfigValue = .fastlaneDefault(scanfile.slackChannel), - slackMessage: OptionalConfigValue = .fastlaneDefault(scanfile.slackMessage), - slackUseWebhookConfiguredUsernameAndIcon: OptionalConfigValue = .fastlaneDefault(scanfile.slackUseWebhookConfiguredUsernameAndIcon), - slackUsername: String = scanfile.slackUsername, - slackIconUrl: String = scanfile.slackIconUrl, - skipSlack: OptionalConfigValue = .fastlaneDefault(scanfile.skipSlack), - slackOnlyOnFailure: OptionalConfigValue = .fastlaneDefault(scanfile.slackOnlyOnFailure), - slackDefaultPayloads: OptionalConfigValue<[String]?> = .fastlaneDefault(scanfile.slackDefaultPayloads), - destination: Any? = scanfile.destination, - runRosettaSimulator: OptionalConfigValue = .fastlaneDefault(scanfile.runRosettaSimulator), - catalystPlatform: OptionalConfigValue = .fastlaneDefault(scanfile.catalystPlatform), - customReportFileName: OptionalConfigValue = .fastlaneDefault(scanfile.customReportFileName), - xcodebuildCommand: String = scanfile.xcodebuildCommand, - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(scanfile.clonedSourcePackagesPath), - packageCachePath: OptionalConfigValue = .fastlaneDefault(scanfile.packageCachePath), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(scanfile.skipPackageDependenciesResolution), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(scanfile.disablePackageAutomaticUpdates), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(scanfile.skipPackageRepositoryFetches), - useSystemScm: OptionalConfigValue = .fastlaneDefault(scanfile.useSystemScm), - numberOfRetries: Int = scanfile.numberOfRetries, - failBuild: OptionalConfigValue = .fastlaneDefault(scanfile.failBuild), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(scanfile.packageAuthorizationProvider)) -> [String: Any] -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let packagePathArg = packagePath.asRubyArgument(name: "package_path", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let deviceArg = device.asRubyArgument(name: "device", type: nil) - let devicesArg = devices.asRubyArgument(name: "devices", type: nil) - let skipDetectDevicesArg = skipDetectDevices.asRubyArgument(name: "skip_detect_devices", type: nil) - let ensureDevicesFoundArg = ensureDevicesFound.asRubyArgument(name: "ensure_devices_found", type: nil) - let forceQuitSimulatorArg = forceQuitSimulator.asRubyArgument(name: "force_quit_simulator", type: nil) - let resetSimulatorArg = resetSimulator.asRubyArgument(name: "reset_simulator", type: nil) - let disableSlideToTypeArg = disableSlideToType.asRubyArgument(name: "disable_slide_to_type", type: nil) - let prelaunchSimulatorArg = prelaunchSimulator.asRubyArgument(name: "prelaunch_simulator", type: nil) - let reinstallAppArg = reinstallApp.asRubyArgument(name: "reinstall_app", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) - let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) - let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) - let onlyTestConfigurationsArg = RubyCommand.Argument(name: "only_test_configurations", value: onlyTestConfigurations, type: nil) - let skipTestConfigurationsArg = RubyCommand.Argument(name: "skip_test_configurations", value: skipTestConfigurations, type: nil) - let xctestrunArg = xctestrun.asRubyArgument(name: "xctestrun", type: nil) - let toolchainArg = RubyCommand.Argument(name: "toolchain", value: toolchain, type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let codeCoverageArg = codeCoverage.asRubyArgument(name: "code_coverage", type: nil) - let addressSanitizerArg = addressSanitizer.asRubyArgument(name: "address_sanitizer", type: nil) - let threadSanitizerArg = threadSanitizer.asRubyArgument(name: "thread_sanitizer", type: nil) - let openReportArg = openReport.asRubyArgument(name: "open_report", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputStyleArg = outputStyle.asRubyArgument(name: "output_style", type: nil) - let outputTypesArg = RubyCommand.Argument(name: "output_types", value: outputTypes, type: nil) - let outputFilesArg = outputFiles.asRubyArgument(name: "output_files", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let includeSimulatorLogsArg = includeSimulatorLogs.asRubyArgument(name: "include_simulator_logs", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let outputRemoveRetryAttemptsArg = outputRemoveRetryAttempts.asRubyArgument(name: "output_remove_retry_attempts", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let formatterArg = formatter.asRubyArgument(name: "formatter", type: nil) - let xcprettyFormatterArg = xcprettyFormatter.asRubyArgument(name: "xcpretty_formatter", type: nil) - let xcprettyArgsArg = xcprettyArgs.asRubyArgument(name: "xcpretty_args", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let shouldZipBuildProductsArg = shouldZipBuildProducts.asRubyArgument(name: "should_zip_build_products", type: nil) - let outputXctestrunArg = outputXctestrun.asRubyArgument(name: "output_xctestrun", type: nil) - let resultBundlePathArg = resultBundlePath.asRubyArgument(name: "result_bundle_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let useClangReportNameArg = useClangReportName.asRubyArgument(name: "use_clang_report_name", type: nil) - let parallelTestingArg = parallelTesting.asRubyArgument(name: "parallel_testing", type: nil) - let concurrentWorkersArg = concurrentWorkers.asRubyArgument(name: "concurrent_workers", type: nil) - let maxConcurrentSimulatorsArg = maxConcurrentSimulators.asRubyArgument(name: "max_concurrent_simulators", type: nil) - let disableConcurrentTestingArg = disableConcurrentTesting.asRubyArgument(name: "disable_concurrent_testing", type: nil) - let skipBuildArg = skipBuild.asRubyArgument(name: "skip_build", type: nil) - let testWithoutBuildingArg = testWithoutBuilding.asRubyArgument(name: "test_without_building", type: nil) - let buildForTestingArg = buildForTesting.asRubyArgument(name: "build_for_testing", type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let deploymentTargetVersionArg = deploymentTargetVersion.asRubyArgument(name: "deployment_target_version", type: nil) - let slackUrlArg = slackUrl.asRubyArgument(name: "slack_url", type: nil) - let slackChannelArg = slackChannel.asRubyArgument(name: "slack_channel", type: nil) - let slackMessageArg = slackMessage.asRubyArgument(name: "slack_message", type: nil) - let slackUseWebhookConfiguredUsernameAndIconArg = slackUseWebhookConfiguredUsernameAndIcon.asRubyArgument(name: "slack_use_webhook_configured_username_and_icon", type: nil) - let slackUsernameArg = RubyCommand.Argument(name: "slack_username", value: slackUsername, type: nil) - let slackIconUrlArg = RubyCommand.Argument(name: "slack_icon_url", value: slackIconUrl, type: nil) - let skipSlackArg = skipSlack.asRubyArgument(name: "skip_slack", type: nil) - let slackOnlyOnFailureArg = slackOnlyOnFailure.asRubyArgument(name: "slack_only_on_failure", type: nil) - let slackDefaultPayloadsArg = slackDefaultPayloads.asRubyArgument(name: "slack_default_payloads", type: nil) - let destinationArg = RubyCommand.Argument(name: "destination", value: destination, type: nil) - let runRosettaSimulatorArg = runRosettaSimulator.asRubyArgument(name: "run_rosetta_simulator", type: nil) - let catalystPlatformArg = catalystPlatform.asRubyArgument(name: "catalyst_platform", type: nil) - let customReportFileNameArg = customReportFileName.asRubyArgument(name: "custom_report_file_name", type: nil) - let xcodebuildCommandArg = RubyCommand.Argument(name: "xcodebuild_command", value: xcodebuildCommand, type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let numberOfRetriesArg = RubyCommand.Argument(name: "number_of_retries", value: numberOfRetries, type: nil) - let failBuildArg = failBuild.asRubyArgument(name: "fail_build", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - packagePathArg, - schemeArg, - deviceArg, - devicesArg, - skipDetectDevicesArg, - ensureDevicesFoundArg, - forceQuitSimulatorArg, - resetSimulatorArg, - disableSlideToTypeArg, - prelaunchSimulatorArg, - reinstallAppArg, - appIdentifierArg, - onlyTestingArg, - skipTestingArg, - testplanArg, - onlyTestConfigurationsArg, - skipTestConfigurationsArg, - xctestrunArg, - toolchainArg, - cleanArg, - codeCoverageArg, - addressSanitizerArg, - threadSanitizerArg, - openReportArg, - outputDirectoryArg, - outputStyleArg, - outputTypesArg, - outputFilesArg, - buildlogPathArg, - includeSimulatorLogsArg, - suppressXcodeOutputArg, - xcodebuildFormatterArg, - outputRemoveRetryAttemptsArg, - disableXcprettyArg, - formatterArg, - xcprettyFormatterArg, - xcprettyArgsArg, - derivedDataPathArg, - shouldZipBuildProductsArg, - outputXctestrunArg, - resultBundlePathArg, - resultBundleArg, - useClangReportNameArg, - parallelTestingArg, - concurrentWorkersArg, - maxConcurrentSimulatorsArg, - disableConcurrentTestingArg, - skipBuildArg, - testWithoutBuildingArg, - buildForTestingArg, - sdkArg, - configurationArg, - xcargsArg, - xcconfigArg, - appNameArg, - deploymentTargetVersionArg, - slackUrlArg, - slackChannelArg, - slackMessageArg, - slackUseWebhookConfiguredUsernameAndIconArg, - slackUsernameArg, - slackIconUrlArg, - skipSlackArg, - slackOnlyOnFailureArg, - slackDefaultPayloadsArg, - destinationArg, - runRosettaSimulatorArg, - catalystPlatformArg, - customReportFileNameArg, - xcodebuildCommandArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - useSystemScmArg, - numberOfRetriesArg, - failBuildArg, - packageAuthorizationProviderArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "scan", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Transfer files via SCP - - - parameters: - - username: Username - - password: Password - - host: Hostname - - port: Port - - upload: Upload - - download: Download - */ -public func scp(username: String, - password: OptionalConfigValue = .fastlaneDefault(nil), - host: String, - port: String = "22", - upload: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - download: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil)) -{ - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let passwordArg = password.asRubyArgument(name: "password", type: nil) - let hostArg = RubyCommand.Argument(name: "host", value: host, type: nil) - let portArg = RubyCommand.Argument(name: "port", value: port, type: nil) - let uploadArg = upload.asRubyArgument(name: "upload", type: nil) - let downloadArg = download.asRubyArgument(name: "download", type: nil) - let array: [RubyCommand.Argument?] = [usernameArg, - passwordArg, - hostArg, - portArg, - uploadArg, - downloadArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "scp", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `capture_android_screenshots` action - - - parameters: - - androidHome: Path to the root of your Android SDK installation, e.g. ~/tools/android-sdk-macosx - - buildToolsVersion: **DEPRECATED!** The Android build tools version to use, e.g. '23.0.2' - - locales: A list of locales which should be used - - clearPreviousScreenshots: Enabling this option will automatically clear previously generated screenshots before running screengrab - - outputDirectory: The directory where to store the screenshots - - skipOpenSummary: Don't open the summary after running _screengrab_ - - appPackageName: The package name of the app under test (e.g. com.yourcompany.yourapp) - - testsPackageName: The package name of the tests bundle (e.g. com.yourcompany.yourapp.test) - - useTestsInPackages: Only run tests in these Java packages - - useTestsInClasses: Only run tests in these Java classes - - launchArguments: Additional launch arguments - - testInstrumentationRunner: The fully qualified class name of your test instrumentation runner - - endingLocale: **DEPRECATED!** Return the device to this locale after running tests - - useAdbRoot: **DEPRECATED!** Restarts the adb daemon using `adb root` to allow access to screenshots directories on device. Use if getting 'Permission denied' errors - - appApkPath: The path to the APK for the app under test - - testsApkPath: The path to the APK for the tests bundle - - specificDevice: Use the device or emulator with the given serial number or qualifier - - deviceType: Type of device used for screenshots. Matches Google Play Types (phone, sevenInch, tenInch, tv, wear) - - exitOnTestFailure: Whether or not to exit Screengrab on test failure. Exiting on failure will not copy screenshots to local machine nor open screenshots summary - - reinstallApp: Enabling this option will automatically uninstall the application before running it - - useTimestampSuffix: Add timestamp suffix to screenshot filename - - adbHost: Configure the host used by adb to connect, allows running on remote devices farm - */ -public func screengrab(androidHome: OptionalConfigValue = .fastlaneDefault(screengrabfile.androidHome), - buildToolsVersion: OptionalConfigValue = .fastlaneDefault(screengrabfile.buildToolsVersion), - locales: [String] = screengrabfile.locales, - clearPreviousScreenshots: OptionalConfigValue = .fastlaneDefault(screengrabfile.clearPreviousScreenshots), - outputDirectory: String = screengrabfile.outputDirectory, - skipOpenSummary: OptionalConfigValue = .fastlaneDefault(screengrabfile.skipOpenSummary), - appPackageName: String = screengrabfile.appPackageName, - testsPackageName: OptionalConfigValue = .fastlaneDefault(screengrabfile.testsPackageName), - useTestsInPackages: OptionalConfigValue<[String]?> = .fastlaneDefault(screengrabfile.useTestsInPackages), - useTestsInClasses: OptionalConfigValue<[String]?> = .fastlaneDefault(screengrabfile.useTestsInClasses), - launchArguments: OptionalConfigValue<[String]?> = .fastlaneDefault(screengrabfile.launchArguments), - testInstrumentationRunner: String = screengrabfile.testInstrumentationRunner, - endingLocale: String = screengrabfile.endingLocale, - useAdbRoot: OptionalConfigValue = .fastlaneDefault(screengrabfile.useAdbRoot), - appApkPath: OptionalConfigValue = .fastlaneDefault(screengrabfile.appApkPath), - testsApkPath: OptionalConfigValue = .fastlaneDefault(screengrabfile.testsApkPath), - specificDevice: OptionalConfigValue = .fastlaneDefault(screengrabfile.specificDevice), - deviceType: String = screengrabfile.deviceType, - exitOnTestFailure: OptionalConfigValue = .fastlaneDefault(screengrabfile.exitOnTestFailure), - reinstallApp: OptionalConfigValue = .fastlaneDefault(screengrabfile.reinstallApp), - useTimestampSuffix: OptionalConfigValue = .fastlaneDefault(screengrabfile.useTimestampSuffix), - adbHost: OptionalConfigValue = .fastlaneDefault(screengrabfile.adbHost)) -{ - let androidHomeArg = androidHome.asRubyArgument(name: "android_home", type: nil) - let buildToolsVersionArg = buildToolsVersion.asRubyArgument(name: "build_tools_version", type: nil) - let localesArg = RubyCommand.Argument(name: "locales", value: locales, type: nil) - let clearPreviousScreenshotsArg = clearPreviousScreenshots.asRubyArgument(name: "clear_previous_screenshots", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let skipOpenSummaryArg = skipOpenSummary.asRubyArgument(name: "skip_open_summary", type: nil) - let appPackageNameArg = RubyCommand.Argument(name: "app_package_name", value: appPackageName, type: nil) - let testsPackageNameArg = testsPackageName.asRubyArgument(name: "tests_package_name", type: nil) - let useTestsInPackagesArg = useTestsInPackages.asRubyArgument(name: "use_tests_in_packages", type: nil) - let useTestsInClassesArg = useTestsInClasses.asRubyArgument(name: "use_tests_in_classes", type: nil) - let launchArgumentsArg = launchArguments.asRubyArgument(name: "launch_arguments", type: nil) - let testInstrumentationRunnerArg = RubyCommand.Argument(name: "test_instrumentation_runner", value: testInstrumentationRunner, type: nil) - let endingLocaleArg = RubyCommand.Argument(name: "ending_locale", value: endingLocale, type: nil) - let useAdbRootArg = useAdbRoot.asRubyArgument(name: "use_adb_root", type: nil) - let appApkPathArg = appApkPath.asRubyArgument(name: "app_apk_path", type: nil) - let testsApkPathArg = testsApkPath.asRubyArgument(name: "tests_apk_path", type: nil) - let specificDeviceArg = specificDevice.asRubyArgument(name: "specific_device", type: nil) - let deviceTypeArg = RubyCommand.Argument(name: "device_type", value: deviceType, type: nil) - let exitOnTestFailureArg = exitOnTestFailure.asRubyArgument(name: "exit_on_test_failure", type: nil) - let reinstallAppArg = reinstallApp.asRubyArgument(name: "reinstall_app", type: nil) - let useTimestampSuffixArg = useTimestampSuffix.asRubyArgument(name: "use_timestamp_suffix", type: nil) - let adbHostArg = adbHost.asRubyArgument(name: "adb_host", type: nil) - let array: [RubyCommand.Argument?] = [androidHomeArg, - buildToolsVersionArg, - localesArg, - clearPreviousScreenshotsArg, - outputDirectoryArg, - skipOpenSummaryArg, - appPackageNameArg, - testsPackageNameArg, - useTestsInPackagesArg, - useTestsInClassesArg, - launchArgumentsArg, - testInstrumentationRunnerArg, - endingLocaleArg, - useAdbRootArg, - appApkPathArg, - testsApkPathArg, - specificDeviceArg, - deviceTypeArg, - exitOnTestFailureArg, - reinstallAppArg, - useTimestampSuffixArg, - adbHostArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "screengrab", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Set the build number from the current repository - - - parameters: - - useHgRevisionNumber: Use hg revision number instead of hash (ignored for non-hg repos) - - xcodeproj: explicitly specify which xcodeproj to use - - This action will set the **build number** according to what the SCM HEAD reports. - Currently supported SCMs are svn (uses root revision), git-svn (uses svn revision) and git (uses short hash) and mercurial (uses short hash or revision number). - There is an option, `:use_hg_revision_number`, which allows to use mercurial revision number instead of hash. - */ -public func setBuildNumberRepository(useHgRevisionNumber: OptionalConfigValue = .fastlaneDefault(false), - xcodeproj: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let useHgRevisionNumberArg = useHgRevisionNumber.asRubyArgument(name: "use_hg_revision_number", type: nil) - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let array: [RubyCommand.Argument?] = [useHgRevisionNumberArg, - xcodeprojArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "set_build_number_repository", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Set the changelog for all languages on App Store Connect - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - appIdentifier: The bundle identifier of your app - - username: Your Apple ID Username - - version: The version number to create/update - - changelog: Changelog text that should be uploaded to App Store Connect - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - platform: The platform of the app (ios, appletvos, xros, mac) - - This is useful if you have only one changelog for all languages. - You can store the changelog in `./changelog.txt` and it will automatically get loaded from there. This integration is useful if you support e.g. 10 languages and want to use the same "What's new"-text for all languages. - Defining the version is optional. _fastlane_ will try to automatically detect it if you don't provide one. - */ -public func setChangelog(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appIdentifier: String, - username: OptionalConfigValue = .fastlaneDefault(nil), - version: OptionalConfigValue = .fastlaneDefault(nil), - changelog: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios") -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let versionArg = version.asRubyArgument(name: "version", type: nil) - let changelogArg = changelog.asRubyArgument(name: "changelog", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - appIdentifierArg, - usernameArg, - versionArg, - changelogArg, - teamIdArg, - teamNameArg, - platformArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "set_changelog", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This will create a new release on GitHub and upload assets for it - - - parameters: - - repositoryName: The path to your repo, e.g. 'fastlane/fastlane' - - serverUrl: The server url. e.g. 'https://your.internal.github.host/api/v3' (Default: 'https://api.github.com') - - apiToken: Personal API Token for GitHub - generate one at https://github.com/settings/tokens - - apiBearer: Use a Bearer authorization token. Usually generated by GitHub Apps, e.g. GitHub Actions GITHUB_TOKEN environment variable - - tagName: Pass in the tag name - - name: Name of this release - - commitish: Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists. Default: the repository's default branch (usually master) - - description: Description of this release - - isDraft: Whether the release should be marked as draft - - isPrerelease: Whether the release should be marked as prerelease - - isGenerateReleaseNotes: Whether the name and body of this release should be generated automatically - - uploadAssets: Path to assets to be uploaded with the release - - - returns: A hash containing all relevant information of this release - Access things like 'html_url', 'tag_name', 'name', 'body' - - Creates a new release on GitHub. You must provide your GitHub Personal token (get one from [https://github.com/settings/tokens/new](https://github.com/settings/tokens/new)), the repository name and tag name. By default, that's `master`. - If the tag doesn't exist, one will be created on the commit or branch passed in as commitish. - Out parameters provide the release's id, which can be used for later editing and the release HTML link to GitHub. You can also specify a list of assets to be uploaded to the release with the `:upload_assets` parameter. - */ -@discardableResult public func setGithubRelease(repositoryName: String, - serverUrl: String = "https://api.github.com", - apiToken: OptionalConfigValue = .fastlaneDefault(nil), - apiBearer: OptionalConfigValue = .fastlaneDefault(nil), - tagName: String, - name: OptionalConfigValue = .fastlaneDefault(nil), - commitish: OptionalConfigValue = .fastlaneDefault(nil), - description: OptionalConfigValue = .fastlaneDefault(nil), - isDraft: OptionalConfigValue = .fastlaneDefault(false), - isPrerelease: OptionalConfigValue = .fastlaneDefault(false), - isGenerateReleaseNotes: OptionalConfigValue = .fastlaneDefault(false), - uploadAssets: OptionalConfigValue<[String]?> = .fastlaneDefault(nil)) -> [String: Any] -{ - let repositoryNameArg = RubyCommand.Argument(name: "repository_name", value: repositoryName, type: nil) - let serverUrlArg = RubyCommand.Argument(name: "server_url", value: serverUrl, type: nil) - let apiTokenArg = apiToken.asRubyArgument(name: "api_token", type: nil) - let apiBearerArg = apiBearer.asRubyArgument(name: "api_bearer", type: nil) - let tagNameArg = RubyCommand.Argument(name: "tag_name", value: tagName, type: nil) - let nameArg = name.asRubyArgument(name: "name", type: nil) - let commitishArg = commitish.asRubyArgument(name: "commitish", type: nil) - let descriptionArg = description.asRubyArgument(name: "description", type: nil) - let isDraftArg = isDraft.asRubyArgument(name: "is_draft", type: nil) - let isPrereleaseArg = isPrerelease.asRubyArgument(name: "is_prerelease", type: nil) - let isGenerateReleaseNotesArg = isGenerateReleaseNotes.asRubyArgument(name: "is_generate_release_notes", type: nil) - let uploadAssetsArg = uploadAssets.asRubyArgument(name: "upload_assets", type: nil) - let array: [RubyCommand.Argument?] = [repositoryNameArg, - serverUrlArg, - apiTokenArg, - apiBearerArg, - tagNameArg, - nameArg, - commitishArg, - descriptionArg, - isDraftArg, - isPrereleaseArg, - isGenerateReleaseNotesArg, - uploadAssetsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "set_github_release", className: nil, args: args) - return parseDictionary(fromString: runner.executeCommand(command)) -} - -/** - Sets value to Info.plist of your project as native Ruby data structures - - - parameters: - - key: Name of key in plist - - subkey: Name of subkey in plist - - value: Value to setup - - path: Path to plist file you want to update - - outputFileName: Path to the output file you want to generate - */ -public func setInfoPlistValue(key: String, - subkey: OptionalConfigValue = .fastlaneDefault(nil), - value: String, - path: String, - outputFileName: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let keyArg = RubyCommand.Argument(name: "key", value: key, type: nil) - let subkeyArg = subkey.asRubyArgument(name: "subkey", type: nil) - let valueArg = RubyCommand.Argument(name: "value", value: value, type: nil) - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let outputFileNameArg = outputFileName.asRubyArgument(name: "output_file_name", type: nil) - let array: [RubyCommand.Argument?] = [keyArg, - subkeyArg, - valueArg, - pathArg, - outputFileNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "set_info_plist_value", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Sets a value for a key with cocoapods-keys - - - parameters: - - useBundleExec: Use bundle exec when there is a Gemfile presented - - key: The key to be saved with cocoapods-keys - - value: The value to be saved with cocoapods-keys - - project: The project name - - Adds a key to [cocoapods-keys](https://github.com/orta/cocoapods-keys) - */ -public func setPodKey(useBundleExec: OptionalConfigValue = .fastlaneDefault(true), - key: String, - value: String, - project: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let useBundleExecArg = useBundleExec.asRubyArgument(name: "use_bundle_exec", type: nil) - let keyArg = RubyCommand.Argument(name: "key", value: key, type: nil) - let valueArg = RubyCommand.Argument(name: "value", value: value, type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let array: [RubyCommand.Argument?] = [useBundleExecArg, - keyArg, - valueArg, - projectArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "set_pod_key", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Setup the keychain and match to work with CI - - - parameters: - - force: Force setup, even if not executed by CI - - provider: CI provider. If none is set, the provider is detected automatically - - timeout: Set a custom timeout in seconds for keychain. Set `0` if you want to specify 'no time-out' - - keychainName: Set a custom keychain name - - - Creates a new temporary keychain for use with match| - - Switches match to `readonly` mode to not create new profiles/cert on CI| - - Sets up log and test result paths to be easily collectible| - >| - This action helps with CI integration. Add this to the top of your Fastfile if you use CI. - */ -public func setupCi(force: OptionalConfigValue = .fastlaneDefault(false), - provider: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 3600, - keychainName: String = "fastlane_tmp_keychain") -{ - let forceArg = force.asRubyArgument(name: "force", type: nil) - let providerArg = provider.asRubyArgument(name: "provider", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let keychainNameArg = RubyCommand.Argument(name: "keychain_name", value: keychainName, type: nil) - let array: [RubyCommand.Argument?] = [forceArg, - providerArg, - timeoutArg, - keychainNameArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "setup_ci", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Setup the keychain and match to work with CircleCI - - - parameter force: Force setup, even if not executed by CircleCI - - - Creates a new temporary keychain for use with match| - - Switches match to `readonly` mode to not create new profiles/cert on CI| - - Sets up log and test result paths to be easily collectible| - >| - This action helps with CircleCI integration. Add this to the top of your Fastfile if you use CircleCI. - */ -public func setupCircleCi(force: OptionalConfigValue = .fastlaneDefault(false)) { - let forceArg = force.asRubyArgument(name: "force", type: nil) - let array: [RubyCommand.Argument?] = [forceArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "setup_circle_ci", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Setup xcodebuild, gym and scan for easier Jenkins integration - - - parameters: - - force: Force setup, even if not executed by Jenkins - - unlockKeychain: Unlocks keychain - - addKeychainToSearchList: Add to keychain search list, valid values are true, false, :add, and :replace - - setDefaultKeychain: Set keychain as default - - keychainPath: Path to keychain - - keychainPassword: Keychain password - - setCodeSigningIdentity: Set code signing identity from CODE_SIGNING_IDENTITY environment - - codeSigningIdentity: Code signing identity - - outputDirectory: The directory in which the ipa file should be stored in - - derivedDataPath: The directory where built products and other derived data will go - - resultBundle: Produce the result bundle describing what occurred will be placed - - - Adds and unlocks keychains from Jenkins 'Keychains and Provisioning Profiles Plugin'| - - Sets unlocked keychain to be used by Match| - - Sets code signing identity from Jenkins 'Keychains and Provisioning Profiles Plugin'| - - Sets output directory to './output' (gym, scan and backup_xcarchive)| - - Sets derived data path to './derivedData' (xcodebuild, gym, scan and clear_derived_data, carthage)| - - Produce result bundle (gym and scan)| - >| - This action helps with Jenkins integration. Creates own derived data for each job. All build results like IPA files and archives will be stored in the `./output` directory. - The action also works with [Keychains and Provisioning Profiles Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Keychains+and+Provisioning+Profiles+Plugin), the selected keychain will be automatically unlocked and the selected code signing identity will be used. - [Match](https://docs.fastlane.tools/actions/match/) will be also set up to use the unlocked keychain and set in read-only mode, if its environment variables were not yet defined. - By default this action will only work when _fastlane_ is executed on a CI system. - */ -public func setupJenkins(force: OptionalConfigValue = .fastlaneDefault(false), - unlockKeychain: OptionalConfigValue = .fastlaneDefault(true), - addKeychainToSearchList: String = "replace", - setDefaultKeychain: OptionalConfigValue = .fastlaneDefault(true), - keychainPath: OptionalConfigValue = .fastlaneDefault(nil), - keychainPassword: String, - setCodeSigningIdentity: OptionalConfigValue = .fastlaneDefault(true), - codeSigningIdentity: OptionalConfigValue = .fastlaneDefault(nil), - outputDirectory: String = "./output", - derivedDataPath: String = "./derivedData", - resultBundle: OptionalConfigValue = .fastlaneDefault(true)) -{ - let forceArg = force.asRubyArgument(name: "force", type: nil) - let unlockKeychainArg = unlockKeychain.asRubyArgument(name: "unlock_keychain", type: nil) - let addKeychainToSearchListArg = RubyCommand.Argument(name: "add_keychain_to_search_list", value: addKeychainToSearchList, type: nil) - let setDefaultKeychainArg = setDefaultKeychain.asRubyArgument(name: "set_default_keychain", type: nil) - let keychainPathArg = keychainPath.asRubyArgument(name: "keychain_path", type: nil) - let keychainPasswordArg = RubyCommand.Argument(name: "keychain_password", value: keychainPassword, type: nil) - let setCodeSigningIdentityArg = setCodeSigningIdentity.asRubyArgument(name: "set_code_signing_identity", type: nil) - let codeSigningIdentityArg = codeSigningIdentity.asRubyArgument(name: "code_signing_identity", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let derivedDataPathArg = RubyCommand.Argument(name: "derived_data_path", value: derivedDataPath, type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let array: [RubyCommand.Argument?] = [forceArg, - unlockKeychainArg, - addKeychainToSearchListArg, - setDefaultKeychainArg, - keychainPathArg, - keychainPasswordArg, - setCodeSigningIdentityArg, - codeSigningIdentityArg, - outputDirectoryArg, - derivedDataPathArg, - resultBundleArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "setup_jenkins", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Setup the keychain and match to work with Travis CI - - - parameter force: Force setup, even if not executed by travis - - - Creates a new temporary keychain for use with match| - - Switches match to `readonly` mode to not create new profiles/cert on CI| - >| - This action helps with Travis integration. Add this to the top of your Fastfile if you use Travis. - */ -public func setupTravis(force: OptionalConfigValue = .fastlaneDefault(false)) { - let forceArg = force.asRubyArgument(name: "force", type: nil) - let array: [RubyCommand.Argument?] = [forceArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "setup_travis", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs a shell command - - - parameters: - - command: Shell command to be executed - - log: Determines whether fastlane should print out the executed command itself and output of the executed command. If command line option --troubleshoot is used, then it overrides this option to true - - errorCallback: A callback invoked with the command output if there is a non-zero exit status - - - returns: Outputs the string and executes it. When running in tests, it returns the actual command instead of executing it - - Allows running an arbitrary shell command. - Be aware of a specific behavior of `sh` action with regard to the working directory. For details, refer to [Advanced](https://docs.fastlane.tools/advanced/#directory-behavior). - */ -@discardableResult public func sh(command: String, - log: OptionalConfigValue = .fastlaneDefault(true), - errorCallback: ((String) -> Void)? = nil) -> String -{ - let commandArg = RubyCommand.Argument(name: "command", value: command, type: nil) - let logArg = log.asRubyArgument(name: "log", type: nil) - let errorCallbackArg = RubyCommand.Argument(name: "error_callback", value: errorCallback, type: .stringClosure) - let array: [RubyCommand.Argument?] = [commandArg, - logArg, - errorCallbackArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "sh", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Alias for the `get_provisioning_profile` action - - - parameters: - - adhoc: Setting this flag will generate AdHoc profiles instead of App Store Profiles - - developerId: Setting this flag will generate Developer ID profiles instead of App Store Profiles - - development: Renew the development certificate instead of the production one - - skipInstall: By default, the certificate will be added to your local machine. Setting this flag will skip this action - - force: Renew provisioning profiles regardless of its state - to automatically add all devices for ad hoc profiles - - includeMacInProfiles: Include Apple Silicon Mac devices in provisioning profiles for iOS/iPadOS apps - - appIdentifier: The bundle identifier of your app - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - provisioningName: The name of the profile that is used on the Apple Developer Portal - - ignoreProfilesWithDifferentName: Use in combination with :provisioning_name - when true only profiles matching this exact name will be downloaded - - outputPath: Directory in which the profile should be stored - - certId: The ID of the code signing certificate to use (e.g. 78ADL6LVAA) - - certOwnerName: The certificate name to use for new profiles, or to renew with. (e.g. "Felix Krause") - - filename: Filename to use for the generated provisioning profile (must include .mobileprovision) - - skipFetchProfiles: Skips the verification of existing profiles which is useful if you have thousands of profiles - - includeAllCertificates: Include all matching certificates in the provisioning profile. Works only for the 'development' provisioning profile type - - skipCertificateVerification: Skips the verification of the certificates for every existing profiles. This will make sure the provisioning profile can be used on the local machine - - platform: Set the provisioning profile's platform (i.e. ios, tvos, macos, catalyst) - - readonly: Only fetch existing profile, don't generate new ones - - templateName: **DEPRECATED!** Removed since May 2025 on App Store Connect API OpenAPI v3.8.0 - Learn more: https://docs.fastlane.tools/actions/match/#managed-capabilities - The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. "Apple Pay Pass Suppression Development") - - failOnNameTaken: Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first - - cachedCertificates: A list of cached certificates - - cachedDevices: A list of cached devices - - cachedBundleIds: A list of cached bundle ids - - cachedProfiles: A list of cached bundle ids - - - returns: The UUID of the profile sigh just fetched/generated - - **Note**: It is recommended to use [match](https://docs.fastlane.tools/actions/match/) according to the [codesigning.guide](https://codesigning.guide) for generating and maintaining your provisioning profiles. Use _sigh_ directly only if you want full control over what's going on and know more about codesigning. - */ -@discardableResult public func sigh(adhoc: OptionalConfigValue = .fastlaneDefault(false), - developerId: OptionalConfigValue = .fastlaneDefault(false), - development: OptionalConfigValue = .fastlaneDefault(false), - skipInstall: OptionalConfigValue = .fastlaneDefault(false), - force: OptionalConfigValue = .fastlaneDefault(false), - includeMacInProfiles: OptionalConfigValue = .fastlaneDefault(false), - appIdentifier: String, - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - provisioningName: OptionalConfigValue = .fastlaneDefault(nil), - ignoreProfilesWithDifferentName: OptionalConfigValue = .fastlaneDefault(false), - outputPath: String = ".", - certId: OptionalConfigValue = .fastlaneDefault(nil), - certOwnerName: OptionalConfigValue = .fastlaneDefault(nil), - filename: OptionalConfigValue = .fastlaneDefault(nil), - skipFetchProfiles: OptionalConfigValue = .fastlaneDefault(false), - includeAllCertificates: OptionalConfigValue = .fastlaneDefault(false), - skipCertificateVerification: OptionalConfigValue = .fastlaneDefault(false), - platform: Any = "ios", - readonly: OptionalConfigValue = .fastlaneDefault(false), - templateName: OptionalConfigValue = .fastlaneDefault(nil), - failOnNameTaken: OptionalConfigValue = .fastlaneDefault(false), - cachedCertificates: Any? = nil, - cachedDevices: Any? = nil, - cachedBundleIds: Any? = nil, - cachedProfiles: Any? = nil) -> String -{ - let adhocArg = adhoc.asRubyArgument(name: "adhoc", type: nil) - let developerIdArg = developerId.asRubyArgument(name: "developer_id", type: nil) - let developmentArg = development.asRubyArgument(name: "development", type: nil) - let skipInstallArg = skipInstall.asRubyArgument(name: "skip_install", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let includeMacInProfilesArg = includeMacInProfiles.asRubyArgument(name: "include_mac_in_profiles", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let provisioningNameArg = provisioningName.asRubyArgument(name: "provisioning_name", type: nil) - let ignoreProfilesWithDifferentNameArg = ignoreProfilesWithDifferentName.asRubyArgument(name: "ignore_profiles_with_different_name", type: nil) - let outputPathArg = RubyCommand.Argument(name: "output_path", value: outputPath, type: nil) - let certIdArg = certId.asRubyArgument(name: "cert_id", type: nil) - let certOwnerNameArg = certOwnerName.asRubyArgument(name: "cert_owner_name", type: nil) - let filenameArg = filename.asRubyArgument(name: "filename", type: nil) - let skipFetchProfilesArg = skipFetchProfiles.asRubyArgument(name: "skip_fetch_profiles", type: nil) - let includeAllCertificatesArg = includeAllCertificates.asRubyArgument(name: "include_all_certificates", type: nil) - let skipCertificateVerificationArg = skipCertificateVerification.asRubyArgument(name: "skip_certificate_verification", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let readonlyArg = readonly.asRubyArgument(name: "readonly", type: nil) - let templateNameArg = templateName.asRubyArgument(name: "template_name", type: nil) - let failOnNameTakenArg = failOnNameTaken.asRubyArgument(name: "fail_on_name_taken", type: nil) - let cachedCertificatesArg = RubyCommand.Argument(name: "cached_certificates", value: cachedCertificates, type: nil) - let cachedDevicesArg = RubyCommand.Argument(name: "cached_devices", value: cachedDevices, type: nil) - let cachedBundleIdsArg = RubyCommand.Argument(name: "cached_bundle_ids", value: cachedBundleIds, type: nil) - let cachedProfilesArg = RubyCommand.Argument(name: "cached_profiles", value: cachedProfiles, type: nil) - let array: [RubyCommand.Argument?] = [adhocArg, - developerIdArg, - developmentArg, - skipInstallArg, - forceArg, - includeMacInProfilesArg, - appIdentifierArg, - apiKeyPathArg, - apiKeyArg, - usernameArg, - teamIdArg, - teamNameArg, - provisioningNameArg, - ignoreProfilesWithDifferentNameArg, - outputPathArg, - certIdArg, - certOwnerNameArg, - filenameArg, - skipFetchProfilesArg, - includeAllCertificatesArg, - skipCertificateVerificationArg, - platformArg, - readonlyArg, - templateNameArg, - failOnNameTakenArg, - cachedCertificatesArg, - cachedDevicesArg, - cachedBundleIdsArg, - cachedProfilesArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "sigh", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Skip the creation of the fastlane/README.md file when running fastlane - - Tell _fastlane_ to not automatically create a `fastlane/README.md` when running _fastlane_. You can always trigger the creation of this file manually by running `fastlane docs`. - */ -public func skipDocs() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "skip_docs", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Send a success/error message to your [Slack](https://slack.com) group - - - parameters: - - message: The message that should be displayed on Slack. This supports the standard Slack markup language - - pretext: This is optional text that appears above the message attachment block. This supports the standard Slack markup language - - channel: #channel or @username - - useWebhookConfiguredUsernameAndIcon: Use webhook's default username and icon settings? (true/false) - - slackUrl: Create an Incoming WebHook for your Slack group - - username: Overrides the webhook's username property if use_webhook_configured_username_and_icon is false - - iconUrl: Specifies a URL of an image to use as the photo of the message. Overrides the webhook's image property if use_webhook_configured_username_and_icon is false - - iconEmoji: Specifies an emoji (using colon shortcodes, eg. :white_check_mark:) to use as the photo of the message. Overrides the webhook's image property if use_webhook_configured_username_and_icon is false. This parameter takes precedence over icon_url - - payload: Add additional information to this post. payload must be a hash containing any key with any value - - defaultPayloads: Specifies default payloads to include. Pass an empty array to suppress all the default payloads - - attachmentProperties: Merge additional properties in the slack attachment, see https://api.slack.com/docs/attachments - - success: Was this build successful? (true/false) - - failOnError: Should an error sending the slack notification cause a failure? (true/false) - - linkNames: Find and link channel names and usernames (true/false) - - Create an Incoming WebHook and export this as `SLACK_URL`. Can send a message to **#channel** (by default), a direct message to **@username** or a message to a private group **group** with success (green) or failure (red) status. - */ -public func slack(message: OptionalConfigValue = .fastlaneDefault(nil), - pretext: OptionalConfigValue = .fastlaneDefault(nil), - channel: OptionalConfigValue = .fastlaneDefault(nil), - useWebhookConfiguredUsernameAndIcon: OptionalConfigValue = .fastlaneDefault(false), - slackUrl: String, - username: String = "fastlane", - iconUrl: String = "https://fastlane.tools/assets/img/fastlane_icon.png", - iconEmoji: OptionalConfigValue = .fastlaneDefault(nil), - payload: [String: Any] = [:], - defaultPayloads: [String] = ["lane", "test_result", "git_branch", "git_author", "last_git_commit", "last_git_commit_hash"], - attachmentProperties: [String: Any] = [:], - success: OptionalConfigValue = .fastlaneDefault(true), - failOnError: OptionalConfigValue = .fastlaneDefault(true), - linkNames: OptionalConfigValue = .fastlaneDefault(false)) -{ - let messageArg = message.asRubyArgument(name: "message", type: nil) - let pretextArg = pretext.asRubyArgument(name: "pretext", type: nil) - let channelArg = channel.asRubyArgument(name: "channel", type: nil) - let useWebhookConfiguredUsernameAndIconArg = useWebhookConfiguredUsernameAndIcon.asRubyArgument(name: "use_webhook_configured_username_and_icon", type: nil) - let slackUrlArg = RubyCommand.Argument(name: "slack_url", value: slackUrl, type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let iconUrlArg = RubyCommand.Argument(name: "icon_url", value: iconUrl, type: nil) - let iconEmojiArg = iconEmoji.asRubyArgument(name: "icon_emoji", type: nil) - let payloadArg = RubyCommand.Argument(name: "payload", value: payload, type: nil) - let defaultPayloadsArg = RubyCommand.Argument(name: "default_payloads", value: defaultPayloads, type: nil) - let attachmentPropertiesArg = RubyCommand.Argument(name: "attachment_properties", value: attachmentProperties, type: nil) - let successArg = success.asRubyArgument(name: "success", type: nil) - let failOnErrorArg = failOnError.asRubyArgument(name: "fail_on_error", type: nil) - let linkNamesArg = linkNames.asRubyArgument(name: "link_names", type: nil) - let array: [RubyCommand.Argument?] = [messageArg, - pretextArg, - channelArg, - useWebhookConfiguredUsernameAndIconArg, - slackUrlArg, - usernameArg, - iconUrlArg, - iconEmojiArg, - payloadArg, - defaultPayloadsArg, - attachmentPropertiesArg, - successArg, - failOnErrorArg, - linkNamesArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "slack", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Use slather to generate a code coverage report - - - parameters: - - buildDirectory: The location of the build output - - proj: The project file that slather looks at - - workspace: The workspace that slather looks at - - scheme: Scheme to use when calling slather - - configuration: Configuration to use when calling slather (since slather-2.4.1) - - inputFormat: The input format that slather should look for - - github: Tell slather that it is running on GitHub Actions - - buildkite: Tell slather that it is running on Buildkite - - teamcity: Tell slather that it is running on TeamCity - - jenkins: Tell slather that it is running on Jenkins - - travis: Tell slather that it is running on TravisCI - - travisPro: Tell slather that it is running on TravisCI Pro - - circleci: Tell slather that it is running on CircleCI - - coveralls: Tell slather that it should post data to Coveralls - - simpleOutput: Tell slather that it should output results to the terminal - - gutterJson: Tell slather that it should output results as Gutter JSON format - - coberturaXml: Tell slather that it should output results as Cobertura XML format - - sonarqubeXml: Tell slather that it should output results as SonarQube Generic XML format - - llvmCov: Tell slather that it should output results as llvm-cov show format - - json: Tell slather that it should output results as static JSON report - - html: Tell slather that it should output results as static HTML pages - - show: Tell slather that it should open static html pages automatically - - sourceDirectory: Tell slather the location of your source files - - outputDirectory: Tell slather the location of for your output files - - ignore: Tell slather to ignore files matching a path or any path from an array of paths - - verbose: Tell slather to enable verbose mode - - useBundleExec: Use bundle exec to execute slather. Make sure it is in the Gemfile - - binaryBasename: Basename of the binary file, this should match the name of your bundle excluding its extension (i.e. YourApp [for YourApp.app bundle]) - - binaryFile: Binary file name to be used for code coverage - - arch: Specify which architecture the binary file is in. Needed for universal binaries - - sourceFiles: A Dir.glob compatible pattern used to limit the lookup to specific source files. Ignored in gcov mode - - decimals: The amount of decimals to use for % coverage reporting - - ymlfile: Relative path to a file used in place of '.slather.yml' - - Slather works with multiple code coverage formats, including Xcode 7 code coverage. - Slather is available at [https://github.com/SlatherOrg/slather](https://github.com/SlatherOrg/slather). - */ -public func slather(buildDirectory: OptionalConfigValue = .fastlaneDefault(nil), - proj: OptionalConfigValue = .fastlaneDefault(nil), - workspace: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - inputFormat: OptionalConfigValue = .fastlaneDefault(nil), - github: OptionalConfigValue = .fastlaneDefault(nil), - buildkite: OptionalConfigValue = .fastlaneDefault(nil), - teamcity: OptionalConfigValue = .fastlaneDefault(nil), - jenkins: OptionalConfigValue = .fastlaneDefault(nil), - travis: OptionalConfigValue = .fastlaneDefault(nil), - travisPro: OptionalConfigValue = .fastlaneDefault(nil), - circleci: OptionalConfigValue = .fastlaneDefault(nil), - coveralls: OptionalConfigValue = .fastlaneDefault(nil), - simpleOutput: OptionalConfigValue = .fastlaneDefault(nil), - gutterJson: OptionalConfigValue = .fastlaneDefault(nil), - coberturaXml: OptionalConfigValue = .fastlaneDefault(nil), - sonarqubeXml: OptionalConfigValue = .fastlaneDefault(nil), - llvmCov: OptionalConfigValue = .fastlaneDefault(nil), - json: OptionalConfigValue = .fastlaneDefault(nil), - html: OptionalConfigValue = .fastlaneDefault(nil), - show: OptionalConfigValue = .fastlaneDefault(false), - sourceDirectory: OptionalConfigValue = .fastlaneDefault(nil), - outputDirectory: OptionalConfigValue = .fastlaneDefault(nil), - ignore: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - verbose: OptionalConfigValue = .fastlaneDefault(nil), - useBundleExec: OptionalConfigValue = .fastlaneDefault(false), - binaryBasename: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - binaryFile: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - arch: OptionalConfigValue = .fastlaneDefault(nil), - sourceFiles: OptionalConfigValue = .fastlaneDefault(false), - decimals: OptionalConfigValue = .fastlaneDefault(false), - ymlfile: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let buildDirectoryArg = buildDirectory.asRubyArgument(name: "build_directory", type: nil) - let projArg = proj.asRubyArgument(name: "proj", type: nil) - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let inputFormatArg = inputFormat.asRubyArgument(name: "input_format", type: nil) - let githubArg = github.asRubyArgument(name: "github", type: nil) - let buildkiteArg = buildkite.asRubyArgument(name: "buildkite", type: nil) - let teamcityArg = teamcity.asRubyArgument(name: "teamcity", type: nil) - let jenkinsArg = jenkins.asRubyArgument(name: "jenkins", type: nil) - let travisArg = travis.asRubyArgument(name: "travis", type: nil) - let travisProArg = travisPro.asRubyArgument(name: "travis_pro", type: nil) - let circleciArg = circleci.asRubyArgument(name: "circleci", type: nil) - let coverallsArg = coveralls.asRubyArgument(name: "coveralls", type: nil) - let simpleOutputArg = simpleOutput.asRubyArgument(name: "simple_output", type: nil) - let gutterJsonArg = gutterJson.asRubyArgument(name: "gutter_json", type: nil) - let coberturaXmlArg = coberturaXml.asRubyArgument(name: "cobertura_xml", type: nil) - let sonarqubeXmlArg = sonarqubeXml.asRubyArgument(name: "sonarqube_xml", type: nil) - let llvmCovArg = llvmCov.asRubyArgument(name: "llvm_cov", type: nil) - let jsonArg = json.asRubyArgument(name: "json", type: nil) - let htmlArg = html.asRubyArgument(name: "html", type: nil) - let showArg = show.asRubyArgument(name: "show", type: nil) - let sourceDirectoryArg = sourceDirectory.asRubyArgument(name: "source_directory", type: nil) - let outputDirectoryArg = outputDirectory.asRubyArgument(name: "output_directory", type: nil) - let ignoreArg = ignore.asRubyArgument(name: "ignore", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let useBundleExecArg = useBundleExec.asRubyArgument(name: "use_bundle_exec", type: nil) - let binaryBasenameArg = binaryBasename.asRubyArgument(name: "binary_basename", type: nil) - let binaryFileArg = binaryFile.asRubyArgument(name: "binary_file", type: nil) - let archArg = arch.asRubyArgument(name: "arch", type: nil) - let sourceFilesArg = sourceFiles.asRubyArgument(name: "source_files", type: nil) - let decimalsArg = decimals.asRubyArgument(name: "decimals", type: nil) - let ymlfileArg = ymlfile.asRubyArgument(name: "ymlfile", type: nil) - let array: [RubyCommand.Argument?] = [buildDirectoryArg, - projArg, - workspaceArg, - schemeArg, - configurationArg, - inputFormatArg, - githubArg, - buildkiteArg, - teamcityArg, - jenkinsArg, - travisArg, - travisProArg, - circleciArg, - coverallsArg, - simpleOutputArg, - gutterJsonArg, - coberturaXmlArg, - sonarqubeXmlArg, - llvmCovArg, - jsonArg, - htmlArg, - showArg, - sourceDirectoryArg, - outputDirectoryArg, - ignoreArg, - verboseArg, - useBundleExecArg, - binaryBasenameArg, - binaryFileArg, - archArg, - sourceFilesArg, - decimalsArg, - ymlfileArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "slather", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `capture_ios_screenshots` action - - - parameters: - - workspace: Path to the workspace file - - project: Path to the project file - - xcargs: Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - - xcconfig: Use an extra XCCONFIG file to build your app - - devices: A list of devices you want to take the screenshots from - - languages: A list of languages which should be used - - launchArguments: A list of launch arguments which should be used - - outputDirectory: The directory where to store the screenshots - - outputSimulatorLogs: If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory - - iosVersion: By default, the latest version should be used automatically. If you want to change it, do it here - - skipOpenSummary: Don't open the HTML summary after running _snapshot_ - - skipHelperVersionCheck: Do not check for most recent SnapshotHelper code - - clearPreviousScreenshots: Enabling this option will automatically clear previously generated screenshots before running snapshot - - reinstallApp: Enabling this option will automatically uninstall the application before running it - - eraseSimulator: Enabling this option will automatically erase the simulator before running the application - - headless: Enabling this option will prevent displaying the simulator window - - overrideStatusBar: Enabling this option will automatically override the status bar to show 9:41 AM, full battery, and full reception (Adjust 'SNAPSHOT_SIMULATOR_WAIT_FOR_BOOT_TIMEOUT' environment variable if override status bar is not working. Might be because simulator is not fully booted. Defaults to 10 seconds) - - overrideStatusBarArguments: Fully customize the status bar by setting each option here. Requires `override_status_bar` to be set to `true`. See `xcrun simctl status_bar --help` - - localizeSimulator: Enabling this option will configure the Simulator's system language - - darkMode: Enabling this option will configure the Simulator to be in dark mode (false for light, true for dark) - - appIdentifier: The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) - - addPhotos: A list of photos that should be added to the simulator before running the application - - addVideos: A list of videos that should be added to the simulator before running the application - - htmlTemplate: A path to screenshots.html template - - buildlogPath: The directory where to store the build log - - clean: Should the project be cleaned before building it? - - testWithoutBuilding: Test without building, requires a derived data path - - configuration: The configuration to use when building the app. Defaults to 'Release' - - sdk: The SDK that should be used for building the application - - scheme: The scheme you want to use, this must be the scheme for the UI Tests - - numberOfRetries: The number of times a test can fail before snapshot should stop retrying - - stopAfterFirstError: Should snapshot stop immediately after the tests completely failed on one device? - - derivedDataPath: The directory where build products and other derived data will go - - resultBundle: Should an Xcode result bundle be generated in the output directory - - testTargetName: The name of the target you want to test (if you desire to override the Target Application from Xcode) - - namespaceLogFiles: Separate the log files per device and per language - - concurrentSimulators: Take snapshots on multiple simulators concurrently. Note: This option is only applicable when running against Xcode 9 - - disableSlideToType: Disable the simulator from showing the 'Slide to type' prompt - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - packageCachePath: Sets a custom package cache path for Swift Package Manager dependencies - - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - - skipPackageRepositoryFetches: Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - - testplan: The testplan associated with the scheme that should be used for testing - - onlyTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to run - - skipTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to skip - - xcodebuildFormatter: xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - - xcprettyArgs: **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Additional xcpretty arguments - - disableXcpretty: Disable xcpretty formatting of build - - suppressXcodeOutput: Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - - useSystemScm: Lets xcodebuild use system's scm configuration - */ -public func snapshot(workspace: OptionalConfigValue = .fastlaneDefault(snapshotfile.workspace), - project: OptionalConfigValue = .fastlaneDefault(snapshotfile.project), - xcargs: OptionalConfigValue = .fastlaneDefault(snapshotfile.xcargs), - xcconfig: OptionalConfigValue = .fastlaneDefault(snapshotfile.xcconfig), - devices: OptionalConfigValue<[String]?> = .fastlaneDefault(snapshotfile.devices), - languages: [String] = snapshotfile.languages, - launchArguments: [String] = snapshotfile.launchArguments, - outputDirectory: String = snapshotfile.outputDirectory, - outputSimulatorLogs: OptionalConfigValue = .fastlaneDefault(snapshotfile.outputSimulatorLogs), - iosVersion: OptionalConfigValue = .fastlaneDefault(snapshotfile.iosVersion), - skipOpenSummary: OptionalConfigValue = .fastlaneDefault(snapshotfile.skipOpenSummary), - skipHelperVersionCheck: OptionalConfigValue = .fastlaneDefault(snapshotfile.skipHelperVersionCheck), - clearPreviousScreenshots: OptionalConfigValue = .fastlaneDefault(snapshotfile.clearPreviousScreenshots), - reinstallApp: OptionalConfigValue = .fastlaneDefault(snapshotfile.reinstallApp), - eraseSimulator: OptionalConfigValue = .fastlaneDefault(snapshotfile.eraseSimulator), - headless: OptionalConfigValue = .fastlaneDefault(snapshotfile.headless), - overrideStatusBar: OptionalConfigValue = .fastlaneDefault(snapshotfile.overrideStatusBar), - overrideStatusBarArguments: OptionalConfigValue = .fastlaneDefault(snapshotfile.overrideStatusBarArguments), - localizeSimulator: OptionalConfigValue = .fastlaneDefault(snapshotfile.localizeSimulator), - darkMode: OptionalConfigValue = .fastlaneDefault(snapshotfile.darkMode), - appIdentifier: OptionalConfigValue = .fastlaneDefault(snapshotfile.appIdentifier), - addPhotos: OptionalConfigValue<[String]?> = .fastlaneDefault(snapshotfile.addPhotos), - addVideos: OptionalConfigValue<[String]?> = .fastlaneDefault(snapshotfile.addVideos), - htmlTemplate: OptionalConfigValue = .fastlaneDefault(snapshotfile.htmlTemplate), - buildlogPath: String = snapshotfile.buildlogPath, - clean: OptionalConfigValue = .fastlaneDefault(snapshotfile.clean), - testWithoutBuilding: OptionalConfigValue = .fastlaneDefault(snapshotfile.testWithoutBuilding), - configuration: OptionalConfigValue = .fastlaneDefault(snapshotfile.configuration), - sdk: OptionalConfigValue = .fastlaneDefault(snapshotfile.sdk), - scheme: OptionalConfigValue = .fastlaneDefault(snapshotfile.scheme), - numberOfRetries: Int = snapshotfile.numberOfRetries, - stopAfterFirstError: OptionalConfigValue = .fastlaneDefault(snapshotfile.stopAfterFirstError), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(snapshotfile.derivedDataPath), - resultBundle: OptionalConfigValue = .fastlaneDefault(snapshotfile.resultBundle), - testTargetName: OptionalConfigValue = .fastlaneDefault(snapshotfile.testTargetName), - namespaceLogFiles: Any? = snapshotfile.namespaceLogFiles, - concurrentSimulators: OptionalConfigValue = .fastlaneDefault(snapshotfile.concurrentSimulators), - disableSlideToType: OptionalConfigValue = .fastlaneDefault(snapshotfile.disableSlideToType), - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(snapshotfile.clonedSourcePackagesPath), - packageCachePath: OptionalConfigValue = .fastlaneDefault(snapshotfile.packageCachePath), - skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(snapshotfile.skipPackageDependenciesResolution), - disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(snapshotfile.disablePackageAutomaticUpdates), - skipPackageRepositoryFetches: OptionalConfigValue = .fastlaneDefault(snapshotfile.skipPackageRepositoryFetches), - packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(snapshotfile.packageAuthorizationProvider), - testplan: OptionalConfigValue = .fastlaneDefault(snapshotfile.testplan), - onlyTesting: Any? = snapshotfile.onlyTesting, - skipTesting: Any? = snapshotfile.skipTesting, - xcodebuildFormatter: String = snapshotfile.xcodebuildFormatter, - xcprettyArgs: OptionalConfigValue = .fastlaneDefault(snapshotfile.xcprettyArgs), - disableXcpretty: OptionalConfigValue = .fastlaneDefault(snapshotfile.disableXcpretty), - suppressXcodeOutput: OptionalConfigValue = .fastlaneDefault(snapshotfile.suppressXcodeOutput), - useSystemScm: OptionalConfigValue = .fastlaneDefault(snapshotfile.useSystemScm)) -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let xcargsArg = xcargs.asRubyArgument(name: "xcargs", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let devicesArg = devices.asRubyArgument(name: "devices", type: nil) - let languagesArg = RubyCommand.Argument(name: "languages", value: languages, type: nil) - let launchArgumentsArg = RubyCommand.Argument(name: "launch_arguments", value: launchArguments, type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let outputSimulatorLogsArg = outputSimulatorLogs.asRubyArgument(name: "output_simulator_logs", type: nil) - let iosVersionArg = iosVersion.asRubyArgument(name: "ios_version", type: nil) - let skipOpenSummaryArg = skipOpenSummary.asRubyArgument(name: "skip_open_summary", type: nil) - let skipHelperVersionCheckArg = skipHelperVersionCheck.asRubyArgument(name: "skip_helper_version_check", type: nil) - let clearPreviousScreenshotsArg = clearPreviousScreenshots.asRubyArgument(name: "clear_previous_screenshots", type: nil) - let reinstallAppArg = reinstallApp.asRubyArgument(name: "reinstall_app", type: nil) - let eraseSimulatorArg = eraseSimulator.asRubyArgument(name: "erase_simulator", type: nil) - let headlessArg = headless.asRubyArgument(name: "headless", type: nil) - let overrideStatusBarArg = overrideStatusBar.asRubyArgument(name: "override_status_bar", type: nil) - let overrideStatusBarArgumentsArg = overrideStatusBarArguments.asRubyArgument(name: "override_status_bar_arguments", type: nil) - let localizeSimulatorArg = localizeSimulator.asRubyArgument(name: "localize_simulator", type: nil) - let darkModeArg = darkMode.asRubyArgument(name: "dark_mode", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let addPhotosArg = addPhotos.asRubyArgument(name: "add_photos", type: nil) - let addVideosArg = addVideos.asRubyArgument(name: "add_videos", type: nil) - let htmlTemplateArg = htmlTemplate.asRubyArgument(name: "html_template", type: nil) - let buildlogPathArg = RubyCommand.Argument(name: "buildlog_path", value: buildlogPath, type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let testWithoutBuildingArg = testWithoutBuilding.asRubyArgument(name: "test_without_building", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let numberOfRetriesArg = RubyCommand.Argument(name: "number_of_retries", value: numberOfRetries, type: nil) - let stopAfterFirstErrorArg = stopAfterFirstError.asRubyArgument(name: "stop_after_first_error", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let resultBundleArg = resultBundle.asRubyArgument(name: "result_bundle", type: nil) - let testTargetNameArg = testTargetName.asRubyArgument(name: "test_target_name", type: nil) - let namespaceLogFilesArg = RubyCommand.Argument(name: "namespace_log_files", value: namespaceLogFiles, type: nil) - let concurrentSimulatorsArg = concurrentSimulators.asRubyArgument(name: "concurrent_simulators", type: nil) - let disableSlideToTypeArg = disableSlideToType.asRubyArgument(name: "disable_slide_to_type", type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let packageCachePathArg = packageCachePath.asRubyArgument(name: "package_cache_path", type: nil) - let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) - let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) - let skipPackageRepositoryFetchesArg = skipPackageRepositoryFetches.asRubyArgument(name: "skip_package_repository_fetches", type: nil) - let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) - let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) - let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) - let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) - let xcodebuildFormatterArg = RubyCommand.Argument(name: "xcodebuild_formatter", value: xcodebuildFormatter, type: nil) - let xcprettyArgsArg = xcprettyArgs.asRubyArgument(name: "xcpretty_args", type: nil) - let disableXcprettyArg = disableXcpretty.asRubyArgument(name: "disable_xcpretty", type: nil) - let suppressXcodeOutputArg = suppressXcodeOutput.asRubyArgument(name: "suppress_xcode_output", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - xcargsArg, - xcconfigArg, - devicesArg, - languagesArg, - launchArgumentsArg, - outputDirectoryArg, - outputSimulatorLogsArg, - iosVersionArg, - skipOpenSummaryArg, - skipHelperVersionCheckArg, - clearPreviousScreenshotsArg, - reinstallAppArg, - eraseSimulatorArg, - headlessArg, - overrideStatusBarArg, - overrideStatusBarArgumentsArg, - localizeSimulatorArg, - darkModeArg, - appIdentifierArg, - addPhotosArg, - addVideosArg, - htmlTemplateArg, - buildlogPathArg, - cleanArg, - testWithoutBuildingArg, - configurationArg, - sdkArg, - schemeArg, - numberOfRetriesArg, - stopAfterFirstErrorArg, - derivedDataPathArg, - resultBundleArg, - testTargetNameArg, - namespaceLogFilesArg, - concurrentSimulatorsArg, - disableSlideToTypeArg, - clonedSourcePackagesPathArg, - packageCachePathArg, - skipPackageDependenciesResolutionArg, - disablePackageAutomaticUpdatesArg, - skipPackageRepositoryFetchesArg, - packageAuthorizationProviderArg, - testplanArg, - onlyTestingArg, - skipTestingArg, - xcodebuildFormatterArg, - xcprettyArgsArg, - disableXcprettyArg, - suppressXcodeOutputArg, - useSystemScmArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "snapshot", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Invokes sonar-scanner to programmatically run SonarQube analysis - - - parameters: - - projectConfigurationPath: The path to your sonar project configuration file; defaults to `sonar-project.properties` - - projectKey: The key sonar uses to identify the project, e.g. `name.gretzki.awesomeApp`. Must either be specified here or inside the sonar project configuration file - - projectName: The name of the project that gets displayed on the sonar report page. Must either be specified here or inside the sonar project configuration file - - projectVersion: The project's version that gets displayed on the sonar report page. Must either be specified here or inside the sonar project configuration file - - sourcesPath: Comma-separated paths to directories containing source files. Must either be specified here or inside the sonar project configuration file - - exclusions: Comma-separated paths to directories to be excluded from the analysis - - projectLanguage: Language key, e.g. objc - - sourceEncoding: Used encoding of source files, e.g., UTF-8 - - sonarRunnerArgs: Pass additional arguments to sonar-scanner. Be sure to provide the arguments with a leading `-D` e.g. FL_SONAR_RUNNER_ARGS="-Dsonar.verbose=true" - - sonarLogin: **DEPRECATED!** Login and password were deprecated in favor of login token. See https://community.sonarsource.com/t/deprecating-sonar-login-and-sonar-password-in-favor-of-sonar-token/95829 for more details - Pass the Sonar Login Token (e.g: xxxxxxprivate_token_XXXXbXX7e) - - sonarToken: Pass the Sonar Token (e.g: xxxxxxprivate_token_XXXXbXX7e) - - sonarUrl: Pass the url of the Sonar server - - sonarOrganization: Key of the organization on SonarCloud - - branchName: Pass the branch name which is getting scanned - - pullRequestBranch: The name of the branch that contains the changes to be merged - - pullRequestBase: The long-lived branch into which the PR will be merged - - pullRequestKey: Unique identifier of your PR. Must correspond to the key of the PR in GitHub or TFS - - - returns: The exit code of the sonar-scanner binary - - See [http://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner](http://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner) for details. - It can process unit test results if formatted as junit report as shown in [xctest](https://docs.fastlane.tools/actions/xctest/) action. It can also integrate coverage reports in Cobertura format, which can be transformed into by the [slather](https://docs.fastlane.tools/actions/slather/) action. - */ -public func sonar(projectConfigurationPath: OptionalConfigValue = .fastlaneDefault(nil), - projectKey: OptionalConfigValue = .fastlaneDefault(nil), - projectName: OptionalConfigValue = .fastlaneDefault(nil), - projectVersion: OptionalConfigValue = .fastlaneDefault(nil), - sourcesPath: OptionalConfigValue = .fastlaneDefault(nil), - exclusions: OptionalConfigValue = .fastlaneDefault(nil), - projectLanguage: OptionalConfigValue = .fastlaneDefault(nil), - sourceEncoding: OptionalConfigValue = .fastlaneDefault(nil), - sonarRunnerArgs: OptionalConfigValue = .fastlaneDefault(nil), - sonarLogin: OptionalConfigValue = .fastlaneDefault(nil), - sonarToken: OptionalConfigValue = .fastlaneDefault(nil), - sonarUrl: OptionalConfigValue = .fastlaneDefault(nil), - sonarOrganization: OptionalConfigValue = .fastlaneDefault(nil), - branchName: OptionalConfigValue = .fastlaneDefault(nil), - pullRequestBranch: OptionalConfigValue = .fastlaneDefault(nil), - pullRequestBase: OptionalConfigValue = .fastlaneDefault(nil), - pullRequestKey: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let projectConfigurationPathArg = projectConfigurationPath.asRubyArgument(name: "project_configuration_path", type: nil) - let projectKeyArg = projectKey.asRubyArgument(name: "project_key", type: nil) - let projectNameArg = projectName.asRubyArgument(name: "project_name", type: nil) - let projectVersionArg = projectVersion.asRubyArgument(name: "project_version", type: nil) - let sourcesPathArg = sourcesPath.asRubyArgument(name: "sources_path", type: nil) - let exclusionsArg = exclusions.asRubyArgument(name: "exclusions", type: nil) - let projectLanguageArg = projectLanguage.asRubyArgument(name: "project_language", type: nil) - let sourceEncodingArg = sourceEncoding.asRubyArgument(name: "source_encoding", type: nil) - let sonarRunnerArgsArg = sonarRunnerArgs.asRubyArgument(name: "sonar_runner_args", type: nil) - let sonarLoginArg = sonarLogin.asRubyArgument(name: "sonar_login", type: nil) - let sonarTokenArg = sonarToken.asRubyArgument(name: "sonar_token", type: nil) - let sonarUrlArg = sonarUrl.asRubyArgument(name: "sonar_url", type: nil) - let sonarOrganizationArg = sonarOrganization.asRubyArgument(name: "sonar_organization", type: nil) - let branchNameArg = branchName.asRubyArgument(name: "branch_name", type: nil) - let pullRequestBranchArg = pullRequestBranch.asRubyArgument(name: "pull_request_branch", type: nil) - let pullRequestBaseArg = pullRequestBase.asRubyArgument(name: "pull_request_base", type: nil) - let pullRequestKeyArg = pullRequestKey.asRubyArgument(name: "pull_request_key", type: nil) - let array: [RubyCommand.Argument?] = [projectConfigurationPathArg, - projectKeyArg, - projectNameArg, - projectVersionArg, - sourcesPathArg, - exclusionsArg, - projectLanguageArg, - sourceEncodingArg, - sonarRunnerArgsArg, - sonarLoginArg, - sonarTokenArg, - sonarUrlArg, - sonarOrganizationArg, - branchNameArg, - pullRequestBranchArg, - pullRequestBaseArg, - pullRequestKeyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "sonar", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Generate docs using SourceDocs - - - parameters: - - allModules: Generate documentation for all modules in a Swift package - - spmModule: Generate documentation for Swift Package Manager module - - moduleName: Generate documentation for a Swift module - - linkBeginning: The text to begin links with - - linkEnding: The text to end links with (default: .md) - - outputFolder: Output directory to clean (default: Documentation/Reference) - - minAcl: Access level to include in documentation [private, fileprivate, internal, public, open] (default: public) - - moduleNamePath: Include the module name as part of the output folder path - - clean: Delete output folder before generating documentation - - collapsible: Put methods, properties and enum cases inside collapsible blocks - - tableOfContents: Generate a table of contents with properties and methods for each type - - reproducible: Generate documentation that is reproducible: only depends on the sources - - scheme: Create documentation for specific scheme - - sdkPlatform: Create documentation for specific sdk platform - */ -public func sourcedocs(allModules: OptionalConfigValue = .fastlaneDefault(nil), - spmModule: OptionalConfigValue = .fastlaneDefault(nil), - moduleName: OptionalConfigValue = .fastlaneDefault(nil), - linkBeginning: OptionalConfigValue = .fastlaneDefault(nil), - linkEnding: OptionalConfigValue = .fastlaneDefault(nil), - outputFolder: String, - minAcl: OptionalConfigValue = .fastlaneDefault(nil), - moduleNamePath: OptionalConfigValue = .fastlaneDefault(nil), - clean: OptionalConfigValue = .fastlaneDefault(nil), - collapsible: OptionalConfigValue = .fastlaneDefault(nil), - tableOfContents: OptionalConfigValue = .fastlaneDefault(nil), - reproducible: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - sdkPlatform: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let allModulesArg = allModules.asRubyArgument(name: "all_modules", type: nil) - let spmModuleArg = spmModule.asRubyArgument(name: "spm_module", type: nil) - let moduleNameArg = moduleName.asRubyArgument(name: "module_name", type: nil) - let linkBeginningArg = linkBeginning.asRubyArgument(name: "link_beginning", type: nil) - let linkEndingArg = linkEnding.asRubyArgument(name: "link_ending", type: nil) - let outputFolderArg = RubyCommand.Argument(name: "output_folder", value: outputFolder, type: nil) - let minAclArg = minAcl.asRubyArgument(name: "min_acl", type: nil) - let moduleNamePathArg = moduleNamePath.asRubyArgument(name: "module_name_path", type: nil) - let cleanArg = clean.asRubyArgument(name: "clean", type: nil) - let collapsibleArg = collapsible.asRubyArgument(name: "collapsible", type: nil) - let tableOfContentsArg = tableOfContents.asRubyArgument(name: "table_of_contents", type: nil) - let reproducibleArg = reproducible.asRubyArgument(name: "reproducible", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let sdkPlatformArg = sdkPlatform.asRubyArgument(name: "sdk_platform", type: nil) - let array: [RubyCommand.Argument?] = [allModulesArg, - spmModuleArg, - moduleNameArg, - linkBeginningArg, - linkEndingArg, - outputFolderArg, - minAclArg, - moduleNamePathArg, - cleanArg, - collapsibleArg, - tableOfContentsArg, - reproducibleArg, - schemeArg, - sdkPlatformArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "sourcedocs", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Find, print, and copy Spaceship logs - - - parameters: - - latest: Finds only the latest Spaceshop log file if set to true, otherwise returns all - - printContents: Prints the contents of the found Spaceship log file(s) - - printPaths: Prints the paths of the found Spaceship log file(s) - - copyToPath: Copies the found Spaceship log file(s) to a directory - - copyToClipboard: Copies the contents of the found Spaceship log file(s) to the clipboard - - - returns: The array of Spaceship logs - */ -@discardableResult public func spaceshipLogs(latest: OptionalConfigValue = .fastlaneDefault(true), - printContents: OptionalConfigValue = .fastlaneDefault(false), - printPaths: OptionalConfigValue = .fastlaneDefault(false), - copyToPath: OptionalConfigValue = .fastlaneDefault(nil), - copyToClipboard: OptionalConfigValue = .fastlaneDefault(false)) -> [String] -{ - let latestArg = latest.asRubyArgument(name: "latest", type: nil) - let printContentsArg = printContents.asRubyArgument(name: "print_contents", type: nil) - let printPathsArg = printPaths.asRubyArgument(name: "print_paths", type: nil) - let copyToPathArg = copyToPath.asRubyArgument(name: "copy_to_path", type: nil) - let copyToClipboardArg = copyToClipboard.asRubyArgument(name: "copy_to_clipboard", type: nil) - let array: [RubyCommand.Argument?] = [latestArg, - printContentsArg, - printPathsArg, - copyToPathArg, - copyToClipboardArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "spaceship_logs", className: nil, args: args) - return parseArray(fromString: runner.executeCommand(command)) -} - -/** - Print out Spaceship stats from this session (number of request to each domain) - - - parameter printRequestLogs: Print all URLs requested - */ -public func spaceshipStats(printRequestLogs: OptionalConfigValue = .fastlaneDefault(false)) { - let printRequestLogsArg = printRequestLogs.asRubyArgument(name: "print_request_logs", type: nil) - let array: [RubyCommand.Argument?] = [printRequestLogsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "spaceship_stats", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload dSYM file to [Splunk MINT](https://mint.splunk.com/) - - - parameters: - - dsym: dSYM.zip file to upload to Splunk MINT - - apiKey: Splunk MINT App API key e.g. f57a57ca - - apiToken: Splunk MINT API token e.g. e05ba40754c4869fb7e0b61 - - verbose: Make detailed output - - uploadProgress: Show upload progress - - proxyUsername: Proxy username - - proxyPassword: Proxy password - - proxyAddress: Proxy address - - proxyPort: Proxy port - */ -public func splunkmint(dsym: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: String, - apiToken: String, - verbose: OptionalConfigValue = .fastlaneDefault(false), - uploadProgress: OptionalConfigValue = .fastlaneDefault(false), - proxyUsername: OptionalConfigValue = .fastlaneDefault(nil), - proxyPassword: OptionalConfigValue = .fastlaneDefault(nil), - proxyAddress: OptionalConfigValue = .fastlaneDefault(nil), - proxyPort: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let dsymArg = dsym.asRubyArgument(name: "dsym", type: nil) - let apiKeyArg = RubyCommand.Argument(name: "api_key", value: apiKey, type: nil) - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let uploadProgressArg = uploadProgress.asRubyArgument(name: "upload_progress", type: nil) - let proxyUsernameArg = proxyUsername.asRubyArgument(name: "proxy_username", type: nil) - let proxyPasswordArg = proxyPassword.asRubyArgument(name: "proxy_password", type: nil) - let proxyAddressArg = proxyAddress.asRubyArgument(name: "proxy_address", type: nil) - let proxyPortArg = proxyPort.asRubyArgument(name: "proxy_port", type: nil) - let array: [RubyCommand.Argument?] = [dsymArg, - apiKeyArg, - apiTokenArg, - verboseArg, - uploadProgressArg, - proxyUsernameArg, - proxyPasswordArg, - proxyAddressArg, - proxyPortArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "splunkmint", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs Swift Package Manager on your project - - - parameters: - - command: The swift command (one of: build, test, clean, reset, update, resolve, generate-xcodeproj, init) - - enableCodeCoverage: Enables code coverage for the generated Xcode project when using the 'generate-xcodeproj' and the 'test' command - - scratchPath: Specify build/cache directory [default: ./.build] - - parallel: Enables running tests in parallel when using the 'test' command - - buildPath: **DEPRECATED!** `build_path` option is deprecated, use `scratch_path` instead - Specify build/cache directory [default: ./.build] - - packagePath: Change working directory before any other operation - - xcconfig: Use xcconfig file to override swift package generate-xcodeproj defaults - - configuration: Build with configuration (debug|release) [default: debug] - - disableSandbox: Disable using the sandbox when executing subprocesses - - xcprettyOutput: Specifies the output type for xcpretty. eg. 'test', or 'simple' - - xcprettyArgs: Pass in xcpretty additional command line arguments (e.g. '--test --no-color' or '--tap --no-utf'), requires xcpretty_output to be specified also - - verbose: Increase verbosity of informational output - - veryVerbose: Increase verbosity to include debug output - - simulator: Specifies the simulator to pass for Swift Compiler (one of: iphonesimulator, macosx) - - simulatorArch: Specifies the architecture of the simulator to pass for Swift Compiler (one of: x86_64, arm64). Requires the simulator option to be specified also, otherwise, it's ignored - */ -public func spm(command: String = "build", - enableCodeCoverage: OptionalConfigValue = .fastlaneDefault(nil), - scratchPath: OptionalConfigValue = .fastlaneDefault(nil), - parallel: OptionalConfigValue = .fastlaneDefault(false), - buildPath: OptionalConfigValue = .fastlaneDefault(nil), - packagePath: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - disableSandbox: OptionalConfigValue = .fastlaneDefault(false), - xcprettyOutput: OptionalConfigValue = .fastlaneDefault(nil), - xcprettyArgs: OptionalConfigValue = .fastlaneDefault(nil), - verbose: OptionalConfigValue = .fastlaneDefault(false), - veryVerbose: OptionalConfigValue = .fastlaneDefault(false), - simulator: OptionalConfigValue = .fastlaneDefault(nil), - simulatorArch: String = "arm64") -{ - let commandArg = RubyCommand.Argument(name: "command", value: command, type: nil) - let enableCodeCoverageArg = enableCodeCoverage.asRubyArgument(name: "enable_code_coverage", type: nil) - let scratchPathArg = scratchPath.asRubyArgument(name: "scratch_path", type: nil) - let parallelArg = parallel.asRubyArgument(name: "parallel", type: nil) - let buildPathArg = buildPath.asRubyArgument(name: "build_path", type: nil) - let packagePathArg = packagePath.asRubyArgument(name: "package_path", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let disableSandboxArg = disableSandbox.asRubyArgument(name: "disable_sandbox", type: nil) - let xcprettyOutputArg = xcprettyOutput.asRubyArgument(name: "xcpretty_output", type: nil) - let xcprettyArgsArg = xcprettyArgs.asRubyArgument(name: "xcpretty_args", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let veryVerboseArg = veryVerbose.asRubyArgument(name: "very_verbose", type: nil) - let simulatorArg = simulator.asRubyArgument(name: "simulator", type: nil) - let simulatorArchArg = RubyCommand.Argument(name: "simulator_arch", value: simulatorArch, type: nil) - let array: [RubyCommand.Argument?] = [commandArg, - enableCodeCoverageArg, - scratchPathArg, - parallelArg, - buildPathArg, - packagePathArg, - xcconfigArg, - configurationArg, - disableSandboxArg, - xcprettyOutputArg, - xcprettyArgsArg, - verboseArg, - veryVerboseArg, - simulatorArg, - simulatorArchArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "spm", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Allows remote command execution using ssh - - - parameters: - - username: Username - - password: Password - - host: Hostname - - port: Port - - commands: Commands - - log: Log commands and output - - Lets you execute remote commands via ssh using username/password or ssh-agent. If one of the commands in command-array returns non 0, it fails. - */ -public func ssh(username: String, - password: OptionalConfigValue = .fastlaneDefault(nil), - host: String, - port: String = "22", - commands: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - log: OptionalConfigValue = .fastlaneDefault(true)) -{ - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let passwordArg = password.asRubyArgument(name: "password", type: nil) - let hostArg = RubyCommand.Argument(name: "host", value: host, type: nil) - let portArg = RubyCommand.Argument(name: "port", value: port, type: nil) - let commandsArg = commands.asRubyArgument(name: "commands", type: nil) - let logArg = log.asRubyArgument(name: "log", type: nil) - let array: [RubyCommand.Argument?] = [usernameArg, - passwordArg, - hostArg, - portArg, - commandsArg, - logArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "ssh", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `upload_to_play_store` action - - - parameters: - - packageName: The package name of the application to use - - versionName: Version name (used when uploading new apks/aabs) - defaults to 'versionName' in build.gradle or AndroidManifest.xml - - versionCode: The versionCode for which to download the generated APK - - releaseStatus: Release status (used when uploading new apks/aabs) - valid values are completed, draft, halted, inProgress - - track: The track of the application to use. The default available tracks are: production, beta, alpha, internal - - rollout: The percentage of the user fraction when uploading to the rollout track (setting to 1 will complete the rollout) - - metadataPath: Path to the directory containing the metadata files - - key: **DEPRECATED!** Use `--json_key` instead - The p12 File used to authenticate with Google - - issuer: **DEPRECATED!** Use `--json_key` instead - The issuer of the p12 file (email address of the service account) - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - apk: Path to the APK file to upload - - apkPaths: An array of paths to APK files to upload - - aab: Path to the AAB file to upload - - aabPaths: An array of paths to AAB files to upload - - skipUploadApk: Whether to skip uploading APK - - skipUploadAab: Whether to skip uploading AAB - - skipUploadMetadata: Whether to skip uploading metadata, changelogs not included - - skipUploadChangelogs: Whether to skip uploading changelogs - - skipUploadImages: Whether to skip uploading images, screenshots not included - - skipUploadScreenshots: Whether to skip uploading SCREENSHOTS - - syncImageUpload: Whether to use sha256 comparison to skip upload of images and screenshots that are already in Play Store - - trackPromoteTo: The track to promote to. The default available tracks are: production, beta, alpha, internal - - trackPromoteReleaseStatus: Promoted track release status (used when promoting a track) - valid values are completed, draft, halted, inProgress - - validateOnly: Only validate changes with Google Play rather than actually publish - - mapping: Path to the mapping file to upload (mapping.txt or native-debug-symbols.zip alike) - - mappingPaths: An array of paths to mapping files to upload (mapping.txt or native-debug-symbols.zip alike) - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - checkSupersededTracks: **DEPRECATED!** Google Play does this automatically now - Check the other tracks for superseded versions and disable them - - timeout: Timeout for read, open, and send (in seconds) - - deactivateOnPromote: **DEPRECATED!** Google Play does this automatically now - When promoting to a new track, deactivate the binary in the origin track - - versionCodesToRetain: An array of version codes to retain when publishing a new APK - - changesNotSentForReview: Indicates that the changes in this edit will not be reviewed until they are explicitly sent for review from the Google Play Console UI - - rescueChangesNotSentForReview: Catches changes_not_sent_for_review errors when an edit is committed and retries with the configuration that the error message recommended - - inAppUpdatePriority: In-app update priority for all the newly added apks in the release. Can take values between [0,5] - - obbMainReferencesVersion: References version of 'main' expansion file - - obbMainFileSize: Size of 'main' expansion file in bytes - - obbPatchReferencesVersion: References version of 'patch' expansion file - - obbPatchFileSize: Size of 'patch' expansion file in bytes - - ackBundleInstallationWarning: Must be set to true if the bundle installation may trigger a warning on user devices (e.g can only be downloaded over wifi). Typically this is required for bundles over 150MB - - More information: https://docs.fastlane.tools/actions/supply/ - */ -public func supply(packageName: String, - versionName: OptionalConfigValue = .fastlaneDefault(nil), - versionCode: OptionalConfigValue = .fastlaneDefault(nil), - releaseStatus: String = "completed", - track: String = "production", - rollout: OptionalConfigValue = .fastlaneDefault(nil), - metadataPath: OptionalConfigValue = .fastlaneDefault(nil), - key: OptionalConfigValue = .fastlaneDefault(nil), - issuer: OptionalConfigValue = .fastlaneDefault(nil), - jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - apk: OptionalConfigValue = .fastlaneDefault(nil), - apkPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - aab: OptionalConfigValue = .fastlaneDefault(nil), - aabPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - skipUploadApk: OptionalConfigValue = .fastlaneDefault(false), - skipUploadAab: OptionalConfigValue = .fastlaneDefault(false), - skipUploadMetadata: OptionalConfigValue = .fastlaneDefault(false), - skipUploadChangelogs: OptionalConfigValue = .fastlaneDefault(false), - skipUploadImages: OptionalConfigValue = .fastlaneDefault(false), - skipUploadScreenshots: OptionalConfigValue = .fastlaneDefault(false), - syncImageUpload: OptionalConfigValue = .fastlaneDefault(false), - trackPromoteTo: OptionalConfigValue = .fastlaneDefault(nil), - trackPromoteReleaseStatus: String = "completed", - validateOnly: OptionalConfigValue = .fastlaneDefault(false), - mapping: OptionalConfigValue = .fastlaneDefault(nil), - mappingPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - checkSupersededTracks: OptionalConfigValue = .fastlaneDefault(false), - timeout: Int = 300, - deactivateOnPromote: OptionalConfigValue = .fastlaneDefault(true), - versionCodesToRetain: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - changesNotSentForReview: OptionalConfigValue = .fastlaneDefault(false), - rescueChangesNotSentForReview: OptionalConfigValue = .fastlaneDefault(true), - inAppUpdatePriority: OptionalConfigValue = .fastlaneDefault(nil), - obbMainReferencesVersion: OptionalConfigValue = .fastlaneDefault(nil), - obbMainFileSize: OptionalConfigValue = .fastlaneDefault(nil), - obbPatchReferencesVersion: OptionalConfigValue = .fastlaneDefault(nil), - obbPatchFileSize: OptionalConfigValue = .fastlaneDefault(nil), - ackBundleInstallationWarning: OptionalConfigValue = .fastlaneDefault(false)) -{ - let packageNameArg = RubyCommand.Argument(name: "package_name", value: packageName, type: nil) - let versionNameArg = versionName.asRubyArgument(name: "version_name", type: nil) - let versionCodeArg = versionCode.asRubyArgument(name: "version_code", type: nil) - let releaseStatusArg = RubyCommand.Argument(name: "release_status", value: releaseStatus, type: nil) - let trackArg = RubyCommand.Argument(name: "track", value: track, type: nil) - let rolloutArg = rollout.asRubyArgument(name: "rollout", type: nil) - let metadataPathArg = metadataPath.asRubyArgument(name: "metadata_path", type: nil) - let keyArg = key.asRubyArgument(name: "key", type: nil) - let issuerArg = issuer.asRubyArgument(name: "issuer", type: nil) - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let apkArg = apk.asRubyArgument(name: "apk", type: nil) - let apkPathsArg = apkPaths.asRubyArgument(name: "apk_paths", type: nil) - let aabArg = aab.asRubyArgument(name: "aab", type: nil) - let aabPathsArg = aabPaths.asRubyArgument(name: "aab_paths", type: nil) - let skipUploadApkArg = skipUploadApk.asRubyArgument(name: "skip_upload_apk", type: nil) - let skipUploadAabArg = skipUploadAab.asRubyArgument(name: "skip_upload_aab", type: nil) - let skipUploadMetadataArg = skipUploadMetadata.asRubyArgument(name: "skip_upload_metadata", type: nil) - let skipUploadChangelogsArg = skipUploadChangelogs.asRubyArgument(name: "skip_upload_changelogs", type: nil) - let skipUploadImagesArg = skipUploadImages.asRubyArgument(name: "skip_upload_images", type: nil) - let skipUploadScreenshotsArg = skipUploadScreenshots.asRubyArgument(name: "skip_upload_screenshots", type: nil) - let syncImageUploadArg = syncImageUpload.asRubyArgument(name: "sync_image_upload", type: nil) - let trackPromoteToArg = trackPromoteTo.asRubyArgument(name: "track_promote_to", type: nil) - let trackPromoteReleaseStatusArg = RubyCommand.Argument(name: "track_promote_release_status", value: trackPromoteReleaseStatus, type: nil) - let validateOnlyArg = validateOnly.asRubyArgument(name: "validate_only", type: nil) - let mappingArg = mapping.asRubyArgument(name: "mapping", type: nil) - let mappingPathsArg = mappingPaths.asRubyArgument(name: "mapping_paths", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let checkSupersededTracksArg = checkSupersededTracks.asRubyArgument(name: "check_superseded_tracks", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let deactivateOnPromoteArg = deactivateOnPromote.asRubyArgument(name: "deactivate_on_promote", type: nil) - let versionCodesToRetainArg = versionCodesToRetain.asRubyArgument(name: "version_codes_to_retain", type: nil) - let changesNotSentForReviewArg = changesNotSentForReview.asRubyArgument(name: "changes_not_sent_for_review", type: nil) - let rescueChangesNotSentForReviewArg = rescueChangesNotSentForReview.asRubyArgument(name: "rescue_changes_not_sent_for_review", type: nil) - let inAppUpdatePriorityArg = inAppUpdatePriority.asRubyArgument(name: "in_app_update_priority", type: nil) - let obbMainReferencesVersionArg = obbMainReferencesVersion.asRubyArgument(name: "obb_main_references_version", type: nil) - let obbMainFileSizeArg = obbMainFileSize.asRubyArgument(name: "obb_main_file_size", type: nil) - let obbPatchReferencesVersionArg = obbPatchReferencesVersion.asRubyArgument(name: "obb_patch_references_version", type: nil) - let obbPatchFileSizeArg = obbPatchFileSize.asRubyArgument(name: "obb_patch_file_size", type: nil) - let ackBundleInstallationWarningArg = ackBundleInstallationWarning.asRubyArgument(name: "ack_bundle_installation_warning", type: nil) - let array: [RubyCommand.Argument?] = [packageNameArg, - versionNameArg, - versionCodeArg, - releaseStatusArg, - trackArg, - rolloutArg, - metadataPathArg, - keyArg, - issuerArg, - jsonKeyArg, - jsonKeyDataArg, - apkArg, - apkPathsArg, - aabArg, - aabPathsArg, - skipUploadApkArg, - skipUploadAabArg, - skipUploadMetadataArg, - skipUploadChangelogsArg, - skipUploadImagesArg, - skipUploadScreenshotsArg, - syncImageUploadArg, - trackPromoteToArg, - trackPromoteReleaseStatusArg, - validateOnlyArg, - mappingArg, - mappingPathsArg, - rootUrlArg, - checkSupersededTracksArg, - timeoutArg, - deactivateOnPromoteArg, - versionCodesToRetainArg, - changesNotSentForReviewArg, - rescueChangesNotSentForReviewArg, - inAppUpdatePriorityArg, - obbMainReferencesVersionArg, - obbMainFileSizeArg, - obbPatchReferencesVersionArg, - obbPatchFileSizeArg, - ackBundleInstallationWarningArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "supply", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Run swift code validation using SwiftLint - - - parameters: - - mode: SwiftLint mode: :lint, :fix, :autocorrect or :analyze - - path: Specify path to lint - - outputFile: Path to output SwiftLint result - - configFile: Custom configuration file of SwiftLint - - strict: Fail on warnings? (true/false) - - files: List of files to process - - ignoreExitStatus: Ignore the exit status of the SwiftLint command, so that serious violations don't fail the build (true/false) - - raiseIfSwiftlintError: Raises an error if swiftlint fails, so you can fail CI/CD jobs if necessary (true/false) - - reporter: Choose output reporter. Available: xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging - - quiet: Don't print status logs like 'Linting ' & 'Done linting' - - executable: Path to the `swiftlint` executable on your machine - - format: Format code when mode is :autocorrect - - noCache: Ignore the cache when mode is :autocorrect or :lint - - compilerLogPath: Compiler log path when mode is :analyze - - progress: Show a live-updating progress bar instead of each file being processed - */ -public func swiftlint(mode: String = "lint", - path: OptionalConfigValue = .fastlaneDefault(nil), - outputFile: OptionalConfigValue = .fastlaneDefault(nil), - configFile: OptionalConfigValue = .fastlaneDefault(nil), - strict: OptionalConfigValue = .fastlaneDefault(false), - files: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - ignoreExitStatus: OptionalConfigValue = .fastlaneDefault(false), - raiseIfSwiftlintError: OptionalConfigValue = .fastlaneDefault(false), - reporter: OptionalConfigValue = .fastlaneDefault(nil), - quiet: OptionalConfigValue = .fastlaneDefault(false), - executable: OptionalConfigValue = .fastlaneDefault(nil), - format: OptionalConfigValue = .fastlaneDefault(false), - noCache: OptionalConfigValue = .fastlaneDefault(false), - compilerLogPath: OptionalConfigValue = .fastlaneDefault(nil), - progress: OptionalConfigValue = .fastlaneDefault(false)) -{ - let modeArg = RubyCommand.Argument(name: "mode", value: mode, type: nil) - let pathArg = path.asRubyArgument(name: "path", type: nil) - let outputFileArg = outputFile.asRubyArgument(name: "output_file", type: nil) - let configFileArg = configFile.asRubyArgument(name: "config_file", type: nil) - let strictArg = strict.asRubyArgument(name: "strict", type: nil) - let filesArg = files.asRubyArgument(name: "files", type: nil) - let ignoreExitStatusArg = ignoreExitStatus.asRubyArgument(name: "ignore_exit_status", type: nil) - let raiseIfSwiftlintErrorArg = raiseIfSwiftlintError.asRubyArgument(name: "raise_if_swiftlint_error", type: nil) - let reporterArg = reporter.asRubyArgument(name: "reporter", type: nil) - let quietArg = quiet.asRubyArgument(name: "quiet", type: nil) - let executableArg = executable.asRubyArgument(name: "executable", type: nil) - let formatArg = format.asRubyArgument(name: "format", type: nil) - let noCacheArg = noCache.asRubyArgument(name: "no_cache", type: nil) - let compilerLogPathArg = compilerLogPath.asRubyArgument(name: "compiler_log_path", type: nil) - let progressArg = progress.asRubyArgument(name: "progress", type: nil) - let array: [RubyCommand.Argument?] = [modeArg, - pathArg, - outputFileArg, - configFileArg, - strictArg, - filesArg, - ignoreExitStatusArg, - raiseIfSwiftlintErrorArg, - reporterArg, - quietArg, - executableArg, - formatArg, - noCacheArg, - compilerLogPathArg, - progressArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "swiftlint", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Easily sync your certificates and profiles across your team (via _match_) - - - parameters: - - type: Define the profile type, can be appstore, adhoc, development, enterprise, developer_id, mac_installer_distribution, developer_id_installer - - additionalCertTypes: Create additional cert types needed for macOS installers (valid values: mac_installer_distribution, developer_id_installer) - - readonly: Only fetch existing certificates and profiles, don't generate new ones - - generateAppleCerts: Create a certificate type for Xcode 11 and later (Apple Development or Apple Distribution) - - skipProvisioningProfiles: Skip syncing provisioning profiles - - appIdentifier: The bundle identifier(s) of your app (comma-separated string or array of strings) - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - teamId: The ID of your Developer Portal team if you're in multiple teams - - teamName: The name of your Developer Portal team if you're in multiple teams - - storageMode: Define where you want to store your certificates - - gitUrl: URL to the git repo containing all the certificates - - gitBranch: Specific git branch to use - - gitFullName: git user full name to commit - - gitUserEmail: git user email to commit - - shallowClone: Make a shallow clone of the repository (truncate the history to 1 revision) - - cloneBranchDirectly: Clone just the branch specified, instead of the whole repo. This requires that the branch already exists. Otherwise the command will fail - - gitBasicAuthorization: Use a basic authorization header to access the git repo (e.g.: access via HTTPS, GitHub Actions, etc), usually a string in Base64 - - gitBearerAuthorization: Use a bearer authorization header to access the git repo (e.g.: access to an Azure DevOps repository), usually a string in Base64 - - gitPrivateKey: Use a private key to access the git repo (e.g.: access to GitHub repository via Deploy keys), usually a id_rsa named file or the contents hereof - - googleCloudBucketName: Name of the Google Cloud Storage bucket to use - - googleCloudKeysFile: Path to the gc_keys.json file - - googleCloudProjectId: ID of the Google Cloud project to use for authentication - - skipGoogleCloudAccountConfirmation: Skips confirming to use the system google account - - s3Region: Name of the S3 region - - s3AccessKey: S3 access key - - s3SecretAccessKey: S3 secret access key - - s3SessionToken: S3 session token - - s3Bucket: Name of the S3 bucket - - s3ObjectPrefix: Prefix to be used on all objects uploaded to S3 - - s3SkipEncryption: Skip encryption of all objects uploaded to S3. WARNING: only enable this on S3 buckets with sufficiently restricted permissions and server-side encryption enabled. See https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingEncryption.html - - gitlabProject: GitLab Project Path (i.e. 'gitlab-org/gitlab') - - gitlabHost: GitLab Host (i.e. 'https://gitlab.com') - - jobToken: GitLab CI_JOB_TOKEN - - privateToken: GitLab Access Token - - keychainName: Keychain the items should be imported to - - keychainPassword: This might be required the first time you access certificates on a new mac. For the login/default keychain this is your macOS account password - - force: Renew the provisioning profiles every time you run match - - forceForNewDevices: Renew the provisioning profiles if the device count on the developer portal has changed. Ignored for profile types 'appstore' and 'developer_id' - - includeMacInProfiles: Include Apple Silicon Mac devices in provisioning profiles for iOS/iPadOS apps - - includeAllCertificates: Include all matching certificates in the provisioning profile. Works only for the 'development' provisioning profile type - - certificateId: Select certificate by id. Useful if multiple certificates are stored in one place - - forceForNewCertificates: Renew the provisioning profiles if the certificate count on the developer portal has changed. Works only for the 'development' provisioning profile type. Requires 'include_all_certificates' option to be 'true' - - skipConfirmation: Disables confirmation prompts during nuke, answering them with yes - - safeRemoveCerts: Remove certs from repository during nuke without revoking them on the developer portal - - skipDocs: Skip generation of a README.md for the created git repository - - platform: Set the provisioning profile's platform to work with (i.e. ios, tvos, macos, catalyst) - - deriveCatalystAppIdentifier: Enable this if you have the Mac Catalyst capability enabled and your project was created with Xcode 11.3 or earlier. Prepends 'maccatalyst.' to the app identifier for the provisioning profile mapping - - templateName: **DEPRECATED!** Removed since May 2025 on App Store Connect API OpenAPI v3.8.0 - Learn more: https://docs.fastlane.tools/actions/match/#managed-capabilities - The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. "Apple Pay Pass Suppression Development") - - profileName: A custom name for the provisioning profile. This will replace the default provisioning profile name if specified - - failOnNameTaken: Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first - - skipCertificateMatching: Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action - - outputPath: Path in which to export certificates, key and profile - - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing - - forceLegacyEncryption: Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions - - verbose: Print out extra information and all commands - - More information: https://docs.fastlane.tools/actions/match/ - */ -public func syncCodeSigning(type: String = "development", - additionalCertTypes: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - readonly: OptionalConfigValue = .fastlaneDefault(false), - generateAppleCerts: OptionalConfigValue = .fastlaneDefault(true), - skipProvisioningProfiles: OptionalConfigValue = .fastlaneDefault(false), - appIdentifier: [String], - apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - storageMode: String = "git", - gitUrl: String, - gitBranch: String = "master", - gitFullName: OptionalConfigValue = .fastlaneDefault(nil), - gitUserEmail: OptionalConfigValue = .fastlaneDefault(nil), - shallowClone: OptionalConfigValue = .fastlaneDefault(false), - cloneBranchDirectly: OptionalConfigValue = .fastlaneDefault(false), - gitBasicAuthorization: OptionalConfigValue = .fastlaneDefault(nil), - gitBearerAuthorization: OptionalConfigValue = .fastlaneDefault(nil), - gitPrivateKey: OptionalConfigValue = .fastlaneDefault(nil), - googleCloudBucketName: OptionalConfigValue = .fastlaneDefault(nil), - googleCloudKeysFile: OptionalConfigValue = .fastlaneDefault(nil), - googleCloudProjectId: OptionalConfigValue = .fastlaneDefault(nil), - skipGoogleCloudAccountConfirmation: OptionalConfigValue = .fastlaneDefault(false), - s3Region: OptionalConfigValue = .fastlaneDefault(nil), - s3AccessKey: OptionalConfigValue = .fastlaneDefault(nil), - s3SecretAccessKey: OptionalConfigValue = .fastlaneDefault(nil), - s3SessionToken: OptionalConfigValue = .fastlaneDefault(nil), - s3Bucket: OptionalConfigValue = .fastlaneDefault(nil), - s3ObjectPrefix: OptionalConfigValue = .fastlaneDefault(nil), - s3SkipEncryption: OptionalConfigValue = .fastlaneDefault(false), - gitlabProject: OptionalConfigValue = .fastlaneDefault(nil), - gitlabHost: String = "https://gitlab.com", - jobToken: OptionalConfigValue = .fastlaneDefault(nil), - privateToken: OptionalConfigValue = .fastlaneDefault(nil), - keychainName: String = "login.keychain", - keychainPassword: OptionalConfigValue = .fastlaneDefault(nil), - force: OptionalConfigValue = .fastlaneDefault(false), - forceForNewDevices: OptionalConfigValue = .fastlaneDefault(false), - includeMacInProfiles: OptionalConfigValue = .fastlaneDefault(false), - includeAllCertificates: OptionalConfigValue = .fastlaneDefault(false), - certificateId: OptionalConfigValue = .fastlaneDefault(nil), - forceForNewCertificates: OptionalConfigValue = .fastlaneDefault(false), - skipConfirmation: OptionalConfigValue = .fastlaneDefault(false), - safeRemoveCerts: OptionalConfigValue = .fastlaneDefault(false), - skipDocs: OptionalConfigValue = .fastlaneDefault(false), - platform: String = "ios", - deriveCatalystAppIdentifier: OptionalConfigValue = .fastlaneDefault(false), - templateName: OptionalConfigValue = .fastlaneDefault(nil), - profileName: OptionalConfigValue = .fastlaneDefault(nil), - failOnNameTaken: OptionalConfigValue = .fastlaneDefault(false), - skipCertificateMatching: OptionalConfigValue = .fastlaneDefault(false), - outputPath: OptionalConfigValue = .fastlaneDefault(nil), - skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(false), - forceLegacyEncryption: OptionalConfigValue = .fastlaneDefault(false), - verbose: OptionalConfigValue = .fastlaneDefault(false)) -{ - let typeArg = RubyCommand.Argument(name: "type", value: type, type: nil) - let additionalCertTypesArg = additionalCertTypes.asRubyArgument(name: "additional_cert_types", type: nil) - let readonlyArg = readonly.asRubyArgument(name: "readonly", type: nil) - let generateAppleCertsArg = generateAppleCerts.asRubyArgument(name: "generate_apple_certs", type: nil) - let skipProvisioningProfilesArg = skipProvisioningProfiles.asRubyArgument(name: "skip_provisioning_profiles", type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let storageModeArg = RubyCommand.Argument(name: "storage_mode", value: storageMode, type: nil) - let gitUrlArg = RubyCommand.Argument(name: "git_url", value: gitUrl, type: nil) - let gitBranchArg = RubyCommand.Argument(name: "git_branch", value: gitBranch, type: nil) - let gitFullNameArg = gitFullName.asRubyArgument(name: "git_full_name", type: nil) - let gitUserEmailArg = gitUserEmail.asRubyArgument(name: "git_user_email", type: nil) - let shallowCloneArg = shallowClone.asRubyArgument(name: "shallow_clone", type: nil) - let cloneBranchDirectlyArg = cloneBranchDirectly.asRubyArgument(name: "clone_branch_directly", type: nil) - let gitBasicAuthorizationArg = gitBasicAuthorization.asRubyArgument(name: "git_basic_authorization", type: nil) - let gitBearerAuthorizationArg = gitBearerAuthorization.asRubyArgument(name: "git_bearer_authorization", type: nil) - let gitPrivateKeyArg = gitPrivateKey.asRubyArgument(name: "git_private_key", type: nil) - let googleCloudBucketNameArg = googleCloudBucketName.asRubyArgument(name: "google_cloud_bucket_name", type: nil) - let googleCloudKeysFileArg = googleCloudKeysFile.asRubyArgument(name: "google_cloud_keys_file", type: nil) - let googleCloudProjectIdArg = googleCloudProjectId.asRubyArgument(name: "google_cloud_project_id", type: nil) - let skipGoogleCloudAccountConfirmationArg = skipGoogleCloudAccountConfirmation.asRubyArgument(name: "skip_google_cloud_account_confirmation", type: nil) - let s3RegionArg = s3Region.asRubyArgument(name: "s3_region", type: nil) - let s3AccessKeyArg = s3AccessKey.asRubyArgument(name: "s3_access_key", type: nil) - let s3SecretAccessKeyArg = s3SecretAccessKey.asRubyArgument(name: "s3_secret_access_key", type: nil) - let s3SessionTokenArg = s3SessionToken.asRubyArgument(name: "s3_session_token", type: nil) - let s3BucketArg = s3Bucket.asRubyArgument(name: "s3_bucket", type: nil) - let s3ObjectPrefixArg = s3ObjectPrefix.asRubyArgument(name: "s3_object_prefix", type: nil) - let s3SkipEncryptionArg = s3SkipEncryption.asRubyArgument(name: "s3_skip_encryption", type: nil) - let gitlabProjectArg = gitlabProject.asRubyArgument(name: "gitlab_project", type: nil) - let gitlabHostArg = RubyCommand.Argument(name: "gitlab_host", value: gitlabHost, type: nil) - let jobTokenArg = jobToken.asRubyArgument(name: "job_token", type: nil) - let privateTokenArg = privateToken.asRubyArgument(name: "private_token", type: nil) - let keychainNameArg = RubyCommand.Argument(name: "keychain_name", value: keychainName, type: nil) - let keychainPasswordArg = keychainPassword.asRubyArgument(name: "keychain_password", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let forceForNewDevicesArg = forceForNewDevices.asRubyArgument(name: "force_for_new_devices", type: nil) - let includeMacInProfilesArg = includeMacInProfiles.asRubyArgument(name: "include_mac_in_profiles", type: nil) - let includeAllCertificatesArg = includeAllCertificates.asRubyArgument(name: "include_all_certificates", type: nil) - let certificateIdArg = certificateId.asRubyArgument(name: "certificate_id", type: nil) - let forceForNewCertificatesArg = forceForNewCertificates.asRubyArgument(name: "force_for_new_certificates", type: nil) - let skipConfirmationArg = skipConfirmation.asRubyArgument(name: "skip_confirmation", type: nil) - let safeRemoveCertsArg = safeRemoveCerts.asRubyArgument(name: "safe_remove_certs", type: nil) - let skipDocsArg = skipDocs.asRubyArgument(name: "skip_docs", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let deriveCatalystAppIdentifierArg = deriveCatalystAppIdentifier.asRubyArgument(name: "derive_catalyst_app_identifier", type: nil) - let templateNameArg = templateName.asRubyArgument(name: "template_name", type: nil) - let profileNameArg = profileName.asRubyArgument(name: "profile_name", type: nil) - let failOnNameTakenArg = failOnNameTaken.asRubyArgument(name: "fail_on_name_taken", type: nil) - let skipCertificateMatchingArg = skipCertificateMatching.asRubyArgument(name: "skip_certificate_matching", type: nil) - let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) - let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) - let forceLegacyEncryptionArg = forceLegacyEncryption.asRubyArgument(name: "force_legacy_encryption", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let array: [RubyCommand.Argument?] = [typeArg, - additionalCertTypesArg, - readonlyArg, - generateAppleCertsArg, - skipProvisioningProfilesArg, - appIdentifierArg, - apiKeyPathArg, - apiKeyArg, - usernameArg, - teamIdArg, - teamNameArg, - storageModeArg, - gitUrlArg, - gitBranchArg, - gitFullNameArg, - gitUserEmailArg, - shallowCloneArg, - cloneBranchDirectlyArg, - gitBasicAuthorizationArg, - gitBearerAuthorizationArg, - gitPrivateKeyArg, - googleCloudBucketNameArg, - googleCloudKeysFileArg, - googleCloudProjectIdArg, - skipGoogleCloudAccountConfirmationArg, - s3RegionArg, - s3AccessKeyArg, - s3SecretAccessKeyArg, - s3SessionTokenArg, - s3BucketArg, - s3ObjectPrefixArg, - s3SkipEncryptionArg, - gitlabProjectArg, - gitlabHostArg, - jobTokenArg, - privateTokenArg, - keychainNameArg, - keychainPasswordArg, - forceArg, - forceForNewDevicesArg, - includeMacInProfilesArg, - includeAllCertificatesArg, - certificateIdArg, - forceForNewCertificatesArg, - skipConfirmationArg, - safeRemoveCertsArg, - skipDocsArg, - platformArg, - deriveCatalystAppIdentifierArg, - templateNameArg, - profileNameArg, - failOnNameTakenArg, - skipCertificateMatchingArg, - outputPathArg, - skipSetPartitionListArg, - forceLegacyEncryptionArg, - verboseArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "sync_code_signing", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Specify the Team ID you want to use for the Apple Developer Portal - */ -public func teamId() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "team_id", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Set a team to use by its name - */ -public func teamName() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "team_name", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload a new build to SauceLabs' TestFairy - - - parameters: - - apiKey: API Key for TestFairy - - ipa: Path to your IPA file for iOS - - apk: Path to your APK file for Android - - symbolsFile: Symbols mapping file - - uploadUrl: API URL for TestFairy - - testersGroups: Array of tester groups to be notified - - metrics: Array of metrics to record (cpu,memory,network,phone_signal,gps,battery,mic,wifi) - - comment: Additional release notes for this upload. This text will be added to email notifications - - autoUpdate: Allows an easy upgrade of all users to the current version. To enable set to 'on' - - notify: Send email to testers - - options: Array of options (shake,video_only_wifi,anonymous) - - custom: Array of custom options. Contact support for more information - - timeout: Request timeout in seconds - - tags: Custom tags that can be used to organize your builds - - folderName: Name of the dashboard folder that contains this app - - landingPageMode: Visibility of build landing after upload. Can be 'open' or 'closed' - - uploadToSaucelabs: Upload file directly to Sauce Labs. It can be 'on' or 'off' - - platform: Use if upload build is not iOS or Android. Contact support for more information - - Upload a new build to [TestFairy](https://saucelabs.com/products/mobile-testing/app-betas). - You can retrieve your API key on [your settings page](https://app.testfairy.com/settings/access-key) - - */ -public func testfairy(apiKey: String, - ipa: OptionalConfigValue = .fastlaneDefault(nil), - apk: OptionalConfigValue = .fastlaneDefault(nil), - symbolsFile: OptionalConfigValue = .fastlaneDefault(nil), - uploadUrl: String = "https://upload.testfairy.com", - testersGroups: [String] = [], - metrics: [String] = [], - comment: String = "No comment provided", - autoUpdate: String = "off", - notify: String = "off", - options: [String] = [], - custom: String = "", - timeout: OptionalConfigValue = .fastlaneDefault(nil), - tags: [String] = [], - folderName: String = "", - landingPageMode: String = "open", - uploadToSaucelabs: String = "off", - platform: String = "") -{ - let apiKeyArg = RubyCommand.Argument(name: "api_key", value: apiKey, type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let apkArg = apk.asRubyArgument(name: "apk", type: nil) - let symbolsFileArg = symbolsFile.asRubyArgument(name: "symbols_file", type: nil) - let uploadUrlArg = RubyCommand.Argument(name: "upload_url", value: uploadUrl, type: nil) - let testersGroupsArg = RubyCommand.Argument(name: "testers_groups", value: testersGroups, type: nil) - let metricsArg = RubyCommand.Argument(name: "metrics", value: metrics, type: nil) - let commentArg = RubyCommand.Argument(name: "comment", value: comment, type: nil) - let autoUpdateArg = RubyCommand.Argument(name: "auto_update", value: autoUpdate, type: nil) - let notifyArg = RubyCommand.Argument(name: "notify", value: notify, type: nil) - let optionsArg = RubyCommand.Argument(name: "options", value: options, type: nil) - let customArg = RubyCommand.Argument(name: "custom", value: custom, type: nil) - let timeoutArg = timeout.asRubyArgument(name: "timeout", type: nil) - let tagsArg = RubyCommand.Argument(name: "tags", value: tags, type: nil) - let folderNameArg = RubyCommand.Argument(name: "folder_name", value: folderName, type: nil) - let landingPageModeArg = RubyCommand.Argument(name: "landing_page_mode", value: landingPageMode, type: nil) - let uploadToSaucelabsArg = RubyCommand.Argument(name: "upload_to_saucelabs", value: uploadToSaucelabs, type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let array: [RubyCommand.Argument?] = [apiKeyArg, - ipaArg, - apkArg, - symbolsFileArg, - uploadUrlArg, - testersGroupsArg, - metricsArg, - commentArg, - autoUpdateArg, - notifyArg, - optionsArg, - customArg, - timeoutArg, - tagsArg, - folderNameArg, - landingPageModeArg, - uploadToSaucelabsArg, - platformArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "testfairy", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Alias for the `upload_to_testflight` action - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - appIdentifier: The bundle identifier of the app to upload or manage testers (optional) - - appPlatform: The platform to use (optional) - - appleId: Apple ID property in the App Information section in App Store Connect - - ipa: Path to the ipa file to upload - - pkg: Path to your pkg file - - demoAccountRequired: Do you need a demo account when Apple does review? - - betaAppReviewInfo: Beta app review information for contact info and demo account - - localizedAppInfo: Localized beta app test info for description, feedback email, marketing url, and privacy policy - - betaAppDescription: Provide the 'Beta App Description' when uploading a new build - - betaAppFeedbackEmail: Provide the beta app email when uploading a new build - - localizedBuildInfo: Localized beta app test info for what's new - - changelog: Provide the 'What to Test' text when uploading a new build - - skipSubmission: Skip the distributing action of pilot and only upload the ipa file - - skipWaitingForBuildProcessing: If set to true, the `distribute_external` option won't work and no build will be distributed to testers. (You might want to use this option if you are using this action on CI and have to pay for 'minutes used' on your CI plan). If set to `true` and a changelog is provided, it will partially wait for the build to appear on AppStore Connect so the changelog can be set, and skip the remaining processing steps - - updateBuildInfoOnUpload: **DEPRECATED!** Update build info immediately after validation. This is deprecated and will be removed in a future release. App Store Connect no longer supports setting build info until after build processing has completed, which is when build info is updated by default - - distributeOnly: Distribute a previously uploaded build (equivalent to the `fastlane pilot distribute` command) - - usesNonExemptEncryption: Provide the 'Uses Non-Exempt Encryption' for export compliance. This is used if there is 'ITSAppUsesNonExemptEncryption' is not set in the Info.plist - - distributeExternal: Should the build be distributed to external testers? If set to true, use of `groups` option is required - - notifyExternalTesters: Should notify external testers? (Not setting a value will use App Store Connect's default which is to notify) - - appVersion: The version number of the application build to distribute. If the version number is not specified, then the most recent build uploaded to TestFlight will be distributed. If specified, the most recent build for the version number will be distributed - - buildNumber: The build number of the application build to distribute. If the build number is not specified, the most recent build is distributed - - expirePreviousBuilds: Should expire previous builds? - - firstName: The tester's first name - - lastName: The tester's last name - - email: The tester's email - - testersFilePath: Path to a CSV file of testers - - groups: Associate tester to one group or more by group name / group id. E.g. `-g "Team 1","Team 2"` This is required when `distribute_external` option is set to true or when we want to add a tester to one or more external testing groups - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - devPortalTeamId: The short ID of your team in the developer portal, if you're in multiple teams. Different from your iTC team ID! - - itcProvider: The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column - - providerPublicId: The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication - - waitProcessingInterval: Interval in seconds to wait for App Store Connect processing - - waitProcessingTimeoutDuration: Timeout duration in seconds to wait for App Store Connect processing. If set, after exceeding timeout duration, this will `force stop` to wait for App Store Connect processing and exit with exception - - waitForUploadedBuild: **DEPRECATED!** No longer needed with the transition over to the App Store Connect API - Use version info from uploaded ipa file to determine what build to use for distribution. If set to false, latest processing or any latest build will be used - - rejectBuildWaitingForReview: Expire previous if it's 'waiting for review' - - submitBetaReview: Send the build for a beta review - - More details can be found on https://docs.fastlane.tools/actions/pilot/. - This integration will only do the TestFlight upload. - */ -public func testflight(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - appPlatform: OptionalConfigValue = .fastlaneDefault(nil), - appleId: OptionalConfigValue = .fastlaneDefault(nil), - ipa: OptionalConfigValue = .fastlaneDefault(nil), - pkg: OptionalConfigValue = .fastlaneDefault(nil), - demoAccountRequired: OptionalConfigValue = .fastlaneDefault(nil), - betaAppReviewInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - localizedAppInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - betaAppDescription: OptionalConfigValue = .fastlaneDefault(nil), - betaAppFeedbackEmail: OptionalConfigValue = .fastlaneDefault(nil), - localizedBuildInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - changelog: OptionalConfigValue = .fastlaneDefault(nil), - skipSubmission: OptionalConfigValue = .fastlaneDefault(false), - skipWaitingForBuildProcessing: OptionalConfigValue = .fastlaneDefault(false), - updateBuildInfoOnUpload: OptionalConfigValue = .fastlaneDefault(false), - distributeOnly: OptionalConfigValue = .fastlaneDefault(false), - usesNonExemptEncryption: OptionalConfigValue = .fastlaneDefault(false), - distributeExternal: OptionalConfigValue = .fastlaneDefault(false), - notifyExternalTesters: Any? = nil, - appVersion: OptionalConfigValue = .fastlaneDefault(nil), - buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - expirePreviousBuilds: OptionalConfigValue = .fastlaneDefault(false), - firstName: OptionalConfigValue = .fastlaneDefault(nil), - lastName: OptionalConfigValue = .fastlaneDefault(nil), - email: OptionalConfigValue = .fastlaneDefault(nil), - testersFilePath: String = "./testers.csv", - groups: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - teamId: Any? = nil, - teamName: OptionalConfigValue = .fastlaneDefault(nil), - devPortalTeamId: OptionalConfigValue = .fastlaneDefault(nil), - itcProvider: OptionalConfigValue = .fastlaneDefault(nil), - providerPublicId: OptionalConfigValue = .fastlaneDefault(nil), - waitProcessingInterval: Int = 30, - waitProcessingTimeoutDuration: OptionalConfigValue = .fastlaneDefault(nil), - waitForUploadedBuild: OptionalConfigValue = .fastlaneDefault(false), - rejectBuildWaitingForReview: OptionalConfigValue = .fastlaneDefault(false), - submitBetaReview: OptionalConfigValue = .fastlaneDefault(true)) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let appPlatformArg = appPlatform.asRubyArgument(name: "app_platform", type: nil) - let appleIdArg = appleId.asRubyArgument(name: "apple_id", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let pkgArg = pkg.asRubyArgument(name: "pkg", type: nil) - let demoAccountRequiredArg = demoAccountRequired.asRubyArgument(name: "demo_account_required", type: nil) - let betaAppReviewInfoArg = betaAppReviewInfo.asRubyArgument(name: "beta_app_review_info", type: nil) - let localizedAppInfoArg = localizedAppInfo.asRubyArgument(name: "localized_app_info", type: nil) - let betaAppDescriptionArg = betaAppDescription.asRubyArgument(name: "beta_app_description", type: nil) - let betaAppFeedbackEmailArg = betaAppFeedbackEmail.asRubyArgument(name: "beta_app_feedback_email", type: nil) - let localizedBuildInfoArg = localizedBuildInfo.asRubyArgument(name: "localized_build_info", type: nil) - let changelogArg = changelog.asRubyArgument(name: "changelog", type: nil) - let skipSubmissionArg = skipSubmission.asRubyArgument(name: "skip_submission", type: nil) - let skipWaitingForBuildProcessingArg = skipWaitingForBuildProcessing.asRubyArgument(name: "skip_waiting_for_build_processing", type: nil) - let updateBuildInfoOnUploadArg = updateBuildInfoOnUpload.asRubyArgument(name: "update_build_info_on_upload", type: nil) - let distributeOnlyArg = distributeOnly.asRubyArgument(name: "distribute_only", type: nil) - let usesNonExemptEncryptionArg = usesNonExemptEncryption.asRubyArgument(name: "uses_non_exempt_encryption", type: nil) - let distributeExternalArg = distributeExternal.asRubyArgument(name: "distribute_external", type: nil) - let notifyExternalTestersArg = RubyCommand.Argument(name: "notify_external_testers", value: notifyExternalTesters, type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let expirePreviousBuildsArg = expirePreviousBuilds.asRubyArgument(name: "expire_previous_builds", type: nil) - let firstNameArg = firstName.asRubyArgument(name: "first_name", type: nil) - let lastNameArg = lastName.asRubyArgument(name: "last_name", type: nil) - let emailArg = email.asRubyArgument(name: "email", type: nil) - let testersFilePathArg = RubyCommand.Argument(name: "testers_file_path", value: testersFilePath, type: nil) - let groupsArg = groups.asRubyArgument(name: "groups", type: nil) - let teamIdArg = RubyCommand.Argument(name: "team_id", value: teamId, type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let devPortalTeamIdArg = devPortalTeamId.asRubyArgument(name: "dev_portal_team_id", type: nil) - let itcProviderArg = itcProvider.asRubyArgument(name: "itc_provider", type: nil) - let providerPublicIdArg = providerPublicId.asRubyArgument(name: "provider_public_id", type: nil) - let waitProcessingIntervalArg = RubyCommand.Argument(name: "wait_processing_interval", value: waitProcessingInterval, type: nil) - let waitProcessingTimeoutDurationArg = waitProcessingTimeoutDuration.asRubyArgument(name: "wait_processing_timeout_duration", type: nil) - let waitForUploadedBuildArg = waitForUploadedBuild.asRubyArgument(name: "wait_for_uploaded_build", type: nil) - let rejectBuildWaitingForReviewArg = rejectBuildWaitingForReview.asRubyArgument(name: "reject_build_waiting_for_review", type: nil) - let submitBetaReviewArg = submitBetaReview.asRubyArgument(name: "submit_beta_review", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - usernameArg, - appIdentifierArg, - appPlatformArg, - appleIdArg, - ipaArg, - pkgArg, - demoAccountRequiredArg, - betaAppReviewInfoArg, - localizedAppInfoArg, - betaAppDescriptionArg, - betaAppFeedbackEmailArg, - localizedBuildInfoArg, - changelogArg, - skipSubmissionArg, - skipWaitingForBuildProcessingArg, - updateBuildInfoOnUploadArg, - distributeOnlyArg, - usesNonExemptEncryptionArg, - distributeExternalArg, - notifyExternalTestersArg, - appVersionArg, - buildNumberArg, - expirePreviousBuildsArg, - firstNameArg, - lastNameArg, - emailArg, - testersFilePathArg, - groupsArg, - teamIdArg, - teamNameArg, - devPortalTeamIdArg, - itcProviderArg, - providerPublicIdArg, - waitProcessingIntervalArg, - waitProcessingTimeoutDurationArg, - waitForUploadedBuildArg, - rejectBuildWaitingForReviewArg, - submitBetaReviewArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "testflight", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Convert the Xcode plist log to a JUnit report - - - parameters: - - path: Path to the directory that should be converted - - extension: The extension for the newly created file. Usually .xml or .junit - - outputDirectory: Directory in which the xml files should be written to. Same directory as source by default - - outputFilename: Filename the xml file should be written to. Defaults to name of input file. (Only works if one input file is used) - - failBuild: Should this step stop the build if the tests fail? Set this to false if you're handling this with a test reporter - - xcprettyNaming: Produces class name and test name identical to xcpretty naming in junit file - - forceLegacyXcresulttool: Force the use of the '--legacy' flag for xcresulttool instead of using the new commands - - silent: Silences all output - - outputRemoveRetryAttempts: Doesn't include retry attempts in the output - - - returns: A hash with the key being the path of the generated file, the value being if the tests were successful - */ -public func trainer(path: String = ".", - extension: String = ".xml", - outputDirectory: OptionalConfigValue = .fastlaneDefault(nil), - outputFilename: OptionalConfigValue = .fastlaneDefault(nil), - failBuild: OptionalConfigValue = .fastlaneDefault(true), - xcprettyNaming: OptionalConfigValue = .fastlaneDefault(false), - forceLegacyXcresulttool: OptionalConfigValue = .fastlaneDefault(false), - silent: OptionalConfigValue = .fastlaneDefault(false), - outputRemoveRetryAttempts: OptionalConfigValue = .fastlaneDefault(false)) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let extensionArg = RubyCommand.Argument(name: "extension", value: `extension`, type: nil) - let outputDirectoryArg = outputDirectory.asRubyArgument(name: "output_directory", type: nil) - let outputFilenameArg = outputFilename.asRubyArgument(name: "output_filename", type: nil) - let failBuildArg = failBuild.asRubyArgument(name: "fail_build", type: nil) - let xcprettyNamingArg = xcprettyNaming.asRubyArgument(name: "xcpretty_naming", type: nil) - let forceLegacyXcresulttoolArg = forceLegacyXcresulttool.asRubyArgument(name: "force_legacy_xcresulttool", type: nil) - let silentArg = silent.asRubyArgument(name: "silent", type: nil) - let outputRemoveRetryAttemptsArg = outputRemoveRetryAttempts.asRubyArgument(name: "output_remove_retry_attempts", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - extensionArg, - outputDirectoryArg, - outputFilenameArg, - failBuildArg, - xcprettyNamingArg, - forceLegacyXcresulttoolArg, - silentArg, - outputRemoveRetryAttemptsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "trainer", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload a new build to [Tryouts](https://tryouts.io/) - - - parameters: - - appId: Tryouts application hash - - apiToken: API Token (api_key:api_secret) for Tryouts Access - - buildFile: Path to your IPA or APK file. Optional if you use the _gym_ or _xcodebuild_ action - - notes: Release notes - - notesPath: Release notes text file path. Overrides the :notes parameter - - notify: Notify testers? 0 for no - - status: 2 to make your release public. Release will be distributed to available testers. 1 to make your release private. Release won't be distributed to testers. This also prevents release from showing up for SDK update - - More information: [http://tryouts.readthedocs.org/en/latest/releases.html#create-release](http://tryouts.readthedocs.org/en/latest/releases.html#create-release) - */ -public func tryouts(appId: String, - apiToken: String, - buildFile: String, - notes: OptionalConfigValue = .fastlaneDefault(nil), - notesPath: OptionalConfigValue = .fastlaneDefault(nil), - notify: Int = 1, - status: Int = 2) -{ - let appIdArg = RubyCommand.Argument(name: "app_id", value: appId, type: nil) - let apiTokenArg = RubyCommand.Argument(name: "api_token", value: apiToken, type: nil) - let buildFileArg = RubyCommand.Argument(name: "build_file", value: buildFile, type: nil) - let notesArg = notes.asRubyArgument(name: "notes", type: nil) - let notesPathArg = notesPath.asRubyArgument(name: "notes_path", type: nil) - let notifyArg = RubyCommand.Argument(name: "notify", value: notify, type: nil) - let statusArg = RubyCommand.Argument(name: "status", value: status, type: nil) - let array: [RubyCommand.Argument?] = [appIdArg, - apiTokenArg, - buildFileArg, - notesArg, - notesPathArg, - notifyArg, - statusArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "tryouts", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Post a tweet on [Twitter.com](https://twitter.com) - - - parameters: - - consumerKey: Consumer Key - - consumerSecret: Consumer Secret - - accessToken: Access Token - - accessTokenSecret: Access Token Secret - - message: The tweet - - Post a tweet on Twitter. Requires you to setup an app on [twitter.com](https://twitter.com) and obtain `consumer` and `access_token`. - */ -public func twitter(consumerKey: String, - consumerSecret: String, - accessToken: String, - accessTokenSecret: String, - message: String) -{ - let consumerKeyArg = RubyCommand.Argument(name: "consumer_key", value: consumerKey, type: nil) - let consumerSecretArg = RubyCommand.Argument(name: "consumer_secret", value: consumerSecret, type: nil) - let accessTokenArg = RubyCommand.Argument(name: "access_token", value: accessToken, type: nil) - let accessTokenSecretArg = RubyCommand.Argument(name: "access_token_secret", value: accessTokenSecret, type: nil) - let messageArg = RubyCommand.Argument(name: "message", value: message, type: nil) - let array: [RubyCommand.Argument?] = [consumerKeyArg, - consumerSecretArg, - accessTokenArg, - accessTokenSecretArg, - messageArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "twitter", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Post a message to [Typetalk](https://www.typetalk.com/) - */ -public func typetalk() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "typetalk", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Unlock a keychain - - - parameters: - - path: Path to the keychain file - - password: Keychain password - - addToSearchList: Add to keychain search list, valid values are true, false, :add, and :replace - - setDefault: Set as default keychain - - Unlocks the given keychain file and adds it to the keychain search list. - Keychains can be replaced with `add_to_search_list: :replace`. - */ -public func unlockKeychain(path: String = "login", - password: String, - addToSearchList: OptionalConfigValue = .fastlaneDefault(true), - setDefault: OptionalConfigValue = .fastlaneDefault(false)) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let passwordArg = RubyCommand.Argument(name: "password", value: password, type: nil) - let addToSearchListArg = addToSearchList.asRubyArgument(name: "add_to_search_list", type: nil) - let setDefaultArg = setDefault.asRubyArgument(name: "set_default", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - passwordArg, - addToSearchListArg, - setDefaultArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "unlock_keychain", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This action changes the app group identifiers in the entitlements file - - - parameters: - - entitlementsFile: The path to the entitlement file which contains the app group identifiers - - appGroupIdentifiers: An Array of unique identifiers for the app groups. Eg. ['group.com.test.testapp'] - - Updates the App Group Identifiers in the given Entitlements file, so you can have app groups for the app store build and app groups for an enterprise build. - */ -public func updateAppGroupIdentifiers(entitlementsFile: String, - appGroupIdentifiers: [String]) -{ - let entitlementsFileArg = RubyCommand.Argument(name: "entitlements_file", value: entitlementsFile, type: nil) - let appGroupIdentifiersArg = RubyCommand.Argument(name: "app_group_identifiers", value: appGroupIdentifiers, type: nil) - let array: [RubyCommand.Argument?] = [entitlementsFileArg, - appGroupIdentifiersArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_app_group_identifiers", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Update the project's bundle identifier - - - parameters: - - xcodeproj: Path to your Xcode project - - plistPath: Path to info plist, relative to your Xcode project - - appIdentifier: The app Identifier you want to set - - Update an app identifier by either setting `CFBundleIdentifier` or `PRODUCT_BUNDLE_IDENTIFIER`, depending on which is already in use. - */ -public func updateAppIdentifier(xcodeproj: String, - plistPath: String, - appIdentifier: String) -{ - let xcodeprojArg = RubyCommand.Argument(name: "xcodeproj", value: xcodeproj, type: nil) - let plistPathArg = RubyCommand.Argument(name: "plist_path", value: plistPath, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let array: [RubyCommand.Argument?] = [xcodeprojArg, - plistPathArg, - appIdentifierArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_app_identifier", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Configures Xcode's Codesigning options - - - parameters: - - path: Path to your Xcode project - - useAutomaticSigning: Defines if project should use automatic signing - - sdk: Build target SDKs (iphoneos*, macosx*, iphonesimulator*) - - teamId: Team ID, is used when upgrading project - - targets: Specify targets you want to toggle the signing mech. (default to all targets) - - buildConfigurations: Specify build_configurations you want to toggle the signing mech. (default to all configurations) - - codeSignIdentity: Code signing identity type (iPhone Developer, iPhone Distribution) - - entitlementsFilePath: Path to your entitlements file - - profileName: Provisioning profile name to use for code signing - - profileUuid: Provisioning profile UUID to use for code signing - - bundleIdentifier: Application Product Bundle Identifier - - - returns: The current status (boolean) of codesigning after modification - - Configures Xcode's Codesigning options of all targets in the project - */ -public func updateCodeSigningSettings(path: String, - useAutomaticSigning: OptionalConfigValue = .fastlaneDefault(false), - sdk: OptionalConfigValue = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - targets: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - buildConfigurations: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - codeSignIdentity: OptionalConfigValue = .fastlaneDefault(nil), - entitlementsFilePath: OptionalConfigValue = .fastlaneDefault(nil), - profileName: OptionalConfigValue = .fastlaneDefault(nil), - profileUuid: OptionalConfigValue = .fastlaneDefault(nil), - bundleIdentifier: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let useAutomaticSigningArg = useAutomaticSigning.asRubyArgument(name: "use_automatic_signing", type: nil) - let sdkArg = sdk.asRubyArgument(name: "sdk", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let targetsArg = targets.asRubyArgument(name: "targets", type: nil) - let buildConfigurationsArg = buildConfigurations.asRubyArgument(name: "build_configurations", type: nil) - let codeSignIdentityArg = codeSignIdentity.asRubyArgument(name: "code_sign_identity", type: nil) - let entitlementsFilePathArg = entitlementsFilePath.asRubyArgument(name: "entitlements_file_path", type: nil) - let profileNameArg = profileName.asRubyArgument(name: "profile_name", type: nil) - let profileUuidArg = profileUuid.asRubyArgument(name: "profile_uuid", type: nil) - let bundleIdentifierArg = bundleIdentifier.asRubyArgument(name: "bundle_identifier", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - useAutomaticSigningArg, - sdkArg, - teamIdArg, - targetsArg, - buildConfigurationsArg, - codeSignIdentityArg, - entitlementsFilePathArg, - profileNameArg, - profileUuidArg, - bundleIdentifierArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_code_signing_settings", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Makes sure fastlane-tools are up-to-date when running fastlane - - - parameters: - - noUpdate: Don't update during this run. This is used internally - - nightly: **DEPRECATED!** Nightly builds are no longer being made available - Opt-in to install and use nightly fastlane builds - - This action will update fastlane to the most recent version - major version updates will not be performed automatically, as they might include breaking changes. If an update was performed, fastlane will be restarted before the run continues. - - If you are using rbenv or rvm, everything should be good to go. However, if you are using the system's default ruby, some additional setup is needed for this action to work correctly. In short, fastlane needs to be able to access your gem library without running in `sudo` mode. - - The simplest possible fix for this is putting the following lines into your `~/.bashrc` or `~/.zshrc` file:| - | - ```bash| - export GEM_HOME=~/.gems| - export PATH=$PATH:~/.gems/bin| - ```| - >| - After the above changes, restart your terminal, then run `mkdir $GEM_HOME` to create the new gem directory. After this, you're good to go! - - Recommended usage of the `update_fastlane` action is at the top inside of the `before_all` block, before running any other action. - */ -public func updateFastlane(noUpdate: OptionalConfigValue = .fastlaneDefault(false), - nightly: OptionalConfigValue = .fastlaneDefault(false)) -{ - let noUpdateArg = noUpdate.asRubyArgument(name: "no_update", type: nil) - let nightlyArg = nightly.asRubyArgument(name: "nightly", type: nil) - let array: [RubyCommand.Argument?] = [noUpdateArg, - nightlyArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_fastlane", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This action changes the iCloud container identifiers in the entitlements file - - - parameters: - - entitlementsFile: The path to the entitlement file which contains the iCloud container identifiers - - icloudContainerIdentifiers: An Array of unique identifiers for the iCloud containers. Eg. ['iCloud.com.test.testapp'] - - Updates the iCloud Container Identifiers in the given Entitlements file, so you can use different iCloud containers for different builds like Adhoc, App Store, etc. - */ -public func updateIcloudContainerIdentifiers(entitlementsFile: String, - icloudContainerIdentifiers: [String]) -{ - let entitlementsFileArg = RubyCommand.Argument(name: "entitlements_file", value: entitlementsFile, type: nil) - let icloudContainerIdentifiersArg = RubyCommand.Argument(name: "icloud_container_identifiers", value: icloudContainerIdentifiers, type: nil) - let array: [RubyCommand.Argument?] = [entitlementsFileArg, - icloudContainerIdentifiersArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_icloud_container_identifiers", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Update a Info.plist file with bundle identifier and display name - - - parameters: - - xcodeproj: Path to your Xcode project - - plistPath: Path to info plist - - scheme: Scheme of info plist - - appIdentifier: The App Identifier of your app - - displayName: The Display Name of your app - - block: A block to process plist with custom logic - - This action allows you to modify your `Info.plist` file before building. This may be useful if you want a separate build for alpha, beta or nightly builds, but don't want a separate target. - */ -public func updateInfoPlist(xcodeproj: OptionalConfigValue = .fastlaneDefault(nil), - plistPath: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - displayName: OptionalConfigValue = .fastlaneDefault(nil), - block: ((String) -> Void)? = nil) -{ - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let plistPathArg = plistPath.asRubyArgument(name: "plist_path", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let displayNameArg = displayName.asRubyArgument(name: "display_name", type: nil) - let blockArg = RubyCommand.Argument(name: "block", value: block, type: .stringClosure) - let array: [RubyCommand.Argument?] = [xcodeprojArg, - plistPathArg, - schemeArg, - appIdentifierArg, - displayNameArg, - blockArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_info_plist", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - This action changes the keychain access groups in the entitlements file - - - parameters: - - entitlementsFile: The path to the entitlement file which contains the keychain access groups - - identifiers: An Array of unique identifiers for the keychain access groups. Eg. ['your.keychain.access.groups.identifiers'] - - Updates the Keychain Group Access Groups in the given Entitlements file, so you can have keychain access groups for the app store build and keychain access groups for an enterprise build. - */ -public func updateKeychainAccessGroups(entitlementsFile: String, - identifiers: [String]) -{ - let entitlementsFileArg = RubyCommand.Argument(name: "entitlements_file", value: entitlementsFile, type: nil) - let identifiersArg = RubyCommand.Argument(name: "identifiers", value: identifiers, type: nil) - let array: [RubyCommand.Argument?] = [entitlementsFileArg, - identifiersArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_keychain_access_groups", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Update a plist file - - - parameters: - - plistPath: Path to plist file - - block: A block to process plist with custom logic - - This action allows you to modify any value inside any `plist` file. - */ -public func updatePlist(plistPath: OptionalConfigValue = .fastlaneDefault(nil), - block: ((String) -> Void)? = nil) -{ - let plistPathArg = plistPath.asRubyArgument(name: "plist_path", type: nil) - let blockArg = RubyCommand.Argument(name: "block", value: block, type: .stringClosure) - let array: [RubyCommand.Argument?] = [plistPathArg, - blockArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_plist", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Updated code signing settings from 'Automatic' to a specific profile - - - parameters: - - path: Path to your Xcode project - - udid: **DEPRECATED!** Use `:uuid` instead - - uuid: The UUID of the provisioning profile you want to use - */ -public func updateProjectCodeSigning(path: String, - udid: OptionalConfigValue = .fastlaneDefault(nil), - uuid: String) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let udidArg = udid.asRubyArgument(name: "udid", type: nil) - let uuidArg = RubyCommand.Argument(name: "uuid", value: uuid, type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - udidArg, - uuidArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_project_code_signing", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Update projects code signing settings from your provisioning profile - - - parameters: - - xcodeproj: Path to your Xcode project - - profile: Path to provisioning profile (.mobileprovision) - - targetFilter: A filter for the target name. Use a standard regex - - buildConfigurationFilter: Legacy option, use 'target_filter' instead - - buildConfiguration: A filter for the build configuration name. Use a standard regex. Applied to all configurations if not specified - - certificate: Path to apple root certificate - - codeSigningIdentity: Code sign identity for build configuration - - You should check out the [code signing guide](https://docs.fastlane.tools/codesigning/getting-started/) before using this action. - This action retrieves a provisioning profile UUID from a provisioning profile (`.mobileprovision`) to set up the Xcode projects' code signing settings in `*.xcodeproj/project.pbxproj`. - The `:target_filter` value can be used to only update code signing for the specified targets. - The `:build_configuration` value can be used to only update code signing for the specified build configurations of the targets passing through the `:target_filter`. - Example usage is the WatchKit Extension or WatchKit App, where you need separate provisioning profiles. - Example: `update_project_provisioning(xcodeproj: "..", target_filter: ".*WatchKit App.*")`. - */ -public func updateProjectProvisioning(xcodeproj: OptionalConfigValue = .fastlaneDefault(nil), - profile: String, - targetFilter: OptionalConfigValue = .fastlaneDefault(nil), - buildConfigurationFilter: OptionalConfigValue = .fastlaneDefault(nil), - buildConfiguration: OptionalConfigValue = .fastlaneDefault(nil), - certificate: String = "/tmp/AppleIncRootCertificate.cer", - codeSigningIdentity: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil) - let profileArg = RubyCommand.Argument(name: "profile", value: profile, type: nil) - let targetFilterArg = targetFilter.asRubyArgument(name: "target_filter", type: nil) - let buildConfigurationFilterArg = buildConfigurationFilter.asRubyArgument(name: "build_configuration_filter", type: nil) - let buildConfigurationArg = buildConfiguration.asRubyArgument(name: "build_configuration", type: nil) - let certificateArg = RubyCommand.Argument(name: "certificate", value: certificate, type: nil) - let codeSigningIdentityArg = codeSigningIdentity.asRubyArgument(name: "code_signing_identity", type: nil) - let array: [RubyCommand.Argument?] = [xcodeprojArg, - profileArg, - targetFilterArg, - buildConfigurationFilterArg, - buildConfigurationArg, - certificateArg, - codeSigningIdentityArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_project_provisioning", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Update Xcode Development Team ID - - - parameters: - - path: Path to your Xcode project - - targets: Name of the targets you want to update - - teamid: The Team ID you want to use - - This action updates the Developer Team ID of your Xcode project. - */ -public func updateProjectTeam(path: String, - targets: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - teamid: String) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let targetsArg = targets.asRubyArgument(name: "targets", type: nil) - let teamidArg = RubyCommand.Argument(name: "teamid", value: teamid, type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - targetsArg, - teamidArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_project_team", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Set [Urban Airship](https://www.urbanairship.com/) plist configuration values - - - parameters: - - plistPath: Path to Urban Airship configuration Plist - - developmentAppKey: The development app key - - developmentAppSecret: The development app secret - - productionAppKey: The production app key - - productionAppSecret: The production app secret - - detectProvisioningMode: Automatically detect provisioning mode - - This action updates the `AirshipConfig.plist` needed to configure the Urban Airship SDK at runtime, allowing keys and secrets to easily be set for the Enterprise and Production versions of the application. - */ -public func updateUrbanAirshipConfiguration(plistPath: String, - developmentAppKey: OptionalConfigValue = .fastlaneDefault(nil), - developmentAppSecret: OptionalConfigValue = .fastlaneDefault(nil), - productionAppKey: OptionalConfigValue = .fastlaneDefault(nil), - productionAppSecret: OptionalConfigValue = .fastlaneDefault(nil), - detectProvisioningMode: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let plistPathArg = RubyCommand.Argument(name: "plist_path", value: plistPath, type: nil) - let developmentAppKeyArg = developmentAppKey.asRubyArgument(name: "development_app_key", type: nil) - let developmentAppSecretArg = developmentAppSecret.asRubyArgument(name: "development_app_secret", type: nil) - let productionAppKeyArg = productionAppKey.asRubyArgument(name: "production_app_key", type: nil) - let productionAppSecretArg = productionAppSecret.asRubyArgument(name: "production_app_secret", type: nil) - let detectProvisioningModeArg = detectProvisioningMode.asRubyArgument(name: "detect_provisioning_mode", type: nil) - let array: [RubyCommand.Argument?] = [plistPathArg, - developmentAppKeyArg, - developmentAppSecretArg, - productionAppKeyArg, - productionAppSecretArg, - detectProvisioningModeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_urban_airship_configuration", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Updates the URL schemes in the given Info.plist - - - parameters: - - path: The Plist file's path - - urlSchemes: The new URL schemes - - updateUrlSchemes: Block that is called to update schemes with current schemes passed in as parameter - - This action allows you to update the URL schemes of the app before building it. - For example, you can use this to set a different URL scheme for the alpha or beta version of the app. - */ -public func updateUrlSchemes(path: String, - urlSchemes: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - updateUrlSchemes: ((String) -> Void)? = nil) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let urlSchemesArg = urlSchemes.asRubyArgument(name: "url_schemes", type: nil) - let updateUrlSchemesArg = RubyCommand.Argument(name: "update_url_schemes", value: updateUrlSchemes, type: .stringClosure) - let array: [RubyCommand.Argument?] = [pathArg, - urlSchemesArg, - updateUrlSchemesArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "update_url_schemes", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload App Privacy Details for an app in App Store Connect - - - parameters: - - username: Your Apple ID Username for App Store Connect - - appIdentifier: The bundle identifier of your app - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - jsonPath: Path to the app usage data JSON - - outputJsonPath: Path to the app usage data JSON file generated by interactive questions - - skipJsonFileSaving: Whether to skip the saving of the JSON file - - skipUpload: Whether to skip the upload and only create the JSON file with interactive questions - - skipPublish: Whether to skip the publishing - - Upload App Privacy Details for an app in App Store Connect. For more detail information, view https://docs.fastlane.tools/uploading-app-privacy-details - */ -public func uploadAppPrivacyDetailsToAppStore(username: String, - appIdentifier: String, - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - jsonPath: OptionalConfigValue = .fastlaneDefault(nil), - outputJsonPath: String = "./fastlane/app_privacy_details.json", - skipJsonFileSaving: OptionalConfigValue = .fastlaneDefault(false), - skipUpload: OptionalConfigValue = .fastlaneDefault(false), - skipPublish: OptionalConfigValue = .fastlaneDefault(false)) -{ - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let appIdentifierArg = RubyCommand.Argument(name: "app_identifier", value: appIdentifier, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let jsonPathArg = jsonPath.asRubyArgument(name: "json_path", type: nil) - let outputJsonPathArg = RubyCommand.Argument(name: "output_json_path", value: outputJsonPath, type: nil) - let skipJsonFileSavingArg = skipJsonFileSaving.asRubyArgument(name: "skip_json_file_saving", type: nil) - let skipUploadArg = skipUpload.asRubyArgument(name: "skip_upload", type: nil) - let skipPublishArg = skipPublish.asRubyArgument(name: "skip_publish", type: nil) - let array: [RubyCommand.Argument?] = [usernameArg, - appIdentifierArg, - teamIdArg, - teamNameArg, - jsonPathArg, - outputJsonPathArg, - skipJsonFileSavingArg, - skipUploadArg, - skipPublishArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "upload_app_privacy_details_to_app_store", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload dSYM symbolication files to Crashlytics - - - parameters: - - dsymPath: Path to the DSYM file or zip to upload - - dsymPaths: Paths to the DSYM files or zips to upload - - apiToken: Crashlytics API Key - - gspPath: Path to GoogleService-Info.plist - - appId: Firebase Crashlytics APP ID - - binaryPath: The path to the upload-symbols file of the Fabric app - - platform: The platform of the app (ios, appletvos, mac) - - dsymWorkerThreads: The number of threads to use for simultaneous dSYM upload - - debug: Enable debug mode for upload-symbols - - This action allows you to upload symbolication files to Crashlytics. It's extra useful if you use it to download the latest dSYM files from Apple when you use Bitcode. This action will not fail the build if one of the uploads failed. The reason for that is that sometimes some of dSYM files are invalid, and we don't want them to fail the complete build. - */ -public func uploadSymbolsToCrashlytics(dsymPath: String = "./spec/fixtures/dSYM/Themoji2.dSYM", - dsymPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - apiToken: OptionalConfigValue = .fastlaneDefault(nil), - gspPath: OptionalConfigValue = .fastlaneDefault(nil), - appId: OptionalConfigValue = .fastlaneDefault(nil), - binaryPath: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - dsymWorkerThreads: Int = 1, - debug: OptionalConfigValue = .fastlaneDefault(false)) -{ - let dsymPathArg = RubyCommand.Argument(name: "dsym_path", value: dsymPath, type: nil) - let dsymPathsArg = dsymPaths.asRubyArgument(name: "dsym_paths", type: nil) - let apiTokenArg = apiToken.asRubyArgument(name: "api_token", type: nil) - let gspPathArg = gspPath.asRubyArgument(name: "gsp_path", type: nil) - let appIdArg = appId.asRubyArgument(name: "app_id", type: nil) - let binaryPathArg = binaryPath.asRubyArgument(name: "binary_path", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let dsymWorkerThreadsArg = RubyCommand.Argument(name: "dsym_worker_threads", value: dsymWorkerThreads, type: nil) - let debugArg = debug.asRubyArgument(name: "debug", type: nil) - let array: [RubyCommand.Argument?] = [dsymPathArg, - dsymPathsArg, - apiTokenArg, - gspPathArg, - appIdArg, - binaryPathArg, - platformArg, - dsymWorkerThreadsArg, - debugArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "upload_symbols_to_crashlytics", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload dSYM symbolication files to Sentry - - - parameters: - - apiHost: API host url for Sentry - - apiKey: API key for Sentry - - authToken: Authentication token for Sentry - - orgSlug: Organization slug for Sentry project - - projectSlug: Project slug for Sentry - - dsymPath: Path to your symbols file. For iOS and Mac provide path to app.dSYM.zip - - dsymPaths: Path to an array of your symbols file. For iOS and Mac provide path to app.dSYM.zip - - - returns: The uploaded dSYM path(s) - - This action allows you to upload symbolication files to Sentry. It's extra useful if you use it to download the latest dSYM files from Apple when you use Bitcode. - */ -public func uploadSymbolsToSentry(apiHost: String = "https://app.getsentry.com/api/0", - apiKey: OptionalConfigValue = .fastlaneDefault(nil), - authToken: OptionalConfigValue = .fastlaneDefault(nil), - orgSlug: String, - projectSlug: String, - dsymPath: OptionalConfigValue = .fastlaneDefault(nil), - dsymPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil)) -{ - let apiHostArg = RubyCommand.Argument(name: "api_host", value: apiHost, type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let authTokenArg = authToken.asRubyArgument(name: "auth_token", type: nil) - let orgSlugArg = RubyCommand.Argument(name: "org_slug", value: orgSlug, type: nil) - let projectSlugArg = RubyCommand.Argument(name: "project_slug", value: projectSlug, type: nil) - let dsymPathArg = dsymPath.asRubyArgument(name: "dsym_path", type: nil) - let dsymPathsArg = dsymPaths.asRubyArgument(name: "dsym_paths", type: nil) - let array: [RubyCommand.Argument?] = [apiHostArg, - apiKeyArg, - authTokenArg, - orgSlugArg, - projectSlugArg, - dsymPathArg, - dsymPathsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "upload_symbols_to_sentry", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload metadata and binary to App Store Connect (via _deliver_) - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - appIdentifier: The bundle identifier of your app - - appVersion: The version that should be edited or created - - ipa: Path to your ipa file - - pkg: Path to your pkg file - - buildNumber: If set the given build number (already uploaded to iTC) will be used instead of the current built one - - platform: The platform to use (optional) - - editLive: Modify live metadata, this option disables ipa upload and screenshot upload - - useLiveVersion: Force usage of live version rather than edit version - - metadataPath: Path to the folder containing the metadata files - - screenshotsPath: Path to the folder containing the screenshots - - appPreviewsPath: Path to the folder containing localized App Preview videos - - previewFrameTimeCode: Time code for the App Preview still frame written as hour:minute:second:centisecond (e.g. 00:00:00:01) - - overwritePreviewVideos: Clear all previously uploaded App Preview videos before uploading the new ones - - skipBinaryUpload: Skip uploading an ipa or pkg to App Store Connect - - skipScreenshots: Don't upload the screenshots - - skipMetadata: Don't upload the metadata (e.g. title, description). This will still upload screenshots - - skipAppVersionUpdate: Don’t create or update the app version that is being prepared for submission - - force: Skip verification of HTML preview file - - overwriteScreenshots: Clear all previously uploaded screenshots before uploading the new ones - - screenshotProcessingTimeout: Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing - - syncScreenshots: Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well - - submitForReview: Submit the new version for Review after uploading everything - - verifyOnly: Verifies archive with App Store Connect without uploading - - rejectIfPossible: Rejects the previously submitted build if it's in a state where it's possible - - versionCheckWaitRetryLimit: After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow - - automaticRelease: Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) - - autoReleaseDate: Date in milliseconds for automatically releasing on pending approval (Cannot be used together with `automatic_release`) - - phasedRelease: Enable the phased release feature of iTC - - resetRatings: Reset the summary rating when you release a new version of the application - - priceTier: The price tier of this application - - appRatingConfigPath: Path to the app rating's config - - submissionInformation: Extra information for the submission (e.g. compliance specifications) - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - devPortalTeamId: The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! - - devPortalTeamName: The name of your Developer Portal team if you're in multiple teams - - itcProvider: The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column - - providerPublicId: The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication - - runPrecheckBeforeSubmit: Run precheck before submitting to app review - - precheckDefaultRuleLevel: The default precheck rule level unless otherwise configured - - individualMetadataItems: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - An array of localized metadata items to upload individually by language so that errors can be identified. E.g. ['name', 'keywords', 'description']. Note: slow - - appIcon: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the app icon - - appleWatchAppIcon: **DEPRECATED!** Removed after the migration to the new App Store Connect API in June 2020 - Metadata: The path to the Apple Watch app icon - - copyright: Metadata: The copyright notice - - primaryCategory: Metadata: The english name of the primary category (e.g. `Business`, `Books`) - - secondaryCategory: Metadata: The english name of the secondary category (e.g. `Business`, `Books`) - - primaryFirstSubCategory: Metadata: The english name of the primary first sub category (e.g. `Educational`, `Puzzle`) - - primarySecondSubCategory: Metadata: The english name of the primary second sub category (e.g. `Educational`, `Puzzle`) - - secondaryFirstSubCategory: Metadata: The english name of the secondary first sub category (e.g. `Educational`, `Puzzle`) - - secondarySecondSubCategory: Metadata: The english name of the secondary second sub category (e.g. `Educational`, `Puzzle`) - - tradeRepresentativeContactInformation: **DEPRECATED!** This is no longer used by App Store Connect - Metadata: A hash containing the trade representative contact information - - appReviewInformation: Metadata: A hash containing the review information - - appReviewAttachmentFile: Metadata: Path to the app review attachment file - - description: Metadata: The localised app description - - name: Metadata: The localised app name - - subtitle: Metadata: The localised app subtitle - - keywords: Metadata: An array of localised keywords - - promotionalText: Metadata: An array of localised promotional texts - - releaseNotes: Metadata: Localised release notes for this version - - privacyUrl: Metadata: Localised privacy url - - appleTvPrivacyPolicy: Metadata: Localised Apple TV privacy policy text - - supportUrl: Metadata: Localised support url - - marketingUrl: Metadata: Localised marketing url - - languages: Metadata: List of languages to activate - - ignoreLanguageDirectoryValidation: Ignore errors when invalid languages are found in metadata and screenshot directories - - precheckIncludeInAppPurchases: Should precheck check in-app purchases? - - app: The (spaceship) app ID of the app you want to use/modify - - Using _upload_to_app_store_ after _build_app_ and _capture_screenshots_ will automatically upload the latest ipa and screenshots with no other configuration. - - If you don't want to verify an HTML preview for App Store builds, use the `:force` option. - This is useful when running _fastlane_ on your Continuous Integration server: - `_upload_to_app_store_(force: true)` - If your account is on multiple teams and you need to tell the transporter which provider to use, you can set `:itc_provider` or `:provider_public_id`. - */ -public func uploadToAppStore(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - appVersion: OptionalConfigValue = .fastlaneDefault(nil), - ipa: OptionalConfigValue = .fastlaneDefault(nil), - pkg: OptionalConfigValue = .fastlaneDefault(nil), - buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - platform: String = "ios", - editLive: OptionalConfigValue = .fastlaneDefault(false), - useLiveVersion: OptionalConfigValue = .fastlaneDefault(false), - metadataPath: OptionalConfigValue = .fastlaneDefault(nil), - screenshotsPath: OptionalConfigValue = .fastlaneDefault(nil), - appPreviewsPath: OptionalConfigValue = .fastlaneDefault(nil), - previewFrameTimeCode: String = "00:00:05:00", - overwritePreviewVideos: OptionalConfigValue = .fastlaneDefault(false), - skipBinaryUpload: OptionalConfigValue = .fastlaneDefault(false), - skipScreenshots: OptionalConfigValue = .fastlaneDefault(false), - skipMetadata: OptionalConfigValue = .fastlaneDefault(false), - skipAppVersionUpdate: OptionalConfigValue = .fastlaneDefault(false), - force: OptionalConfigValue = .fastlaneDefault(false), - overwriteScreenshots: OptionalConfigValue = .fastlaneDefault(false), - screenshotProcessingTimeout: Int = 3600, - syncScreenshots: OptionalConfigValue = .fastlaneDefault(false), - submitForReview: OptionalConfigValue = .fastlaneDefault(false), - verifyOnly: OptionalConfigValue = .fastlaneDefault(false), - rejectIfPossible: OptionalConfigValue = .fastlaneDefault(false), - versionCheckWaitRetryLimit: Int = 7, - automaticRelease: OptionalConfigValue = .fastlaneDefault(nil), - autoReleaseDate: OptionalConfigValue = .fastlaneDefault(nil), - phasedRelease: OptionalConfigValue = .fastlaneDefault(false), - resetRatings: OptionalConfigValue = .fastlaneDefault(false), - priceTier: OptionalConfigValue = .fastlaneDefault(nil), - appRatingConfigPath: OptionalConfigValue = .fastlaneDefault(nil), - submissionInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - teamId: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - devPortalTeamId: OptionalConfigValue = .fastlaneDefault(nil), - devPortalTeamName: OptionalConfigValue = .fastlaneDefault(nil), - itcProvider: OptionalConfigValue = .fastlaneDefault(nil), - providerPublicId: OptionalConfigValue = .fastlaneDefault(nil), - runPrecheckBeforeSubmit: OptionalConfigValue = .fastlaneDefault(true), - precheckDefaultRuleLevel: String = "warn", - individualMetadataItems: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - appIcon: OptionalConfigValue = .fastlaneDefault(nil), - appleWatchAppIcon: OptionalConfigValue = .fastlaneDefault(nil), - copyright: OptionalConfigValue = .fastlaneDefault(nil), - primaryCategory: OptionalConfigValue = .fastlaneDefault(nil), - secondaryCategory: OptionalConfigValue = .fastlaneDefault(nil), - primaryFirstSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - primarySecondSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - secondaryFirstSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - secondarySecondSubCategory: OptionalConfigValue = .fastlaneDefault(nil), - tradeRepresentativeContactInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appReviewInformation: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appReviewAttachmentFile: OptionalConfigValue = .fastlaneDefault(nil), - description: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - name: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - subtitle: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - keywords: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - promotionalText: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - releaseNotes: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - privacyUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - appleTvPrivacyPolicy: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - supportUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - marketingUrl: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - languages: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - ignoreLanguageDirectoryValidation: OptionalConfigValue = .fastlaneDefault(false), - precheckIncludeInAppPurchases: OptionalConfigValue = .fastlaneDefault(true), - app: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let pkgArg = pkg.asRubyArgument(name: "pkg", type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) - let editLiveArg = editLive.asRubyArgument(name: "edit_live", type: nil) - let useLiveVersionArg = useLiveVersion.asRubyArgument(name: "use_live_version", type: nil) - let metadataPathArg = metadataPath.asRubyArgument(name: "metadata_path", type: nil) - let screenshotsPathArg = screenshotsPath.asRubyArgument(name: "screenshots_path", type: nil) - let appPreviewsPathArg = appPreviewsPath.asRubyArgument(name: "app_previews_path", type: nil) - let previewFrameTimeCodeArg = RubyCommand.Argument(name: "preview_frame_time_code", value: previewFrameTimeCode, type: nil) - let overwritePreviewVideosArg = overwritePreviewVideos.asRubyArgument(name: "overwrite_preview_videos", type: nil) - let skipBinaryUploadArg = skipBinaryUpload.asRubyArgument(name: "skip_binary_upload", type: nil) - let skipScreenshotsArg = skipScreenshots.asRubyArgument(name: "skip_screenshots", type: nil) - let skipMetadataArg = skipMetadata.asRubyArgument(name: "skip_metadata", type: nil) - let skipAppVersionUpdateArg = skipAppVersionUpdate.asRubyArgument(name: "skip_app_version_update", type: nil) - let forceArg = force.asRubyArgument(name: "force", type: nil) - let overwriteScreenshotsArg = overwriteScreenshots.asRubyArgument(name: "overwrite_screenshots", type: nil) - let screenshotProcessingTimeoutArg = RubyCommand.Argument(name: "screenshot_processing_timeout", value: screenshotProcessingTimeout, type: nil) - let syncScreenshotsArg = syncScreenshots.asRubyArgument(name: "sync_screenshots", type: nil) - let submitForReviewArg = submitForReview.asRubyArgument(name: "submit_for_review", type: nil) - let verifyOnlyArg = verifyOnly.asRubyArgument(name: "verify_only", type: nil) - let rejectIfPossibleArg = rejectIfPossible.asRubyArgument(name: "reject_if_possible", type: nil) - let versionCheckWaitRetryLimitArg = RubyCommand.Argument(name: "version_check_wait_retry_limit", value: versionCheckWaitRetryLimit, type: nil) - let automaticReleaseArg = automaticRelease.asRubyArgument(name: "automatic_release", type: nil) - let autoReleaseDateArg = autoReleaseDate.asRubyArgument(name: "auto_release_date", type: nil) - let phasedReleaseArg = phasedRelease.asRubyArgument(name: "phased_release", type: nil) - let resetRatingsArg = resetRatings.asRubyArgument(name: "reset_ratings", type: nil) - let priceTierArg = priceTier.asRubyArgument(name: "price_tier", type: nil) - let appRatingConfigPathArg = appRatingConfigPath.asRubyArgument(name: "app_rating_config_path", type: nil) - let submissionInformationArg = submissionInformation.asRubyArgument(name: "submission_information", type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let devPortalTeamIdArg = devPortalTeamId.asRubyArgument(name: "dev_portal_team_id", type: nil) - let devPortalTeamNameArg = devPortalTeamName.asRubyArgument(name: "dev_portal_team_name", type: nil) - let itcProviderArg = itcProvider.asRubyArgument(name: "itc_provider", type: nil) - let providerPublicIdArg = providerPublicId.asRubyArgument(name: "provider_public_id", type: nil) - let runPrecheckBeforeSubmitArg = runPrecheckBeforeSubmit.asRubyArgument(name: "run_precheck_before_submit", type: nil) - let precheckDefaultRuleLevelArg = RubyCommand.Argument(name: "precheck_default_rule_level", value: precheckDefaultRuleLevel, type: nil) - let individualMetadataItemsArg = individualMetadataItems.asRubyArgument(name: "individual_metadata_items", type: nil) - let appIconArg = appIcon.asRubyArgument(name: "app_icon", type: nil) - let appleWatchAppIconArg = appleWatchAppIcon.asRubyArgument(name: "apple_watch_app_icon", type: nil) - let copyrightArg = copyright.asRubyArgument(name: "copyright", type: nil) - let primaryCategoryArg = primaryCategory.asRubyArgument(name: "primary_category", type: nil) - let secondaryCategoryArg = secondaryCategory.asRubyArgument(name: "secondary_category", type: nil) - let primaryFirstSubCategoryArg = primaryFirstSubCategory.asRubyArgument(name: "primary_first_sub_category", type: nil) - let primarySecondSubCategoryArg = primarySecondSubCategory.asRubyArgument(name: "primary_second_sub_category", type: nil) - let secondaryFirstSubCategoryArg = secondaryFirstSubCategory.asRubyArgument(name: "secondary_first_sub_category", type: nil) - let secondarySecondSubCategoryArg = secondarySecondSubCategory.asRubyArgument(name: "secondary_second_sub_category", type: nil) - let tradeRepresentativeContactInformationArg = tradeRepresentativeContactInformation.asRubyArgument(name: "trade_representative_contact_information", type: nil) - let appReviewInformationArg = appReviewInformation.asRubyArgument(name: "app_review_information", type: nil) - let appReviewAttachmentFileArg = appReviewAttachmentFile.asRubyArgument(name: "app_review_attachment_file", type: nil) - let descriptionArg = description.asRubyArgument(name: "description", type: nil) - let nameArg = name.asRubyArgument(name: "name", type: nil) - let subtitleArg = subtitle.asRubyArgument(name: "subtitle", type: nil) - let keywordsArg = keywords.asRubyArgument(name: "keywords", type: nil) - let promotionalTextArg = promotionalText.asRubyArgument(name: "promotional_text", type: nil) - let releaseNotesArg = releaseNotes.asRubyArgument(name: "release_notes", type: nil) - let privacyUrlArg = privacyUrl.asRubyArgument(name: "privacy_url", type: nil) - let appleTvPrivacyPolicyArg = appleTvPrivacyPolicy.asRubyArgument(name: "apple_tv_privacy_policy", type: nil) - let supportUrlArg = supportUrl.asRubyArgument(name: "support_url", type: nil) - let marketingUrlArg = marketingUrl.asRubyArgument(name: "marketing_url", type: nil) - let languagesArg = languages.asRubyArgument(name: "languages", type: nil) - let ignoreLanguageDirectoryValidationArg = ignoreLanguageDirectoryValidation.asRubyArgument(name: "ignore_language_directory_validation", type: nil) - let precheckIncludeInAppPurchasesArg = precheckIncludeInAppPurchases.asRubyArgument(name: "precheck_include_in_app_purchases", type: nil) - let appArg = app.asRubyArgument(name: "app", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - usernameArg, - appIdentifierArg, - appVersionArg, - ipaArg, - pkgArg, - buildNumberArg, - platformArg, - editLiveArg, - useLiveVersionArg, - metadataPathArg, - screenshotsPathArg, - appPreviewsPathArg, - previewFrameTimeCodeArg, - overwritePreviewVideosArg, - skipBinaryUploadArg, - skipScreenshotsArg, - skipMetadataArg, - skipAppVersionUpdateArg, - forceArg, - overwriteScreenshotsArg, - screenshotProcessingTimeoutArg, - syncScreenshotsArg, - submitForReviewArg, - verifyOnlyArg, - rejectIfPossibleArg, - versionCheckWaitRetryLimitArg, - automaticReleaseArg, - autoReleaseDateArg, - phasedReleaseArg, - resetRatingsArg, - priceTierArg, - appRatingConfigPathArg, - submissionInformationArg, - teamIdArg, - teamNameArg, - devPortalTeamIdArg, - devPortalTeamNameArg, - itcProviderArg, - providerPublicIdArg, - runPrecheckBeforeSubmitArg, - precheckDefaultRuleLevelArg, - individualMetadataItemsArg, - appIconArg, - appleWatchAppIconArg, - copyrightArg, - primaryCategoryArg, - secondaryCategoryArg, - primaryFirstSubCategoryArg, - primarySecondSubCategoryArg, - secondaryFirstSubCategoryArg, - secondarySecondSubCategoryArg, - tradeRepresentativeContactInformationArg, - appReviewInformationArg, - appReviewAttachmentFileArg, - descriptionArg, - nameArg, - subtitleArg, - keywordsArg, - promotionalTextArg, - releaseNotesArg, - privacyUrlArg, - appleTvPrivacyPolicyArg, - supportUrlArg, - marketingUrlArg, - languagesArg, - ignoreLanguageDirectoryValidationArg, - precheckIncludeInAppPurchasesArg, - appArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "upload_to_app_store", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload metadata, screenshots and binaries to Google Play (via _supply_) - - - parameters: - - packageName: The package name of the application to use - - versionName: Version name (used when uploading new apks/aabs) - defaults to 'versionName' in build.gradle or AndroidManifest.xml - - versionCode: The versionCode for which to download the generated APK - - releaseStatus: Release status (used when uploading new apks/aabs) - valid values are completed, draft, halted, inProgress - - track: The track of the application to use. The default available tracks are: production, beta, alpha, internal - - rollout: The percentage of the user fraction when uploading to the rollout track (setting to 1 will complete the rollout) - - metadataPath: Path to the directory containing the metadata files - - key: **DEPRECATED!** Use `--json_key` instead - The p12 File used to authenticate with Google - - issuer: **DEPRECATED!** Use `--json_key` instead - The issuer of the p12 file (email address of the service account) - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - apk: Path to the APK file to upload - - apkPaths: An array of paths to APK files to upload - - aab: Path to the AAB file to upload - - aabPaths: An array of paths to AAB files to upload - - skipUploadApk: Whether to skip uploading APK - - skipUploadAab: Whether to skip uploading AAB - - skipUploadMetadata: Whether to skip uploading metadata, changelogs not included - - skipUploadChangelogs: Whether to skip uploading changelogs - - skipUploadImages: Whether to skip uploading images, screenshots not included - - skipUploadScreenshots: Whether to skip uploading SCREENSHOTS - - syncImageUpload: Whether to use sha256 comparison to skip upload of images and screenshots that are already in Play Store - - trackPromoteTo: The track to promote to. The default available tracks are: production, beta, alpha, internal - - trackPromoteReleaseStatus: Promoted track release status (used when promoting a track) - valid values are completed, draft, halted, inProgress - - validateOnly: Only validate changes with Google Play rather than actually publish - - mapping: Path to the mapping file to upload (mapping.txt or native-debug-symbols.zip alike) - - mappingPaths: An array of paths to mapping files to upload (mapping.txt or native-debug-symbols.zip alike) - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - checkSupersededTracks: **DEPRECATED!** Google Play does this automatically now - Check the other tracks for superseded versions and disable them - - timeout: Timeout for read, open, and send (in seconds) - - deactivateOnPromote: **DEPRECATED!** Google Play does this automatically now - When promoting to a new track, deactivate the binary in the origin track - - versionCodesToRetain: An array of version codes to retain when publishing a new APK - - changesNotSentForReview: Indicates that the changes in this edit will not be reviewed until they are explicitly sent for review from the Google Play Console UI - - rescueChangesNotSentForReview: Catches changes_not_sent_for_review errors when an edit is committed and retries with the configuration that the error message recommended - - inAppUpdatePriority: In-app update priority for all the newly added apks in the release. Can take values between [0,5] - - obbMainReferencesVersion: References version of 'main' expansion file - - obbMainFileSize: Size of 'main' expansion file in bytes - - obbPatchReferencesVersion: References version of 'patch' expansion file - - obbPatchFileSize: Size of 'patch' expansion file in bytes - - ackBundleInstallationWarning: Must be set to true if the bundle installation may trigger a warning on user devices (e.g can only be downloaded over wifi). Typically this is required for bundles over 150MB - - More information: https://docs.fastlane.tools/actions/supply/ - */ -public func uploadToPlayStore(packageName: String, - versionName: OptionalConfigValue = .fastlaneDefault(nil), - versionCode: OptionalConfigValue = .fastlaneDefault(nil), - releaseStatus: String = "completed", - track: String = "production", - rollout: OptionalConfigValue = .fastlaneDefault(nil), - metadataPath: OptionalConfigValue = .fastlaneDefault(nil), - key: OptionalConfigValue = .fastlaneDefault(nil), - issuer: OptionalConfigValue = .fastlaneDefault(nil), - jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - apk: OptionalConfigValue = .fastlaneDefault(nil), - apkPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - aab: OptionalConfigValue = .fastlaneDefault(nil), - aabPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - skipUploadApk: OptionalConfigValue = .fastlaneDefault(false), - skipUploadAab: OptionalConfigValue = .fastlaneDefault(false), - skipUploadMetadata: OptionalConfigValue = .fastlaneDefault(false), - skipUploadChangelogs: OptionalConfigValue = .fastlaneDefault(false), - skipUploadImages: OptionalConfigValue = .fastlaneDefault(false), - skipUploadScreenshots: OptionalConfigValue = .fastlaneDefault(false), - syncImageUpload: OptionalConfigValue = .fastlaneDefault(false), - trackPromoteTo: OptionalConfigValue = .fastlaneDefault(nil), - trackPromoteReleaseStatus: String = "completed", - validateOnly: OptionalConfigValue = .fastlaneDefault(false), - mapping: OptionalConfigValue = .fastlaneDefault(nil), - mappingPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - checkSupersededTracks: OptionalConfigValue = .fastlaneDefault(false), - timeout: Int = 300, - deactivateOnPromote: OptionalConfigValue = .fastlaneDefault(true), - versionCodesToRetain: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - changesNotSentForReview: OptionalConfigValue = .fastlaneDefault(false), - rescueChangesNotSentForReview: OptionalConfigValue = .fastlaneDefault(true), - inAppUpdatePriority: OptionalConfigValue = .fastlaneDefault(nil), - obbMainReferencesVersion: OptionalConfigValue = .fastlaneDefault(nil), - obbMainFileSize: OptionalConfigValue = .fastlaneDefault(nil), - obbPatchReferencesVersion: OptionalConfigValue = .fastlaneDefault(nil), - obbPatchFileSize: OptionalConfigValue = .fastlaneDefault(nil), - ackBundleInstallationWarning: OptionalConfigValue = .fastlaneDefault(false)) -{ - let packageNameArg = RubyCommand.Argument(name: "package_name", value: packageName, type: nil) - let versionNameArg = versionName.asRubyArgument(name: "version_name", type: nil) - let versionCodeArg = versionCode.asRubyArgument(name: "version_code", type: nil) - let releaseStatusArg = RubyCommand.Argument(name: "release_status", value: releaseStatus, type: nil) - let trackArg = RubyCommand.Argument(name: "track", value: track, type: nil) - let rolloutArg = rollout.asRubyArgument(name: "rollout", type: nil) - let metadataPathArg = metadataPath.asRubyArgument(name: "metadata_path", type: nil) - let keyArg = key.asRubyArgument(name: "key", type: nil) - let issuerArg = issuer.asRubyArgument(name: "issuer", type: nil) - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let apkArg = apk.asRubyArgument(name: "apk", type: nil) - let apkPathsArg = apkPaths.asRubyArgument(name: "apk_paths", type: nil) - let aabArg = aab.asRubyArgument(name: "aab", type: nil) - let aabPathsArg = aabPaths.asRubyArgument(name: "aab_paths", type: nil) - let skipUploadApkArg = skipUploadApk.asRubyArgument(name: "skip_upload_apk", type: nil) - let skipUploadAabArg = skipUploadAab.asRubyArgument(name: "skip_upload_aab", type: nil) - let skipUploadMetadataArg = skipUploadMetadata.asRubyArgument(name: "skip_upload_metadata", type: nil) - let skipUploadChangelogsArg = skipUploadChangelogs.asRubyArgument(name: "skip_upload_changelogs", type: nil) - let skipUploadImagesArg = skipUploadImages.asRubyArgument(name: "skip_upload_images", type: nil) - let skipUploadScreenshotsArg = skipUploadScreenshots.asRubyArgument(name: "skip_upload_screenshots", type: nil) - let syncImageUploadArg = syncImageUpload.asRubyArgument(name: "sync_image_upload", type: nil) - let trackPromoteToArg = trackPromoteTo.asRubyArgument(name: "track_promote_to", type: nil) - let trackPromoteReleaseStatusArg = RubyCommand.Argument(name: "track_promote_release_status", value: trackPromoteReleaseStatus, type: nil) - let validateOnlyArg = validateOnly.asRubyArgument(name: "validate_only", type: nil) - let mappingArg = mapping.asRubyArgument(name: "mapping", type: nil) - let mappingPathsArg = mappingPaths.asRubyArgument(name: "mapping_paths", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let checkSupersededTracksArg = checkSupersededTracks.asRubyArgument(name: "check_superseded_tracks", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let deactivateOnPromoteArg = deactivateOnPromote.asRubyArgument(name: "deactivate_on_promote", type: nil) - let versionCodesToRetainArg = versionCodesToRetain.asRubyArgument(name: "version_codes_to_retain", type: nil) - let changesNotSentForReviewArg = changesNotSentForReview.asRubyArgument(name: "changes_not_sent_for_review", type: nil) - let rescueChangesNotSentForReviewArg = rescueChangesNotSentForReview.asRubyArgument(name: "rescue_changes_not_sent_for_review", type: nil) - let inAppUpdatePriorityArg = inAppUpdatePriority.asRubyArgument(name: "in_app_update_priority", type: nil) - let obbMainReferencesVersionArg = obbMainReferencesVersion.asRubyArgument(name: "obb_main_references_version", type: nil) - let obbMainFileSizeArg = obbMainFileSize.asRubyArgument(name: "obb_main_file_size", type: nil) - let obbPatchReferencesVersionArg = obbPatchReferencesVersion.asRubyArgument(name: "obb_patch_references_version", type: nil) - let obbPatchFileSizeArg = obbPatchFileSize.asRubyArgument(name: "obb_patch_file_size", type: nil) - let ackBundleInstallationWarningArg = ackBundleInstallationWarning.asRubyArgument(name: "ack_bundle_installation_warning", type: nil) - let array: [RubyCommand.Argument?] = [packageNameArg, - versionNameArg, - versionCodeArg, - releaseStatusArg, - trackArg, - rolloutArg, - metadataPathArg, - keyArg, - issuerArg, - jsonKeyArg, - jsonKeyDataArg, - apkArg, - apkPathsArg, - aabArg, - aabPathsArg, - skipUploadApkArg, - skipUploadAabArg, - skipUploadMetadataArg, - skipUploadChangelogsArg, - skipUploadImagesArg, - skipUploadScreenshotsArg, - syncImageUploadArg, - trackPromoteToArg, - trackPromoteReleaseStatusArg, - validateOnlyArg, - mappingArg, - mappingPathsArg, - rootUrlArg, - checkSupersededTracksArg, - timeoutArg, - deactivateOnPromoteArg, - versionCodesToRetainArg, - changesNotSentForReviewArg, - rescueChangesNotSentForReviewArg, - inAppUpdatePriorityArg, - obbMainReferencesVersionArg, - obbMainFileSizeArg, - obbPatchReferencesVersionArg, - obbPatchFileSizeArg, - ackBundleInstallationWarningArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "upload_to_play_store", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload binaries to Google Play Internal App Sharing (via _supply_) - - - parameters: - - packageName: The package name of the application to use - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - apk: Path to the APK file to upload - - apkPaths: An array of paths to APK files to upload - - aab: Path to the AAB file to upload - - aabPaths: An array of paths to AAB files to upload - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - timeout: Timeout for read, open, and send (in seconds) - - - returns: Returns a string containing the download URL for the uploaded APK/AAB (or array of strings if multiple were uploaded). - - More information: https://docs.fastlane.tools/actions/upload_to_play_store_internal_app_sharing/ - */ -public func uploadToPlayStoreInternalAppSharing(packageName: String, - jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - apk: OptionalConfigValue = .fastlaneDefault(nil), - apkPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - aab: OptionalConfigValue = .fastlaneDefault(nil), - aabPaths: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 300) -{ - let packageNameArg = RubyCommand.Argument(name: "package_name", value: packageName, type: nil) - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let apkArg = apk.asRubyArgument(name: "apk", type: nil) - let apkPathsArg = apkPaths.asRubyArgument(name: "apk_paths", type: nil) - let aabArg = aab.asRubyArgument(name: "aab", type: nil) - let aabPathsArg = aabPaths.asRubyArgument(name: "aab_paths", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let array: [RubyCommand.Argument?] = [packageNameArg, - jsonKeyArg, - jsonKeyDataArg, - apkArg, - apkPathsArg, - aabArg, - aabPathsArg, - rootUrlArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "upload_to_play_store_internal_app_sharing", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Upload new binary to App Store Connect for TestFlight beta testing (via _pilot_) - - - parameters: - - apiKeyPath: Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - - apiKey: Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - - username: Your Apple ID Username - - appIdentifier: The bundle identifier of the app to upload or manage testers (optional) - - appPlatform: The platform to use (optional) - - appleId: Apple ID property in the App Information section in App Store Connect - - ipa: Path to the ipa file to upload - - pkg: Path to your pkg file - - demoAccountRequired: Do you need a demo account when Apple does review? - - betaAppReviewInfo: Beta app review information for contact info and demo account - - localizedAppInfo: Localized beta app test info for description, feedback email, marketing url, and privacy policy - - betaAppDescription: Provide the 'Beta App Description' when uploading a new build - - betaAppFeedbackEmail: Provide the beta app email when uploading a new build - - localizedBuildInfo: Localized beta app test info for what's new - - changelog: Provide the 'What to Test' text when uploading a new build - - skipSubmission: Skip the distributing action of pilot and only upload the ipa file - - skipWaitingForBuildProcessing: If set to true, the `distribute_external` option won't work and no build will be distributed to testers. (You might want to use this option if you are using this action on CI and have to pay for 'minutes used' on your CI plan). If set to `true` and a changelog is provided, it will partially wait for the build to appear on AppStore Connect so the changelog can be set, and skip the remaining processing steps - - updateBuildInfoOnUpload: **DEPRECATED!** Update build info immediately after validation. This is deprecated and will be removed in a future release. App Store Connect no longer supports setting build info until after build processing has completed, which is when build info is updated by default - - distributeOnly: Distribute a previously uploaded build (equivalent to the `fastlane pilot distribute` command) - - usesNonExemptEncryption: Provide the 'Uses Non-Exempt Encryption' for export compliance. This is used if there is 'ITSAppUsesNonExemptEncryption' is not set in the Info.plist - - distributeExternal: Should the build be distributed to external testers? If set to true, use of `groups` option is required - - notifyExternalTesters: Should notify external testers? (Not setting a value will use App Store Connect's default which is to notify) - - appVersion: The version number of the application build to distribute. If the version number is not specified, then the most recent build uploaded to TestFlight will be distributed. If specified, the most recent build for the version number will be distributed - - buildNumber: The build number of the application build to distribute. If the build number is not specified, the most recent build is distributed - - expirePreviousBuilds: Should expire previous builds? - - firstName: The tester's first name - - lastName: The tester's last name - - email: The tester's email - - testersFilePath: Path to a CSV file of testers - - groups: Associate tester to one group or more by group name / group id. E.g. `-g "Team 1","Team 2"` This is required when `distribute_external` option is set to true or when we want to add a tester to one or more external testing groups - - teamId: The ID of your App Store Connect team if you're in multiple teams - - teamName: The name of your App Store Connect team if you're in multiple teams - - devPortalTeamId: The short ID of your team in the developer portal, if you're in multiple teams. Different from your iTC team ID! - - itcProvider: The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. To get provider short name run `pathToXcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off`. The short names of providers should be listed in the second column - - providerPublicId: The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication - - waitProcessingInterval: Interval in seconds to wait for App Store Connect processing - - waitProcessingTimeoutDuration: Timeout duration in seconds to wait for App Store Connect processing. If set, after exceeding timeout duration, this will `force stop` to wait for App Store Connect processing and exit with exception - - waitForUploadedBuild: **DEPRECATED!** No longer needed with the transition over to the App Store Connect API - Use version info from uploaded ipa file to determine what build to use for distribution. If set to false, latest processing or any latest build will be used - - rejectBuildWaitingForReview: Expire previous if it's 'waiting for review' - - submitBetaReview: Send the build for a beta review - - More details can be found on https://docs.fastlane.tools/actions/pilot/. - This integration will only do the TestFlight upload. - */ -public func uploadToTestflight(apiKeyPath: OptionalConfigValue = .fastlaneDefault(nil), - apiKey: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - username: OptionalConfigValue = .fastlaneDefault(nil), - appIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - appPlatform: OptionalConfigValue = .fastlaneDefault(nil), - appleId: OptionalConfigValue = .fastlaneDefault(nil), - ipa: OptionalConfigValue = .fastlaneDefault(nil), - pkg: OptionalConfigValue = .fastlaneDefault(nil), - demoAccountRequired: OptionalConfigValue = .fastlaneDefault(nil), - betaAppReviewInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - localizedAppInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - betaAppDescription: OptionalConfigValue = .fastlaneDefault(nil), - betaAppFeedbackEmail: OptionalConfigValue = .fastlaneDefault(nil), - localizedBuildInfo: OptionalConfigValue<[String: Any]?> = .fastlaneDefault(nil), - changelog: OptionalConfigValue = .fastlaneDefault(nil), - skipSubmission: OptionalConfigValue = .fastlaneDefault(false), - skipWaitingForBuildProcessing: OptionalConfigValue = .fastlaneDefault(false), - updateBuildInfoOnUpload: OptionalConfigValue = .fastlaneDefault(false), - distributeOnly: OptionalConfigValue = .fastlaneDefault(false), - usesNonExemptEncryption: OptionalConfigValue = .fastlaneDefault(false), - distributeExternal: OptionalConfigValue = .fastlaneDefault(false), - notifyExternalTesters: Any? = nil, - appVersion: OptionalConfigValue = .fastlaneDefault(nil), - buildNumber: OptionalConfigValue = .fastlaneDefault(nil), - expirePreviousBuilds: OptionalConfigValue = .fastlaneDefault(false), - firstName: OptionalConfigValue = .fastlaneDefault(nil), - lastName: OptionalConfigValue = .fastlaneDefault(nil), - email: OptionalConfigValue = .fastlaneDefault(nil), - testersFilePath: String = "./testers.csv", - groups: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - teamId: Any? = nil, - teamName: OptionalConfigValue = .fastlaneDefault(nil), - devPortalTeamId: OptionalConfigValue = .fastlaneDefault(nil), - itcProvider: OptionalConfigValue = .fastlaneDefault(nil), - providerPublicId: OptionalConfigValue = .fastlaneDefault(nil), - waitProcessingInterval: Int = 30, - waitProcessingTimeoutDuration: OptionalConfigValue = .fastlaneDefault(nil), - waitForUploadedBuild: OptionalConfigValue = .fastlaneDefault(false), - rejectBuildWaitingForReview: OptionalConfigValue = .fastlaneDefault(false), - submitBetaReview: OptionalConfigValue = .fastlaneDefault(true)) -{ - let apiKeyPathArg = apiKeyPath.asRubyArgument(name: "api_key_path", type: nil) - let apiKeyArg = apiKey.asRubyArgument(name: "api_key", type: nil) - let usernameArg = username.asRubyArgument(name: "username", type: nil) - let appIdentifierArg = appIdentifier.asRubyArgument(name: "app_identifier", type: nil) - let appPlatformArg = appPlatform.asRubyArgument(name: "app_platform", type: nil) - let appleIdArg = appleId.asRubyArgument(name: "apple_id", type: nil) - let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) - let pkgArg = pkg.asRubyArgument(name: "pkg", type: nil) - let demoAccountRequiredArg = demoAccountRequired.asRubyArgument(name: "demo_account_required", type: nil) - let betaAppReviewInfoArg = betaAppReviewInfo.asRubyArgument(name: "beta_app_review_info", type: nil) - let localizedAppInfoArg = localizedAppInfo.asRubyArgument(name: "localized_app_info", type: nil) - let betaAppDescriptionArg = betaAppDescription.asRubyArgument(name: "beta_app_description", type: nil) - let betaAppFeedbackEmailArg = betaAppFeedbackEmail.asRubyArgument(name: "beta_app_feedback_email", type: nil) - let localizedBuildInfoArg = localizedBuildInfo.asRubyArgument(name: "localized_build_info", type: nil) - let changelogArg = changelog.asRubyArgument(name: "changelog", type: nil) - let skipSubmissionArg = skipSubmission.asRubyArgument(name: "skip_submission", type: nil) - let skipWaitingForBuildProcessingArg = skipWaitingForBuildProcessing.asRubyArgument(name: "skip_waiting_for_build_processing", type: nil) - let updateBuildInfoOnUploadArg = updateBuildInfoOnUpload.asRubyArgument(name: "update_build_info_on_upload", type: nil) - let distributeOnlyArg = distributeOnly.asRubyArgument(name: "distribute_only", type: nil) - let usesNonExemptEncryptionArg = usesNonExemptEncryption.asRubyArgument(name: "uses_non_exempt_encryption", type: nil) - let distributeExternalArg = distributeExternal.asRubyArgument(name: "distribute_external", type: nil) - let notifyExternalTestersArg = RubyCommand.Argument(name: "notify_external_testers", value: notifyExternalTesters, type: nil) - let appVersionArg = appVersion.asRubyArgument(name: "app_version", type: nil) - let buildNumberArg = buildNumber.asRubyArgument(name: "build_number", type: nil) - let expirePreviousBuildsArg = expirePreviousBuilds.asRubyArgument(name: "expire_previous_builds", type: nil) - let firstNameArg = firstName.asRubyArgument(name: "first_name", type: nil) - let lastNameArg = lastName.asRubyArgument(name: "last_name", type: nil) - let emailArg = email.asRubyArgument(name: "email", type: nil) - let testersFilePathArg = RubyCommand.Argument(name: "testers_file_path", value: testersFilePath, type: nil) - let groupsArg = groups.asRubyArgument(name: "groups", type: nil) - let teamIdArg = RubyCommand.Argument(name: "team_id", value: teamId, type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let devPortalTeamIdArg = devPortalTeamId.asRubyArgument(name: "dev_portal_team_id", type: nil) - let itcProviderArg = itcProvider.asRubyArgument(name: "itc_provider", type: nil) - let providerPublicIdArg = providerPublicId.asRubyArgument(name: "provider_public_id", type: nil) - let waitProcessingIntervalArg = RubyCommand.Argument(name: "wait_processing_interval", value: waitProcessingInterval, type: nil) - let waitProcessingTimeoutDurationArg = waitProcessingTimeoutDuration.asRubyArgument(name: "wait_processing_timeout_duration", type: nil) - let waitForUploadedBuildArg = waitForUploadedBuild.asRubyArgument(name: "wait_for_uploaded_build", type: nil) - let rejectBuildWaitingForReviewArg = rejectBuildWaitingForReview.asRubyArgument(name: "reject_build_waiting_for_review", type: nil) - let submitBetaReviewArg = submitBetaReview.asRubyArgument(name: "submit_beta_review", type: nil) - let array: [RubyCommand.Argument?] = [apiKeyPathArg, - apiKeyArg, - usernameArg, - appIdentifierArg, - appPlatformArg, - appleIdArg, - ipaArg, - pkgArg, - demoAccountRequiredArg, - betaAppReviewInfoArg, - localizedAppInfoArg, - betaAppDescriptionArg, - betaAppFeedbackEmailArg, - localizedBuildInfoArg, - changelogArg, - skipSubmissionArg, - skipWaitingForBuildProcessingArg, - updateBuildInfoOnUploadArg, - distributeOnlyArg, - usesNonExemptEncryptionArg, - distributeExternalArg, - notifyExternalTestersArg, - appVersionArg, - buildNumberArg, - expirePreviousBuildsArg, - firstNameArg, - lastNameArg, - emailArg, - testersFilePathArg, - groupsArg, - teamIdArg, - teamNameArg, - devPortalTeamIdArg, - itcProviderArg, - providerPublicIdArg, - waitProcessingIntervalArg, - waitProcessingTimeoutDurationArg, - waitForUploadedBuildArg, - rejectBuildWaitingForReviewArg, - submitBetaReviewArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "upload_to_testflight", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Validate that the Google Play Store `json_key` works - - - parameters: - - jsonKey: The path to a file containing service account JSON, used to authenticate with Google - - jsonKeyData: The raw service account JSON data used to authenticate with Google - - rootUrl: Root URL for the Google Play API. The provided URL will be used for API calls in place of https://www.googleapis.com/ - - timeout: Timeout for read, open, and send (in seconds) - - Use this action to test and validate your private key json key file used to connect and authenticate with the Google Play API - */ -public func validatePlayStoreJsonKey(jsonKey: OptionalConfigValue = .fastlaneDefault(nil), - jsonKeyData: OptionalConfigValue = .fastlaneDefault(nil), - rootUrl: OptionalConfigValue = .fastlaneDefault(nil), - timeout: Int = 300) -{ - let jsonKeyArg = jsonKey.asRubyArgument(name: "json_key", type: nil) - let jsonKeyDataArg = jsonKeyData.asRubyArgument(name: "json_key_data", type: nil) - let rootUrlArg = rootUrl.asRubyArgument(name: "root_url", type: nil) - let timeoutArg = RubyCommand.Argument(name: "timeout", value: timeout, type: nil) - let array: [RubyCommand.Argument?] = [jsonKeyArg, - jsonKeyDataArg, - rootUrlArg, - timeoutArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "validate_play_store_json_key", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Able to verify various settings in ipa file - - - parameters: - - provisioningType: Required type of provisioning - - provisioningUuid: Required UUID of provisioning profile - - teamIdentifier: Required team identifier - - teamName: Required team name - - appName: Required app name - - bundleIdentifier: Required bundle identifier - - ipaPath: Explicitly set the ipa path - - buildPath: Explicitly set the ipa, app or xcarchive path - - Verifies that the built app was built using the expected build resources. This is relevant for people who build on machines that are used to build apps with different profiles, certificates and/or bundle identifiers to guard against configuration mistakes. - */ -public func verifyBuild(provisioningType: OptionalConfigValue = .fastlaneDefault(nil), - provisioningUuid: OptionalConfigValue = .fastlaneDefault(nil), - teamIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - teamName: OptionalConfigValue = .fastlaneDefault(nil), - appName: OptionalConfigValue = .fastlaneDefault(nil), - bundleIdentifier: OptionalConfigValue = .fastlaneDefault(nil), - ipaPath: OptionalConfigValue = .fastlaneDefault(nil), - buildPath: OptionalConfigValue = .fastlaneDefault(nil)) -{ - let provisioningTypeArg = provisioningType.asRubyArgument(name: "provisioning_type", type: nil) - let provisioningUuidArg = provisioningUuid.asRubyArgument(name: "provisioning_uuid", type: nil) - let teamIdentifierArg = teamIdentifier.asRubyArgument(name: "team_identifier", type: nil) - let teamNameArg = teamName.asRubyArgument(name: "team_name", type: nil) - let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) - let bundleIdentifierArg = bundleIdentifier.asRubyArgument(name: "bundle_identifier", type: nil) - let ipaPathArg = ipaPath.asRubyArgument(name: "ipa_path", type: nil) - let buildPathArg = buildPath.asRubyArgument(name: "build_path", type: nil) - let array: [RubyCommand.Argument?] = [provisioningTypeArg, - provisioningUuidArg, - teamIdentifierArg, - teamNameArg, - appNameArg, - bundleIdentifierArg, - ipaPathArg, - buildPathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "verify_build", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Verifies all keys referenced from the Podfile are non-empty - - Runs a check against all keys specified in your Podfile to make sure they're more than a single character long. This is to ensure you don't deploy with stubbed keys. - */ -public func verifyPodKeys() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "verify_pod_keys", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Verifies that the Xcode installation is properly signed by Apple - - - parameter xcodePath: The path to the Xcode installation to test - - This action was implemented after the recent Xcode attack to make sure you're not using a [hacked Xcode installation](http://researchcenter.paloaltonetworks.com/2015/09/novel-malware-xcodeghost-modifies-xcode-infects-apple-ios-apps-and-hits-app-store/). - */ -public func verifyXcode(xcodePath: String) { - let xcodePathArg = RubyCommand.Argument(name: "xcode_path", value: xcodePath, type: nil) - let array: [RubyCommand.Argument?] = [xcodePathArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "verify_xcode", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Increment or set the version in a podspec file - - - parameters: - - path: You must specify the path to the podspec file to update - - bumpType: The type of this version bump. Available: patch, minor, major - - versionNumber: Change to a specific version. This will replace the bump type value - - versionAppendix: Change version appendix to a specific value. For example 1.4.14.4.1 -> 1.4.14.5 - - requireVariablePrefix: true by default, this is used for non CocoaPods version bumps only - - You can use this action to manipulate any 'version' variable contained in a ruby file. - For example, you can use it to bump the version of a CocoaPods' podspec file. - It also supports versions that are not semantic: `1.4.14.4.1`. - For such versions, there is an option to change the appendix (e.g. `4.1`). - */ -public func versionBumpPodspec(path: String, - bumpType: String = "patch", - versionNumber: OptionalConfigValue = .fastlaneDefault(nil), - versionAppendix: OptionalConfigValue = .fastlaneDefault(nil), - requireVariablePrefix: OptionalConfigValue = .fastlaneDefault(true)) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let bumpTypeArg = RubyCommand.Argument(name: "bump_type", value: bumpType, type: nil) - let versionNumberArg = versionNumber.asRubyArgument(name: "version_number", type: nil) - let versionAppendixArg = versionAppendix.asRubyArgument(name: "version_appendix", type: nil) - let requireVariablePrefixArg = requireVariablePrefix.asRubyArgument(name: "require_variable_prefix", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - bumpTypeArg, - versionNumberArg, - versionAppendixArg, - requireVariablePrefixArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "version_bump_podspec", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Receive the version number from a podspec file - - - parameters: - - path: You must specify the path to the podspec file - - requireVariablePrefix: true by default, this is used for non CocoaPods version bumps only - */ -public func versionGetPodspec(path: String, - requireVariablePrefix: OptionalConfigValue = .fastlaneDefault(true)) -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let requireVariablePrefixArg = requireVariablePrefix.asRubyArgument(name: "require_variable_prefix", type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - requireVariablePrefixArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "version_get_podspec", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Archives the project using `xcodebuild` - */ -public func xcarchive() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xcarchive", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Builds the project using `xcodebuild` - */ -public func xcbuild() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xcbuild", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Cleans the project using `xcodebuild` - */ -public func xcclean() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xcclean", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Exports the project using `xcodebuild` - */ -public func xcexport() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xcexport", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Make sure a certain version of Xcode is installed - - - parameters: - - version: The version number of the version of Xcode to install - - username: Your Apple ID Username - - teamId: The ID of your team if you're in multiple teams - - downloadRetryAttempts: Number of times the download will be retried in case of failure - - - returns: The path to the newly installed Xcode version - - Makes sure a specific version of Xcode is installed. If that's not the case, it will automatically be downloaded by the [xcode_install](https://github.com/neonichu/xcode-install) gem. This will make sure to use the correct Xcode for later actions. - */ -@discardableResult public func xcodeInstall(version: String, - username: String, - teamId: OptionalConfigValue = .fastlaneDefault(nil), - downloadRetryAttempts: Int = 3) -> String -{ - let versionArg = RubyCommand.Argument(name: "version", value: version, type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let teamIdArg = teamId.asRubyArgument(name: "team_id", type: nil) - let downloadRetryAttemptsArg = RubyCommand.Argument(name: "download_retry_attempts", value: downloadRetryAttempts, type: nil) - let array: [RubyCommand.Argument?] = [versionArg, - usernameArg, - teamIdArg, - downloadRetryAttemptsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "xcode_install", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Change the xcode-path to use. Useful for beta versions of Xcode - - Select and build with the Xcode installed at the provided path. - Use the `xcodes` action if you want to select an Xcode: - - Based on a version specifier or - - You don't have known, stable paths, as may happen in a CI environment. - */ -public func xcodeSelect() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xcode_select", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Downloads Xcode Bot assets like the `.xcarchive` and logs - - - parameters: - - host: IP Address/Hostname of Xcode Server - - botName: Name of the Bot to pull assets from - - integrationNumber: Optionally you can override which integration's assets should be downloaded. If not provided, the latest integration is used - - username: Username for your Xcode Server - - password: Password for your Xcode Server - - targetFolder: Relative path to a folder into which to download assets - - keepAllAssets: Whether to keep all assets or let the script delete everything except for the .xcarchive - - trustSelfSignedCerts: Whether to trust self-signed certs on your Xcode Server - - This action downloads assets from your Xcode Server Bot (works with Xcode Server using Xcode 6 and 7. By default, this action downloads all assets, unzips them and deletes everything except for the `.xcarchive`. - If you'd like to keep all downloaded assets, pass `keep_all_assets: true`. - This action returns the path to the downloaded assets folder and puts into shared values the paths to the asset folder and to the `.xcarchive` inside it. - */ -@discardableResult public func xcodeServerGetAssets(host: String, - botName: String, - integrationNumber: OptionalConfigValue = .fastlaneDefault(nil), - username: String = "", - password: OptionalConfigValue = .fastlaneDefault(nil), - targetFolder: String = "./xcs_assets", - keepAllAssets: OptionalConfigValue = .fastlaneDefault(false), - trustSelfSignedCerts: OptionalConfigValue = .fastlaneDefault(true)) -> [String] -{ - let hostArg = RubyCommand.Argument(name: "host", value: host, type: nil) - let botNameArg = RubyCommand.Argument(name: "bot_name", value: botName, type: nil) - let integrationNumberArg = integrationNumber.asRubyArgument(name: "integration_number", type: nil) - let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) - let passwordArg = password.asRubyArgument(name: "password", type: nil) - let targetFolderArg = RubyCommand.Argument(name: "target_folder", value: targetFolder, type: nil) - let keepAllAssetsArg = keepAllAssets.asRubyArgument(name: "keep_all_assets", type: nil) - let trustSelfSignedCertsArg = trustSelfSignedCerts.asRubyArgument(name: "trust_self_signed_certs", type: nil) - let array: [RubyCommand.Argument?] = [hostArg, - botNameArg, - integrationNumberArg, - usernameArg, - passwordArg, - targetFolderArg, - keepAllAssetsArg, - trustSelfSignedCertsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "xcode_server_get_assets", className: nil, args: args) - return parseArray(fromString: runner.executeCommand(command)) -} - -/** - Use the `xcodebuild` command to build and sign your app - - **Note**: `xcodebuild` is a complex command, so it is recommended to use [_gym_](https://docs.fastlane.tools/actions/gym/) for building your ipa file and [_scan_](https://docs.fastlane.tools/actions/scan/) for testing your app instead. - */ -public func xcodebuild() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xcodebuild", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Make sure a certain version of Xcode is installed, installing it only if needed - - - parameters: - - version: The version number of the version of Xcode to install. Defaults to the value specified in the .xcode-version file - - updateList: Whether the list of available Xcode versions should be updated before running the install command - - selectForCurrentBuildOnly: When true, it won't attempt to install an Xcode version, just find the installed Xcode version that best matches the passed version argument, and select it for the current build steps. It doesn't change the global Xcode version (e.g. via 'xcrun xcode-select'), which would require sudo permissions — when this option is true, this action doesn't require sudo permissions - - binaryPath: Where the xcodes binary lives on your system (full path) - - xcodesArgs: Pass in xcodes command line arguments directly. When present, other parameters are ignored and only this parameter is used to build the command to be executed - - - returns: The path to the newly installed Xcode version - - Makes sure a specific version of Xcode is installed. If that's not the case, it will automatically be downloaded by [xcodes](https://github.com/RobotsAndPencils/xcodes). - This will make sure to use the correct Xcode version for later actions. - Note that this action depends on [xcodes](https://github.com/RobotsAndPencils/xcodes) CLI, so make sure you have it installed in your environment. For the installation guide, see: https://github.com/RobotsAndPencils/xcodes#installation - */ -@discardableResult public func xcodes(version: String, - updateList: OptionalConfigValue = .fastlaneDefault(true), - selectForCurrentBuildOnly: OptionalConfigValue = .fastlaneDefault(false), - binaryPath: String = "/opt/homebrew/bin/xcodes", - xcodesArgs: OptionalConfigValue = .fastlaneDefault(nil)) -> String -{ - let versionArg = RubyCommand.Argument(name: "version", value: version, type: nil) - let updateListArg = updateList.asRubyArgument(name: "update_list", type: nil) - let selectForCurrentBuildOnlyArg = selectForCurrentBuildOnly.asRubyArgument(name: "select_for_current_build_only", type: nil) - let binaryPathArg = RubyCommand.Argument(name: "binary_path", value: binaryPath, type: nil) - let xcodesArgsArg = xcodesArgs.asRubyArgument(name: "xcodes_args", type: nil) - let array: [RubyCommand.Argument?] = [versionArg, - updateListArg, - selectForCurrentBuildOnlyArg, - binaryPathArg, - xcodesArgsArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "xcodes", className: nil, args: args) - return runner.executeCommand(command) -} - -/** - Nice code coverage reports without hassle - - - parameters: - - workspace: Path the workspace file - - project: Path the project file - - scheme: The project's scheme. Make sure it's marked as `Shared` - - configuration: The configuration used when building the app. Defaults to 'Release' - - sourceDirectory: The path to project's root directory - - derivedDataPath: The directory where build products and other derived data will go - - xccovFileDirectPath: The path or array of paths to the xccoverage/xccovreport/xcresult files to parse to generate code coverage - - outputDirectory: The directory in which all reports will be stored - - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - - useSystemScm: Lets xcodebuild use system's scm configuration - - isSwiftPackage: Enables generating coverage reports for Package.swift derived projects - - htmlReport: Produce an HTML report - - markdownReport: Produce a Markdown report - - jsonReport: Produce a JSON report - - minimumCoveragePercentage: Raise exception if overall coverage percentage is under this value (ie. 75) - - slackUrl: Create an Incoming WebHook for your Slack group to post results there - - slackChannel: #channel or @username - - skipSlack: Don't publish to slack, even when an URL is given - - slackUsername: The username which is used to publish to slack - - slackMessage: The message which is published together with a successful report - - ignoreFilePath: Relative or absolute path to the file containing the list of ignored files - - includeTestTargets: Enables coverage reports for .xctest targets - - includeZeroTargets: Final report will include target even if the coverage is 0% - - excludeTargets: Comma separated list of targets to exclude from coverage report - - includeTargets: Comma separated list of targets to include in coverage report. If specified then exlude_targets will be ignored - - onlyProjectTargets: Display the coverage only for main project targets (e.g. skip Pods targets) - - disableCoveralls: Add this flag to disable automatic submission to Coveralls - - coverallsServiceName: Name of the CI service compatible with Coveralls. i.e. travis-ci. This option must be defined along with coveralls_service_job_id - - coverallsServiceJobId: Name of the current job running on a CI service compatible with Coveralls. This option must be defined along with coveralls_service_name - - coverallsRepoToken: Repository token to be used by integrations not compatible with Coveralls - - xcconfig: Use an extra XCCONFIG file to build your app - - ideFoundationPath: Absolute path to the IDEFoundation.framework binary - - legacySupport: Whether xcov should parse a xccoverage file instead on xccovreport - - Create nice code coverage reports and post coverage summaries on Slack *(xcov gem is required)*. - More information: [https://github.com/fastlane-community/xcov](https://github.com/fastlane-community/xcov). - */ -public func xcov(workspace: OptionalConfigValue = .fastlaneDefault(nil), - project: OptionalConfigValue = .fastlaneDefault(nil), - scheme: OptionalConfigValue = .fastlaneDefault(nil), - configuration: OptionalConfigValue = .fastlaneDefault(nil), - sourceDirectory: OptionalConfigValue = .fastlaneDefault(nil), - derivedDataPath: OptionalConfigValue = .fastlaneDefault(nil), - xccovFileDirectPath: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), - outputDirectory: String = "./xcov_report", - clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false), - isSwiftPackage: OptionalConfigValue = .fastlaneDefault(false), - htmlReport: OptionalConfigValue = .fastlaneDefault(true), - markdownReport: OptionalConfigValue = .fastlaneDefault(false), - jsonReport: OptionalConfigValue = .fastlaneDefault(false), - minimumCoveragePercentage: Float = 0.0, - slackUrl: OptionalConfigValue = .fastlaneDefault(nil), - slackChannel: OptionalConfigValue = .fastlaneDefault(nil), - skipSlack: OptionalConfigValue = .fastlaneDefault(false), - slackUsername: String = "xcov", - slackMessage: String = "Your *xcov* coverage report", - ignoreFilePath: String = "./.xcovignore", - includeTestTargets: OptionalConfigValue = .fastlaneDefault(false), - includeZeroTargets: OptionalConfigValue = .fastlaneDefault(true), - excludeTargets: OptionalConfigValue = .fastlaneDefault(nil), - includeTargets: OptionalConfigValue = .fastlaneDefault(nil), - onlyProjectTargets: OptionalConfigValue = .fastlaneDefault(false), - disableCoveralls: OptionalConfigValue = .fastlaneDefault(false), - coverallsServiceName: OptionalConfigValue = .fastlaneDefault(nil), - coverallsServiceJobId: OptionalConfigValue = .fastlaneDefault(nil), - coverallsRepoToken: OptionalConfigValue = .fastlaneDefault(nil), - xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - ideFoundationPath: String = "/Applications/Xcode_16.4.app/Contents/Developer/../Frameworks/IDEFoundation.framework/Versions/A/IDEFoundation", - legacySupport: OptionalConfigValue = .fastlaneDefault(false)) -{ - let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) - let projectArg = project.asRubyArgument(name: "project", type: nil) - let schemeArg = scheme.asRubyArgument(name: "scheme", type: nil) - let configurationArg = configuration.asRubyArgument(name: "configuration", type: nil) - let sourceDirectoryArg = sourceDirectory.asRubyArgument(name: "source_directory", type: nil) - let derivedDataPathArg = derivedDataPath.asRubyArgument(name: "derived_data_path", type: nil) - let xccovFileDirectPathArg = xccovFileDirectPath.asRubyArgument(name: "xccov_file_direct_path", type: nil) - let outputDirectoryArg = RubyCommand.Argument(name: "output_directory", value: outputDirectory, type: nil) - let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) - let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) - let isSwiftPackageArg = isSwiftPackage.asRubyArgument(name: "is_swift_package", type: nil) - let htmlReportArg = htmlReport.asRubyArgument(name: "html_report", type: nil) - let markdownReportArg = markdownReport.asRubyArgument(name: "markdown_report", type: nil) - let jsonReportArg = jsonReport.asRubyArgument(name: "json_report", type: nil) - let minimumCoveragePercentageArg = RubyCommand.Argument(name: "minimum_coverage_percentage", value: minimumCoveragePercentage, type: nil) - let slackUrlArg = slackUrl.asRubyArgument(name: "slack_url", type: nil) - let slackChannelArg = slackChannel.asRubyArgument(name: "slack_channel", type: nil) - let skipSlackArg = skipSlack.asRubyArgument(name: "skip_slack", type: nil) - let slackUsernameArg = RubyCommand.Argument(name: "slack_username", value: slackUsername, type: nil) - let slackMessageArg = RubyCommand.Argument(name: "slack_message", value: slackMessage, type: nil) - let ignoreFilePathArg = RubyCommand.Argument(name: "ignore_file_path", value: ignoreFilePath, type: nil) - let includeTestTargetsArg = includeTestTargets.asRubyArgument(name: "include_test_targets", type: nil) - let includeZeroTargetsArg = includeZeroTargets.asRubyArgument(name: "include_zero_targets", type: nil) - let excludeTargetsArg = excludeTargets.asRubyArgument(name: "exclude_targets", type: nil) - let includeTargetsArg = includeTargets.asRubyArgument(name: "include_targets", type: nil) - let onlyProjectTargetsArg = onlyProjectTargets.asRubyArgument(name: "only_project_targets", type: nil) - let disableCoverallsArg = disableCoveralls.asRubyArgument(name: "disable_coveralls", type: nil) - let coverallsServiceNameArg = coverallsServiceName.asRubyArgument(name: "coveralls_service_name", type: nil) - let coverallsServiceJobIdArg = coverallsServiceJobId.asRubyArgument(name: "coveralls_service_job_id", type: nil) - let coverallsRepoTokenArg = coverallsRepoToken.asRubyArgument(name: "coveralls_repo_token", type: nil) - let xcconfigArg = xcconfig.asRubyArgument(name: "xcconfig", type: nil) - let ideFoundationPathArg = RubyCommand.Argument(name: "ideFoundationPath", value: ideFoundationPath, type: nil) - let legacySupportArg = legacySupport.asRubyArgument(name: "legacy_support", type: nil) - let array: [RubyCommand.Argument?] = [workspaceArg, - projectArg, - schemeArg, - configurationArg, - sourceDirectoryArg, - derivedDataPathArg, - xccovFileDirectPathArg, - outputDirectoryArg, - clonedSourcePackagesPathArg, - useSystemScmArg, - isSwiftPackageArg, - htmlReportArg, - markdownReportArg, - jsonReportArg, - minimumCoveragePercentageArg, - slackUrlArg, - slackChannelArg, - skipSlackArg, - slackUsernameArg, - slackMessageArg, - ignoreFilePathArg, - includeTestTargetsArg, - includeZeroTargetsArg, - excludeTargetsArg, - includeTargetsArg, - onlyProjectTargetsArg, - disableCoverallsArg, - coverallsServiceNameArg, - coverallsServiceJobIdArg, - coverallsRepoTokenArg, - xcconfigArg, - ideFoundationPathArg, - legacySupportArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "xcov", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Runs tests on the given simulator - */ -public func xctest() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xctest", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Run tests using xctool - - You can run any `xctool` action. This will require having [xctool](https://github.com/facebook/xctool) installed through [Homebrew](http://brew.sh). - It is recommended to store the build configuration in the `.xctool-args` file. - More information: [https://docs.fastlane.tools/actions/xctool/](https://docs.fastlane.tools/actions/xctool/). - */ -public func xctool() { - let args: [RubyCommand.Argument] = [] - let command = RubyCommand(commandID: "", methodName: "xctool", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Select an Xcode to use by version specifier - - - parameter version: The version of Xcode to select specified as a Gem::Version requirement string (e.g. '~> 7.1.0'). Defaults to the value specified in the .xcode-version file - - Finds and selects a version of an installed Xcode that best matches the provided [`Gem::Version` requirement specifier](http://www.rubydoc.info/github/rubygems/rubygems/Gem/Version) - You can either manually provide a specific version using `version:` or you make use of the `.xcode-version` file. - */ -public func xcversion(version: String) { - let versionArg = RubyCommand.Argument(name: "version", value: version, type: nil) - let array: [RubyCommand.Argument?] = [versionArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "xcversion", className: nil, args: args) - _ = runner.executeCommand(command) -} - -/** - Compress a file or folder to a zip - - - parameters: - - path: Path to the directory or file to be zipped - - outputPath: The name of the resulting zip file - - verbose: Enable verbose output of zipped file - - password: Encrypt the contents of the zip archive using a password - - symlinks: Store symbolic links as such in the zip archive - - include: Array of paths or patterns to include - - exclude: Array of paths or patterns to exclude - - - returns: The path to the output zip file - */ -@discardableResult public func zip(path: String, - outputPath: OptionalConfigValue = .fastlaneDefault(nil), - verbose: OptionalConfigValue = .fastlaneDefault(true), - password: OptionalConfigValue = .fastlaneDefault(nil), - symlinks: OptionalConfigValue = .fastlaneDefault(false), - include: [String] = [], - exclude: [String] = []) -> String -{ - let pathArg = RubyCommand.Argument(name: "path", value: path, type: nil) - let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) - let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) - let passwordArg = password.asRubyArgument(name: "password", type: nil) - let symlinksArg = symlinks.asRubyArgument(name: "symlinks", type: nil) - let includeArg = RubyCommand.Argument(name: "include", value: include, type: nil) - let excludeArg = RubyCommand.Argument(name: "exclude", value: exclude, type: nil) - let array: [RubyCommand.Argument?] = [pathArg, - outputPathArg, - verboseArg, - passwordArg, - symlinksArg, - includeArg, - excludeArg] - let args: [RubyCommand.Argument] = array - .filter { $0?.value != nil } - .compactMap { $0 } - let command = RubyCommand(commandID: "", methodName: "zip", className: nil, args: args) - return runner.executeCommand(command) -} - -/// These are all the parsing functions needed to transform our data into the expected types -func parseArray(fromString: String, function: String = #function) -> [String] { - verbose(message: "parsing an Array from data: \(fromString), from function: \(function)") - let potentialArray: String - if fromString.count < 2 { - potentialArray = "[\(fromString)]" - } else { - potentialArray = fromString - } - return try! JSONSerialization.jsonObject(with: potentialArray.data(using: .utf8)!, options: []) as! [String] -} - -func parseDictionary(fromString: String, function: String = #function) -> [String: String] { - return parseDictionaryHelper(fromString: fromString, function: function) as! [String: String] -} - -func parseDictionary(fromString: String, function: String = #function) -> [String: Any] { - return parseDictionaryHelper(fromString: fromString, function: function) -} - -func parseDictionaryHelper(fromString: String, function: String = #function) -> [String: Any] { - verbose(message: "parsing an Array from data: \(fromString), from function: \(function)") - let potentialDictionary: String - if fromString.count < 2 { - verbose(message: "Dictionary value too small: \(fromString), from function: \(function)") - potentialDictionary = "{}" - } else { - potentialDictionary = fromString - } - return try! JSONSerialization.jsonObject(with: potentialDictionary.data(using: .utf8)!, options: []) as! [String: Any] -} - -func parseBool(fromString: String, function: String = #function) -> Bool { - verbose(message: "parsing a Bool from data: \(fromString), from function: \(function)") - return NSString(string: fromString.trimmingCharacters(in: .punctuationCharacters)).boolValue -} - -func parseInt(fromString: String, function: String = #function) -> Int { - verbose(message: "parsing an Int from data: \(fromString), from function: \(function)") - return NSString(string: fromString.trimmingCharacters(in: .punctuationCharacters)).integerValue -} - -public let deliverfile: Deliverfile = .init() -public let gymfile: Gymfile = .init() -public let matchfile: Matchfile = .init() -public let precheckfile: Precheckfile = .init() -public let scanfile: Scanfile = .init() -public let screengrabfile: Screengrabfile = .init() -public let snapshotfile: Snapshotfile = .init() - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.198] diff --git a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.pbxproj b/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.pbxproj deleted file mode 100644 index 6a28b8232..000000000 --- a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,445 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 0311E387230AC1B20060BB5C /* Plugins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0311E386230AC1B20060BB5C /* Plugins.swift */; }; - 0311E38B230AC9490060BB5C /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0311E38A230AC9490060BB5C /* Actions.swift */; }; - 1257253924B7992C00E04FA3 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1257253824B7992B00E04FA3 /* main.swift */; }; - 3593732A0C736B4F7477241F /* OptionalConfigValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D29187D0D39921A55D419B3 /* OptionalConfigValue.swift */; }; - B302067B1F5E3E9000DE6EBD /* SnapshotfileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30206741F5E3E9000DE6EBD /* SnapshotfileProtocol.swift */; }; - B302067C1F5E3E9000DE6EBD /* GymfileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30206751F5E3E9000DE6EBD /* GymfileProtocol.swift */; }; - B302067D1F5E3E9000DE6EBD /* MatchfileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30206761F5E3E9000DE6EBD /* MatchfileProtocol.swift */; }; - B302067E1F5E3E9000DE6EBD /* PrecheckfileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30206771F5E3E9000DE6EBD /* PrecheckfileProtocol.swift */; }; - B302067F1F5E3E9000DE6EBD /* ScanfileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30206781F5E3E9000DE6EBD /* ScanfileProtocol.swift */; }; - B30206801F5E3E9000DE6EBD /* ScreengrabfileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30206791F5E3E9000DE6EBD /* ScreengrabfileProtocol.swift */; }; - B30206811F5E3E9000DE6EBD /* DeliverfileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B302067A1F5E3E9000DE6EBD /* DeliverfileProtocol.swift */; }; - B3BA65A61F5A269100B34850 /* Fastlane.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA659D1F5A269100B34850 /* Fastlane.swift */; }; - B3BA65A71F5A269100B34850 /* LaneFileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA659E1F5A269100B34850 /* LaneFileProtocol.swift */; }; - B3BA65A91F5A269100B34850 /* RubyCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA65A01F5A269100B34850 /* RubyCommand.swift */; }; - B3BA65AA1F5A269100B34850 /* Runner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA65A11F5A269100B34850 /* Runner.swift */; }; - B3BA65AB1F5A269100B34850 /* SocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA65A21F5A269100B34850 /* SocketClient.swift */; }; - B3BA65AC1F5A269100B34850 /* SocketClientDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA65A31F5A269100B34850 /* SocketClientDelegateProtocol.swift */; }; - B3BA65AD1F5A269100B34850 /* SocketResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA65A41F5A269100B34850 /* SocketResponse.swift */; }; - B3BA65AF1F5A2D5C00B34850 /* RunnerArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BA65AE1F5A2D5C00B34850 /* RunnerArgument.swift */; }; - BD87876253B404EE75BD2547 /* FastlaneRunner in FastlaneRunnerCopySigned */ = {isa = PBXBuildFile; fileRef = D556D6A91F6A08F5003108E3 /* FastlaneRunner */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - D55B28C31F6C588300DC42C5 /* Deliverfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B28BC1F6C588300DC42C5 /* Deliverfile.swift */; }; - D55B28C41F6C588300DC42C5 /* Gymfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B28BD1F6C588300DC42C5 /* Gymfile.swift */; }; - D55B28C51F6C588300DC42C5 /* Matchfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B28BE1F6C588300DC42C5 /* Matchfile.swift */; }; - D55B28C61F6C588300DC42C5 /* Precheckfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B28BF1F6C588300DC42C5 /* Precheckfile.swift */; }; - D55B28C71F6C588300DC42C5 /* Scanfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B28C01F6C588300DC42C5 /* Scanfile.swift */; }; - D55B28C81F6C588300DC42C5 /* Screengrabfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B28C11F6C588300DC42C5 /* Screengrabfile.swift */; }; - D55B28C91F6C588300DC42C5 /* Snapshotfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B28C21F6C588300DC42C5 /* Snapshotfile.swift */; }; - D5A7C48F1F7C4DAF00A91DE6 /* Appfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A7C48D1F7C4DAF00A91DE6 /* Appfile.swift */; }; - D5A7C4901F7C4DAF00A91DE6 /* Fastfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A7C48E1F7C4DAF00A91DE6 /* Fastfile.swift */; }; - D5B8A5B31FFDC49E00536B24 /* ControlCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B8A5B21FFDC49D00536B24 /* ControlCommand.swift */; }; - D5BAFD121F7DAAFC0030B324 /* ArgumentProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BAFD111F7DAAFC0030B324 /* ArgumentProcessor.swift */; }; - D5D1DE991FFEE8EA00502A00 /* RubyCommandable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D1DE981FFEE8E900502A00 /* RubyCommandable.swift */; }; - E5F819452AE32E8BA4E29811 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F752C072DD1434CB0290C9A /* Atomic.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 59DC705C6F84F39290AFE2C5 /* FastlaneRunnerCopySigned */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = $SRCROOT/../..; - dstSubfolderSpec = 0; - files = ( - BD87876253B404EE75BD2547 /* FastlaneRunner in FastlaneRunnerCopySigned */, - ); - name = FastlaneRunnerCopySigned; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 0311E386230AC1B20060BB5C /* Plugins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Plugins.swift; path = ../Plugins.swift; sourceTree = ""; }; - 0311E38A230AC9490060BB5C /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Actions.swift; path = ../Actions.swift; sourceTree = ""; }; - 1257253824B7992B00E04FA3 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = ../main.swift; sourceTree = ""; }; - 1D29187D0D39921A55D419B3 /* OptionalConfigValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OptionalConfigValue.swift; path = ../OptionalConfigValue.swift; sourceTree = ""; }; - 7F752C072DD1434CB0290C9A /* Atomic.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Atomic.swift; path = ../Atomic.swift; sourceTree = ""; }; - B30206741F5E3E9000DE6EBD /* SnapshotfileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotfileProtocol.swift; path = ../SnapshotfileProtocol.swift; sourceTree = ""; }; - B30206751F5E3E9000DE6EBD /* GymfileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GymfileProtocol.swift; path = ../GymfileProtocol.swift; sourceTree = ""; }; - B30206761F5E3E9000DE6EBD /* MatchfileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MatchfileProtocol.swift; path = ../MatchfileProtocol.swift; sourceTree = ""; }; - B30206771F5E3E9000DE6EBD /* PrecheckfileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PrecheckfileProtocol.swift; path = ../PrecheckfileProtocol.swift; sourceTree = ""; }; - B30206781F5E3E9000DE6EBD /* ScanfileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScanfileProtocol.swift; path = ../ScanfileProtocol.swift; sourceTree = ""; }; - B30206791F5E3E9000DE6EBD /* ScreengrabfileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreengrabfileProtocol.swift; path = ../ScreengrabfileProtocol.swift; sourceTree = ""; }; - B302067A1F5E3E9000DE6EBD /* DeliverfileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DeliverfileProtocol.swift; path = ../DeliverfileProtocol.swift; sourceTree = ""; }; - B3144C072005533400470AFE /* README.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.txt; sourceTree = ""; }; - B3144C08200553C800470AFE /* README.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.txt; sourceTree = ""; }; - B3144C09200553D400470AFE /* README.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.txt; sourceTree = ""; }; - B3144C0A200553DC00470AFE /* README.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.txt; sourceTree = ""; }; - B3BA659D1F5A269100B34850 /* Fastlane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Fastlane.swift; path = ../Fastlane.swift; sourceTree = ""; }; - B3BA659E1F5A269100B34850 /* LaneFileProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LaneFileProtocol.swift; path = ../LaneFileProtocol.swift; sourceTree = ""; }; - B3BA65A01F5A269100B34850 /* RubyCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RubyCommand.swift; path = ../RubyCommand.swift; sourceTree = ""; }; - B3BA65A11F5A269100B34850 /* Runner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Runner.swift; path = ../Runner.swift; sourceTree = ""; }; - B3BA65A21F5A269100B34850 /* SocketClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketClient.swift; path = ../SocketClient.swift; sourceTree = ""; }; - B3BA65A31F5A269100B34850 /* SocketClientDelegateProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketClientDelegateProtocol.swift; path = ../SocketClientDelegateProtocol.swift; sourceTree = ""; }; - B3BA65A41F5A269100B34850 /* SocketResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketResponse.swift; path = ../SocketResponse.swift; sourceTree = ""; }; - B3BA65AE1F5A2D5C00B34850 /* RunnerArgument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RunnerArgument.swift; path = ../RunnerArgument.swift; sourceTree = ""; }; - D556D6A91F6A08F5003108E3 /* FastlaneRunner */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = FastlaneRunner; sourceTree = BUILT_PRODUCTS_DIR; }; - D55B28BC1F6C588300DC42C5 /* Deliverfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Deliverfile.swift; path = ../Deliverfile.swift; sourceTree = ""; }; - D55B28BD1F6C588300DC42C5 /* Gymfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Gymfile.swift; path = ../Gymfile.swift; sourceTree = ""; }; - D55B28BE1F6C588300DC42C5 /* Matchfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Matchfile.swift; path = ../Matchfile.swift; sourceTree = ""; }; - D55B28BF1F6C588300DC42C5 /* Precheckfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Precheckfile.swift; path = ../Precheckfile.swift; sourceTree = ""; }; - D55B28C01F6C588300DC42C5 /* Scanfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Scanfile.swift; path = ../Scanfile.swift; sourceTree = ""; }; - D55B28C11F6C588300DC42C5 /* Screengrabfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Screengrabfile.swift; path = ../Screengrabfile.swift; sourceTree = ""; }; - D55B28C21F6C588300DC42C5 /* Snapshotfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Snapshotfile.swift; path = ../Snapshotfile.swift; sourceTree = ""; }; - D5A7C48D1F7C4DAF00A91DE6 /* Appfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Appfile.swift; path = ../../Appfile.swift; sourceTree = ""; }; - D5A7C48E1F7C4DAF00A91DE6 /* Fastfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Fastfile.swift; path = ../../Fastfile.swift; sourceTree = ""; }; - D5B8A5B21FFDC49D00536B24 /* ControlCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControlCommand.swift; path = ../ControlCommand.swift; sourceTree = ""; }; - D5BAFD111F7DAAFC0030B324 /* ArgumentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ArgumentProcessor.swift; path = ../ArgumentProcessor.swift; sourceTree = ""; }; - D5D1DE981FFEE8E900502A00 /* RubyCommandable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RubyCommandable.swift; path = ../RubyCommandable.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B33BAF541F51F8D90001A751 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - B33BAF4E1F51F8D90001A751 = { - isa = PBXGroup; - children = ( - B3BA65B01F5A324A00B34850 /* Fastlane Runner */, - D556D6A91F6A08F5003108E3 /* FastlaneRunner */, - ); - sourceTree = ""; - }; - B3BA65B01F5A324A00B34850 /* Fastlane Runner */ = { - isa = PBXGroup; - children = ( - B3BA65B21F5A327B00B34850 /* Autogenerated API */, - B3BA65B31F5A329800B34850 /* Fastfile Components */, - B3BA65B11F5A325E00B34850 /* Networking */, - D512BA011F7C7F40000D2137 /* Runner Code */, - D5A7C48D1F7C4DAF00A91DE6 /* Appfile.swift */, - D55B28BC1F6C588300DC42C5 /* Deliverfile.swift */, - D5A7C48E1F7C4DAF00A91DE6 /* Fastfile.swift */, - D55B28BD1F6C588300DC42C5 /* Gymfile.swift */, - D55B28BE1F6C588300DC42C5 /* Matchfile.swift */, - D55B28BF1F6C588300DC42C5 /* Precheckfile.swift */, - D55B28C01F6C588300DC42C5 /* Scanfile.swift */, - D55B28C11F6C588300DC42C5 /* Screengrabfile.swift */, - D55B28C21F6C588300DC42C5 /* Snapshotfile.swift */, - ); - name = "Fastlane Runner"; - sourceTree = ""; - }; - B3BA65B11F5A325E00B34850 /* Networking */ = { - isa = PBXGroup; - children = ( - B3144C072005533400470AFE /* README.txt */, - D5B8A5B21FFDC49D00536B24 /* ControlCommand.swift */, - B3BA65A01F5A269100B34850 /* RubyCommand.swift */, - D5D1DE981FFEE8E900502A00 /* RubyCommandable.swift */, - B3BA65A11F5A269100B34850 /* Runner.swift */, - B3BA65A21F5A269100B34850 /* SocketClient.swift */, - B3BA65A31F5A269100B34850 /* SocketClientDelegateProtocol.swift */, - B3BA65A41F5A269100B34850 /* SocketResponse.swift */, - 7F752C072DD1434CB0290C9A /* Atomic.swift */, - ); - name = Networking; - sourceTree = ""; - }; - B3BA65B21F5A327B00B34850 /* Autogenerated API */ = { - isa = PBXGroup; - children = ( - B3144C09200553D400470AFE /* README.txt */, - 0311E38A230AC9490060BB5C /* Actions.swift */, - B3BA659D1F5A269100B34850 /* Fastlane.swift */, - B302067A1F5E3E9000DE6EBD /* DeliverfileProtocol.swift */, - B30206751F5E3E9000DE6EBD /* GymfileProtocol.swift */, - B30206761F5E3E9000DE6EBD /* MatchfileProtocol.swift */, - 0311E386230AC1B20060BB5C /* Plugins.swift */, - B30206771F5E3E9000DE6EBD /* PrecheckfileProtocol.swift */, - B30206781F5E3E9000DE6EBD /* ScanfileProtocol.swift */, - B30206791F5E3E9000DE6EBD /* ScreengrabfileProtocol.swift */, - B30206741F5E3E9000DE6EBD /* SnapshotfileProtocol.swift */, - ); - name = "Autogenerated API"; - sourceTree = ""; - }; - B3BA65B31F5A329800B34850 /* Fastfile Components */ = { - isa = PBXGroup; - children = ( - B3144C08200553C800470AFE /* README.txt */, - B3BA659E1F5A269100B34850 /* LaneFileProtocol.swift */, - 1D29187D0D39921A55D419B3 /* OptionalConfigValue.swift */, - ); - name = "Fastfile Components"; - sourceTree = ""; - }; - D512BA011F7C7F40000D2137 /* Runner Code */ = { - isa = PBXGroup; - children = ( - 1257253824B7992B00E04FA3 /* main.swift */, - B3144C0A200553DC00470AFE /* README.txt */, - D5BAFD111F7DAAFC0030B324 /* ArgumentProcessor.swift */, - B3BA65AE1F5A2D5C00B34850 /* RunnerArgument.swift */, - ); - name = "Runner Code"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - B33BAF561F51F8D90001A751 /* FastlaneRunner */ = { - isa = PBXNativeTarget; - buildConfigurationList = B33BAF5E1F51F8D90001A751 /* Build configuration list for PBXNativeTarget "FastlaneRunner" */; - buildPhases = ( - B33BAF531F51F8D90001A751 /* Sources */, - B33BAF541F51F8D90001A751 /* Frameworks */, - 59DC705C6F84F39290AFE2C5 /* FastlaneRunnerCopySigned */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = FastlaneRunner; - productName = FastlaneSwiftRunner; - productReference = D556D6A91F6A08F5003108E3 /* FastlaneRunner */; - productType = "com.apple.product-type.tool"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - B33BAF4F1F51F8D90001A751 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0830; - LastUpgradeCheck = 0900; - ORGANIZATIONNAME = "Joshua Liebowitz"; - TargetAttributes = { - B33BAF561F51F8D90001A751 = { - CreatedOnToolsVersion = 8.3.3; - LastSwiftMigration = 0900; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = B33BAF521F51F8D90001A751 /* Build configuration list for PBXProject "FastlaneSwiftRunner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - English, - en, - ); - mainGroup = B33BAF4E1F51F8D90001A751; - productRefGroup = B33BAF4E1F51F8D90001A751; - projectDirPath = ""; - projectRoot = ""; - targets = ( - B33BAF561F51F8D90001A751 /* FastlaneRunner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXSourcesBuildPhase section */ - B33BAF531F51F8D90001A751 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B3BA65A91F5A269100B34850 /* RubyCommand.swift in Sources */, - D5D1DE991FFEE8EA00502A00 /* RubyCommandable.swift in Sources */, - D55B28C41F6C588300DC42C5 /* Gymfile.swift in Sources */, - B302067D1F5E3E9000DE6EBD /* MatchfileProtocol.swift in Sources */, - B3BA65AC1F5A269100B34850 /* SocketClientDelegateProtocol.swift in Sources */, - B3BA65A71F5A269100B34850 /* LaneFileProtocol.swift in Sources */, - D55B28C61F6C588300DC42C5 /* Precheckfile.swift in Sources */, - B302067F1F5E3E9000DE6EBD /* ScanfileProtocol.swift in Sources */, - D55B28C51F6C588300DC42C5 /* Matchfile.swift in Sources */, - B30206801F5E3E9000DE6EBD /* ScreengrabfileProtocol.swift in Sources */, - D5BAFD121F7DAAFC0030B324 /* ArgumentProcessor.swift in Sources */, - B302067C1F5E3E9000DE6EBD /* GymfileProtocol.swift in Sources */, - B302067B1F5E3E9000DE6EBD /* SnapshotfileProtocol.swift in Sources */, - D55B28C31F6C588300DC42C5 /* Deliverfile.swift in Sources */, - D5A7C4901F7C4DAF00A91DE6 /* Fastfile.swift in Sources */, - 0311E38B230AC9490060BB5C /* Actions.swift in Sources */, - D5A7C48F1F7C4DAF00A91DE6 /* Appfile.swift in Sources */, - B3BA65AB1F5A269100B34850 /* SocketClient.swift in Sources */, - B30206811F5E3E9000DE6EBD /* DeliverfileProtocol.swift in Sources */, - B3BA65AA1F5A269100B34850 /* Runner.swift in Sources */, - B3BA65AF1F5A2D5C00B34850 /* RunnerArgument.swift in Sources */, - D5B8A5B31FFDC49E00536B24 /* ControlCommand.swift in Sources */, - 1257253924B7992C00E04FA3 /* main.swift in Sources */, - B302067E1F5E3E9000DE6EBD /* PrecheckfileProtocol.swift in Sources */, - B3BA65AD1F5A269100B34850 /* SocketResponse.swift in Sources */, - D55B28C71F6C588300DC42C5 /* Scanfile.swift in Sources */, - 0311E387230AC1B20060BB5C /* Plugins.swift in Sources */, - D55B28C91F6C588300DC42C5 /* Snapshotfile.swift in Sources */, - B3BA65A61F5A269100B34850 /* Fastlane.swift in Sources */, - D55B28C81F6C588300DC42C5 /* Screengrabfile.swift in Sources */, - 3593732A0C736B4F7477241F /* OptionalConfigValue.swift in Sources */, - E5F819452AE32E8BA4E29811 /* Atomic.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - B33BAF5C1F51F8D90001A751 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - }; - name = Debug; - }; - B33BAF5D1F51F8D90001A751 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - }; - name = Release; - }; - B33BAF5F1F51F8D90001A751 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "-"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.12; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - }; - name = Debug; - }; - B33BAF601F51F8D90001A751 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "-"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.12; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - B33BAF521F51F8D90001A751 /* Build configuration list for PBXProject "FastlaneSwiftRunner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B33BAF5C1F51F8D90001A751 /* Debug */, - B33BAF5D1F51F8D90001A751 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B33BAF5E1F51F8D90001A751 /* Build configuration list for PBXNativeTarget "FastlaneRunner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B33BAF5F1F51F8D90001A751 /* Debug */, - B33BAF601F51F8D90001A751 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = B33BAF4F1F51F8D90001A751 /* Project object */; -} diff --git a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 44be89b1f..000000000 --- a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme b/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme deleted file mode 100644 index 0ad7a489d..000000000 --- a/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fastlane/swift/FastlaneSwiftRunner/README.txt b/fastlane/swift/FastlaneSwiftRunner/README.txt deleted file mode 100644 index 5fb55ccd2..000000000 --- a/fastlane/swift/FastlaneSwiftRunner/README.txt +++ /dev/null @@ -1,10 +0,0 @@ -Don't modify the structure of this group including but not limited to: -- renaming this group -- adding sub groups -- removing sub groups -- adding new files -- removing files - -If you modify anything in this folder, future fastlane upgrades may not be able to be applied automatically. - -If you need to add new groups, please add them at the root of the "Fastlane Runner" group. diff --git a/fastlane/swift/Gymfile.swift b/fastlane/swift/Gymfile.swift deleted file mode 100644 index 4b7ab3fc1..000000000 --- a/fastlane/swift/Gymfile.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Gymfile.swift -// Copyright (c) 2021 FastlaneTools - -// This class is automatically included in FastlaneRunner during build - -// This autogenerated file will be overwritten or replaced during build time, or when you initialize `gym` -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -public class Gymfile: GymfileProtocol { - // If you want to enable `gym`, run `fastlane gym init` - // After, this file will be replaced with a custom implementation that contains values you supplied - // during the `init` process, and you won't see this message -} - -// Generated with fastlane 2.178.0 diff --git a/fastlane/swift/GymfileProtocol.swift b/fastlane/swift/GymfileProtocol.swift deleted file mode 100644 index fe4614bf4..000000000 --- a/fastlane/swift/GymfileProtocol.swift +++ /dev/null @@ -1,395 +0,0 @@ -// GymfileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -public protocol GymfileProtocol: AnyObject { - /// Path to the workspace file - var workspace: String? { get } - - /// Path to the project file - var project: String? { get } - - /// The project's scheme. Make sure it's marked as `Shared` - var scheme: String? { get } - - /// Should the project be cleaned before building it? - var clean: Bool { get } - - /// The directory in which the ipa file should be stored in - var outputDirectory: String { get } - - /// The name of the resulting ipa file - var outputName: String? { get } - - /// App name to use in logfile name - var appName: String? { get } - - /// The configuration to use when building the app. Defaults to 'Release' - var configuration: String? { get } - - /// Hide all information that's not necessary while building - var silent: Bool { get } - - /// The name of the code signing identity to use. It has to match the name exactly. e.g. 'iPhone Distribution: SunApps GmbH' - var codesigningIdentity: String? { get } - - /// Should we skip packaging the ipa? - var skipPackageIpa: Bool { get } - - /// Should we skip packaging the pkg? - var skipPackagePkg: Bool { get } - - /// Should the ipa file include symbols? - var includeSymbols: Bool? { get } - - /// Should the ipa file include bitcode? - var includeBitcode: Bool? { get } - - /// Method used to export the archive. Valid values are: app-store, validation, ad-hoc, package, enterprise, development, developer-id and mac-application - var exportMethod: String? { get } - - /// Path to an export options plist or a hash with export options. Use 'xcodebuild -help' to print the full set of available options - var exportOptions: [String: Any]? { get } - - /// Pass additional arguments to xcodebuild for the package phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - var exportXcargs: String? { get } - - /// Export ipa from previously built xcarchive. Uses archive_path as source - var skipBuildArchive: Bool? { get } - - /// After building, don't archive, effectively not including -archivePath param - var skipArchive: Bool? { get } - - /// Build without codesigning - var skipCodesigning: Bool? { get } - - /// Platform to build when using a Catalyst enabled app. Valid values are: ios, macos - var catalystPlatform: String? { get } - - /// Full name of 3rd Party Mac Developer Installer or Developer ID Installer certificate. Example: `3rd Party Mac Developer Installer: Your Company (ABC1234XWYZ)` - var installerCertName: String? { get } - - /// The directory in which the archive should be stored in - var buildPath: String? { get } - - /// The path to the created archive - var archivePath: String? { get } - - /// The directory where built products and other derived data will go - var derivedDataPath: String? { get } - - /// Should an Xcode result bundle be generated in the output directory - var resultBundle: Bool { get } - - /// Path to the result bundle directory to create. Ignored if `result_bundle` if false - var resultBundlePath: String? { get } - - /// The directory where to store the build log - var buildlogPath: String { get } - - /// The SDK that should be used for building the application - var sdk: String? { get } - - /// The toolchain that should be used for building the application (e.g. com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a) - var toolchain: String? { get } - - /// Use a custom destination for building the app - var destination: String? { get } - - /// Optional: Sometimes you need to specify a team id when exporting the ipa file - var exportTeamId: String? { get } - - /// Pass additional arguments to xcodebuild for the build phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - var xcargs: String? { get } - - /// Use an extra XCCONFIG file to build your app - var xcconfig: String? { get } - - /// Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - var suppressXcodeOutput: Bool? { get } - - /// xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - var xcodebuildFormatter: String { get } - - /// Create a build timing summary - var buildTimingSummary: Bool { get } - - /// **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Disable xcpretty formatting of build output - var disableXcpretty: Bool? { get } - - /// Use the test (RSpec style) format for build output - var xcprettyTestFormat: Bool? { get } - - /// A custom xcpretty formatter to use - var xcprettyFormatter: String? { get } - - /// Have xcpretty create a JUnit-style XML report at the provided path - var xcprettyReportJunit: String? { get } - - /// Have xcpretty create a simple HTML report at the provided path - var xcprettyReportHtml: String? { get } - - /// Have xcpretty create a JSON compilation database at the provided path - var xcprettyReportJson: String? { get } - - /// Have xcpretty use unicode encoding when reporting builds - var xcprettyUtf: Bool? { get } - - /// Analyze the project build time and store the output in 'culprits.txt' file - var analyzeBuildTime: Bool? { get } - - /// Do not try to build a profile mapping from the xcodeproj. Match or a manually provided mapping should be used - var skipProfileDetection: Bool { get } - - /// Allows for override of the default `xcodebuild` command - var xcodebuildCommand: String { get } - - /// Sets a custom path for Swift Package Manager dependencies - var clonedSourcePackagesPath: String? { get } - - /// Sets a custom package cache path for Swift Package Manager dependencies - var packageCachePath: String? { get } - - /// Skips resolution of Swift Package Manager dependencies - var skipPackageDependenciesResolution: Bool { get } - - /// Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - var disablePackageAutomaticUpdates: Bool { get } - - /// Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - var skipPackageRepositoryFetches: Bool { get } - - /// Lets xcodebuild use system's scm configuration - var useSystemScm: Bool { get } - - /// Lets xcodebuild use a specified package authorization provider (keychain|netrc) - var packageAuthorizationProvider: String? { get } - - /// Generate AppStoreInfo.plist using swinfo for app-store exports - var generateAppstoreInfo: Bool { get } -} - -public extension GymfileProtocol { - var workspace: String? { - return nil - } - - var project: String? { - return nil - } - - var scheme: String? { - return nil - } - - var clean: Bool { - return false - } - - var outputDirectory: String { - return "." - } - - var outputName: String? { - return nil - } - - var appName: String? { - return nil - } - - var configuration: String? { - return nil - } - - var silent: Bool { - return false - } - - var codesigningIdentity: String? { - return nil - } - - var skipPackageIpa: Bool { - return false - } - - var skipPackagePkg: Bool { - return false - } - - var includeSymbols: Bool? { - return nil - } - - var includeBitcode: Bool? { - return nil - } - - var exportMethod: String? { - return nil - } - - var exportOptions: [String: Any]? { - return nil - } - - var exportXcargs: String? { - return nil - } - - var skipBuildArchive: Bool? { - return nil - } - - var skipArchive: Bool? { - return nil - } - - var skipCodesigning: Bool? { - return nil - } - - var catalystPlatform: String? { - return nil - } - - var installerCertName: String? { - return nil - } - - var buildPath: String? { - return nil - } - - var archivePath: String? { - return nil - } - - var derivedDataPath: String? { - return nil - } - - var resultBundle: Bool { - return false - } - - var resultBundlePath: String? { - return nil - } - - var buildlogPath: String { - return "~/Library/Logs/gym" - } - - var sdk: String? { - return nil - } - - var toolchain: String? { - return nil - } - - var destination: String? { - return nil - } - - var exportTeamId: String? { - return nil - } - - var xcargs: String? { - return nil - } - - var xcconfig: String? { - return nil - } - - var suppressXcodeOutput: Bool? { - return nil - } - - var xcodebuildFormatter: String { - return "xcbeautify" - } - - var buildTimingSummary: Bool { - return false - } - - var disableXcpretty: Bool? { - return nil - } - - var xcprettyTestFormat: Bool? { - return nil - } - - var xcprettyFormatter: String? { - return nil - } - - var xcprettyReportJunit: String? { - return nil - } - - var xcprettyReportHtml: String? { - return nil - } - - var xcprettyReportJson: String? { - return nil - } - - var xcprettyUtf: Bool? { - return nil - } - - var analyzeBuildTime: Bool? { - return nil - } - - var skipProfileDetection: Bool { - return false - } - - var xcodebuildCommand: String { - return "xcodebuild" - } - - var clonedSourcePackagesPath: String? { - return nil - } - - var packageCachePath: String? { - return nil - } - - var skipPackageDependenciesResolution: Bool { - return false - } - - var disablePackageAutomaticUpdates: Bool { - return false - } - - var skipPackageRepositoryFetches: Bool { - return false - } - - var useSystemScm: Bool { - return false - } - - var packageAuthorizationProvider: String? { - return nil - } - - var generateAppstoreInfo: Bool { - return false - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.148] diff --git a/fastlane/swift/LaneFileProtocol.swift b/fastlane/swift/LaneFileProtocol.swift deleted file mode 100644 index e40d4b231..000000000 --- a/fastlane/swift/LaneFileProtocol.swift +++ /dev/null @@ -1,161 +0,0 @@ -// LaneFileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -public protocol LaneFileProtocol: AnyObject { - var fastlaneVersion: String { get } - static func runLane(from fastfile: LaneFile?, named lane: String, with parameters: [String: String]) -> Bool - - func recordLaneDescriptions() - func beforeAll(with lane: String) - func afterAll(with lane: String) - func onError(currentLane: String, errorInfo: String, errorClass: String?, errorMessage: String?) -} - -public extension LaneFileProtocol { - var fastlaneVersion: String { - return "" - } // Defaults to "" because that means any is fine - func beforeAll(with _: String) {} // No-op by default - func afterAll(with _: String) {} // No-op by default - func recordLaneDescriptions() {} // No-op by default -} - -@objcMembers -open class LaneFile: NSObject, LaneFileProtocol { - private(set) static var fastfileInstance: LaneFile? - private static var onErrorCalled = Set() - - private static func trimLaneFromName(laneName: String) -> String { - return String(laneName.prefix(laneName.count - 4)) - } - - private static func trimLaneWithOptionsFromName(laneName: String) -> String { - return String(laneName.prefix(laneName.count - 12)) - } - - open func beforeAll(with _: String) {} - - open func afterAll(with _: String) {} - - open func onError(currentLane: String, errorInfo _: String, errorClass _: String?, errorMessage _: String?) { - LaneFile.onErrorCalled.insert(currentLane) - } - - private static var laneFunctionNames: [String] { - var lanes: [String] = [] - var methodCount: UInt32 = 0 - #if !SWIFT_PACKAGE - let methodList = class_copyMethodList(self, &methodCount) - #else - // In SPM we're calling this functions out of the scope of the normal binary that it's - // being built, so *self* in this scope would be the SPM executable instead of the Fastfile - // that we'd normally expect. - let methodList = class_copyMethodList(type(of: fastfileInstance!), &methodCount) - #endif - for i in 0 ..< Int(methodCount) { - let selName = sel_getName(method_getName(methodList![i])) - let name = String(cString: selName) - let lowercasedName = name.lowercased() - if lowercasedName.hasSuffix("lane") || lowercasedName.hasSuffix("lanewithoptions:") { - lanes.append(name) - } - } - return lanes - } - - public static var lanes: [String: String] { - var laneToMethodName: [String: String] = [:] - for name in laneFunctionNames { - let lowercasedName = name.lowercased() - if lowercasedName.hasSuffix("lane") { - laneToMethodName[lowercasedName] = name - let lowercasedNameNoLane = trimLaneFromName(laneName: lowercasedName) - laneToMethodName[lowercasedNameNoLane] = name - } else if lowercasedName.hasSuffix("lanewithoptions:") { - let lowercasedNameNoOptions = trimLaneWithOptionsFromName(laneName: lowercasedName) - laneToMethodName[lowercasedNameNoOptions] = name - let lowercasedNameNoLane = trimLaneFromName(laneName: lowercasedNameNoOptions) - laneToMethodName[lowercasedNameNoLane] = name - } - } - - return laneToMethodName - } - - public static func loadFastfile() { - if fastfileInstance == nil { - let fastfileType: AnyObject.Type = NSClassFromString(className())! - let fastfileAsNSObjectType: NSObject.Type = fastfileType as! NSObject.Type - let currentFastfileInstance: Fastfile? = fastfileAsNSObjectType.init() as? Fastfile - fastfileInstance = currentFastfileInstance - } - } - - public static func runLane(from fastfile: LaneFile?, named lane: String, with parameters: [String: String]) -> Bool { - log(message: "Running lane: \(lane)") - #if !SWIFT_PACKAGE - // When not in SPM environment, we load the Fastfile from its `className()`. - loadFastfile() - guard let fastfileInstance = fastfileInstance as? Fastfile else { - let message = "Unable to instantiate class named: \(className())" - log(message: message) - fatalError(message) - } - #else - // When in SPM environment, we can't load the Fastfile from its `className()` because the executable is in - // another scope, so `className()` won't be the expected Fastfile. Instead, we load the Fastfile as a Lanefile - // in a static way, by parameter. - guard let fastfileInstance = fastfile else { - log(message: "Found nil instance of fastfile") - preconditionFailure() - } - self.fastfileInstance = fastfileInstance - #endif - let currentLanes = lanes - let lowerCasedLaneRequested = lane.lowercased() - - guard let laneMethod = currentLanes[lowerCasedLaneRequested] else { - let laneNames = laneFunctionNames.map { laneFunctionName in - if laneFunctionName.hasSuffix("lanewithoptions:") { - return trimLaneWithOptionsFromName(laneName: laneFunctionName) - } else { - return trimLaneFromName(laneName: laneFunctionName) - } - }.joined(separator: ", ") - - let message = "[!] Could not find lane '\(lane)'. Available lanes: \(laneNames)" - log(message: message) - - let shutdownCommand = ControlCommand(commandType: .cancel(cancelReason: .clientError), message: message) - _ = runner.executeCommand(shutdownCommand) - return false - } - - // Call all methods that need to be called before we start calling lanes. - fastfileInstance.beforeAll(with: lane) - - // We need to catch all possible errors here and display a nice message. - _ = fastfileInstance.perform(NSSelectorFromString(laneMethod), with: parameters) - - // Call only on success. - if !LaneFile.onErrorCalled.contains(lane) { - fastfileInstance.afterAll(with: lane) - } - - log(message: "Done running lane: \(lane) 🚀") - return true - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/MainProcess.swift b/fastlane/swift/MainProcess.swift deleted file mode 100644 index 4a26b3ab3..000000000 --- a/fastlane/swift/MainProcess.swift +++ /dev/null @@ -1,79 +0,0 @@ -// MainProcess.swift -// Copyright (c) 2021 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation -#if canImport(SwiftShell) - import SwiftShell -#endif - -let argumentProcessor = ArgumentProcessor(args: CommandLine.arguments) -let timeout = argumentProcessor.commandTimeout - -class MainProcess { - var doneRunningLane = false - var thread: Thread! - #if SWIFT_PACKAGE - var lastPrintDate = Date.distantFuture - var timeBetweenPrints = Int.min - var rubySocketCommand: AsyncCommand! - #endif - - @objc func connectToFastlaneAndRunLane(_ fastfile: LaneFile?) { - runner.startSocketThread(port: argumentProcessor.port) - - let completedRun = Fastfile.runLane(from: fastfile, named: argumentProcessor.currentLane, with: argumentProcessor.laneParameters()) - if completedRun { - runner.disconnectFromFastlaneProcess() - } - - doneRunningLane = true - } - - func startFastlaneThread(with fastFile: LaneFile?) { - #if !SWIFT_PACKAGE - thread = Thread(target: self, selector: #selector(connectToFastlaneAndRunLane), object: nil) - #else - thread = Thread(target: self, selector: #selector(connectToFastlaneAndRunLane), object: fastFile) - #endif - thread.name = "worker thread" - #if SWIFT_PACKAGE - let PATH = run("/bin/bash", "-c", "-l", "eval $(/usr/libexec/path_helper -s) ; echo $PATH").stdout - main.env["PATH"] = PATH - let path = main.run(bash: "which fastlane").stdout - let pids = main.run("lsof", "-t", "-i", ":2000").stdout.split(separator: "\n") - pids.forEach { main.run("kill", "-9", $0) } - rubySocketCommand = main.runAsync(path, "socket_server", "-c", "1200") - lastPrintDate = Date() - rubySocketCommand.stderror.onStringOutput { print($0) } - rubySocketCommand.stdout.onStringOutput { stdout in - print(stdout) - self.timeBetweenPrints = Int(self.lastPrintDate.timeIntervalSinceNow) - } - - // swiftformat:disable:next redundantSelf - _ = Runner.waitWithPolling(self.timeBetweenPrints, toEventually: { $0 > 5 }, timeout: 10) - thread.start() - #endif - } -} - -public class Main { - let process = MainProcess() - - public init() {} - - public func run(with fastFile: LaneFile?) { - process.startFastlaneThread(with: fastFile) - - while !process.doneRunningLane, RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date(timeIntervalSinceNow: 2)) { - // no op - } - } -} diff --git a/fastlane/swift/Matchfile.swift b/fastlane/swift/Matchfile.swift deleted file mode 100644 index 105f32733..000000000 --- a/fastlane/swift/Matchfile.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Matchfile.swift -// Copyright (c) 2021 FastlaneTools - -// This class is automatically included in FastlaneRunner during build - -// This autogenerated file will be overwritten or replaced during build time, or when you initialize `match` -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -public class Matchfile: MatchfileProtocol { - // If you want to enable `match`, run `fastlane match init` - // After, this file will be replaced with a custom implementation that contains values you supplied - // during the `init` process, and you won't see this message -} - -// Generated with fastlane 2.178.0 diff --git a/fastlane/swift/MatchfileProtocol.swift b/fastlane/swift/MatchfileProtocol.swift deleted file mode 100644 index e0c3be080..000000000 --- a/fastlane/swift/MatchfileProtocol.swift +++ /dev/null @@ -1,409 +0,0 @@ -// MatchfileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -public protocol MatchfileProtocol: AnyObject { - /// Define the profile type, can be appstore, adhoc, development, enterprise, developer_id, mac_installer_distribution, developer_id_installer - var type: String { get } - - /// Create additional cert types needed for macOS installers (valid values: mac_installer_distribution, developer_id_installer) - var additionalCertTypes: [String]? { get } - - /// Only fetch existing certificates and profiles, don't generate new ones - var readonly: Bool { get } - - /// Create a certificate type for Xcode 11 and later (Apple Development or Apple Distribution) - var generateAppleCerts: Bool { get } - - /// Skip syncing provisioning profiles - var skipProvisioningProfiles: Bool { get } - - /// The bundle identifier(s) of your app (comma-separated string or array of strings) - var appIdentifier: [String] { get } - - /// Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - var apiKeyPath: String? { get } - - /// Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - var apiKey: [String: Any]? { get } - - /// Your Apple ID Username - var username: String? { get } - - /// The ID of your Developer Portal team if you're in multiple teams - var teamId: String? { get } - - /// The name of your Developer Portal team if you're in multiple teams - var teamName: String? { get } - - /// Define where you want to store your certificates - var storageMode: String { get } - - /// URL to the git repo containing all the certificates - var gitUrl: String { get } - - /// Specific git branch to use - var gitBranch: String { get } - - /// git user full name to commit - var gitFullName: String? { get } - - /// git user email to commit - var gitUserEmail: String? { get } - - /// Make a shallow clone of the repository (truncate the history to 1 revision) - var shallowClone: Bool { get } - - /// Clone just the branch specified, instead of the whole repo. This requires that the branch already exists. Otherwise the command will fail - var cloneBranchDirectly: Bool { get } - - /// Use a basic authorization header to access the git repo (e.g.: access via HTTPS, GitHub Actions, etc), usually a string in Base64 - var gitBasicAuthorization: String? { get } - - /// Use a bearer authorization header to access the git repo (e.g.: access to an Azure DevOps repository), usually a string in Base64 - var gitBearerAuthorization: String? { get } - - /// Use a private key to access the git repo (e.g.: access to GitHub repository via Deploy keys), usually a id_rsa named file or the contents hereof - var gitPrivateKey: String? { get } - - /// Name of the Google Cloud Storage bucket to use - var googleCloudBucketName: String? { get } - - /// Path to the gc_keys.json file - var googleCloudKeysFile: String? { get } - - /// ID of the Google Cloud project to use for authentication - var googleCloudProjectId: String? { get } - - /// Skips confirming to use the system google account - var skipGoogleCloudAccountConfirmation: Bool { get } - - /// Name of the S3 region - var s3Region: String? { get } - - /// S3 access key - var s3AccessKey: String? { get } - - /// S3 secret access key - var s3SecretAccessKey: String? { get } - - /// S3 session token - var s3SessionToken: String? { get } - - /// Name of the S3 bucket - var s3Bucket: String? { get } - - /// Prefix to be used on all objects uploaded to S3 - var s3ObjectPrefix: String? { get } - - /// Skip encryption of all objects uploaded to S3. WARNING: only enable this on S3 buckets with sufficiently restricted permissions and server-side encryption enabled. See https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingEncryption.html - var s3SkipEncryption: Bool { get } - - /// GitLab Project Path (i.e. 'gitlab-org/gitlab') - var gitlabProject: String? { get } - - /// GitLab Host (i.e. 'https://gitlab.com') - var gitlabHost: String { get } - - /// GitLab CI_JOB_TOKEN - var jobToken: String? { get } - - /// GitLab Access Token - var privateToken: String? { get } - - /// Keychain the items should be imported to - var keychainName: String { get } - - /// This might be required the first time you access certificates on a new mac. For the login/default keychain this is your macOS account password - var keychainPassword: String? { get } - - /// Renew the provisioning profiles every time you run match - var force: Bool { get } - - /// Renew the provisioning profiles if the device count on the developer portal has changed. Ignored for profile types 'appstore' and 'developer_id' - var forceForNewDevices: Bool { get } - - /// Include Apple Silicon Mac devices in provisioning profiles for iOS/iPadOS apps - var includeMacInProfiles: Bool { get } - - /// Include all matching certificates in the provisioning profile. Works only for the 'development' provisioning profile type - var includeAllCertificates: Bool { get } - - /// Select certificate by id. Useful if multiple certificates are stored in one place - var certificateId: String? { get } - - /// Renew the provisioning profiles if the certificate count on the developer portal has changed. Works only for the 'development' provisioning profile type. Requires 'include_all_certificates' option to be 'true' - var forceForNewCertificates: Bool { get } - - /// Disables confirmation prompts during nuke, answering them with yes - var skipConfirmation: Bool { get } - - /// Remove certs from repository during nuke without revoking them on the developer portal - var safeRemoveCerts: Bool { get } - - /// Skip generation of a README.md for the created git repository - var skipDocs: Bool { get } - - /// Set the provisioning profile's platform to work with (i.e. ios, tvos, macos, catalyst) - var platform: String { get } - - /// Enable this if you have the Mac Catalyst capability enabled and your project was created with Xcode 11.3 or earlier. Prepends 'maccatalyst.' to the app identifier for the provisioning profile mapping - var deriveCatalystAppIdentifier: Bool { get } - - /// **DEPRECATED!** Removed since May 2025 on App Store Connect API OpenAPI v3.8.0 - Learn more: https://docs.fastlane.tools/actions/match/#managed-capabilities - The name of provisioning profile template. If the developer account has provisioning profile templates (aka: custom entitlements), the template name can be found by inspecting the Entitlements drop-down while creating/editing a provisioning profile (e.g. "Apple Pay Pass Suppression Development") - var templateName: String? { get } - - /// A custom name for the provisioning profile. This will replace the default provisioning profile name if specified - var profileName: String? { get } - - /// Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first - var failOnNameTaken: Bool { get } - - /// Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action - var skipCertificateMatching: Bool { get } - - /// Path in which to export certificates, key and profile - var outputPath: String? { get } - - /// Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing - var skipSetPartitionList: Bool { get } - - /// Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions - var forceLegacyEncryption: Bool { get } - - /// Print out extra information and all commands - var verbose: Bool { get } -} - -public extension MatchfileProtocol { - var type: String { - return "development" - } - - var additionalCertTypes: [String]? { - return nil - } - - var readonly: Bool { - return false - } - - var generateAppleCerts: Bool { - return true - } - - var skipProvisioningProfiles: Bool { - return false - } - - var appIdentifier: [String] { - return [] - } - - var apiKeyPath: String? { - return nil - } - - var apiKey: [String: Any]? { - return nil - } - - var username: String? { - return nil - } - - var teamId: String? { - return nil - } - - var teamName: String? { - return nil - } - - var storageMode: String { - return "git" - } - - var gitUrl: String { - return "" - } - - var gitBranch: String { - return "master" - } - - var gitFullName: String? { - return nil - } - - var gitUserEmail: String? { - return nil - } - - var shallowClone: Bool { - return false - } - - var cloneBranchDirectly: Bool { - return false - } - - var gitBasicAuthorization: String? { - return nil - } - - var gitBearerAuthorization: String? { - return nil - } - - var gitPrivateKey: String? { - return nil - } - - var googleCloudBucketName: String? { - return nil - } - - var googleCloudKeysFile: String? { - return nil - } - - var googleCloudProjectId: String? { - return nil - } - - var skipGoogleCloudAccountConfirmation: Bool { - return false - } - - var s3Region: String? { - return nil - } - - var s3AccessKey: String? { - return nil - } - - var s3SecretAccessKey: String? { - return nil - } - - var s3SessionToken: String? { - return nil - } - - var s3Bucket: String? { - return nil - } - - var s3ObjectPrefix: String? { - return nil - } - - var s3SkipEncryption: Bool { - return false - } - - var gitlabProject: String? { - return nil - } - - var gitlabHost: String { - return "https://gitlab.com" - } - - var jobToken: String? { - return nil - } - - var privateToken: String? { - return nil - } - - var keychainName: String { - return "login.keychain" - } - - var keychainPassword: String? { - return nil - } - - var force: Bool { - return false - } - - var forceForNewDevices: Bool { - return false - } - - var includeMacInProfiles: Bool { - return false - } - - var includeAllCertificates: Bool { - return false - } - - var certificateId: String? { - return nil - } - - var forceForNewCertificates: Bool { - return false - } - - var skipConfirmation: Bool { - return false - } - - var safeRemoveCerts: Bool { - return false - } - - var skipDocs: Bool { - return false - } - - var platform: String { - return "ios" - } - - var deriveCatalystAppIdentifier: Bool { - return false - } - - var templateName: String? { - return nil - } - - var profileName: String? { - return nil - } - - var failOnNameTaken: Bool { - return false - } - - var skipCertificateMatching: Bool { - return false - } - - var outputPath: String? { - return nil - } - - var skipSetPartitionList: Bool { - return false - } - - var forceLegacyEncryption: Bool { - return false - } - - var verbose: Bool { - return false - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.142] diff --git a/fastlane/swift/OptionalConfigValue.swift b/fastlane/swift/OptionalConfigValue.swift deleted file mode 100644 index d1eb9297a..000000000 --- a/fastlane/swift/OptionalConfigValue.swift +++ /dev/null @@ -1,101 +0,0 @@ -// OptionalConfigValue.swift -// Copyright (c) 2021 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -public enum OptionalConfigValue { - case fastlaneDefault(T) - case userDefined(T) - case `nil` - - func asRubyArgument(name: String, type: RubyCommand.Argument.ArgType? = nil) -> RubyCommand.Argument? { - if case let .userDefined(value) = self { - return RubyCommand.Argument(name: name, value: value, type: type) - } - return nil - } -} - -extension OptionalConfigValue: ExpressibleByUnicodeScalarLiteral where T == String? { - public typealias UnicodeScalarLiteralType = String - - public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - self = .userDefined(value) - } -} - -extension OptionalConfigValue: ExpressibleByExtendedGraphemeClusterLiteral where T == String? { - public typealias ExtendedGraphemeClusterLiteralType = String - - public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - self = .userDefined(value) - } -} - -extension OptionalConfigValue: ExpressibleByStringLiteral where T == String? { - public typealias StringLiteralType = String - - public init(stringLiteral value: StringLiteralType) { - self = .userDefined(value) - } -} - -extension OptionalConfigValue: ExpressibleByStringInterpolation where T == String? {} - -extension OptionalConfigValue: ExpressibleByNilLiteral { - public init(nilLiteral _: ()) { - self = .nil - } -} - -extension OptionalConfigValue: ExpressibleByIntegerLiteral where T == Int? { - public typealias IntegerLiteralType = Int - - public init(integerLiteral value: IntegerLiteralType) { - self = .userDefined(value) - } -} - -extension OptionalConfigValue: ExpressibleByArrayLiteral where T == [String] { - public typealias ArrayLiteralElement = String - - public init(arrayLiteral elements: ArrayLiteralElement...) { - self = .userDefined(elements) - } -} - -extension OptionalConfigValue: ExpressibleByFloatLiteral where T == Float { - public typealias FloatLiteralType = Float - - public init(floatLiteral value: FloatLiteralType) { - self = .userDefined(value) - } -} - -extension OptionalConfigValue: ExpressibleByBooleanLiteral where T == Bool { - public typealias BooleanLiteralType = Bool - - public init(booleanLiteral value: BooleanLiteralType) { - self = .userDefined(value) - } -} - -extension OptionalConfigValue: ExpressibleByDictionaryLiteral where T == [String: Any] { - public typealias Key = String - public typealias Value = Any - - public init(dictionaryLiteral elements: (Key, Value)...) { - var dict: [Key: Value] = [:] - elements.forEach { - dict[$0.0] = $0.1 - } - self = .userDefined(dict) - } -} diff --git a/fastlane/swift/Plugins.swift b/fastlane/swift/Plugins.swift deleted file mode 100644 index 16b631fae..000000000 --- a/fastlane/swift/Plugins.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Plugins.swift -// Copyright (c) 2026 FastlaneTools - -// This autogenerated file will be overwritten or replaced when installing/updating plugins or running "fastlane generate_swift" -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.56] diff --git a/fastlane/swift/Precheckfile.swift b/fastlane/swift/Precheckfile.swift deleted file mode 100644 index 1a8683b31..000000000 --- a/fastlane/swift/Precheckfile.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Precheckfile.swift -// Copyright (c) 2021 FastlaneTools - -// This class is automatically included in FastlaneRunner during build - -// This autogenerated file will be overwritten or replaced during build time, or when you initialize `precheck` -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -public class Precheckfile: PrecheckfileProtocol { - // If you want to enable `precheck`, run `fastlane precheck init` - // After, this file will be replaced with a custom implementation that contains values you supplied - // during the `init` process, and you won't see this message -} - -// Generated with fastlane 2.178.0 diff --git a/fastlane/swift/PrecheckfileProtocol.swift b/fastlane/swift/PrecheckfileProtocol.swift deleted file mode 100644 index 935c5ca07..000000000 --- a/fastlane/swift/PrecheckfileProtocol.swift +++ /dev/null @@ -1,87 +0,0 @@ -// PrecheckfileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -public protocol PrecheckfileProtocol: AnyObject { - /// Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file) - var apiKeyPath: String? { get } - - /// Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option) - var apiKey: [String: Any]? { get } - - /// The bundle identifier of your app - var appIdentifier: String { get } - - /// Your Apple ID Username - var username: String? { get } - - /// The ID of your App Store Connect team if you're in multiple teams - var teamId: String? { get } - - /// The name of your App Store Connect team if you're in multiple teams - var teamName: String? { get } - - /// The platform to use (optional) - var platform: String { get } - - /// The default rule level unless otherwise configured - var defaultRuleLevel: String { get } - - /// Should check in-app purchases? - var includeInAppPurchases: Bool { get } - - /// Should force check live app? - var useLive: Bool { get } - - /// using text indicating that your IAP is free - var freeStuffInIap: String? { get } -} - -public extension PrecheckfileProtocol { - var apiKeyPath: String? { - return nil - } - - var apiKey: [String: Any]? { - return nil - } - - var appIdentifier: String { - return "" - } - - var username: String? { - return nil - } - - var teamId: String? { - return nil - } - - var teamName: String? { - return nil - } - - var platform: String { - return "ios" - } - - var defaultRuleLevel: String { - return "error" - } - - var includeInAppPurchases: Bool { - return true - } - - var useLive: Bool { - return false - } - - var freeStuffInIap: String? { - return nil - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.141] diff --git a/fastlane/swift/RubyCommand.swift b/fastlane/swift/RubyCommand.swift deleted file mode 100644 index 5aa35a653..000000000 --- a/fastlane/swift/RubyCommand.swift +++ /dev/null @@ -1,156 +0,0 @@ -// RubyCommand.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -struct RubyCommand: RubyCommandable { - var type: CommandType { - return .action - } - - struct Argument { - enum ArgType { - case stringClosure - - var typeString: String { - switch self { - case .stringClosure: - return "string_closure" // this should match when is in ruby's SocketServerActionCommandExecutor - } - } - } - - let name: String - let value: Any? - let type: ArgType? - - init(name: String, value: Any?, type: ArgType? = nil) { - self.name = name - self.value = value - self.type = type - } - - var hasValue: Bool { - return value != nil - } - - var json: String { - if let someValue = value { - let typeJson: String - if let type = type { - typeJson = ", \"value_type\" : \"\(type.typeString)\"" - } else { - typeJson = "" - } - - if type == .stringClosure { - return "{\"name\" : \"\(name)\", \"value\" : \"ignored_for_closure\"\(typeJson)}" - } else if let array = someValue as? [String] { - return "{\"name\" : \"\(name)\", \"value\" : \(array)\(typeJson)}" - } else if let hash = someValue as? [String: Any] { - let jsonData = try! JSONSerialization.data(withJSONObject: hash, options: []) - let jsonString = String(data: jsonData, encoding: .utf8)! - return "{\"name\" : \"\(name)\", \"value\" : \(jsonString)\(typeJson)}" - } else { - let dictionary = [ - "name": name, - "value": someValue, - ] - let jsonData = try! JSONSerialization.data(withJSONObject: dictionary, options: []) - return String(data: jsonData, encoding: .utf8)! - } - } else { - // Just exclude this arg if it doesn't have a value - return "" - } - } - } - - let commandID: String - let methodName: String - let className: String? - let args: [Argument] - let id: String = UUID().uuidString - - var closure: ((String) -> Void)? { - let callbacks = args.filter { ($0.type != nil) && $0.type == .stringClosure } - guard let callback = callbacks.first else { - return nil - } - - guard let callbackArgValue = callback.value else { - return nil - } - - guard let callbackClosure = callbackArgValue as? ((String) -> Void) else { - return nil - } - return callbackClosure - } - - func callbackClosure(_ callbackArg: String) -> ((String) -> Void)? { - // WARNING: This will perform the first callback it receives - let callbacks = args.filter { ($0.type != nil) && $0.type == .stringClosure } - guard let callback = callbacks.first else { - verbose(message: "received call to performCallback with \(callbackArg), but no callback available to perform") - return nil - } - - guard let callbackArgValue = callback.value else { - verbose(message: "received call to performCallback with \(callbackArg), but callback is nil") - return nil - } - - guard let callbackClosure = callbackArgValue as? ((String) -> Void) else { - verbose(message: "received call to performCallback with \(callbackArg), but callback type is unknown \(callbackArgValue.self)") - return nil - } - return callbackClosure - } - - func performCallback(callbackArg: String, socket: SocketClient, completion: @escaping () -> Void) { - verbose(message: "Performing callback with: \(callbackArg)") - socket.leave() - callbackClosure(callbackArg)?(callbackArg) - completion() - } - - var commandJson: String { - let argsArrayJson = args - .map { $0.json } - .filter { $0 != "" } - - let argsJson: String? - if !argsArrayJson.isEmpty { - argsJson = "\"args\" : [\(argsArrayJson.joined(separator: ","))]" - } else { - argsJson = nil - } - - let commandIDJson = "\"commandID\" : \"\(commandID)\"" - let methodNameJson = "\"methodName\" : \"\(methodName)\"" - - var jsonParts = [commandIDJson, methodNameJson] - if let argsJson = argsJson { - jsonParts.append(argsJson) - } - - if let className = className { - let classNameJson = "\"className\" : \"\(className)\"" - jsonParts.append(classNameJson) - } - - return "{\(jsonParts.joined(separator: ","))}" - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/RubyCommandable.swift b/fastlane/swift/RubyCommandable.swift deleted file mode 100644 index 6c6340027..000000000 --- a/fastlane/swift/RubyCommandable.swift +++ /dev/null @@ -1,43 +0,0 @@ -// RubyCommandable.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -enum CommandType { - case action - case control - - var token: String { - switch self { - case .action: - return "action" - case .control: - return "control" - } - } -} - -protocol RubyCommandable { - var type: CommandType { get } - var commandJson: String { get } - var id: String { get } -} - -extension RubyCommandable { - var json: String { - return """ - { "commandType": "\(type.token)", "command": \(commandJson) } - """ - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/Runner.swift b/fastlane/swift/Runner.swift deleted file mode 100644 index ffb32a3ee..000000000 --- a/fastlane/swift/Runner.swift +++ /dev/null @@ -1,279 +0,0 @@ -// Runner.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -let logger: Logger = .init() - -let runner: Runner = .init() - -func desc(_: String) { - // no-op, this is handled in fastlane/lane_list.rb -} - -class Runner { - private var thread: Thread! - private var socketClient: SocketClient! - private let dispatchGroup = DispatchGroup() - private var returnValue: String? // lol, so safe - private var currentlyExecutingCommand: RubyCommandable? - private var shouldLeaveDispatchGroupDuringDisconnect = false - private var executeNext: AtomicDictionary = { - if #available(macOS 10.12, *) { - return UnfairAtomicDictionary() - } else { - return OSSPinAtomicDictionary() - } - }() - - func executeCommand(_ command: RubyCommandable) -> String { - dispatchGroup.enter() - currentlyExecutingCommand = command - socketClient.send(rubyCommand: command) - - let secondsToWait = DispatchTimeInterval.seconds(SocketClient.defaultCommandTimeoutSeconds) - // swiftformat:disable:next redundantSelf - let timeoutResult = Self.waitWithPolling(self.executeNext[command.id], toEventually: { $0 == true }, timeout: SocketClient.defaultCommandTimeoutSeconds) - executeNext.removeValue(forKey: command.id) - let failureMessage = "command didn't execute in: \(SocketClient.defaultCommandTimeoutSeconds) seconds" - let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait) - guard success else { - log(message: "command timeout") - preconditionFailure() - } - - if let _returnValue = returnValue { - return _returnValue - } else { - return "" - } - } - - static func waitWithPolling(_ expression: @autoclosure @escaping () throws -> T, toEventually predicate: @escaping (T) -> Bool, timeout: Int, pollingInterval: DispatchTimeInterval = .milliseconds(4)) -> DispatchTimeoutResult { - func memoizedClosure(_ closure: @escaping () throws -> T) -> (Bool) throws -> T { - var cache: T? - return { withoutCaching in - if withoutCaching || cache == nil { - cache = try closure() - } - guard let cache = cache else { - preconditionFailure() - } - - return cache - } - } - - let runLoop = RunLoop.current - let timeoutDate = Date(timeInterval: TimeInterval(timeout), since: Date()) - var fulfilled = false - let _expression = memoizedClosure(expression) - repeat { - do { - let exp = try _expression(true) - fulfilled = predicate(exp) - } catch { - fatalError("Error raised \(error.localizedDescription)") - } - if !fulfilled { - runLoop.run(until: Date(timeIntervalSinceNow: pollingInterval.timeInterval)) - } else { - break - } - } while Date().compare(timeoutDate) == .orderedAscending - - if fulfilled { - return .success - } else { - return .timedOut - } - } -} - -/// Handle threading stuff -extension Runner { - func startSocketThread(port: UInt32) { - let secondsToWait = DispatchTimeInterval.seconds(SocketClient.connectTimeoutSeconds) - - dispatchGroup.enter() - - socketClient = SocketClient(port: port, commandTimeoutSeconds: timeout, socketDelegate: self) - thread = Thread(target: self, selector: #selector(startSocketComs), object: nil) - guard let thread = thread else { - preconditionFailure("Thread did not instantiate correctly") - } - - thread.name = "socket thread" - thread.start() - - let connectTimeout = DispatchTime.now() + secondsToWait - let timeoutResult = dispatchGroup.wait(timeout: connectTimeout) - - let failureMessage = "couldn't start socket thread in: \(SocketClient.connectTimeoutSeconds) seconds" - let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait) - guard success else { - log(message: "socket thread timeout") - preconditionFailure() - } - } - - func disconnectFromFastlaneProcess() { - shouldLeaveDispatchGroupDuringDisconnect = true - dispatchGroup.enter() - socketClient.sendComplete() - - let connectTimeout = DispatchTime.now() + 2 - _ = dispatchGroup.wait(timeout: connectTimeout) - } - - @objc func startSocketComs() { - guard let socketClient = socketClient else { - return - } - - socketClient.connectAndOpenStreams() - dispatchGroup.leave() - } - - private func testDispatchTimeoutResult(_ timeoutResult: DispatchTimeoutResult, failureMessage: String, timeToWait _: DispatchTimeInterval) -> Bool { - switch timeoutResult { - case .success: - return true - case .timedOut: - log(message: "timeout: \(failureMessage)") - return false - } - } -} - -extension Runner: SocketClientDelegateProtocol { - func commandExecuted(serverResponse: SocketClientResponse, completion: (SocketClient) -> Void) { - switch serverResponse { - case let .success(returnedObject, closureArgumentValue): - verbose(message: "command executed") - returnValue = returnedObject - if let command = currentlyExecutingCommand as? RubyCommand { - if let closureArgumentValue = closureArgumentValue, !closureArgumentValue.isEmpty { - command.performCallback(callbackArg: closureArgumentValue, socket: socketClient) { - self.executeNext[command.id] = true - } - } else { - executeNext[command.id] = true - } - } - dispatchGroup.leave() - completion(socketClient) - case .clientInitiatedCancelAcknowledged: - verbose(message: "server acknowledged a cancel request") - dispatchGroup.leave() - if let command = currentlyExecutingCommand as? RubyCommand { - executeNext[command.id] = true - } - completion(socketClient) - case .alreadyClosedSockets, .connectionFailure, .malformedRequest, .malformedResponse, .serverError: - log(message: "error encountered while executing command:\n\(serverResponse)") - dispatchGroup.leave() - if let command = currentlyExecutingCommand as? RubyCommand { - executeNext[command.id] = true - } - completion(socketClient) - case let .commandTimeout(timeout): - log(message: "Runner timed out after \(timeout) second(s)") - } - } - - func connectionsOpened() { - DispatchQueue.main.async { - verbose(message: "connected!") - } - } - - func connectionsClosed() { - DispatchQueue.main.async { - if let thread = self.thread { - thread.cancel() - } - self.thread = nil - self.socketClient.closeSession() - self.socketClient = nil - verbose(message: "connection closed!") - if self.shouldLeaveDispatchGroupDuringDisconnect { - self.dispatchGroup.leave() - } - exit(0) - } - } -} - -class Logger { - enum LogMode { - init(logMode: String) { - switch logMode { - case "normal", "default": - self = .normal - case "verbose": - self = .verbose - default: - logger.log(message: "unrecognized log mode: \(logMode), defaulting to 'normal'") - self = .normal - } - } - - case normal - case verbose - } - - static var logMode: LogMode = .normal - - func log(message: String) { - let timestamp = NSDate().timeIntervalSince1970 - print("[\(timestamp)]: \(message)") - } - - func verbose(message: String) { - if Logger.logMode == .verbose { - let timestamp = NSDate().timeIntervalSince1970 - print("[\(timestamp)]: \(message)") - } - } -} - -func log(message: String) { - logger.log(message: message) -} - -func verbose(message: String) { - logger.verbose(message: message) -} - -private extension DispatchTimeInterval { - var timeInterval: TimeInterval { - var result: TimeInterval = 0 - switch self { - case let .seconds(value): - result = TimeInterval(value) - case let .milliseconds(value): - result = TimeInterval(value) * 0.001 - case let .microseconds(value): - result = TimeInterval(value) * 0.000_001 - case let .nanoseconds(value): - result = TimeInterval(value) * 0.000_000_001 - case .never: - fatalError() - @unknown default: - fatalError() - } - return result - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/RunnerArgument.swift b/fastlane/swift/RunnerArgument.swift deleted file mode 100644 index 8e0ffe10c..000000000 --- a/fastlane/swift/RunnerArgument.swift +++ /dev/null @@ -1,20 +0,0 @@ -// RunnerArgument.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -struct RunnerArgument { - let name: String - let value: String -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/Scanfile.swift b/fastlane/swift/Scanfile.swift deleted file mode 100644 index fd15c5951..000000000 --- a/fastlane/swift/Scanfile.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Scanfile.swift -// Copyright (c) 2021 FastlaneTools - -// This class is automatically included in FastlaneRunner during build - -// This autogenerated file will be overwritten or replaced during build time, or when you initialize `scan` -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -public class Scanfile: ScanfileProtocol { - // If you want to enable `scan`, run `fastlane scan init` - // After, this file will be replaced with a custom implementation that contains values you supplied - // during the `init` process, and you won't see this message -} - -// Generated with fastlane 2.178.0 diff --git a/fastlane/swift/ScanfileProtocol.swift b/fastlane/swift/ScanfileProtocol.swift deleted file mode 100644 index 6b4e8c79b..000000000 --- a/fastlane/swift/ScanfileProtocol.swift +++ /dev/null @@ -1,577 +0,0 @@ -// ScanfileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -public protocol ScanfileProtocol: AnyObject { - /// Path to the workspace file - var workspace: String? { get } - - /// Path to the project file - var project: String? { get } - - /// Path to the Swift Package - var packagePath: String? { get } - - /// The project's scheme. Make sure it's marked as `Shared` - var scheme: String? { get } - - /// The name of the simulator type you want to run tests on (e.g. 'iPhone 6' or 'iPhone SE (2nd generation) (14.5)') - var device: String? { get } - - /// Array of devices to run the tests on (e.g. ['iPhone 6', 'iPad Air', 'iPhone SE (2nd generation) (14.5)']) - var devices: [String]? { get } - - /// Should skip auto detecting of devices if none were specified - var skipDetectDevices: Bool { get } - - /// Should fail if devices not found - var ensureDevicesFound: Bool { get } - - /// Enabling this option will automatically killall Simulator processes before the run - var forceQuitSimulator: Bool { get } - - /// Enabling this option will automatically erase the simulator before running the application - var resetSimulator: Bool { get } - - /// Enabling this option will disable the simulator from showing the 'Slide to type' prompt - var disableSlideToType: Bool { get } - - /// Enabling this option will launch the first simulator prior to calling any xcodebuild command - var prelaunchSimulator: Bool? { get } - - /// Enabling this option will automatically uninstall the application before running it - var reinstallApp: Bool { get } - - /// The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) - var appIdentifier: String? { get } - - /// Array of test identifiers to run. Expected format: TestTarget[/TestSuite[/TestCase]] - var onlyTesting: String? { get } - - /// Array of test identifiers to skip. Expected format: TestTarget[/TestSuite[/TestCase]] - var skipTesting: String? { get } - - /// The testplan associated with the scheme that should be used for testing - var testplan: String? { get } - - /// Array of strings matching test plan configurations to run - var onlyTestConfigurations: String? { get } - - /// Array of strings matching test plan configurations to skip - var skipTestConfigurations: String? { get } - - /// Run tests using the provided `.xctestrun` file - var xctestrun: String? { get } - - /// The toolchain that should be used for building the application (e.g. `com.apple.dt.toolchain.Swift_2_3, org.swift.30p620160816a`) - var toolchain: String? { get } - - /// Should the project be cleaned before building it? - var clean: Bool { get } - - /// Should code coverage be generated? (Xcode 7 and up) - var codeCoverage: Bool? { get } - - /// Should the address sanitizer be turned on? - var addressSanitizer: Bool? { get } - - /// Should the thread sanitizer be turned on? - var threadSanitizer: Bool? { get } - - /// Should the HTML report be opened when tests are completed? - var openReport: Bool { get } - - /// The directory in which all reports will be stored - var outputDirectory: String { get } - - /// Define how the output should look like. Valid values are: standard, basic, rspec, or raw (disables xcpretty during xcodebuild) - var outputStyle: String? { get } - - /// Comma separated list of the output types (e.g. html, junit, json-compilation-database) - var outputTypes: String { get } - - /// Comma separated list of the output files, corresponding to the types provided by :output_types (order should match). If specifying an output type of json-compilation-database with :use_clang_report_name enabled, that option will take precedence - var outputFiles: String? { get } - - /// The directory where to store the raw log - var buildlogPath: String { get } - - /// If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory - var includeSimulatorLogs: Bool { get } - - /// Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - var suppressXcodeOutput: Bool? { get } - - /// xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - var xcodebuildFormatter: String { get } - - /// Remove retry attempts from test results table and the JUnit report (if not using xcpretty) - var outputRemoveRetryAttempts: Bool { get } - - /// **DEPRECATED!** Use `output_style: 'raw'` instead - Disable xcpretty formatting of build, similar to `output_style='raw'` but this will also skip the test results table - var disableXcpretty: Bool? { get } - - /// **DEPRECATED!** Use 'xcpretty_formatter' instead - A custom xcpretty formatter to use - var formatter: String? { get } - - /// A custom xcpretty formatter to use - var xcprettyFormatter: String? { get } - - /// Pass in xcpretty additional command line arguments (e.g. '--test --no-color' or '--tap --no-utf') - var xcprettyArgs: String? { get } - - /// The directory where build products and other derived data will go - var derivedDataPath: String? { get } - - /// Should zip the derived data build products and place in output path? - var shouldZipBuildProducts: Bool { get } - - /// Should provide additional copy of .xctestrun file (settings.xctestrun) and place in output path? - var outputXctestrun: Bool { get } - - /// Custom path for the result bundle, overrides result_bundle - var resultBundlePath: String? { get } - - /// Should an Xcode result bundle be generated in the output directory - var resultBundle: Bool { get } - - /// Generate the json compilation database with clang naming convention (compile_commands.json) - var useClangReportName: Bool { get } - - /// Optionally override the per-target setting in the scheme for running tests in parallel. Equivalent to -parallel-testing-enabled - var parallelTesting: Bool? { get } - - /// Specify the exact number of test runners that will be spawned during parallel testing. Equivalent to -parallel-testing-worker-count - var concurrentWorkers: Int? { get } - - /// Constrain the number of simulator devices on which to test concurrently. Equivalent to -maximum-concurrent-test-simulator-destinations - var maxConcurrentSimulators: Int? { get } - - /// Do not run test bundles in parallel on the specified destinations. Testing will occur on each destination serially. Equivalent to -disable-concurrent-testing - var disableConcurrentTesting: Bool { get } - - /// Should debug build be skipped before test build? - var skipBuild: Bool { get } - - /// Test without building, requires a derived data path - var testWithoutBuilding: Bool? { get } - - /// Build for testing only, does not run tests - var buildForTesting: Bool? { get } - - /// The SDK that should be used for building the application - var sdk: String? { get } - - /// The configuration to use when building the app. Defaults to 'Release' - var configuration: String? { get } - - /// Pass additional arguments to xcodebuild. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - var xcargs: String? { get } - - /// Use an extra XCCONFIG file to build your app - var xcconfig: String? { get } - - /// App name to use in slack message and logfile name - var appName: String? { get } - - /// Target version of the app being build or tested. Used to filter out simulator version - var deploymentTargetVersion: String? { get } - - /// Create an Incoming WebHook for your Slack group to post results there - var slackUrl: String? { get } - - /// #channel or @username - var slackChannel: String? { get } - - /// The message included with each message posted to slack - var slackMessage: String? { get } - - /// Use webhook's default username and icon settings? (true/false) - var slackUseWebhookConfiguredUsernameAndIcon: Bool { get } - - /// Overrides the webhook's username property if slack_use_webhook_configured_username_and_icon is false - var slackUsername: String { get } - - /// Overrides the webhook's image property if slack_use_webhook_configured_username_and_icon is false - var slackIconUrl: String { get } - - /// Don't publish to slack, even when an URL is given - var skipSlack: Bool { get } - - /// Only post on Slack if the tests fail - var slackOnlyOnFailure: Bool { get } - - /// Specifies default payloads to include in Slack messages. For more info visit https://docs.fastlane.tools/actions/slack - var slackDefaultPayloads: [String]? { get } - - /// Use only if you're a pro, use the other options instead - var destination: String? { get } - - /// Adds arch=x86_64 to the xcodebuild 'destination' argument to run simulator in a Rosetta mode - var runRosettaSimulator: Bool { get } - - /// Platform to build when using a Catalyst enabled app. Valid values are: ios, macos - var catalystPlatform: String? { get } - - /// **DEPRECATED!** Use `--output_files` instead - Sets custom full report file name when generating a single report - var customReportFileName: String? { get } - - /// Allows for override of the default `xcodebuild` command - var xcodebuildCommand: String { get } - - /// Sets a custom path for Swift Package Manager dependencies - var clonedSourcePackagesPath: String? { get } - - /// Sets a custom package cache path for Swift Package Manager dependencies - var packageCachePath: String? { get } - - /// Skips resolution of Swift Package Manager dependencies - var skipPackageDependenciesResolution: Bool { get } - - /// Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - var disablePackageAutomaticUpdates: Bool { get } - - /// Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - var skipPackageRepositoryFetches: Bool { get } - - /// Lets xcodebuild use system's scm configuration - var useSystemScm: Bool { get } - - /// The number of times a test can fail - var numberOfRetries: Int { get } - - /// Should this step stop the build if the tests fail? Set this to false if you're using trainer - var failBuild: Bool { get } - - /// Lets xcodebuild use a specified package authorization provider (keychain|netrc) - var packageAuthorizationProvider: String? { get } -} - -public extension ScanfileProtocol { - var workspace: String? { - return nil - } - - var project: String? { - return nil - } - - var packagePath: String? { - return nil - } - - var scheme: String? { - return nil - } - - var device: String? { - return nil - } - - var devices: [String]? { - return nil - } - - var skipDetectDevices: Bool { - return false - } - - var ensureDevicesFound: Bool { - return false - } - - var forceQuitSimulator: Bool { - return false - } - - var resetSimulator: Bool { - return false - } - - var disableSlideToType: Bool { - return true - } - - var prelaunchSimulator: Bool? { - return nil - } - - var reinstallApp: Bool { - return false - } - - var appIdentifier: String? { - return nil - } - - var onlyTesting: String? { - return nil - } - - var skipTesting: String? { - return nil - } - - var testplan: String? { - return nil - } - - var onlyTestConfigurations: String? { - return nil - } - - var skipTestConfigurations: String? { - return nil - } - - var xctestrun: String? { - return nil - } - - var toolchain: String? { - return nil - } - - var clean: Bool { - return false - } - - var codeCoverage: Bool? { - return nil - } - - var addressSanitizer: Bool? { - return nil - } - - var threadSanitizer: Bool? { - return nil - } - - var openReport: Bool { - return false - } - - var outputDirectory: String { - return "./test_output" - } - - var outputStyle: String? { - return nil - } - - var outputTypes: String { - return "html,junit" - } - - var outputFiles: String? { - return nil - } - - var buildlogPath: String { - return "~/Library/Logs/scan" - } - - var includeSimulatorLogs: Bool { - return false - } - - var suppressXcodeOutput: Bool? { - return nil - } - - var xcodebuildFormatter: String { - return "xcbeautify" - } - - var outputRemoveRetryAttempts: Bool { - return false - } - - var disableXcpretty: Bool? { - return nil - } - - var formatter: String? { - return nil - } - - var xcprettyFormatter: String? { - return nil - } - - var xcprettyArgs: String? { - return nil - } - - var derivedDataPath: String? { - return nil - } - - var shouldZipBuildProducts: Bool { - return false - } - - var outputXctestrun: Bool { - return false - } - - var resultBundlePath: String? { - return nil - } - - var resultBundle: Bool { - return false - } - - var useClangReportName: Bool { - return false - } - - var parallelTesting: Bool? { - return nil - } - - var concurrentWorkers: Int? { - return nil - } - - var maxConcurrentSimulators: Int? { - return nil - } - - var disableConcurrentTesting: Bool { - return false - } - - var skipBuild: Bool { - return false - } - - var testWithoutBuilding: Bool? { - return nil - } - - var buildForTesting: Bool? { - return nil - } - - var sdk: String? { - return nil - } - - var configuration: String? { - return nil - } - - var xcargs: String? { - return nil - } - - var xcconfig: String? { - return nil - } - - var appName: String? { - return nil - } - - var deploymentTargetVersion: String? { - return nil - } - - var slackUrl: String? { - return nil - } - - var slackChannel: String? { - return nil - } - - var slackMessage: String? { - return nil - } - - var slackUseWebhookConfiguredUsernameAndIcon: Bool { - return false - } - - var slackUsername: String { - return "fastlane" - } - - var slackIconUrl: String { - return "https://fastlane.tools/assets/img/fastlane_icon.png" - } - - var skipSlack: Bool { - return false - } - - var slackOnlyOnFailure: Bool { - return false - } - - var slackDefaultPayloads: [String]? { - return nil - } - - var destination: String? { - return nil - } - - var runRosettaSimulator: Bool { - return false - } - - var catalystPlatform: String? { - return nil - } - - var customReportFileName: String? { - return nil - } - - var xcodebuildCommand: String { - return "env NSUnbufferedIO=YES xcodebuild" - } - - var clonedSourcePackagesPath: String? { - return nil - } - - var packageCachePath: String? { - return nil - } - - var skipPackageDependenciesResolution: Bool { - return false - } - - var disablePackageAutomaticUpdates: Bool { - return false - } - - var skipPackageRepositoryFetches: Bool { - return false - } - - var useSystemScm: Bool { - return false - } - - var numberOfRetries: Int { - return 0 - } - - var failBuild: Bool { - return true - } - - var packageAuthorizationProvider: String? { - return nil - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.153] diff --git a/fastlane/swift/Screengrabfile.swift b/fastlane/swift/Screengrabfile.swift deleted file mode 100644 index 163a5dc0d..000000000 --- a/fastlane/swift/Screengrabfile.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Screengrabfile.swift -// Copyright (c) 2021 FastlaneTools - -// This class is automatically included in FastlaneRunner during build - -// This autogenerated file will be overwritten or replaced during build time, or when you initialize `screengrab` -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -public class Screengrabfile: ScreengrabfileProtocol { - // If you want to enable `screengrab`, run `fastlane screengrab init` - // After, this file will be replaced with a custom implementation that contains values you supplied - // during the `init` process, and you won't see this message -} - -// Generated with fastlane 2.178.0 diff --git a/fastlane/swift/ScreengrabfileProtocol.swift b/fastlane/swift/ScreengrabfileProtocol.swift deleted file mode 100644 index 31f3b558f..000000000 --- a/fastlane/swift/ScreengrabfileProtocol.swift +++ /dev/null @@ -1,164 +0,0 @@ -// ScreengrabfileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -public protocol ScreengrabfileProtocol: AnyObject { - /// Path to the root of your Android SDK installation, e.g. ~/tools/android-sdk-macosx - var androidHome: String? { get } - - /// **DEPRECATED!** The Android build tools version to use, e.g. '23.0.2' - var buildToolsVersion: String? { get } - - /// A list of locales which should be used - var locales: [String] { get } - - /// Enabling this option will automatically clear previously generated screenshots before running screengrab - var clearPreviousScreenshots: Bool { get } - - /// The directory where to store the screenshots - var outputDirectory: String { get } - - /// Don't open the summary after running _screengrab_ - var skipOpenSummary: Bool { get } - - /// The package name of the app under test (e.g. com.yourcompany.yourapp) - var appPackageName: String { get } - - /// The package name of the tests bundle (e.g. com.yourcompany.yourapp.test) - var testsPackageName: String? { get } - - /// Only run tests in these Java packages - var useTestsInPackages: [String]? { get } - - /// Only run tests in these Java classes - var useTestsInClasses: [String]? { get } - - /// Additional launch arguments - var launchArguments: [String]? { get } - - /// The fully qualified class name of your test instrumentation runner - var testInstrumentationRunner: String { get } - - /// **DEPRECATED!** Return the device to this locale after running tests - var endingLocale: String { get } - - /// **DEPRECATED!** Restarts the adb daemon using `adb root` to allow access to screenshots directories on device. Use if getting 'Permission denied' errors - var useAdbRoot: Bool { get } - - /// The path to the APK for the app under test - var appApkPath: String? { get } - - /// The path to the APK for the tests bundle - var testsApkPath: String? { get } - - /// Use the device or emulator with the given serial number or qualifier - var specificDevice: String? { get } - - /// Type of device used for screenshots. Matches Google Play Types (phone, sevenInch, tenInch, tv, wear) - var deviceType: String { get } - - /// Whether or not to exit Screengrab on test failure. Exiting on failure will not copy screenshots to local machine nor open screenshots summary - var exitOnTestFailure: Bool { get } - - /// Enabling this option will automatically uninstall the application before running it - var reinstallApp: Bool { get } - - /// Add timestamp suffix to screenshot filename - var useTimestampSuffix: Bool { get } - - /// Configure the host used by adb to connect, allows running on remote devices farm - var adbHost: String? { get } -} - -public extension ScreengrabfileProtocol { - var androidHome: String? { - return nil - } - - var buildToolsVersion: String? { - return nil - } - - var locales: [String] { - return ["en-US"] - } - - var clearPreviousScreenshots: Bool { - return false - } - - var outputDirectory: String { - return "fastlane/metadata/android" - } - - var skipOpenSummary: Bool { - return false - } - - var appPackageName: String { - return "" - } - - var testsPackageName: String? { - return nil - } - - var useTestsInPackages: [String]? { - return nil - } - - var useTestsInClasses: [String]? { - return nil - } - - var launchArguments: [String]? { - return nil - } - - var testInstrumentationRunner: String { - return "androidx.test.runner.AndroidJUnitRunner" - } - - var endingLocale: String { - return "en-US" - } - - var useAdbRoot: Bool { - return false - } - - var appApkPath: String? { - return nil - } - - var testsApkPath: String? { - return nil - } - - var specificDevice: String? { - return nil - } - - var deviceType: String { - return "phone" - } - - var exitOnTestFailure: Bool { - return true - } - - var reinstallApp: Bool { - return false - } - - var useTimestampSuffix: Bool { - return true - } - - var adbHost: String? { - return nil - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.143] diff --git a/fastlane/swift/Snapshotfile.swift b/fastlane/swift/Snapshotfile.swift deleted file mode 100644 index 1a74bb460..000000000 --- a/fastlane/swift/Snapshotfile.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Snapshotfile.swift -// Copyright (c) 2021 FastlaneTools - -// This class is automatically included in FastlaneRunner during build - -// This autogenerated file will be overwritten or replaced during build time, or when you initialize `snapshot` -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -public class Snapshotfile: SnapshotfileProtocol { - // If you want to enable `snapshot`, run `fastlane snapshot init` - // After, this file will be replaced with a custom implementation that contains values you supplied - // during the `init` process, and you won't see this message -} - -// Generated with fastlane 2.178.0 diff --git a/fastlane/swift/SnapshotfileProtocol.swift b/fastlane/swift/SnapshotfileProtocol.swift deleted file mode 100644 index 666e35a35..000000000 --- a/fastlane/swift/SnapshotfileProtocol.swift +++ /dev/null @@ -1,374 +0,0 @@ -// SnapshotfileProtocol.swift -// Copyright (c) 2026 FastlaneTools - -public protocol SnapshotfileProtocol: AnyObject { - /// Path to the workspace file - var workspace: String? { get } - - /// Path to the project file - var project: String? { get } - - /// Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - var xcargs: String? { get } - - /// Use an extra XCCONFIG file to build your app - var xcconfig: String? { get } - - /// A list of devices you want to take the screenshots from - var devices: [String]? { get } - - /// A list of languages which should be used - var languages: [String] { get } - - /// A list of launch arguments which should be used - var launchArguments: [String] { get } - - /// The directory where to store the screenshots - var outputDirectory: String { get } - - /// If the logs generated by the app (e.g. using NSLog, perror, etc.) in the Simulator should be written to the output_directory - var outputSimulatorLogs: Bool { get } - - /// By default, the latest version should be used automatically. If you want to change it, do it here - var iosVersion: String? { get } - - /// Don't open the HTML summary after running _snapshot_ - var skipOpenSummary: Bool { get } - - /// Do not check for most recent SnapshotHelper code - var skipHelperVersionCheck: Bool { get } - - /// Enabling this option will automatically clear previously generated screenshots before running snapshot - var clearPreviousScreenshots: Bool { get } - - /// Enabling this option will automatically uninstall the application before running it - var reinstallApp: Bool { get } - - /// Enabling this option will automatically erase the simulator before running the application - var eraseSimulator: Bool { get } - - /// Enabling this option will prevent displaying the simulator window - var headless: Bool { get } - - /// Enabling this option will automatically override the status bar to show 9:41 AM, full battery, and full reception (Adjust 'SNAPSHOT_SIMULATOR_WAIT_FOR_BOOT_TIMEOUT' environment variable if override status bar is not working. Might be because simulator is not fully booted. Defaults to 10 seconds) - var overrideStatusBar: Bool { get } - - /// Fully customize the status bar by setting each option here. Requires `override_status_bar` to be set to `true`. See `xcrun simctl status_bar --help` - var overrideStatusBarArguments: String? { get } - - /// Enabling this option will configure the Simulator's system language - var localizeSimulator: Bool { get } - - /// Enabling this option will configure the Simulator to be in dark mode (false for light, true for dark) - var darkMode: Bool? { get } - - /// The bundle identifier of the app to uninstall (only needed when enabling reinstall_app) - var appIdentifier: String? { get } - - /// A list of photos that should be added to the simulator before running the application - var addPhotos: [String]? { get } - - /// A list of videos that should be added to the simulator before running the application - var addVideos: [String]? { get } - - /// A path to screenshots.html template - var htmlTemplate: String? { get } - - /// The directory where to store the build log - var buildlogPath: String { get } - - /// Should the project be cleaned before building it? - var clean: Bool { get } - - /// Test without building, requires a derived data path - var testWithoutBuilding: Bool? { get } - - /// The configuration to use when building the app. Defaults to 'Release' - var configuration: String? { get } - - /// The SDK that should be used for building the application - var sdk: String? { get } - - /// The scheme you want to use, this must be the scheme for the UI Tests - var scheme: String? { get } - - /// The number of times a test can fail before snapshot should stop retrying - var numberOfRetries: Int { get } - - /// Should snapshot stop immediately after the tests completely failed on one device? - var stopAfterFirstError: Bool { get } - - /// The directory where build products and other derived data will go - var derivedDataPath: String? { get } - - /// Should an Xcode result bundle be generated in the output directory - var resultBundle: Bool { get } - - /// The name of the target you want to test (if you desire to override the Target Application from Xcode) - var testTargetName: String? { get } - - /// Separate the log files per device and per language - var namespaceLogFiles: String? { get } - - /// Take snapshots on multiple simulators concurrently. Note: This option is only applicable when running against Xcode 9 - var concurrentSimulators: Bool { get } - - /// Disable the simulator from showing the 'Slide to type' prompt - var disableSlideToType: Bool { get } - - /// Sets a custom path for Swift Package Manager dependencies - var clonedSourcePackagesPath: String? { get } - - /// Sets a custom package cache path for Swift Package Manager dependencies - var packageCachePath: String? { get } - - /// Skips resolution of Swift Package Manager dependencies - var skipPackageDependenciesResolution: Bool { get } - - /// Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild - var disablePackageAutomaticUpdates: Bool { get } - - /// Skips updating package dependencies from their remote. This translates in the option `-skipPackageUpdates` being passed to xcodebuild - var skipPackageRepositoryFetches: Bool { get } - - /// Lets xcodebuild use a specified package authorization provider (keychain|netrc) - var packageAuthorizationProvider: String? { get } - - /// The testplan associated with the scheme that should be used for testing - var testplan: String? { get } - - /// Array of strings matching Test Bundle/Test Suite/Test Cases to run - var onlyTesting: String? { get } - - /// Array of strings matching Test Bundle/Test Suite/Test Cases to skip - var skipTesting: String? { get } - - /// xcodebuild formatter to use (ex: 'xcbeautify', 'xcbeautify --quieter', 'xcpretty', 'xcpretty -test'). Use empty string (ex: '') to disable any formatter (More information: https://docs.fastlane.tools/best-practices/xcodebuild-formatters/) - var xcodebuildFormatter: String { get } - - /// **DEPRECATED!** Use `xcodebuild_formatter: ''` instead - Additional xcpretty arguments - var xcprettyArgs: String? { get } - - /// Disable xcpretty formatting of build - var disableXcpretty: Bool? { get } - - /// Suppress the output of xcodebuild to stdout. Output is still saved in buildlog_path - var suppressXcodeOutput: Bool? { get } - - /// Lets xcodebuild use system's scm configuration - var useSystemScm: Bool { get } -} - -public extension SnapshotfileProtocol { - var workspace: String? { - return nil - } - - var project: String? { - return nil - } - - var xcargs: String? { - return nil - } - - var xcconfig: String? { - return nil - } - - var devices: [String]? { - return nil - } - - var languages: [String] { - return ["en-US"] - } - - var launchArguments: [String] { - return [""] - } - - var outputDirectory: String { - return "screenshots" - } - - var outputSimulatorLogs: Bool { - return false - } - - var iosVersion: String? { - return nil - } - - var skipOpenSummary: Bool { - return false - } - - var skipHelperVersionCheck: Bool { - return false - } - - var clearPreviousScreenshots: Bool { - return false - } - - var reinstallApp: Bool { - return false - } - - var eraseSimulator: Bool { - return false - } - - var headless: Bool { - return true - } - - var overrideStatusBar: Bool { - return false - } - - var overrideStatusBarArguments: String? { - return nil - } - - var localizeSimulator: Bool { - return false - } - - var darkMode: Bool? { - return nil - } - - var appIdentifier: String? { - return nil - } - - var addPhotos: [String]? { - return nil - } - - var addVideos: [String]? { - return nil - } - - var htmlTemplate: String? { - return nil - } - - var buildlogPath: String { - return "~/Library/Logs/snapshot" - } - - var clean: Bool { - return false - } - - var testWithoutBuilding: Bool? { - return nil - } - - var configuration: String? { - return nil - } - - var sdk: String? { - return nil - } - - var scheme: String? { - return nil - } - - var numberOfRetries: Int { - return 1 - } - - var stopAfterFirstError: Bool { - return false - } - - var derivedDataPath: String? { - return nil - } - - var resultBundle: Bool { - return false - } - - var testTargetName: String? { - return nil - } - - var namespaceLogFiles: String? { - return nil - } - - var concurrentSimulators: Bool { - return true - } - - var disableSlideToType: Bool { - return false - } - - var clonedSourcePackagesPath: String? { - return nil - } - - var packageCachePath: String? { - return nil - } - - var skipPackageDependenciesResolution: Bool { - return false - } - - var disablePackageAutomaticUpdates: Bool { - return false - } - - var skipPackageRepositoryFetches: Bool { - return false - } - - var packageAuthorizationProvider: String? { - return nil - } - - var testplan: String? { - return nil - } - - var onlyTesting: String? { - return nil - } - - var skipTesting: String? { - return nil - } - - var xcodebuildFormatter: String { - return "xcbeautify" - } - - var xcprettyArgs: String? { - return nil - } - - var disableXcpretty: Bool? { - return nil - } - - var suppressXcodeOutput: Bool? { - return nil - } - - var useSystemScm: Bool { - return false - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.137] diff --git a/fastlane/swift/SocketClient.swift b/fastlane/swift/SocketClient.swift deleted file mode 100644 index b997614b6..000000000 --- a/fastlane/swift/SocketClient.swift +++ /dev/null @@ -1,332 +0,0 @@ -// SocketClient.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Dispatch -import Foundation - -public enum SocketClientResponse: Error { - case alreadyClosedSockets - case malformedRequest - case malformedResponse - case serverError - case clientInitiatedCancelAcknowledged - case commandTimeout(seconds: Int) - case connectionFailure - case success(returnedObject: String?, closureArgumentValue: String?) -} - -class SocketClient: NSObject { - enum SocketStatus { - case ready - case closed - } - - static let connectTimeoutSeconds = 2 - static let defaultCommandTimeoutSeconds = 10800 // 3 hours - static let doneToken = "done" // TODO: remove these - static let cancelToken = "cancelFastlaneRun" - - fileprivate var inputStream: InputStream! - fileprivate var outputStream: OutputStream! - fileprivate var cleaningUpAfterDone = false - fileprivate let dispatchGroup = DispatchGroup() - fileprivate let readSemaphore = DispatchSemaphore(value: 1) - fileprivate let writeSemaphore = DispatchSemaphore(value: 1) - fileprivate let commandTimeoutSeconds: Int - - private let writeQueue: DispatchQueue - private let readQueue: DispatchQueue - private let streamQueue: DispatchQueue - private let host: String - private let port: UInt32 - - let maxReadLength = 65536 // max for ipc on 10.12 is kern.ipc.maxsockbuf: 8388608 ($sysctl kern.ipc.maxsockbuf) - - private(set) weak var socketDelegate: SocketClientDelegateProtocol? - - private(set) var socketStatus: SocketStatus - - /// localhost only, this prevents other computers from connecting - init(host: String = "localhost", port: UInt32 = 2000, commandTimeoutSeconds: Int = defaultCommandTimeoutSeconds, socketDelegate: SocketClientDelegateProtocol) { - self.host = host - self.port = port - self.commandTimeoutSeconds = commandTimeoutSeconds - readQueue = DispatchQueue(label: "readQueue", qos: .background, attributes: .concurrent) - writeQueue = DispatchQueue(label: "writeQueue", qos: .background, attributes: .concurrent) - streamQueue = DispatchQueue.global(qos: .background) - socketStatus = .closed - self.socketDelegate = socketDelegate - super.init() - } - - func connectAndOpenStreams() { - var readStream: Unmanaged? - var writeStream: Unmanaged? - - streamQueue.sync { - CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, self.host as CFString, self.port, &readStream, &writeStream) - - self.inputStream = readStream!.takeRetainedValue() - self.outputStream = writeStream!.takeRetainedValue() - - self.inputStream.delegate = self - self.outputStream.delegate = self - - self.inputStream.schedule(in: .main, forMode: .defaultRunLoopMode) - self.outputStream.schedule(in: .main, forMode: .defaultRunLoopMode) - } - - dispatchGroup.enter() - readQueue.sync { - self.inputStream.open() - } - - dispatchGroup.enter() - writeQueue.sync { - self.outputStream.open() - } - - let secondsToWait = DispatchTimeInterval.seconds(SocketClient.connectTimeoutSeconds) - let connectTimeout = DispatchTime.now() + secondsToWait - - let timeoutResult = dispatchGroup.wait(timeout: connectTimeout) - let failureMessage = "Couldn't connect to ruby process within: \(SocketClient.connectTimeoutSeconds) seconds" - - let success = testDispatchTimeoutResult(timeoutResult, failureMessage: failureMessage, timeToWait: secondsToWait) - - guard success else { - socketDelegate?.commandExecuted(serverResponse: .connectionFailure) { _ in } - return - } - - socketStatus = .ready - socketDelegate?.connectionsOpened() - } - - func send(rubyCommand: RubyCommandable) { - verbose(message: "sending: \(rubyCommand.json)") - send(string: rubyCommand.json) - writeSemaphore.signal() - } - - func sendComplete() { - closeSession(sendAbort: true) - } - - private func testDispatchTimeoutResult(_ timeoutResult: DispatchTimeoutResult, failureMessage: String, timeToWait: DispatchTimeInterval) -> Bool { - switch timeoutResult { - case .success: - return true - case .timedOut: - log(message: "Timeout: \(failureMessage)") - - if case let .seconds(seconds) = timeToWait { - socketDelegate?.commandExecuted(serverResponse: .commandTimeout(seconds: seconds)) { _ in } - } - return false - } - } - - private func stopInputSession() { - inputStream.close() - } - - private func stopOutputSession() { - outputStream.close() - } - - private func sendThroughQueue(string: String) { - let data = string.data(using: .utf8)! - data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in - if let buffer = buffer.baseAddress { - self.outputStream.write(buffer.assumingMemoryBound(to: UInt8.self), maxLength: data.count) - } - } - } - - private func privateSend(string: String) { - writeQueue.sync { - writeSemaphore.wait() - self.sendThroughQueue(string: string) - writeSemaphore.signal() - let timeoutSeconds = self.cleaningUpAfterDone ? 1 : self.commandTimeoutSeconds - let timeToWait = DispatchTimeInterval.seconds(timeoutSeconds) - let commandTimeout = DispatchTime.now() + timeToWait - let timeoutResult = writeSemaphore.wait(timeout: commandTimeout) - - _ = self.testDispatchTimeoutResult(timeoutResult, failureMessage: "Ruby process didn't return after: \(SocketClient.connectTimeoutSeconds) seconds", timeToWait: timeToWait) - } - } - - private func send(string: String) { - guard !cleaningUpAfterDone else { - // This will happen after we abort if there are commands waiting to be executed - // Need to check state of SocketClient in command runner to make sure we can accept `send` - socketDelegate?.commandExecuted(serverResponse: .alreadyClosedSockets) { _ in } - return - } - - if string == SocketClient.doneToken { - cleaningUpAfterDone = true - } - - privateSend(string: string) - } - - func closeSession(sendAbort: Bool = true) { - socketStatus = .closed - - stopInputSession() - - if sendAbort { - send(rubyCommand: ControlCommand(commandType: .done)) - } - - stopOutputSession() - socketDelegate?.connectionsClosed() - } - - func enter() { - dispatchGroup.enter() - } - - func leave() { - readSemaphore.signal() - writeSemaphore.signal() - } -} - -extension SocketClient: StreamDelegate { - func stream(_ aStream: Stream, handle eventCode: Stream.Event) { - guard !cleaningUpAfterDone else { - // Still getting response from server even though we are done. - // No big deal, we're closing the streams anyway. - // That being said, we need to balance out the dispatchGroups - dispatchGroup.leave() - return - } - - if aStream === inputStream { - switch eventCode { - case Stream.Event.openCompleted: - dispatchGroup.leave() - - case Stream.Event.errorOccurred: - verbose(message: "input stream error occurred") - closeSession(sendAbort: true) - - case Stream.Event.hasBytesAvailable: - read() - - case Stream.Event.endEncountered: - // nothing special here - break - - case Stream.Event.hasSpaceAvailable: - // we don't care about this - break - - default: - verbose(message: "input stream caused unrecognized event: \(eventCode)") - } - - } else if aStream === outputStream { - switch eventCode { - case Stream.Event.openCompleted: - dispatchGroup.leave() - - case Stream.Event.errorOccurred: - // probably safe to close all the things because Ruby already disconnected - verbose(message: "output stream received error") - - case Stream.Event.endEncountered: - // nothing special here - break - - case Stream.Event.hasSpaceAvailable: - // we don't care about this - break - - default: - verbose(message: "output stream caused unrecognized event: \(eventCode)") - } - } - } - - func read() { - readQueue.sync { - self.readSemaphore.wait() - var buffer = [UInt8](repeating: 0, count: maxReadLength) - var output = "" - while self.inputStream!.hasBytesAvailable { - let bytesRead: Int = inputStream!.read(&buffer, maxLength: buffer.count) - if bytesRead >= 0 { - guard let read = String(bytes: buffer[.. Void) -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/SocketResponse.swift b/fastlane/swift/SocketResponse.swift deleted file mode 100644 index 3d1c14a5d..000000000 --- a/fastlane/swift/SocketResponse.swift +++ /dev/null @@ -1,84 +0,0 @@ -// SocketResponse.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -struct SocketResponse { - enum ResponseType { - case parseFailure(failureInformation: [String]) - case failure(failureInformation: [String], failureClass: String?, failureMessage: String?) - case readyForNext(returnedObject: String?, closureArgumentValue: String?) - case clientInitiatedCancel - - init(statusDictionary: [String: Any]) { - guard let status = statusDictionary["status"] as? String else { - self = .parseFailure(failureInformation: ["Message failed to parse from Ruby server"]) - return - } - - if status == "ready_for_next" { - verbose(message: "ready for next") - let returnedObject = statusDictionary["return_object"] as? String - let closureArgumentValue = statusDictionary["closure_argument_value"] as? String - self = .readyForNext(returnedObject: returnedObject, closureArgumentValue: closureArgumentValue) - return - - } else if status == "cancelled" { - self = .clientInitiatedCancel - return - - } else if status == "failure" { - guard let failureInformation = statusDictionary["failure_information"] as? [String] else { - self = .parseFailure(failureInformation: ["Ruby server indicated failure but Swift couldn't receive it"]) - return - } - - let failureClass = statusDictionary["failure_class"] as? String - let failureMessage = statusDictionary["failure_message"] as? String - self = .failure(failureInformation: failureInformation, failureClass: failureClass, failureMessage: failureMessage) - return - } - self = .parseFailure(failureInformation: ["Message status: \(status) not a supported status"]) - } - } - - let responseType: ResponseType - - init(payload: String) { - guard let data = SocketResponse.convertToDictionary(text: payload) else { - responseType = .parseFailure(failureInformation: ["Unable to parse message from Ruby server"]) - return - } - - guard case let statusDictionary? = data["payload"] as? [String: Any] else { - responseType = .parseFailure(failureInformation: ["Payload missing from Ruby server response"]) - return - } - - responseType = ResponseType(statusDictionary: statusDictionary) - } -} - -extension SocketResponse { - static func convertToDictionary(text: String) -> [String: Any]? { - if let data = text.data(using: .utf8) { - do { - return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] - } catch { - log(message: error.localizedDescription) - } - } - return nil - } -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/formatting/Brewfile b/fastlane/swift/formatting/Brewfile deleted file mode 100644 index 742b17576..000000000 --- a/fastlane/swift/formatting/Brewfile +++ /dev/null @@ -1 +0,0 @@ -brew("swiftformat") diff --git a/fastlane/swift/formatting/Brewfile.lock.json b/fastlane/swift/formatting/Brewfile.lock.json deleted file mode 100644 index 904d2b1b2..000000000 --- a/fastlane/swift/formatting/Brewfile.lock.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "entries": { - "brew": { - "swiftformat": { - "version": "0.47.12", - "bottle": { - "rebuild": 0, - "cellar": ":any_skip_relocation", - "prefix": "/usr/local", - "root_url": "https://homebrew.bintray.com/bottles", - "files": { - "arm64_big_sur": { - "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.12.arm64_big_sur.bottle.tar.gz", - "sha256": "334b736f7c78b1bc48882f55558e79571be591281462bc91dedea6dac10034be" - }, - "big_sur": { - "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.12.big_sur.bottle.tar.gz", - "sha256": "b7ba5043f29c548dd05374125faa61dd07690bffe373890cb608609e4a7e2413" - }, - "catalina": { - "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.12.catalina.bottle.tar.gz", - "sha256": "030b2e18168f5680c4ee387812b14057c4cb148b6f6800b983b1b298f4af15b1" - }, - "mojave": { - "url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.12.mojave.bottle.tar.gz", - "sha256": "b3e35821d3094d08eb9f8423ce7d18cb5462bb5b1d02a3156ed1a10f6539709a" - } - } - } - } - } - }, - "system": { - "macos": { - "catalina": { - "HOMEBREW_VERSION": "3.0.4-64-g31a4989", - "HOMEBREW_PREFIX": "/usr/local", - "Homebrew/homebrew-core": "443ddf805144323198a5b37b071516534243377b", - "CLT": "11.0.33.12", - "Xcode": "12.2", - "macOS": "10.15.7" - }, - "big_sur": { - "HOMEBREW_VERSION": "2.4.13-249-g6454504", - "HOMEBREW_PREFIX": "/usr/local", - "Homebrew/homebrew-core": "020491c34515c229d904e6e69e14157cb728d2fa", - "CLT": "11.0.28.3", - "Xcode": "12.0", - "macOS": "11.0" - } - } - } -} diff --git a/fastlane/swift/formatting/Rakefile b/fastlane/swift/formatting/Rakefile deleted file mode 100644 index ad91133d3..000000000 --- a/fastlane/swift/formatting/Rakefile +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -task(default: %w[setup]) - -task(setup: [:brew, :lint]) - -task(:brew) do - raise '`brew` is required. Please install brew. https://brew.sh/' unless system('which brew') - - puts('➡️ Brew') - sh('brew bundle') -end - -task(:lint) do - Dir.chdir('..') do - sh("swiftformat . --config formatting/.swiftformat --verbose --selfrequired waitWithPolling --exclude Fastfile.swift --swiftversion 4.0") - end -end diff --git a/fastlane/swift/main.swift b/fastlane/swift/main.swift deleted file mode 100644 index 0374588cf..000000000 --- a/fastlane/swift/main.swift +++ /dev/null @@ -1,47 +0,0 @@ -// main.swift -// Copyright (c) 2026 FastlaneTools - -// -// ** NOTE ** -// This file is provided by fastlane and WILL be overwritten in future updates -// If you want to add extra functionality to this project, create a new file in a -// new group so that it won't be marked for upgrade -// - -import Foundation - -let argumentProcessor = ArgumentProcessor(args: CommandLine.arguments) -let timeout = argumentProcessor.commandTimeout - -class MainProcess { - var doneRunningLane = false - var thread: Thread! - - @objc func connectToFastlaneAndRunLane() { - runner.startSocketThread(port: argumentProcessor.port) - - let completedRun = Fastfile.runLane(from: nil, named: argumentProcessor.currentLane, with: argumentProcessor.laneParameters()) - if completedRun { - runner.disconnectFromFastlaneProcess() - } - - doneRunningLane = true - } - - func startFastlaneThread() { - thread = Thread(target: self, selector: #selector(connectToFastlaneAndRunLane), object: nil) - thread.name = "worker thread" - thread.start() - } -} - -let process = MainProcess() -process.startFastlaneThread() - -while !process.doneRunningLane, RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date(timeIntervalSinceNow: 2)) { - // no op -} - -// Please don't remove the lines below -// They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.2] diff --git a/fastlane/swift/upgrade_manifest.json b/fastlane/swift/upgrade_manifest.json deleted file mode 100644 index bb103d39f..000000000 --- a/fastlane/swift/upgrade_manifest.json +++ /dev/null @@ -1 +0,0 @@ -{"Actions.swift":"Autogenerated API","Fastlane.swift":"Autogenerated API","DeliverfileProtocol.swift":"Autogenerated API","GymfileProtocol.swift":"Autogenerated API","MatchfileProtocol.swift":"Autogenerated API","Plugins.swift":"Autogenerated API","PrecheckfileProtocol.swift":"Autogenerated API","ScanfileProtocol.swift":"Autogenerated API","ScreengrabfileProtocol.swift":"Autogenerated API","SnapshotfileProtocol.swift":"Autogenerated API","LaneFileProtocol.swift":"Fastfile Components","ControlCommand.swift":"Networking","RubyCommand.swift":"Networking","RubyCommandable.swift":"Networking","Runner.swift":"Networking","SocketClient.swift":"Networking","SocketClientDelegateProtocol.swift":"Networking","SocketResponse.swift":"Networking","main.swift":"Runner Code","ArgumentProcessor.swift":"Runner Code","RunnerArgument.swift":"Runner Code"} \ No newline at end of file