feat: automated mobile deployments rd2 (#498)

* migrate build logic from previous branch

* fix command

* move .actrc file

* clean up

* use env vars

* setup provisioning profile path within action

* fix flow

* fix fastfile flow and update react native

* disable play store uploading

* hard code xcode version

* fixes

* more provisioning debugging

* fix keychain path

* set keychain to what was created by the github action

* attempt to build again

* test fix

* print xcode build settings

* debug ios build

* fix xcargs path

* use manual code signing

* save wip

* fix building locally

* fix variable

* save wip

* clean up long comand

* clean up

* install bundle and gems

* install pods

* fix pod installation

* sort

* better naming

* fix android issues

* update lock

* clean up artifacts

* format

* save wip slack upload logic

* prettier

* fix indent

* save wip

* save wip

* save wip

* save wip

* save wip

* clean up

* simplify slack calls

* revert slack logic

* save working slack upload example

* make title nicer

* clean up slack upload

* upload paths

* enable github commit

* fix path

* fix commit step

* fix git committing

* update markdown

* fix git commit rule

* better commit message

* chore: incrementing ios build number for version 2.4.9 [skip ci]

* better name

---------

Co-authored-by: Self GitHub Actions <action@github.com>
This commit is contained in:
Justin Hernandez
2025-04-11 12:51:02 -05:00
committed by GitHub
parent 5c450285c4
commit a3dc3bcfd1
24 changed files with 2722 additions and 161 deletions

3
.actrc Normal file
View File

@@ -0,0 +1,3 @@
--container-architecture linux/amd64
--platform macos-latest=catthehacker/ubuntu:runner-latest
--secret-file ./app/fastlane/.env.secrets

17
.github/actions/get-version/action.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Get Version from package.json
description: "Gets the version from package.json and sets it as an environment variable"
inputs:
app_path:
description: "Path to the app directory"
required: true
runs:
using: "composite"
steps:
- name: Get version from package.json
shell: bash
run: |
VERSION=$(node -p "require('${{ inputs.app_path }}/package.json').version")
echo "VERSION=$VERSION" >> $GITHUB_ENV

56
.github/actions/mobile-setup/action.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Setup Mobile Environment
description: "Sets up the environment for mobile app builds"
inputs:
app_path:
description: "Path to the app directory"
required: true
node_version:
description: "Node version"
required: true
ruby_version:
description: "Ruby version"
required: true
workspace:
description: "Workspace directory path"
required: true
runs:
using: "composite"
steps:
- name: Install locales and dialog for local development
if: ${{ env.ACT }}
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y locales dialog unzip
# for fastlane
- name: Install locales and dialog
if: runner.os != 'macOS'
shell: bash
run: |
sudo locale-gen en_US.UTF-8
sudo update-locale LANG=en_US.UTF-8
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Ruby environment
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ inputs.ruby_version }}
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node_version }}
- name: Install app dependencies
shell: bash
run: |
cd ${{ inputs.app_path }}
corepack enable
yarn install
yarn install-app:deploy

87
.github/actions/push-changes/action.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Push Build Version Changes
description: "Commits and pushes build version changes for mobile platforms"
inputs:
commit_message:
description: "Commit message"
required: true
commit_paths:
description: "Space-separated list of paths to check for changes (e.g. 'ios/file.txt android/another/file.xml')"
required: true
runs:
using: "composite"
steps:
- name: Configure Git
shell: bash
run: |
set -e
git config --global user.email "action@github.com"
git config --global user.name "Self GitHub Actions"
- name: Commit Changes
shell: bash
run: |
set -e
set -x
# Restore the logic for checking specific paths existence
commit_paths_input="${{ inputs.commit_paths }}"
paths_to_commit=""
for path in $commit_paths_input; do
if [ ! -e "$path" ]; then
echo "Error: Path $path does not exist"
exit 1
fi
paths_to_commit="$paths_to_commit $path"
done
if [ -z "$paths_to_commit" ]; then
echo "No valid paths provided."
exit 1
fi
# Remove leading space if present
paths_to_commit=$(echo "$paths_to_commit" | sed 's/^ *//')
# Stage ONLY the specified paths
git add $paths_to_commit
# Check if there are staged changes ONLY in the specified paths
if git diff --staged --quiet -- $paths_to_commit; then
echo "No changes to commit in the specified paths: $paths_to_commit"
else
echo "Changes to be committed in paths: $paths_to_commit"
# Show the staged diff for the specified paths
git diff --cached -- $paths_to_commit
git commit -m "chore: ${{ inputs.commit_message }} [github action]"
fi
- name: Push Changes
shell: bash
run: |
set -e
set -x
if git rev-parse --verify HEAD >/dev/null 2>&1; then
if [[ ${{ github.ref }} == refs/pull/* ]]; then
CURRENT_BRANCH=${{ github.head_ref }}
else
CURRENT_BRANCH=$(echo ${{ github.ref }} | sed 's|refs/heads/||')
fi
echo "Pushing changes to branch: $CURRENT_BRANCH"
# Add --autostash to handle potential unstaged changes gracefully
git pull --rebase --autostash origin $CURRENT_BRANCH || {
echo "Failed to pull from $CURRENT_BRANCH"
exit 1
}
git push origin HEAD:$CURRENT_BRANCH || {
echo "Failed to push to $CURRENT_BRANCH"
exit 1
}
else
echo "No new commits to push"
fi

486
.github/workflows/mobile-deploy.yml vendored Normal file
View File

@@ -0,0 +1,486 @@
name: Mobile App Deployments
env:
# Branch configuration
IS_PR: ${{ github.event.pull_request.number != null }}
STAGING_BRANCH: dev
MAIN_BRANCH: main
# Build environment versions
NODE_VERSION: 18
RUBY_VERSION: 3.2
JAVA_VERSION: 17
ANDROID_API_LEVEL: 35
ANDROID_NDK_VERSION: 26.1.10909125
# Path configuration
WORKSPACE: ${{ github.workspace }}
APP_PATH: ${{ github.workspace }}/app
# Certificate/keystore paths
ANDROID_KEYSTORE_PATH: /android/app/upload-keystore.jks
ANDROID_PLAY_STORE_JSON_KEY_PATH: /android/play-store-key.json
IOS_DIST_CERT_PATH: /ios/certs/dist_cert.p12
IOS_CONNECT_API_KEY_PATH: /ios/certs/connect_api_key.p8
IOS_PROV_PROFILE_PROJ_PATH: /ios/certs/profile.mobileprovision
IOS_PROV_PROFILE_DIRECTORY: "~/Library/MobileDevice/Provisioning\ Profiles/"
permissions:
contents: write
pull-requests: write
on:
push:
branches:
- dev
- main
paths:
- "app/**"
- ".github/workflows/mobile-deploy.yml"
pull_request:
paths:
- "app/**"
- ".github/workflows/mobile-deploy.yml"
jobs:
build-ios:
runs-on: macos-latest
steps:
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
# some cocoapods won't compile with xcode 16.3
xcode-version: "16.2"
- uses: actions/checkout@v4
- name: Install Mobile Dependencies
uses: ./.github/actions/mobile-setup
with:
app_path: ${{ env.APP_PATH }}
node_version: ${{ env.NODE_VERSION }}
ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }}
- name: Verify iOS Secrets
run: |
# Verify App Store Connect API Key exists and contains PEM header
if [ -z "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" ]; then
echo "❌ Error: App Store Connect API Key cannot be empty"
exit 1
fi
# Verify Issuer ID is in correct format (UUID)
if ! echo "${{ secrets.IOS_CONNECT_ISSUER_ID }}" | grep -E "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" >/dev/null; then
echo "❌ Error: Invalid App Store Connect Issuer ID format (should be UUID)"
exit 1
fi
# Verify Key ID is in correct format (alphanumeric)
if ! echo "${{ secrets.IOS_CONNECT_KEY_ID }}" | grep -E "^[A-Z0-9]{10}$" >/dev/null; then
echo "❌ Error: Invalid App Store Connect Key ID format"
exit 1
fi
# Verify P12 password is not empty and meets basic security requirements
if [ -z "${{ secrets.IOS_P12_PASSWORD }}" ]; then
echo "❌ Error: P12 password cannot be empty"
exit 1
fi
# Verify base64 secrets are not empty
if [ -z "${{ secrets.IOS_DIST_CERT_BASE64 }}" ]; then
echo "❌ Error: Distribution certificate cannot be empty"
exit 1
fi
if [ -z "${{ secrets.IOS_PROV_PROFILE_BASE64 }}" ]; then
echo "❌ Error: Provisioning profile cannot be empty"
exit 1
fi
echo "✅ All iOS secrets verified successfully!"
- name: Decode certificate and profile
run: |
mkdir -p "${{ env.APP_PATH }}$(dirname "${{ env.IOS_DIST_CERT_PATH }}")"
echo "${{ secrets.IOS_DIST_CERT_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}
echo "${{ secrets.IOS_PROV_PROFILE_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}
echo "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }}
# for debugging...which can take some time :(
- name: Verify ios secret checksums
if: false # for debugging
run: |
echo "SHA256 of dist_cert.p12:"
shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}
echo "SHA256 of profile.mobileprovision:"
shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}
echo "SHA256 of connect_api_key.p8:"
shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }}
echo "Certificate file size:"
ls -l ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}
echo "SHA256 of password:"
echo -n "${{ secrets.IOS_P12_PASSWORD }}" | shasum -a 256
echo "SHA256 of connect_api_key_base64:"
echo -n "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" | shasum -a 256
echo "Verifying certificate..."
if openssl pkcs12 -in ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -password pass:'${{ secrets.IOS_P12_PASSWORD }}' -info >/dev/null 2>&1 || openssl pkcs12 -in ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -password pass:'${{ secrets.IOS_P12_PASSWORD }}' -info 2>&1 | grep -q "MAC:"; then
echo "✅ Certificate verification successful (algorithm warning can be safely ignored)"
else
echo "❌ Certificate verification failed - please check certificate validity"
exit 1
fi
- name: Verify iOS certificate and environment
if: ${{ !env.ACT }}
run: |
# Check if certificate directory exists
if [ ! -d "${{ env.APP_PATH }}/ios/certs" ]; then
echo "❌ Error: iOS certificates directory not found at ${{ env.APP_PATH }}/ios/certs"
exit 1
fi
# Check if certificate file exists
if [ ! -f "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" ]; then
echo "❌ Error: Distribution certificate not found at ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}"
exit 1
fi
# Check certificate file permissions
CERT_PERMS=$(ls -l "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" | awk '{print $1}')
if [ "$CERT_PERMS" != "-rw-r--r--" ]; then
echo "❌ Error: Distribution certificate has incorrect permissions: $CERT_PERMS"
echo "Expected: -rw-r--r--"
exit 1
fi
# Check certificate file size
CERT_SIZE=$(stat -f%z "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" 2>/dev/null || stat -c%s "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}")
if [ "$CERT_SIZE" -lt 1000 ]; then
echo "❌ Error: Distribution certificate file size ($CERT_SIZE bytes) is suspiciously small"
exit 1
fi
# Check if we can create a test keychain
TEST_KEYCHAIN="test.keychain"
if ! security create-keychain -p "" "$TEST_KEYCHAIN" >/dev/null 2>&1; then
echo "❌ Error: Unable to create test keychain. Check permissions."
exit 1
fi
security delete-keychain "$TEST_KEYCHAIN" >/dev/null 2>&1
echo "✅ Certificate and environment verification passed!"
- name: Install certificate
if: ${{ !env.ACT }}
run: |
security create-keychain -p "" build.keychain >/dev/null 2>&1
security default-keychain -s build.keychain >/dev/null 2>&1
security unlock-keychain -p "" build.keychain >/dev/null 2>&1
security import ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -k build.keychain -P '${{ secrets.IOS_P12_PASSWORD }}' -T /usr/bin/codesign >/dev/null 2>&1
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain >/dev/null 2>&1
- name: Install provisioning profile
if: ${{ !env.ACT }}
env:
IOS_APP_IDENTIFIER: ${{ secrets.IOS_APP_IDENTIFIER }}
IOS_PROV_PROFILE_NAME: ${{ secrets.IOS_PROV_PROFILE_NAME }}
IOS_PROV_PROFILE_PATH: ${{ env.IOS_PROV_PROFILE_PATH }}
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
run: |
# Verify file exists before proceeding
echo "Checking for provisioning profile at: ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}"
ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}"
if [ ! -f "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" ]; then
echo "❌ Error: Provisioning profile not found at specified path."
exit 1
fi
echo "Provisioning profile found."
# Print file details
echo "Provisioning Profile File Details:"
echo "--------------------------------"
echo "File size: $(stat -f%z "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" 2>/dev/null || stat -c%s "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}") bytes"
echo "File permissions: $(ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" | awk '{print $1}')"
echo "File owner: $(ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" | awk '{print $3}')"
echo "--------------------------------"
# Create a temporary plist file to extract UUID
TEMP_PLIST_PATH=$(mktemp /tmp/profile_plist.XXXXXX)
# Extract plist from mobileprovision file
echo "Extracting plist from provisioning profile..."
security cms -D -i "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" -o "$TEMP_PLIST_PATH"
if [ $? -ne 0 ]; then
echo "❌ Error: Failed to extract plist from provisioning profile"
exit 1
fi
# Extract UUID and profile name from plist
echo "Extracting UUID and profile name from plist..."
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" "$TEMP_PLIST_PATH" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$PROFILE_UUID" ]; then
echo "❌ Error: Failed to extract UUID from provisioning profile"
cat "$TEMP_PLIST_PATH" | head -20
exit 1
fi
# Extract the actual profile name from within the file
PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$TEMP_PLIST_PATH" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$PROFILE_NAME" ]; then
echo "⚠️ Warning: Failed to extract Name from provisioning profile, will use provided IOS_PROV_PROFILE_NAME"
PROFILE_NAME="$IOS_PROV_PROFILE_NAME"
fi
echo "Profile UUID: $PROFILE_UUID"
echo "Profile Name: $PROFILE_NAME"
# Install provisioning profile in the correct location with UUID filename
echo "Installing provisioning profile in filesystem..."
mkdir -p "/Users/runner/Library/MobileDevice/Provisioning Profiles"
# Copy with the UUID as filename
UUID_TARGET_PATH="/Users/runner/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
cp -v "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" "$UUID_TARGET_PATH"
# Set correct permissions on the profile
chmod 644 "$UUID_TARGET_PATH"
chown runner:staff "$UUID_TARGET_PATH"
# Save the profile path and name to environment for later steps
echo "IOS_PROV_PROFILE_PATH=$UUID_TARGET_PATH" >> $GITHUB_ENV
echo "IOS_PROV_PROFILE_NAME=$PROFILE_NAME" >> $GITHUB_ENV
# Print provisioning profile information
echo "Provisioning Profile Information:"
echo "--------------------------------"
echo "Profile Name (from file): $PROFILE_NAME"
echo "Profile Name (from env): $IOS_PROV_PROFILE_NAME"
echo "Profile Path: $UUID_TARGET_PATH"
echo "Profile UUID: $PROFILE_UUID"
echo "App Identifier: $IOS_APP_IDENTIFIER"
echo "Team ID: $IOS_TEAM_ID"
echo "--------------------------------"
# List all provisioning profiles in the system with detailed info
echo "List of all provisioning profiles in system:"
ls -la "/Users/runner/Library/MobileDevice/Provisioning Profiles/"
# Clean up temp file
rm -f "$TEMP_PLIST_PATH"
echo "✅ Provisioning profile installation steps completed."
# act won't work with macos, but you can test with `bundle exec fastlane ios ...`
- name: Build and upload to TestFlight (Internal)
if: ${{ !env.ACT }}
env:
IS_PR: ${{ env.IS_PR }}
IOS_APP_IDENTIFIER: ${{ secrets.IOS_APP_IDENTIFIER }}
IOS_CONNECT_API_KEY_BASE64: ${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}
IOS_CONNECT_API_KEY_PATH: ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }}
IOS_CONNECT_ISSUER_ID: ${{ secrets.IOS_CONNECT_ISSUER_ID }}
IOS_CONNECT_KEY_ID: ${{ secrets.IOS_CONNECT_KEY_ID }}
IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
IOS_PROJECT_NAME: ${{ secrets.IOS_PROJECT_NAME }}
IOS_PROJECT_SCHEME: ${{ secrets.IOS_PROJECT_SCHEME }}
IOS_PROV_PROFILE_DIR: ${{ env.IOS_PROV_PROFILE_DIRECTORY }}
IOS_PROV_PROFILE_NAME: ${{ secrets.IOS_PROV_PROFILE_NAME }}
IOS_PROV_PROFILE_PATH: ${{ env.IOS_PROV_PROFILE_PATH }}
IOS_SIGNING_CERTIFICATE: ${{ secrets.IOS_SIGNING_CERTIFICATE }}
IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
IOS_TEAM_NAME: ${{ secrets.IOS_TEAM_NAME }}
NODE_OPTIONS: "--max-old-space-size=8192"
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
timeout-minutes: 90
run: |
cd ${{ env.APP_PATH }}
echo "--- Pre-Fastlane Diagnostics ---"
echo "Running as user: $(whoami)"
echo "Default keychain:"
security list-keychains -d user
echo "Identities in build.keychain:"
security find-identity -v -p codesigning build.keychain || echo "Failed to find identities in build.keychain"
echo "--- Starting Fastlane ---"
# if pushing to main, deploy to App Store
if [ "${{ github.ref }}" = "refs/heads/${{ env.MAIN_BRANCH }}" ]; then
bundle exec fastlane ios deploy --verbose
# else to upload to TestFlight Internal Testing
else
bundle exec fastlane ios internal_test --verbose
fi
- name: Remove project.pbxproj updates we don't want to commit
run: |
PBXPROJ_FILE="app/ios/Self.xcodeproj/project.pbxproj"
# Create a temporary file to store version info
echo "Extracting version information..."
rm -f versions.txt
grep -E 'CURRENT_PROJECT_VERSION = [0-9]+;|MARKETING_VERSION = [0-9]+\.[0-9]+\.[0-9]+;' "${PBXPROJ_FILE}" > versions.txt
# Check if we have version information
if [ -s versions.txt ]; then
echo "Found version information. Resetting file and re-applying versions..."
# Store the version values
CURRENT_VERSION=$(grep 'CURRENT_PROJECT_VERSION' versions.txt | head -1 | sed 's/.*CURRENT_PROJECT_VERSION = \([0-9]*\);.*/\1/')
MARKETING_VERSION=$(grep 'MARKETING_VERSION' versions.txt | head -1 | sed 's/.*MARKETING_VERSION = \([0-9]*\.[0-9]*\.[0-9]*\);.*/\1/')
echo "Current version: $CURRENT_VERSION"
echo "Marketing version: $MARKETING_VERSION"
# Reset the file to HEAD
git checkout HEAD -- "${PBXPROJ_FILE}"
# Update the versions if they exist
if [ ! -z "$CURRENT_VERSION" ]; then
sed -i '' "s/\(CURRENT_PROJECT_VERSION = \)[0-9]*;/\1$CURRENT_VERSION;/g" "${PBXPROJ_FILE}"
fi
if [ ! -z "$MARKETING_VERSION" ]; then
sed -i '' "s/\(MARKETING_VERSION = \)[0-9]*\.[0-9]*\.[0-9]*;/\1$MARKETING_VERSION;/g" "${PBXPROJ_FILE}"
fi
echo "Version information successfully applied."
else
echo "No version information found. Resetting file..."
git checkout HEAD -- "${PBXPROJ_FILE}"
fi
# Clean up
rm -f versions.txt
- name: Get version from package.json
uses: ./.github/actions/get-version
with:
app_path: ${{ env.APP_PATH }}
- name: Commit updated build number
if: ${{ !env.ACT }}
uses: ./.github/actions/push-changes
with:
commit_message: "incrementing ios build number for version ${{ env.VERSION }}"
commit_paths: "./app/ios/OpenPassport/Info.plist ./app/ios/Self.xcodeproj/project.pbxproj"
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Mobile Dependencies
uses: ./.github/actions/mobile-setup
with:
app_path: ${{ env.APP_PATH }}
node_version: ${{ env.NODE_VERSION }}
ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }}
# android specific steps
- name: Setup Java environment
uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
accept-android-sdk-licenses: true
- name: Install NDK
run: |
max_attempts=5
attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts to install NDK..."
if sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"; then
echo "Successfully installed NDK"
exit 0
fi
echo "Failed to install NDK on attempt $attempt"
if [ $attempt -eq $max_attempts ]; then
echo "All attempts to install NDK failed"
exit 1
fi
# Exponential backoff: 2^attempt seconds
wait_time=$((2 ** attempt))
echo "Waiting $wait_time seconds before retrying..."
sleep $wait_time
attempt=$((attempt + 1))
done
- name: Set Gradle JVM options
if: ${{ env.ACT }} # run when testing locally with act to prevent gradle crashes
run: |
echo "org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8" >> ${{ env.APP_PATH }}/android/gradle.properties
- name: Decode Android Secrets
run: |
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}
echo "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_PLAY_STORE_JSON_KEY_PATH }}
# run secrets check after keytool has been setup
- name: Verify Android Secrets
run: |
# Verify Play Store JSON key base64 secret exists and is valid
if [ -z "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" ]; then
echo "❌ Error: Play Store JSON key base64 secret cannot be empty"
exit 1
fi
# Verify the base64 can be decoded
if ! echo "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" | base64 --decode >/dev/null 2>&1; then
echo "❌ Error: Invalid Play Store JSON key base64 format"
exit 1
fi
# Verify keystore file exists and is valid
if [ ! -f "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" ]; then
echo "❌ Error: Keystore file was not created successfully"
exit 1
fi
# Try to verify the keystore with the provided password
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >/dev/null 2>&1; then
echo "❌ Error: Invalid keystore password"
exit 1
fi
# Verify the key alias exists
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" >/dev/null 2>&1; then
echo "❌ Error: Key alias '${{ secrets.ANDROID_KEY_ALIAS }}' not found in keystore"
exit 1
fi
# Verify the key password
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" -keypass "${{ secrets.ANDROID_KEY_PASSWORD }}" >/dev/null 2>&1; then
echo "❌ Error: Invalid key password"
exit 1
fi
echo "✅ All Android secrets verified successfully!"
- name: Build and upload to Google Play Internal Testing
env:
IS_PR: ${{ env.IS_PR }}
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEYSTORE_PATH: ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }}
ANDROID_PLAY_STORE_JSON_KEY_PATH: ${{ env.APP_PATH }}${{ env.ANDROID_PLAY_STORE_JSON_KEY_PATH }}
NODE_OPTIONS: "--max-old-space-size=8192"
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
run: |
cd ${{ env.APP_PATH }}
# if pushing to main, deploy to Play Store
if [ "${{ github.ref }}" = "refs/heads/${{ env.MAIN_BRANCH }}" ]; then
bundle exec fastlane android deploy --verbose
# else to upload to Play Store Internal Testing
else
bundle exec fastlane android internal_test --verbose
fi
- name: Get version from package.json
uses: ./.github/actions/get-version
with:
app_path: ${{ env.APP_PATH }}
- name: Commit updated build version
if: ${{ !env.ACT }}
uses: ./.github/actions/push-changes
with:
commit_message: "incrementing android build version for version ${{ env.VERSION }}"
commit_paths: "./app/android/app/build.gradle"

5
app/.gitignore vendored
View File

@@ -22,6 +22,7 @@ DerivedData
*.xcuserstate
ios/.xcode.env.local
**/.xcode.env.local
ios/certs
# Android/IntelliJ
@@ -37,6 +38,9 @@ local.properties
# debug bundled builds
android/app/src/main/assets/*android.bundle
android/app/src/main/res/*/node_modules*
android/.kotlin/
android/app/upload-keystore.jks
android/app/play-store-key.json
# node.js
#
@@ -55,6 +59,7 @@ yarn-error.log
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
**/fastlane/.env.secrets
# Bundle artifact
*.jsbundle

View File

@@ -5,4 +5,5 @@ node_modules/
src/assets/animations/
witnesscalc/
vendor/
android/
android/
*.md

1
app/.ruby-version Normal file
View File

@@ -0,0 +1 @@
3.2.7

View File

@@ -1,8 +1,20 @@
source 'https://rubygems.org'
source "https://rubygems.org"
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
ruby ">= 3.2"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem "cocoapods", ">= 1.13", "!= 1.15.0", "!= 1.15.1"
gem "activesupport", ">= 6.1.7.5", "!= 7.1.0"
# Add fastlane for CI/CD
gem "fastlane", "~> 2.227.0"
group :development do
gem "dotenv"
# Exclude nokogiri for GitHub Actions and Act
gem "nokogiri", "~> 1.18", platform: :ruby unless ENV["GITHUB_ACTIONS"] || ENV["ACT"]
end
plugins_path = File.join(File.dirname(__FILE__), "fastlane", "Pluginfile")
eval_gemfile(plugins_path) if File.exist?(plugins_path)

View File

@@ -1,27 +1,57 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.6)
CFPropertyList (3.0.7)
base64
nkf
rexml
activesupport (6.1.7.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
activesupport (7.2.2.1)
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)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.2)
aws-partitions (1.1069.0)
aws-sdk-core (3.220.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.182.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
claide (1.1.0)
cocoapods (1.12.1)
cocoapods (1.16.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.12.1)
cocoapods-core (= 1.16.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.6.0, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
@@ -33,8 +63,8 @@ GEM
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.12.1)
xcodeproj (>= 1.27.0, < 2.0)
cocoapods-core (1.16.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
@@ -45,7 +75,7 @@ GEM
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
@@ -53,48 +83,234 @@ GEM
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
concurrent-ruby (1.2.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
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.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
ffi (1.15.5)
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.227.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.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-increment_version_code (0.4.3)
fastlane-plugin-versioning_android (0.1.1)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.1)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.14.1)
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)
json (2.6.3)
minitest (5.18.1)
jmespath (1.6.2)
json (2.10.2)
jwt (2.10.1)
base64
logger (1.6.6)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
minitest (5.25.5)
molinillo (0.8.0)
nanaimo (0.3.0)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
nkf (0.2.0)
nokogiri (1.18.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (4.0.7)
rexml (3.2.5)
racc (1.8.1)
rake (13.2.1)
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)
ruby-macho (2.5.1)
typhoeus (1.4.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
securerandom (0.4.1)
security (0.1.5)
signet (0.19.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
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)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
xcodeproj (1.22.0)
uber (0.1.0)
unicode-display_width (2.6.0)
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.3.0)
rexml (~> 3.2.4)
zeitwerk (2.6.8)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (~> 1.12)
activesupport (>= 6.1.7.5, != 7.1.0)
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
dotenv
fastlane (~> 2.227.0)
fastlane-plugin-increment_version_code
fastlane-plugin-versioning_android
nokogiri (~> 1.18)
RUBY VERSION
ruby 2.6.10p210
ruby 3.2.7p253
BUNDLED WITH
1.17.2
2.4.19

View File

@@ -2,10 +2,10 @@
buildscript {
ext {
buildToolsVersion = "34.0.0"
buildToolsVersion = "35.0.0"
minSdkVersion = 23
compileSdkVersion = 34
targetSdkVersion = 34
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "26.1.10909125"
kotlinVersion = "1.9.24"
}

View File

@@ -0,0 +1,22 @@
ANDROID_KEYSTORE=
ANDROID_KEYSTORE_PASSWORD=
ANDROID_KEY_ALIAS=
ANDROID_KEY_PASSWORD=
ANDROID_PACKAGE_NAME=
ANDROID_PLAY_STORE_JSON_KEY_BASE64=
IOS_APP_IDENTIFIER=
IOS_CONNECT_API_KEY_BASE64=
IOS_CONNECT_ISSUER_ID=
IOS_CONNECT_KEY_ID=
IOS_DIST_CERT_BASE64=
IOS_PROJECT_NAME=
IOS_PROJECT_SCHEME=
IOS_PROV_PROFILE_BASE64=
IOS_PROV_PROFILE_NAME=
IOS_P12_PASSWORD=
IOS_SIGNING_CERTIFICATE=
IOS_TEAM_ID=
IOS_TEAM_NAME=
IOS_TESTFLIGHT_GROUPS=
SLACK_CHANNEL_ID=
SLACK_BOT_TOKEN=

377
app/fastlane/DEV.md Normal file
View File

@@ -0,0 +1,377 @@
# Fastlane & CI/CD Development Guide 🚀
This document outlines how to work with the Fastlane setup and the GitHub Actions CI/CD pipeline for this mobile application.
## Table of Contents
- [Prerequisites](#prerequisites-)
- [Setup](#setup-)
- [Workflow Overview](#workflow-overview-)
- [Local Development](#local-development-)
- [CI/CD Pipeline](#cicd-pipeline-)
- [Version Management](#version-management-)
- [Platform-Specific Notes](#platform-specific-notes-)
- [Troubleshooting](#troubleshooting-)
- [Additional Resources](#additional-resources-)
## Prerequisites 🛠️
Before working with this setup, ensure you have the following installed:
* **Ruby** - Fastlane requires Ruby (version 2.6.0 or higher recommended)
* **Bundler** - For managing Ruby dependencies
* **Xcode** - For iOS development (latest stable version recommended)
* **Android Studio** - For Android development
* **Node.js & Yarn** - For JavaScript dependencies
* **Docker** - Optional, required for local testing with `act`
## Setup ⚙️
### Local Fastlane Setup
1. Install Fastlane via Bundler:
```bash
cd app
bundle install
```
2. Verify installation:
```bash
bundle exec fastlane --version
```
### Secrets Management (`.env.secrets`) 🔑
Fastlane requires various secrets to interact with the app stores and sign applications:
1. **Create Your Local Secrets File:** Copy the template file to create your secrets file:
```bash
cp app/fastlane/.env.secrets.example app/fastlane/.env.secrets
```
2. **Populate Values:** Fill in the values in your newly created `.env.secrets` file. Obtain these credentials from the appropriate platform developer portals or your team's administrator.
3. **Keep it Private:** The `.env.secrets` file is included in the project's `.gitignore` and **must not** be committed to the repository.
4. **CI/CD Setup:** For the GitHub Actions workflow, these same secrets must be configured as [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) in the repository settings.
### Environment Secrets Reference 📝
#### Android Secrets 🤖
| Secret | Description |
|--------|-------------|
| `ANDROID_KEYSTORE` | Path to keystore file used for signing Android apps |
| `ANDROID_KEYSTORE_PASSWORD` | Password for the Android keystore |
| `ANDROID_KEY_ALIAS` | Alias of the key in the keystore |
| `ANDROID_KEY_PASSWORD` | Password for the specified key |
| `ANDROID_PACKAGE_NAME` | Package name/application ID of the Android app |
| `ANDROID_PLAY_STORE_JSON_KEY_BASE64` | Base64 encoded Google Play Store service account JSON key file for API access |
#### iOS Secrets 🍏
| Secret | Description |
|--------|-------------|
| `IOS_APP_IDENTIFIER` | Bundle identifier for the iOS app |
| `IOS_CONNECT_API_KEY_BASE64` | Base64 encoded App Store Connect API key for authentication |
| `IOS_CONNECT_ISSUER_ID` | App Store Connect issuer ID associated with the API key |
| `IOS_CONNECT_KEY_ID` | App Store Connect key ID for API access |
| `IOS_DIST_CERT_BASE64` | Base64 encoded iOS distribution certificate for code signing |
| `IOS_PROV_PROFILE_BASE64` | Base64 encoded provisioning profile for the app |
| `IOS_PROV_PROFILE_NAME` | Name of the provisioning profile |
| `IOS_P12_PASSWORD` | Password for the p12 certificate file |
| `IOS_TEAM_ID` | Apple Developer Team ID |
| `IOS_TEAM_NAME` | Apple Developer Team name |
| `IOS_TESTFLIGHT_GROUPS` | Comma-separated list of TestFlight groups to distribute the app to |
## Workflow Overview 🔄
### Fastlane Lanes
The project uses several custom Fastlane lanes to handle different build and deployment scenarios:
#### iOS Lanes
| Lane | Description | Usage |
|------|-------------|-------|
| `internal_test` | Builds a beta version and uploads to TestFlight | `bundle exec fastlane ios internal_test` |
| `deploy` | Builds a production version and uploads to App Store Connect | `bundle exec fastlane ios deploy` |
| `sync_version` | Syncs version from package.json to Info.plist | `bundle exec fastlane ios sync_version` |
#### Android Lanes
| Lane | Description | Usage |
|------|-------------|-------|
| `internal_test` | Builds a beta version and uploads to Google Play Internal Testing | `bundle exec fastlane android internal_test` |
| `deploy` | Builds a production version and uploads to Google Play Production | `bundle exec fastlane android deploy` |
| `sync_version` | Syncs version from package.json to build.gradle | `bundle exec fastlane android sync_version` |
### Deployment Flow
1. **Version Management**: Update version in package.json using bump scripts
2. **Build Process**: Run the appropriate lane for internal testing or production
3. **Auto Build Numbers**: System automatically increments build numbers
4. **Upload**: Artifacts are uploaded to respective app stores
5. **Notification**: Slack notifications sent upon successful builds
## Local Development 💻
### Package Scripts
Several scripts in `app/package.json` facilitate common Fastlane and versioning tasks:
#### Debug Builds 🐞
**`yarn ios:fastlane-debug`** / **`yarn android:fastlane-debug`**
* Executes the `internal_test` Fastlane lane for the respective platforms
* Builds the app in a debug configuration for internal testing
* Uploads to TestFlight (iOS) or Google Play Internal Testing (Android) if permissions allow
* Cleans build directories (`ios/build`, `android/app/build`) before running
#### Forced Local Deployment 🚀
**`yarn force-local-upload-deploy`**
**`yarn force-local-upload-deploy:ios`**
**`yarn force-local-upload-deploy:android`**
* Runs the `deploy` Fastlane lane with local development settings
* Uses `FORCE_UPLOAD_LOCAL_DEV=true` to bypass CI checks
* Useful for testing deployment process locally or manual deploys
* Cleans build directories first
* **Use with caution!** Will attempt to upload to production if you have permissions
#### Forced Local Testing 🧪
**`yarn force-local-upload-test`**
**`yarn force-local-upload-test:ios`**
**`yarn force-local-upload-test:android`**
* Similar to deploy version, but runs `internal_test` lane locally
* Useful for testing the internal distribution process
* Uses `FORCE_UPLOAD_LOCAL_DEV=true` flag
### Version Management 🏷️
**`yarn bump-version:major|minor|patch`**
* Increments version in `package.json` according to semantic versioning
* Creates version commit and tag automatically
* Calls `sync-versions` afterwards
**`yarn sync-versions`**
* Synchronizes the version from `package.json` to native files
* Updates iOS `Info.plist` and Android `build.gradle`
* Ensures consistency across JS bundle and native app wrappers
### Local Testing with `act` 🧰
You can test the GitHub Actions workflow locally using [`act`](https://github.com/nektos/act):
1. **Install `act`:** Follow the installation instructions in the `act` repository.
2. **Run Jobs:** From the *root* of the project repository:
```bash
# Test the Android build
act -j build-android --secret-file app/fastlane/.env.secrets
# Test the iOS build (limited functionality on non-macOS systems)
act -j build-ios --secret-file app/fastlane/.env.secrets
```
3. **Advanced Usage:**
* When running with `act`, the environment variable `ACT=true` is set automatically
* This causes certain steps to be skipped, like code signing and store uploads
* You can modify the workflow file locally to focus on specific steps by adding `if: false` to steps you want to skip
4. **Limitations:**
* iOS builds require macOS-specific tools not available in Docker
* Certificate/provisioning profile handling may not work as expected
* Network access to Apple/Google services may be limited
## CI/CD Pipeline 🔄
The primary CI/CD workflow is defined in `.github/workflows/mobile-deploy.yml`. It automates the build and deployment process.
### Triggers
* **Push Events:** Runs on pushes to `dev` or `main` branches that change files in `app/` or the workflow file
* **Pull Request Events:** Runs on PRs to `dev` or `main` branches that change files in `app/` or the workflow file
### Jobs
The workflow consists of parallel jobs for each platform:
#### `build-ios` Job
Runs on `macos-latest` and performs the following steps:
1. Sets up the environment (Node.js, Ruby, CocoaPods)
2. Processes iOS secrets and certificates
3. Runs appropriate Fastlane lane based on branch
4. Commits updated build numbers back to the repository
#### `build-android` Job
Runs on `ubuntu-latest` and performs the following steps:
1. Sets up the environment (Node.js, Java, Android SDK)
2. Processes Android secrets
3. Runs appropriate Fastlane lane based on branch
4. Commits updated version code back to the repository
### Deployment Destinations
* **Internal Testing:**
* iOS: TestFlight
* Android: Google Play Internal Testing track
* Triggered on pushes to `dev` branch and pull requests
* **Production:**
* iOS: App Store Connect (ready for submission)
* Android: Google Play Production track
* Triggered on pushes to `main` branch
## Auto Build Number Incrementing 🔢
The CI/CD pipeline automatically manages build numbers/version codes:
### iOS Build Numbers
1. **Automatic Fetching:**
* The pipeline fetches the latest build number from TestFlight via the App Store Connect API
* Increments by 1 for the new build
2. **Implementation:**
```ruby
latest_build = Fastlane::Actions::LatestTestflightBuildNumberAction.run(
api_key: api_key,
app_identifier: ENV["IOS_APP_IDENTIFIER"],
platform: "ios",
)
new_build_number = latest_build + 1
```
3. **Commit Back to Repository:**
* After incrementing, changes are automatically committed back to the branch
* Files affected: `./app/ios/OpenPassport/Info.plist` and `./app/ios/Self.xcodeproj/project.pbxproj`
### Android Version Code
1. **Local Incrementing:**
* The pipeline increments the version code in the Gradle file
* Cannot verify against Google Play due to permission issues (see Android Caveats)
2. **Commit Back to Repository:**
* After building, the workflow commits the incremented version code
* File affected: `./app/android/app/build.gradle`
## Slack Notifications 💬
The CI/CD pipeline sends notifications to Slack after successful builds:
1. **Configuration:**
* Set `SLACK_API_TOKEN` and `SLACK_ANNOUNCE_CHANNEL_NAME` in your `.env.secrets` file
* For CI, add these as GitHub Actions Secrets
2. **Notification Content:**
* iOS: `🍎 iOS v{version} (Build {build_number}) deployed to TestFlight/App Store Connect`
* Android: `🤖 Android v{version} (Build {version_code}) deployed to Internal Testing/Google Play`
* Includes the built artifact (IPA/AAB) as an attachment
3. **Testing Notifications:**
* You can test Slack notifications locally with the `force-local-upload-test` scripts
* Requires a valid Slack API token with proper permissions
## Platform-Specific Notes 📱
### Android Deployment Caveats ⚠️
There are important limitations when working with Android deployments:
1. **Google Play Store Permission Limitations:**
* The pipeline currently **lacks permissions** to directly upload builds to the Google Play Store
* The `android_has_permissions` flag in helpers.rb is set to false, preventing direct uploads
2. **Manual Upload Process Required:**
* After the Android build job finishes, you must:
1. Download the AAB artifact from the GitHub Actions run
2. Manually upload the AAB file to the Google Play Console
3. Complete the release process in the Play Console UI
3. **Version Code Management:**
* Unlike iOS, we cannot automatically fetch the current Android build number (version code)
* After building, you need to manually commit the updated version number
4. **For Local Developers:**
* When testing Android deployment locally:
```bash
yarn android:build-release # Build the AAB
# The AAB will be in android/app/build/outputs/bundle/release/app-release.aab
```
* Note that the `force-local-upload-deploy:android` script will attempt to deploy but will fail due to permission issues
## Troubleshooting 🔍
### Version Syncing Issues
If you encounter issues with version syncing between `package.json` and native projects:
1. **Manual Sync:**
```bash
yarn sync-versions
```
This runs the Fastlane lanes to sync versions without building or deploying.
2. **Version Mismatch Checking:**
```bash
# Check version in Info.plist
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" app/ios/OpenPassport/Info.plist
# Check version in build.gradle
grep "versionName" app/android/app/build.gradle
```
3. **Fixing Discrepancies:**
* Always update `package.json` version first using the `bump-version` scripts
* Then run `sync-versions` to update native files
* For manual fixes, edit the version in each file and commit the changes
### iOS Build Issues
1. **Certificate/Provisioning Profile Errors**
* Ensure your certificate and provisioning profile are valid and not expired
* Verify that the correct team ID is being used
* Try using `fastlane match` to manage certificates and profiles
2. **TestFlight Upload Failures**
* Check that your App Store Connect API key has sufficient permissions
* Verify your app's version and build numbers are incremented properly
* Ensure binary is properly signed with distribution certificate
### Android Build Issues
1. **Keystore Issues**
* Verify keystore path, password, and key alias are correct
* Check file permissions on the keystore file
* Ensure you're using the correct signing configuration in Gradle
2. **Google Play Upload Failures**
* Verify the service account has proper permissions in the Google Play Console
* Check that the app's version code has been incremented
* Ensure the JSON key file is valid and not expired
## Additional Resources 📚
### Official Documentation
* [Fastlane Documentation](https://docs.fastlane.tools/)
* [GitHub Actions Documentation](https://docs.github.com/en/actions)
* [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi)
* [Google Play Developer API](https://developers.google.com/android-publisher)
### Helpful Tools
* [Match](https://docs.fastlane.tools/actions/match/) - Fastlane tool for iOS code signing
* [Supply](https://docs.fastlane.tools/actions/supply/) - Fastlane tool for Android app deployment
* [Gym](https://docs.fastlane.tools/actions/gym/) - Fastlane tool for building iOS apps

293
app/fastlane/Fastfile Normal file
View File

@@ -0,0 +1,293 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
# https://docs.fastlane.tools/actions
#
opt_out_usage
require "bundler/setup"
require "base64"
require_relative "helpers"
# load secrets before project configuration
Fastlane::Helpers.dev_load_dotenv_secrets
is_ci = Fastlane::Helpers.is_ci_environment?
local_development = !is_ci
# checks after calling Dotenv.load
attempt_force_upload_local_dev = ENV["FORCE_UPLOAD_LOCAL_DEV"] == "true"
android_has_permissions = false
if local_development
# confirm that we want to force upload
Fastlane::Helpers.confirm_force_upload if attempt_force_upload_local_dev
end
# Project configuration
PROJECT_NAME = ENV["IOS_PROJECT_NAME"]
PROJECT_SCHEME = ENV["IOS_PROJECT_SCHEME"]
SIGNING_CERTIFICATE = ENV["IOS_SIGNING_CERTIFICATE"]
# Environment setup
package_version = JSON.parse(File.read("../package.json"))["version"]
# most of these values are for local development
android_aab_path = "../android/app/build/outputs/bundle/release/app-release.aab"
android_gradle_file_path = "../android/app/build.gradle"
android_keystore_path = "../android/app/upload-keystore.jks"
android_play_store_json_key_path = "../android/app/play-store-key.json"
ios_connect_api_key_path = "../ios/certs/connect_api_key.p8"
ios_provisioning_profile_directory = "~/Library/MobileDevice/Provisioning\ Profiles"
ios_xcode_profile_path = "../ios/#{PROJECT_NAME}.xcodeproj"
default_platform(:ios)
platform :ios do
desc "Sync ios version"
lane :sync_version do
increment_version_number(
xcodeproj: "ios/#{PROJECT_NAME}.xcodeproj",
version_number: package_version,
)
end
desc "Push a new build to TestFlight Internal Testing"
lane :internal_test do
result = prepare_ios_build(prod_release: false)
upload_to_testflight(
api_key: result[:api_key],
distribute_external: true,
groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","),
changelog: "",
skip_waiting_for_build_processing: false,
) if result[:should_upload]
# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"]
Fastlane::Helpers.upload_file_to_slack(
file_path: result[:ipa_path],
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "🍎 iOS v#{package_version} (Build #{result[:build_number]}) deployed to TestFlight",
title: "#{PROJECT_NAME}-#{package_version}-#{result[:build_number]}.ipa",
)
else
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
end
end
desc "Prepare a new build for App Store submission"
lane :deploy do
result = prepare_ios_build(prod_release: true)
upload_to_app_store(
api_key: result[:api_key],
skip_screenshots: true,
skip_metadata: true,
submit_for_review: false,
automatic_release: false,
skip_app_version_update: true,
) if result[:should_upload]
# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"]
Fastlane::Helpers.upload_file_to_slack(
file_path: result[:ipa_path],
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "🍎 iOS (Ready for Submission) v#{package_version} (Build #{result[:build_number]}) deployed to App Store Connect",
title: "#{PROJECT_NAME}-#{package_version}-#{result[:build_number]}.ipa",
)
else
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
end
end
private_lane :prepare_ios_build do |options|
if local_development
# app breaks with Xcode 16.3
xcode_select "/Applications/Xcode-16-2.app"
# Set up API key, profile, and potentially certificate for local dev
Fastlane::Helpers.ios_dev_setup_connect_api_key(ios_connect_api_key_path)
Fastlane::Helpers.ios_dev_setup_provisioning_profile(ios_provisioning_profile_directory)
Fastlane::Helpers.ios_dev_setup_certificate
else
# we need this for building ios apps in CI
# else build will hang on "[CP] Embed Pods Frameworks"
setup_ci(
keychain_name: "build.keychain",
)
end
required_env_vars = [
"IOS_APP_IDENTIFIER",
"IOS_CONNECT_API_KEY_BASE64",
"IOS_CONNECT_API_KEY_PATH",
"IOS_CONNECT_ISSUER_ID",
"IOS_CONNECT_KEY_ID",
"IOS_PROJECT_NAME",
"IOS_PROJECT_SCHEME",
"IOS_PROV_PROFILE_NAME",
"IOS_PROV_PROFILE_PATH",
"IOS_TEAM_ID",
"IOS_TEAM_NAME",
]
target_platform = options[:prod_release] ? "App Store" : "TestFlight"
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
workspace_path = File.expand_path("../ios/#{PROJECT_NAME}.xcworkspace", Dir.pwd)
ios_signing_certificate_name = "iPhone Distribution: #{ENV["IOS_TEAM_NAME"]} (#{ENV["IOS_TEAM_ID"]})"
Fastlane::Helpers.verify_env_vars(required_env_vars)
build_number = Fastlane::Helpers.ios_increment_build_number(ios_xcode_profile_path)
Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path)
Fastlane::Helpers.ios_verify_provisioning_profile
api_key = app_store_connect_api_key(
key_id: ENV["IOS_CONNECT_KEY_ID"],
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
in_house: false,
)
# Update project to use manual code signing
update_code_signing_settings(
use_automatic_signing: false,
path: "ios/#{PROJECT_NAME}.xcodeproj",
team_id: ENV["IOS_TEAM_ID"],
targets: [PROJECT_NAME],
code_sign_identity: ios_signing_certificate_name,
profile_name: ENV["IOS_PROV_PROFILE_NAME"],
bundle_identifier: ENV["IOS_APP_IDENTIFIER"],
build_configurations: ["Release"],
)
clear_derived_data
# Print final build settings before archiving
sh "xcodebuild -showBuildSettings -workspace #{workspace_path} " \
"-scheme #{PROJECT_SCHEME} -configuration Release " \
"| grep 'CODE_SIGN_STYLE\|PROVISIONING_PROFILE_SPECIFIER\|CODE_SIGN_IDENTITY\|DEVELOPMENT_TEAM' || true"
cocoapods(
podfile: "ios/Podfile",
clean_install: true,
deployment: true,
)
ipa_path = build_app({
workspace: "#{workspace_path}",
scheme: PROJECT_SCHEME,
export_method: "app-store",
output_directory: "build",
clean: true,
export_options: {
method: "app-store",
signingStyle: "manual",
provisioningProfiles: {
ENV["IOS_APP_IDENTIFIER"] => ENV["IOS_PROV_PROFILE_NAME"],
},
signingCertificate: ios_signing_certificate_name,
teamID: ENV["IOS_TEAM_ID"],
},
})
{
api_key: api_key,
build_number: build_number,
ipa_path: ipa_path,
should_upload: should_upload,
}
end
end
platform :android do
desc "Sync android version"
lane :sync_version do
android_set_version_name(
version_name: package_version,
gradle_file: android_gradle_file_path,
)
end
desc "Push a new build to Google Play Internal Testing"
lane :internal_test do
upload_android_build(track: "internal")
end
desc "Push a new build to Google Play Store"
lane :deploy do
upload_android_build(track: "production")
end
private_lane :upload_android_build do |options|
if local_development
if ENV["ANDROID_KEYSTORE_PATH"].nil?
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
end
if ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"].nil?
ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"] = Fastlane::Helpers.android_create_play_store_key(android_play_store_json_key_path)
end
end
required_env_vars = [
"ANDROID_KEYSTORE",
"ANDROID_KEYSTORE_PASSWORD",
"ANDROID_KEYSTORE_PATH",
"ANDROID_KEY_ALIAS",
"ANDROID_KEY_PASSWORD",
"ANDROID_PACKAGE_NAME",
"ANDROID_PLAY_STORE_JSON_KEY_PATH",
]
Fastlane::Helpers.verify_env_vars(required_env_vars)
version_code = Fastlane::Helpers.android_increment_version_code(android_gradle_file_path)
# TODO: uncomment when we have the permissions to run this action
# Fastlane::Helpers.android_verify_version_code(android_gradle_file_path)
target_platform = options[:track] == "production" ? "Google Play" : "Internal Testing"
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
validate_play_store_json_key(
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
)
Fastlane::Helpers.with_retry(max_retries: 3, delay: 10) do
gradle(
task: "clean bundleRelease",
project_dir: "android/",
properties: {
"android.injected.signing.store.file" => ENV["ANDROID_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["ANDROID_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["ANDROID_KEY_PASSWORD"],
},
)
end
upload_to_play_store(
track: options[:track],
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
package_name: ENV["ANDROID_PACKAGE_NAME"],
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
track_promote_release_status: "completed",
aab: android_aab_path,
) if should_upload && android_has_permissions
# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"]
Fastlane::Helpers.upload_file_to_slack(
file_path: android_aab_path,
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "🤖 Android v#{package_version} (Build #{version_code}) deployed to #{target_platform}",
title: "#{PROJECT_NAME}-#{package_version}-#{version_code}.aab",
)
else
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
end
end
end

6
app/fastlane/Pluginfile Normal file
View File

@@ -0,0 +1,6 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-increment_version_code'
gem 'fastlane-plugin-versioning_android'

77
app/fastlane/README.md Normal file
View File

@@ -0,0 +1,77 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## iOS
### ios sync_version
```sh
[bundle exec] fastlane ios sync_version
```
Sync ios version
### ios internal_test
```sh
[bundle exec] fastlane ios internal_test
```
Push a new build to TestFlight Internal Testing
### ios deploy
```sh
[bundle exec] fastlane ios deploy
```
Prepare a new build for App Store submission
----
## Android
### android sync_version
```sh
[bundle exec] fastlane android sync_version
```
Sync android version
### android internal_test
```sh
[bundle exec] fastlane android internal_test
```
Push a new build to Google Play Internal Testing
### android deploy
```sh
[bundle exec] fastlane android deploy
```
Push a new build to Google Play Store
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

739
app/fastlane/helpers.rb Normal file
View File

@@ -0,0 +1,739 @@
require "bundler/setup"
require "fastlane"
require "tempfile"
require "fileutils"
require "base64"
require "shellwords"
require "net/http"
require "uri"
require "json"
# Load secrets before defining constants
module Fastlane
module Helpers
def self.is_ci_environment?
ENV["CI"] == "true" && ENV["ACT"] != "true"
end
def self.dev_load_dotenv_secrets
if !is_ci_environment?
puts "Loading .env.secrets"
require "dotenv"
Dotenv.load("./.env.secrets")
end
end
# Simple multipart boundary generator
def self.generate_boundary
"----FastlaneSlackUploadBoundary#{rand(1000000)}"
end
end
end
# Call load_dotenv_secrets before setting constants
Fastlane::Helpers.dev_load_dotenv_secrets
# Now set constants after secrets are loaded
SLACK_TOKEN = ENV["SLACK_API_TOKEN"]
CHANNEL_NAME = ENV["SLACK_ANNOUNCE_CHANNEL_NAME"] || "deploy-mobile"
module Fastlane
module Helpers
@@android_has_permissions = false
### UI and Reporting Methods ###
def self.report_error(message, suggestion = nil, abort_message = nil)
UI.error("#{message}")
UI.error(suggestion) if suggestion
UI.abort_with_message!(abort_message || message)
end
def self.report_success(message)
UI.success("#{message}")
end
### Environment and Configuration Methods ###
def self.verify_env_vars(required_vars)
missing_vars = required_vars.select { |var| ENV[var].nil? || ENV[var].to_s.strip.empty? }
if missing_vars.any?
report_error(
"Missing required environment variables: #{missing_vars.join(", ")}",
"Please check your secrets",
"Environment verification failed"
)
else
report_success("All required environment variables are present")
end
end
def self.should_upload_app(platform)
if ENV["ACT"] == "true"
puts "Skipping upload to #{platform} we are testing using `act`"
return false
end
if ENV["IS_PR"] == "true"
puts "Skipping upload to #{platform} because we are in a pull request"
return false
end
# upload app if we are in CI or forcing local upload
ENV["CI"] == "true" || ENV["FORCE_UPLOAD_LOCAL_DEV"] == "true"
end
def self.confirm_force_upload
UI.important "⚠️ FORCE_UPLOAD_LOCAL_DEV is set to true. This will upload the build to the store."
UI.important "Are you sure you want to continue? (y/n)"
response = STDIN.gets.chomp
unless response.downcase == "y"
UI.user_error!("Upload cancelled by user")
end
end
def self.with_retry(max_retries: 3, delay: 5)
attempts = 0
begin
yield
rescue => e
attempts += 1
if attempts < max_retries
UI.important("Retry ##{attempts} after error: #{e.message}")
sleep(delay)
retry
else
UI.user_error!("Failed after #{max_retries} retries: #{e.message}")
end
end
end
def self.ios_verify_app_store_build_number(ios_xcode_profile_path)
api_key = Fastlane::Actions::AppStoreConnectApiKeyAction.run(
key_id: ENV["IOS_CONNECT_KEY_ID"],
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
in_house: false,
)
latest_build = Fastlane::Actions::LatestTestflightBuildNumberAction.run(
api_key: api_key,
app_identifier: ENV["IOS_APP_IDENTIFIER"],
platform: "ios",
)
project = Xcodeproj::Project.open(ios_xcode_profile_path)
target = project.targets.first
current_build = target.build_configurations.first.build_settings["CURRENT_PROJECT_VERSION"]
if current_build.to_i <= latest_build.to_i
report_error(
"Build number must be greater than latest TestFlight build!",
"Latest TestFlight build: #{latest_build}\nCurrent build: #{current_build}\nPlease increment the build number in the project settings",
"Build number verification failed"
)
else
report_success("Build number verified (Current: #{current_build}, Latest TestFlight: #{latest_build})")
end
end
def self.ios_ensure_generic_versioning(ios_xcode_profile_path)
puts "Opening Xcode project at: #{File.expand_path(ios_xcode_profile_path)}"
unless File.exist?(ios_xcode_profile_path)
report_error(
"Xcode project not found at #{project_path}",
"Please ensure you're running this command from the correct directory",
"Project file not found"
)
end
project = Xcodeproj::Project.open(ios_xcode_profile_path)
project.targets.each do |target|
target.build_configurations.each do |config|
if config.build_settings["VERSIONING_SYSTEM"] != "apple-generic"
puts "Enabling Apple Generic Versioning for #{target.name} - #{config.name}"
config.build_settings["VERSIONING_SYSTEM"] = "apple-generic"
config.build_settings["CURRENT_PROJECT_VERSION"] ||= "1"
end
end
end
project.save
report_success("Enabled Apple Generic Versioning in Xcode project")
end
def self.ios_increment_build_number(ios_xcode_profile_path)
# First ensure Apple Generic Versioning is enabled
ios_ensure_generic_versioning(ios_xcode_profile_path)
api_key = Fastlane::Actions::AppStoreConnectApiKeyAction.run(
key_id: ENV["IOS_CONNECT_KEY_ID"],
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
in_house: false,
)
latest_build = Fastlane::Actions::LatestTestflightBuildNumberAction.run(
api_key: api_key,
app_identifier: ENV["IOS_APP_IDENTIFIER"],
platform: "ios",
)
new_build_number = latest_build + 1
Fastlane::Actions::IncrementBuildNumberAction.run(
build_number: new_build_number,
xcodeproj: ios_xcode_profile_path,
)
report_success("Incremented build number to #{new_build_number} (previous TestFlight build: #{latest_build})")
new_build_number
end
def self.ios_dev_setup_certificate
unless ENV["IOS_DIST_CERT_BASE64"]
report_error(
"Missing IOS_DIST_CERT_BASE64 environment variable.",
"This variable is required for local certificate installation.",
"Certificate installation failed"
)
end
unless ENV["IOS_P12_PASSWORD"]
report_error(
"Missing IOS_P12_PASSWORD environment variable.",
"This password is required to import the certificate (.p12 file).",
"Certificate installation failed"
)
end
decoded_cert_data = Base64.decode64(ENV["IOS_DIST_CERT_BASE64"])
if decoded_cert_data.empty?
report_error(
"IOS_DIST_CERT_BASE64 seems to be empty or invalid.",
"Please check the value of the environment variable.",
"Certificate decoding failed"
)
end
cert_password = ENV["IOS_P12_PASSWORD"] || ""
temp_p12 = nil
begin
temp_p12 = Tempfile.new(["fastlane_local_cert", ".p12"])
temp_p12.binmode
temp_p12.write(decoded_cert_data)
temp_p12.close
puts "Temporarily wrote decoded certificate to: #{temp_p12.path}"
# Import the certificate into the default keychain
# Omitting -k targets the default keychain.
# -T /usr/bin/codesign allows codesign to use the key without prompting every time.
import_command = "security import #{Shellwords.escape(temp_p12.path)} -P #{Shellwords.escape(cert_password)} -T /usr/bin/codesign"
puts "Running: #{import_command}"
import_output = `#{import_command} 2>&1`
unless $?.success?
report_error(
"Failed to import certificate into default keychain.",
"Command: #{import_command}\nOutput: #{import_output}",
"Certificate import failed"
)
end
report_success("Certificate imported successfully into default keychain.")
rescue => e
report_error("An error occurred during certificate installation: #{e.message}", e.backtrace.join("\n"), "Certificate installation failed")
ensure
# Clean up temporary file
if temp_p12
temp_p12.unlink
puts "Cleaned up temp certificate: #{temp_p12.path}"
end
end
end
def self.ios_dev_setup_connect_api_key(api_key_path)
api_key_full_path = File.expand_path(api_key_path, File.dirname(__FILE__))
ENV["IOS_CONNECT_API_KEY_PATH"] = api_key_full_path
if ENV["IOS_CONNECT_API_KEY_BASE64"]
puts "Decoding iOS Connect API key..."
begin
decoded_key = Base64.decode64(ENV["IOS_CONNECT_API_KEY_BASE64"])
if decoded_key.empty?
report_error(
"IOS_CONNECT_API_KEY_BASE64 seems to be empty or invalid.",
"Please check the value of the environment variable.",
"Connect API Key decoding failed"
)
end
FileUtils.mkdir_p(File.dirname(api_key_full_path))
File.write(api_key_full_path, decoded_key)
report_success("Connect API Key written to: #{api_key_full_path}")
rescue => e
report_error("Error writing decoded API key: #{e.message}", nil, "Connect API Key setup failed")
end
elsif !File.exist?(api_key_full_path)
report_error(
"IOS_CONNECT_API_KEY_BASE64 not set and key file not found.",
"Please provide the key via environment variable or ensure it exists at #{api_key_full_path}",
"Connect API Key setup failed"
)
else
puts "Using existing Connect API Key at: #{api_key_full_path}"
end
begin
verified_path = File.realpath(api_key_full_path)
puts "Verified Connect API Key path: #{verified_path}"
verified_path
rescue Errno::ENOENT
report_error("Connect API Key file not found at expected location: #{api_key_full_path}", nil, "Connect API Key verification failed")
end
end
def self.ios_dev_setup_provisioning_profile(provisioning_profile_directory)
unless ENV["IOS_PROV_PROFILE_BASE64"]
report_error(
"Missing IOS_PROV_PROFILE_BASE64 environment variable.",
"This variable is required for local development profile setup.",
"Provisioning profile setup failed"
)
end
decoded_profile_data = Base64.decode64(ENV["IOS_PROV_PROFILE_BASE64"])
if decoded_profile_data.empty?
report_error(
"IOS_PROV_PROFILE_BASE64 seems to be empty or invalid.",
"Please check the value of the environment variable.",
"Provisioning profile decoding failed"
)
end
temp_profile = nil
temp_plist = nil
final_path = nil
begin
temp_profile = Tempfile.new(["fastlane_local_profile", ".mobileprovision"])
temp_profile.binmode
temp_profile.write(decoded_profile_data)
temp_profile.close
puts "Temporarily wrote decoded profile to: #{temp_profile.path}"
temp_plist = Tempfile.new(["fastlane_temp_plist", ".plist"])
temp_plist_path = temp_plist.path
temp_plist.close
puts "Temporary plist path: #{temp_plist_path}"
security_command = "security cms -D -i #{Shellwords.escape(temp_profile.path)} -o #{Shellwords.escape(temp_plist_path)}"
puts "Running: #{security_command}"
security_output = `#{security_command} 2>&1`
unless $?.success?
report_error(
"Failed to extract plist from provisioning profile using security cms.",
"Command failed: #{security_command}\nOutput: #{security_output}",
"Provisioning profile UUID extraction failed"
)
end
puts "Successfully extracted plist."
unless File.exist?(temp_plist_path) && File.size(temp_plist_path) > 0
report_error(
"Plist file was not created or is empty after security command.",
"Expected plist at: #{temp_plist_path}",
"Provisioning profile UUID extraction failed"
)
end
plistbuddy_command = "/usr/libexec/PlistBuddy -c \"Print :UUID\" #{Shellwords.escape(temp_plist_path)}"
puts "Running: #{plistbuddy_command}"
profile_uuid = `#{plistbuddy_command} 2>&1`.strip
unless $?.success? && !profile_uuid.empty? && profile_uuid !~ /does not exist/
report_error(
"Failed to extract UUID using PlistBuddy or UUID was empty.",
"Command: #{plistbuddy_command}\nOutput: #{profile_uuid}",
"Provisioning profile UUID extraction failed"
)
end
report_success("Extracted profile UUID: #{profile_uuid}")
profile_dir = File.expand_path(provisioning_profile_directory)
FileUtils.mkdir_p(profile_dir)
final_path = File.join(profile_dir, "#{profile_uuid}.mobileprovision")
puts "Copying profile to: #{final_path}"
FileUtils.cp(temp_profile.path, final_path)
report_success("Provisioning profile installed successfully.")
ENV["IOS_PROV_PROFILE_PATH"] = final_path
rescue => e
report_error("An error occurred during provisioning profile setup: #{e.message}", e.backtrace.join("\n"), "Provisioning profile setup failed")
ensure
if temp_profile
temp_profile.unlink
puts "Cleaned up temp profile: #{temp_profile.path}"
end
if temp_plist_path && File.exist?(temp_plist_path)
File.unlink(temp_plist_path)
puts "Cleaned up temp plist: #{temp_plist_path}"
end
end
final_path
end
def self.ios_verify_provisioning_profile
profile_path = ENV["IOS_PROV_PROFILE_PATH"]
unless profile_path && !profile_path.empty?
report_error(
"ENV['IOS_PROV_PROFILE_PATH'] is not set.",
"Ensure ios_dev_setup_provisioning_profile ran successfully or the path is set correctly in CI.",
"Provisioning profile verification failed"
)
end
puts "Verifying provisioning profile exists at: #{profile_path}"
begin
File.realpath(profile_path)
report_success("iOS provisioning profile verified successfully at #{profile_path}")
rescue Errno::ENOENT
report_error("Provisioning profile not found at: #{profile_path}")
rescue => e
report_error("Error accessing provisioning profile at #{profile_path}: #{e.message}")
end
# Print current user
current_user = ENV["USER"] || `whoami`.strip
puts "Current user: #{current_user}"
# List all provisioning profiles in user's directory
profiles_dir = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
if Dir.exist?(profiles_dir)
puts "Listing mobile provisioning profiles in #{profiles_dir}:"
profiles = Dir.glob(File.join(profiles_dir, "*.mobileprovision"))
if profiles.empty?
puts " No provisioning profiles found"
else
profiles.each do |profile|
uuid = File.basename(profile, ".mobileprovision")
puts " - #{uuid}.mobileprovision"
end
puts "Total provisioning profiles found: #{profiles.count}"
end
else
puts "Provisioning profiles directory not found at: #{profiles_dir}"
end
# Advanced checks for provisioning profile
puts "\n--- Advanced Provisioning Profile Diagnostics ---"
# Check if profile can be parsed
if File.exist?(profile_path)
puts "Testing if profile can be parsed with security tool:"
temp_plist = Tempfile.new(["profile_info", ".plist"])
begin
security_cmd = "security cms -D -i #{Shellwords.escape(profile_path)} -o #{Shellwords.escape(temp_plist.path)}"
security_output = `#{security_cmd} 2>&1`
security_success = $?.success?
if security_success
puts "✅ Profile can be parsed successfully"
# Extract and display important profile information
puts "\nExtracting profile information:"
# Get profile UUID
uuid_cmd = "/usr/libexec/PlistBuddy -c 'Print :UUID' #{Shellwords.escape(temp_plist.path)}"
uuid = `#{uuid_cmd}`.strip
puts "Profile UUID: #{uuid}"
# Get App ID/Bundle ID
app_id_cmd = "/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' #{Shellwords.escape(temp_plist.path)}"
app_id = `#{app_id_cmd}`.strip
puts "App Identifier: #{app_id}"
# Get Team ID
team_id_cmd = "/usr/libexec/PlistBuddy -c 'Print :TeamIdentifier:0' #{Shellwords.escape(temp_plist.path)}"
team_id = `#{team_id_cmd}`.strip
puts "Team Identifier: #{team_id}"
# Get profile type (development, distribution, etc.)
profile_type_cmd = "/usr/libexec/PlistBuddy -c 'Print :Entitlements:get-task-allow' #{Shellwords.escape(temp_plist.path)} 2>/dev/null"
get_task_allow = `#{profile_type_cmd}`.strip.downcase
if get_task_allow == "true"
puts "Profile Type: Development"
else
distribution_cmd = "/usr/libexec/PlistBuddy -c 'Print :ProvisionsAllDevices' #{Shellwords.escape(temp_plist.path)} 2>/dev/null"
provisions_all = `#{distribution_cmd}`.strip.downcase
if provisions_all == "true"
puts "Profile Type: Enterprise Distribution"
else
puts "Profile Type: App Store Distribution"
end
end
# Get expiration date
expiration_cmd = "/usr/libexec/PlistBuddy -c 'Print :ExpirationDate' #{Shellwords.escape(temp_plist.path)}"
expiration = `#{expiration_cmd}`.strip
puts "Expiration Date: #{expiration}"
else
puts "❌ Failed to parse profile: #{security_output}"
end
ensure
temp_plist.close
temp_plist.unlink
end
end
# Check code signing identities
puts "\nInspecting code signing identities:"
signing_identities = `security find-identity -v -p codesigning 2>&1`
puts signing_identities
# Check keychain configuration
puts "\nKeychain configuration:"
puts `security list-keychains -d user 2>&1`
# Check Xcode configuration
puts "\nXcode code signing search paths:"
puts "Provisioning profiles search path: ~/Library/MobileDevice/Provisioning Profiles/"
puts "Recommended check: In Xcode settings, verify your Apple ID is correctly logged in"
puts "--- End of Provisioning Profile Diagnostics ---\n"
end
### Android-specific Methods ###
def self.android_create_keystore(keystore_path)
if ENV["ANDROID_KEYSTORE"]
puts "Decoding Android keystore..."
FileUtils.mkdir_p(File.dirname(keystore_path))
File.write(keystore_path, Base64.decode64(ENV["ANDROID_KEYSTORE"]))
end
File.realpath(keystore_path)
end
def self.android_create_play_store_key(key_path)
if ENV["ANDROID_PLAY_STORE_JSON_KEY_BASE64"]
puts "Decoding Android Play Store JSON key..."
FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, Base64.decode64(ENV["ANDROID_PLAY_STORE_JSON_KEY_BASE64"]))
end
File.realpath(key_path)
end
# unused to do api key permissions
def self.android_verify_version_code(gradle_file_path)
latest_version = Fastlane::Actions::GooglePlayTrackVersionCodesAction.run(
track: "internal",
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
package_name: ENV["ANDROID_PACKAGE_NAME"],
).first
version_code_line = File.readlines(gradle_file_path).find { |line| line.include?("versionCode") }
current_version = version_code_line.match(/versionCode\s+(\d+)/)[1].to_i
if current_version <= latest_version
report_error(
"Version code must be greater than latest Play Store version!",
"Latest Play Store version: #{latest_version}\nCurrent version: #{current_version}\nPlease increment the version code in android/app/build.gradle",
"Version code verification failed"
)
else
report_success("Version code verified (Current: #{current_version}, Latest Play Store: #{latest_version})")
end
end
def self.android_increment_version_code(gradle_file_path)
gradle_file_full_path = File.expand_path(gradle_file_path, File.dirname(__FILE__))
unless File.exist?(gradle_file_full_path)
UI.error("Could not find build.gradle at: #{gradle_file_full_path}")
UI.user_error!("Please ensure the Android project is properly set up")
end
# Read current version code
gradle_content = File.read(gradle_file_full_path)
version_code_match = gradle_content.match(/versionCode\s+(\d+)/)
current_version_code = version_code_match ? version_code_match[1].to_i : 0
# TODO: fetch version code from play store when we have permissions
new_version = current_version_code + 1
# Update version code in file
if @@android_has_permissions
updated_content = gradle_content.gsub(/versionCode\s+\d+/, "versionCode #{new_version}")
File.write(gradle_file_full_path, updated_content)
end
report_success("Version code incremented from #{current_version_code} to #{new_version}")
@@android_has_permissions ? new_version : current_version_code
end
# Helper to log keychain diagnostics
def self.log_keychain_diagnostics(certificate_name)
puts "--- Fastlane Pre-Build Diagnostics ---"
begin
system("echo 'Running as user: $(whoami)'")
system("echo 'Default keychain:'")
system("security list-keychains -d user")
system("echo 'Identities in build.keychain:'")
# Use the absolute path expected in the GH runner environment
keychain_path = "/Users/runner/Library/Keychains/build.keychain-db"
system("security find-identity -v -p codesigning #{keychain_path} || echo 'No identities found or build.keychain doesn\'t exist at #{keychain_path}'")
rescue => e
puts "Error running security command: #{e.message}"
end
puts "Certificate name constructed by Fastlane: #{certificate_name}"
puts "--- End Fastlane Diagnostics ---"
end
### Slack Methods ###
# Uploads a file to Slack using the files.upload API endpoint.
# Handles multipart/form-data request construction.
#
# Args:
# file_path (String): Path to the file to upload.
# channel_id (String): ID of the channel to upload the file to.
# initial_comment (String, optional): Message to post alongside the file.
# thread_ts (String, optional): Timestamp of a message to reply to (creates a thread).
# title (String, optional): Title for the uploaded file (defaults to filename).
def self.upload_file_to_slack(file_path:, channel_id:, initial_comment: nil, thread_ts: nil, title: nil)
unless SLACK_TOKEN && !SLACK_TOKEN.strip.empty?
report_error("Missing SLACK_API_TOKEN environment variable.", "Cannot upload file to Slack without API token.", "Slack Upload Failed")
return false
end
unless File.exist?(file_path)
report_error("File not found at path: #{file_path}", "Please ensure the file exists before uploading.", "Slack Upload Failed")
return false
end
file_name = File.basename(file_path)
file_size = File.size(file_path)
file_title = title || file_name
begin
upload_url = nil
file_id = nil
# Step 1: Get Upload URL
with_retry(max_retries: 3, delay: 5) do
UI.message("Step 1: Getting Slack upload URL for #{file_name}...")
uri = URI.parse("https://slack.com/api/files.getUploadURLExternal")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{SLACK_TOKEN}"
request.set_form_data(filename: file_name, length: file_size)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
raise "Slack API (files.getUploadURLExternal) failed: #{response.code} #{response.body}"
end
response_json = JSON.parse(response.body)
unless response_json["ok"]
raise "Slack API Error (files.getUploadURLExternal): #{response_json["error"]}"
end
upload_url = response_json["upload_url"]
file_id = response_json["file_id"]
UI.message("Got upload URL and file ID: #{file_id}")
end
# Step 2: Upload file content to the obtained URL
with_retry(max_retries: 3, delay: 5) do
UI.message("Step 2: Uploading file content to Slack...")
upload_uri = URI.parse(upload_url)
# Net::HTTP::Post requires the request body to be an IO object or string
# Reading the file content here for the request body
file_content = File.binread(file_path)
upload_request = Net::HTTP::Post.new(upload_uri)
upload_request.body = file_content
# Slack's upload URL expects the raw file bytes in the body
# Content-Type is often application/octet-stream, but Slack might infer
upload_request["Content-Type"] = "application/octet-stream"
upload_request["Content-Length"] = file_size.to_s
upload_http = Net::HTTP.new(upload_uri.host, upload_uri.port)
upload_http.use_ssl = true
upload_response = upload_http.request(upload_request)
# Check for a 200 OK response for the file upload itself
unless upload_response.is_a?(Net::HTTPOK)
raise "File content upload failed: #{upload_response.code} #{upload_response.message} Body: #{upload_response.body}"
end
UI.message("File content uploaded successfully.")
end
# Step 3: Complete the upload
final_file_info = nil
with_retry(max_retries: 3, delay: 5) do
UI.message("Step 3: Completing Slack upload for file ID #{file_id}...")
complete_uri = URI.parse("https://slack.com/api/files.completeUploadExternal")
complete_request = Net::HTTP::Post.new(complete_uri)
complete_request["Authorization"] = "Bearer #{SLACK_TOKEN}"
complete_request["Content-Type"] = "application/json; charset=utf-8"
payload = {
files: [{ id: file_id, title: file_title }],
channel_id: channel_id,
}
payload[:initial_comment] = initial_comment if initial_comment
payload[:thread_ts] = thread_ts if thread_ts
complete_request.body = payload.to_json
complete_http = Net::HTTP.new(complete_uri.host, complete_uri.port)
complete_http.use_ssl = true
complete_response = complete_http.request(complete_request)
unless complete_response.is_a?(Net::HTTPSuccess)
raise "Slack API (files.completeUploadExternal) failed: #{complete_response.code} #{complete_response.body}"
end
complete_response_json = JSON.parse(complete_response.body)
unless complete_response_json["ok"]
# Specific error handling for common issues
if complete_response_json["error"] == "invalid_channel"
UI.error("Error: Invalid SLACK_CHANNEL_ID: '#{channel_id}'. Please verify the channel ID.")
elsif complete_response_json["error"] == "channel_not_found"
UI.error("Error: Channel '#{channel_id}' not found. Ensure the bot is invited or the ID is correct.")
end
raise "Slack API Error (files.completeUploadExternal): #{complete_response_json["error"]} - #{complete_response_json["response_metadata"]&.[]("messages")&.join(", ")}"
end
# Expecting an array of file objects
final_file_info = complete_response_json["files"]&.first
unless final_file_info
raise "Upload completed but no file information returned in response: #{complete_response.body}"
end
report_success("Successfully uploaded and shared #{file_name} (ID: #{final_file_info["id"]}) to Slack channel #{channel_id}")
end
return final_file_info # Return the first file object on success
rescue JSON::ParserError => e
report_error("Failed to parse Slack API response.", "Error: #{e.message}", "Slack Upload Failed")
return false
rescue => e
# Include backtrace for better debugging
report_error("Error during Slack upload process: #{e.message}", e.backtrace.join("\n"), "Slack Upload Failed")
return false
end
end
end
end

View File

@@ -25,7 +25,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<string>111</string>
<key>LSApplicationCategoryType</key>
<string />
<key>LSRequiresIPhoneOS</key>

View File

@@ -1,4 +1,5 @@
use_frameworks!
require 'tmpdir'
# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
@@ -9,7 +10,7 @@ require Pod::Executable.execute_command('node', ['-p',
project 'Self.xcodeproj'
platform :ios, '15.0'
platform :ios, '15.0' if !ENV['ACT']
prepare_react_native_project!
@@ -98,12 +99,40 @@ target 'Self' do
# update QKCutoutView.swift to hide OCR border
qkCutoutView = 'Pods/QKMRZScanner/QKMRZScanner/QKCutoutView.swift'
if File.exist?(qkCutoutView)
text = File.read(qkCutoutView)
# Comment out the line containing "addBorderAroundCutout()"
new_text = text.gsub(/^(\s*addBorderAroundCutout\s*\(\s*\))/, '// \1')
File.open(qkCutoutView, "w") { |file| file.puts new_text }
puts "Adding build phase script to patch QKCutoutView.swift"
phase_name = "Patch QKCutoutView to hide border"
# Find the QKMRZScanner target
qkmrz_target = installer.pods_project.targets.find { |t| t.name == 'QKMRZScanner' }
if qkmrz_target
# Check if the phase already exists to avoid duplicates
unless qkmrz_target.shell_script_build_phases.any? { |bp| bp.name == phase_name }
# Add a build phase that will patch the file during build time
phase = qkmrz_target.new_shell_script_build_phase(phase_name)
phase.shell_script = <<~SCRIPT
QKCUTOUT_PATH="${PODS_TARGET_SRCROOT}/QKMRZScanner/QKCutoutView.swift"
if [ -f "$QKCUTOUT_PATH" ]; then
# Use sed to comment out the line with addBorderAroundCutout
sed -i '' 's/^\\(\\s*addBorderAroundCutout\\s*(.*)\\)/\\/\\/\\1/' "$QKCUTOUT_PATH"
echo "Successfully patched QKCutoutView.swift to hide border"
else
echo "Warning: Could not find QKCutoutView.swift at $QKCUTOUT_PATH"
fi
SCRIPT
end
else
puts "Warning: Could not find QKMRZScanner target to add build phase"
end
else
puts "Warning: Could not find QKCutoutView.swift at #{qkCutoutView}"
end
# Disable code signing for Pod targets to avoid conflicts with main app signing
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
end
end
end
end

View File

@@ -1627,7 +1627,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSentry (6.8.0):
- RNSentry (6.10.0):
- DoubleConversion
- glog
- RCT-Folly (= 2024.01.01.00)
@@ -1647,17 +1647,17 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Sentry/HybridSDK (= 8.45.0)
- Sentry/HybridSDK (= 8.48.0)
- Yoga
- RNSVG (15.11.1):
- React-Core
- segment-analytics-react-native (2.20.3):
- React-Core
- sovran-react-native
- Sentry (8.45.0):
- Sentry/Core (= 8.45.0)
- Sentry/Core (8.45.0)
- Sentry/HybridSDK (8.45.0)
- Sentry (8.48.0):
- Sentry/Core (= 8.48.0)
- Sentry/Core (8.48.0)
- Sentry/HybridSDK (8.48.0)
- SentryPrivate (8.21.0)
- SocketRocket (0.7.0)
- sovran-react-native (1.1.3):
@@ -1960,93 +1960,92 @@ SPEC CHECKSUMS:
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: db03203d873afcbb6e0cea6a262a88d95e64a98f
lottie-react-native: 3ffec00c889acded6057766c99adf8eaced7790c
NFCPassportReader: e931c61c189e08a4b4afa0ed4014af19eab2f129
OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346
QKMRZParser: 6b419b6f07d6bff6b50429b97de10846dc902c29
QKMRZScanner: cf2348fd6ce441e758328da4adf231ef2b51d769
RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
RCT-Folly: 34124ae2e667a0e5f0ea378db071d27548124321
RCTDeprecation: 726d24248aeab6d7180dac71a936bbca6a994ed1
RCTRequired: a94e7febda6db0345d207e854323c37e3a31d93b
RCTTypeSafety: 28e24a6e44f5cbf912c66dde6ab7e07d1059a205
React: c2830fa483b0334bda284e46a8579ebbe0c5447e
React-callinvoker: 4aecde929540c26b841a4493f70ebf6016691eb8
React-Core: 1e3c04337857fa7fb7559f73f6f29a2a83a84b9c
React-CoreModules: 9fac2d31803c0ed03e4ddaa17f1481714f8633a5
React-cxxreact: c72a7a8066fc4323ea85a3137de50c8a10a69794
React-Core: 65374ea054f3f00eaa3c8bb5e989cb1ba8128844
React-CoreModules: f53e0674e1747fa41c83bc970e82add97b14ad87
React-cxxreact: bb77e88b645c5378ecd0c30c94f965a8294001d8
React-debug: 7e346b6eeacd2ee1118a0ee7d39f613b428b4be8
React-defaultsnativemodule: e40e760aa97a7183d5f5a8174e44026673c4b995
React-domnativemodule: 9fef73afd600e7c7d7f540d82532a113830bbdda
React-Fabric: dcd7ec3ea4da022b6c3f025e2567c9860ff1f760
React-FabricComponents: 7e67af984cab1d6d1c02aae4a62933abc1baa5d3
React-FabricImage: 77ca01a0a2bca3e1d39967220d7af7e3de033c9f
React-defaultsnativemodule: 4f1e9236c048fce31ebaf2c9c59ad7e76fb971a1
React-domnativemodule: 0d0e04cd8a68f3984b7b15aada7ff531dfc3c3bd
React-Fabric: fa636eabfe3c8a3af3a9bface586956e90619ebf
React-FabricComponents: 52382f668a934df9cef21a7893beffbe0e2b2f5e
React-FabricImage: 69b745c0231d9360180f5e411370c6fb0c3cb546
React-featureflags: 4c45b3c06f9a124d2598aff495bfc59470f40597
React-featureflagsnativemodule: d37e4fe27bd4f541d6d46f05e899345018067314
React-graphics: a2e6209991a191c94405a234460e05291fa986b9
React-idlecallbacksnativemodule: fa07e0af59ec6c950b2156b14c73c7fce4d0a663
React-ImageManager: 17772f78d93539a1a10901b5f537031772fa930c
React-featureflagsnativemodule: 110c225191b3bca92694d36303385c2c299c12e5
React-graphics: eb61d404819486a2d9335c043a967a0c4b8ca743
React-idlecallbacksnativemodule: ca6930a17eaae01591610c87b19dbd90515f54a1
React-ImageManager: 6652c4cc3de260b5269d58277de383cacd53a234
React-jsc: 4d3352be620f3fe2272238298aaccc9323b01824
React-jserrorhandler: 62af5111f6444688182a5850d4b584cbc0c5d6a8
React-jsi: 490deef195fd3f01d57dc89dda8233a84bd54b83
React-jsiexecutor: 13bcb5e11822b2a6b69dbb175a24a39e24a02312
React-jsinspector: 6961a23d4c11b72f3cbeb5083b0b18cc22bc48a1
React-jsitracing: dab78a74a581f63320604c9de4ab9039209e0501
React-logger: d79b704bf215af194f5213a6b7deec50ba8e6a9b
React-Mapbuffer: 42c779748af341935a63ad8831723b8cb1e97830
React-microtasksnativemodule: 744f7e26200ea3976fef8453101cefcc08756008
react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc
react-native-cloud-storage: 4c68bc6025c3624164461e15231efb28576f78a8
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
react-native-nfc-manager: 5213321cf6c18d879c8092c0bf56806b771ec5ac
react-native-quick-crypto: 07fd0ba954a08d8c6b4cd1ff8b1fd91df2b650d6
react-native-safe-area-context: 849d7df29ecb2a7155c769c0b76849ba952c2aa3
React-jserrorhandler: 552c5fcd2ee64307c568734b965ea082e1be25cf
React-jsi: b187c826e5bda25afb36ede4c54c146cd50c9d6c
React-jsiexecutor: ac8478b6c5f53bcf411a66bf4461e923dafeb0bd
React-jsinspector: a82cfe0794b831d6e36cf0c8c07da56a0aaa1282
React-jsitracing: e512a1023a25de831b51be1c773caa6036125a44
React-logger: 80d87daf2f98bf95ab668b79062c1e0c3f0c2f8a
React-Mapbuffer: b2642edd9be75d51ead8cda109c986665eae09cf
React-microtasksnativemodule: 7ebf131e1792a668004d2719a36da0ff8d19c43c
react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d
react-native-cloud-storage: 74d1f1456d714e0fca6d10c7ab6fe9a52ba203b6
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-nfc-manager: a280ef94cd4871a471b052f0dc70381cf1223049
react-native-quick-crypto: a7cc5f870f270488fee9047b56e481de85c32e33
react-native-safe-area-context: 3e33e7c43c8b74dba436a5a32651cb8d7064c740
React-nativeconfig: 31072ab0146e643594f6959c7f970a04b6c9ddd0
React-NativeModulesApple: 5df767d9a2197ac25f4d8dd2d4ae1af3624022e2
React-NativeModulesApple: 4ffcab4cdf34002540799bffbedd6466e8023c3a
React-perflogger: 59e1a3182dca2cee7b9f1f7aab204018d46d1914
React-performancetimeline: 3d70a278cc3344def506e97aff3640e658656110
React-performancetimeline: 2bf8625ff44f482cba84e48e4ab21dee405d68cd
React-RCTActionSheet: d80e68d3baa163e4012a47c1f42ddd8bcd9672cc
React-RCTAnimation: bde981f6bd7f8493696564da9b3bd05721d3b3cc
React-RCTAppDelegate: bc9c02d6dd4d162e3e1850283aba81bd246fc688
React-RCTBlob: e492d54533e61a81f2601494a6f393b3e15e33b9
React-RCTFabric: 4556aa70bd55b48d793cfb87e80d687c164298e2
React-RCTImage: 90448d2882464af6015ed57c98f463f8748be465
React-RCTLinking: 1bd95d0a704c271d21d758e0f0388cced768d77d
React-RCTNetwork: 218af6e63eb9b47935cc5a775b7a1396cf10ff91
React-RCTSettings: e10b8e42b0fce8a70fbf169de32a2ae03243ef6b
React-RCTText: e7bf9f4997a1a0b45c052d4ad9a0fe653061cf29
React-RCTVibration: 5b70b7f11e48d1c57e0d4832c2097478adbabe93
React-RCTAnimation: 051f0781709c5ed80ba8aa2b421dfb1d72a03162
React-RCTAppDelegate: 99345256dcceddcacab539ff8f56635de6a2f551
React-RCTBlob: e949797c162421e363f93bfd8b546b7e632ba847
React-RCTFabric: 396093d9aeee4bd3a6021ec6df8ed012f78763ef
React-RCTImage: b73149c0cd54b641dba2d6250aaf168fee784d9f
React-RCTLinking: 23e519712285427e50372fbc6e0265d422abf462
React-RCTNetwork: a5d06d122588031989115f293654b13353753630
React-RCTSettings: 87d03b5d94e6eadd1e8c1d16a62f790751aafb55
React-RCTText: 75e9dd39684f4bcd1836134ac2348efaca7437b3
React-RCTVibration: 033c161fe875e6fa096d0d9733c2e2501682e3d4
React-rendererconsistency: 35cef4bc4724194c544b6e5e2bd9b3f7aff52082
React-rendererdebug: 9b1a6a2d4f8086a438f75f28350ccba16b7b706a
React-rendererdebug: 4e801e9f8d16d21877565dca2845a2e56202b8c6
React-rncore: 2c7c94d6e92db0850549223eb2fa8272e0942ac2
React-RuntimeApple: 22397aca29a0c9be681db02c68416e508a381ef1
React-RuntimeCore: a6d413611876d8180a5943b80cba3cefdf95ad5f
React-RuntimeApple: 0f661760cfcfa5d9464f7e05506874643e88fc2d
React-RuntimeCore: 1d0fcc0eb13807818e79ccaf48915596f0f5f0e6
React-runtimeexecutor: ea90d8e3a9e0f4326939858dafc6ab17c031a5d3
React-runtimescheduler: e041df0539ad8a8a370e3507c39a9ab0571bb848
React-utils: 768a7eb396b7df37aa19389201652eac613490cd
ReactCodegen: c53f8a0fa088739ee9929820feec1508043c7e6c
ReactCommon: 03d2d48fcd1329fe3bc4e428a78a0181b68068c2
RNCAsyncStorage: 03861ec2e1e46b20e51963c62c51dc288beb7c43
RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d
RNDeviceInfo: feea80a690d2bde1fe51461cf548039258bd03f2
RNGestureHandler: 5639cd6112a3aa3bebc3871e3bf4e83940e20f6f
RNGoogleSignin: ee2938633d996756819e3212c8e0f7f696b380d0
RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678
RNLocalize: 06991b9c31e7a898a9fa6ddb204ce0f53a967248
RNReactNativeHapticFeedback: cba92e59f56506f6058d261dc85986012b2c5032
RNScreens: d2a8ff4833a42f4eeadaea244f0bd793301b8810
RNSentry: 7f68d46fd7f2315484bc1bdf54184d5fab48db5d
RNSVG: 669ed128ab9005090c612a0d627dbecb6ab5c76f
segment-analytics-react-native: d57ed4971cbb995706babf29215ebdbf242ecdab
Sentry: f7c0c4b82e2f7e5909a660544dfaff84e35d5e03
React-runtimescheduler: 6b33edee8c830c7926670df4232d51f4f6a82795
React-utils: 7198bd077f07ce8f9263c05bf610da6e251058ad
ReactCodegen: a2d336e0bec3d2f45475df55e7a02cc4e4c19623
ReactCommon: b02a50498cb1071cd793044ddbd5d2b5f4db0a34
RNCAsyncStorage: af7b591318005069c3795076addc83a4dd5c0a2e
RNCClipboard: 4abb037e8fe3b98a952564c9e0474f91c492df6d
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNGestureHandler: 9c3877d98d4584891b69d16ebca855ac46507f4d
RNGoogleSignin: b8760528f2a7cbe157ecfdcc13bdb7d2745c9389
RNKeychain: bbe2f6d5cc008920324acb49ef86ccc03d3b38e4
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
RNReactNativeHapticFeedback: e19b9b2e2ecf5593de8c4ef1496e1e31ae227514
RNScreens: b7e8d29c6be98f478bc3fb4a97cc770aa9ba7509
RNSentry: c462461c0a5aaba206265f1f3db01b237cd33239
RNSVG: 46769c92d1609e617dbf9326ad8a0cff912d0982
segment-analytics-react-native: 6f98edf18246782ee7428c5380c6519a3d2acf5e
Sentry: 1ca8405451040482877dcd344dfa3ef80b646631
SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
sovran-react-native: eec37f82e4429f0e3661f46aaf4fcd85d1b54f60
sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594
SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
Yoga: b05994d1933f507b0a28ceaa4fdb968dc18da178
PODFILE CHECKSUM: 4083907fcd59614c8d3d8cef569079861356d36f
PODFILE CHECKSUM: 9eaf085590e4280f6aedd49c2efe960fbf2b4079
COCOAPODS: 1.16.2

View File

@@ -481,7 +481,7 @@
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 110;
CURRENT_PROJECT_VERSION = 111;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5B29R5LYHQ;
ENABLE_BITCODE = NO;
@@ -619,7 +619,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements;
CURRENT_PROJECT_VERSION = 110;
CURRENT_PROJECT_VERSION = 111;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5B29R5LYHQ;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -6,22 +6,40 @@
"analyze:android": "yarn reinstall && react-native-bundle-visualizer --platform android --dev",
"analyze:ios": "yarn reinstall && react-native-bundle-visualizer --platform ios --dev",
"android": "react-native run-android",
"android:build-apk": "yarn reinstall && cd ./android && ./gradlew clean assembleRelease && cd ..",
"android:build-debug": "yarn reinstall && cd ./android && yarn android:build-debug-bundle && ./gradlew clean assembleDebug && cd ..",
"android:build-debug-bundle": "yarn react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/",
"android:build-release": "yarn reinstall && cd ./android && ./gradlew clean bundleRelease && cd ..",
"android:build-apk": "yarn reinstall && cd ./android && ./gradlew clean assembleRelease && cd ..",
"android:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose android internal_test",
"bump-version:major": "npm version major && yarn sync-versions",
"bump-version:minor": "npm version minor && yarn sync-versions",
"bump-version:patch": "npm version patch && yarn sync-versions",
"clean": "watchman watch-del-all && rm -rf node_modules ios/Pods ios/build android/app/build android/build .yarn/cache ios/.xcode.env.local",
"clean:android": "rm -rf android/app/build android/build",
"clean:ios": "rm -rf ios/Pods ios/build",
"clean:xcode-env-local": "rm -f ios/.xcode.env.local",
"fmt": "prettier --check .",
"fmt:fix": "prettier --write .",
"force-local-upload-deploy": "yarn force-local-upload-deploy:android && yarn force-local-upload-deploy:ios",
"force-local-upload-deploy:android": "yarn clean:android && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane android deploy --verbose",
"force-local-upload-deploy:ios": "yarn clean:ios && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane ios deploy --verbose",
"force-local-upload-test": "yarn force-local-upload-test:android && yarn force-local-upload-test:ios",
"force-local-upload-test:android": "yarn clean:android && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane android internal_test --verbose",
"force-local-upload-test:ios": "yarn clean:ios && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane ios internal_test --verbose",
"ia": "yarn install-app",
"install-app": "cd ../common && yarn && cd ../app && yarn && cd ios && pod install && cd .. && yarn clean:xcode-env-local",
"install-app": "yarn install-app:setup && cd ios && bundle exec pod install && cd .. && yarn clean:xcode-env-local",
"install-app:deploy": "yarn install-app:setup && yarn clean:xcode-env-local",
"install-app:setup": "cd ../common && yarn && cd ../app && yarn && cd ios && bundle install && cd ..",
"ios": "react-native run-ios",
"ios:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose ios internal_test",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"nice": "yarn lint:fix && yarn fmt:fix",
"reinstall": "yarn clean && yarn install && yarn install-app",
"start": "watchman watch-del-all && react-native start",
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
"tag:release": "node scripts/tag.js release",
"tag:remove": "node scripts/tag.js remove",
"test": "jest --passWithNoTests"
},
"dependencies": {
@@ -37,7 +55,7 @@
"@react-navigation/native-stack": "^7.2.0",
"@segment/analytics-react-native": "^2.20.3",
"@segment/sovran-react-native": "^1.1.3",
"@sentry/react-native": "^6.8.0",
"@sentry/react-native": "^6.10.0",
"@stablelib/cbor": "^2.0.1",
"@tamagui/config": "1.110.0",
"@tamagui/lucide-icons": "1.110.0",

117
app/scripts/tag.js Normal file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Get package version
function getVersion() {
const packageJson = JSON.parse(
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'),
);
return packageJson.version;
}
// Check if working directory is clean
function isWorkingDirectoryClean() {
try {
const status = execSync('git status --porcelain').toString();
return status.trim() === '';
} catch (error) {
console.error('Error checking git status:', error.message);
return false;
}
}
// Create an empty commit
function createEmptyCommit(version) {
try {
execSync(`git commit --allow-empty -m "chore: release v${version}"`);
console.log(`Created empty commit for v${version}`);
} catch (error) {
console.error('Error creating commit:', error.message);
process.exit(1);
}
}
// Create git tag
function createTag(version) {
try {
execSync(`git tag v${version}`);
console.log(`Created tag v${version}`);
} catch (error) {
console.error('Error creating tag:', error.message);
process.exit(1);
}
}
// Push tag to remote
function pushTag(version) {
try {
execSync(`git push origin v${version}`);
console.log(`Pushed tag v${version} to remote`);
} catch (error) {
console.error('Error pushing tag:', error.message);
process.exit(1);
}
}
// Remove tag locally and from remote
function removeTag(version) {
try {
execSync(`git tag -d v${version}`);
execSync(`git push origin :refs/tags/v${version}`);
console.log(`Removed tag v${version}`);
} catch (error) {
console.error('Error removing tag:', error.message);
process.exit(1);
}
}
// Main function to handle commands
function main() {
const command = process.argv[2];
const version = getVersion();
switch (command) {
case 'commit':
if (!isWorkingDirectoryClean()) {
console.error(
'Error: Working directory is not clean. Please commit or stash changes first.',
);
process.exit(1);
}
createEmptyCommit(version);
break;
case 'create':
createTag(version);
break;
case 'push':
pushTag(version);
break;
case 'remove':
removeTag(version);
break;
case 'release':
if (!isWorkingDirectoryClean()) {
console.error(
'Error: Working directory is not clean. Please commit or stash changes first.',
);
process.exit(1);
}
createEmptyCommit(version);
createTag(version);
pushTag(version);
break;
default:
console.log('Usage: node tag.js [commit|create|push|remove|release]');
process.exit(1);
}
}
main();

View File

@@ -2915,10 +2915,10 @@ __metadata:
languageName: node
linkType: hard
"@sentry/babel-plugin-component-annotate@npm:3.2.0":
version: 3.2.0
resolution: "@sentry/babel-plugin-component-annotate@npm:3.2.0"
checksum: 10c0/f9ca9c5f64b86e129bb9bb4101d16c6e274b915d415279b1a09d2b80513c7280f5802e17b8a991674390d68cfa48d5033055e1632779f82d92bf9466ee2428a6
"@sentry/babel-plugin-component-annotate@npm:3.2.2":
version: 3.2.2
resolution: "@sentry/babel-plugin-component-annotate@npm:3.2.2"
checksum: 10c0/96835b165b6f7904eb74d117fed137f724490bc919940c740113eb2d8836de6ccd977b7ec056a0ca20712e6cdb19c30a9f515ec0028ecac70c7880f0a6b9a58d
languageName: node
linkType: hard
@@ -2935,66 +2935,66 @@ __metadata:
languageName: node
linkType: hard
"@sentry/cli-darwin@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli-darwin@npm:2.42.1"
"@sentry/cli-darwin@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli-darwin@npm:2.42.4"
conditions: os=darwin
languageName: node
linkType: hard
"@sentry/cli-linux-arm64@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli-linux-arm64@npm:2.42.1"
"@sentry/cli-linux-arm64@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli-linux-arm64@npm:2.42.4"
conditions: (os=linux | os=freebsd) & cpu=arm64
languageName: node
linkType: hard
"@sentry/cli-linux-arm@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli-linux-arm@npm:2.42.1"
"@sentry/cli-linux-arm@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli-linux-arm@npm:2.42.4"
conditions: (os=linux | os=freebsd) & cpu=arm
languageName: node
linkType: hard
"@sentry/cli-linux-i686@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli-linux-i686@npm:2.42.1"
"@sentry/cli-linux-i686@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli-linux-i686@npm:2.42.4"
conditions: (os=linux | os=freebsd) & (cpu=x86 | cpu=ia32)
languageName: node
linkType: hard
"@sentry/cli-linux-x64@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli-linux-x64@npm:2.42.1"
"@sentry/cli-linux-x64@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli-linux-x64@npm:2.42.4"
conditions: (os=linux | os=freebsd) & cpu=x64
languageName: node
linkType: hard
"@sentry/cli-win32-i686@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli-win32-i686@npm:2.42.1"
"@sentry/cli-win32-i686@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli-win32-i686@npm:2.42.4"
conditions: os=win32 & (cpu=x86 | cpu=ia32)
languageName: node
linkType: hard
"@sentry/cli-win32-x64@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli-win32-x64@npm:2.42.1"
"@sentry/cli-win32-x64@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli-win32-x64@npm:2.42.4"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@sentry/cli@npm:2.42.1":
version: 2.42.1
resolution: "@sentry/cli@npm:2.42.1"
"@sentry/cli@npm:2.42.4":
version: 2.42.4
resolution: "@sentry/cli@npm:2.42.4"
dependencies:
"@sentry/cli-darwin": "npm:2.42.1"
"@sentry/cli-linux-arm": "npm:2.42.1"
"@sentry/cli-linux-arm64": "npm:2.42.1"
"@sentry/cli-linux-i686": "npm:2.42.1"
"@sentry/cli-linux-x64": "npm:2.42.1"
"@sentry/cli-win32-i686": "npm:2.42.1"
"@sentry/cli-win32-x64": "npm:2.42.1"
"@sentry/cli-darwin": "npm:2.42.4"
"@sentry/cli-linux-arm": "npm:2.42.4"
"@sentry/cli-linux-arm64": "npm:2.42.4"
"@sentry/cli-linux-i686": "npm:2.42.4"
"@sentry/cli-linux-x64": "npm:2.42.4"
"@sentry/cli-win32-i686": "npm:2.42.4"
"@sentry/cli-win32-x64": "npm:2.42.4"
https-proxy-agent: "npm:^5.0.0"
node-fetch: "npm:^2.6.7"
progress: "npm:^2.0.3"
@@ -3017,7 +3017,7 @@ __metadata:
optional: true
bin:
sentry-cli: bin/sentry-cli
checksum: 10c0/2e1cfd587c7e6f4a5f5e056043eb3205ae6a716b9d1a1b690857fc5905df1785a903103510e95645a23274605ac560061183d294815c3ec45bdb0ca77726f101
checksum: 10c0/e3900743803470874228a7d9b02f54e7973b01c89d433cd03d7d6fe71ae44df3f4420743fcae821613a572978ab1ff6fa397ab447a320bbe021bee333f04f397
languageName: node
linkType: hard
@@ -3028,13 +3028,13 @@ __metadata:
languageName: node
linkType: hard
"@sentry/react-native@npm:^6.8.0":
version: 6.8.0
resolution: "@sentry/react-native@npm:6.8.0"
"@sentry/react-native@npm:^6.10.0":
version: 6.10.0
resolution: "@sentry/react-native@npm:6.10.0"
dependencies:
"@sentry/babel-plugin-component-annotate": "npm:3.2.0"
"@sentry/babel-plugin-component-annotate": "npm:3.2.2"
"@sentry/browser": "npm:8.54.0"
"@sentry/cli": "npm:2.42.1"
"@sentry/cli": "npm:2.42.4"
"@sentry/core": "npm:8.54.0"
"@sentry/react": "npm:8.54.0"
"@sentry/types": "npm:8.54.0"
@@ -3048,7 +3048,7 @@ __metadata:
optional: true
bin:
sentry-expo-upload-sourcemaps: scripts/expo-upload-sourcemaps.js
checksum: 10c0/6bcf9b9ca5cd855740c3300ee407421e055615f05e1bb3ecb5ef308afea873bb3795329610a2e8bccfa99343d1a37fa01627207064c44ca85c603f8412b01eb2
checksum: 10c0/792cbb4437edea18f99bd0a5d1106523321556737758a88c742cdbdaf111eb344c736dfa4ef4a6d43213f56153714a2184dddde8d625c0932de36c9de5d32d29
languageName: node
linkType: hard
@@ -11964,7 +11964,7 @@ __metadata:
"@react-navigation/native-stack": "npm:^7.2.0"
"@segment/analytics-react-native": "npm:^2.20.3"
"@segment/sovran-react-native": "npm:^1.1.3"
"@sentry/react-native": "npm:^6.8.0"
"@sentry/react-native": "npm:^6.10.0"
"@stablelib/cbor": "npm:^2.0.1"
"@tamagui/config": "npm:1.110.0"
"@tamagui/lucide-icons": "npm:1.110.0"