Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 60 additions & 10 deletions iznik-nuxt3/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,18 @@ platform :ios do
File.read('../ios/App/App.xcodeproj/project.pbxproj').include?('NotificationServiceExtension')
UI.message(nse_present ? "✅ NSE target wired into App.xcodeproj" : "ℹ️ NSE not wired — building app without the extension")

# --- Share Extension: ensure the Xcode target exists (idempotent, GATED) ---
# No-op unless ENABLE_SHARE_EXTENSION=true (see add_share_extension_target.rb).
# When the flag is off this changes nothing — the app builds + signs exactly
# as before — so it can never affect the live release until deliberately
# enabled (which also requires the App Group + profiles to exist; see header).
sh("bundle exec ruby fastlane/add_share_extension_target.rb || bundle exec ruby add_share_extension_target.rb || echo '⚠️ Share Extension wiring step did not run'")
share_ext_present = ENV['ENABLE_SHARE_EXTENSION'] == 'true' &&
File.exist?('../ios/App/ShareExtension/ShareViewController.swift') &&
File.exist?('../ios/App/App.xcodeproj/project.pbxproj') &&
File.read('../ios/App/App.xcodeproj/project.pbxproj').include?('ShareExtension')
UI.message(share_ext_present ? "✅ Share Extension target wired into App.xcodeproj" : "ℹ️ Share Extension not wired (disabled or unavailable)")

# Read pre-calculated version from workspace file (same as Android)
# Version was already incremented by the increment-version job
version_file = '../.new_version'
Expand Down Expand Up @@ -554,6 +566,35 @@ platform :ios do
UI.success("✅ NSE signing configured (profile: #{nse_profile_name})")
end

# --- Share Extension signing (only when the target is wired; GATED) ---
# Mirrors the NSE: the App ID + App Store distribution profile for the Share
# Extension are pre-created ONCE by hand in the Apple Developer portal (with
# the App Group capability). We download-only (readonly) here — produce()
# needs interactive 2FA and would hang CI. No-op unless share_ext_present.
share_ext_profile_name = nil
if share_ext_present
share_ext_bundle = "org.ilovefreegle.iphone.ShareExtension"
get_provisioning_profile(app_identifier: share_ext_bundle, adhoc: false, readonly: true)
share_ext_profile_name = lane_context[SharedValues::SIGH_NAME]
share_ext_profile_path = lane_context[SharedValues::SIGH_PROFILE_PATH] ||
(lane_context[SharedValues::SIGH_PROFILE_PATHS] || []).first

update_project_provisioning(
xcodeproj: "ios/App/App.xcodeproj",
target_filter: "ShareExtension",
profile: share_ext_profile_path,
code_signing_identity: "iPhone Distribution"
)
update_code_signing_settings(
use_automatic_signing: false,
path: "ios/App/App.xcodeproj",
targets: ["ShareExtension"],
code_sign_identity: "iPhone Distribution",
bundle_identifier: share_ext_bundle
)
UI.success("✅ Share Extension signing configured (profile: #{share_ext_profile_name})")
end

# The Freegle profile is installed from a file; read its Name for the export map.
freegle_profile_file = ENV['IOS_PROVISIONING_PROFILE_PATH'] || 'ios_appstore.mobileprovision'
freegle_profile_file = "../#{freegle_profile_file}" unless freegle_profile_file.start_with?('/')
Expand All @@ -564,9 +605,21 @@ platform :ios do
freegle_profile_name = m && m[1]
end

# Two-target manual export needs an explicit provisioningProfiles map; only enable it
# when everything resolved, otherwise fall back to today's exact single-target build.
do_nse_export = nse_present && nse_profile_name && freegle_profile_name
# Manual multi-target export needs an explicit provisioningProfiles map; only
# enable it when at least one extension is wired AND everything resolved,
# otherwise fall back to today's exact single-target build. When the Share
# Extension is disabled this composes to exactly the previous app(+NSE) map.
manual_profiles = {}
manual_profiles["org.ilovefreegle.iphone"] = freegle_profile_name if freegle_profile_name
if nse_present && nse_profile_name
manual_profiles["org.ilovefreegle.iphone.NotificationServiceExtension"] = nse_profile_name
end
if share_ext_present && share_ext_profile_name
manual_profiles["org.ilovefreegle.iphone.ShareExtension"] = share_ext_profile_name
end
needs_manual_export = freegle_profile_name &&
((nse_present && nse_profile_name) ||
(share_ext_present && share_ext_profile_name))

gym_opts = {
scheme: "Freegle",
Expand All @@ -576,18 +629,15 @@ platform :ios do
output_directory: "./build",
skip_package_dependencies_resolution: true
}
if do_nse_export
if needs_manual_export
gym_opts[:export_options] = {
method: "app-store",
signingStyle: "manual",
provisioningProfiles: {
"org.ilovefreegle.iphone" => freegle_profile_name,
"org.ilovefreegle.iphone.NotificationServiceExtension" => nse_profile_name
}
provisioningProfiles: manual_profiles
}
UI.success("📦 Exporting app + NSE (profiles: #{freegle_profile_name} / #{nse_profile_name})")
UI.success("📦 Exporting app + extensions (#{manual_profiles.keys.join(', ')})")
else
UI.message("📦 Exporting app only (NSE export not enabled this build)")
UI.message("📦 Exporting app only (no extensions enabled this build)")
end

# Build the app
Expand Down
128 changes: 128 additions & 0 deletions iznik-nuxt3/fastlane/add_share_extension_target.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Adds the "Share into Freegle" Share Extension target to ios/App/App.xcodeproj so
# it builds + signs on CircleCI like any other target — no manual Xcode step.
#
# GATED: does nothing unless ENABLE_SHARE_EXTENSION=true. By default it is a no-op,
# so the main app's entitlements + signing are byte-for-byte unaffected and the
# production build is unchanged. Enable ONLY AFTER:
# 1. Registering the App Group `group.org.ilovefreegle.iphone` in the Apple
# Developer portal and enabling it on BOTH App IDs
# (org.ilovefreegle.iphone and org.ilovefreegle.iphone.ShareExtension).
# 2. Regenerating the app + extension App Store provisioning profiles so they
# include that App Group — otherwise signing/export fails.
#
# Idempotent: safe to run on every CI build. Unlike the NSE, the source is
# committed (ios/App/ShareExtension/), so there is nothing to copy in.
#
# Requires the `xcodeproj` gem (a fastlane dependency; run under `bundle exec`).

require 'fileutils'
require 'xcodeproj'

unless ENV['ENABLE_SHARE_EXTENSION'] == 'true'
puts 'add_share_extension_target: ENABLE_SHARE_EXTENSION not set — skipping (no-op).'
exit 0
end

EXT_NAME = 'ShareExtension'
APP_TARGET = 'Freegle'
APP_BUNDLE_ID = 'org.ilovefreegle.iphone'
EXT_BUNDLE_ID = "#{APP_BUNDLE_ID}.#{EXT_NAME}"
APP_GROUP = 'group.org.ilovefreegle.iphone'
DEPLOY_TARGET = '14.0'

root = File.expand_path('..', __dir__) # fastlane/ -> project root
proj_path = File.join(root, 'ios/App/App.xcodeproj')
ext_dir = File.join(root, 'ios/App', EXT_NAME)
app_entitle = File.join(root, 'ios/App/App/App.entitlements')

unless File.directory?(File.dirname(proj_path))
warn "add_share_extension_target: #{proj_path} not found — has cap sync run? Skipping."
exit 0
end
unless File.exist?(File.join(ext_dir, 'ShareViewController.swift'))
warn "add_share_extension_target: source not found in #{ext_dir} — skipping."
exit 0
end

project = Xcodeproj::Project.open(proj_path)
app_target = project.targets.find { |t| t.name == APP_TARGET }
unless app_target
warn "add_share_extension_target: main target '#{APP_TARGET}' not found — skipping."
exit 0
end

def apply_ext_settings(config)
s = config.build_settings
s['PRODUCT_BUNDLE_IDENTIFIER'] = EXT_BUNDLE_ID
s['PRODUCT_NAME'] = '$(TARGET_NAME)'
s['INFOPLIST_FILE'] = "#{EXT_NAME}/Info.plist"
s['CODE_SIGN_ENTITLEMENTS'] = "#{EXT_NAME}/ShareExtension.entitlements"
s['SWIFT_VERSION'] = '5.0'
s['IPHONEOS_DEPLOYMENT_TARGET'] = DEPLOY_TARGET
s['TARGETED_DEVICE_FAMILY'] = '1,2'
s['CODE_SIGN_STYLE'] = 'Manual'
s['SKIP_INSTALL'] = 'NO'
s['MARKETING_VERSION'] ||= '1.0'
s['CURRENT_PROJECT_VERSION'] ||= '1'
end

ext_target = project.targets.find { |t| t.name == EXT_NAME }
if ext_target
puts 'add_share_extension_target: target already exists — refreshing settings + file refs'
ext_target.build_configurations.each { |c| apply_ext_settings(c) }
else
puts "add_share_extension_target: creating '#{EXT_NAME}' app-extension target"
ext_target = project.new_target(:app_extension, EXT_NAME, :ios, DEPLOY_TARGET)
ext_target.build_configurations.each { |c| apply_ext_settings(c) }

# Build the app target after the extension, and embed the .appex in the app.
app_target.add_dependency(ext_target)

embed = app_target.copy_files_build_phases.find { |p| p.symbol_dst_subfolder_spec == :plug_ins }
if embed.nil?
embed = app_target.new_copy_files_build_phase('Embed App Extensions')
embed.symbol_dst_subfolder_spec = :plug_ins
end
build_file = embed.add_file_reference(ext_target.product_reference)
build_file.settings = { 'ATTRIBUTES' => ['RemoveHeadersOnCopy'] }
end

# Group + file references, relative to SOURCE_ROOT (the .xcodeproj dir = ios/App).
group = project.main_group.find_subpath(EXT_NAME, true)
group.set_source_tree('SOURCE_ROOT')
group.set_path(EXT_NAME)

swift_name = 'ShareViewController.swift'
swift_ref = group.files.find { |f| f.display_name == swift_name } ||
group.new_reference(swift_name)
unless ext_target.source_build_phase.files_references.include?(swift_ref)
ext_target.add_file_references([swift_ref])
end
['Info.plist', 'ShareExtension.entitlements'].each do |fname|
group.new_reference(fname) unless group.files.any? { |f| f.display_name == fname }
end

# The main app must join the same App Group so it can read what the extension
# wrote. Patch App.entitlements at BUILD TIME only (never committed), so the
# default build is untouched. The app's provisioning profile must already
# include this App Group (regenerated by hand — see header).
if File.exist?(app_entitle)
xml = File.read(app_entitle)
unless xml.include?(APP_GROUP)
inject = <<-PLIST
<key>com.apple.security.application-groups</key>
<array>
<string>#{APP_GROUP}</string>
</array>
PLIST
xml = xml.sub('</dict>', "#{inject}</dict>")
File.write(app_entitle, xml)
puts "add_share_extension_target: added #{APP_GROUP} to main app entitlements (build-time)"
end
end

project.save
puts "add_share_extension_target: done (#{EXT_BUNDLE_ID})"
8 changes: 8 additions & 0 deletions iznik-nuxt3/ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
<string>fb134980666550322</string>
</array>
</dict>
<dict>
<key>CFBundleURLName</key>
<string>org.ilovefreegle.iphone.share</string>
<key>CFBundleURLSchemes</key>
<array>
<string>freegleshare</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
Expand Down
39 changes: 39 additions & 0 deletions iznik-nuxt3/ios/App/ShareExtension/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Freegle</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
</dict>
</plist>
10 changes: 10 additions & 0 deletions iznik-nuxt3/ios/App/ShareExtension/ShareExtension.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.ilovefreegle.iphone</string>
</array>
</dict>
</plist>
Loading