diff --git a/.actrc b/.actrc
new file mode 100644
index 000000000..ff718c5a1
--- /dev/null
+++ b/.actrc
@@ -0,0 +1,3 @@
+--container-architecture linux/amd64
+--platform macos-latest=catthehacker/ubuntu:runner-latest
+--secret-file ./app/fastlane/.env.secrets
\ No newline at end of file
diff --git a/.github/actions/get-version/action.yml b/.github/actions/get-version/action.yml
new file mode 100644
index 000000000..7cba6b139
--- /dev/null
+++ b/.github/actions/get-version/action.yml
@@ -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
\ No newline at end of file
diff --git a/.github/actions/mobile-setup/action.yml b/.github/actions/mobile-setup/action.yml
new file mode 100644
index 000000000..c7228f4ed
--- /dev/null
+++ b/.github/actions/mobile-setup/action.yml
@@ -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
diff --git a/.github/actions/push-changes/action.yml b/.github/actions/push-changes/action.yml
new file mode 100644
index 000000000..c9afa32af
--- /dev/null
+++ b/.github/actions/push-changes/action.yml
@@ -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
diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml
new file mode 100644
index 000000000..c96b0e9ab
--- /dev/null
+++ b/.github/workflows/mobile-deploy.yml
@@ -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"
diff --git a/app/.gitignore b/app/.gitignore
index 7b8700026..d065a83f6 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -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
diff --git a/app/.prettierignore b/app/.prettierignore
index 11053cea8..5d6c3d38e 100644
--- a/app/.prettierignore
+++ b/app/.prettierignore
@@ -5,4 +5,5 @@ node_modules/
src/assets/animations/
witnesscalc/
vendor/
-android/
\ No newline at end of file
+android/
+*.md
diff --git a/app/.ruby-version b/app/.ruby-version
new file mode 100644
index 000000000..6a3913b04
--- /dev/null
+++ b/app/.ruby-version
@@ -0,0 +1 @@
+3.2.7
\ No newline at end of file
diff --git a/app/Gemfile b/app/Gemfile
index 5039155be..e8bce84e1 100644
--- a/app/Gemfile
+++ b/app/Gemfile
@@ -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'
\ No newline at end of file
+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)
diff --git a/app/Gemfile.lock b/app/Gemfile.lock
index 44b8ca7c5..bfd946d08 100644
--- a/app/Gemfile.lock
+++ b/app/Gemfile.lock
@@ -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
diff --git a/app/android/build.gradle b/app/android/build.gradle
index 247f7ae9f..0524c12b7 100644
--- a/app/android/build.gradle
+++ b/app/android/build.gradle
@@ -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"
}
diff --git a/app/fastlane/.env.secrets.example b/app/fastlane/.env.secrets.example
new file mode 100644
index 000000000..ef4932820
--- /dev/null
+++ b/app/fastlane/.env.secrets.example
@@ -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=
diff --git a/app/fastlane/DEV.md b/app/fastlane/DEV.md
new file mode 100644
index 000000000..16a9fc732
--- /dev/null
+++ b/app/fastlane/DEV.md
@@ -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
diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile
new file mode 100644
index 000000000..85c0ddf57
--- /dev/null
+++ b/app/fastlane/Fastfile
@@ -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
diff --git a/app/fastlane/Pluginfile b/app/fastlane/Pluginfile
new file mode 100644
index 000000000..ad2875569
--- /dev/null
+++ b/app/fastlane/Pluginfile
@@ -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'
diff --git a/app/fastlane/README.md b/app/fastlane/README.md
new file mode 100644
index 000000000..61b95b6cc
--- /dev/null
+++ b/app/fastlane/README.md
@@ -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).
diff --git a/app/fastlane/helpers.rb b/app/fastlane/helpers.rb
new file mode 100644
index 000000000..13a8088b0
--- /dev/null
+++ b/app/fastlane/helpers.rb
@@ -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
diff --git a/app/ios/OpenPassport/Info.plist b/app/ios/OpenPassport/Info.plist
index 9cad2fbc7..907b17fc9 100644
--- a/app/ios/OpenPassport/Info.plist
+++ b/app/ios/OpenPassport/Info.plist
@@ -25,7 +25,7 @@
CFBundleSignature
????
CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
+ 111
LSApplicationCategoryType
LSRequiresIPhoneOS
diff --git a/app/ios/Podfile b/app/ios/Podfile
index 9c50642fb..0bcb6b9a6 100644
--- a/app/ios/Podfile
+++ b/app/ios/Podfile
@@ -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
\ No newline at end of file
diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock
index de70d23e0..13630a92b 100644
--- a/app/ios/Podfile.lock
+++ b/app/ios/Podfile.lock
@@ -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
-
diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj
index cff1d2675..037535aaa 100644
--- a/app/ios/Self.xcodeproj/project.pbxproj
+++ b/app/ios/Self.xcodeproj/project.pbxproj
@@ -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 = (
diff --git a/app/package.json b/app/package.json
index 97d351249..5962ed6db 100644
--- a/app/package.json
+++ b/app/package.json
@@ -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",
diff --git a/app/scripts/tag.js b/app/scripts/tag.js
new file mode 100644
index 000000000..40a2f78bc
--- /dev/null
+++ b/app/scripts/tag.js
@@ -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();
diff --git a/app/yarn.lock b/app/yarn.lock
index ef43d8d8c..537e3725d 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -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"