Kaynağa Gözat

Merge branch 'main' into mc/sending

Morgan Chen 1 yıl önce
ebeveyn
işleme
080b98fa39
100 değiştirilmiş dosya ile 1423 ekleme ve 705 silme
  1. 3 5
      .github/workflows/client_app.yml
  2. 1 1
      .github/workflows/firestore-nightly.yml
  3. 5 1
      .github/workflows/firestore.yml
  4. 3 0
      .github/workflows/prerelease.yml
  5. 3 0
      .github/workflows/release.yml
  6. 4 0
      Crashlytics/CHANGELOG.md
  7. 1 1
      Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.m
  8. 11 11
      Crashlytics/Crashlytics/Helpers/FIRCLSAllocate.c
  9. 6 6
      Crashlytics/Crashlytics/Helpers/FIRCLSFile.m
  10. 2 2
      Crashlytics/Crashlytics/Helpers/FIRCLSUtility.m
  11. 19 4
      Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.m
  12. 1 0
      Crashlytics/CrashlyticsInputFiles.xcfilelist
  13. 1 1
      Crashlytics/Shared/FIRCLSByteUtility.m
  14. 1 1
      Crashlytics/Shared/FIRCLSMachO/FIRCLSMachOBinary.m
  15. 1 1
      Crashlytics/UnitTests/FIRCLSCompactUnwindTests.m
  16. 1 1
      Crashlytics/UnitTests/FIRCLSDwarfTests.m
  17. 2 2
      Crashlytics/UnitTests/FIRCLSFileTests.m
  18. 1 1
      Crashlytics/UnitTests/FIRCLSLoggingTests.m
  19. BIN
      Crashlytics/upload-symbols
  20. 24 24
      Firebase.podspec
  21. 2 2
      FirebaseABTesting.podspec
  22. 5 5
      FirebaseAnalytics.podspec
  23. 2 2
      FirebaseAnalyticsOnDeviceConversion.podspec
  24. 2 2
      FirebaseAppCheck.podspec
  25. 1 1
      FirebaseAppCheckInterop.podspec
  26. 2 2
      FirebaseAppDistribution.podspec
  27. 3 3
      FirebaseAuth.podspec
  28. 11 1
      FirebaseAuth/CHANGELOG.md
  29. 10 0
      FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift
  30. 11 1
      FirebaseAuth/Sources/Swift/Auth/Auth.swift
  31. 15 0
      FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift
  32. 14 4
      FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
  33. 10 0
      FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift
  34. 77 83
      FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift
  35. 1 0
      FirebaseAuth/Sources/Swift/SystemService/AuthNotificationManager.swift
  36. 12 3
      FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift
  37. 1 1
      FirebaseAuth/Sources/Swift/User/User.swift
  38. 55 14
      FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift
  39. 11 1
      FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift
  40. 9 11
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample.xcodeproj/project.pbxproj
  41. 4 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppDelegate.swift
  42. 3 3
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Contents.json
  43. 0 21
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/Contents.json
  44. BIN
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/logo-1024px.png
  45. 142 140
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/CustomViews/LoginView.swift
  46. 6 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift
  47. 1 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift
  48. 4 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift
  49. 71 131
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift
  50. 0 139
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/LoginController.swift
  51. 5 1
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/PasswordlessViewController.swift
  52. 83 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift
  53. 45 0
      FirebaseAuth/Tests/Unit/AuthBackendTests.swift
  54. 3 0
      FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift
  55. 2 0
      FirebaseAuth/Tests/Unit/ObjCAPITests.m
  56. 2 0
      FirebaseAuth/Tests/Unit/RPCBaseTests.swift
  57. 5 1
      FirebaseAuth/Tests/Unit/SwiftAPI.swift
  58. 1 1
      FirebaseAuthInterop.podspec
  59. 2 2
      FirebaseCombineSwift.podspec
  60. 2 2
      FirebaseCore.podspec
  61. 2 2
      FirebaseCoreExtension.podspec
  62. 1 1
      FirebaseCoreInternal.podspec
  63. 2 2
      FirebaseCrashlytics.podspec
  64. 2 2
      FirebaseDatabase.podspec
  65. 5 2
      FirebaseDynamicLinks.podspec
  66. 4 1
      FirebaseDynamicLinks/CHANGELOG.md
  67. 4 4
      FirebaseFirestore.podspec
  68. 2 2
      FirebaseFirestoreInternal.podspec
  69. 3 3
      FirebaseFunctions.podspec
  70. 50 15
      FirebaseFunctions/Backend/index.js
  71. 5 2
      FirebaseFunctions/Backend/package.json
  72. 2 0
      FirebaseFunctions/Backend/start.sh
  73. 2 2
      FirebaseInAppMessaging.podspec
  74. 2 2
      FirebaseInstallations.podspec
  75. 3 3
      FirebaseMLModelDownloader.podspec
  76. 2 2
      FirebaseMessaging.podspec
  77. 1 1
      FirebaseMessaging/Apps/README.md
  78. 5 5
      FirebaseMessaging/Apps/Shared/LiveActivityView.swift
  79. 3 0
      FirebaseMessaging/CHANGELOG.md
  80. 1 1
      FirebaseMessaging/Sources/Token/FIRMessagingTokenDeleteOperation.m
  81. 1 1
      FirebaseMessaging/Sources/Token/FIRMessagingTokenFetchOperation.m
  82. 1 1
      FirebaseMessagingInterop.podspec
  83. 2 2
      FirebasePerformance.podspec
  84. 2 2
      FirebaseRemoteConfig.podspec
  85. 9 1
      FirebaseRemoteConfig/CHANGELOG.md
  86. 106 0
      FirebaseRemoteConfig/Sources/FIRRemoteConfig.m
  87. 5 0
      FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h
  88. 106 0
      FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h
  89. 27 0
      FirebaseRemoteConfig/Sources/RCNConfigSettings.m
  90. 2 0
      FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h
  91. 16 0
      FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m
  92. 107 0
      FirebaseRemoteConfig/Swift/CustomSignals.swift
  93. 1 0
      FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift
  94. 50 0
      FirebaseRemoteConfig/Tests/Swift/SwiftAPI/AsyncAwaitTests.swift
  95. 14 0
      FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift
  96. 114 0
      FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m
  97. 27 0
      FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m
  98. 2 2
      FirebaseRemoteConfigInterop.podspec
  99. 3 3
      FirebaseSessions.podspec
  100. 2 2
      FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.h

+ 3 - 5
.github/workflows/client_app.yml

@@ -30,9 +30,7 @@ jobs:
       matrix:
         #TODO(ncooke3): Add multi-platform support: tvOS, macOS, catalyst
         platform: [iOS]
-        scheme: [ClientApp-iOS13]
-        # TODO(ncooke3): Re-enable after updating Firestore binary.
-        #scheme: [ClientApp, ClientApp-iOS13]
+        scheme: [ClientApp]
     steps:
       - uses: actions/checkout@v4
       - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126
@@ -53,7 +51,7 @@ jobs:
         matrix:
           #TODO(ncooke3): Add multi-platform support: tvOS, macOS, catalyst
           platform: [iOS]
-          scheme: [ClientApp, ClientApp-iOS13]
+          scheme: [ClientApp]
       steps:
         - uses: actions/checkout@v4
         - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126
@@ -70,7 +68,7 @@ jobs:
     runs-on: macos-14
     strategy:
       matrix:
-        scheme: [ClientApp-CocoaPods, ClientApp-CocoaPods-iOS13]
+        scheme: [ClientApp-CocoaPods]
     steps:
       - uses: actions/checkout@v4
       - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126

+ 1 - 1
.github/workflows/firestore-nightly.yml

@@ -71,7 +71,7 @@ jobs:
 
     - uses: actions/setup-python@v5
       with:
-        python-version: '3.7'
+        python-version: '3.11'
 
     - name: Install Secret GoogleService-Info.plist
       run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/firestore-nightly.plist.gpg \

+ 5 - 1
.github/workflows/firestore.yml

@@ -15,6 +15,7 @@
 name: firestore
 
 on:
+  workflow_dispatch:
   pull_request:
   schedule:
     # Run every day at 12am (PST) - cron uses UTC times
@@ -57,6 +58,9 @@ jobs:
               - 'FirebaseFirestoreInternal.podspec'
               - 'FirebaseFirestore.podspec'
 
+              # Package.swift
+              - 'Package.swift'
+
               # CMake
               - '**CMakeLists.txt'
               - 'cmake/**'
@@ -311,7 +315,7 @@ jobs:
 
     - uses: actions/setup-python@v5
       with:
-        python-version: '3.7'
+        python-version: '3.11'
 
     - name: Setup build
       run:  scripts/install_prereqs.sh Firestore ${{ runner.os }} cmake

+ 3 - 0
.github/workflows/prerelease.yml

@@ -9,6 +9,9 @@ on:
     # Run every day at 9pm (PST) - cron uses UTC times
     - cron:  '0 5 * * *'
 
+env:
+  FIREBASE_CI: true
+
 concurrency:
     group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
     cancel-in-progress: true

+ 3 - 0
.github/workflows/release.yml

@@ -11,6 +11,9 @@ on:
     # Run every day at 9pm (PST) - cron uses UTC times
     - cron:  '0 5 * * *'
 
+env:
+  FIREBASE_CI: true
+
 concurrency:
     group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
     cancel-in-progress: true

+ 4 - 0
Crashlytics/CHANGELOG.md

@@ -1,3 +1,7 @@
+# 11.7.0
+- [fixed] Updated `upload-symbols` to version 3.20, wait for `debug.dylib` DWARF content getting generated when build with `--build-phase` option. Added `debug.dylib` DWARF content to run script input file list for user who enabled user script sandboxing (#14054).
+- [fixed] Updated all memory allocation from `malloc()` to `calloc()` (#14209).
+
 # 11.5.0
 - [changed] Updated `upload-symbols` to version 3.19, removed all methods require CFRelease and switch to modern classes (#13420).
 

+ 1 - 1
Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.m

@@ -409,7 +409,7 @@ static void FIRCLSBinaryImageChanged(bool added,
   // fill imageDetails fields using slice & vmaddr_slide
   FIRCLSBinaryImageFillInImageDetails(&imageDetails);
 
-  FIRCLSImageChange* change = malloc(sizeof(FIRCLSImageChange));
+  FIRCLSImageChange* change = calloc(1, sizeof(FIRCLSImageChange));
   if (!change) return;
   change->added = added;
   change->details = imageDetails;

+ 11 - 11
Crashlytics/Crashlytics/Helpers/FIRCLSAllocate.c

@@ -56,7 +56,7 @@ FIRCLSAllocatorRef FIRCLSAllocatorCreate(size_t writableSpace, size_t readableSp
   }
 
   // Make one big, continuous allocation, adding additional pages for our guards.  Note
-  // that we cannot use malloc (or valloc) in this case, because we need to assert full
+  // that we cannot use malloc, calloc (or valloc) in this case, because we need to assert full
   // ownership over these allocations.  mmap is a much better choice.  We also mark these
   // pages as MAP_NOCACHE.
   allocationSize = writableRegion.size + readableRegion.size + pageSize * 3;
@@ -174,10 +174,10 @@ void* FIRCLSAllocatorSafeAllocateFromRegion(FIRCLSAllocationRegion* region, size
 
     // this shouldn't happen unless we make a mistake with our size pre-computations
     if ((uintptr_t)originalCursor - (uintptr_t)region->start + size > region->size) {
-      FIRCLSSDKLog("Unable to allocate sufficient memory, falling back to malloc\n");
-      void* ptr = malloc(size);
+      FIRCLSSDKLog("Unable to allocate sufficient memory, falling back to calloc\n");
+      void* ptr = calloc(1, size);
       if (!ptr) {
-        FIRCLSSDKLog("Unable to malloc in FIRCLSAllocatorSafeAllocateFromRegion\n");
+        FIRCLSSDKLog("Unable to calloc in FIRCLSAllocatorSafeAllocateFromRegion\n");
         return NULL;
       }
       return ptr;
@@ -195,21 +195,21 @@ void* FIRCLSAllocatorSafeAllocate(FIRCLSAllocatorRef allocator,
   FIRCLSAllocationRegion* region;
 
   if (!allocator) {
-    // fall back to malloc in this case
-    FIRCLSSDKLog("Allocator invalid, falling back to malloc\n");
-    void* ptr = malloc(size);
+    // fall back to calloc in this case
+    FIRCLSSDKLog("Allocator invalid, falling back to calloc\n");
+    void* ptr = calloc(1, size);
     if (!ptr) {
-      FIRCLSSDKLog("Unable to malloc in FIRCLSAllocatorSafeAllocate\n");
+      FIRCLSSDKLog("Unable to calloc in FIRCLSAllocatorSafeAllocate\n");
       return NULL;
     }
     return ptr;
   }
 
   if (allocator->protectionEnabled) {
-    FIRCLSSDKLog("Allocator already protected, falling back to malloc\n");
-    void* ptr = malloc(size);
+    FIRCLSSDKLog("Allocator already protected, falling back to calloc\n");
+    void* ptr = calloc(1, size);
     if (!ptr) {
-      FIRCLSSDKLog("Unable to malloc in FIRCLSAllocatorSafeAllocate\n");
+      FIRCLSSDKLog("Unable to calloc in FIRCLSAllocatorSafeAllocate\n");
       return NULL;
     }
     return ptr;

+ 6 - 6
Crashlytics/Crashlytics/Helpers/FIRCLSFile.m

@@ -74,9 +74,9 @@ static bool FIRCLSFileInit(
 
   file->bufferWrites = bufferWrites;
   if (bufferWrites) {
-    file->writeBuffer = malloc(FIRCLSWriteBufferLength * sizeof(char));
+    file->writeBuffer = calloc(1, FIRCLSWriteBufferLength * sizeof(char));
     if (!file->writeBuffer) {
-      FIRCLSErrorLog(@"Unable to malloc in FIRCLSFileInit");
+      FIRCLSErrorLog(@"Unable to calloc in FIRCLSFileInit");
       return false;
     }
 
@@ -668,10 +668,10 @@ NSArray* FIRCLSFileReadSections(const char* path,
 
 NSString* FIRCLSFileHexEncodeString(const char* string) {
   size_t length = strlen(string);
-  char* encodedBuffer = malloc(length * 2 + 1);
+  char* encodedBuffer = calloc(1, length * 2 + 1);
 
   if (!encodedBuffer) {
-    FIRCLSErrorLog(@"Unable to malloc in FIRCLSFileHexEncodeString");
+    FIRCLSErrorLog(@"Unable to calloc in FIRCLSFileHexEncodeString");
     return nil;
   }
 
@@ -693,9 +693,9 @@ NSString* FIRCLSFileHexEncodeString(const char* string) {
 
 NSString* FIRCLSFileHexDecodeString(const char* string) {
   size_t length = strlen(string);
-  char* decodedBuffer = malloc(length);  // too long, but safe
+  char* decodedBuffer = calloc(1, length);  // too long, but safe
   if (!decodedBuffer) {
-    FIRCLSErrorLog(@"Unable to malloc in FIRCLSFileHexDecodeString");
+    FIRCLSErrorLog(@"Unable to calloc in FIRCLSFileHexDecodeString");
     return nil;
   }
 

+ 2 - 2
Crashlytics/Crashlytics/Helpers/FIRCLSUtility.m

@@ -185,10 +185,10 @@ NSString* FIRCLSNSDataToNSString(NSData* data) {
   // null terminator
   length = [data length];
   size = (length * 2) + 1;
-  buffer = malloc(sizeof(char) * size);
+  buffer = calloc(1, sizeof(char) * size);
 
   if (!buffer) {
-    FIRCLSErrorLog(@"Unable to malloc in FIRCLSNSDataToNSString");
+    FIRCLSErrorLog(@"Unable to calloc in FIRCLSNSDataToNSString");
     return nil;
   }
 

+ 19 - 4
Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.m

@@ -171,7 +171,7 @@
 
   NSArray<NSString *> *clsRecords = [self clsRecordFilePaths];
   google_crashlytics_FilesPayload_File *files =
-      malloc(sizeof(google_crashlytics_FilesPayload_File) * clsRecords.count);
+      calloc(1, sizeof(google_crashlytics_FilesPayload_File) * clsRecords.count);
 
   if (files == NULL) {
     // files and files_count are initialized to NULL and 0 by default.
@@ -236,7 +236,7 @@
   }
 }
 
-/** Mallocs a pb_bytes_array and copies the given NSString's bytes into the bytes array.
+/** Callocs a pb_bytes_array and copies the given NSString's bytes into the bytes array.
  * @note Memory needs to be freed manually, through pb_free or pb_release.
  * @param string The string to encode as pb_bytes.
  */
@@ -251,12 +251,27 @@ pb_bytes_array_t *FIRCLSEncodeString(NSString *string) {
   return FIRCLSEncodeData(stringBytes);
 }
 
-/** Mallocs a pb_bytes_array and copies the given NSData bytes into the bytes array.
+/** Callocs a pb_bytes_array and copies the given NSData bytes into the bytes array.
  * @note Memory needs to be free manually, through pb_free or pb_release.
  * @param data The data to copy into the new bytes array.
  */
 pb_bytes_array_t *FIRCLSEncodeData(NSData *data) {
-  pb_bytes_array_t *pbBytes = malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(data.length));
+  // We have received couple security tickets before for using calloc here.
+  // Here is a short explaination on how it is calculated so buffer overflow is prevented:
+  // We will alloc an amount of memeory for struct `pb_bytes_array_t`, this struct contains two
+  // attributes:
+  //    pb_size_t size
+  //    pb_byte_t bytes[1]
+  // It contains the size the of the data and the actually data information in byte form (which
+  // is represented by a pointer), for more information check the declaration in nanopb/pb.h.
+
+  // For size, NSData return size in `unsigned long` type which is the same size as `pb_size_t` and
+  // it is declared in compile time depending on the arch of system. If overflow happened it should
+  // happend at NSData level first when user trying to inserting data to NSData.
+  // For bytes, it is just a strict memeory copy of the data in NSData.
+  // The whole structure will be freed as a part of process for deallocing report in dealloc() of
+  // this class
+  pb_bytes_array_t *pbBytes = calloc(1, PB_BYTES_ARRAY_T_ALLOCSIZE(data.length));
   if (pbBytes == NULL) {
     return NULL;
   }

+ 1 - 0
Crashlytics/CrashlyticsInputFiles.xcfilelist

@@ -3,3 +3,4 @@ $(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)
 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
 ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
+${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib

+ 1 - 1
Crashlytics/Shared/FIRCLSByteUtility.m

@@ -58,7 +58,7 @@ NSString *FIRCLSNSDataPrettyDescription(NSData *data) {
   // null terminator
   length = data.length;
   size = (length * 2) + 1;
-  buffer = malloc(sizeof(char) * size);
+  buffer = calloc(1, sizeof(char) * size);
 
   if (!buffer) {
     return nil;

+ 1 - 1
Crashlytics/Shared/FIRCLSMachO/FIRCLSMachOBinary.m

@@ -144,7 +144,7 @@ static NSString* FIRCLSNSDataToNSString(NSData* data) {
   // null terminator
   length = [data length];
   size = (length * 2) + 1;
-  buffer = malloc(sizeof(char) * size);
+  buffer = calloc(1, sizeof(char) * size);
 
   if (!buffer) {
     return nil;

+ 1 - 1
Crashlytics/UnitTests/FIRCLSCompactUnwindTests.m

@@ -33,7 +33,7 @@
 - (void)setUp {
   [super setUp];
 
-  _firclsContext.readonly = malloc(sizeof(FIRCLSReadOnlyContext));
+  _firclsContext.readonly = calloc(1, sizeof(FIRCLSReadOnlyContext));
   _firclsContext.readonly->logPath = "/tmp/test.log";
 }
 

+ 1 - 1
Crashlytics/UnitTests/FIRCLSDwarfTests.m

@@ -33,7 +33,7 @@
 - (void)setUp {
   [super setUp];
 
-  _firclsContext.readonly = malloc(sizeof(FIRCLSReadOnlyContext));
+  _firclsContext.readonly = calloc(1, sizeof(FIRCLSReadOnlyContext));
   _firclsContext.readonly->logPath = "/tmp/test.log";
 }
 

+ 2 - 2
Crashlytics/UnitTests/FIRCLSFileTests.m

@@ -217,7 +217,7 @@
                              filePath:(NSString *)filePath
                                length:(size_t)length
                              buffered:(BOOL)buffered {
-  char *longString = malloc(length * sizeof(char));
+  char *longString = calloc(1, length * sizeof(char));
 
   memset(longString, 'a', length);  // fill it with 'a' characters
   longString[length - 1] = 0;       // null terminate
@@ -432,7 +432,7 @@
 
 - (void)testLoggingInputLongerThanBuffer {
   size_t inputLength = (FIRCLSWriteBufferLength + 2) * sizeof(char);
-  char *input = malloc(inputLength);
+  char *input = calloc(1, inputLength);
   for (size_t i = 0; i < inputLength - 1; i++) {
     input[i] = i % 26 + 'a';
   }

+ 1 - 1
Crashlytics/UnitTests/FIRCLSLoggingTests.m

@@ -302,7 +302,7 @@
 
 - (void)testLargeLogLine {
   size_t strLength = 100 * 1024;  // Attempt to write 100k of data
-  char* longLine = malloc(strLength + 1);
+  char* longLine = calloc(1, strLength + 1);
   memset(longLine, 'a', strLength);
   longLine[strLength] = '\0';
   NSString* longStr = [[NSString alloc] initWithBytesNoCopy:longLine

BIN
Crashlytics/upload-symbols


+ 24 - 24
Firebase.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'Firebase'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase'
 
   s.description      = <<-DESC
@@ -36,14 +36,14 @@ Simplify your app development, grow your user base, and monetize more effectivel
     ss.ios.deployment_target = '12.0'
     ss.osx.deployment_target = '10.15'
     ss.tvos.deployment_target = '13.0'
-    ss.ios.dependency 'FirebaseAnalytics', '~> 11.7.0'
-    ss.osx.dependency 'FirebaseAnalytics', '~> 11.7.0'
-    ss.tvos.dependency 'FirebaseAnalytics', '~> 11.7.0'
+    ss.ios.dependency 'FirebaseAnalytics', '~> 11.8.0'
+    ss.osx.dependency 'FirebaseAnalytics', '~> 11.8.0'
+    ss.tvos.dependency 'FirebaseAnalytics', '~> 11.8.0'
     ss.dependency 'Firebase/CoreOnly'
   end
 
   s.subspec 'CoreOnly' do |ss|
-    ss.dependency 'FirebaseCore', '~> 11.7.0'
+    ss.dependency 'FirebaseCore', '~> 11.8.0'
     ss.source_files = 'CoreOnly/Sources/Firebase.h'
     ss.preserve_paths = 'CoreOnly/Sources/module.modulemap'
     if ENV['FIREBASE_POD_REPO_FOR_DEV_POD'] then
@@ -79,13 +79,13 @@ Simplify your app development, grow your user base, and monetize more effectivel
     ss.ios.deployment_target = '12.0'
     ss.osx.deployment_target = '10.15'
     ss.tvos.deployment_target = '13.0'
-    ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 11.7.0'
+    ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 11.8.0'
     ss.dependency 'Firebase/CoreOnly'
   end
 
   s.subspec 'ABTesting' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseABTesting', '~> 11.7.0'
+    ss.dependency 'FirebaseABTesting', '~> 11.8.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
@@ -95,13 +95,13 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'AppDistribution' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebaseAppDistribution', '~> 11.7.0-beta'
+    ss.ios.dependency 'FirebaseAppDistribution', '~> 11.8.0-beta'
     ss.ios.deployment_target = '13.0'
   end
 
   s.subspec 'AppCheck' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseAppCheck', '~> 11.7.0'
+    ss.dependency 'FirebaseAppCheck', '~> 11.8.0'
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
     ss.tvos.deployment_target = '13.0'
@@ -110,7 +110,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Auth' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseAuth', '~> 11.7.0'
+    ss.dependency 'FirebaseAuth', '~> 11.8.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
@@ -120,7 +120,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Crashlytics' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseCrashlytics', '~> 11.7.0'
+    ss.dependency 'FirebaseCrashlytics', '~> 11.8.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '12.0'
     ss.osx.deployment_target = '10.15'
@@ -130,7 +130,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Database' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseDatabase', '~> 11.7.0'
+    ss.dependency 'FirebaseDatabase', '~> 11.8.0'
     # Standard platforms PLUS watchOS 7.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
@@ -140,13 +140,13 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'DynamicLinks' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebaseDynamicLinks', '~> 11.7.0'
+    ss.ios.dependency 'FirebaseDynamicLinks', '~> 11.8.0'
     ss.ios.deployment_target = '13.0'
   end
 
   s.subspec 'Firestore' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseFirestore', '~> 11.7.0'
+    ss.dependency 'FirebaseFirestore', '~> 11.8.0'
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
     ss.tvos.deployment_target = '13.0'
@@ -154,7 +154,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Functions' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseFunctions', '~> 11.7.0'
+    ss.dependency 'FirebaseFunctions', '~> 11.8.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
@@ -164,20 +164,20 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'InAppMessaging' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebaseInAppMessaging', '~> 11.7.0-beta'
-    ss.tvos.dependency 'FirebaseInAppMessaging', '~> 11.7.0-beta'
+    ss.ios.dependency 'FirebaseInAppMessaging', '~> 11.8.0-beta'
+    ss.tvos.dependency 'FirebaseInAppMessaging', '~> 11.8.0-beta'
     ss.ios.deployment_target = '13.0'
     ss.tvos.deployment_target = '13.0'
   end
 
   s.subspec 'Installations' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseInstallations', '~> 11.7.0'
+    ss.dependency 'FirebaseInstallations', '~> 11.8.0'
   end
 
   s.subspec 'Messaging' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseMessaging', '~> 11.7.0'
+    ss.dependency 'FirebaseMessaging', '~> 11.8.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
@@ -187,7 +187,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'MLModelDownloader' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseMLModelDownloader', '~> 11.7.0-beta'
+    ss.dependency 'FirebaseMLModelDownloader', '~> 11.8.0-beta'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
@@ -197,15 +197,15 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Performance' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebasePerformance', '~> 11.7.0'
-    ss.tvos.dependency 'FirebasePerformance', '~> 11.7.0'
+    ss.ios.dependency 'FirebasePerformance', '~> 11.8.0'
+    ss.tvos.dependency 'FirebasePerformance', '~> 11.8.0'
     ss.ios.deployment_target = '13.0'
     ss.tvos.deployment_target = '13.0'
   end
 
   s.subspec 'RemoteConfig' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseRemoteConfig', '~> 11.7.0'
+    ss.dependency 'FirebaseRemoteConfig', '~> 11.8.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'
@@ -215,7 +215,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Storage' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseStorage', '~> 11.7.0'
+    ss.dependency 'FirebaseStorage', '~> 11.8.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '13.0'
     ss.osx.deployment_target = '10.15'

+ 2 - 2
FirebaseABTesting.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseABTesting'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase ABTesting'
 
   s.description      = <<-DESC
@@ -52,7 +52,7 @@ Firebase Cloud Messaging and Firebase Remote Config in your app.
     'GCC_C_LANGUAGE_STANDARD' => 'c99',
     'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"'
   }
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
 
   s.test_spec 'unit' do |unit_tests|
     unit_tests.scheme = { :code_coverage => true }

+ 5 - 5
FirebaseAnalytics.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
     s.name             = 'FirebaseAnalytics'
-    s.version          = '11.7.0'
+    s.version          = '11.8.0'
     s.summary          = 'Firebase Analytics for iOS'
 
     s.description      = <<-DESC
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
     s.authors          = 'Google, Inc.'
 
     s.source           = {
-        :http => 'https://dl.google.com/firebase/ios/analytics/edf73aefd77661bd/FirebaseAnalytics-11.4.0.tar.gz'
+        :http => 'https://dl.google.com/firebase/ios/analytics/f18d9810c6c5311c/FirebaseAnalytics-11.7.0.tar.gz'
     }
 
     s.cocoapods_version = '>= 1.12.0'
@@ -26,7 +26,7 @@ Pod::Spec.new do |s|
     s.libraries  = 'c++', 'sqlite3', 'z'
     s.frameworks = 'StoreKit'
 
-    s.dependency 'FirebaseCore', '~> 11.7.0'
+    s.dependency 'FirebaseCore', '~> 11.8.0'
     s.dependency 'FirebaseInstallations', '~> 11.0'
     s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0'
     s.dependency 'GoogleUtilities/MethodSwizzler', '~> 8.0'
@@ -37,12 +37,12 @@ Pod::Spec.new do |s|
     s.default_subspecs = 'AdIdSupport'
 
     s.subspec 'AdIdSupport' do |ss|
-        ss.dependency 'GoogleAppMeasurement', '11.7.0'
+        ss.dependency 'GoogleAppMeasurement', '11.8.0'
         ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework'
     end
 
     s.subspec 'WithoutAdIdSupport' do |ss|
-        ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '11.7.0'
+        ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '11.8.0'
         ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework'
     end
 

+ 2 - 2
FirebaseAnalyticsOnDeviceConversion.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
     s.name             = 'FirebaseAnalyticsOnDeviceConversion'
-    s.version          = '11.7.0'
+    s.version          = '11.8.0'
     s.summary          = 'On device conversion measurement plugin for FirebaseAnalytics. Not intended for direct use.'
 
     s.description      = <<-DESC
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
 
     s.cocoapods_version = '>= 1.12.0'
 
-    s.dependency 'GoogleAppMeasurementOnDeviceConversion', '11.7.0'
+    s.dependency 'GoogleAppMeasurementOnDeviceConversion', '11.8.0'
 
     s.static_framework = true
 

+ 2 - 2
FirebaseAppCheck.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseAppCheck'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase App Check SDK.'
 
   s.description      = <<-DESC
@@ -46,7 +46,7 @@ Pod::Spec.new do |s|
 
   s.dependency 'AppCheckCore', '~> 11.0'
   s.dependency 'FirebaseAppCheckInterop', '~> 11.0'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'
   s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0'
 

+ 1 - 1
FirebaseAppCheckInterop.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseAppCheckInterop'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Interfaces that allow other Firebase SDKs to use AppCheck functionality.'
 
   s.description      = <<-DESC

+ 2 - 2
FirebaseAppDistribution.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseAppDistribution'
-  s.version          = '11.7.0-beta'
+  s.version          = '11.8.0-beta'
   s.summary          = 'App Distribution for Firebase iOS SDK.'
 
   s.description      = <<-DESC
@@ -30,7 +30,7 @@ iOS SDK for App Distribution for Firebase.
   ]
   s.public_header_files = base_dir + 'Public/FirebaseAppDistribution/*.h'
 
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0'
   s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0'
   s.dependency 'FirebaseInstallations', '~> 11.0'

+ 3 - 3
FirebaseAuth.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseAuth'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Apple platform client for Firebase Authentication'
 
   s.description      = <<-DESC
@@ -58,8 +58,8 @@ supports email and password accounts, as well as several 3rd party authenticatio
   s.ios.framework = 'SafariServices'
   s.dependency 'FirebaseAuthInterop', '~> 11.0'
   s.dependency 'FirebaseAppCheckInterop', '~> 11.0'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
-  s.dependency 'FirebaseCoreExtension', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
+  s.dependency 'FirebaseCoreExtension', '~> 11.8.0'
   s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0'
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'
   s.dependency 'GTMSessionFetcher/Core', '>= 3.4', '< 5.0'

+ 11 - 1
FirebaseAuth/CHANGELOG.md

@@ -1,7 +1,17 @@
-# Unreleased
+# 11.8.0
+- [added] Added `ActionCodeSettings.linkDomain` to customize the Firebase Hosting link domain
+  that is used in out-of-band email action flows.
+- [deprecated] Deprecated `ActionCodeSettings.dynamicLinkDomain`.
+
+# 11.7.0
 - [fixed] Fix Multi-factor session crash on second Firebase app. (#14238)
 - [fixed] Updated most decoders to be consistent with Firebase 10's behavior
   for decoding `nil` values. (#14212)
+- [fixed] Address Xcode 16.2 concurrency compile time issues. (#14279)
+- [fixed] Fix handling of cloud blocking function errors. (#14052)
+- [fixed] Fix phone auth flow to skip RCE verification if appVerificationDisabledForTesting is set. (#14242)
+- [fixed] Avoid over release crash by making concurrently accessed properties
+  atomic (#14308).
 
 # 11.6.0
 - [added] Added reCAPTCHA Enterprise support for app verification during phone

+ 10 - 0
FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift

@@ -40,8 +40,18 @@ import Foundation
   @objc open var androidInstallIfNotAvailable: Bool = false
 
   /// The Firebase Dynamic Link domain used for out of band code flow.
+  #if !FIREBASE_CI
+    @available(
+      *,
+      deprecated,
+      message: "Firebase Dynamic Links is deprecated. Migrate to use Firebase Hosting link and use `linkDomain` to set a custom domain instead."
+    )
+  #endif // !FIREBASE_CI
   @objc open var dynamicLinkDomain: String?
 
+  /// The out of band custom domain for handling code in app.
+  @objc public var linkDomain: String?
+
   /// Sets the iOS bundle ID.
   @objc override public init() {
     iOSBundleID = Bundle.main.bundleIdentifier

+ 11 - 1
FirebaseAuth/Sources/Swift/Auth/Auth.swift

@@ -144,6 +144,7 @@ extension Auth: AuthInterop {
 ///
 /// This class is thread-safe.
 @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
+@preconcurrency
 @objc(FIRAuth) open class Auth: NSObject {
   /// Gets the auth object for the default Firebase app.
   ///
@@ -2381,7 +2382,16 @@ extension Auth: AuthInterop {
   private let keychainServices: AuthKeychainServices
 
   /// The user access (ID) token used last time for posting auth state changed notification.
-  private var lastNotifiedUserToken: String?
+  ///
+  /// - Note: The atomic wrapper can be removed when the SDK is fully
+  /// synchronized with structured concurrency.
+  private var lastNotifiedUserToken: String? {
+    get { lastNotifiedUserTokenLock.withLock { _lastNotifiedUserToken } }
+    set { lastNotifiedUserTokenLock.withLock { _lastNotifiedUserToken = newValue } }
+  }
+
+  private var _lastNotifiedUserToken: String?
+  private var lastNotifiedUserTokenLock = NSLock()
 
   /// This flag denotes whether or not tokens should be automatically refreshed.
   /// Will only be set to `true` if the another Firebase service is included (additionally to

+ 15 - 0
FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift

@@ -203,6 +203,21 @@ import Foundation
       }
 
       let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
+
+      if let settings = auth.settings,
+         settings.isAppVerificationDisabledForTesting {
+        // If app verification is disabled for testing
+        // do not fetch recaptcha config, as this is not implemented in emulator
+        // Treat this same as RCE enable status off
+
+        return try await verifyClAndSendVerificationCode(
+          toPhoneNumber: phoneNumber,
+          retryOnInvalidAppCredential: true,
+          multiFactorSession: multiFactorSession,
+          uiDelegate: uiDelegate
+        )
+      }
+
       try await recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true)
 
       switch recaptchaVerifier.enablementStatus(forProvider: .phone) {

+ 14 - 4
FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

@@ -293,14 +293,22 @@ final class AuthBackend: AuthBackendProtocol {
       .unexpectedErrorResponse(deserializedResponse: dictionary, underlyingError: error)
   }
 
+  private static func splitStringAtFirstColon(_ input: String) -> (before: String, after: String) {
+    guard let colonIndex = input.firstIndex(of: ":") else {
+      return (input, "") // No colon, return original string before and empty after
+    }
+    let before = String(input.prefix(upTo: colonIndex))
+      .trimmingCharacters(in: .whitespacesAndNewlines)
+    let after = String(input.suffix(from: input.index(after: colonIndex)))
+      .trimmingCharacters(in: .whitespacesAndNewlines)
+    return (before, after.isEmpty ? "" : after) // Return empty after if it's empty
+  }
+
   private static func clientError(withServerErrorMessage serverErrorMessage: String,
                                   errorDictionary: [String: Any],
                                   response: AuthRPCResponse?,
                                   error: Error?) -> Error? {
-    let split = serverErrorMessage.split(separator: ":")
-    let shortErrorMessage = split.first?.trimmingCharacters(in: .whitespacesAndNewlines)
-    let serverDetailErrorMessage = String(split.count > 1 ? split[1] : "")
-      .trimmingCharacters(in: .whitespacesAndNewlines)
+    let (shortErrorMessage, serverDetailErrorMessage) = splitStringAtFirstColon(serverErrorMessage)
     switch shortErrorMessage {
     case "USER_NOT_FOUND": return AuthErrorUtils
       .userNotFoundError(message: serverDetailErrorMessage)
@@ -377,6 +385,8 @@ final class AuthBackend: AuthBackendProtocol {
       .missingAppCredential(message: serverDetailErrorMessage)
     case "INVALID_CODE": return AuthErrorUtils
       .invalidVerificationCodeError(message: serverDetailErrorMessage)
+    case "INVALID_HOSTING_LINK_DOMAIN": return AuthErrorUtils
+      .invalidHostingLinkDomainError(message: serverDetailErrorMessage)
     case "INVALID_SESSION_INFO": return AuthErrorUtils
       .invalidVerificationIDError(message: serverDetailErrorMessage)
     case "SESSION_EXPIRED": return AuthErrorUtils

+ 10 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift

@@ -78,6 +78,9 @@ private let kCanHandleCodeInAppKey = "canHandleCodeInApp"
 /// The key for the "dynamic link domain" value in the request.
 private let kDynamicLinkDomainKey = "dynamicLinkDomain"
 
+/// The key for the "link domain" value in the request.
+private let kLinkDomainKey = "linkDomain"
+
 /// The value for the "PASSWORD_RESET" request type.
 private let kPasswordResetRequestTypeValue = "PASSWORD_RESET"
 
@@ -140,6 +143,9 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
   /// The Firebase Dynamic Link domain used for out of band code flow.
   private let dynamicLinkDomain: String?
 
+  /// The Firebase Hosting domain used for out of band code flow.
+  private(set) var linkDomain: String?
+
   /// Response to the captcha.
   var captchaResponse: String?
 
@@ -172,6 +178,7 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
     androidInstallApp = actionCodeSettings?.androidInstallIfNotAvailable ?? false
     handleCodeInApp = actionCodeSettings?.handleCodeInApp ?? false
     dynamicLinkDomain = actionCodeSettings?.dynamicLinkDomain
+    linkDomain = actionCodeSettings?.linkDomain
 
     super.init(
       endpoint: kGetOobConfirmationCodeEndpoint,
@@ -274,6 +281,9 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
     if let dynamicLinkDomain {
       body[kDynamicLinkDomainKey] = dynamicLinkDomain
     }
+    if let linkDomain {
+      body[kLinkDomainKey] = linkDomain
+    }
     if let captchaResponse {
       body[kCaptchaResponseKey] = captchaResponse
     }

+ 77 - 83
FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift

@@ -67,6 +67,53 @@ import Foundation
                      displayName: String?,
                      completion: ((Error?) -> Void)?) {
       // TODO: Refactor classes so this duplicated code isn't necessary for phone and totp.
+
+      guard
+        assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID ||
+        assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID
+      else {
+        return
+      }
+
+      guard let user, let auth = user.auth else {
+        fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
+      }
+
+      let request = Self.enrollmentFinalizationRequest(
+        with: assertion,
+        displayName: displayName,
+        user: user,
+        auth: auth
+      )
+
+      Task {
+        do {
+          let response = try await auth.backend.call(with: request)
+          let user = try await auth.completeSignIn(withAccessToken: response.idToken,
+                                                   accessTokenExpirationDate: nil,
+                                                   refreshToken: response.refreshToken,
+                                                   anonymous: false)
+          try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
+          if let completion {
+            DispatchQueue.main.async {
+              completion(nil)
+            }
+          }
+        } catch {
+          if let completion {
+            DispatchQueue.main.async {
+              completion(error)
+            }
+          }
+        }
+      }
+    }
+
+    private static func enrollmentFinalizationRequest(with assertion: MultiFactorAssertion,
+                                                      displayName: String?,
+                                                      user: User,
+                                                      auth: Auth) -> FinalizeMFAEnrollmentRequest {
+      var request: FinalizeMFAEnrollmentRequest? = nil
       if assertion.factorID == PhoneMultiFactorInfo.TOTPMultiFactorID {
         guard let totpAssertion = assertion as? TOTPMultiFactorAssertion else {
           fatalError("Auth Internal Error: Failed to find TOTPMultiFactorAssertion")
@@ -74,97 +121,44 @@ import Foundation
         switch totpAssertion.secretOrID {
         case .enrollmentID: fatalError("Missing secret in totpAssertion")
         case let .secret(secret):
-          guard let user = user, let auth = user.auth else {
-            fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
-          }
           let finalizeMFATOTPRequestInfo =
             AuthProtoFinalizeMFATOTPEnrollmentRequestInfo(sessionInfo: secret.sessionInfo,
                                                           verificationCode: totpAssertion
                                                             .oneTimePassword)
-          let request = FinalizeMFAEnrollmentRequest(idToken: self.user?.rawAccessToken(),
-                                                     displayName: displayName,
-                                                     totpVerificationInfo: finalizeMFATOTPRequestInfo,
-                                                     requestConfiguration: user
-                                                       .requestConfiguration)
-          Task {
-            do {
-              let response = try await auth.backend.call(with: request)
-              do {
-                let user = try await auth.completeSignIn(withAccessToken: response.idToken,
-                                                         accessTokenExpirationDate: nil,
-                                                         refreshToken: response.refreshToken,
-                                                         anonymous: false)
-                try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
-                if let completion {
-                  DispatchQueue.main.async {
-                    completion(nil)
-                  }
-                }
-              } catch {
-                DispatchQueue.main.async {
-                  if let completion {
-                    completion(error)
-                  }
-                }
-              }
-            } catch {
-              if let completion {
-                completion(error)
-              }
-            }
-          }
+          request = FinalizeMFAEnrollmentRequest(idToken: user.rawAccessToken(),
+                                                 displayName: displayName,
+                                                 totpVerificationInfo: finalizeMFATOTPRequestInfo,
+                                                 requestConfiguration: user
+                                                   .requestConfiguration)
         }
-        return
-      } else if assertion.factorID != PhoneMultiFactorInfo.PhoneMultiFactorID {
-        return
-      }
-      let phoneAssertion = assertion as? PhoneMultiFactorAssertion
-      guard let credential = phoneAssertion?.authCredential else {
-        fatalError("Internal Error: Missing credential")
-      }
-      switch credential.credentialKind {
-      case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
-      case let .verification(verificationID, code):
-        let finalizeMFAPhoneRequestInfo =
-          AuthProtoFinalizeMFAPhoneRequestInfo(sessionInfo: verificationID, verificationCode: code)
-        guard let user = user, let auth = user.auth else {
-          fatalError("Internal Auth error: failed to get user enrolling in MultiFactor")
+      } else if assertion.factorID == PhoneMultiFactorInfo.PhoneMultiFactorID {
+        let phoneAssertion = assertion as? PhoneMultiFactorAssertion
+        guard let credential = phoneAssertion?.authCredential else {
+          fatalError("Internal Error: Missing credential")
         }
-        let request = FinalizeMFAEnrollmentRequest(
-          idToken: self.user?.rawAccessToken(),
-          displayName: displayName,
-          phoneVerificationInfo: finalizeMFAPhoneRequestInfo,
-          requestConfiguration: user.requestConfiguration
-        )
-
-        Task {
-          do {
-            let response = try await auth.backend.call(with: request)
-            do {
-              let user = try await auth.completeSignIn(withAccessToken: response.idToken,
-                                                       accessTokenExpirationDate: nil,
-                                                       refreshToken: response.refreshToken,
-                                                       anonymous: false)
-              try auth.updateCurrentUser(user, byForce: false, savingToDisk: true)
-              if let completion {
-                DispatchQueue.main.async {
-                  completion(nil)
-                }
-              }
-            } catch {
-              DispatchQueue.main.async {
-                if let completion {
-                  completion(error)
-                }
-              }
-            }
-          } catch {
-            if let completion {
-              completion(error)
-            }
-          }
+        switch credential.credentialKind {
+        case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
+        case let .verification(verificationID, code):
+          let finalizeMFAPhoneRequestInfo =
+            AuthProtoFinalizeMFAPhoneRequestInfo(
+              sessionInfo: verificationID,
+              verificationCode: code
+            )
+          request = FinalizeMFAEnrollmentRequest(
+            idToken: user.rawAccessToken(),
+            displayName: displayName,
+            phoneVerificationInfo: finalizeMFAPhoneRequestInfo,
+            requestConfiguration: user.requestConfiguration
+          )
         }
       }
+
+      guard let request else {
+        // Assertion is not a phone assertion or TOTP assertion.
+        fatalError("Internal Error: Unsupported assertion with factor ID: \(assertion.factorID).")
+      }
+
+      return request
     }
 
     /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the

+ 1 - 0
FirebaseAuth/Sources/Swift/SystemService/AuthNotificationManager.swift

@@ -18,6 +18,7 @@
 
   /// A class represents a credential that proves the identity of the app.
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
+  @preconcurrency
   class AuthNotificationManager {
     /// The key to locate payload data in the remote notification.
     private let kNotificationDataKey = "com.google.firebase.auth"

+ 12 - 3
FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift

@@ -125,8 +125,17 @@ class SecureTokenService: NSObject, NSSecureCoding {
   ///
   /// This method is specifically for providing the access token to internal clients during
   /// deserialization and sign-in events, and should not be used to retrieve the access token by
-  ///     anyone else.
-  var accessToken: String
+  /// anyone else.
+  ///
+  /// - Note: The atomic wrapper can be removed when the SDK is fully
+  /// synchronized with structured concurrency.
+  var accessToken: String {
+    get { accessTokenLock.withLock { _accessToken } }
+    set { accessTokenLock.withLock { _accessToken = newValue } }
+  }
+
+  private var _accessToken: String
+  private let accessTokenLock = NSLock()
 
   /// The refresh token for the user, or `nil` if the user has yet completed sign-in flow.
   ///
@@ -147,7 +156,7 @@ class SecureTokenService: NSObject, NSSecureCoding {
        refreshToken: String) {
     internalService = SecureTokenServiceInternal()
     self.requestConfiguration = requestConfiguration
-    self.accessToken = accessToken
+    _accessToken = accessToken
     self.accessTokenExpirationDate = accessTokenExpirationDate
     self.refreshToken = refreshToken
   }

+ 1 - 1
FirebaseAuth/Sources/Swift/User/User.swift

@@ -1764,7 +1764,7 @@ extension User: NSSecureCoding {}
     requestConfiguration = AuthRequestConfiguration(apiKey: apiKey ?? "", appID: appID ?? "")
 
     // This property will be overwritten later via the `user.auth` property update. For now, a
-    // placeholder is set as the property update should happen right after this intializer.
+    // placeholder is set as the property update should happen right after this initializer.
     backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer())
 
     userProfileUpdate = UserProfileUpdate()

+ 55 - 14
FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift

@@ -370,6 +370,10 @@ class AuthErrorUtils {
     error(code: .invalidDynamicLinkDomain, message: message)
   }
 
+  static func invalidHostingLinkDomainError(message: String?) -> Error {
+    error(code: .invalidHostingLinkDomain, message: message)
+  }
+
   static func missingOrInvalidNonceError(message: String?) -> Error {
     error(code: .missingOrInvalidNonce, message: message)
   }
@@ -510,25 +514,62 @@ class AuthErrorUtils {
     return error(code: .accountExistsWithDifferentCredential, userInfo: userInfo)
   }
 
+  private static func extractJSONObjectFromString(from string: String) -> [String: Any]? {
+    // 1. Find the start of the JSON object.
+    guard let start = string.firstIndex(of: "{") else {
+      return nil // No JSON object found
+    }
+    // 2. Find the end of the JSON object.
+    // Start from the first curly brace `{`
+    var curlyLevel = 0
+    var endIndex: String.Index?
+
+    for index in string.indices.suffix(from: start) {
+      let char = string[index]
+      if char == "{" {
+        curlyLevel += 1
+      } else if char == "}" {
+        curlyLevel -= 1
+        if curlyLevel == 0 {
+          endIndex = index
+          break
+        }
+      }
+    }
+    guard let end = endIndex else {
+      return nil // Unbalanced curly braces
+    }
+
+    // 3. Extract the JSON string.
+    let jsonString = String(string[start ... end])
+
+    // 4. Convert JSON String to JSON Object
+    guard let jsonData = jsonString.data(using: .utf8) else {
+      return nil // Could not convert String to Data
+    }
+
+    do {
+      if let jsonObject = try JSONSerialization
+        .jsonObject(with: jsonData, options: []) as? [String: Any] {
+        return jsonObject
+      } else {
+        return nil // JSON Object is not a dictionary
+      }
+    } catch {
+      return nil // Failed to deserialize JSON
+    }
+  }
+
   static func blockingCloudFunctionServerResponse(message: String?) -> Error {
     guard let message else {
       return error(code: .blockingCloudFunctionError, message: message)
     }
-    var jsonString = message.replacingOccurrences(
-      of: "HTTP Cloud Function returned an error:",
-      with: ""
-    )
-    jsonString = jsonString.trimmingCharacters(in: .whitespaces)
-    let jsonData = jsonString.data(using: .utf8) ?? Data()
-    do {
-      let jsonDict = try JSONSerialization
-        .jsonObject(with: jsonData, options: []) as? [String: Any] ?? [:]
-      let errorDict = jsonDict["error"] as? [String: Any] ?? [:]
-      let errorMessage = errorDict["message"] as? String
-      return error(code: .blockingCloudFunctionError, message: errorMessage)
-    } catch {
-      return JSONSerializationError(underlyingError: error)
+    guard let jsonDict = extractJSONObjectFromString(from: message) else {
+      return error(code: .blockingCloudFunctionError, message: message)
     }
+    let errorDict = jsonDict["error"] as? [String: Any] ?? [:]
+    let errorMessage = errorDict["message"] as? String
+    return error(code: .blockingCloudFunctionError, message: errorMessage)
   }
 
   #if os(iOS)

+ 11 - 1
FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift

@@ -258,6 +258,9 @@ import Foundation
   /// unauthorized for the current project.
   case invalidDynamicLinkDomain = 17074
 
+  /// Indicates that the provided Firebase Hosting Link domain is not owned by the current project.
+  case invalidHostingLinkDomain = 17214
+
   /// Indicates that the credential is rejected because it's malformed or mismatching.
   case rejectedCredential = 17075
 
@@ -303,7 +306,7 @@ import Foundation
   /// Indicates that the nonce is missing or invalid.
   case missingOrInvalidNonce = 17094
 
-  /// Raised when n Cloud Function returns a blocking error. Will include a message returned from
+  /// Raised when a Cloud Function returns a blocking error. Will include a message returned from
   /// the function.
   case blockingCloudFunctionError = 17105
 
@@ -468,6 +471,8 @@ import Foundation
       return kErrorInvalidProviderID
     case .invalidDynamicLinkDomain:
       return kErrorInvalidDynamicLinkDomain
+    case .invalidHostingLinkDomain:
+      return kErrorInvalidHostingLinkDomain
     case .webInternalError:
       return kErrorWebInternalError
     case .webSignInUserInteractionFailure:
@@ -661,6 +666,8 @@ import Foundation
       return "ERROR_INVALID_PROVIDER_ID"
     case .invalidDynamicLinkDomain:
       return "ERROR_INVALID_DYNAMIC_LINK_DOMAIN"
+    case .invalidHostingLinkDomain:
+      return "ERROR_INVALID_HOSTING_LINK_DOMAIN"
     case .webInternalError:
       return "ERROR_WEB_INTERNAL_ERROR"
     case .webSignInUserInteractionFailure:
@@ -905,6 +912,9 @@ private let kErrorInvalidProviderID =
 private let kErrorInvalidDynamicLinkDomain =
   "The Firebase Dynamic Link domain used is either not configured or is unauthorized for the current project."
 
+private let kErrorInvalidHostingLinkDomain =
+  "The provided hosting link domain is not configured in Firebase Hosting or is not owned by the current project."
+
 private let kErrorInternalError =
   "An internal error has occurred, print and inspect the error details for more information."
 

+ 9 - 11
FirebaseAuth/Tests/SampleSwift/AuthenticationExample.xcodeproj/project.pbxproj

@@ -46,6 +46,7 @@
 		EA20B50C249E8F0700B5E581 /* AuthMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA20B50B249E8F0700B5E581 /* AuthMenu.swift */; };
 		EA20B510249FDB4400B5E581 /* OtherAuthMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA20B50F249FDB4400B5E581 /* OtherAuthMethods.swift */; };
 		EA217895248979E200736757 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EA217894248979E200736757 /* LaunchScreen.storyboard */; };
+		EA3348322C90EFF40091D7C2 /* LoginViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3348312C90EFE40091D7C2 /* LoginViewSwiftUI.swift */; };
 		EA527CAA24A0766D00ADB9A2 /* OtherAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA527CA924A0766D00ADB9A2 /* OtherAuthViewController.swift */; };
 		EA527CAC24A0EE9600ADB9A2 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA527CAB24A0EE9600ADB9A2 /* LoginView.swift */; };
 		EAB3A1792494433500385291 /* DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB3A1782494433500385291 /* DataSourceProvider.swift */; };
@@ -57,7 +58,6 @@
 		EAE4CBC924855E3A00245E92 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBC824855E3A00245E92 /* AuthViewController.swift */; };
 		EAE4CBCE24855E3D00245E92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAE4CBCD24855E3D00245E92 /* Assets.xcassets */; };
 		EAE4CBE724855E3E00245E92 /* AuthenticationExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBE624855E3E00245E92 /* AuthenticationExampleUITests.swift */; };
-		EAE4CBF524857A5100245E92 /* LoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE4CBF424857A5100245E92 /* LoginController.swift */; };
 		EAEBCE0F2489FFDE00FCEA92 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEBCE0E2489FFDE00FCEA92 /* Extensions.swift */; };
 		EAEBCE11248A9AA000FCEA92 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEBCE10248A9AA000FCEA92 /* Section.swift */; };
 		EAFDF2BE2490439F0082B6F1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFDF2BD2490439F0082B6F1 /* Animator.swift */; };
@@ -125,8 +125,8 @@
 		EA20B50B249E8F0700B5E581 /* AuthMenu.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = AuthMenu.swift; sourceTree = "<group>"; };
 		EA20B50F249FDB4400B5E581 /* OtherAuthMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherAuthMethods.swift; sourceTree = "<group>"; };
 		EA217894248979E200736757 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
+		EA3348312C90EFE40091D7C2 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
 		EA527CA924A0766D00ADB9A2 /* OtherAuthViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OtherAuthViewController.swift; sourceTree = "<group>"; };
-		EA527CAB24A0EE9600ADB9A2 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
 		EAB3A1782494433500385291 /* DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceProvider.swift; sourceTree = "<group>"; };
 		EAB3A17B2494628200385291 /* UserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserViewController.swift; sourceTree = "<group>"; };
 		EAD8BD3F2CE535C400E23E30 /* MFALoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFALoginView.swift; sourceTree = "<group>"; };
@@ -140,7 +140,6 @@
 		EAE4CBE224855E3E00245E92 /* AuthenticationExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthenticationExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		EAE4CBE624855E3E00245E92 /* AuthenticationExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationExampleUITests.swift; sourceTree = "<group>"; };
 		EAE4CBE824855E3E00245E92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		EAE4CBF424857A5100245E92 /* LoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginController.swift; sourceTree = "<group>"; };
 		EAEBCE0E2489FFDE00FCEA92 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
 		EAEBCE10248A9AA000FCEA92 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
 		EAFDF2BD2490439F0082B6F1 /* Animator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = "<group>"; };
@@ -224,7 +223,6 @@
 			children = (
 				EAE08EB424CF5D09006FA3A5 /* AccountLinkingViewController.swift */,
 				EAE4CBC824855E3A00245E92 /* AuthViewController.swift */,
-				EAE4CBF424857A5100245E92 /* LoginController.swift */,
 				EAB3A17B2494628200385291 /* UserViewController.swift */,
 				EA02F68E24A0714B0079D000 /* OtherAuthMethodControllers */,
 				DEC2E5DC2A95331D0090260A /* SettingsViewController.swift */,
@@ -248,7 +246,7 @@
 			children = (
 				EAD8BD3F2CE535C400E23E30 /* MFALoginView.swift */,
 				EA20B46B2495A9F900B5E581 /* SignedOutView.swift */,
-				EA527CAB24A0EE9600ADB9A2 /* LoginView.swift */,
+				EA3348312C90EFE40091D7C2 /* LoginView.swift */,
 			);
 			path = CustomViews;
 			sourceTree = "<group>";
@@ -550,6 +548,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				EA20B46E2495B2C700B5E581 /* DataSourceProtocols.swift in Sources */,
+				EA3348322C90EFF40091D7C2 /* LoginView.swift in Sources */,
 				EAB3A1792494433500385291 /* DataSourceProvider.swift in Sources */,
 				EA20B46C2495A9F900B5E581 /* SignedOutView.swift in Sources */,
 				EA527CAA24A0766D00ADB9A2 /* OtherAuthViewController.swift in Sources */,
@@ -560,7 +559,6 @@
 				EA02F68524A000E00079D000 /* UserActions.swift in Sources */,
 				EA02F68D24A063E90079D000 /* LoginDelegate.swift in Sources */,
 				EA20B50A249D3D8600B5E581 /* PasswordlessViewController.swift in Sources */,
-				EAE4CBF524857A5100245E92 /* LoginController.swift in Sources */,
 				EA20B510249FDB4400B5E581 /* OtherAuthMethods.swift in Sources */,
 				EA12697F29E33A5D00D79E66 /* CryptoUtils.swift in Sources */,
 				EAEBCE11248A9AA000FCEA92 /* Section.swift in Sources */,
@@ -572,7 +570,6 @@
 				EAFDF2BE2490439F0082B6F1 /* Animator.swift in Sources */,
 				EAE4CBC724855E3A00245E92 /* SceneDelegate.swift in Sources */,
 				EAE08EB524CF5D09006FA3A5 /* AccountLinkingViewController.swift in Sources */,
-				EA527CAC24A0EE9600ADB9A2 /* LoginView.swift in Sources */,
 				EAEBCE0F2489FFDE00FCEA92 /* Extensions.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -717,7 +714,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.6;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				ONLY_ACTIVE_ARCH = YES;
@@ -772,7 +769,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.6;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				OTHER_LDFLAGS = "-ObjC";
@@ -815,6 +812,7 @@
 				"CODE_SIGN_IDENTITY[sdk=*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Manual;
 				DEVELOPMENT_TEAM = "";
+				"DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV;
 				INFOPLIST_FILE = AuthenticationExample/SwiftApplication.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 15.6;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -839,7 +837,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = SwiftApiTests/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.6;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -864,7 +862,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = SwiftApiTests/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.6;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",

+ 4 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppDelegate.swift

@@ -71,5 +71,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
   private func configureApplicationAppearance() {
     UINavigationBar.appearance().tintColor = .systemOrange
     UITabBar.appearance().tintColor = .systemOrange
+    // Handles iOS 15 behavior change where tab bar become translucent during transitions.
+    let appearance = UITabBarAppearance()
+    appearance.configureWithOpaqueBackground()
+    UITabBar.appearance().scrollEdgeAppearance = appearance
   }
 }

+ 3 - 3
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Contents.json

@@ -1,6 +1,6 @@
 {
   "info" : {
-    "version" : 1,
-    "author" : "xcode"
+    "author" : "xcode",
+    "version" : 1
   }
-}
+}

+ 0 - 21
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/Contents.json

@@ -1,21 +0,0 @@
-{
-  "images" : [
-    {
-      "idiom" : "universal",
-      "filename" : "logo-1024px.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "universal",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "universal",
-      "scale" : "3x"
-    }
-  ],
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}

BIN
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/firebaseLogo.imageset/logo-1024px.png


+ 142 - 140
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/CustomViews/LoginView.swift

@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// Copyright 2024 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,162 +12,164 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import UIKit
+import SwiftUI
 
-/// Login View presented when performing Email & Password Login Flow
-class LoginView: UIView {
-  var emailTextField: UITextField! {
-    didSet {
-      emailTextField.textContentType = .emailAddress
-    }
-  }
+import FirebaseAuth
 
-  var passwordTextField: UITextField! {
-    didSet {
-      passwordTextField.textContentType = .password
-    }
-  }
+struct LoginView: View {
+  @Environment(\.dismiss) private var dismiss
 
-  var emailTopConstraint: NSLayoutConstraint!
-  var passwordTopConstraint: NSLayoutConstraint!
-
-  lazy var loginButton: UIButton = {
-    let button = UIButton()
-    button.setTitle("Login", for: .normal)
-    button.setTitleColor(.white, for: .normal)
-    button.setTitleColor(.highlightedLabel, for: .highlighted)
-    button.setBackgroundImage(UIColor.systemOrange.image, for: .normal)
-    button.setBackgroundImage(UIColor.systemOrange.highlighted.image, for: .highlighted)
-    button.clipsToBounds = true
-    button.layer.cornerRadius = 14
-    return button
-  }()
-
-  lazy var createAccountButton: UIButton = {
-    let button = UIButton()
-    button.setTitle("Create Account", for: .normal)
-    button.setTitleColor(.secondaryLabel, for: .normal)
-    button.setTitleColor(UIColor.secondaryLabel.highlighted, for: .highlighted)
-    return button
-  }()
-
-  convenience init() {
-    self.init(frame: .zero)
-    setupSubviews()
-  }
+  @State private var email: String = ""
+  @State private var password: String = ""
 
-  // MARK: - Subviews Setup
+  // Properties for displaying error alerts.
+  @State private var showingAlert: Bool = false
+  @State private var error: Error?
 
-  private func setupSubviews() {
-    backgroundColor = .systemBackground
-    clipsToBounds = true
+  private weak var delegate: (any LoginDelegate)?
 
-    setupFirebaseLogoImage()
-    setupEmailTextfield()
-    setupPasswordTextField()
-    setupLoginButton()
-    setupCreateAccountButton()
+  init(delegate: (any LoginDelegate)? = nil) {
+    self.delegate = delegate
   }
 
-  private func setupFirebaseLogoImage() {
-    let firebaseLogo = UIImage(named: "firebaseLogo")
-    let imageView = UIImageView(image: firebaseLogo)
-    imageView.contentMode = .scaleAspectFit
-    addSubview(imageView)
-    imageView.translatesAutoresizingMaskIntoConstraints = false
-    NSLayoutConstraint.activate([
-      imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -55),
-      imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 55),
-      imageView.widthAnchor.constraint(equalToConstant: 325),
-      imageView.heightAnchor.constraint(equalToConstant: 325),
-    ])
+  private func login() {
+    Task {
+      do {
+        _ = try await AppManager.shared
+          .auth()
+          .signIn(withEmail: email, password: password)
+        await MainActor.run {
+          dismiss()
+          delegate?.loginDidOccur(resolver: nil)
+        }
+        // TODO(ncooke3): Investigate possible improvements.
+//      } catch let error as AuthErrorCode
+//        where error.code == .secondFactorRequired {
+//        // error as? AuthErrorCode == nil because AuthErrorUtils returns generic
+//        /Errors
+//        // https://firebase.google.com/docs/auth/ios/totp-mfa#sign_in_users_with_a_second_factor
+      } catch {
+        let error = error as NSError
+        if error.code == AuthErrorCode.secondFactorRequired.rawValue {
+          let mfaKey = AuthErrorUserInfoMultiFactorResolverKey
+          if let resolver = error.userInfo[mfaKey] as? MultiFactorResolver {
+            // Multi-factor auth is required is to complete sign-in.
+            await MainActor.run {
+              dismiss()
+              delegate?.loginDidOccur(resolver: resolver)
+            }
+          }
+        }
+
+        print(error.localizedDescription)
+        self.error = error
+        self.showingAlert.toggle()
+      }
+    }
   }
 
-  private func setupEmailTextfield() {
-    emailTextField = textField(placeholder: "Email", symbolName: "person.crop.circle")
-    emailTextField.translatesAutoresizingMaskIntoConstraints = false
-    addSubview(emailTextField)
-    NSLayoutConstraint.activate([
-      emailTextField.leadingAnchor.constraint(
-        equalTo: safeAreaLayoutGuide.leadingAnchor,
-        constant: 15
-      ),
-      emailTextField.trailingAnchor.constraint(
-        equalTo: safeAreaLayoutGuide.trailingAnchor,
-        constant: -15
-      ),
-      emailTextField.heightAnchor.constraint(equalToConstant: 45),
-    ])
-
-    let constant: CGFloat = UIDevice.current.orientation.isLandscape ? 15 : 50
-    emailTopConstraint = emailTextField.topAnchor.constraint(
-      equalTo: safeAreaLayoutGuide.topAnchor,
-      constant: constant
-    )
-    emailTopConstraint.isActive = true
+  private func createUser() {
+    Task {
+      do {
+        _ = try await AppManager.shared.auth().createUser(
+          withEmail: email,
+          password: password
+        )
+        // Sign-in was successful.
+        await MainActor.run {
+          dismiss()
+          delegate?.loginDidOccur(resolver: nil)
+        }
+      } catch {
+        print(error.localizedDescription)
+        self.error = error
+        self.showingAlert.toggle()
+      }
+    }
   }
+}
 
-  private func setupPasswordTextField() {
-    passwordTextField = textField(placeholder: "Password", symbolName: "lock.fill")
-    passwordTextField.translatesAutoresizingMaskIntoConstraints = false
-    addSubview(passwordTextField)
-    NSLayoutConstraint.activate([
-      passwordTextField.leadingAnchor.constraint(
-        equalTo: safeAreaLayoutGuide.leadingAnchor,
-        constant: 15
-      ),
-      passwordTextField.trailingAnchor.constraint(
-        equalTo: safeAreaLayoutGuide.trailingAnchor,
-        constant: -15
-      ),
-      passwordTextField.heightAnchor.constraint(equalToConstant: 45),
-    ])
-
-    let constant: CGFloat = UIDevice.current.orientation.isLandscape ? 5 : 20
-    passwordTopConstraint =
-      passwordTextField.topAnchor.constraint(
-        equalTo: emailTextField.bottomAnchor,
-        constant: constant
+extension LoginView {
+  var body: some View {
+    VStack(alignment: .leading) {
+      Text(
+        "Login or create an account using the Email/Password auth " +
+          "provider.\n\nEnsure that the Email/Password provider is " +
+          "enabled on the Firebase console for the given project."
       )
-    passwordTopConstraint.isActive = true
+      .fixedSize(horizontal: false, vertical: true)
+      .padding(.bottom)
+
+      TextField("Email", text: $email)
+        .textFieldStyle(SymbolTextFieldStyle(symbolName: "person.crop.circle"))
+
+      TextField("Password", text: $password)
+        .textFieldStyle(SymbolTextFieldStyle(symbolName: "lock.fill"))
+        .padding(.bottom)
+
+      Group {
+        Button(action: login) {
+          Text("Login")
+            .bold()
+        }
+        .buttonStyle(CustomButtonStyle(backgroundColor: .orange, foregroundColor: .white))
+
+        Button(action: createUser) {
+          Text("Create Account")
+            .bold()
+        }
+        .buttonStyle(CustomButtonStyle(backgroundColor: .primary, foregroundColor: .orange))
+      }
+      .disabled(email.isEmpty || password.isEmpty)
+
+      Spacer()
+    }
+    .padding()
+    .alert("Error", isPresented: $showingAlert) {
+      if let error {
+        Text(error.localizedDescription)
+      }
+      Button("OK", role: .cancel) {
+        showingAlert.toggle()
+      }
+    }
   }
+}
 
-  private func setupLoginButton() {
-    addSubview(loginButton)
-    loginButton.translatesAutoresizingMaskIntoConstraints = false
-    NSLayoutConstraint.activate([
-      loginButton.leadingAnchor.constraint(
-        equalTo: safeAreaLayoutGuide.leadingAnchor,
-        constant: 15
-      ),
-      loginButton.trailingAnchor.constraint(
-        equalTo: safeAreaLayoutGuide.trailingAnchor,
-        constant: -15
-      ),
-      loginButton.heightAnchor.constraint(equalToConstant: 45),
-      loginButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 5),
-    ])
+private struct SymbolTextFieldStyle: TextFieldStyle {
+  let symbolName: String
+
+  func _body(configuration: TextField<Self._Label>) -> some View {
+    HStack {
+      Image(systemName: symbolName)
+        .foregroundColor(.orange)
+        .imageScale(.large)
+        .padding(.leading)
+      configuration
+        .padding([.vertical, .trailing])
+    }
+    .background(Color(uiColor: .secondarySystemBackground))
+    .cornerRadius(14)
+    .textInputAutocapitalization(.never)
   }
+}
 
-  private func setupCreateAccountButton() {
-    addSubview(createAccountButton)
-    createAccountButton.translatesAutoresizingMaskIntoConstraints = false
-    NSLayoutConstraint.activate([
-      createAccountButton.centerXAnchor.constraint(equalTo: centerXAnchor),
-      createAccountButton.topAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: 5),
-    ])
+private struct CustomButtonStyle: ButtonStyle {
+  let backgroundColor: Color
+  let foregroundColor: Color
+  func makeBody(configuration: Configuration) -> some View {
+    HStack {
+      Spacer()
+      configuration.label
+      Spacer()
+    }
+    .padding()
+    .background(backgroundColor, in: RoundedRectangle(cornerRadius: 14))
+    .foregroundStyle(foregroundColor)
+    .opacity(configuration.isPressed ? 0.5 : 1)
   }
+}
 
-  // MARK: - Private Helpers
-
-  private func textField(placeholder: String, symbolName: String) -> UITextField {
-    let textfield = UITextField()
-    textfield.backgroundColor = .secondarySystemBackground
-    textfield.layer.cornerRadius = 14
-    textfield.placeholder = placeholder
-    textfield.tintColor = .systemOrange
-    let symbol = UIImage(systemName: symbolName)
-    textfield.setImage(symbol)
-    return textfield
-  }
+#Preview {
+  LoginView()
 }

+ 6 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift

@@ -43,6 +43,7 @@ enum AuthMenu: String {
   case deleteApp
   case actionType
   case continueURL
+  case linkDomain
   case requestVerifyEmail
   case requestPasswordReset
   case resetPassword
@@ -117,6 +118,8 @@ enum AuthMenu: String {
       return "Action Type"
     case .continueURL:
       return "Continue URL"
+    case .linkDomain:
+      return "Link Domain"
     case .requestVerifyEmail:
       return "Request Verify Email"
     case .requestPasswordReset:
@@ -197,6 +200,8 @@ enum AuthMenu: String {
       self = .actionType
     case "Continue URL":
       self = .continueURL
+    case "Link Domain":
+      self = .linkDomain
     case "Request Verify Email":
       self = .requestVerifyEmail
     case "Request Password Reset":
@@ -328,6 +333,7 @@ class AuthMenuData: DataSourceProvidable {
     let items: [Item] = [
       Item(title: AuthMenu.actionType.name, detailTitle: ActionCodeRequestType.inApp.name),
       Item(title: AuthMenu.continueURL.name, detailTitle: "--", isEditable: true),
+      Item(title: AuthMenu.linkDomain.name, detailTitle: "--", isEditable: true),
       Item(title: AuthMenu.requestVerifyEmail.name),
       Item(title: AuthMenu.requestPasswordReset.name),
       Item(title: AuthMenu.resetPassword.name),

+ 1 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift

@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import FirebaseAuth
+import SwiftUI
 import UIKit
 
 // MARK: - Extending a `Firebase User` to conform to `DataSourceProvidable`

+ 4 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift

@@ -354,6 +354,9 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
   /// Similar to in `PasswordlessViewController`, enter the authorized domain.
   /// Please refer to this Quickstart's README for more information.
   private let authorizedDomain: String = "ENTER AUTHORIZED DOMAIN"
+
+  /// This is the replacement for customized dynamic link domain.
+  private let customDomain: String = "ENTER AUTHORIZED HOSTING DOMAIN"
   /// Maintain a reference to the email entered for linking user to Passwordless.
   private var email: String?
 
@@ -380,6 +383,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
     // The sign-in operation must be completed in the app.
     actionCodeSettings.handleCodeInApp = true
     actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
+    actionCodeSettings.linkDomain = customDomain
 
     AppManager.shared.auth()
       .sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in

+ 71 - 131
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift

@@ -16,6 +16,8 @@
 // [START auth_import]
 import FirebaseCore
 
+import SwiftUI
+
 // For Sign in with Facebook
 import FBSDKLoginKit
 
@@ -41,6 +43,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
   var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = []
   var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = []
   var actionCodeContinueURL: URL?
+  var actionCodeLinkDomain: String?
   var actionCodeRequestType: ActionCodeRequestType = .inApp
 
   let spinner = UIActivityIndicatorView(style: .medium)
@@ -71,6 +74,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
     let settings = ActionCodeSettings()
     settings.url = actionCodeContinueURL
     settings.handleCodeInApp = (actionCodeRequestType == .inApp)
+    settings.linkDomain = actionCodeLinkDomain
     return settings
   }
 
@@ -158,6 +162,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
     case .continueURL:
       changeActionCodeContinueURL(at: indexPath)
 
+    case .linkDomain:
+      changeActionCodeLinkDomain(at: indexPath)
+
     case .requestVerifyEmail:
       requestVerifyEmail()
 
@@ -180,7 +187,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
       phoneEnroll()
 
     case .totpEnroll:
-      totpEnroll()
+      Task { await totpEnroll() }
 
     case .multifactorUnenroll:
       mfaUnenroll()
@@ -336,9 +343,10 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
   }
 
   private func performDemoEmailPasswordLoginFlow() {
-    let loginController = LoginController()
-    loginController.delegate = self
-    navigationController?.pushViewController(loginController, animated: true)
+    let loginView = LoginView(delegate: self)
+    let hostingController = UIHostingController(rootView: loginView)
+    hostingController.title = "Email/Password Auth"
+    navigationController?.pushViewController(hostingController, animated: true)
   }
 
   private func performPasswordlessLoginFlow() {
@@ -558,7 +566,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
   private func changeActionCodeContinueURL(at indexPath: IndexPath) {
     showTextInputPrompt(with: "Continue URL:", completion: { newContinueURL in
       self.actionCodeContinueURL = URL(string: newContinueURL)
-      print("Successfully set Continue URL  to: \(newContinueURL)")
+      print("Successfully set Continue URL to: \(newContinueURL)")
       self.dataSourceProvider.updateItem(
         at: indexPath,
         item: Item(
@@ -571,6 +579,22 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
     })
   }
 
+  private func changeActionCodeLinkDomain(at indexPath: IndexPath) {
+    showTextInputPrompt(with: "Link Domain:", completion: { newLinkDomain in
+      self.actionCodeLinkDomain = newLinkDomain
+      print("Successfully set Link Domain to: \(newLinkDomain)")
+      self.dataSourceProvider.updateItem(
+        at: indexPath,
+        item: Item(
+          title: AuthMenu.linkDomain.name,
+          detailTitle: self.actionCodeLinkDomain,
+          isEditable: true
+        )
+      )
+      self.tableView.reloadData()
+    })
+  }
+
   private func requestVerifyEmail() {
     showSpinner()
     let completionHandler: ((any Error)?) -> Void = { [weak self] error in
@@ -786,89 +810,53 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
     }
   }
 
-  private func totpEnroll() {
-    guard let user = AppManager.shared.auth().currentUser else {
-      print("Error: User must be logged in first.")
+  private func totpEnroll() async {
+    guard
+      let user = AppManager.shared.auth().currentUser,
+      let accountName = user.email
+    else {
+      showAlert(for: "Enrollment failed: User must be logged and have email address.")
       return
     }
 
-    user.multiFactor.getSessionWithCompletion { session, error in
-      guard let session = session, error == nil else {
-        if let error = error {
-          self.showAlert(for: "Enrollment failed")
-          print("Multi factor start enroll failed. Error: \(error.localizedDescription)")
-        } else {
-          self.showAlert(for: "Enrollment failed")
-          print("Multi factor start enroll failed with unknown error.")
-        }
+    guard let issuer = AppManager.shared.auth().app?.name else {
+      showAlert(for: "Enrollment failed: Firebase app is missing name.")
+      return
+    }
+
+    do {
+      let session = try await user.multiFactor.session()
+      let secret = try await TOTPMultiFactorGenerator.generateSecret(with: session)
+      print("Secret: " + secret.sharedSecretKey())
+
+      let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
+      guard !url.isEmpty else {
+        showAlert(for: "Enrollment failed")
+        print("Multi factor finalize enroll failed. Could not generate URL.")
         return
       }
+      secret.openInOTPApp(withQRCodeURL: url)
 
-      TOTPMultiFactorGenerator.generateSecret(with: session) { secret, error in
-        guard let secret = secret, error == nil else {
-          if let error = error {
-            self.showAlert(for: "Enrollment failed")
-            print("Error generating TOTP secret. Error: \(error.localizedDescription)")
-          } else {
-            self.showAlert(for: "Enrollment failed")
-            print("Error generating TOTP secret.")
-          }
-          return
-        }
-
-        guard let accountName = user.email, let issuer = Auth.auth().app?.name else {
-          self.showAlert(for: "Enrollment failed")
-          print("Multi factor finalize enroll failed. Could not get account details.")
-          return
-        }
-
-        DispatchQueue.main.async {
-          let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
-
-          guard !url.isEmpty else {
-            self.showAlert(for: "Enrollment failed")
-            print("Multi factor finalize enroll failed. Could not generate URL.")
-            return
-          }
-
-          secret.openInOTPApp(withQRCodeURL: url)
-
-          self
-            .showQRCodePromptWithTextInput(with: "Scan this QR code and enter OTP:",
-                                           url: url) { oneTimePassword in
-              guard !oneTimePassword.isEmpty else {
-                self.showAlert(for: "Display name must not be empty")
-                print("OTP not entered.")
-                return
-              }
+      guard
+        let oneTimePassword = await showTextInputPrompt(with: "Enter the one time passcode.")
+      else {
+        showAlert(for: "Enrollment failed: one time passcode not entered.")
+        return
+      }
 
-              let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
-                with: secret,
-                oneTimePassword: oneTimePassword
-              )
+      let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
+        with: secret,
+        oneTimePassword: oneTimePassword
+      )
 
-              self.showTextInputPrompt(with: "Display Name") { displayName in
-                guard !displayName.isEmpty else {
-                  self.showAlert(for: "Display name must not be empty")
-                  print("Display name not entered.")
-                  return
-                }
+      // TODO(nickcooke): Provide option to enter display name.
+      try await user.multiFactor.enroll(with: assertion, displayName: "TOTP")
 
-                user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
-                  if let error = error {
-                    self.showAlert(for: "Enrollment failed")
-                    print(
-                      "Multi factor finalize enroll failed. Error: \(error.localizedDescription)"
-                    )
-                  } else {
-                    self.showAlert(for: "Successfully enrolled: \(displayName)")
-                    print("Multi factor finalize enroll succeeded.")
-                  }
-                }
-              }
-            }
-        }
-      }
+      showAlert(for: "Successfully enrolled: TOTP")
+      print("Multi factor finalize enroll succeeded.")
+    } catch {
+      print(error)
+      showAlert(for: "Enrollment failed: \(error.localizedDescription)")
     }
   }
 
@@ -964,60 +952,12 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
     present(editController, animated: true, completion: nil)
   }
 
-  private func showQRCodePromptWithTextInput(with message: String, url: String,
-                                             completion: ((String) -> Void)? = nil) {
-    // Create a UIAlertController
-    let alertController = UIAlertController(
-      title: "QR Code Prompt",
-      message: message,
-      preferredStyle: .alert
-    )
-
-    // Add a text field for input
-    alertController.addTextField { textField in
-      textField.placeholder = "Enter text"
-    }
-
-    // Create a UIImage from the URL
-    guard let image = generateQRCode(from: url) else {
-      print("Failed to generate QR code")
-      return
-    }
-
-    // Create an image view to display the QR code
-    let imageView = UIImageView(image: image)
-    imageView.contentMode = .scaleAspectFit
-    imageView.translatesAutoresizingMaskIntoConstraints = false
-
-    // Add the image view to the alert controller
-    alertController.view.addSubview(imageView)
-
-    // Add constraints to position the image view
-    NSLayoutConstraint.activate([
-      imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 20),
-      imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),
-      imageView.widthAnchor.constraint(equalToConstant: 200),
-      imageView.heightAnchor.constraint(equalToConstant: 200),
-    ])
-
-    // Add actions
-    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
-    let submitAction = UIAlertAction(title: "Submit", style: .default) { _ in
-      if let completion,
-         let text = alertController.textFields?.first?.text {
-        completion(text)
+  private func showTextInputPrompt(with message: String) async -> String? {
+    await withCheckedContinuation { continuation in
+      showTextInputPrompt(with: message) { inputText in
+        continuation.resume(returning: inputText.isEmpty ? nil : inputText)
       }
     }
-
-    alertController.addAction(cancelAction)
-    alertController.addAction(submitAction)
-
-    // Present the alert controller
-    UIApplication.shared.windows.first?.rootViewController?.present(
-      alertController,
-      animated: true,
-      completion: nil
-    )
   }
 
   // Function to generate QR code from a string

+ 0 - 139
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/LoginController.swift

@@ -1,139 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import FirebaseAuth
-import UIKit
-
-class LoginController: UIViewController {
-  weak var delegate: (any LoginDelegate)?
-
-  private var loginView: LoginView { view as! LoginView }
-
-  private var email: String { loginView.emailTextField.text! }
-  private var password: String { loginView.passwordTextField.text! }
-
-  // Hides tab bar when view controller is presented
-  override var hidesBottomBarWhenPushed: Bool { get { true } set {} }
-
-  // MARK: - View Controller Lifecycle Methods
-
-  override func loadView() {
-    view = LoginView()
-  }
-
-  override func viewDidLoad() {
-    super.viewDidLoad()
-    configureNavigationBar()
-    configureDelegatesAndHandlers()
-  }
-
-  override func viewWillAppear(_ animated: Bool) {
-    super.viewWillAppear(animated)
-    navigationController?.setTitleColor(.label)
-  }
-
-  override func viewWillDisappear(_ animated: Bool) {
-    super.viewWillDisappear(animated)
-    view.endEditing(true)
-    navigationController?.setTitleColor(.systemOrange)
-  }
-
-  override func viewDidDisappear(_ animated: Bool) {
-    super.viewDidDisappear(animated)
-    navigationController?.popViewController(animated: false)
-  }
-
-  // Dismisses keyboard when view is tapped
-  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
-    super.touchesBegan(touches, with: event)
-    view.endEditing(true)
-  }
-
-  // MARK: - Firebase 🔥
-
-  private func login(with email: String, password: String) {
-    AppManager.shared.auth().signIn(withEmail: email, password: password) { result, error in
-      if let error {
-        let authError = error as NSError
-        if authError.code == AuthErrorCode.secondFactorRequired.rawValue {
-          let resolver = authError
-            .userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
-          self.delegate?.loginDidOccur(resolver: resolver)
-        } else {
-          self.displayError(error)
-        }
-      } else {
-        self.delegate?.loginDidOccur(resolver: nil)
-      }
-    }
-  }
-
-  private func createUser(email: String, password: String) {
-    AppManager.shared.auth().createUser(withEmail: email, password: password) { authResult, error in
-      guard error == nil else { return self.displayError(error) }
-      self.delegate?.loginDidOccur(resolver: nil)
-    }
-  }
-
-  // MARK: - Action Handlers
-
-  @objc
-  private func handleLogin() {
-    login(with: email, password: password)
-  }
-
-  @objc
-  private func handleCreateAccount() {
-    createUser(email: email, password: password)
-  }
-
-  // MARK: - UI Configuration
-
-  private func configureNavigationBar() {
-    navigationItem.title = "Welcome"
-    navigationItem.backBarButtonItem?.tintColor = .systemYellow
-    navigationController?.navigationBar.prefersLargeTitles = true
-  }
-
-  private func configureDelegatesAndHandlers() {
-    loginView.emailTextField.delegate = self
-    loginView.passwordTextField.delegate = self
-    loginView.loginButton.addTarget(self, action: #selector(handleLogin), for: .touchUpInside)
-    loginView.createAccountButton.addTarget(
-      self,
-      action: #selector(handleCreateAccount),
-      for: .touchUpInside
-    )
-  }
-
-  override func viewWillTransition(to size: CGSize,
-                                   with coordinator: any UIViewControllerTransitionCoordinator) {
-    super.viewWillTransition(to: size, with: coordinator)
-    loginView.emailTopConstraint.constant = UIDevice.current.orientation.isLandscape ? 15 : 50
-    loginView.passwordTopConstraint.constant = UIDevice.current.orientation.isLandscape ? 5 : 20
-  }
-}
-
-// MARK: - UITextFieldDelegate
-
-extension LoginController: UITextFieldDelegate {
-  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
-    if loginView.emailTextField.isFirstResponder, loginView.passwordTextField.text!.isEmpty {
-      loginView.passwordTextField.becomeFirstResponder()
-    } else {
-      textField.resignFirstResponder()
-    }
-    return true
-  }
-}

+ 5 - 1
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/PasswordlessViewController.swift

@@ -31,7 +31,10 @@ class PasswordlessViewController: OtherAuthViewController {
 
   // MARK: - Firebase 🔥
 
-  private let authorizedDomain: String = "ENTER AUTHORIZED DOMAIN"
+  private let authorizedDomain: String =
+    "fir-ios-auth-sample.firebaseapp.com" // Enter AUTHORIZED_DOMAIN
+  private let customDomain: String =
+    "firebaseiosauthsample.testdomaindonotuse.com" // Enter AUTHORIZED_HOSTING_DOMAIN
 
   private func sendSignInLink(to email: String) {
     let actionCodeSettings = ActionCodeSettings()
@@ -42,6 +45,7 @@ class PasswordlessViewController: OtherAuthViewController {
     // The sign-in operation must be completed in the app.
     actionCodeSettings.handleCodeInApp = true
     actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
+    actionCodeSettings.linkDomain = customDomain
 
     AppManager.shared.auth()
       .sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in

+ 83 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift

@@ -226,6 +226,89 @@ class AuthenticationExampleUITests: XCTestCase {
     removeUIInterruptionMonitor(interruptionMonitor)
   }
 
+  func testEmailLinkSentSuccessfully() {
+    app.staticTexts["Email Link/Passwordless"].tap()
+
+    let testEmail = "test@test.com"
+    app.textFields["Enter Authentication Email"].tap()
+    app.textFields["Enter Authentication Email"].typeText(testEmail)
+    app.buttons["return"].tap() // Dismiss keyboard
+    app.buttons["Send Sign In Link"].tap()
+
+    // Wait for the error message to appear (if there is an error)
+    let errorAlert = app.alerts.staticTexts["Error"]
+    let errorExists = errorAlert.waitForExistence(timeout: 5.0)
+
+    app.swipeDown(velocity: .fast)
+
+    // Assert that there is no error message (success case)
+    // The email sign in link is sent successfully if no error message appears
+    XCTAssertFalse(errorExists, "Error")
+
+    // Go back and check that there is no user that is signed in
+    app.tabBars.firstMatch.buttons.element(boundBy: 1).tap()
+    wait(forElement: app.navigationBars["User"], timeout: 5.0)
+    XCTAssertEqual(
+      app.cells.count,
+      0,
+      "The user shouldn't be signed in and the user view should have no cells."
+    )
+  }
+
+  func testResetPasswordLinkCustomDomain() {
+    // assuming action type is in-app + continue URL everytime the app launches
+
+    // set Authorized Domain as Continue URL
+    let testContinueURL = "fir-ios-auth-sample.firebaseapp.com"
+    app.staticTexts["Continue URL"].tap()
+    app.alerts.textFields.element.typeText(testContinueURL)
+    app.buttons["Save"].tap()
+
+    // set Custom Hosting Domain as Link Domain
+    let testLinkDomain = "http://firebaseiosauthsample.testdomaindonotuse.com"
+    app.staticTexts["Link Domain"].tap()
+    app.alerts.textFields.element.typeText(testLinkDomain)
+    app.buttons["Save"].tap()
+
+    app.staticTexts["Request Password Reset"].tap()
+    let testEmail = "test@test.com"
+    app.alerts.textFields.element.typeText(testEmail)
+    app.buttons["Save"].tap()
+
+    // Go back and check that there is no user that is signed in
+    app.tabBars.firstMatch.buttons.element(boundBy: 1).tap()
+    wait(forElement: app.navigationBars["User"], timeout: 5.0)
+    XCTAssertEqual(
+      app.cells.count,
+      0,
+      "The user shouldn't be signed in and the user view should have no cells."
+    )
+  }
+
+  func testResetPasswordLinkDefaultDomain() {
+    // assuming action type is in-app + continue URL everytime the app launches
+
+    // set Authorized Domain as Continue URL
+    let testContinueURL = "fir-ios-auth-sample.firebaseapp.com"
+    app.staticTexts["Continue URL"].tap()
+    app.alerts.textFields.element.typeText(testContinueURL)
+    app.buttons["Save"].tap()
+
+    app.staticTexts["Request Password Reset"].tap()
+    let testEmail = "test@test.com"
+    app.alerts.textFields.element.typeText(testEmail)
+    app.buttons["Save"].tap()
+
+    // Go back and check that there is no user that is signed in
+    app.tabBars.firstMatch.buttons.element(boundBy: 1).tap()
+    wait(forElement: app.navigationBars["User"], timeout: 5.0)
+    XCTAssertEqual(
+      app.cells.count,
+      0,
+      "The user shouldn't be signed in and the user view should have no cells."
+    )
+  }
+
   // MARK: - Private Helpers
 
   private func signOut() {

+ 45 - 0
FirebaseAuth/Tests/Unit/AuthBackendTests.swift

@@ -372,6 +372,51 @@ class AuthBackendTests: RPCBaseTests {
     }
   }
 
+  /// Test Blocking Function Error Response flow
+  func testBlockingFunctionError() async throws {
+    let kErrorMessageBlocking = "BLOCKING_FUNCTION_ERROR_RESPONSE"
+    let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
+    let request = FakeRequest(withRequestBody: [:])
+    rpcIssuer.respondBlock = {
+      try self.rpcIssuer.respond(serverErrorMessage: kErrorMessageBlocking, error: responseError)
+    }
+    do {
+      let _ = try await authBackend.call(with: request)
+      XCTFail("Expected to throw")
+    } catch {
+      let rpcError = error as NSError
+      XCTAssertEqual(rpcError.domain, AuthErrors.domain)
+      XCTAssertEqual(rpcError.code, AuthErrorCode.blockingCloudFunctionError.rawValue)
+    }
+  }
+
+  /// Test Blocking Function Error Response flow - including JSON parsing.
+  /// Regression Test for #14052
+  func testBlockingFunctionErrorWithJSON() async throws {
+    let kErrorMessageBlocking = "BLOCKING_FUNCTION_ERROR_RESPONSE"
+    let stringWithJSON = "BLOCKING_FUNCTION_ERROR_RESPONSE : ((HTTP request to" +
+      "http://127.0.0.1:9999/project-id/us-central1/beforeUserCreated returned HTTP error 400:" +
+      " {\"error\":{\"details\":{\"code\":\"invalid-email\"},\"message\":\"invalid " +
+      "email\",\"status\":\"INVALID_ARGUMENT\"}}))"
+    let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
+    let request = FakeRequest(withRequestBody: [:])
+    rpcIssuer.respondBlock = {
+      try self.rpcIssuer.respond(
+        serverErrorMessage: kErrorMessageBlocking + " : " + stringWithJSON,
+        error: responseError
+      )
+    }
+    do {
+      let _ = try await authBackend.call(with: request)
+      XCTFail("Expected to throw")
+    } catch {
+      let rpcError = error as NSError
+      XCTAssertEqual(rpcError.domain, AuthErrors.domain)
+      XCTAssertEqual(rpcError.code, AuthErrorCode.blockingCloudFunctionError.rawValue)
+      XCTAssertEqual(rpcError.localizedDescription, "invalid email")
+    }
+  }
+
   /** @fn testDecodableErrorResponseWithUnknownMessage
       @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
           response deserialized by @c NSJSONSerialization represents a valid error response (and an

+ 3 - 0
FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift

@@ -34,6 +34,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests {
   private let kAndroidMinimumVersionKey = "androidMinimumVersion"
   private let kCanHandleCodeInAppKey = "canHandleCodeInApp"
   private let kDynamicLinkDomainKey = "dynamicLinkDomain"
+  private let kLinkDomainKey = "linkDomain"
   private let kExpectedAPIURL =
     "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=APIKey"
   private let kOOBCodeKey = "oobCode"
@@ -66,6 +67,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests {
       XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true)
       XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true)
       XCTAssertEqual(decodedRequest[kDynamicLinkDomainKey] as? String, kDynamicLinkDomain)
+      XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain)
     }
   }
 
@@ -110,6 +112,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests {
       XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true)
       XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true)
       XCTAssertEqual(decodedRequest[kDynamicLinkDomainKey] as? String, kDynamicLinkDomain)
+      XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain)
       XCTAssertEqual(decodedRequest[kCaptchaResponseKey] as? String, kTestCaptchaResponse)
       XCTAssertEqual(decodedRequest[kClientTypeKey] as? String, kTestClientType)
       XCTAssertEqual(decodedRequest[kRecaptchaVersionKey] as? String, kTestRecaptchaVersion)

+ 2 - 0
FirebaseAuth/Tests/Unit/ObjCAPITests.m

@@ -65,6 +65,7 @@
   s = [codeSettings androidPackageName];
   s = [codeSettings androidMinimumVersion];
   s = [codeSettings dynamicLinkDomain];
+  s = [codeSettings linkDomain];
 }
 
 - (void)FIRAuthAdditionalUserInfo_h:(FIRAdditionalUserInfo *)additionalUserInfo {
@@ -280,6 +281,7 @@
   c = FIRAuthErrorCodeTenantIDMismatch;
   c = FIRAuthErrorCodeUnsupportedTenantOperation;
   c = FIRAuthErrorCodeInvalidDynamicLinkDomain;
+  c = FIRAuthErrorCodeInvalidHostingLinkDomain;
   c = FIRAuthErrorCodeRejectedCredential;
   c = FIRAuthErrorCodeGameKitNotLinked;
   c = FIRAuthErrorCodeSecondFactorRequired;

+ 2 - 0
FirebaseAuth/Tests/Unit/RPCBaseTests.swift

@@ -38,6 +38,7 @@ class RPCBaseTests: XCTestCase {
   let kAndroidPackageName = "androidpackagename"
   let kAndroidMinimumVersion = "3.0"
   let kDynamicLinkDomain = "test.page.link"
+  let kLinkDomain = "link.firebaseapp.com"
   let kTestPhotoURL = "https://host.domain/image"
   let kCreationDateTimeIntervalInSeconds = 1_505_858_500.0
   let kLastSignInDateTimeIntervalInSeconds = 1_505_858_583.0
@@ -304,6 +305,7 @@ class RPCBaseTests: XCTestCase {
     settings.handleCodeInApp = true
     settings.url = URL(string: kContinueURL)
     settings.dynamicLinkDomain = kDynamicLinkDomain
+    settings.linkDomain = kLinkDomain
     return settings
   }
 

+ 5 - 1
FirebaseAuth/Tests/Unit/SwiftAPI.swift

@@ -41,7 +41,10 @@ class AuthAPI_hOnlyTests: XCTestCase {
        let _: String = codeSettings.iOSBundleID,
        let _: String = codeSettings.androidPackageName,
        let _: String = codeSettings.androidMinimumVersion,
-       let _: String = codeSettings.dynamicLinkDomain {}
+       let _: String = codeSettings.dynamicLinkDomain,
+       let _: String = codeSettings.linkDomain {}
+    codeSettings.linkDomain = nil
+    codeSettings.linkDomain = ""
   }
 
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
@@ -276,6 +279,7 @@ class AuthAPI_hOnlyTests: XCTestCase {
     _ = AuthErrorCode.tenantIDMismatch
     _ = AuthErrorCode.unsupportedTenantOperation
     _ = AuthErrorCode.invalidDynamicLinkDomain
+    _ = AuthErrorCode.invalidHostingLinkDomain
     _ = AuthErrorCode.rejectedCredential
     _ = AuthErrorCode.gameKitNotLinked
     _ = AuthErrorCode.secondFactorRequired

+ 1 - 1
FirebaseAuthInterop.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseAuthInterop'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Interfaces that allow other Firebase SDKs to use Auth functionality.'
 
   s.description      = <<-DESC

+ 2 - 2
FirebaseCombineSwift.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseCombineSwift'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Swift extensions with Combine support for Firebase'
 
   s.description      = <<-DESC
@@ -51,7 +51,7 @@ for internal testing only. It should not be published.
   s.osx.framework = 'AppKit'
   s.tvos.framework = 'UIKit'
 
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'FirebaseAuth', '~> 11.0'
   s.dependency 'FirebaseFunctions', '~> 11.0'
   s.dependency 'FirebaseFirestore', '~> 11.0'

+ 2 - 2
FirebaseCore.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseCore'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Core'
 
   s.description      = <<-DESC
@@ -53,7 +53,7 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration
   # Remember to also update version in `cmake/external/GoogleUtilities.cmake`
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'
   s.dependency 'GoogleUtilities/Logger', '~> 8.0'
-  s.dependency 'FirebaseCoreInternal', '~> 11.7.0'
+  s.dependency 'FirebaseCoreInternal', '~> 11.8.0'
 
   s.pod_target_xcconfig = {
     'GCC_C_LANGUAGE_STANDARD' => 'c99',

+ 2 - 2
FirebaseCoreExtension.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
     s.name             = 'FirebaseCoreExtension'
-    s.version          = '11.7.0'
+    s.version          = '11.8.0'
     s.summary          = 'Extended FirebaseCore APIs for Firebase product SDKs'
 
     s.description      = <<-DESC
@@ -34,5 +34,5 @@ Pod::Spec.new do |s|
       "#{s.module_name}_Privacy" => 'FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy'
     }
 
-    s.dependency 'FirebaseCore', '~> 11.7.0'
+    s.dependency 'FirebaseCore', '~> 11.8.0'
   end

+ 1 - 1
FirebaseCoreInternal.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseCoreInternal'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'APIs for internal FirebaseCore usage.'
 
   s.description      = <<-DESC

+ 2 - 2
FirebaseCrashlytics.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseCrashlytics'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Best and lightest-weight crash reporting for mobile, desktop and tvOS.'
   s.description      = 'Firebase Crashlytics helps you track, prioritize, and fix stability issues that erode app quality.'
   s.homepage         = 'https://firebase.google.com/'
@@ -59,7 +59,7 @@ Pod::Spec.new do |s|
     cp -f ./Crashlytics/CrashlyticsInputFiles.xcfilelist ./CrashlyticsInputFiles.xcfilelist
   PREPARE_COMMAND_END
 
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'FirebaseInstallations', '~> 11.0'
   s.dependency 'FirebaseSessions', '~> 11.0'
   s.dependency 'FirebaseRemoteConfigInterop', '~> 11.0'

+ 2 - 2
FirebaseDatabase.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseDatabase'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Realtime Database'
 
   s.description      = <<-DESC
@@ -47,7 +47,7 @@ Simplify your iOS development, grow your user base, and monetize more effectivel
   s.macos.frameworks = 'CFNetwork', 'Security', 'SystemConfiguration'
   s.watchos.frameworks = 'CFNetwork', 'Security', 'WatchKit'
   s.dependency 'leveldb-library', '~> 1.22'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'FirebaseAppCheckInterop', '~> 11.0'
   s.dependency 'FirebaseSharedSwift', '~> 11.0'
   s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0'

+ 5 - 2
FirebaseDynamicLinks.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseDynamicLinks'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Dynamic Links'
 
   s.description      = <<-DESC
@@ -20,6 +20,9 @@ Firebase Dynamic Links are deep links that enhance user experience and increase
 
   s.swift_version = '5.9'
 
+  # See https://firebase.google.com/support/dynamic-links-faq
+  s.deprecated = true
+
   s.cocoapods_version = '>= 1.12.0'
   s.prefix_header_file = false
 
@@ -34,7 +37,7 @@ Firebase Dynamic Links are deep links that enhance user experience and increase
   }
   s.frameworks = 'QuartzCore'
   s.weak_framework = 'WebKit'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
 
   s.pod_target_xcconfig = {
     'GCC_C_LANGUAGE_STANDARD' => 'c99',

+ 4 - 1
FirebaseDynamicLinks/CHANGELOG.md

@@ -1,5 +1,8 @@
+# 11.8.0
+- [deprecated] The `FirebaseDynamicLinks` CocoaPod is deprecated. For information about timelines and alternatives, see the [Dynamic Links deprecation FAQ](https://firebase.google.com/support/dynamic-links-faq).
+
 # 10.27.0
-- [added] Added deprecation warning in advance of August 25, 2025 Dynamic Links service shutdown. (#12995)
+- [deprecated] Dynamic Links is deprecated. For information about timelines and alternatives, see the [Dynamic Links deprecation FAQ](https://firebase.google.com/support/dynamic-links-faq)
 
 # 10.3.0
 - [fixed] Fixes issue where `utmParametersDictionary` / `minimumAppVersion` were not provided and their value were set to `[NSNull null]` instead of `nil`.

+ 4 - 4
FirebaseFirestore.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseFirestore'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Google Cloud Firestore'
   s.description      = <<-DESC
 Google Cloud Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development.
@@ -35,9 +35,9 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling,
     "#{s.module_name}_Privacy" => 'Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy'
   }
 
-  s.dependency 'FirebaseCore', '~> 11.7.0'
-  s.dependency 'FirebaseCoreExtension', '~> 11.7.0'
-  s.dependency 'FirebaseFirestoreInternal', '11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
+  s.dependency 'FirebaseCoreExtension', '~> 11.8.0'
+  s.dependency 'FirebaseFirestoreInternal', '11.8.0'
   s.dependency 'FirebaseSharedSwift', '~> 11.0'
 
 end

+ 2 - 2
FirebaseFirestoreInternal.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseFirestoreInternal'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Google Cloud Firestore'
 
   s.description      = <<-DESC
@@ -93,7 +93,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling,
   }
 
   s.dependency 'FirebaseAppCheckInterop', '~> 11.0'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
 
   abseil_version = '~> 1.20240116.1'
   s.dependency 'abseil/algorithm', abseil_version

+ 3 - 3
FirebaseFunctions.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseFunctions'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Cloud Functions for Firebase'
 
   s.description      = <<-DESC
@@ -35,8 +35,8 @@ Cloud Functions for Firebase.
     'FirebaseFunctions/Sources/**/*.swift',
   ]
 
-  s.dependency 'FirebaseCore', '~> 11.7.0'
-  s.dependency 'FirebaseCoreExtension', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
+  s.dependency 'FirebaseCoreExtension', '~> 11.8.0'
   s.dependency 'FirebaseAppCheckInterop', '~> 11.0'
   s.dependency 'FirebaseAuthInterop', '~> 11.0'
   s.dependency 'FirebaseMessagingInterop', '~> 11.0'

+ 50 - 15
FirebaseFunctions/Backend/index.js

@@ -13,9 +13,10 @@
 // limitations under the License.
 
 const assert = require('assert');
-const functions = require('firebase-functions');
+const functionsV1 = require('firebase-functions/v1');
+const functionsV2 = require('firebase-functions/v2');
 
-exports.dataTest = functions.https.onRequest((request, response) => {
+exports.dataTest = functionsV1.https.onRequest((request, response) => {
   assert.deepEqual(request.body, {
     data: {
       bool: true,
@@ -41,39 +42,39 @@ exports.dataTest = functions.https.onRequest((request, response) => {
   });
 });
 
-exports.scalarTest = functions.https.onRequest((request, response) => {
+exports.scalarTest = functionsV1.https.onRequest((request, response) => {
   assert.deepEqual(request.body, { data: 17 });
   response.send({ data: 76 });
 });
 
-exports.tokenTest = functions.https.onRequest((request, response) => {
+exports.tokenTest = functionsV1.https.onRequest((request, response) => {
   assert.equal('Bearer token', request.get('Authorization'));
   assert.deepEqual(request.body, { data: {} });
   response.send({ data: {} });
 });
 
-exports.FCMTokenTest = functions.https.onRequest((request, response) => {
+exports.FCMTokenTest = functionsV1.https.onRequest((request, response) => {
   assert.equal(request.get('Firebase-Instance-ID-Token'), 'fakeFCMToken');
   assert.deepEqual(request.body, { data: {} });
   response.send({ data: {} });
 });
 
-exports.nullTest = functions.https.onRequest((request, response) => {
+exports.nullTest = functionsV1.https.onRequest((request, response) => {
   assert.deepEqual(request.body, { data: null });
   response.send({ data: null });
 });
 
-exports.missingResultTest = functions.https.onRequest((request, response) => {
+exports.missingResultTest = functionsV1.https.onRequest((request, response) => {
   assert.deepEqual(request.body, { data: null });
   response.send({});
 });
 
-exports.unhandledErrorTest = functions.https.onRequest((request, response) => {
+exports.unhandledErrorTest = functionsV1.https.onRequest((request, response) => {
   // Fail in a way that the client shouldn't see.
   throw 'nope';
 });
 
-exports.unknownErrorTest = functions.https.onRequest((request, response) => {
+exports.unknownErrorTest = functionsV1.https.onRequest((request, response) => {
   // Send an http error with a body with an explicit code.
   response.status(400).send({
     error: {
@@ -83,7 +84,7 @@ exports.unknownErrorTest = functions.https.onRequest((request, response) => {
   });
 });
 
-exports.explicitErrorTest = functions.https.onRequest((request, response) => {
+exports.explicitErrorTest = functionsV1.https.onRequest((request, response) => {
   // Send an http error with a body with an explicit code.
   // Note that eventually the SDK will have a helper to automatically return
   // the appropriate http status code for an error.
@@ -103,18 +104,52 @@ exports.explicitErrorTest = functions.https.onRequest((request, response) => {
   });
 });
 
-exports.httpErrorTest = functions.https.onRequest((request, response) => {
+exports.httpErrorTest = functionsV1.https.onRequest((request, response) => {
   // Send an http error with no body.
   response.status(400).send();
 });
 
 // Regression test for https://github.com/firebase/firebase-ios-sdk/issues/9855
-exports.throwTest = functions.https.onCall((data) => {
-  throw new functions.https.HttpsError('invalid-argument', 'Invalid test requested.');
+exports.throwTest = functionsV1.https.onCall((data) => {
+  throw new functionsV1.https.HttpsError('invalid-argument', 'Invalid test requested.');
 });
 
-exports.timeoutTest = functions.https.onRequest((request, response) => {
+exports.timeoutTest = functionsV1.https.onRequest((request, response) => {
   // Wait for longer than 500ms.
-  setTimeout(() => response.send({data: true}), 500);
+  setTimeout(() => response.send({ data: true }), 500);
 });
 
+const streamData = ["hello", "world", "this", "is", "cool"]
+
+function sleep(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+};
+
+async function* generateText() {
+  for (const chunk of streamData) {
+    yield chunk;
+    await sleep(1000);
+  }
+};
+
+exports.genStream = functionsV2.https.onCall(
+  async (request, response) => {
+    if (request.acceptsStreaming) {
+      for await (const chunk of generateText()) {
+        response.sendChunk({ chunk });
+      }
+    }
+    return streamData.join(" ");
+  }
+);
+
+exports.genStreamError = functionsV2.https.onCall(
+  async (request, response) => {
+    if (request.acceptsStreaming) {
+      for await (const chunk of generateText()) {
+        response.write({ chunk });
+      }
+      throw Error("BOOM")
+    }
+  }
+);

+ 5 - 2
FirebaseFunctions/Backend/package.json

@@ -2,8 +2,11 @@
   "name": "functions",
   "description": "Cloud Functions for Firebase",
   "dependencies": {
-    "firebase-admin": "^9.2.0",
-    "firebase-functions": "^3.0.0"
+    "firebase-admin": "^13.0.0",
+    "firebase-functions": "^6.2.0"
+  },
+  "engines": {
+    "node": "22"
   },
   "private": true,
   "devDependencies": {

+ 2 - 0
FirebaseFunctions/Backend/start.sh

@@ -55,6 +55,8 @@ FUNCTIONS_BIN="./node_modules/.bin/functions"
 "${FUNCTIONS_BIN}" deploy httpErrorTest --trigger-http
 "${FUNCTIONS_BIN}" deploy throwTest --trigger-http
 "${FUNCTIONS_BIN}" deploy timeoutTest --trigger-http
+"${FUNCTIONS_BIN}" deploy genStream --trigger-http
+"${FUNCTIONS_BIN}" deploy genStreamError --trigger-http
 
 if [ "$1" != "synchronous" ]; then
   # Wait for the user to tell us to stop the server.

+ 2 - 2
FirebaseInAppMessaging.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseInAppMessaging'
-  s.version          = '11.7.0-beta'
+  s.version          = '11.8.0-beta'
   s.summary          = 'Firebase In-App Messaging for iOS'
 
   s.description      = <<-DESC
@@ -80,7 +80,7 @@ See more product details at https://firebase.google.com/products/in-app-messagin
 
   s.framework = 'UIKit'
 
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'FirebaseInstallations', '~> 11.0'
   s.dependency 'FirebaseABTesting', '~> 11.0'
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'

+ 2 - 2
FirebaseInstallations.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseInstallations'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Installations'
 
   s.description      = <<-DESC
@@ -45,7 +45,7 @@ Pod::Spec.new do |s|
   }
 
   s.framework = 'Security'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'PromisesObjC', '~> 2.4'
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'
   s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0'

+ 3 - 3
FirebaseMLModelDownloader.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseMLModelDownloader'
-  s.version          = '11.7.0-beta'
+  s.version          = '11.8.0-beta'
   s.summary          = 'Firebase ML Model Downloader'
 
   s.description      = <<-DESC
@@ -36,8 +36,8 @@ Pod::Spec.new do |s|
   ]
 
   s.framework = 'Foundation'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
-  s.dependency 'FirebaseCoreExtension', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
+  s.dependency 'FirebaseCoreExtension', '~> 11.8.0'
   s.dependency 'FirebaseInstallations', '~> 11.0'
   s.dependency 'GoogleDataTransport', '~> 10.0'
   s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0'

+ 2 - 2
FirebaseMessaging.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseMessaging'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Messaging'
 
   s.description      = <<-DESC
@@ -62,7 +62,7 @@ device, and it is completely free.
   s.osx.framework = 'SystemConfiguration'
   s.weak_framework = 'UserNotifications'
   s.dependency 'FirebaseInstallations', '~> 11.0'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0'
   s.dependency 'GoogleUtilities/Reachability', '~> 8.0'
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'

+ 1 - 1
FirebaseMessaging/Apps/README.md

@@ -7,7 +7,7 @@ the sample code. The one inside the AdvancedSample folder is a test app
 based on the test app in the Sample folder. Both Apps share most of the
 code in the main target. The AdvancedSample app provides advanced app
 extensions features, such as Notification Service Extension, Shared
-Extension, App Clips, Live Activites and a watchOS app. The advanced sample app has more
+Extension, App Clips, Live Activities and a watchOS app. The advanced sample app has more
 targets than the Sample app and requires your own provisioning profiles and
 certificates for each extension target to be setup so that it can be run on
 a real device.

+ 5 - 5
FirebaseMessaging/Apps/Shared/LiveActivityView.swift

@@ -66,14 +66,14 @@ struct LiveActivityView: View {
       let ptsToken = Activity<SampleLiveActivityAttributes>.pushToStartToken
 
       if ptsToken != nil {
-        let ptsTokenString = getFormatedToken(token: ptsToken!)
+        let ptsTokenString = getFormattedToken(token: ptsToken!)
         activityTokenDict["PTS"] = ptsTokenString
       } else {
         activityTokenDict["PTS"] = "Not available yet.!"
         Task {
           for await ptsToken in Activity<SampleLiveActivityAttributes>
             .pushToStartTokenUpdates {
-            let ptsTokenString = getFormatedToken(token: ptsToken)
+            let ptsTokenString = getFormattedToken(token: ptsToken)
             activityTokenDict["PTS"] = ptsTokenString
             refreshAcitivtyList()
           }
@@ -83,7 +83,7 @@ struct LiveActivityView: View {
       let activities = Activity<SampleLiveActivityAttributes>.activities
       for activity in activities {
         if activity.pushToken != nil {
-          let activityToken = getFormatedToken(token: activity.pushToken!)
+          let activityToken = getFormattedToken(token: activity.pushToken!)
           activityTokenDict[activity.id] = activityToken
         } else {
           activityTokenDict[activity.id] = "Not available yet!"
@@ -92,7 +92,7 @@ struct LiveActivityView: View {
     }
   }
 
-  func getFormatedToken(token: Data) -> String {
+  func getFormattedToken(token: Data) -> String {
     return token.reduce("") {
       $0 + String(format: "%02x", $1)
     }
@@ -114,7 +114,7 @@ struct LiveActivityView: View {
       if activity != nil {
         Task {
           for await pushToken in activity!.pushTokenUpdates {
-            let activityToken = getFormatedToken(token: pushToken)
+            let activityToken = getFormattedToken(token: pushToken)
             activityTokenDict[activity!.id] = activityToken
             refreshAcitivtyList()
           }

+ 3 - 0
FirebaseMessaging/CHANGELOG.md

@@ -1,3 +1,6 @@
+# 11.8.0
+- [fixed] Don't cache FCM registration token operations. (#14352).
+
 # 11.5.0
 - [fixed] Improve token-fetch failure logging with detailed error info. (#13997).
 

+ 1 - 1
FirebaseMessaging/Sources/Token/FIRMessagingTokenDeleteOperation.m

@@ -77,7 +77,7 @@
         [self handleResponseWithData:data response:response error:error];
       };
 
-  NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
+  NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
   config.timeoutIntervalForResource = 60.0f;  // 1 minute
   NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
   self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler];

+ 1 - 1
FirebaseMessaging/Sources/Token/FIRMessagingTokenFetchOperation.m

@@ -108,7 +108,7 @@ NSString *const kFIRMessagingFirebaseHeartbeatKey = @"X-firebase-client-log-type
         FIRMessaging_STRONGIFY(self);
         [self handleResponseWithData:data response:response error:error];
       };
-  NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
+  NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
   config.timeoutIntervalForResource = 60.0f;  // 1 minute
   NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
   self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler];

+ 1 - 1
FirebaseMessagingInterop.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseMessagingInterop'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Interfaces that allow other Firebase SDKs to use Messaging functionality.'
 
   s.description      = <<-DESC

+ 2 - 2
FirebasePerformance.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebasePerformance'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Performance'
 
   s.description      = <<-DESC
@@ -59,7 +59,7 @@ Firebase Performance library to measure performance of Mobile and Web Apps.
   s.ios.framework = 'CoreTelephony'
   s.framework = 'QuartzCore'
   s.framework = 'SystemConfiguration'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'FirebaseInstallations', '~> 11.0'
   s.dependency 'FirebaseRemoteConfig', '~> 11.0'
   s.dependency 'FirebaseSessions', '~> 11.0'

+ 2 - 2
FirebaseRemoteConfig.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseRemoteConfig'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Remote Config'
 
   s.description      = <<-DESC
@@ -52,7 +52,7 @@ app update.
   }
   s.dependency 'FirebaseABTesting', '~> 11.0'
   s.dependency 'FirebaseSharedSwift', '~> 11.0'
-  s.dependency 'FirebaseCore', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
   s.dependency 'FirebaseInstallations', '~> 11.0'
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'
   s.dependency 'GoogleUtilities/NSData+zlib', '~> 8.0'

+ 9 - 1
FirebaseRemoteConfig/CHANGELOG.md

@@ -1,5 +1,13 @@
-# Unreleased
+# 11.8.0
+- [fixed] Mark completion handlers as Sendable in RemoteConfig class.
+  Some completions handlers were missed in the 11.7.0 update. (#14257)
+
+# 11.7.0
 - [fixed] Mark ConfigUpdateListenerRegistration Sendable. (#14215)
+- [fixed] Mark completion handlers as Sendable in RemoteConfig class. (#14257)
+- [feature] Added support for custom signal targeting in Remote Config. Use
+  `setCustomSignals` API for setting custom signals and use them to build
+  custom targeting conditions in Remote Config. (#13976)
 
 # 11.5.0
 - [fixed] Mark two internal properties as `atomic` to prevent concurrency

+ 106 - 0
FirebaseRemoteConfig/Sources/FIRRemoteConfig.m

@@ -34,6 +34,9 @@
 /// Remote Config Error Domain.
 /// TODO: Rename according to obj-c style for constants.
 NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain";
+// Remote Config Custom Signals Error Domain
+NSString *const FIRRemoteConfigCustomSignalsErrorDomain =
+    @"com.google.remoteconfig.customsignals.ErrorDomain";
 // Remote Config Realtime Error Domain
 NSString *const FIRRemoteConfigUpdateErrorDomain = @"com.google.remoteconfig.update.ErrorDomain";
 /// Remote Config Error Info End Time Seconds;
@@ -47,6 +50,12 @@ const NSNotificationName FIRRemoteConfigActivateNotification =
     @"FIRRemoteConfigActivateNotification";
 static NSNotificationName FIRRolloutsStateDidChangeNotificationName =
     @"FIRRolloutsStateDidChangeNotification";
+/// Maximum allowed length for a custom signal key (in characters).
+static const NSUInteger FIRRemoteConfigCustomSignalsMaxKeyLength = 250;
+/// Maximum allowed length for a string value in custom signals (in characters).
+static const NSUInteger FIRRemoteConfigCustomSignalsMaxStringValueLength = 500;
+/// Maximum number of custom signals allowed.
+static const NSUInteger FIRRemoteConfigCustomSignalsMaxCount = 100;
 
 /// Listener for the get methods.
 typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull);
@@ -237,6 +246,103 @@ static NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, FIRRemote
   }
 }
 
+- (void)setCustomSignals:(nonnull NSDictionary<NSString *, NSObject *> *)customSignals
+          withCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler {
+  void (^setCustomSignalsBlock)(void) = ^{
+    // Validate value type, and key and value length
+    for (NSString *key in customSignals) {
+      NSObject *value = customSignals[key];
+      if (![value isKindOfClass:[NSNull class]] && ![value isKindOfClass:[NSString class]] &&
+          ![value isKindOfClass:[NSNumber class]]) {
+        if (completionHandler) {
+          dispatch_async(dispatch_get_main_queue(), ^{
+            NSError *error =
+                [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
+                                    code:FIRRemoteConfigCustomSignalsErrorInvalidValueType
+                                userInfo:@{
+                                  NSLocalizedDescriptionKey :
+                                      @"Invalid value type. Must be NSString, NSNumber or NSNull"
+                                }];
+            completionHandler(error);
+          });
+        }
+        return;
+      }
+
+      if (key.length > FIRRemoteConfigCustomSignalsMaxKeyLength ||
+          ([value isKindOfClass:[NSString class]] &&
+           [(NSString *)value length] > FIRRemoteConfigCustomSignalsMaxStringValueLength)) {
+        if (completionHandler) {
+          dispatch_async(dispatch_get_main_queue(), ^{
+            NSError *error = [NSError
+                errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
+                           code:FIRRemoteConfigCustomSignalsErrorLimitExceeded
+                       userInfo:@{
+                         NSLocalizedDescriptionKey : [NSString
+                             stringWithFormat:@"Custom signal keys and string values must be "
+                                              @"%lu and %lu characters or less respectively.",
+                                              FIRRemoteConfigCustomSignalsMaxKeyLength,
+                                              FIRRemoteConfigCustomSignalsMaxStringValueLength]
+                       }];
+            completionHandler(error);
+          });
+        }
+        return;
+      }
+    }
+
+    // Merge new signals with existing ones, overwriting existing keys.
+    // Also, remove entries where the new value is null.
+    NSMutableDictionary<NSString *, NSString *> *newCustomSignals =
+        [[NSMutableDictionary alloc] initWithDictionary:self->_settings.customSignals];
+
+    for (NSString *key in customSignals) {
+      NSObject *value = customSignals[key];
+      if (![value isKindOfClass:[NSNull class]]) {
+        NSString *stringValue = [value isKindOfClass:[NSNumber class]]
+                                    ? [(NSNumber *)value stringValue]
+                                    : (NSString *)value;
+        [newCustomSignals setObject:stringValue forKey:key];
+      } else {
+        [newCustomSignals removeObjectForKey:key];
+      }
+    }
+
+    // Check the size limit.
+    if (newCustomSignals.count > FIRRemoteConfigCustomSignalsMaxCount) {
+      if (completionHandler) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+          NSError *error = [NSError
+              errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
+                         code:FIRRemoteConfigCustomSignalsErrorLimitExceeded
+                     userInfo:@{
+                       NSLocalizedDescriptionKey : [NSString
+                           stringWithFormat:@"Custom signals count exceeds the limit of %lu.",
+                                            FIRRemoteConfigCustomSignalsMaxCount]
+                     }];
+          completionHandler(error);
+        });
+      }
+      return;
+    }
+
+    // Update only if there are changes.
+    if (![newCustomSignals isEqualToDictionary:self->_settings.customSignals]) {
+      self->_settings.customSignals = newCustomSignals;
+    }
+    // Log the keys of the updated custom signals.
+    FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000078", @"Keys of updated custom signals: %@",
+                [newCustomSignals allKeys]);
+
+    if (completionHandler) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        completionHandler(nil);
+      });
+    }
+  };
+  dispatch_async(_queue, setCustomSignalsBlock);
+}
+
 #pragma mark - fetch
 
 - (void)fetchWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler {

+ 5 - 0
FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h

@@ -81,6 +81,11 @@
 /// Last active template version.
 @property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion;
 
+#pragma mark - Custom Signals
+
+/// A dictionary to hold custom signals that are set by the developer.
+@property(nonatomic, readwrite, strong) NSDictionary<NSString *, NSString *> *customSignals;
+
 #pragma mark Throttling properties
 
 /// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff

+ 106 - 0
FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h

@@ -97,6 +97,19 @@ typedef NS_ERROR_ENUM(FIRRemoteConfigUpdateErrorDomain, FIRRemoteConfigUpdateErr
     FIRRemoteConfigUpdateErrorUnavailable = 8004,
 } NS_SWIFT_NAME(RemoteConfigUpdateError);
 
+/// Error domain for custom signals errors.
+extern NSString *const _Nonnull FIRRemoteConfigCustomSignalsErrorDomain NS_SWIFT_NAME(RemoteConfigCustomSignalsErrorDomain);
+
+/// Firebase Remote Config custom signals error.
+typedef NS_ERROR_ENUM(FIRRemoteConfigCustomSignalsErrorDomain, FIRRemoteConfigCustomSignalsError){
+    /// Unknown error.
+    FIRRemoteConfigCustomSignalsErrorUnknown = 8101,
+    /// Invalid value type in the custom signals dictionary.
+    FIRRemoteConfigCustomSignalsErrorInvalidValueType = 8102,
+    /// Limit exceeded for key length, value length, or number of signals.
+    FIRRemoteConfigCustomSignalsErrorLimitExceeded = 8103,
+} NS_SWIFT_NAME(RemoteConfigCustomSignalsError);
+
 /// Enumerated value that indicates the source of Remote Config data. Data can come from
 /// the Remote Config service, the DefaultConfig that is available when the app is first installed,
 /// or a static initialized value if data is not available from the service or DefaultConfig.
@@ -218,11 +231,35 @@ NS_SWIFT_NAME(RemoteConfig)
 /// Unavailable. Use +remoteConfig instead.
 - (nonnull instancetype)init __attribute__((unavailable("Use +remoteConfig instead.")));
 
+#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180000)
+/// Ensures initialization is complete and clients can begin querying for Remote Config values.
+/// @param completionHandler Initialization complete callback with error parameter.
+- (void)ensureInitializedWithCompletionHandler:
+    (void (^_Nonnull NS_SWIFT_SENDABLE)(NSError *_Nullable initializationError))completionHandler;
+#else
 /// Ensures initialization is complete and clients can begin querying for Remote Config values.
 /// @param completionHandler Initialization complete callback with error parameter.
 - (void)ensureInitializedWithCompletionHandler:
     (void (^_Nonnull)(NSError *_Nullable initializationError))completionHandler;
+#endif
+
 #pragma mark - Fetch
+
+#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180000)
+/// Fetches Remote Config data with a callback. Call `activate()` to make fetched data
+/// available to your app.
+///
+/// Note: This method uses a Firebase Installations token to identify the app instance, and once
+/// it's called, it periodically sends data to the Firebase backend. (see
+/// `Installations.authToken(completion:)`).
+/// To stop the periodic sync, call `Installations.delete(completion:)`
+/// and avoid calling this method again.
+///
+/// @param completionHandler Fetch operation callback with status and error parameters.
+- (void)fetchWithCompletionHandler:
+    (void (^_Nullable NS_SWIFT_SENDABLE)(FIRRemoteConfigFetchStatus status,
+                                         NSError *_Nullable error))completionHandler;
+#else
 /// Fetches Remote Config data with a callback. Call `activate()` to make fetched data
 /// available to your app.
 ///
@@ -235,7 +272,27 @@ NS_SWIFT_NAME(RemoteConfig)
 /// @param completionHandler Fetch operation callback with status and error parameters.
 - (void)fetchWithCompletionHandler:(void (^_Nullable)(FIRRemoteConfigFetchStatus status,
                                                       NSError *_Nullable error))completionHandler;
+#endif
 
+#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180000)
+/// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
+/// Call `activateWithCompletion:` to make fetched data available to your app.
+///
+/// Note: This method uses a Firebase Installations token to identify the app instance, and once
+/// it's called, it periodically sends data to the Firebase backend. (see
+/// `Installations.authToken(completion:)`).
+/// To stop the periodic sync, call `Installations.delete(completion:)`
+/// and avoid calling this method again.
+///
+/// @param expirationDuration  Override the (default or optionally set `minimumFetchInterval`
+/// property in RemoteConfigSettings) `minimumFetchInterval` for only the current request, in
+/// seconds. Setting a value of 0 seconds will force a fetch to the backend.
+/// @param completionHandler   Fetch operation callback with status and error parameters.
+- (void)fetchWithExpirationDuration:(NSTimeInterval)expirationDuration
+                  completionHandler:(void (^_Nullable NS_SWIFT_SENDABLE)(
+                                        FIRRemoteConfigFetchStatus status,
+                                        NSError *_Nullable error))completionHandler;
+#else
 /// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
 /// Call `activateWithCompletion:` to make fetched data available to your app.
 ///
@@ -252,7 +309,23 @@ NS_SWIFT_NAME(RemoteConfig)
 - (void)fetchWithExpirationDuration:(NSTimeInterval)expirationDuration
                   completionHandler:(void (^_Nullable)(FIRRemoteConfigFetchStatus status,
                                                        NSError *_Nullable error))completionHandler;
+#endif
 
+#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180000)
+/// Fetches Remote Config data and if successful, activates fetched data. Optional completion
+/// handler callback is invoked after the attempted activation of data, if the fetch call succeeded.
+///
+/// Note: This method uses a Firebase Installations token to identify the app instance, and once
+/// it's called, it periodically sends data to the Firebase backend. (see
+/// `Installations.authToken(completion:)`).
+/// To stop the periodic sync, call `Installations.delete(completion:)`
+/// and avoid calling this method again.
+///
+/// @param completionHandler Fetch operation callback with status and error parameters.
+- (void)fetchAndActivateWithCompletionHandler:
+    (void (^_Nullable NS_SWIFT_SENDABLE)(FIRRemoteConfigFetchAndActivateStatus status,
+                                         NSError *_Nullable error))completionHandler;
+#else
 /// Fetches Remote Config data and if successful, activates fetched data. Optional completion
 /// handler callback is invoked after the attempted activation of data, if the fetch call succeeded.
 ///
@@ -266,14 +339,23 @@ NS_SWIFT_NAME(RemoteConfig)
 - (void)fetchAndActivateWithCompletionHandler:
     (void (^_Nullable)(FIRRemoteConfigFetchAndActivateStatus status,
                        NSError *_Nullable error))completionHandler;
+#endif
 
 #pragma mark - Apply
 
+#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180000)
+/// Applies Fetched Config data to the Active Config, causing updates to the behavior and appearance
+/// of the app to take effect (depending on how config data is used in the app).
+/// @param completion Activate operation callback with changed and error parameters.
+- (void)activateWithCompletion:
+    (void (^_Nullable NS_SWIFT_SENDABLE)(BOOL changed, NSError *_Nullable error))completion;
+#else
 /// Applies Fetched Config data to the Active Config, causing updates to the behavior and appearance
 /// of the app to take effect (depending on how config data is used in the app).
 /// @param completion Activate operation callback with changed and error parameters.
 - (void)activateWithCompletion:(void (^_Nullable)(BOOL changed,
                                                   NSError *_Nullable error))completion;
+#endif
 
 #pragma mark - Get Config
 /// Enables access to configuration values by using object subscripting syntax.
@@ -340,6 +422,25 @@ typedef void (^FIRRemoteConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable
                                                 NSError *_Nullable error)
     NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
 
+#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180000)
+/// Start listening for real-time config updates from the Remote Config backend and automatically
+/// fetch updates when they're available.
+///
+/// If a connection to the Remote Config backend is not already open, calling this method will
+/// open it. Multiple listeners can be added by calling this method again, but subsequent calls
+/// re-use the same connection to the backend.
+///
+/// Note: Real-time Remote Config requires the Firebase Remote Config Realtime API. See Get started
+/// with Firebase Remote Config at https://firebase.google.com/docs/remote-config/get-started for
+/// more information.
+///
+/// @param listener              The configured listener that is called for every config update.
+/// @return              Returns a registration representing the listener. The registration contains
+/// a remove method, which can be used to stop receiving updates for the provided listener.
+- (FIRConfigUpdateListenerRegistration *_Nonnull)addOnConfigUpdateListener:
+    (FIRRemoteConfigUpdateCompletion _Nonnull NS_SWIFT_SENDABLE)listener
+    NS_SWIFT_NAME(addOnConfigUpdateListener(remoteConfigUpdateCompletion:));
+#else
 /// Start listening for real-time config updates from the Remote Config backend and automatically
 /// fetch updates when they're available.
 ///
@@ -357,5 +458,10 @@ typedef void (^FIRRemoteConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable
 - (FIRConfigUpdateListenerRegistration *_Nonnull)addOnConfigUpdateListener:
     (FIRRemoteConfigUpdateCompletion _Nonnull)listener
     NS_SWIFT_NAME(addOnConfigUpdateListener(remoteConfigUpdateCompletion:));
+#endif
+
+- (void)setCustomSignals:(nonnull NSDictionary<NSString *, NSObject *> *)customSignals
+          withCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler
+    NS_REFINED_FOR_SWIFT;
 
 @end

+ 27 - 0
FirebaseRemoteConfig/Sources/RCNConfigSettings.m

@@ -404,6 +404,25 @@ static const int kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4;  // 4 hour
       }
     }
   }
+
+  NSDictionary<NSString *, NSString *> *customSignals = [self customSignals];
+  if (customSignals.count > 0) {
+    NSError *error;
+    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:customSignals
+                                                       options:0
+                                                         error:&error];
+    if (!error) {
+      ret = [ret
+          stringByAppendingString:[NSString
+                                      stringWithFormat:@", custom_signals:%@",
+                                                       [[NSString alloc]
+                                                           initWithData:jsonData
+                                                               encoding:NSUTF8StringEncoding]]];
+      // Log the keys of the custom signals sent during fetch.
+      FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000078",
+                  @"Keys of custom signals during fetch: %@", [customSignals allKeys]);
+    }
+  }
   ret = [ret stringByAppendingString:@"}"];
   return ret;
 }
@@ -473,6 +492,14 @@ static const int kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4;  // 4 hour
                      completionHandler:nil];
 }
 
+- (NSDictionary<NSString *, NSString *> *)customSignals {
+  return [_userDefaultsManager customSignals];
+}
+
+- (void)setCustomSignals:(NSDictionary<NSString *, NSString *> *)customSignals {
+  [_userDefaultsManager setCustomSignals:customSignals];
+}
+
 #pragma mark Throttling
 
 - (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval {

+ 2 - 0
FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h

@@ -47,6 +47,8 @@ NS_ASSUME_NONNULL_BEGIN
 @property(nonatomic, assign) NSString *lastFetchedTemplateVersion;
 /// Last active template version.
 @property(nonatomic, assign) NSString *lastActiveTemplateVersion;
+/// A dictionary to hold the latest custom signals set by the developer.
+@property(nonatomic, readwrite, strong) NSDictionary<NSString *, NSString *> *customSignals;
 
 /// Designated initializer.
 - (instancetype)initWithAppName:(NSString *)appName

+ 16 - 0
FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m

@@ -34,6 +34,7 @@ static NSString *const kRCNUserDefaultsKeyNameRealtimeThrottleEndTime = @"thrott
 static NSString *const kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval =
     @"currentRealtimeThrottlingRetryInterval";
 static NSString *const kRCNUserDefaultsKeyNameRealtimeRetryCount = @"realtimeRetryCount";
+static NSString *const kRCNUserDefaultsKeyCustomSignals = @"customSignals";
 
 @interface RCNUserDefaultsManager () {
   /// User Defaults instance for this bundleID. NSUserDefaults is guaranteed to be thread-safe.
@@ -141,6 +142,21 @@ static NSString *const kRCNUserDefaultsKeyNameRealtimeRetryCount = @"realtimeRet
   }
 }
 
+- (NSDictionary<NSString *, NSString *> *)customSignals {
+  NSDictionary *userDefaults = [self instanceUserDefaults];
+  if ([userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]) {
+    return [userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals];
+  }
+
+  return [[NSDictionary<NSString *, NSString *> alloc] init];
+}
+
+- (void)setCustomSignals:(NSDictionary<NSString *, NSString *> *)customSignals {
+  if (customSignals) {
+    [self setInstanceUserDefaultsValue:customSignals forKey:kRCNUserDefaultsKeyCustomSignals];
+  }
+}
+
 - (NSTimeInterval)lastETagUpdateTime {
   NSNumber *lastETagUpdateTime =
       [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime];

+ 107 - 0
FirebaseRemoteConfig/Swift/CustomSignals.swift

@@ -0,0 +1,107 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+#if SWIFT_PACKAGE
+  @_exported import FirebaseRemoteConfigInternal
+#endif // SWIFT_PACKAGE
+
+/// Represents a value associated with a key in a custom signal, restricted to the allowed data
+/// types : String, Int, Double.
+public struct CustomSignalValue {
+  private enum Kind {
+    case string(String)
+    case integer(Int)
+    case double(Double)
+  }
+
+  private let kind: Kind
+
+  private init(kind: Kind) {
+    self.kind = kind
+  }
+
+  /// Returns a string backed custom signal.
+  /// - Parameter string: The given string to back the custom signal with.
+  /// - Returns: A string backed custom signal.
+  public static func string(_ string: String) -> Self {
+    Self(kind: .string(string))
+  }
+
+  /// Returns an integer backed custom signal.
+  /// - Parameter integer: The given integer to back the custom signal with.
+  /// - Returns: An integer backed custom signal.
+  public static func integer(_ integer: Int) -> Self {
+    Self(kind: .integer(integer))
+  }
+
+  /// Returns an floating-point backed custom signal.
+  /// - Parameter double: The given floating-point value to back the custom signal with.
+  /// - Returns: An floating-point backed custom signal
+  public static func double(_ double: Double) -> Self {
+    Self(kind: .double(double))
+  }
+
+  fileprivate func toNSObject() -> NSObject {
+    switch kind {
+    case let .string(string):
+      return string as NSString
+    case let .integer(int):
+      return int as NSNumber
+    case let .double(double):
+      return double as NSNumber
+    }
+  }
+}
+
+extension CustomSignalValue: ExpressibleByStringInterpolation {
+  public init(stringLiteral value: String) {
+    self = .string(value)
+  }
+}
+
+extension CustomSignalValue: ExpressibleByIntegerLiteral {
+  public init(integerLiteral value: Int) {
+    self = .integer(value)
+  }
+}
+
+extension CustomSignalValue: ExpressibleByFloatLiteral {
+  public init(floatLiteral value: Double) {
+    self = .double(value)
+  }
+}
+
+@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
+public extension RemoteConfig {
+  /// Sets custom signals for this Remote Config instance.
+  /// - Parameter customSignals: A dictionary mapping string keys to custom
+  /// signals to be set for the app instance.
+  ///
+  /// When a new key is provided, a new key-value pair is added to the custom signals.
+  /// If an existing key is provided with a new value, the corresponding signal is updated.
+  /// If the value for a key is `nil`, the signal associated with that key is removed.
+  func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
+    return try await withCheckedThrowingContinuation { continuation in
+      let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
+      self.__setCustomSignals(customSignals) { error in
+        if let error {
+          continuation.resume(throwing: error)
+        } else {
+          continuation.resume()
+        }
+      }
+    }
+  }
+}

+ 1 - 0
FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift

@@ -57,6 +57,7 @@ class APITestBase: XCTestCase {
     let settings = RemoteConfigSettings()
     settings.minimumFetchInterval = 0
     config.configSettings = settings
+    config.settings.customSignals = [:]
 
     let jsonData = try JSONSerialization.data(
       withJSONObject: Constants.jsonValue

+ 50 - 0
FirebaseRemoteConfig/Tests/Swift/SwiftAPI/AsyncAwaitTests.swift

@@ -129,4 +129,54 @@ class AsyncAwaitTests: APITestBase {
     XCTAssertTrue(config.configValue(forKey: Constants.jedi).dataValue.isEmpty,
                   "Remote config should have been deleted.")
   }
+
+  func testSetCustomSignals() async throws {
+    let testSignals: [String: CustomSignalValue?] = [
+      "signal_1": .integer(5),
+      "signal_2": .string("basic"),
+      "signal_3": .double(3.14159),
+    ]
+
+    let expectedSignals: [String: String] = [
+      "signal_1": "5",
+      "signal_2": "basic",
+      "signal_3": "3.14159",
+    ]
+
+    _ = try await config.setCustomSignals(testSignals)
+    XCTAssertEqual(config.settings.customSignals, expectedSignals)
+  }
+
+  func testSetCustomSignalsMultipleTimes() async throws {
+    let testSignals: [String: CustomSignalValue?] = [
+      "signal_1": 6,
+      "signal_2": "basic",
+      "signal_3": 3.14,
+    ]
+
+    let expectedSignals: [String: String] = [
+      "signal_1": "6",
+      "signal_2": "basic",
+      "signal_3": "3.14",
+    ]
+
+    _ = try await config.setCustomSignals(testSignals)
+    XCTAssertEqual(config.settings.customSignals, expectedSignals)
+
+    let testSignals2: [String: CustomSignalValue?] = [
+      "signal_4": .integer(100),
+      "signal_3": nil,
+      "signal_5": .double(3.1234),
+    ]
+
+    let expectedSignals2: [String: String] = [
+      "signal_1": "6",
+      "signal_2": "basic",
+      "signal_4": "100",
+      "signal_5": "3.1234",
+    ]
+
+    _ = try await config.setCustomSignals(testSignals2)
+    XCTAssertEqual(config.settings.customSignals, expectedSignals2)
+  }
 }

+ 14 - 0
FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift

@@ -223,5 +223,19 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase {
 
     struct MyEncodableValue: Encodable {}
     let _: Void = try config.setDefaults(from: MyEncodableValue())
+
+    Task {
+      let signals: [String: CustomSignalValue?] = [
+        "signal_1": .integer(5),
+        "signal_2": .string("enable_feature"),
+        "signal_3": 5,
+        "signal_4": "enable_feature",
+        "signal_5": "enable_feature_\("secret")",
+        "signal_6": .double(3.14),
+        "signal_7": 3.14159,
+        "signal_8": nil, // Used to delete the custom signal for a given key.
+      ]
+      try await config.setCustomSignals(signals)
+    }
   }
 }

+ 114 - 0
FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m

@@ -1834,6 +1834,120 @@ static NSString *UTCToLocal(NSString *utcTime) {
   [self waitForExpectations:@[ notificationExpectation ] timeout:_expectationTimeout];
 }
 
+- (void)testSetCustomSignals {
+  NSMutableArray<XCTestExpectation *> *expectations =
+      [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
+
+  for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
+    expectations[i] = [self
+        expectationWithDescription:[NSString
+                                       stringWithFormat:@"Set custom signals - instance %d", i]];
+
+    NSDictionary<NSString *, NSObject *> *testSignals = @{
+      @"signal1" : @"stringValue",
+      @"signal2" : @"stringValue2",
+    };
+
+    [_configInstances[i] setCustomSignals:testSignals
+                           withCompletion:^(NSError *_Nullable error) {
+                             XCTAssertNil(error);
+                             NSDictionary<NSString *, NSString *> *retrievedSignals =
+                                 self->_configInstances[i].settings.customSignals;
+                             XCTAssertEqualObjects(retrievedSignals, testSignals);
+                             [expectations[i] fulfill];
+                           }];
+  }
+  [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
+}
+
+- (void)testSetCustomSignalsMultipleTimes {
+  NSMutableArray<XCTestExpectation *> *expectations =
+      [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
+
+  for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
+    expectations[i] = [self
+        expectationWithDescription:
+            [NSString stringWithFormat:@"Set custom signals multiple times - instance %d", i]];
+
+    // First set of signals
+    NSDictionary<NSString *, NSObject *> *testSignals1 = @{
+      @"signal1" : @"stringValue1",
+      @"signal2" : @"stringValue2",
+    };
+
+    // Second set of signals (overwrites, remove and adds new)
+    NSDictionary<NSString *, NSObject *> *testSignals2 = @{
+      @"signal1" : @"updatedValue1",
+      @"signal2" : [NSNull null],
+      @"signal3" : @5,
+    };
+
+    // Expected final set of signals
+    NSDictionary<NSString *, NSString *> *expectedSignals = @{
+      @"signal1" : @"updatedValue1",
+      @"signal3" : @"5",
+    };
+
+    [_configInstances[i] setCustomSignals:testSignals1
+                           withCompletion:^(NSError *_Nullable error) {
+                             XCTAssertNil(error);
+                             [_configInstances[i]
+                                 setCustomSignals:testSignals2
+                                   withCompletion:^(NSError *_Nullable error) {
+                                     XCTAssertNil(error);
+                                     NSDictionary<NSString *, NSString *> *retrievedSignals =
+                                         self->_configInstances[i].settings.customSignals;
+                                     XCTAssertEqualObjects(retrievedSignals, expectedSignals);
+                                     [expectations[i] fulfill];
+                                   }];
+                           }];
+  }
+  [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
+}
+
+- (void)testSetCustomSignals_invalidInput_throwsException {
+  NSMutableArray<XCTestExpectation *> *expectations =
+      [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances];
+
+  for (int i = 0; i < RCNTestRCNumTotalInstances; i++) {
+    expectations[i] =
+        [self expectationWithDescription:
+                  [NSString stringWithFormat:@"Set custom signals expects error - instance %d", i]];
+
+    // Invalid value type.
+    NSDictionary<NSString *, NSObject *> *invalidSignals1 = @{@"name" : [NSDate date]};
+
+    // Key length exceeds limit.
+    NSDictionary<NSString *, NSObject *> *invalidSignals2 =
+        @{[@"a" stringByPaddingToLength:251 withString:@"a" startingAtIndex:0] : @"value"};
+
+    // Value length exceeds limit.
+    NSDictionary<NSString *, NSObject *> *invalidSignals3 =
+        @{@"key" : [@"a" stringByPaddingToLength:501 withString:@"a" startingAtIndex:0]};
+
+    [_configInstances[i]
+        setCustomSignals:invalidSignals1
+          withCompletion:^(NSError *_Nullable error) {
+            XCTAssertNotNil(error);
+            XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorInvalidValueType);
+          }];
+    [_configInstances[i]
+        setCustomSignals:invalidSignals2
+          withCompletion:^(NSError *_Nullable error) {
+            XCTAssertNotNil(error);
+            XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded);
+          }];
+    [_configInstances[i]
+        setCustomSignals:invalidSignals3
+          withCompletion:^(NSError *_Nullable error) {
+            XCTAssertNotNil(error);
+            XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded);
+            [expectations[i] fulfill];
+          }];
+  }
+  [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil];
+}
+
 #pragma mark - Test Helpers
 
 - (FIROptions *)firstAppOptions {

+ 27 - 0
FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m

@@ -23,6 +23,8 @@ static NSTimeInterval RCNUserDefaultsSampleTimeStamp = 0;
 static NSString* const AppName = @"testApp";
 static NSString* const FQNamespace1 = @"testNamespace1:testApp";
 static NSString* const FQNamespace2 = @"testNamespace2:testApp";
+static NSMutableDictionary<NSString*, NSString*>* customSignals1 = nil;
+static NSMutableDictionary<NSString*, NSString*>* customSignals2 = nil;
 
 @interface RCNUserDefaultsManagerTests : XCTestCase
 
@@ -36,6 +38,13 @@ static NSString* const FQNamespace2 = @"testNamespace2:testApp";
   [[NSUserDefaults standardUserDefaults]
       removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier];
   RCNUserDefaultsSampleTimeStamp = [[NSDate date] timeIntervalSince1970];
+
+  customSignals1 = [[NSMutableDictionary alloc] initWithDictionary:@{
+    @"signal1" : @"stringValue",
+  }];
+  customSignals2 = [[NSMutableDictionary alloc] initWithDictionary:@{
+    @"signal2" : @"stringValue2",
+  }];
 }
 
 - (void)testUserDefaultsEtagWriteAndRead {
@@ -168,6 +177,18 @@ static NSString* const FQNamespace2 = @"testNamespace2:testApp";
                  RCNUserDefaultsSampleTimeStamp - 2.0);
 }
 
+- (void)testUserDefaultsCustomSignalsWriteAndRead {
+  RCNUserDefaultsManager* manager =
+      [[RCNUserDefaultsManager alloc] initWithAppName:AppName
+                                             bundleID:[NSBundle mainBundle].bundleIdentifier
+                                            namespace:FQNamespace1];
+  [manager setCustomSignals:customSignals1];
+  XCTAssertEqualObjects([manager customSignals], customSignals1);
+
+  [manager setCustomSignals:customSignals2];
+  XCTAssertEqualObjects([manager customSignals], customSignals2);
+}
+
 - (void)testUserDefaultsForMultipleNamespaces {
   RCNUserDefaultsManager* manager1 =
       [[RCNUserDefaultsManager alloc] initWithAppName:AppName
@@ -248,6 +269,12 @@ static NSString* const FQNamespace2 = @"testNamespace2:testApp";
   [manager2 setLastActiveTemplateVersion:@"2"];
   XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1");
   XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2");
+
+  /// Custom Signals
+  [manager1 setCustomSignals:customSignals1];
+  [manager2 setCustomSignals:customSignals2];
+  XCTAssertEqualObjects([manager1 customSignals], customSignals1);
+  XCTAssertEqualObjects([manager2 customSignals], customSignals2);
 }
 
 - (void)testUserDefaultsReset {

+ 2 - 2
FirebaseRemoteConfigInterop.podspec

@@ -1,11 +1,11 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseRemoteConfigInterop'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Interfaces that allow other Firebase SDKs to use Remote Config functionality.'
 
   s.description      = <<-DESC
   Not for public use.
-  A set of protocols that other Firebase SDKs can use to interoperate with FirebaseRemoetConfig in a safe
+  A set of protocols that other Firebase SDKs can use to interoperate with FirebaseRemoteConfig in a safe
   and reliable manner.
                        DESC
 

+ 3 - 3
FirebaseSessions.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseSessions'
-  s.version          = '11.7.0'
+  s.version          = '11.8.0'
   s.summary          = 'Firebase Sessions'
 
   s.description      = <<-DESC
@@ -39,8 +39,8 @@ Pod::Spec.new do |s|
     base_dir + 'SourcesObjC/**/*.{c,h,m,mm}',
   ]
 
-  s.dependency 'FirebaseCore', '~> 11.7.0'
-  s.dependency 'FirebaseCoreExtension', '~> 11.7.0'
+  s.dependency 'FirebaseCore', '~> 11.8.0'
+  s.dependency 'FirebaseCoreExtension', '~> 11.8.0'
   s.dependency 'FirebaseInstallations', '~> 11.0'
   s.dependency 'GoogleDataTransport', '~> 10.0'
   s.dependency 'GoogleUtilities/Environment', '~> 8.0'

+ 2 - 2
FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.h

@@ -49,12 +49,12 @@ NSData* _Nullable FIRSESEncodeProto(const pb_field_t fields[],
                                     NSError** error);
 #pragma clang diagnostic pop
 
-/// Mallocs a pb_bytes_array and copies the given NSData bytes into the bytes array.
+/// Callocs a pb_bytes_array and copies the given NSData bytes into the bytes array.
 /// @note Memory needs to be freed manually, through pb_free or pb_release.
 /// @param data The data to copy into the new bytes array.
 pb_bytes_array_t* _Nullable FIRSESEncodeData(NSData* _Nullable data);
 
-/// Mallocs a pb_bytes_array and copies the given NSString's bytes into the bytes array.
+/// Callocs a pb_bytes_array and copies the given NSString's bytes into the bytes array.
 /// @note Memory needs to be freed manually, through pb_free or pb_release.
 /// @param string The string to encode as pb_bytes.
 pb_bytes_array_t* _Nullable FIRSESEncodeString(NSString* _Nullable string);

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor