Skip to content

Commit 7e0be0a

Browse files
authored
StoreKit2 support for Apple devices (#1846)
* StoreKit2 support for Apple devices * Add conditional building for win / android and make the build.sh changed as I have make the complier xcode * Linux fix * Clean up * revert the to include the old way of header generator * clean up build and path finding * Fix tvOS build for Swift by directly compiling in target * Add in dummy async func to force backward compatability * [StoreKit2] Fix Xcode 15 dyld launch crashes on iOS <=16 and update documentation This commit resolves several known compiler and runtime issues when compiling C++ alongside modern Swift (Concurrency) using Xcode 15+ for deployment on iOS 15/16: 1. `swift_task_alloc` (EXC_BAD_ACCESS): Mixing Swift Concurrency with `@objc` interfaces and C++ pointers on older runtimes causes memory segmentation faults. We introduced an Objective-C++ Block Barrier to keep C pointers on the C++ side (`analytics_ios.mm`) and used a detached `Task` with a pure Swift struct (`AppleTransactionVerifier.swift`) to safely isolate the async `Transaction.all` loop from the bridging boundary. 2. `AsyncIteratorProtocol` (Witness Table Corruption): Using a `for await` loop within an `@objc` class on iOS 15/16 triggers a known Xcode 15+ compiler bug leading to a runtime crash. We bypassed this by extracting the logic into a pure Swift struct and replacing the `for await` loop with manual `while let ... = await iterator.next()` iteration. 3. `__libcpp_verbose_abort`: Xcode 15 requires this symbol for C++ exception handling, but it is missing from the iOS 15/16 system `libc++`. We disabled verbose abort globally via `CMakeLists.txt` and provided a weak fallback implementation in `app_framework.cc` for integration tests to prevent a `dyld` launch crash. 4. `libswift_Concurrency.dylib`: The Swift compiler sometimes fails to weak-link the Concurrency backward compatibility library in Objective-C++ projects. We now natively inject `-Wl,-weak-lswift_Concurrency` into all integration test Xcode projects during `setup_integration_tests.py`. Internal dialog comments have been cleaned up and replaced with professional explanations suitable for open-source maintainers. Documentation in `release_build_files/readme.md` has been updated to provide clear instructions for manual C++ integrators to apply these linker/compiler flags if they experience similar crashes. * simplify the build as we have xcode 16 as minimum and we only support ios 15+ which should bundle the concurrency headers * Remove the old changes * clean up pbxproj * revert setup changes * Continue the clean up * Clean up some of the un needed changes * More of the clean up * Cleanup * More clean up * Comment why bitcode was removed * Fix header inclusion after analytics order of operation change * Verify readme and clean up tvos build * Fix MSVC implicit include flags * update readme * Fix __libcpp_verbose_abort signature for Xcode 16 * fix formating * Remove the FIREBASE_XCODE_TARGET_FORMAT STREQUAL "frameworks")
1 parent 3fe6b6e commit 7e0be0a

17 files changed

Lines changed: 411 additions & 60 deletions

File tree

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,14 @@ set(FIREBASE_XCODE_TARGET_FORMAT "frameworks" CACHE STRING
9696
set(FIREBASE_CPP_SDK_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR})
9797

9898
project (firebase NONE)
99+
100+
set(CMAKE_Swift_LANGUAGE_VERSION 6.2)
101+
99102
enable_language(C)
100103
enable_language(CXX)
104+
if(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "tvOS")
105+
enable_language(Swift)
106+
endif()
101107

102108
if(NOT DEFINED CMAKE_CXX_COMPILER_LAUNCHER)
103109
find_program(CCACHE_PROGRAM ccache)
@@ -477,6 +483,7 @@ if(DESKTOP)
477483
-DUSE_LIBUV=1
478484
)
479485
elseif(APPLE)
486+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_LIBCPP_DISABLE_AVAILABILITY")
480487
set(websockets_additional_defines
481488
${websockets_additional_defines}
482489
-DUSE_LIBUV=1

analytics/CMakeLists.txt

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ set(android_SRCS
7272

7373
# Source files used by the iOS implementation.
7474
set(ios_SRCS
75-
src/analytics_ios.mm)
75+
src/analytics_ios.mm
76+
src/ios/swift/AppleTransactionVerifier.swift)
7677

7778
# Source files used by the desktop / stub implementation.
7879
set(desktop_SRCS
@@ -88,8 +89,12 @@ if(ANDROID)
8889
set(analytics_platform_SRCS
8990
"${android_SRCS}")
9091
elseif(IOS)
92+
if(CMAKE_GENERATOR STREQUAL "Unix Makefiles")
93+
message(FATAL_ERROR "Swift is not supported by the 'Unix Makefiles' generator on iOS. Please use the Xcode generator (-G Xcode) or Ninja (-G Ninja).")
94+
endif()
9195
set(analytics_platform_SRCS
9296
"${ios_SRCS}")
97+
9398
else()
9499
set(analytics_platform_SRCS
95100
"${desktop_SRCS}")
@@ -99,6 +104,9 @@ add_library(firebase_analytics STATIC
99104
${common_SRCS}
100105
${analytics_platform_SRCS})
101106

107+
108+
add_dependencies(firebase_analytics FIREBASE_ANALYTICS_GENERATED_HEADERS)
109+
102110
set_property(TARGET firebase_analytics PROPERTY FOLDER "Firebase Cpp")
103111

104112
# Set up the dependency on Firebase App.
@@ -122,20 +130,86 @@ target_compile_definitions(firebase_analytics
122130
-DINTERNAL_EXPERIMENTAL=1
123131
)
124132
# Automatically include headers that might not be declared.
125-
if(MSVC)
126-
add_definitions(/FI"assert.h" /FI"string.h" /FI"stdint.h")
133+
if(IOS)
134+
if(MSVC)
135+
target_compile_options(firebase_analytics PRIVATE
136+
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:/FI"assert.h">
137+
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:/FI"string.h">
138+
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:/FI"stdint.h">)
139+
else()
140+
target_compile_options(firebase_analytics PRIVATE
141+
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:SHELL:-include assert.h -include string.h>
142+
)
143+
endif()
127144
else()
128-
add_definitions(-include assert.h -include string.h)
145+
if(MSVC)
146+
target_compile_options(firebase_analytics PRIVATE
147+
/FI"assert.h" /FI"string.h" /FI"stdint.h")
148+
else()
149+
target_compile_options(firebase_analytics PRIVATE
150+
SHELL:-include assert.h -include string.h
151+
)
152+
endif()
129153
endif()
130154

131155
if(ANDROID)
132156
firebase_cpp_proguard_file(analytics)
133157
elseif(IOS)
134-
# Enable Automatic Reference Counting (ARC) and Bitcode.
158+
# Enable Automatic Reference Counting (ARC) and Bitcode specifically for Objective-C++ files.
159+
# Note: -fembed-bitcode is placed here for src/analytics_ios.mm so that it is not passed
160+
# to the Swift compiler, which does not support the flag.
161+
set_source_files_properties(src/analytics_ios.mm PROPERTIES COMPILE_OPTIONS "-fobjc-arc;-fembed-bitcode")
162+
163+
if(CMAKE_GENERATOR STREQUAL "Xcode")
164+
target_include_directories(firebase_analytics PRIVATE "$(DERIVED_FILE_DIR)")
165+
target_compile_options(firebase_analytics PRIVATE
166+
"-I$(OBJECT_FILE_DIR_normal)/$(CURRENT_ARCH)"
167+
)
168+
else()
169+
target_include_directories(firebase_analytics PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
170+
endif()
171+
172+
# Swift needs to find the FirebaseAnalytics module from CocoaPods
173+
set(pods_dir "${FIREBASE_POD_DIR}/Pods")
174+
175+
# Point to the base directories containing the .xcframework folders.
176+
# Xcode natively handles XCFrameworks and will pick the right slice automatically.
177+
# Determine the xcframework architecture slice based on the target platform
178+
# and if it is running on simulator or device.
179+
string(TOLOWER "${CMAKE_OSX_SYSROOT}" sysroot_lower)
180+
if(CMAKE_SYSTEM_NAME STREQUAL "tvOS")
181+
if(sysroot_lower MATCHES "simulator")
182+
set(analytics_slice "tvos-arm64_x86_64-simulator")
183+
else()
184+
set(analytics_slice "tvos-arm64")
185+
endif()
186+
else()
187+
if(sysroot_lower MATCHES "simulator")
188+
set(analytics_slice "ios-arm64_x86_64-simulator")
189+
else()
190+
set(analytics_slice "ios-arm64")
191+
endif()
192+
endif()
193+
194+
set(analytics_framework_dir "${pods_dir}/FirebaseAnalytics/Frameworks/FirebaseAnalytics.xcframework/${analytics_slice}")
195+
set(measurement_framework_dir "${pods_dir}/GoogleAppMeasurement/Frameworks/GoogleAppMeasurement.xcframework/${analytics_slice}")
196+
135197
target_compile_options(firebase_analytics
136-
PUBLIC "-fobjc-arc" "-fembed-bitcode")
137-
target_link_libraries(firebase_analytics
138-
PUBLIC "-fembed-bitcode")
198+
PRIVATE
199+
$<$<COMPILE_LANGUAGE:Swift>:-F${analytics_framework_dir}>
200+
$<$<COMPILE_LANGUAGE:Swift>:-F${measurement_framework_dir}>
201+
)
202+
203+
target_link_options(firebase_analytics
204+
PUBLIC
205+
"-F${analytics_framework_dir}"
206+
"-F${measurement_framework_dir}"
207+
)
208+
209+
# Prevent Xcode from trying to build or evaluate headers for unused architectures
210+
set_target_properties(firebase_analytics PROPERTIES
211+
XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH "YES"
212+
)
139213

140214
setup_pod_headers(
141215
firebase_analytics

analytics/integration_test/readme.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,37 @@ Building and Running the sample
6464
"Analytics" tab accessible from
6565
[https://firebase.google.com/console/](https://firebase.google.com/console/).
6666
67+
#### iOS Testing LogAppleTransaction
68+
69+
To test the log apple transaction function, you should use the existing test app and xcode's simulated transactions.
70+
The manual test will involve running the integration test: `firebase_analytics_test/TestLogAppleTransaction` and verifying that it logs a transaction to the console.
71+
72+
- Step 1: Set up the Local Xcode Environment
73+
- In Xcode, go to File > New > File from Template and create a StoreKit Configuration File (.storekit).
74+
- Give the configuration any name.
75+
- Target both integration_test and integration_test_tvos
76+
- Add at least one dummy product to this file.
77+
- Do this by selecting the file in xcode and clicking the + button in the bottom left corner.
78+
- Choose a Non-Consumable in app purchase product.
79+
- Give it a Reference name of your choice (e.g. "ReferenceAppleIapProduct").
80+
- Give it a Product ID of your choice (e.g. "com.example.nonconsumable").
81+
- Make the app use the store kit file. In the top bar go to Product > Scheme > Edit Scheme...
82+
- In the left hand menu select Run
83+
- Select the Options tab on the right
84+
- Set the StoreKit Configuration dropdown to your new .storekit file.
85+
- Step 2: Validate logging transactions
86+
- Try running the test app with the dummy transaction ID. It should return an error from the
87+
LogAppleTransactions function.
88+
- After runnign the app once you can create a simulated transaction for testing.
89+
- To create a simulated transaction ID:
90+
- Go to Debug > StoreKit > Manage Transactions.
91+
- Click the + button in the bottom left corner.
92+
- Select the Non-Consumable in app purchase product.
93+
- Copy the transaction ID to the test case and replace 'dummy_transaction_id' with your new transaction ID. e.g. '0'
94+
- Make sure to update the testcase to now expect success.
95+
- Then try running the test app again with the simulated transaction ID.
96+
- It should log the transaction to the console. Both the Xcode console and firebase console should show a log for an in app purchase.
97+
6798
### Android
6899
- Register your Android app with Firebase.
69100
- Create a new app on the [Firebase console](https://firebase.google.com/console/), and attach

analytics/integration_test/src/integration_test.cc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
#include <ctime>
2323
#include <future>
2424

25+
#if defined(__APPLE__)
26+
#include <TargetConditionals.h>
27+
#endif
28+
2529
#include "app_framework.h" // NOLINT
2630
#include "firebase/analytics.h"
2731
#include "firebase/analytics/event_names.h"
@@ -299,6 +303,21 @@ TEST_F(FirebaseAnalyticsTest, TestDesktopDebugMode) {
299303
firebase::analytics::SetDesktopDebugMode(false);
300304
}
301305

306+
TEST_F(FirebaseAnalyticsTest, TestLogAppleTransaction) {
307+
auto future =
308+
firebase::analytics::LogAppleTransaction("dummy_transaction_id");
309+
WaitForCompletionAnyResult(future, "LogAppleTransaction");
310+
#if defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)
311+
// On iOS/tvOS, passing a dummy transaction ID will fail to find a verified
312+
// transaction.
313+
EXPECT_NE(future.error(), 0);
314+
#else
315+
// On Android and Desktop (including macOS), LogAppleTransaction is a no-op
316+
// that returns success.
317+
EXPECT_EQ(future.error(), 0);
318+
#endif
319+
}
320+
302321
TEST_F(FirebaseAnalyticsTest, TestLogEvents) {
303322
// Log an event with no parameters.
304323
firebase::analytics::LogEvent(firebase::analytics::kEventLogin);

analytics/src/analytics_android.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,30 @@ void LogEvent(const char* name) {
502502
LogEvent(name, nullptr, static_cast<size_t>(0));
503503
}
504504

505+
/// Log an Apple StoreKit 2 transaction. This is a no-op on Android and returns
506+
/// success.
507+
Future<void> LogAppleTransaction(const char* transaction_id) {
508+
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
509+
: nullptr;
510+
if (!api) {
511+
return Future<void>();
512+
}
513+
const auto future_handle =
514+
api->SafeAlloc<void>(internal::kAnalyticsFnLogAppleTransaction);
515+
api->Complete(future_handle, 0, "");
516+
return Future<void>(api, future_handle.get());
517+
}
518+
519+
Future<void> LogAppleTransactionLastResult() {
520+
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
521+
: nullptr;
522+
if (!api) {
523+
return Future<void>();
524+
}
525+
return static_cast<const Future<void>&>(
526+
api->LastResult(internal::kAnalyticsFnLogAppleTransaction));
527+
}
528+
505529
// Log an event with associated parameters.
506530
void LogEvent(const char* name, const Parameter* parameters,
507531
size_t number_of_parameters) {

analytics/src/analytics_common.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ namespace internal {
2626
enum AnalyticsFn {
2727
kAnalyticsFnGetAnalyticsInstanceId,
2828
kAnalyticsFnGetSessionId,
29-
kAnalyticsFnCount
29+
kAnalyticsFnLogAppleTransaction,
30+
kAnalyticsFnCount,
3031
};
3132

3233
// Data structure which holds the Future API for this module.

analytics/src/analytics_desktop.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,30 @@ void LogEvent(const char* name) {
467467
LogEvent(name, static_cast<const Parameter*>(nullptr), 0);
468468
}
469469

470+
/// Log an Apple StoreKit 2 transaction. This is a no-op on Desktop and returns
471+
/// success.
472+
Future<void> LogAppleTransaction(const char* transaction_id) {
473+
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
474+
: nullptr;
475+
if (!api) {
476+
return Future<void>();
477+
}
478+
const auto future_handle =
479+
api->SafeAlloc<void>(internal::kAnalyticsFnLogAppleTransaction);
480+
api->Complete(future_handle, 0, "");
481+
return Future<void>(api, future_handle.get());
482+
}
483+
484+
Future<void> LogAppleTransactionLastResult() {
485+
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
486+
: nullptr;
487+
if (!api) {
488+
return Future<void>();
489+
}
490+
return static_cast<const Future<void>&>(
491+
api->LastResult(internal::kAnalyticsFnLogAppleTransaction));
492+
}
493+
470494
void LogEvent(const char* name, const char* parameter_name,
471495
const char* parameter_value) {
472496
if (parameter_name == nullptr) {

analytics/src/analytics_ios.mm

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
#import "FIRAnalytics+OnDevice.h"
2121
#import "FIRAnalytics.h"
2222

23+
#include "analytics/src/analytics_common.h"
2324
#include "analytics/src/include/firebase/analytics.h"
2425

25-
#include "analytics/src/analytics_common.h"
26+
// Include the generated Swift header for the C++ bridge.
27+
#include "firebase_analytics-Swift.h"
28+
2629
#include "app/src/assert.h"
2730
#include "app/src/include/firebase/internal/mutex.h"
2831
#include "app/src/include/firebase/version.h"
@@ -231,6 +234,42 @@ void LogEvent(const char* name) {
231234
[FIRAnalytics logEventWithName:@(name) parameters:@{}];
232235
}
233236

237+
Future<void> LogAppleTransaction(const char* transaction_id) {
238+
MutexLock lock(g_mutex);
239+
FIREBASE_ASSERT_RETURN(Future<void>(), internal::IsInitialized());
240+
241+
auto* api = internal::FutureData::Get()->api();
242+
const auto future_handle = api->SafeAlloc<void>(internal::kAnalyticsFnLogAppleTransaction);
243+
244+
if (!transaction_id) {
245+
api->Complete(future_handle, -1, "Transaction ID is null");
246+
return Future<void>(api, future_handle.get());
247+
}
248+
249+
[AppleTransactionVerifier
250+
verifyWithTransactionId:SafeString(transaction_id)
251+
completion:^(BOOL isFound) {
252+
MutexLock lock(g_mutex);
253+
if (!internal::IsInitialized()) return;
254+
255+
auto* api = internal::FutureData::Get()->api();
256+
if (isFound) {
257+
api->Complete(future_handle, 0, "");
258+
} else {
259+
api->Complete(future_handle, -1, "StoreKit 2 transaction not found.");
260+
}
261+
}];
262+
263+
return Future<void>(api, future_handle.get());
264+
}
265+
266+
Future<void> LogAppleTransactionLastResult() {
267+
MutexLock lock(g_mutex);
268+
FIREBASE_ASSERT_RETURN(Future<void>(), internal::IsInitialized());
269+
return static_cast<const Future<void>&>(
270+
internal::FutureData::Get()->api()->LastResult(internal::kAnalyticsFnLogAppleTransaction));
271+
}
272+
234273
// Declared here so that it can be used, defined below.
235274
NSDictionary* MapToDictionary(const std::map<Variant, Variant>& map);
236275

analytics/src/include/firebase/analytics.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,37 @@ void LogEvent(const char* name, const char* parameter_name,
455455
/// @endif
456456
void LogEvent(const char* name);
457457

458+
/// @brief Logs an in-app purchase transaction specifically for Apple's
459+
/// StoreKit 2.
460+
///
461+
/// This function is intended for developers on iOS who process transactions
462+
/// via custom native plugins or engines and need to securely log those
463+
/// transactions natively through Google Analytics. The provided ID must map 1:1
464+
/// with the native Apple `Transaction.id`. If a matching transaction is not
465+
/// found in the Apple device's purchase history, nothing will be logged to
466+
/// Analytics.
467+
///
468+
/// @note Finished consumable transactions are removed from the local
469+
/// transaction history and cannot be retrieved by this function once
470+
/// finished. Developers should either call this function before finishing
471+
/// the transaction or use `FirebaseAnalytics.LogEvent` directly as a
472+
/// fallback.
473+
///
474+
/// @param transaction_id The native Apple transaction identifier as a
475+
/// null-terminated string.
476+
///
477+
/// @returns A Future<void> that completes successfully when the native
478+
/// StoreKit 2 transaction is found and logged. If the transaction
479+
/// cannot be found, the Future will complete with a non-zero error().
480+
Future<void> LogAppleTransaction(const char* transaction_id);
481+
482+
/// @brief Get the result of the most recent LogAppleTransaction() call.
483+
///
484+
/// @returns A Future<void> that completes successfully when the native
485+
/// StoreKit 2 transaction is found and logged. If the transaction
486+
/// cannot be found, the Future will complete with a non-zero error().
487+
Future<void> LogAppleTransactionLastResult();
488+
458489
/// @brief Log an event with associated parameters.
459490
///
460491
/// An Event is an important occurrence in your app that you want to

0 commit comments

Comments
 (0)