diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index bfbc7bd2e2..f7530fc5e2 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -288,7 +288,6 @@ jobs: APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} ENVIRONMENT: ${{ inputs.environment || 'development' }} - BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }} GITHUB_REF: ${{ github.ref }} FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt index 1687a7ba95..c380a0a6a5 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt @@ -315,6 +315,7 @@ interface NetworkApi { fun hasCertificate(): Boolean fun getClientPointer(): Long fun setRequestHeaders(headers: Map, serverUrls: List, token: String?) + fun getAppGroupId(): String companion object { /** The codec used by NetworkApi. */ @@ -430,6 +431,21 @@ interface NetworkApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getAppGroupId()) + } catch (exception: Throwable) { + NetworkPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt index 85b7a6c730..4479ec7701 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt @@ -13,7 +13,7 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware { private var networkApi: NetworkApiImpl? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - networkApi = NetworkApiImpl() + networkApi = NetworkApiImpl(binding.applicationContext) NetworkApi.setUp(binding.binaryMessenger, networkApi) } @@ -39,9 +39,11 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware { } } -private class NetworkApiImpl : NetworkApi { +private class NetworkApiImpl(private val context: Context) : NetworkApi { var activity: Activity? = null + override fun getAppGroupId(): String = context.packageName + override fun addCertificate(clientData: ClientCertData, callback: (Result) -> Unit) { try { HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray()) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index fbffa69ba5..e45914336b 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -718,6 +718,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; + CUSTOM_GROUP_ID = group.app.immich.share.profile; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -750,7 +751,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 240; - CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2W7AC6T8T5; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -801,6 +801,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; + CUSTOM_GROUP_ID = group.app.immich.share.debug; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -860,6 +861,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; + CUSTOM_GROUP_ID = group.app.immich.share; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -894,7 +896,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 240; - CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2W7AC6T8T5; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -924,7 +925,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 240; - CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2W7AC6T8T5; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -1080,7 +1080,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 240; - CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2W7AC6T8T5; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1124,7 +1123,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 240; - CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2W7AC6T8T5; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1165,7 +1163,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 240; - CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2W7AC6T8T5; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift index 7d9b9f14be..265923d165 100644 --- a/mobile/ios/Runner/Core/Network.g.swift +++ b/mobile/ios/Runner/Core/Network.g.swift @@ -288,6 +288,7 @@ protocol NetworkApi { func hasCertificate() throws -> Bool func getClientPointer() throws -> Int64 func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws + func getAppGroupId() throws -> String } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -388,5 +389,18 @@ class NetworkApiSetup { } else { setRequestHeadersChannel.setMessageHandler(nil) } + let getAppGroupIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getAppGroupIdChannel.setMessageHandler { _, reply in + do { + let result = try api.getAppGroupId() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAppGroupIdChannel.setMessageHandler(nil) + } } } diff --git a/mobile/ios/Runner/Core/NetworkApiImpl.swift b/mobile/ios/Runner/Core/NetworkApiImpl.swift index 82a913d837..288b8e9539 100644 --- a/mobile/ios/Runner/Core/NetworkApiImpl.swift +++ b/mobile/ios/Runner/Core/NetworkApiImpl.swift @@ -61,6 +61,10 @@ class NetworkApiImpl: NetworkApi { return Int64(Int(bitPattern: pointer)) } + func getAppGroupId() throws -> String { + return Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String + } + func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws { URLSessionManager.setServerUrls(serverUrls) diff --git a/mobile/ios/Runner/Core/URLSessionManager.swift b/mobile/ios/Runner/Core/URLSessionManager.swift index e9d65d3113..48963aa577 100644 --- a/mobile/ios/Runner/Core/URLSessionManager.swift +++ b/mobile/ios/Runner/Core/URLSessionManager.swift @@ -4,7 +4,7 @@ import native_video_player let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity" let HEADERS_KEY = "immich.request_headers" let SERVER_URLS_KEY = "immich.server_urls" -let APP_GROUP = "group.app.immich.share" +let APP_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String let COOKIE_EXPIRY_DAYS: TimeInterval = 400 enum AuthCookie: CaseIterable { diff --git a/mobile/ios/Runner/Runner.entitlements b/mobile/ios/Runner/Runner.entitlements index e5862cb213..c8a6be9cbb 100644 --- a/mobile/ios/Runner/Runner.entitlements +++ b/mobile/ios/Runner/Runner.entitlements @@ -10,7 +10,7 @@ com.apple.security.application-groups - group.app.immich.share + $(CUSTOM_GROUP_ID) diff --git a/mobile/ios/Runner/RunnerProfile.entitlements b/mobile/ios/Runner/RunnerProfile.entitlements index 6a5c086baf..93a4aab552 100644 --- a/mobile/ios/Runner/RunnerProfile.entitlements +++ b/mobile/ios/Runner/RunnerProfile.entitlements @@ -12,7 +12,7 @@ com.apple.security.application-groups - group.app.immich.share + $(CUSTOM_GROUP_ID) diff --git a/mobile/ios/ShareExtension/ShareExtension.entitlements b/mobile/ios/ShareExtension/ShareExtension.entitlements index d16dcca065..1328658832 100644 --- a/mobile/ios/ShareExtension/ShareExtension.entitlements +++ b/mobile/ios/ShareExtension/ShareExtension.entitlements @@ -4,7 +4,7 @@ com.apple.security.application-groups - group.app.immich.share + $(CUSTOM_GROUP_ID) \ No newline at end of file diff --git a/mobile/ios/WidgetExtension/ImmichAPI.swift b/mobile/ios/WidgetExtension/ImmichAPI.swift index 6ae2d502f8..c5d7971aba 100644 --- a/mobile/ios/WidgetExtension/ImmichAPI.swift +++ b/mobile/ios/WidgetExtension/ImmichAPI.swift @@ -2,7 +2,7 @@ import Foundation import SwiftUI import WidgetKit -let IMMICH_SHARE_GROUP = "group.app.immich.share" +let IMMICH_SHARE_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String enum WidgetError: Error, Codable { case noLogin diff --git a/mobile/ios/WidgetExtension/Info.plist b/mobile/ios/WidgetExtension/Info.plist index d4e598ee31..15b00baa20 100644 --- a/mobile/ios/WidgetExtension/Info.plist +++ b/mobile/ios/WidgetExtension/Info.plist @@ -2,6 +2,8 @@ + AppGroupId + $(CUSTOM_GROUP_ID) NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/mobile/ios/WidgetExtension/WidgetExtension.entitlements b/mobile/ios/WidgetExtension/WidgetExtension.entitlements index d16dcca065..1328658832 100644 --- a/mobile/ios/WidgetExtension/WidgetExtension.entitlements +++ b/mobile/ios/WidgetExtension/WidgetExtension.entitlements @@ -4,7 +4,7 @@ com.apple.security.application-groups - group.app.immich.share + $(CUSTOM_GROUP_ID) \ No newline at end of file diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index ff9fc4580f..f5f7f7887a 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -21,6 +21,7 @@ platform :ios do CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})" BASE_BUNDLE_ID = "app.alextran.immich" DEV_BUNDLE_ID = "tech.futo.immich.testflight" + DEV_GROUP_ID = "group.app.immich.share.testflight" # Helper method to get App Store Connect API key def get_api_key @@ -33,6 +34,13 @@ platform :ios do ) end + # Helper method to assemble xcargs with optional CUSTOM_GROUP_ID override + def build_xcargs(group_id: nil) + args = "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual" + args += " CUSTOM_GROUP_ID='#{group_id}'" if group_id + args + end + # Helper method to get version from pubspec.yaml def get_version_from_pubspec require 'yaml' @@ -89,7 +97,8 @@ end version_number: nil, profile_name_main:, profile_name_share:, - profile_name_widget: + profile_name_widget:, + group_id: nil ) app_identifier = base_bundle_id @@ -97,7 +106,7 @@ end if version_number increment_version_number(version_number: version_number) end - + # Increment build number increment_build_number( build_number: latest_testflight_build_number( @@ -106,14 +115,14 @@ end ) + 1, xcodeproj: "./Runner.xcodeproj" ) - + # Build the app build_app( scheme: "Runner", workspace: "Runner.xcworkspace", configuration: configuration, export_method: "app-store", - xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", + xcargs: build_xcargs(group_id: group_id), export_options: { provisioningProfiles: { "#{app_identifier}" => profile_name_main, @@ -165,7 +174,8 @@ end distribute_external: false, profile_name_main: main_profile_name, profile_name_share: share_profile_name, - profile_name_widget: widget_profile_name + profile_name_widget: widget_profile_name, + group_id: DEV_GROUP_ID ) end @@ -274,7 +284,7 @@ end configuration: "Release", export_method: "app-store", skip_package_ipa: true, - xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", + xcargs: build_xcargs(group_id: DEV_GROUP_ID), export_options: { provisioningProfiles: { DEV_BUNDLE_ID => main_profile_name, diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 1748a2a57d..409cd38431 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -30,7 +30,6 @@ const int kTimelineAssetLoadBatchSize = 1024; const int kTimelineAssetLoadOppositeSize = 64; // Widget keys -const String appShareGroupId = "group.app.immich.share"; const String kWidgetAuthToken = "widget_auth_token"; const String kWidgetServerEndpoint = "widget_server_url"; const String kWidgetCustomHeaders = "widget_custom_headers"; diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart index 7fab476694..6258060bfb 100644 --- a/mobile/lib/platform/network_api.g.dart +++ b/mobile/lib/platform/network_api.g.dart @@ -309,4 +309,23 @@ class NetworkApi { _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } + + Future getAppGroupId() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as String; + } } diff --git a/mobile/lib/repositories/widget.repository.dart b/mobile/lib/repositories/widget.repository.dart index f21d31e1ec..87209ac983 100644 --- a/mobile/lib/repositories/widget.repository.dart +++ b/mobile/lib/repositories/widget.repository.dart @@ -1,5 +1,6 @@ import 'package:home_widget/home_widget.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; final widgetRepositoryProvider = Provider((_) => const WidgetRepository()); @@ -14,7 +15,7 @@ class WidgetRepository { await HomeWidget.updateWidget(iOSName: iosName, qualifiedAndroidName: androidName); } - Future setAppGroupId(String appGroupId) async { - await HomeWidget.setAppGroupId(appGroupId); + Future setAppGroupId() async { + await HomeWidget.setAppGroupId(await networkApi.getAppGroupId()); } } diff --git a/mobile/lib/services/widget.service.dart b/mobile/lib/services/widget.service.dart index 23ec0aa770..91f65dd684 100644 --- a/mobile/lib/services/widget.service.dart +++ b/mobile/lib/services/widget.service.dart @@ -12,7 +12,7 @@ class WidgetService { const WidgetService(this._repository); Future writeCredentials(String serverURL, String sessionKey, String? customHeaders) async { - await _repository.setAppGroupId(appShareGroupId); + await _repository.setAppGroupId(); await _repository.saveData(kWidgetServerEndpoint, serverURL); await _repository.saveData(kWidgetAuthToken, sessionKey); @@ -25,7 +25,7 @@ class WidgetService { } Future clearCredentials() async { - await _repository.setAppGroupId(appShareGroupId); + await _repository.setAppGroupId(); await _repository.saveData(kWidgetServerEndpoint, ""); await _repository.saveData(kWidgetAuthToken, ""); await _repository.saveData(kWidgetCustomHeaders, ""); diff --git a/mobile/pigeon/network_api.dart b/mobile/pigeon/network_api.dart index 704efed770..a701995342 100644 --- a/mobile/pigeon/network_api.dart +++ b/mobile/pigeon/network_api.dart @@ -44,4 +44,6 @@ abstract class NetworkApi { int getClientPointer(); void setRequestHeaders(Map headers, List serverUrls, String? token); + + String getAppGroupId(); }