diff --git a/CODEOWNERS b/.github/CODEOWNERS
similarity index 100%
rename from CODEOWNERS
rename to .github/CODEOWNERS
diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml
new file mode 100644
index 0000000..55afdf8
--- /dev/null
+++ b/.github/actions/build-xcframework/action.yml
@@ -0,0 +1,52 @@
+name: Build XCFramework
+description: Build XCFramework for iOS and iOS Simulator
+
+inputs:
+ project_name:
+ description: Name of the Xcode project or scheme
+ required: true
+
+outputs:
+ xcframework_path:
+ description: Final path to the generated XCFramework
+ value: ${{ steps.build.outputs.xcframework_path }}
+
+runs:
+ using: "composite"
+ steps:
+ - name: Build XCFramework
+ id: build
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ PROJECT_NAME="${{ inputs.project_name }}"
+ BUILD_DIR="./build"
+
+ echo "š ļø Building XCFramework..."
+
+ xcodebuild archive \
+ -scheme "$PROJECT_NAME" \
+ -configuration Release \
+ -destination 'generic/platform=iOS Simulator' \
+ -archivePath "$BUILD_DIR/${PROJECT_NAME}.framework-iphonesimulator.xcarchive" \
+ SKIP_INSTALL=NO \
+ BUILD_LIBRARIES_FOR_DISTRIBUTION=YES | xcbeautify
+
+ xcodebuild archive \
+ -scheme "$PROJECT_NAME" \
+ -configuration Release \
+ -destination 'generic/platform=iOS' \
+ -archivePath "$BUILD_DIR/${PROJECT_NAME}.framework-iphoneos.xcarchive" \
+ SKIP_INSTALL=NO \
+ BUILD_LIBRARIES_FOR_DISTRIBUTION=YES | xcbeautify
+
+ XCFRAMEWORK_PATH="$BUILD_DIR/${PROJECT_NAME}.xcframework"
+
+ xcodebuild -create-xcframework \
+ -framework "$BUILD_DIR/${PROJECT_NAME}.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/${PROJECT_NAME}.framework" \
+ -framework "$BUILD_DIR/${PROJECT_NAME}.framework-iphoneos.xcarchive/Products/Library/Frameworks/${PROJECT_NAME}.framework" \
+ -output "$XCFRAMEWORK_PATH"
+
+ echo "ā
XCFramework built successfully"
+ echo "xcframework_path=$XCFRAMEWORK_PATH" >> "$GITHUB_OUTPUT"
diff --git a/.github/actions/get-project-version/action.yml b/.github/actions/get-project-version/action.yml
new file mode 100644
index 0000000..5ee6a08
--- /dev/null
+++ b/.github/actions/get-project-version/action.yml
@@ -0,0 +1,45 @@
+name: Get Project Version
+description: Extracts MARKETING_VERSION from a .pbxproj file
+
+inputs:
+ project_name:
+ description: Name of the Xcode project (without .xcodeproj)
+ required: false
+ pbxproj_path:
+ description: Path to the project.pbxproj file (overrides project_name if provided)
+ required: false
+
+outputs:
+ version:
+ description: The extracted project version
+ value: ${{ steps.extract.outputs.version }}
+
+runs:
+ using: "composite"
+ steps:
+ - name: Extract current project version
+ id: extract
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ # Determine the pbxproj path
+ if [[ -n "${{ inputs.pbxproj_path }}" ]]; then
+ PBXPROJ="${{ inputs.pbxproj_path }}"
+ elif [[ -n "${{ inputs.project_name }}" ]]; then
+ PBXPROJ="${{ inputs.project_name }}.xcodeproj/project.pbxproj"
+ else
+ echo "ā Either 'project_name' or 'pbxproj_path' must be provided."
+ exit 1
+ fi
+
+ # Check if the pbxproj file exists
+ if [[ ! -f "$PBXPROJ" ]]; then
+ echo "ā Project file not found: $PBXPROJ"
+ exit 1
+ fi
+
+ echo "š¦ Extracting current MARKETING_VERSION from $PBXPROJ..."
+ VERSION=$(grep -m1 'MARKETING_VERSION =' "$PBXPROJ" | sed -E 's/.*MARKETING_VERSION = ([^;]+);/\1/' | xargs)
+ echo "š¢ Current version: $VERSION"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml
new file mode 100644
index 0000000..b300ab2
--- /dev/null
+++ b/.github/actions/install-dependencies/action.yml
@@ -0,0 +1,50 @@
+name: Install Dependencies
+description: Checks for Homebrew and installs any missing CLI tools and Ruby gems
+
+inputs:
+ tools:
+ description: Space-separated list of tools to check and install via Homebrew
+ required: false
+ gems:
+ description: Space-separated list of gems to check and install via gem
+ required: false
+
+runs:
+ using: "composite"
+ steps:
+ - name: Install Dependencies
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ if [ -n "${{ inputs.tools }}" ]; then
+ echo "š Checking for Homebrew..."
+ if ! command -v brew >/dev/null; then
+ echo "ā Homebrew is required but not installed. Aborting."
+ exit 1
+ fi
+
+ echo "š§ Installing missing brew tools..."
+ for tool in ${{ inputs.tools }}; do
+ if command -v "$tool" >/dev/null; then
+ echo "ā
$tool is already installed."
+ else
+ echo "š¦ Installing $tool via brew..."
+ brew install "$tool"
+ fi
+ done
+ fi
+
+ if [ -n "${{ inputs.gems }}" ]; then
+ echo "š§ Installing missing gems..."
+ for gem in ${{ inputs.gems }}; do
+ if gem list -i "$gem" >/dev/null; then
+ echo "ā
$gem gem is already installed."
+ else
+ echo "š Installing $gem via gem..."
+ gem install "$gem"
+ fi
+ done
+ fi
+
+ echo "ā
All dependencies are ready."
diff --git a/.github/actions/package-xcframework/action.yaml b/.github/actions/package-xcframework/action.yaml
new file mode 100644
index 0000000..cb6154a
--- /dev/null
+++ b/.github/actions/package-xcframework/action.yaml
@@ -0,0 +1,34 @@
+name: Package XCFramework and LICENSE
+description: Zips the built XCFramework and LICENSE into a versioned release artifact
+
+inputs:
+ package_name:
+ description: The name of the package (e.g., MyLibrary-1.0.0)
+ required: true
+ xcframework_path:
+ description: The path to the built .xcframework
+ required: true
+ license_path:
+ description: The path to the LICENSE file
+ required: true
+
+outputs:
+ zip_name:
+ description: The name of the created zip file
+ value: ${{ steps.package.outputs.zip_name }}
+
+runs:
+ using: "composite"
+ steps:
+ - id: package
+ shell: bash
+ run: |
+ set -euo pipefail
+ ZIP_NAME="${{ inputs.package_name }}.zip"
+ mkdir -p release
+ cp -R "${{ inputs.xcframework_path }}" release/
+ cp "${{ inputs.license_path }}" release/
+ cd release
+ zip -r "../$ZIP_NAME" .
+ echo "ā
Packaged XCFramework and LICENSE into $ZIP_NAME"
+ echo "zip_name=$ZIP_NAME" >> "$GITHUB_OUTPUT"
diff --git a/.github/actions/set-xcode-version/action.yml b/.github/actions/set-xcode-version/action.yml
new file mode 100644
index 0000000..8432bca
--- /dev/null
+++ b/.github/actions/set-xcode-version/action.yml
@@ -0,0 +1,18 @@
+name: Set Xcode Version
+description: Selects the desired Xcode version using xcode-select.
+inputs:
+ xcode-version:
+ description: The Xcode version to select (e.g., 16.4)
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - run: |
+ set -e
+ echo "Setting Xcode version to ${{ inputs.xcode-version }}..."
+ if ! sudo xcode-select -s /Applications/Xcode_${{ inputs.xcode-version }}.app/Contents/Developer; then
+ echo "ā Failed to select Xcode ${{ inputs.xcode-version }}. Listing available Xcodes:"
+ ls /Applications | grep Xcode
+ exit 1
+ fi
+ shell: bash
diff --git a/pull_request_template.md b/.github/pull_request_template.md
similarity index 92%
rename from pull_request_template.md
rename to .github/pull_request_template.md
index cf46729..826cef6 100644
--- a/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -10,7 +10,7 @@
- [ ] Fix (non-breaking change which fixes an issue)
- [ ] Feature (non-breaking change which adds functionality)
- [ ] Refactor (cosmetic changes)
-- [ ] Breaking change (change that would cause existing functionality not to work as expected)
+- [ ] Breaking change (change that would cause existing functionality to not work as expected)
## Tests
@@ -21,7 +21,7 @@
## Checklist
- [ ] Pull request title follows the format `RNMT-XXXX
`
-- [ ] Code follows the code style of this project
+- [ ] Code follows code style of this project
- [ ] CHANGELOG.md file is correctly updated
- [ ] Changes require an update to the documentation
- [ ] Documentation has been updated accordingly
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c704144
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,180 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: read
+ pull-requests: write # For PR comments
+
+env:
+ PROJECT_NAME: OSInAppBrowserLib
+ SCHEME_NAME: OSInAppBrowserLib
+ XCODEPROJ_PATH: OSInAppBrowserLib.xcodeproj
+ XCODE_VERSION: 16.4
+ DESTINATION: 'platform=iOS Simulator,OS=latest,name=iPhone 16'
+ COVERAGE_TARGET_FILTER: OSInAppBrowserLib
+ BUILD_REPORTS_DIR: build/reports
+ SONAR_REPORTS_DIR: sonar-reports
+
+jobs:
+ test:
+ name: Run Tests
+ runs-on: macos-15
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install Dependencies
+ uses: ./.github/actions/install-dependencies
+ with:
+ tools: swiftlint xcbeautify
+
+ - name: Set Xcode version
+ uses: ./.github/actions/set-xcode-version
+ with:
+ xcode-version: ${{ env.XCODE_VERSION }}
+
+ - name: Run Unit Tests
+ id: unit_tests
+ env:
+ SCHEME_NAME: ${{ env.SCHEME_NAME }}
+ XCODEPROJ_PATH: ${{ env.XCODEPROJ_PATH }}
+ IOS_SIMULATOR_DEVICE: ${{ env.IOS_SIMULATOR_DEVICE }}
+ DESTINATION: ${{ env.DESTINATION }}
+ run: |
+ set -euo pipefail
+ XCRESULT_NAME="TestResults.xcresult"
+ mkdir -p "$BUILD_REPORTS_DIR"
+ xcodebuild test \
+ -project "$XCODEPROJ_PATH" \
+ -scheme "$SCHEME_NAME" \
+ -destination "$DESTINATION" \
+ -configuration Debug \
+ -enableCodeCoverage YES \
+ -resultBundlePath "$XCRESULT_NAME" \
+ SKIP_SCRIPT_PHASES=YES \
+ CODE_SIGNING_ALLOWED=NO | xcbeautify --report junit --report-path "$BUILD_REPORTS_DIR"
+ echo "xcresult_name=$XCRESULT_NAME" >> "$GITHUB_OUTPUT"
+
+ - name: Generate Code Coverage Report for SonarQube
+ continue-on-error: true
+ env:
+ XCRESULT_NAME: ${{ steps.unit_tests.outputs.xcresult_name }}
+ run: |
+ set -euo pipefail
+ echo "š Generating SonarQube coverage report..."
+
+ if [ ! -d "$XCRESULT_NAME" ]; then
+ echo "ā ļø $XCRESULT_NAME not found. Skipping coverage report generation."
+ exit 0
+ fi
+
+ mkdir -p ${{ env.SONAR_REPORTS_DIR }}
+
+ echo "š¦ Downloading coverage converter script..."
+ curl -sSL https://raw.githubusercontent.com/SonarSource/sonar-scanning-examples/master/swift-coverage/swift-coverage-example/xccov-to-sonarqube-generic.sh -o xccov-to-sonarqube-generic.sh
+ chmod +x xccov-to-sonarqube-generic.sh
+
+ echo "š Running coverage converter..."
+ ./xccov-to-sonarqube-generic.sh TestResults.xcresult > ${{ env.SONAR_REPORTS_DIR }}/sonarqube-generic-coverage.xml
+ echo "ā
SonarQube coverage report generated successfully"
+
+ - name: Run SwiftLint for SonarQube
+ run: |
+ set -euo pipefail
+ echo "š Running SwiftLint..."
+ mkdir -p ${{ env.SONAR_REPORTS_DIR }}
+ swiftlint --reporter checkstyle > "${{ env.SONAR_REPORTS_DIR }}/swiftlint.xml" || {
+ echo "ā ļø SwiftLint finished with issues."
+ exit 0
+ }
+ echo "ā
SwiftLint report generated successfully"
+
+ - name: Setup SonarQube Scanner
+ uses: warchant/setup-sonar-scanner@v8
+
+ - name: Send to SonarCloud
+ id: sonarcloud
+ continue-on-error: true
+ run: |
+ set -euo pipefail
+ if [ -z "${{ secrets.SONAR_TOKEN }}" ]; then
+ echo "ā ļø SONAR_TOKEN secret is not set. Skipping SonarCloud analysis."
+ exit 0
+ fi
+ if [ -f "sonar-project.properties" ]; then
+ echo "š Sending results to SonarCloud..."
+ echo "š¦ Commit: ${{ github.sha }}"
+ if [ "${{ github.ref_name }}" = "main" ]; then
+ echo "š Analyzing main branch"
+ sonar-scanner
+ else
+ echo "šæ Analyzing feature branch: ${{ github.ref_name }}"
+ sonar-scanner -Dsonar.branch.name="${{ github.ref_name }}"
+ fi
+ else
+ echo "ā ļø sonar-project.properties not found, skipping SonarCloud"
+ fi
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: test-results
+ path: |
+ ${{ steps.unit_tests.outputs.xcresult_name }}
+ ${{ env.SONAR_REPORTS_DIR }}
+ ${{ env.BUILD_REPORTS_DIR }}
+
+ - name: Comment Test Results
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ env:
+ XCRESULT_NAME: ${{ steps.unit_tests.outputs.xcresult_name }}
+ COVERAGE_TARGET_FILTER: ${{ env.COVERAGE_TARGET_FILTER }}
+ with:
+ script: |
+ const { execSync } = require('child_process');
+ const fs = require('fs');
+
+ console.log('š Starting to comment test results...');
+ let coveragePercentage = 'N/A';
+ try {
+ const xcresultName = process.env.XCRESULT_NAME;
+ const coverageTarget = process.env.COVERAGE_TARGET_FILTER;
+ console.log(`Checking result file: ${xcresultName}`);
+ if (fs.existsSync(xcresultName)) {
+ console.log('Result file found. Calculating coverage...');
+ const output = execSync(`xcrun xccov view --report "${xcresultName}"`).toString();
+ const match = output.match(new RegExp(`${coverageTarget}.*?([0-9]+\\.[0-9]+%)`));
+ if (match && match[1]) {
+ coveragePercentage = match[1];
+ console.log(`Coverage found: ${coveragePercentage}`);
+ } else {
+ console.log('Coverage not found in report.');
+ }
+ } else {
+ console.log('Result file not found.');
+ }
+ } catch (e) {
+ console.error('Error calculating coverage:', e);
+ coveragePercentage = 'N/A';
+ }
+
+ console.log('Commenting on PR with test results and coverage...');
+ await github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: `ā
**Tests**: All passed\nš **Coverage**: ${coveragePercentage}`
+ });
+ console.log('Comment sent successfully.');
diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml
deleted file mode 100644
index 541a5f5..0000000
--- a/.github/workflows/continuous_integration.yml
+++ /dev/null
@@ -1,51 +0,0 @@
-name: Continuous Integration
-
-on:
- push:
- branches:
- - main
- pull_request:
- types: [opened, synchronize, reopened]
-
-jobs:
- sonarcloud:
- name: Unit-Tests
- runs-on: macos-14
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Java 17
- uses: actions/setup-java@v4
- with:
- distribution: 'zulu'
- java-version: '17'
-
- - name: Link SwiftLint or install it
- run: brew link --overwrite swiftlint || brew install swiftlint
-
- - name: Set up XCode
- run: sudo xcode-select --switch /Applications/Xcode_15.0.app
-
- - name: Bundle Install
- run: bundle install
-
- - name: Unit tests
- run: bundle exec fastlane unit_tests
- env:
- SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
-
- - name: Code Coverage
- run: bundle exec fastlane coverage
-
- - name: Lint
- run: bundle exec fastlane lint
-
- - name: Setup sonarqube
- uses: warchant/setup-sonar-scanner@v8
-
- - name: Send to Sonarcloud
- run: bundle exec fastlane sonarqube
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONARCLOUD_KEY }}
diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml
index edecd2c..eaa2140 100644
--- a/.github/workflows/prepare_release.yml
+++ b/.github/workflows/prepare_release.yml
@@ -3,79 +3,147 @@ name: Prepare Release
on:
workflow_dispatch:
inputs:
- versionBumpLevel:
- description: 'Version bump level (patch, minor, major)'
- required: true
+ bump:
+ description: 'Version bump type (ignored if version is set)'
+ required: false
type: choice
- default: 'patch'
options:
- - patch
- - minor
- - major
+ - patch
+ - minor
+ - major
+ version:
+ description: 'Set specific version (e.g., 1.2.3)'
+ required: false
+
+env:
+ PROJECT_NAME: OSInAppBrowserLib
+ PBXPROJ: OSInAppBrowserLib.xcodeproj/project.pbxproj
+ XCODE_VERSION: 16.4
+ CHANGELOG_PATH: CHANGELOG.md
+ LICENSE_PATH: LICENSE
+ PODSPEC_FILE: OSInAppBrowserLib.podspec
jobs:
- build-and-release:
- if: github.ref == 'refs/heads/main'
- runs-on: macos-14
+ prepare-release:
+ name: Prepare Release
+ runs-on: macos-15
+ if: github.ref_name == 'main'
+
steps:
- - name: Checkout
+ - name: Checkout code
uses: actions/checkout@v4
-
- - name: Link SwiftLint or install it
- run: brew link --overwrite swiftlint || brew install swiftlint
-
- - name: Set up XCode
- run: sudo xcode-select --switch /Applications/Xcode_15.0.app
-
- - name: Set up Ruby
- uses: ruby/setup-ruby@v1
with:
- ruby-version: '3.3'
-
- - name: Bump version
- run: ruby ./scripts/bump_versions.rb ${{ github.event.inputs.versionBumpLevel }}
+ fetch-depth: 0
- - name: Build XCFramework
- run: ./scripts/build_framework.sh
+ - name: Install Dependencies
+ uses: ./.github/actions/install-dependencies
+ with:
+ tools: gh
- - name: Get new version
- id: version
- run: echo "VERSION=$(ruby -e 'puts File.read("./OSInAppBrowserLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV
+ - name: Get current project version
+ id: get_version
+ uses: ./.github/actions/get-project-version
+ with:
+ project_name: ${{ env.PROJECT_NAME }}
- - name: Create new branch
+ - name: Bump project version
+ id: bump_version
+ env:
+ CURRENT_VERSION: ${{ steps.get_version.outputs.version }}
+ INPUT_BUMP: ${{ github.event.inputs.bump }}
+ INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
- git switch --create "prepare-new-release-${{ env.VERSION }}"
+ set -euo pipefail
+ echo "ā¬ļø Bumping project version..."
+ if [ -n "$INPUT_VERSION" ]; then
+ if ! [[ "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "ā Version must be in format M.m.p (e.g., 1.2.3)"
+ exit 1
+ fi
+ NEW_VERSION="$INPUT_VERSION"
+ else
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
+ PATCH="${PATCH:-0}"
+ case "$INPUT_BUMP" in
+ major)
+ MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0;;
+ minor)
+ MINOR=$((MINOR+1)); PATCH=0;;
+ *)
+ PATCH=$((PATCH+1));;
+ esac
+ NEW_VERSION="$MAJOR.$MINOR.$PATCH"
+ fi
+ echo "š¢ New version: $NEW_VERSION"
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- - name: Move zip file to root and push changes
+ - name: Update project files
+ env:
+ PBXPROJ: ${{ env.PBXPROJ }}
+ PODSPEC_FILE: ${{ env.PODSPEC_FILE }}
+ CHANGELOG_PATH: ${{ env.CHANGELOG_PATH }}
+ NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
run: |
- if [ -f OSInAppBrowserLib.zip ]; then
- rm OSInAppBrowserLib.zip
+ set -euo pipefail
+
+ echo "š ļø Updating MARKETING_VERSION..."
+ sed -i '' -E "s/MARKETING_VERSION = [0-9]+\.[0-9]+(\.[0-9]+)?;/MARKETING_VERSION = $NEW_VERSION;/g" "$PBXPROJ"
+ echo "š ļø Updating CURRENT_PROJECT_VERSION..."
+ current_proj_version=$(grep -m1 'CURRENT_PROJECT_VERSION =' "$PBXPROJ" | sed -E 's/.*CURRENT_PROJECT_VERSION = ([0-9]+);/\1/')
+ new_proj_version=$((current_proj_version+1))
+ sed -i '' -E "s/CURRENT_PROJECT_VERSION = [0-9]+;/CURRENT_PROJECT_VERSION = $new_proj_version;/g" "$PBXPROJ"
+ echo "ā
Bumped MARKETING_VERSION to $NEW_VERSION, CURRENT_PROJECT_VERSION to $new_proj_version"
+
+ echo "š ļø Updating version in $PODSPEC_FILE..."
+ if [ -f "$PODSPEC_FILE" ]; then
+ sed -i '' -E "s/^([[:space:]]*[^[:space:]]+\.version[[:space:]]*=[[:space:]]*['\"])[^'\"]+(['\"])/\1$NEW_VERSION\2/" "$PODSPEC_FILE"
+ echo "ā
Podspec version updated to $NEW_VERSION"
else
- echo "File does not exist."
+ echo "ā ļø Podspec file not found: $PODSPEC_FILE"
fi
- mv build/OSInAppBrowserLib.zip .
- git config --global user.name 'github-actions[bot]'
- git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- git add .
- git commit -m "chore: Bump version to ${{ env.VERSION }}"
- git push origin HEAD:prepare-new-release-${{ env.VERSION }}
- - name: Create pull request
- id: create_pr
+ echo "š Updating $CHANGELOG_PATH..."
+ TODAY=$(date +%Y-%m-%d)
+ awk -v ver="$NEW_VERSION" -v today="$TODAY" '
+ BEGIN { unreleased_found=0 }
+ /^## \[Unreleased\]/ {
+ print $0; print ""; print "## [" ver "] - " today; unreleased_found=1; next
+ }
+ { print $0 }
+ ' "$CHANGELOG_PATH" > "$CHANGELOG_PATH.tmp" && mv "$CHANGELOG_PATH.tmp" "$CHANGELOG_PATH"
+ echo "ā
CHANGELOG updated for version $NEW_VERSION"
+
+ - name: Create release branch, commit and push changes
+ id: create_release_branch
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ steps.bump_version.outputs.new_version }}
run: |
- gh pr create -B main -H prepare-new-release-${{ env.VERSION }} --title 'Prepare `main` to Release `${{ env.VERSION }}`' --body 'Bumps version to `${{ env.VERSION }}`.
Creates an updated and ready-to-be-released `OSInAppBrowserLib.zip`.'
- PR_NUMBER=$(gh pr view --json number --jq '.number')
- echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV
+ set -euo pipefail
+ BRANCH_NAME="release/v$VERSION"
+ git config user.name "github-actions[bot] (on behalf of ${{ github.actor }})"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git checkout -b "$BRANCH_NAME"
+ git add .
+ git commit -m "chore(release): prepare release v$VERSION"
+ git push origin "$BRANCH_NAME"
+ echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
+
+ - name: Ensure 'release' label exists
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: gh label create release --color FFD700 --description "Release PRs" || true
- - name: Add label to the pull request
- run: |
- gh api \
- --method POST \
- -H "Accept: application/vnd.github+json" \
- -H "X-GitHub-Api-Version: 2022-11-28" \
- /repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/labels \
- -f "labels[]=release"
+ - name: Create Pull Request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ steps.bump_version.outputs.new_version }}
+ BRANCH_NAME: ${{ steps.create_release_branch.outputs.branch_name }}
+ run: |
+ set -euo pipefail
+ gh pr create \
+ --title "Release v$VERSION" \
+ --body "Automated PR to release v$VERSION." \
+ --head $BRANCH_NAME \
+ --base main \
+ --label release
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..9f5e460
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,145 @@
+name: Release
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - 'main'
+
+env:
+ PROJECT_NAME: OSInAppBrowserLib
+ XCODE_VERSION: 16.4
+ CHANGELOG_PATH: CHANGELOG.md
+ LICENSE_PATH: LICENSE
+ PODSPEC_FILE: OSInAppBrowserLib.podspec
+
+jobs:
+ tag_and_release:
+ name: Release and Publish
+ runs-on: macos-15
+ if: >-
+ github.event.pull_request.merged == true &&
+ contains(github.event.pull_request.labels.*.name, 'release')
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install Dependencies
+ uses: ./.github/actions/install-dependencies
+ with:
+ tools: gh
+ gems: cocoapods
+
+ - name: Get current project version
+ id: get_version
+ uses: ./.github/actions/get-project-version
+ with:
+ project_name: ${{ env.PROJECT_NAME }}
+
+ - name: Get release notes for this version
+ id: release_notes
+ env:
+ VERSION: ${{ steps.get_version.outputs.version }}
+ CHANGELOG_PATH: ${{ env.CHANGELOG_PATH }}
+ run: |
+ set -euo pipefail
+ if [ ! -f "$CHANGELOG_PATH" ]; then
+ echo "ā Changelog file not found: $CHANGELOG_PATH"
+ exit 1
+ fi
+ echo "š Extracting release notes for version $VERSION..."
+ release_notes_section=$(awk "/^## \[${VERSION}\]/ {flag=1; next} flag && /^## \\[/ {exit} flag {print}" "$CHANGELOG_PATH" | sed '/^\s*$/d')
+ if [ -n "$release_notes_section" ]; then
+ echo "$release_notes_section"
+ echo 'release_notes<> $GITHUB_OUTPUT
+ echo "$release_notes_section" >> $GITHUB_OUTPUT
+ echo 'EOF' >> $GITHUB_OUTPUT
+ else
+ echo "ā ļø No release notes found for version $VERSION."
+ exit 0
+ fi
+
+ - name: Set Xcode version
+ uses: ./.github/actions/set-xcode-version
+ with:
+ xcode-version: ${{ env.XCODE_VERSION }}
+
+ - name: Build XCFramework
+ id: build_xcframework
+ uses: ./.github/actions/build-xcframework
+ with:
+ project_name: ${{ env.PROJECT_NAME }}
+
+ - name: Package XCFramework
+ uses: ./.github/actions/package-xcframework
+ id: package
+ with:
+ package_name: ${{ env.PROJECT_NAME }}
+ xcframework_path: ${{ steps.build_xcframework.outputs.xcframework_path }}
+ license_path: ${{ env.LICENSE_PATH }}
+
+ - name: Create tag
+ id: create_tag
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ steps.get_version.outputs.version }}
+ run: |
+ set -euo pipefail
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git tag v$VERSION -m "Release version $VERSION"
+ git push origin v$VERSION
+ echo "Tag v$VERSION created."
+ echo "tag=v$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Create GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TITLE: ${{ steps.create_tag.outputs.tag }}
+ RELEASE_NOTES: ${{ steps.release_notes.outputs.release_notes }}
+ ASSET_PATH: ${{ steps.package.outputs.zip_name }}
+ run: |
+ set -euo pipefail
+ gh release create $TITLE \
+ --title "$TITLE" \
+ --notes "$RELEASE_NOTES" \
+ "$ASSET_PATH"
+
+ - name: Publish Pod
+ if: hashFiles('${{ env.PODSPEC_FILE }}') != ''
+ env:
+ COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
+ run: |
+ set -euo pipefail
+ if [ -z "${COCOAPODS_TRUNK_TOKEN:-}" ]; then
+ echo "ā COCOAPODS_TRUNK_TOKEN secret is not set. Please set it in your repository secrets."
+ exit 1
+ fi
+ echo "š Deploying podspec to CocoaPods..."
+ pod trunk push "$PODSPEC_FILE" --allow-warnings
+
+ - name: Delete source branch
+ if: github.event.pull_request.head.ref != 'main'
+ run: |
+ set +e
+ git push origin --delete ${{ github.event.pull_request.head.ref }}
+ set -e
+
+ delete_branch_if_pr_closed_without_merge:
+ name: Delete Source Branch If PR Closed Without Merge
+ runs-on: macos-15
+ if: >-
+ github.event.pull_request.merged == false &&
+ github.event.pull_request.head.ref != 'main' &&
+ contains(github.event.pull_request.labels.*.name, 'release')
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Delete source branch
+ run: |
+ set +e
+ git push origin --delete ${{ github.event.pull_request.head.ref }}
+ set -e
diff --git a/.github/workflows/release_and_publish.yml b/.github/workflows/release_and_publish.yml
deleted file mode 100644
index 398321c..0000000
--- a/.github/workflows/release_and_publish.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-name: Release and Publish
-
-on:
- pull_request:
- types: [closed]
- branches:
- - 'main'
-
-jobs:
- post-merge:
- if: contains(github.event.pull_request.labels.*.name, 'release') && github.event.pull_request.merged == true
- runs-on: macos-14
-
- steps:
- - name: Checkout Repository
- uses: actions/checkout@v4
-
- - name: Set up Cocoapods
- run: gem install cocoapods
-
- - name: Get new version
- id: version
- run: echo "VERSION=$(ruby -e 'puts File.read("./OSInAppBrowserLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV
-
- - name: Extract release notes
- run: sh scripts/extract_release_notes.sh "${{ env.VERSION }}" >> release_notes.md
-
- - name: Create Tag
- id: create_tag
- run: |
- # Define the tag name and message
- TAG_NAME="${{ env.VERSION }}"
- TAG_MESSAGE="Tag for version ${{ env.VERSION }}"
-
- # Create the tag
- git tag -a "$TAG_NAME" -m "$TAG_MESSAGE"
- git push origin "$TAG_NAME"
-
- echo "Tag created: $TAG_NAME"
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Create Release
- run: |
- # Extract the tag name
- TAG_NAME="${{ env.VERSION }}"
- RELEASE_NOTES="$(cat release_notes.md)"
-
- # Create the release using GitHub CLI
- gh release create "$TAG_NAME" \
- --title "$TAG_NAME" \
- --notes "$RELEASE_NOTES" \
- "OSInAppBrowserLib.zip"
-
- echo "Release created for tag: $TAG_NAME"
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Deploy to Cocoapods
- run: pod trunk push ./OSInAppBrowserLib.podspec --allow-warnings
- env:
- COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
-
- - name: Delete Release Branch
- run: git push origin --delete prepare-new-release-${{ env.VERSION }}
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Gemfile b/Gemfile
deleted file mode 100644
index 484334d..0000000
--- a/Gemfile
+++ /dev/null
@@ -1,4 +0,0 @@
-source "https://rubygems.org"
-
-gem "fastlane"
-gem "slather"
diff --git a/Gemfile.lock b/Gemfile.lock
deleted file mode 100644
index 68743c5..0000000
--- a/Gemfile.lock
+++ /dev/null
@@ -1,279 +0,0 @@
-GEM
- remote: https://rubygems.org/
- specs:
- CFPropertyList (3.0.7)
- base64
- nkf
- rexml
- activesupport (8.0.2)
- base64
- benchmark (>= 0.3)
- bigdecimal
- concurrent-ruby (~> 1.0, >= 1.3.1)
- connection_pool (>= 2.2.5)
- drb
- i18n (>= 1.6, < 2)
- logger (>= 1.4.2)
- minitest (>= 5.1)
- securerandom (>= 0.3)
- tzinfo (~> 2.0, >= 2.0.5)
- uri (>= 0.13.1)
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
- artifactory (3.0.17)
- atomos (0.1.3)
- aws-eventstream (1.4.0)
- aws-partitions (1.1116.0)
- aws-sdk-core (3.225.2)
- aws-eventstream (~> 1, >= 1.3.0)
- aws-partitions (~> 1, >= 1.992.0)
- aws-sigv4 (~> 1.9)
- base64
- jmespath (~> 1, >= 1.6.1)
- logger
- aws-sdk-kms (1.104.0)
- aws-sdk-core (~> 3, >= 3.225.0)
- aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.189.1)
- aws-sdk-core (~> 3, >= 3.225.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.3.0)
- benchmark (0.4.1)
- bigdecimal (3.2.2)
- claide (1.1.0)
- clamp (1.3.2)
- colored (1.2)
- colored2 (3.1.2)
- commander (4.6.0)
- highline (~> 2.0.0)
- concurrent-ruby (1.3.5)
- connection_pool (2.5.3)
- declarative (0.0.20)
- digest-crc (0.7.0)
- rake (>= 12.0.0, < 14.0.0)
- domain_name (0.6.20240107)
- dotenv (2.8.1)
- drb (2.2.3)
- emoji_regex (3.2.3)
- excon (0.112.0)
- faraday (1.10.4)
- faraday-em_http (~> 1.0)
- faraday-em_synchrony (~> 1.0)
- faraday-excon (~> 1.1)
- faraday-httpclient (~> 1.0)
- faraday-multipart (~> 1.0)
- faraday-net_http (~> 1.0)
- faraday-net_http_persistent (~> 1.0)
- faraday-patron (~> 1.0)
- faraday-rack (~> 1.0)
- faraday-retry (~> 1.0)
- ruby2_keywords (>= 0.0.4)
- faraday-cookie_jar (0.0.7)
- faraday (>= 0.8.0)
- http-cookie (~> 1.0.0)
- faraday-em_http (1.0.0)
- faraday-em_synchrony (1.0.0)
- faraday-excon (1.1.0)
- faraday-httpclient (1.0.1)
- faraday-multipart (1.1.0)
- multipart-post (~> 2.0)
- faraday-net_http (1.0.2)
- faraday-net_http_persistent (1.2.0)
- faraday-patron (1.0.0)
- faraday-rack (1.0.0)
- faraday-retry (1.0.3)
- faraday_middleware (1.2.1)
- faraday (~> 1.0)
- fastimage (2.4.0)
- fastlane (2.228.0)
- CFPropertyList (>= 2.3, < 4.0.0)
- addressable (>= 2.8, < 3.0.0)
- artifactory (~> 3.0)
- aws-sdk-s3 (~> 1.0)
- babosa (>= 1.0.3, < 2.0.0)
- bundler (>= 1.12.0, < 3.0.0)
- colored (~> 1.2)
- commander (~> 4.6)
- dotenv (>= 2.1.1, < 3.0.0)
- emoji_regex (>= 0.1, < 4.0)
- excon (>= 0.71.0, < 1.0.0)
- faraday (~> 1.0)
- faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (~> 1.0)
- fastimage (>= 2.1.0, < 3.0.0)
- fastlane-sirp (>= 1.0.0)
- 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.0.0)
- google-cloud-storage (~> 1.31)
- highline (~> 2.0)
- http-cookie (~> 1.0.5)
- json (< 3.0.0)
- jwt (>= 2.1.0, < 3)
- mini_magick (>= 4.9.4, < 5.0.0)
- multipart-post (>= 2.0.0, < 3.0.0)
- naturally (~> 2.2)
- optparse (>= 0.1.1, < 1.0.0)
- plist (>= 3.1.0, < 4.0.0)
- rubyzip (>= 2.0.0, < 3.0.0)
- security (= 0.1.5)
- simctl (~> 1.6.3)
- terminal-notifier (>= 2.0.0, < 3.0.0)
- terminal-table (~> 3)
- tty-screen (>= 0.6.3, < 1.0.0)
- tty-spinner (>= 0.8.0, < 1.0.0)
- word_wrap (~> 1.0.0)
- xcodeproj (>= 1.13.0, < 2.0.0)
- xcpretty (~> 0.4.1)
- xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
- fastlane-sirp (1.0.0)
- sysrandom (~> 1.0)
- gh_inspector (1.1.3)
- google-apis-androidpublisher_v3 (0.54.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-apis-core (0.11.3)
- addressable (~> 2.5, >= 2.5.1)
- googleauth (>= 0.16.2, < 2.a)
- httpclient (>= 2.8.1, < 3.a)
- mini_mime (~> 1.0)
- representable (~> 3.0)
- retriable (>= 2.0, < 4.a)
- rexml
- google-apis-iamcredentials_v1 (0.17.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-apis-playcustomapp_v1 (0.13.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-apis-storage_v1 (0.31.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-cloud-core (1.8.0)
- google-cloud-env (>= 1.0, < 3.a)
- google-cloud-errors (~> 1.0)
- google-cloud-env (1.6.0)
- faraday (>= 0.17.3, < 3.0)
- google-cloud-errors (1.5.0)
- google-cloud-storage (1.47.0)
- addressable (~> 2.8)
- digest-crc (~> 0.4)
- google-apis-iamcredentials_v1 (~> 0.1)
- google-apis-storage_v1 (~> 0.31.0)
- google-cloud-core (~> 1.6)
- googleauth (>= 0.16.2, < 2.a)
- mini_mime (~> 1.0)
- googleauth (1.8.1)
- faraday (>= 0.17.3, < 3.a)
- jwt (>= 1.4, < 3.0)
- multi_json (~> 1.11)
- os (>= 0.9, < 2.0)
- 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.7)
- concurrent-ruby (~> 1.0)
- jmespath (1.6.2)
- json (2.12.2)
- jwt (2.10.1)
- base64
- logger (1.7.0)
- mini_magick (4.13.2)
- mini_mime (1.1.5)
- mini_portile2 (2.8.9)
- minitest (5.25.5)
- multi_json (1.15.0)
- multipart-post (2.4.1)
- mutex_m (0.3.0)
- nanaimo (0.4.0)
- naturally (2.3.0)
- nkf (0.2.0)
- nokogiri (1.18.8)
- mini_portile2 (~> 2.8.2)
- racc (~> 1.4)
- nokogiri (1.18.8-aarch64-linux-gnu)
- racc (~> 1.4)
- nokogiri (1.18.8-arm-linux-gnu)
- racc (~> 1.4)
- nokogiri (1.18.8-arm64-darwin)
- racc (~> 1.4)
- nokogiri (1.18.8-x86_64-darwin)
- racc (~> 1.4)
- nokogiri (1.18.8-x86_64-linux-gnu)
- racc (~> 1.4)
- optparse (0.6.0)
- os (1.1.4)
- plist (3.7.2)
- public_suffix (6.0.2)
- racc (1.8.1)
- rake (13.3.0)
- representable (3.2.0)
- declarative (< 0.1.0)
- trailblazer-option (>= 0.1.1, < 0.2.0)
- uber (< 0.2.0)
- retriable (3.1.2)
- rexml (3.4.1)
- rouge (3.28.0)
- ruby2_keywords (0.0.5)
- rubyzip (2.4.1)
- securerandom (0.4.1)
- security (0.1.5)
- signet (0.20.0)
- addressable (~> 2.8)
- faraday (>= 0.17.5, < 3.a)
- jwt (>= 1.5, < 3.0)
- multi_json (~> 1.10)
- simctl (1.6.10)
- CFPropertyList
- naturally
- slather (2.8.5)
- CFPropertyList (>= 2.2, < 4)
- activesupport
- clamp (~> 1.3)
- nokogiri (>= 1.14.3)
- xcodeproj (~> 1.27)
- sysrandom (1.0.5)
- terminal-notifier (2.0.0)
- terminal-table (3.0.2)
- unicode-display_width (>= 1.1.1, < 3)
- trailblazer-option (0.1.2)
- tty-cursor (0.7.1)
- tty-screen (0.8.2)
- tty-spinner (0.9.3)
- tty-cursor (~> 0.7)
- tzinfo (2.0.6)
- concurrent-ruby (~> 1.0)
- uber (0.1.0)
- unicode-display_width (2.6.0)
- uri (1.0.3)
- word_wrap (1.0.0)
- xcodeproj (1.27.0)
- CFPropertyList (>= 2.3.3, < 4.0)
- atomos (~> 0.1.3)
- claide (>= 1.0.2, < 2.0)
- colored2 (~> 3.1)
- nanaimo (~> 0.4.0)
- rexml (>= 3.3.6, < 4.0)
- xcpretty (0.4.1)
- rouge (~> 3.28.0)
- xcpretty-travis-formatter (1.0.1)
- xcpretty (~> 0.2, >= 0.0.7)
-
-PLATFORMS
- aarch64-linux
- arm-linux
- arm64-darwin
- x86-linux
- x86_64-darwin
- x86_64-linux
-
-DEPENDENCIES
- fastlane
- slather
-
-BUNDLED WITH
- 2.4.19
diff --git a/OSInAppBrowserLib.xcodeproj/project.pbxproj b/OSInAppBrowserLib.xcodeproj/project.pbxproj
index cc4d6bd..d915de2 100644
--- a/OSInAppBrowserLib.xcodeproj/project.pbxproj
+++ b/OSInAppBrowserLib.xcodeproj/project.pbxproj
@@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */
1BB4F8BC2C36F54800F50A33 /* OSIABErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BB4F8BB2C36F54800F50A33 /* OSIABErrorView.swift */; };
- 7506BDD22C622B8D00409255 /* OSIABWebView13WrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7506BDD12C622B8D00409255 /* OSIABWebView13WrapperView.swift */; };
750949C02C11D992006843E1 /* OSIABToolbarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750949BE2C11D992006843E1 /* OSIABToolbarPosition.swift */; };
750949C12C11D992006843E1 /* OSIABWebViewOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750949BF2C11D992006843E1 /* OSIABWebViewOptions.swift */; };
750949C32C11D9D0006843E1 /* OSIABWebViewRouterAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750949C22C11D9D0006843E1 /* OSIABWebViewRouterAdapter.swift */; };
@@ -40,6 +39,12 @@
75EF1AFB2C13069E005D7164 /* OSIABViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EF1AFA2C13069E005D7164 /* OSIABViewModelTests.swift */; };
75EF1AFD2C1306B1005D7164 /* OSIABCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EF1AFC2C1306B1005D7164 /* OSIABCacheManagerTests.swift */; };
75EF1B012C134878005D7164 /* OSIABWebViewStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EF1B002C134878005D7164 /* OSIABWebViewStub.swift */; };
+ 887287832E717E8F00D9E41B /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 887287822E717E8F00D9E41B /* ViewInspector */; };
+ 8872878B2E71806200D9E41B /* OSIABWebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872878A2E71806200D9E41B /* OSIABWebViewTests.swift */; };
+ 8872878F2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872878E2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift */; };
+ 887287912E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887287902E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift */; };
+ 887287932E7196C800D9E41B /* OSIABErrorViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887287922E7196C800D9E41B /* OSIABErrorViewTests.swift */; };
+ 887287992E71C2E100D9E41B /* OSIABNavigationViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887287982E71C2E100D9E41B /* OSIABNavigationViewTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -54,7 +59,6 @@
/* Begin PBXFileReference section */
1BB4F8BB2C36F54800F50A33 /* OSIABErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABErrorView.swift; sourceTree = ""; };
- 7506BDD12C622B8D00409255 /* OSIABWebView13WrapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebView13WrapperView.swift; sourceTree = ""; };
750949BE2C11D992006843E1 /* OSIABToolbarPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSIABToolbarPosition.swift; sourceTree = ""; };
750949BF2C11D992006843E1 /* OSIABWebViewOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSIABWebViewOptions.swift; sourceTree = ""; };
750949C22C11D9D0006843E1 /* OSIABWebViewRouterAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSIABWebViewRouterAdapter.swift; sourceTree = ""; };
@@ -87,6 +91,11 @@
75EF1AFA2C13069E005D7164 /* OSIABViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABViewModelTests.swift; sourceTree = ""; };
75EF1AFC2C1306B1005D7164 /* OSIABCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABCacheManagerTests.swift; sourceTree = ""; };
75EF1B002C134878005D7164 /* OSIABWebViewStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebViewStub.swift; sourceTree = ""; };
+ 8872878A2E71806200D9E41B /* OSIABWebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebViewTests.swift; sourceTree = ""; };
+ 8872878E2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebViewRepresentableTests.swift; sourceTree = ""; };
+ 887287902E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABApplicationRouterAdapterTests.swift; sourceTree = ""; };
+ 887287922E7196C800D9E41B /* OSIABErrorViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABErrorViewTests.swift; sourceTree = ""; };
+ 887287982E71C2E100D9E41B /* OSIABNavigationViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABNavigationViewTests.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -102,6 +111,7 @@
buildActionMask = 2147483647;
files = (
7575CF6A2BFCEE6F008F3FD0 /* OSInAppBrowserLib.framework in Frameworks */,
+ 887287832E717E8F00D9E41B /* ViewInspector in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -126,7 +136,6 @@
759027B22C1AFBCB00E02498 /* OSIABNavigationView.swift */,
750949C52C11D9E2006843E1 /* OSIABWebView.swift */,
750949C92C11D9E2006843E1 /* OSIABWebViewRepresentable.swift */,
- 7506BDD12C622B8D00409255 /* OSIABWebView13WrapperView.swift */,
750949C72C11D9E2006843E1 /* OSIABWebViewWrapperView.swift */,
);
path = Views;
@@ -209,6 +218,8 @@
7575CF6D2BFCEE6F008F3FD0 /* OSInAppBrowserLibTests */ = {
isa = PBXGroup;
children = (
+ 887287902E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift */,
+ 8872877F2E717C4600D9E41B /* WebView */,
7575CF7E2BFCEEEA008F3FD0 /* Helper Files */,
75EF1AFC2C1306B1005D7164 /* OSIABCacheManagerTests.swift */,
7575CF7D2BFCEEEA008F3FD0 /* OSIABEngineTests.swift */,
@@ -230,6 +241,25 @@
path = "Helper Files";
sourceTree = "";
};
+ 8872877E2E717C4600D9E41B /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 887287982E71C2E100D9E41B /* OSIABNavigationViewTests.swift */,
+ 887287922E7196C800D9E41B /* OSIABErrorViewTests.swift */,
+ 8872878A2E71806200D9E41B /* OSIABWebViewTests.swift */,
+ 8872878E2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ 8872877F2E717C4600D9E41B /* WebView */ = {
+ isa = PBXGroup;
+ children = (
+ 8872877E2E717C4600D9E41B /* Views */,
+ );
+ path = WebView;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -309,6 +339,9 @@
Base,
);
mainGroup = 7575CF572BFCEE6F008F3FD0;
+ packageReferences = (
+ 887287812E717E8F00D9E41B /* XCRemoteSwiftPackageReference "ViewInspector" */,
+ );
productRefGroup = 7575CF622BFCEE6F008F3FD0 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -371,7 +404,6 @@
750949C02C11D992006843E1 /* OSIABToolbarPosition.swift in Sources */,
750949CA2C11D9E2006843E1 /* OSIABCacheManager.swift in Sources */,
759027B72C1B13CA00E02498 /* OSIABWebViewUIModel.swift in Sources */,
- 7506BDD22C622B8D00409255 /* OSIABWebView13WrapperView.swift in Sources */,
754451F62C04AB69003D7361 /* OSIABViewStyle.swift in Sources */,
756346642C00F21000685AA3 /* OSIABApplicationRouterAdapter.swift in Sources */,
750949CF2C11D9E2006843E1 /* OSIABWebViewRepresentable.swift in Sources */,
@@ -394,12 +426,17 @@
buildActionMask = 2147483647;
files = (
756346482C00DD4700685AA3 /* OSIABSafariViewControllerRouterAdapterTests.swift in Sources */,
+ 8872878F2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift in Sources */,
+ 8872878B2E71806200D9E41B /* OSIABWebViewTests.swift in Sources */,
75EF1AFB2C13069E005D7164 /* OSIABViewModelTests.swift in Sources */,
7575CF812BFCEEEA008F3FD0 /* OSIABRouterSpy.swift in Sources */,
75094A072C121BD9006843E1 /* OSIABCacheManagerStub.swift in Sources */,
+ 887287932E7196C800D9E41B /* OSIABErrorViewTests.swift in Sources */,
+ 887287992E71C2E100D9E41B /* OSIABNavigationViewTests.swift in Sources */,
75EF1AFD2C1306B1005D7164 /* OSIABCacheManagerTests.swift in Sources */,
7575CF802BFCEEEA008F3FD0 /* OSIABEngineTests.swift in Sources */,
75EF1B012C134878005D7164 /* OSIABWebViewStub.swift in Sources */,
+ 887287912E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift in Sources */,
75094A052C1213A3006843E1 /* OSIABWebViewRouterAdapterTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -559,7 +596,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -593,7 +630,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -619,7 +656,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -641,7 +678,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -685,6 +722,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ 887287812E717E8F00D9E41B /* XCRemoteSwiftPackageReference "ViewInspector" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/nalexn/ViewInspector";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 0.10.2;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 887287822E717E8F00D9E41B /* ViewInspector */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 887287812E717E8F00D9E41B /* XCRemoteSwiftPackageReference "ViewInspector" */;
+ productName = ViewInspector;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = 7575CF582BFCEE6F008F3FD0 /* Project object */;
}
diff --git a/OSInAppBrowserLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OSInAppBrowserLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..267a91d
--- /dev/null
+++ b/OSInAppBrowserLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,15 @@
+{
+ "originHash" : "7abf1dd76d62709d50ab97ba7b5907de6a95cbc813d2bf154db0688b92d85914",
+ "pins" : [
+ {
+ "identity" : "viewinspector",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/nalexn/ViewInspector",
+ "state" : {
+ "revision" : "a6fcac8485bc8f57b2d2b55bb6d97138e8659e4b",
+ "version" : "0.10.2"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/OSInAppBrowserLib.xcodeproj/xcshareddata/xcschemes/OSInAppBrowserLibTests.xcscheme b/OSInAppBrowserLib.xcodeproj/xcshareddata/xcschemes/OSInAppBrowserLibTests.xcscheme
new file mode 100644
index 0000000..4780f54
--- /dev/null
+++ b/OSInAppBrowserLib.xcodeproj/xcshareddata/xcschemes/OSInAppBrowserLibTests.xcscheme
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift
index 2c54194..67b48f1 100644
--- a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift
+++ b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift
@@ -1,14 +1,35 @@
import UIKit
-/// Adapter that makes the required calls so that can perform the External Browser routing.
-public class OSIABApplicationRouterAdapter: OSIABRouter {
- public typealias ReturnType = Bool
-
- /// Constructor method.
+public protocol URLOpener {
+ func canOpenURL(_ url: URL) -> Bool
+ func open(_ url: URL, completionHandler: ((Bool) -> Void)?)
+}
+
+public class DefaultURLOpener: URLOpener {
public init() {}
+ public func canOpenURL(_ url: URL) -> Bool {
+ UIApplication.shared.canOpenURL(url)
+ }
+
+ public func open(_ url: URL, completionHandler: ((Bool) -> Void)?) {
+ UIApplication.shared.open(url, completionHandler: completionHandler ?? { _ in })
+ }
+}
+
+public class OSIABApplicationRouterAdapter: OSIABRouter {
+ public typealias ReturnType = Bool
+
+ private let urlOpener: URLOpener
+
+ public init(urlOpener: URLOpener = DefaultURLOpener()) {
+ self.urlOpener = urlOpener
+ }
+
public func handleOpen(_ url: URL, _ completionHandler: @escaping (ReturnType) -> Void) {
- guard UIApplication.shared.canOpenURL(url) else { return completionHandler(false) }
- UIApplication.shared.open(url, completionHandler: completionHandler)
+ guard urlOpener.canOpenURL(url) else {
+ return completionHandler(false)
+ }
+ urlOpener.open(url, completionHandler: completionHandler)
}
}
diff --git a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift
index 0a5ab4b..e8fe3ba 100644
--- a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift
+++ b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift
@@ -53,11 +53,7 @@ public class OSIABWebViewRouterAdapter: NSObject, OSIABRouter {
let dismissCallback: () -> Void = { self.callbackHandler.onBrowserClosed(true) }
let hostingController: UIViewController
- if #available(iOS 14.0, *) {
- hostingController = OSIABWebViewController(rootView: .init(viewModel), dismiss: dismissCallback)
- } else {
- hostingController = OSIABWebView13Controller(rootView: .init(viewModel), dismiss: dismissCallback)
- }
+ hostingController = OSIABWebViewController(rootView: .init(viewModel), dismiss: dismissCallback)
hostingController.modalPresentationStyle = options.modalPresentationStyle
hostingController.modalTransitionStyle = options.modalTransitionStyle
hostingController.presentationController?.delegate = self
@@ -127,31 +123,3 @@ private class OSIABWebViewController: UIHostingController {
- /// Callback to trigger when the view controller is closed.
- let dismiss: (() -> Void)?
-
- /// Constructor method.
- /// - Parameters:
- /// - rootView: The root view of the SwiftUI view hierarchy that you want to manage using the hosting view controller.
- /// - dismiss: The callback to trigger when the view controller is dismissed.
- init(rootView: OSIABWebView13WrapperView, dismiss: (() -> Void)?) {
- self.dismiss = dismiss
- super.init(rootView: rootView)
- }
-
- @MainActor required dynamic init?(coder aDecoder: NSCoder) {
- self.dismiss = nil
- super.init(coder: aDecoder)
- }
-
- override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
- super.dismiss(animated: flag, completion: {
- self.dismiss?()
- completion?()
- })
- }
-}
diff --git a/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift b/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift
index a4323bd..91322ee 100644
--- a/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift
+++ b/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift
@@ -38,7 +38,14 @@ class OSIABWebViewModel: NSObject, ObservableObject {
@Published private(set) var addressLabel: String = ""
private var cancellables = Set()
-
+
+ #if DEBUG
+ /// Test-only method to set error for unit tests
+ func setErrorForTesting(_ error: Error?) {
+ self.error = error
+ }
+ #endif
+
/// Constructor method.
/// - Parameters:
/// - url: The current URL being displayed
diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift
index e9f8e31..bce82d1 100644
--- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift
+++ b/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift
@@ -1,12 +1,12 @@
import SwiftUI
-private struct OSIABNavigationButton: View {
+internal struct OSIABNavigationButton: View {
/// Handler to trigger when the button is pressed.
private let buttonPressed: () -> Void
/// The icon to set the button with.
private let iconName: String
/// Indicates if the button should appeared as enabled or not.
- private let isDisabled: Bool
+ internal let isDisabled: Bool
/// Constructor method.
/// - Parameters:
@@ -88,7 +88,7 @@ struct OSIABNavigationView: View {
OSIABNavigationButton(
forwardButtonPressed,
- iconName: "chevron.forward",
+ iconName: "chevron.forward",
isDisabled: !forwardButtonEnabled
)
}
@@ -151,6 +151,7 @@ private struct OSIABTestNavigationView: View {
}
}
+@available(*, unavailable)
struct OSIABNavigationView_Previews: PreviewProvider {
static var previews: some View {
// Default - Light Mode
diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift
index 161147d..6d5f389 100644
--- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift
+++ b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift
@@ -151,6 +151,7 @@ private struct OSIABTestWebView: View {
}
}
+@available(*, unavailable)
struct OSIABWebView_Previews: PreviewProvider {
static var previews: some View {
// MARK: - Default Views
diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView13WrapperView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView13WrapperView.swift
deleted file mode 100644
index fba1aa9..0000000
--- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView13WrapperView.swift
+++ /dev/null
@@ -1,104 +0,0 @@
-import SwiftUI
-import UIKit
-
-/// View that manages which view to present, depending if the page load was successful or not or is being loaded.
-@available(iOS, deprecated: 14.0, message: "Use OSIABWebViewWrapperView for iOS 14.0+")
-struct OSIABWebView13WrapperView: View {
- /// View Model containing all the customisable elements.
- @ObservedObject private var model: OSIABWebViewModel
-
- /// Constructor method.
- /// - Parameter model: View Model containing all the customisable elements.
- init(_ model: OSIABWebViewModel) {
- self._model = ObservedObject(wrappedValue: model)
- }
-
- var body: some View {
- ZStack {
- OSIABWebView(model)
- .edgesIgnoringSafeArea(model.toolbarPosition == .bottom ? [] : .bottom)
- if model.isLoading {
- OSIABActivityIndicator(isAnimating: .constant(true), style: .large)
- }
- }
- }
-}
-
-private struct OSIABActivityIndicator: UIViewRepresentable {
- @Binding private var isAnimating: Bool
- private let style: UIActivityIndicatorView.Style
-
- init(isAnimating: Binding, style: UIActivityIndicatorView.Style) {
- self._isAnimating = isAnimating
- self.style = style
- }
-
- func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView {
- return UIActivityIndicatorView(style: style)
- }
-
- func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) {
- if self.isAnimating {
- uiView.startAnimating()
- } else {
- uiView.stopAnimating()
- }
- }
-}
-
-// MARK: - OSIABViewModel's constructor accelerator.
-private extension OSIABWebViewModel {
- convenience init(toolbarPosition: OSIABToolbarPosition = .defaultValue) {
- let configurationModel = OSIABWebViewConfigurationModel()
- self.init(
- url: .init(string: "https://outsystems.com")!,
- webViewConfiguration: configurationModel.toWebViewConfiguration(),
- uiModel: .init(toolbarPosition: toolbarPosition),
- callbackHandler: .init(
- onDelegateURL: { _ in },
- onDelegateAlertController: { _ in },
- onBrowserPageLoad: {},
- onBrowserClosed: { _ in },
- onBrowserPageNavigationCompleted: { _ in }
- )
- )
- }
-
- convenience init(url: String) {
- let configurationModel = OSIABWebViewConfigurationModel()
- self.init(
- url: .init(string: url)!,
- webViewConfiguration: configurationModel.toWebViewConfiguration(),
- uiModel: .init(),
- callbackHandler: .init(
- onDelegateURL: { _ in },
- onDelegateAlertController: { _ in },
- onBrowserPageLoad: {},
- onBrowserClosed: { _ in },
- onBrowserPageNavigationCompleted: { _ in }
- )
- )
- }
-}
-
-struct OSIABWebView13WrapperView_Previews: PreviewProvider {
- static var previews: some View {
-
- // Default - Light Mode
- OSIABWebView13WrapperView(.init())
-
- // Default - Dark Mode
- OSIABWebView13WrapperView(.init())
- .preferredColorScheme(.dark)
-
- // Bottom Toolbar Defined
- OSIABWebView13WrapperView(.init(toolbarPosition: .bottom))
-
- // Error View - Light mode
- OSIABWebView13WrapperView(.init(url: "https://outsystems/"))
-
- // Error View - Dark mode
- OSIABWebView13WrapperView(.init(url: "https://outsystems/"))
- .preferredColorScheme(.dark)
- }
-}
diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift
index 40adc94..75fdcff 100644
--- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift
+++ b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift
@@ -1,7 +1,6 @@
import SwiftUI
/// View that manages which view to present, depending if the page load was successful or not or is being loaded.
-@available(iOS 14.0, *)
struct OSIABWebViewWrapperView: View {
/// View Model containing all the customisable elements.
@StateObject private var model: OSIABWebViewModel
@@ -23,59 +22,40 @@ struct OSIABWebViewWrapperView: View {
}
}
-// MARK: - OSIABViewModel's constructor accelerator.
-private extension OSIABWebViewModel {
- convenience init(toolbarPosition: OSIABToolbarPosition = .defaultValue) {
- let configurationModel = OSIABWebViewConfigurationModel()
- self.init(
- url: .init(string: "https://outsystems.com")!,
- webViewConfiguration: configurationModel.toWebViewConfiguration(),
- uiModel: .init(toolbarPosition: toolbarPosition),
- callbackHandler: .init(
- onDelegateURL: { _ in },
- onDelegateAlertController: { _ in },
- onBrowserPageLoad: {},
- onBrowserClosed: { _ in },
- onBrowserPageNavigationCompleted: { _ in }
- )
- )
- }
-
- convenience init(url: String) {
- let configurationModel = OSIABWebViewConfigurationModel()
- self.init(
- url: .init(string: url)!,
- webViewConfiguration: configurationModel.toWebViewConfiguration(),
- uiModel: .init(),
- callbackHandler: .init(
- onDelegateURL: { _ in },
- onDelegateAlertController: { _ in },
- onBrowserPageLoad: {},
- onBrowserClosed: { _ in },
- onBrowserPageNavigationCompleted: { _ in }
- )
- )
- }
-}
-
-@available(iOS 14.0, *)
+@available(*, unavailable)
struct OSIABWebViewWrapperView_Previews: PreviewProvider {
static var previews: some View {
// Default - Light Mode
- OSIABWebViewWrapperView(.init())
+ OSIABWebViewWrapperView(makeModel())
// Default - Dark Mode
- OSIABWebViewWrapperView(.init())
+ OSIABWebViewWrapperView(makeModel())
.preferredColorScheme(.dark)
// Bottom Toolbar Defined
- OSIABWebViewWrapperView(.init(toolbarPosition: .bottom))
+ OSIABWebViewWrapperView(makeModel(toolbarPosition: .bottom))
// Error View - Light mode
- OSIABWebViewWrapperView(.init(url: "https://outsystems/"))
+ OSIABWebViewWrapperView(makeModel(url: "https://outsystems/"))
// Error View - Dark mode
- OSIABWebViewWrapperView(.init(url: "https://outsystems/"))
+ OSIABWebViewWrapperView(makeModel(url: "https://outsystems/"))
.preferredColorScheme(.dark)
}
+
+ private static func makeModel(url: String = "https://outsystems.com", toolbarPosition: OSIABToolbarPosition = .defaultValue) -> OSIABWebViewModel {
+ let configurationModel = OSIABWebViewConfigurationModel()
+ return .init(
+ url: .init(string: url)!,
+ webViewConfiguration: configurationModel.toWebViewConfiguration(),
+ uiModel: .init(toolbarPosition: toolbarPosition),
+ callbackHandler: .init(
+ onDelegateURL: { _ in },
+ onDelegateAlertController: { _ in },
+ onBrowserPageLoad: {},
+ onBrowserClosed: { _ in },
+ onBrowserPageNavigationCompleted: { _ in }
+ )
+ )
+ }
}
diff --git a/Tests/OSInAppBrowserLibTests/OSIABApplicationRouterAdapterTests.swift b/Tests/OSInAppBrowserLibTests/OSIABApplicationRouterAdapterTests.swift
new file mode 100644
index 0000000..a38ac3a
--- /dev/null
+++ b/Tests/OSInAppBrowserLibTests/OSIABApplicationRouterAdapterTests.swift
@@ -0,0 +1,62 @@
+import XCTest
+@testable import OSInAppBrowserLib
+
+class MockURLOpener: URLOpener {
+ var canOpenURLCalled = false
+ var openCalled = false
+
+ var canOpenURLResult = true
+ var lastOpenedURL: URL?
+ var completionResult = true
+
+ func canOpenURL(_ url: URL) -> Bool {
+ canOpenURLCalled = true
+ return canOpenURLResult
+ }
+
+ func open(_ url: URL, completionHandler: ((Bool) -> Void)?) {
+ openCalled = true
+ lastOpenedURL = url
+ completionHandler?(completionResult)
+ }
+}
+
+final class OSIABApplicationRouterAdapterTests: XCTestCase {
+
+ func test_handleOpen_whenCanOpenURLIsFalse_shouldCallCompletionWithFalse() {
+ let mock = MockURLOpener()
+ mock.canOpenURLResult = false
+ let sut = OSIABApplicationRouterAdapter(urlOpener: mock)
+
+ let expectation = XCTestExpectation(description: "Completion called")
+
+ sut.handleOpen(URL(string: "https://example.com")!) { success in
+ XCTAssertFalse(success)
+ XCTAssertTrue(mock.canOpenURLCalled)
+ XCTAssertFalse(mock.openCalled)
+ expectation.fulfill()
+ }
+
+ wait(for: [expectation], timeout: 1)
+ }
+
+ func test_handleOpen_whenCanOpenURLIsTrue_shouldOpenURL() {
+ let mock = MockURLOpener()
+ mock.canOpenURLResult = true
+ mock.completionResult = true
+ let sut = OSIABApplicationRouterAdapter(urlOpener: mock)
+
+ let url = URL(string: "https://example.com")!
+ let expectation = XCTestExpectation(description: "Completion called")
+
+ sut.handleOpen(url) { success in
+ XCTAssertTrue(success)
+ XCTAssertTrue(mock.canOpenURLCalled)
+ XCTAssertTrue(mock.openCalled)
+ XCTAssertEqual(mock.lastOpenedURL, url)
+ expectation.fulfill()
+ }
+
+ wait(for: [expectation], timeout: 1)
+ }
+}
diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABErrorViewTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABErrorViewTests.swift
new file mode 100644
index 0000000..c43e042
--- /dev/null
+++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABErrorViewTests.swift
@@ -0,0 +1,49 @@
+import XCTest
+import SwiftUI
+import ViewInspector
+@testable import OSInAppBrowserLib
+
+extension OSIABErrorView: Inspectable {}
+
+final class OSIABErrorViewTests: XCTestCase {
+ func testShowsErrorMessage() throws {
+ let view = OSIABErrorView(
+ NSError(domain: "Test", code: 1, userInfo: nil),
+ reload: {},
+ reloadViewLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let vStack = try view.inspect().vStack()
+ let text = try vStack.text(0).string()
+ XCTAssertEqual(text, "Couldn't load the page content.")
+ }
+
+ func testReloadButtonAction() throws {
+ var reloaded = false
+ let view = OSIABErrorView(
+ NSError(domain: "Test", code: 1, userInfo: nil),
+ reload: { reloaded = true },
+ reloadViewLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let vStack = try view.inspect().vStack()
+ let hStack = try vStack.hStack(1)
+ let button = try hStack.button(0)
+ try button.tap()
+ XCTAssertTrue(reloaded)
+ }
+
+ func testReloadButtonLabel() throws {
+ let view = OSIABErrorView(
+ NSError(domain: "Test", code: 1, userInfo: nil),
+ reload: {},
+ reloadViewLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let vStack = try view.inspect().vStack()
+ let hStack = try vStack.hStack(1)
+ let button = try hStack.button(0)
+ let labelHStack = try button.labelView().hStack()
+ let imageName = try labelHStack.image(0).actualImage().name()
+ let text = try labelHStack.text(1).string()
+ XCTAssertEqual(imageName, "arrow.clockwise")
+ XCTAssertEqual(text, "Reload page")
+ }
+}
diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABNavigationViewTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABNavigationViewTests.swift
new file mode 100644
index 0000000..0ae1a40
--- /dev/null
+++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABNavigationViewTests.swift
@@ -0,0 +1,120 @@
+import XCTest
+import SwiftUI
+import ViewInspector
+@testable import OSInAppBrowserLib
+
+extension OSIABNavigationView: Inspectable {}
+
+final class OSIABNavigationViewTests: XCTestCase {
+ func testNavigationButtonsAreVisibleWhenEnabled() throws {
+ let view = OSIABNavigationView(
+ showNavigationButtons: true,
+ backButtonPressed: {},
+ backButtonEnabled: true,
+ forwardButtonPressed: {},
+ forwardButtonEnabled: true,
+ addressLabel: "Test URL",
+ addressLabelAlignment: .trailing,
+ buttonLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let hStack = try view.inspect().hStack()
+ XCTAssertNoThrow(try hStack.find(OSIABNavigationButton.self))
+ }
+
+ func testNavigationButtonsAreHiddenWhenDisabled() throws {
+ let view = OSIABNavigationView(
+ showNavigationButtons: false,
+ backButtonPressed: {},
+ backButtonEnabled: true,
+ forwardButtonPressed: {},
+ forwardButtonEnabled: true,
+ addressLabel: "Test URL",
+ addressLabelAlignment: .trailing,
+ buttonLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let hStack = try view.inspect().hStack()
+ XCTAssertThrowsError(try hStack.find(OSIABNavigationButton.self))
+ }
+
+ func testBackButtonIsDisabled() throws {
+ let view = OSIABNavigationView(
+ showNavigationButtons: true,
+ backButtonPressed: {},
+ backButtonEnabled: false,
+ forwardButtonPressed: {},
+ forwardButtonEnabled: true,
+ addressLabel: "Test URL",
+ addressLabelAlignment: .trailing,
+ buttonLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let backButton = try view.inspect().findAll(OSIABNavigationButton.self)[0]
+ let isDisabled = try backButton.actualView().isDisabled
+ XCTAssertTrue(isDisabled)
+ }
+
+ func testForwardButtonIsDisabled() throws {
+ let view = OSIABNavigationView(
+ showNavigationButtons: true,
+ backButtonPressed: {},
+ backButtonEnabled: true,
+ forwardButtonPressed: {},
+ forwardButtonEnabled: false,
+ addressLabel: "Test URL",
+ addressLabelAlignment: .trailing,
+ buttonLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let forwardButton = try view.inspect().findAll(OSIABNavigationButton.self)[1]
+ let isDisabled = try forwardButton.actualView().isDisabled
+ XCTAssertTrue(isDisabled)
+ }
+
+ func testAddressLabelIsVisibleAndCorrect() throws {
+ let label = "Test URL"
+ let view = OSIABNavigationView(
+ showNavigationButtons: false,
+ backButtonPressed: {},
+ backButtonEnabled: true,
+ forwardButtonPressed: {},
+ forwardButtonEnabled: true,
+ addressLabel: label,
+ addressLabelAlignment: .center,
+ buttonLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let text = try view.inspect().find(ViewType.Text.self).string()
+ XCTAssertEqual(text, label)
+ }
+
+ func testBackButtonActionIsCalled() throws {
+ let exp = expectation(description: "Back button pressed")
+ let view = OSIABNavigationView(
+ showNavigationButtons: true,
+ backButtonPressed: { exp.fulfill() },
+ backButtonEnabled: true,
+ forwardButtonPressed: {},
+ forwardButtonEnabled: true,
+ addressLabel: "Test URL",
+ addressLabelAlignment: .trailing,
+ buttonLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let backButton = try view.inspect().findAll(OSIABNavigationButton.self)[0]
+ try backButton.button().tap()
+ wait(for: [exp], timeout: 1)
+ }
+
+ func testForwardButtonActionIsCalled() throws {
+ let exp = expectation(description: "Forward button pressed")
+ let view = OSIABNavigationView(
+ showNavigationButtons: true,
+ backButtonPressed: {},
+ backButtonEnabled: true,
+ forwardButtonPressed: { exp.fulfill() },
+ forwardButtonEnabled: true,
+ addressLabel: "Test URL",
+ addressLabelAlignment: .trailing,
+ buttonLayoutDirection: .fixed(value: .leftToRight)
+ )
+ let forwardButton = try view.inspect().findAll(OSIABNavigationButton.self)[1]
+ try forwardButton.button().tap()
+ wait(for: [exp], timeout: 1)
+ }
+}
diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewRepresentableTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewRepresentableTests.swift
new file mode 100644
index 0000000..87eb8d6
--- /dev/null
+++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewRepresentableTests.swift
@@ -0,0 +1,12 @@
+import XCTest
+import WebKit
+@testable import OSInAppBrowserLib
+
+final class OSIABWebViewRepresentableTests: XCTestCase {
+
+ func testInitializerStoresInjectedWebView() {
+ let webView = WKWebView()
+ let representable = OSIABWebViewRepresentable(webView)
+ XCTAssertNotNil(representable)
+ }
+}
diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewTests.swift
new file mode 100644
index 0000000..f9f2241
--- /dev/null
+++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewTests.swift
@@ -0,0 +1,155 @@
+import XCTest
+import ViewInspector
+@testable import OSInAppBrowserLib
+
+final class OSIABWebViewTests: XCTestCase {
+ func testToolbarAppearsAtTop() throws {
+ let model = OSIABWebViewModel(
+ url: URL(string: "https://test.com")!,
+ webViewConfiguration: .init(),
+ uiModel: .init(
+ showURL: true,
+ showToolbar: true,
+ toolbarPosition: .top,
+ showNavigationButtons: true,
+ leftToRight: false,
+ closeButtonText: "Close"
+ ),
+ callbackHandler: .init(
+ onDelegateURL: { _ in },
+ onDelegateAlertController: { _ in },
+ onBrowserPageLoad: {},
+ onBrowserClosed: { _ in },
+ onBrowserPageNavigationCompleted: { _ in }
+ )
+ )
+ let view = OSIABWebView(model)
+ let vStack = try view.inspect().vStack()
+ // Toolbar should be the first element
+ XCTAssertNoThrow(try vStack.hStack(0))
+ }
+
+ func testToolbarAppearsAtBottom() throws {
+ let model = OSIABWebViewModel(
+ url: URL(string: "https://test.com")!,
+ webViewConfiguration: .init(),
+ uiModel: .init(
+ showURL: true,
+ showToolbar: true,
+ toolbarPosition: .bottom,
+ showNavigationButtons: true,
+ leftToRight: false,
+ closeButtonText: "Close"
+ ),
+ callbackHandler: .init(
+ onDelegateURL: { _ in },
+ onDelegateAlertController: { _ in },
+ onBrowserPageLoad: {},
+ onBrowserClosed: { _ in },
+ onBrowserPageNavigationCompleted: { _ in }
+ )
+ )
+ let view = OSIABWebView(model)
+ let vStack = try view.inspect().vStack()
+ // Toolbar should be the last element
+ XCTAssertNoThrow(try vStack.view(OSIABNavigationView.self, 2))
+ }
+
+ func testErrorViewAppears() throws {
+ let model = OSIABWebViewModel(
+ url: URL(string: "https://test.com")!,
+ webViewConfiguration: .init(),
+ uiModel: .init(
+ showURL: true,
+ showToolbar: true,
+ toolbarPosition: .top,
+ showNavigationButtons: true,
+ leftToRight: false,
+ closeButtonText: "Close"
+ ),
+ callbackHandler: .init(
+ onDelegateURL: { _ in },
+ onDelegateAlertController: { _ in },
+ onBrowserPageLoad: {},
+ onBrowserClosed: { _ in },
+ onBrowserPageNavigationCompleted: { _ in }
+ )
+ )
+ // Simulate error using test-only method
+ model.setErrorForTesting(NSError(domain: "Test", code: 1, userInfo: nil))
+ let view = OSIABWebView(model)
+ let vStack = try view.inspect().vStack()
+ // Error view should be present
+ XCTAssertNoThrow(try vStack.view(OSIABErrorView.self, 1))
+ }
+
+ func testCloseButtonAction() throws {
+ var closed = false
+ let model = OSIABWebViewModel(
+ url: URL(string: "https://test.com")!,
+ webViewConfiguration: .init(),
+ uiModel: .init(
+ showURL: true,
+ showToolbar: true,
+ toolbarPosition: .top,
+ showNavigationButtons: true,
+ leftToRight: false,
+ closeButtonText: "Close"
+ ),
+ callbackHandler: .init(
+ onDelegateURL: { _ in },
+ onDelegateAlertController: { _ in },
+ onBrowserPageLoad: {},
+ onBrowserClosed: { _ in closed = true },
+ onBrowserPageNavigationCompleted: { _ in }
+ )
+ )
+ let view = OSIABWebView(model)
+ let vStack = try view.inspect().vStack()
+ let hStack = try vStack.hStack(0)
+ // Find the button with label "Close"
+ var found = false
+ for idx in 0..