Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,14 @@ set(FIREBASE_XCODE_TARGET_FORMAT "frameworks" CACHE STRING
set(FIREBASE_CPP_SDK_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR})

project (firebase NONE)

set(CMAKE_Swift_LANGUAGE_VERSION 5.9)

enable_language(C)
enable_language(CXX)
if (IOS)
enable_language(Swift)
endif()

if(NOT DEFINED CMAKE_CXX_COMPILER_LAUNCHER)
find_program(CCACHE_PROGRAM ccache)
Expand Down
89 changes: 81 additions & 8 deletions analytics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ set(android_SRCS

# Source files used by the iOS implementation.
set(ios_SRCS
src/analytics_ios.mm)
src/analytics_ios.mm
src/ios/swift/StoreKit2Bridge.swift)

# Source files used by the desktop / stub implementation.
set(desktop_SRCS
Expand All @@ -88,8 +89,12 @@ if(ANDROID)
set(analytics_platform_SRCS
"${android_SRCS}")
elseif(IOS)
if(CMAKE_GENERATOR STREQUAL "Unix Makefiles")
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).")
endif()
set(analytics_platform_SRCS
"${ios_SRCS}")

else()
set(analytics_platform_SRCS
"${desktop_SRCS}")
Expand All @@ -99,6 +104,9 @@ add_library(firebase_analytics STATIC
${common_SRCS}
${analytics_platform_SRCS})


add_dependencies(firebase_analytics FIREBASE_ANALYTICS_GENERATED_HEADERS)

set_property(TARGET firebase_analytics PROPERTY FOLDER "Firebase Cpp")

# Set up the dependency on Firebase App.
Expand All @@ -122,20 +130,85 @@ target_compile_definitions(firebase_analytics
-DINTERNAL_EXPERIMENTAL=1
)
# Automatically include headers that might not be declared.
if(MSVC)
add_definitions(/FI"assert.h" /FI"string.h" /FI"stdint.h")
if(IOS)
if(MSVC)
target_compile_options(firebase_analytics PRIVATE
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:/FI"assert.h">
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:/FI"string.h">
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:/FI"stdint.h">)
else()
target_compile_options(firebase_analytics PRIVATE
$<$<NOT:$<COMPILE_LANGUAGE:Swift>>:SHELL:-include assert.h -include string.h>
)
endif()
else()
add_definitions(-include assert.h -include string.h)
if(MSVC)
target_compile_options(firebase_analytics PRIVATE
/FI"assert.h" /FI"string.h" /FI"stdint.h")
else()
target_compile_options(firebase_analytics PRIVATE
SHELL:-include assert.h -include string.h
)
endif()
endif()

if(ANDROID)
firebase_cpp_proguard_file(analytics)
elseif(IOS)
# Enable Automatic Reference Counting (ARC) and Bitcode.
# Enable Automatic Reference Counting (ARC) and Bitcode specifically for Objective-C++ files.
# Note: -fembed-bitcode is placed here for src/analytics_ios.mm so that it is not passed
# to the Swift compiler, which does not support the flag.
set_source_files_properties(src/analytics_ios.mm PROPERTIES COMPILE_OPTIONS "-fobjc-arc;-fembed-bitcode")

target_include_directories(firebase_analytics
PRIVATE
"$(DERIVED_FILE_DIR)"
)

if(IOS OR TVOS)
# Swift needs to find the FirebaseAnalytics module from CocoaPods
set(pods_dir "${FIREBASE_POD_DIR}/Pods")

# Point to the base directories containing the .xcframework folders.
# Xcode natively handles XCFrameworks and will pick the right slice automatically.
# Determine the xcframework architecture slice based on the target platform
# and if it is running on simulator or device.
if(IOS)
string(TOLOWER "${CMAKE_OSX_SYSROOT}" sysroot_lower)
if(sysroot_lower MATCHES "simulator")
set(analytics_slice "ios-arm64_x86_64-simulator")
else()
set(analytics_slice "ios-arm64")
endif()
elseif(TVOS)
string(TOLOWER "${CMAKE_OSX_SYSROOT}" sysroot_lower)
if(sysroot_lower MATCHES "simulator")
set(analytics_slice "tvos-arm64_x86_64-simulator")
else()
set(analytics_slice "tvos-arm64")
endif()
endif()

set(analytics_framework_dir "${pods_dir}/FirebaseAnalytics/Frameworks/FirebaseAnalytics.xcframework/${analytics_slice}")
set(measurement_framework_dir "${pods_dir}/GoogleAppMeasurement/Frameworks/GoogleAppMeasurement.xcframework/${analytics_slice}")

target_compile_options(firebase_analytics
PUBLIC "-fobjc-arc" "-fembed-bitcode")
target_link_libraries(firebase_analytics
PUBLIC "-fembed-bitcode")
PRIVATE
$<$<COMPILE_LANGUAGE:Swift>:-F${analytics_framework_dir}>
$<$<COMPILE_LANGUAGE:Swift>:-F${measurement_framework_dir}>
)

target_link_options(firebase_analytics
PUBLIC
"-F${analytics_framework_dir}"
"-F${measurement_framework_dir}"
)

# Prevent Xcode from trying to build or evaluate headers for unused architectures
set_target_properties(firebase_analytics PROPERTIES
XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH "YES"
)
endif()

setup_pod_headers(
firebase_analytics
Expand Down
31 changes: 31 additions & 0 deletions analytics/integration_test/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,37 @@ Building and Running the sample
"Analytics" tab accessible from
[https://firebase.google.com/console/](https://firebase.google.com/console/).

#### iOS Testing LogAppleTransaction

To test the log apple transaction function, you should use the existing test app and xcode's simulated transactions.
The manual test will involve running the integration test: `firebase_analytics_test/TestLogAppleTransaction` and verifying that it logs a transaction to the console.

- Step 1: Set up the Local Xcode Environment
- In Xcode, go to File > New > File from Template and create a StoreKit Configuration File (.storekit).
- Give the configuration any name.
- Target both integration_test and integration_test_tvos
- Add at least one dummy product to this file.
- Do this by selecting the file in xcode and clicking the + button in the bottom left corner.
- Choose a Non-Consumable in app purchase product.
- Give it a Reference name of your choice (e.g. "ReferenceAppleIapProduct").
- Give it a Product ID of your choice (e.g. "com.example.nonconsumable").
- Make the app use the store kit file. In the top bar go to Product > Scheme > Edit Scheme... >
- In the left hand menu select Run
- Select the Options tab on the right
- Set the StoreKit Configuration dropdown to your new .storekit file.
- Step 2: Validate logging transactions
- Try running the test app with the dummy transaction ID. It should return an error from the
LogAppleTransactions function.
- After runnign the app once you can create a simulated transaction for testing.
- To create a simulated transaction ID:
- Go to Debug > StoreKit > Manage Transactions.
- Click the + button in the bottom left corner.
- Select the Non-Consumable in app purchase product.
- Copy the transaction ID to the test case and replace 'dummy_transaction_id' with your new transaction ID. e.g. '0'
- Make sure to update the testcase to now expect success.
- Then try running the test app again with the simulated transaction ID.
- It should log the transaction to the console. Both the Xcode console and firebase console should show a log for an in app purchase.

### Android
- Register your Android app with Firebase.
- Create a new app on the [Firebase console](https://firebase.google.com/console/), and attach
Expand Down
19 changes: 19 additions & 0 deletions analytics/integration_test/src/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
#include <ctime>
#include <future>

#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif

#include "app_framework.h" // NOLINT
#include "firebase/analytics.h"
#include "firebase/analytics/event_names.h"
Expand Down Expand Up @@ -299,6 +303,21 @@ TEST_F(FirebaseAnalyticsTest, TestDesktopDebugMode) {
firebase::analytics::SetDesktopDebugMode(false);
}

TEST_F(FirebaseAnalyticsTest, TestLogAppleTransaction) {
auto future =
firebase::analytics::LogAppleTransaction("dummy_transaction_id");
WaitForCompletionAnyResult(future, "LogAppleTransaction");
#if defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)
// On iOS/tvOS, passing a dummy transaction ID will fail to find a verified
// transaction.
EXPECT_NE(future.error(), 0);
#else
// On Android and Desktop (including macOS), LogAppleTransaction is a no-op
// that returns success.
EXPECT_EQ(future.error(), 0);
#endif
}

TEST_F(FirebaseAnalyticsTest, TestLogEvents) {
// Log an event with no parameters.
firebase::analytics::LogEvent(firebase::analytics::kEventLogin);
Expand Down
24 changes: 24 additions & 0 deletions analytics/src/analytics_android.cc
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,30 @@ void LogEvent(const char* name) {
LogEvent(name, nullptr, static_cast<size_t>(0));
}

/// Log an Apple StoreKit 2 transaction. This is a no-op on Android and returns
/// success.
Future<void> LogAppleTransaction(const char* transaction_id) {
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
: nullptr;
if (!api) {
return Future<void>();
}
const auto future_handle =
api->SafeAlloc<void>(internal::kAnalyticsFnLogAppleTransaction);
api->Complete(future_handle, 0, "");
return Future<void>(api, future_handle.get());
}

Future<void> LogAppleTransactionLastResult() {
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
: nullptr;
if (!api) {
return Future<void>();
}
return static_cast<const Future<void>&>(
api->LastResult(internal::kAnalyticsFnLogAppleTransaction));
}

// Log an event with associated parameters.
void LogEvent(const char* name, const Parameter* parameters,
size_t number_of_parameters) {
Expand Down
5 changes: 3 additions & 2 deletions analytics/src/analytics_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ namespace analytics {
namespace internal {

enum AnalyticsFn {
kAnalyticsFnGetAnalyticsInstanceId,
kAnalyticsFnGetAnalyticsInstanceId = 0,
kAnalyticsFnGetSessionId,
kAnalyticsFnCount
kAnalyticsFnLogAppleTransaction,
kAnalyticsFnCount,
};

// Data structure which holds the Future API for this module.
Expand Down
24 changes: 24 additions & 0 deletions analytics/src/analytics_desktop.cc
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,30 @@ void LogEvent(const char* name) {
LogEvent(name, static_cast<const Parameter*>(nullptr), 0);
}

/// Log an Apple StoreKit 2 transaction. This is a no-op on Desktop and returns
/// success.
Future<void> LogAppleTransaction(const char* transaction_id) {
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
: nullptr;
if (!api) {
return Future<void>();
}
const auto future_handle =
api->SafeAlloc<void>(internal::kAnalyticsFnLogAppleTransaction);
api->Complete(future_handle, 0, "");
return Future<void>(api, future_handle.get());
}

Future<void> LogAppleTransactionLastResult() {
auto* api = internal::FutureData::Get() ? internal::FutureData::Get()->api()
: nullptr;
if (!api) {
return Future<void>();
}
return static_cast<const Future<void>&>(
api->LastResult(internal::kAnalyticsFnLogAppleTransaction));
}

void LogEvent(const char* name, const char* parameter_name,
const char* parameter_value) {
if (parameter_name == nullptr) {
Expand Down
54 changes: 53 additions & 1 deletion analytics/src/analytics_ios.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
#import "FIRAnalytics+OnDevice.h"
#import "FIRAnalytics.h"

#include "analytics/src/analytics_common.h"
#include "analytics/src/include/firebase/analytics.h"

#include "analytics/src/analytics_common.h"
// Include the generated Swift header for the C++ bridge.
#include <firebase_analytics/firebase_analytics-Swift.h>

#include "app/src/assert.h"
#include "app/src/include/firebase/internal/mutex.h"
#include "app/src/include/firebase/version.h"
Expand Down Expand Up @@ -231,6 +234,55 @@ void LogEvent(const char* name) {
[FIRAnalytics logEventWithName:@(name) parameters:@{}];
}

extern "C" {
void CompleteLogAppleTransaction(const void* context, bool success) {
FIREBASE_ASSERT_RETURN_VOID(context != nullptr);
auto* handle_ptr = static_cast<SafeFutureHandle<void>*>(const_cast<void*>(context));

MutexLock lock(g_mutex);
if (!internal::IsInitialized()) {
delete handle_ptr;
return;
}

auto* api = internal::FutureData::Get()->api();
if (success) {
api->Complete(*handle_ptr, 0, "");
} else {
api->Complete(*handle_ptr, -1, "StoreKit 2 transaction not found.");
}
delete handle_ptr;
}
}

Future<void> LogAppleTransaction(const char* transaction_id) {
MutexLock lock(g_mutex);
FIREBASE_ASSERT_RETURN(Future<void>(), internal::IsInitialized());

auto* api = internal::FutureData::Get()->api();
const auto future_handle = api->SafeAlloc<void>(internal::kAnalyticsFnLogAppleTransaction);

if (!transaction_id) {
api->Complete(future_handle, -1, "Transaction ID is null");
return Future<void>(api, future_handle.get());
}

SafeFutureHandle<void>* handle_ptr = new SafeFutureHandle<void>(future_handle);

[StoreKit2Bridge logTransaction:SafeString(transaction_id)
context:handle_ptr
completion:CompleteLogAppleTransaction];

return Future<void>(api, future_handle.get());
}

Future<void> LogAppleTransactionLastResult() {
MutexLock lock(g_mutex);
FIREBASE_ASSERT_RETURN(Future<void>(), internal::IsInitialized());
return static_cast<const Future<void>&>(
internal::FutureData::Get()->api()->LastResult(internal::kAnalyticsFnLogAppleTransaction));
}

// Declared here so that it can be used, defined below.
NSDictionary* MapToDictionary(const std::map<Variant, Variant>& map);

Expand Down
31 changes: 31 additions & 0 deletions analytics/src/include/firebase/analytics.h
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,37 @@ void LogEvent(const char* name, const char* parameter_name,
/// @endif
void LogEvent(const char* name);

/// @brief Logs an in-app purchase transaction specifically for Apple's
/// StoreKit 2.
///
/// This function is intended for developers on iOS who process transactions
/// via custom native plugins or engines and need to securely log those
/// transactions natively through Google Analytics. The provided ID must map 1:1
/// with the native Apple `Transaction.id`. If a matching transaction is not
/// found in the Apple device's purchase history, nothing will be logged to
/// Analytics.
///
/// @note Finished consumable transactions are removed from the local
/// transaction history and cannot be retrieved by this function once
/// finished. Developers should either call this function before finishing
/// the transaction or use `FirebaseAnalytics.LogEvent` directly as a
/// fallback.
///
/// @param transaction_id The native Apple transaction identifier as a
/// null-terminated string.
///
/// @returns A Future<void> that completes successfully when the native
/// StoreKit 2 transaction is found and logged. If the transaction
/// cannot be found, the Future will complete with a non-zero error().
Future<void> LogAppleTransaction(const char* transaction_id);

/// @brief Get the result of the most recent LogAppleTransaction() call.
///
/// @returns A Future<void> that completes successfully when the native
/// StoreKit 2 transaction is found and logged. If the transaction
/// cannot be found, the Future will complete with a non-zero error().
Future<void> LogAppleTransactionLastResult();

/// @brief Log an event with associated parameters.
///
/// An Event is an important occurrence in your app that you want to
Expand Down
Loading
Loading