diff --git a/.circleci/config.yml b/.circleci/config.yml index e1dcd081..1cb09d34 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -98,6 +98,29 @@ jobs: command: | yarn prepare + pod-install: + <<: *macos_config + # React Native 0.81's pod install requires Xcode 16.1 or newer. + macos: + xcode: 26.4.0 + steps: + - attach_project + - node/install: + node-version: 22.14.0 + - run: + # The Xcode 26 image does not ship the Ruby pinned in .ruby-version. + name: Install Ruby and gems + command: | + rbenv install --skip-existing + bundle install + - run: + name: Install example pods with an empty Intercom iOS SDK cache + command: | + export INTERCOM_IOS_SDK_CACHE_DIR="$(mktemp -d)" + cd examples/example/ios + bundle exec pod install + ls "$INTERCOM_IOS_SDK_CACHE_DIR"/*/Intercom.xcframework.zip + workflows: version: 2.1 build-and-test: @@ -120,3 +143,7 @@ workflows: <<: *run_always requires: - install-dependencies + - pod-install: + <<: *run_always + requires: + - install-dependencies diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index 83fa8687..9fa5529a 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -36,7 +36,7 @@ jobs: id: current-versions run: | # Extract current iOS version from podspec - IOS_VERSION=$(grep -E "s\.dependency.*Intercom" intercom-react-native.podspec | sed -E "s/.*'([^']+)'.*/\1/" | sed 's/~> //') + IOS_VERSION=$(sed -nE 's/^intercom_ios_sdk_version = "([^"]+)"$/\1/p' intercom-react-native.podspec) # Extract current Android version from build.gradle ANDROID_VERSION=$(grep -E "io\.intercom\.android:intercom-sdk:" android/build.gradle | head -1 | sed -E "s/.*:([^']+)'.*/\1/") @@ -56,6 +56,7 @@ jobs: IOS_RELEASE=$(curl -s https://api.github.com/repos/intercom/intercom-ios/releases/latest) IOS_VERSION=$(echo "$IOS_RELEASE" | jq -r '.tag_name // "null"') IOS_CHANGELOG=$(echo "$IOS_RELEASE" | jq -r '.body // "No changelog available"') + IOS_ZIP_DIGEST=$(echo "$IOS_RELEASE" | jq -r '[.assets[]? | select(.name == "Intercom.xcframework.zip") | .digest][0] // ""') # Get Android release ANDROID_RELEASE=$(curl -s https://api.github.com/repos/intercom/intercom-android/releases/latest) @@ -66,6 +67,7 @@ jobs: echo "🤖 Latest Android SDK: $ANDROID_VERSION" echo "ios_version=$IOS_VERSION" >> $GITHUB_OUTPUT + echo "ios_zip_digest=$IOS_ZIP_DIGEST" >> $GITHUB_OUTPUT echo "android_version=$ANDROID_VERSION" >> $GITHUB_OUTPUT echo "ios_changelog<> $GITHUB_OUTPUT echo "$IOS_CHANGELOG" >> $GITHUB_OUTPUT @@ -150,7 +152,7 @@ jobs: echo " Target: iOS=$TARGET_IOS, Android=$TARGET_ANDROID" # Find existing PRs - EXISTING_PRS=$(gh pr list --state open --search "update Intercom SDK dependencies" --json number,title,headRefName 2>/dev/null || echo "[]") + EXISTING_PRS=$(gh pr list --state open --search "update Intercom SDK dependencies in:title" --json number,title,headRefName 2>/dev/null || echo "[]") if [ "$EXISTING_PRS" = "[]" ] || [ "$(echo "$EXISTING_PRS" | jq 'length')" -eq 0 ]; then echo "✅ No existing PRs found - proceeding with update" @@ -252,6 +254,7 @@ jobs: IOS_NEEDS_UPDATE: ${{ steps.update-analysis.outputs.ios_needs_update }} IOS_NEW_VERSION: ${{ steps.update-analysis.outputs.ios_new_version }} IOS_OLD_VERSION: ${{ steps.current-versions.outputs.ios_version }} + IOS_RELEASE_DIGEST: ${{ steps.latest-versions.outputs.ios_zip_digest }} ANDROID_NEEDS_UPDATE: ${{ steps.update-analysis.outputs.android_needs_update }} ANDROID_NEW_VERSION: ${{ steps.update-analysis.outputs.android_new_version }} ANDROID_OLD_VERSION: ${{ steps.current-versions.outputs.android_version }} @@ -262,7 +265,21 @@ jobs: # Update iOS if needed if [ "$IOS_NEEDS_UPDATE" = "true" ]; then echo "📱 Updating iOS SDK: $IOS_OLD_VERSION → $IOS_NEW_VERSION" - sed -i '' "s/s\.dependency \"Intercom\", '~> [^']*'/s.dependency \"Intercom\", '~> $IOS_NEW_VERSION'/" intercom-react-native.podspec + IOS_ZIP="$RUNNER_TEMP/Intercom.xcframework.zip" + curl --fail --silent --show-error --location --retry 3 --output "$IOS_ZIP" \ + "https://github.com/intercom/intercom-ios/releases/download/$IOS_NEW_VERSION/Intercom.xcframework.zip" + IOS_SHA256=$(shasum -a 256 "$IOS_ZIP" | cut -d ' ' -f 1) + if [ "$IOS_RELEASE_DIGEST" != "sha256:$IOS_SHA256" ]; then + echo "❌ Downloaded Intercom.xcframework.zip ($IOS_SHA256) does not match the release digest ($IOS_RELEASE_DIGEST)" + exit 1 + fi + sed -i '' -E "s/^intercom_ios_sdk_version = \"[^\"]*\"/intercom_ios_sdk_version = \"$IOS_NEW_VERSION\"/" intercom-react-native.podspec + sed -i '' -E "s/^intercom_ios_sdk_sha256 = \"[^\"]*\"/intercom_ios_sdk_sha256 = \"$IOS_SHA256\"/" intercom-react-native.podspec + if ! grep -qxF "intercom_ios_sdk_version = \"$IOS_NEW_VERSION\"" intercom-react-native.podspec \ + || ! grep -qxF "intercom_ios_sdk_sha256 = \"$IOS_SHA256\"" intercom-react-native.podspec; then + echo "❌ Could not update the iOS SDK version and checksum in intercom-react-native.podspec" + exit 1 + fi CHANGES="$CHANGES\\n- Updated iOS SDK from $IOS_OLD_VERSION to $IOS_NEW_VERSION" PR_TITLE_PARTS="iOS: $IOS_OLD_VERSION → $IOS_NEW_VERSION" @@ -343,7 +360,7 @@ jobs: if [ -f "$dir/Podfile" ]; then echo "📱 Updating pods in $dir" cd "$dir" - bundle exec pod install --repo-update || pod install --repo-update || pod update Intercom || echo "Pod install failed in $dir" + bundle exec pod install --repo-update || pod install --repo-update || echo "Pod install failed in $dir" cd - > /dev/null # Add any updated Podfile.lock files diff --git a/.gitignore b/.gitignore index 4b7725b2..2a0e607b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ examples/*/vendor/ # Cocoapods # examples/*/ios/Pods +intercom-ios-sdk/ # node.js # diff --git a/README.md b/README.md index 0233def2..077c04bc 100644 --- a/README.md +++ b/README.md @@ -738,11 +738,13 @@ The Expo plugin automatically generates a `FirebaseMessagingService` for Android ### Versioning in dependencies -We define the iOS and Android versions used in React Native with optimistic operators -i.e. `~> for iOS` and `+ for Android`, ensuring compatibility while allowing for non-breaking updates: - -iOS (~> in CocoaPods): -We use a version constraint e.g.`~> 18.6.1` which allows updates from `18.6.1 to 18.6.x` (up to but not including 18.7.0). +iOS (exact version): +The Intercom iOS SDK is no longer published to the CocoaPods trunk. During `pod install`, the podspec downloads +`Intercom.xcframework.zip` for the exact iOS SDK version this release was built against from the +[intercom-ios GitHub release](https://github.com/intercom/intercom-ios/releases), verifies its SHA-256 checksum +and links it. You don't need to add the `Intercom` pod to your Podfile (remove it if you added it yourself). +The download is cached in `~/Library/Caches/intercom-react-native`; set `INTERCOM_IOS_SDK_CACHE_DIR` to use +another directory, for example one your CI caches or one you pre-fill for offline builds. Android (+ in Gradle): We use a version constraint e.g. `18.6.+` which allows updates to any patch version within 18.6.x diff --git a/examples/example/ios/Podfile.lock b/examples/example/ios/Podfile.lock index faf993e9..1ef5132a 100644 --- a/examples/example/ios/Podfile.lock +++ b/examples/example/ios/Podfile.lock @@ -8,7 +8,6 @@ PODS: - hermes-engine (0.81.1): - hermes-engine/Pre-built (= 0.81.1) - hermes-engine/Pre-built (0.81.1) - - Intercom (19.9.2) - intercom-react-native (10.9.0): - boost - DoubleConversion @@ -16,7 +15,6 @@ PODS: - fmt - glog - hermes-engine - - Intercom (~> 19.9.2) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -2398,7 +2396,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - Intercom - SocketRocket EXTERNAL SOURCES: @@ -2560,8 +2557,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca - Intercom: c69d3e6909b6ef64b1bfbac5ca746bcdf2f67a1f - intercom-react-native: 2cbdb91ae67222232817745b3fcdeb2379e8ec76 + intercom-react-native: 4972c5f28ed080e890474dd446c8040c4a9e08e8 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/examples/with-notifications/ios/Podfile.lock b/examples/with-notifications/ios/Podfile.lock index 4471bf68..5dc470d7 100644 --- a/examples/with-notifications/ios/Podfile.lock +++ b/examples/with-notifications/ios/Podfile.lock @@ -8,7 +8,6 @@ PODS: - hermes-engine (0.81.1): - hermes-engine/Pre-built (= 0.81.1) - hermes-engine/Pre-built (0.81.1) - - Intercom (19.9.2) - intercom-react-native (10.9.0): - boost - DoubleConversion @@ -16,7 +15,6 @@ PODS: - fmt - glog - hermes-engine - - Intercom (~> 19.9.2) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -2603,7 +2601,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - Intercom - MMKV - MMKVCore - SocketRocket @@ -2773,8 +2770,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca - Intercom: c69d3e6909b6ef64b1bfbac5ca746bcdf2f67a1f - intercom-react-native: 2cbdb91ae67222232817745b3fcdeb2379e8ec76 + intercom-react-native: 4972c5f28ed080e890474dd446c8040c4a9e08e8 MMKV: 7b5df6a8bf785c6705cc490c541b9d8a957c4a64 MMKVCore: 3f40b896e9ab522452df9df3ce983471aa2449ba RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f diff --git a/intercom-react-native.podspec b/intercom-react-native.podspec index 971b2e49..56d24758 100644 --- a/intercom-react-native.podspec +++ b/intercom-react-native.podspec @@ -1,7 +1,10 @@ require "json" +require File.join(__dir__, "ios", "intercom_ios_sdk") package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +intercom_ios_sdk_version = "19.9.2" +intercom_ios_sdk_sha256 = "6af284508f58de833313b263f3d82eaebc255c4d9a56ad9adf2fc6af4228c0f4" Pod::Spec.new do |s| s.name = "intercom-react-native" @@ -19,7 +22,13 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" } - s.dependency "Intercom", '~> 19.9.2' + s.vendored_frameworks = IntercomIosSdk.install_xcframework( + version: intercom_ios_sdk_version, + sha256: intercom_ios_sdk_sha256, + package_root: __dir__ + ) + s.frameworks = ["Foundation", "UIKit", "Accelerate", "Security", "SystemConfiguration", "MobileCoreServices", "ImageIO", "AudioToolbox", "QuartzCore", "CoreGraphics", "Photos", "WebKit"] + s.libraries = "icucore", "xml2" is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' diff --git a/ios/intercom_ios_sdk.rb b/ios/intercom_ios_sdk.rb new file mode 100644 index 00000000..017f2017 --- /dev/null +++ b/ios/intercom_ios_sdk.rb @@ -0,0 +1,66 @@ +require 'digest' +require 'fileutils' +require 'tmpdir' + +# The Intercom pod is no longer published to the CocoaPods trunk; fetch the GitHub release instead. +module IntercomIosSdk + RELEASE_URL = 'https://github.com/intercom/intercom-ios/releases/download/%s/Intercom.xcframework.zip'.freeze + + module_function + + def install_xcframework(version:, sha256:, package_root:) + relative_path = File.join('intercom-ios-sdk', version, 'Intercom.xcframework') + framework_path = File.join(package_root, relative_path) + unless File.exist?(File.join(framework_path, 'Info.plist')) + extract(cached_zip(version, sha256), File.dirname(framework_path)) + remove_other_versions(File.join(package_root, 'intercom-ios-sdk'), version) + end + relative_path + end + + def remove_other_versions(sdk_root, version) + (Dir.children(sdk_root) - [version]).each { |entry| FileUtils.rm_rf(File.join(sdk_root, entry)) } + end + + def log(message) + defined?(Pod::UI) ? Pod::UI.puts(message) : puts(message) + end + + def cache_dir + ENV['INTERCOM_IOS_SDK_CACHE_DIR'] || File.join(Dir.home, 'Library', 'Caches', 'intercom-react-native') + end + + def cached_zip(version, sha256) + zip_path = File.join(cache_dir, version, 'Intercom.xcframework.zip') + return zip_path if File.exist?(zip_path) && Digest::SHA256.file(zip_path).hexdigest == sha256 + + url = format(RELEASE_URL, version) + download_path = "#{zip_path}.download" + FileUtils.mkdir_p(File.dirname(zip_path)) + log "[Intercom] Downloading the Intercom iOS SDK #{version} from #{url}" + unless system('curl', '--fail', '--silent', '--show-error', '--location', '--retry', '3', '--output', download_path, url) + FileUtils.rm_f(download_path) + raise "[Intercom] Could not download #{url}. Check your network, or place the zip at #{zip_path} " \ + '(set INTERCOM_IOS_SDK_CACHE_DIR to use another cache directory).' + end + + actual = Digest::SHA256.file(download_path).hexdigest + unless actual == sha256 + FileUtils.rm_f(download_path) + raise "[Intercom] Checksum mismatch for #{url}: expected #{sha256}, got #{actual}." + end + File.rename(download_path, zip_path) + zip_path + end + + def extract(zip_path, destination) + FileUtils.mkdir_p(destination) + Dir.mktmpdir('extract-', destination) do |tmp| + raise "[Intercom] Could not unzip #{zip_path}." unless system('unzip', '-q', zip_path, '-d', tmp) + + target = File.join(destination, 'Intercom.xcframework') + FileUtils.rm_rf(target) + File.rename(File.join(tmp, 'Intercom.xcframework'), target) + end + end +end