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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/analytics/RNFBAnalytics.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Pod::Spec.new do |s|
s.ios.deployment_target = firebase_ios_target
s.macos.deployment_target = firebase_macos_target
s.tvos.deployment_target = firebase_tvos_target
s.source_files = 'ios/**/*.{h,m}'
s.source_files = 'ios/**/*.{h,m,mm,cpp,swift}'
s.swift_version = '5.0'

s.dependency 'RNFBApp'

Expand Down
5 changes: 5 additions & 0 deletions packages/analytics/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import analytics, {
initializeAnalytics,
getGoogleAnalyticsClientId,
logEvent,
logTransaction,
setAnalyticsCollectionEnabled,
setSessionTimeoutDuration,
getAppInstanceId,
Expand Down Expand Up @@ -1001,6 +1002,10 @@ describe('Analytics', function () {
it('`settings` function is properly exposed to end user', function () {
expect(settings).toBeDefined();
});

it('`logTransaction` function is properly exposed to end user', function () {
expect(logTransaction).toBeDefined();
});
});

describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ public void setConsent(ReadableMap consentSettings, Promise promise) {
});
}

/** Rejects with unimplemented; logTransaction is iOS-only (StoreKit 2). */
@ReactMethod
public void logTransaction(String transactionId, Promise promise) {
rejectPromiseWithCodeAndMessage(
promise, "unimplemented", "logTransaction is only available on iOS");
}

private Bundle toBundle(ReadableMap readableMap) {
Bundle bundle = Arguments.toBundle(readableMap);
if (bundle == null) {
Expand Down
62 changes: 62 additions & 0 deletions packages/analytics/e2e/analytics.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,68 @@ describe('analytics()', function () {
});
});

describe('logTransaction()', function () {
it('throws when transactionId is not a valid numeric string', async function () {
if (!Platform.ios) {
this.skip();
}
try {
const { getAnalytics, logTransaction } = analyticsModular;
await logTransaction(getAnalytics(), 'not_a_number');
fail('Should have thrown an error');
} catch (e) {
if (!(e && e.message && e.message.includes('Invalid transactionId'))) {
throw e;
}
}
});

it('throws when transactionId is valid format but transaction not found in StoreKit', async function () {
if (!Platform.ios) {
this.skip();
}
try {
const { getAnalytics, logTransaction } = analyticsModular;
await logTransaction(getAnalytics(), '12345');
fail('Should have thrown an error');
} catch (e) {
if (!(e && e.message && e.message.includes('Transaction not found'))) {
throw e;
}
}
});

it('rejects with unimplemented on Android', async function () {
if (!Platform.android) {
this.skip();
}
try {
const { getAnalytics, logTransaction } = analyticsModular;
await logTransaction(getAnalytics(), '12345');
fail('Should have thrown an error');
} catch (e) {
if (!(e && e.message && e.message.includes('logTransaction is only available on iOS'))) {
throw e;
}
}
});

it('rejects with unimplemented on web (other platform)', async function () {
if (!Platform.other) {
this.skip();
}
try {
const { getAnalytics, logTransaction } = analyticsModular;
await logTransaction(getAnalytics(), '12345');
fail('Should have thrown an error');
} catch (e) {
if (!(e && e.message && e.message.includes('logTransaction is only available on iOS'))) {
throw e;
}
}
});
});

describe('getGoogleAnalyticsClientId()', function () {
it('Error for getGoogleAnalyticsClientId() on non-other platforms', async function () {
if (Platform.other) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
2744B98621F45429004F8E3F /* RNFBAnalyticsModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 2744B98521F45429004F8E3F /* RNFBAnalyticsModule.m */; };
2744B99021F45429004F8E3F /* RNFBAnalyticsLogTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2744B98F21F45429004F8E3F /* RNFBAnalyticsLogTransaction.swift */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -26,6 +27,7 @@
2744B98221F45429004F8E3F /* libRNFBAnalytics.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNFBAnalytics.a; sourceTree = BUILT_PRODUCTS_DIR; };
2744B98421F45429004F8E3F /* RNFBAnalyticsModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNFBAnalyticsModule.h; path = RNFBAnalytics/RNFBAnalyticsModule.h; sourceTree = SOURCE_ROOT; };
2744B98521F45429004F8E3F /* RNFBAnalyticsModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNFBAnalyticsModule.m; path = RNFBAnalytics/RNFBAnalyticsModule.m; sourceTree = SOURCE_ROOT; };
2744B98F21F45429004F8E3F /* RNFBAnalyticsLogTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RNFBAnalyticsLogTransaction.swift; path = RNFBAnalytics/RNFBAnalyticsLogTransaction.swift; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -54,6 +56,7 @@
2744B98C21F45C64004F8E3F /* common */,
2744B98421F45429004F8E3F /* RNFBAnalyticsModule.h */,
2744B98521F45429004F8E3F /* RNFBAnalyticsModule.m */,
2744B98F21F45429004F8E3F /* RNFBAnalyticsLogTransaction.swift */,
);
path = RNFBAnalytics;
sourceTree = "<group>";
Expand Down Expand Up @@ -125,6 +128,7 @@
buildActionMask = 2147483647;
files = (
2744B98621F45429004F8E3F /* RNFBAnalyticsModule.m in Sources */,
2744B99021F45429004F8E3F /* RNFBAnalyticsLogTransaction.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -166,6 +170,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
Expand Down Expand Up @@ -200,6 +205,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library 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
import FirebaseAnalytics
import StoreKit

/// Swift wrapper for logging a verified StoreKit 2 transaction to Firebase Analytics.
/// Accessible from Objective-C; necessary because StoreKit 2 and Analytics.logTransaction use Swift async APIs.
/// Call from ObjC only when @available(iOS 15.0, *) (see RNFBFunctionsStreamHandler pattern).
@available(iOS 15.0, macOS 12.0, *)
@objcMembers public class RNFBAnalyticsLogTransaction: NSObject {

private static let kCode = "firebase_analytics"
private var logTask: Task<Void, Never>?

/// Resolve/reject types matching RCTPromiseResolveBlock / RCTPromiseRejectBlock for React Native bridge.
@objc public func logTransaction(
transactionId: String,
resolve: @escaping (Any?) -> Void,
reject: @escaping (String, String, NSError?) -> Void
) {
logTask = Task {
await performLogTransaction(transactionId: transactionId, resolve: resolve, reject: reject)
logTask = nil
}
}

private func performLogTransaction(
transactionId: String,
resolve: @escaping (Any?) -> Void,
reject: @escaping (String, String, NSError?) -> Void
) async {
do {
guard let id = UInt64(transactionId) else {
await MainActor.run { reject(Self.kCode, "Invalid transactionId", nil) }
return
}

var foundTransaction: StoreKit.Transaction?
for await result in StoreKit.Transaction.all {
switch result {
case let .verified(transaction):
if transaction.id == id {
foundTransaction = transaction
break
}
case .unverified:
continue
}
}

guard let transaction = foundTransaction else {
await MainActor.run { reject(Self.kCode, "Transaction not found", nil) }
return
}

Analytics.logTransaction(transaction)
await MainActor.run { resolve(NSNull()) }
} catch {

Check warning on line 74 in packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsLogTransaction.swift

View workflow job for this annotation

GitHub Actions / iOS (release, 0)

'catch' block is unreachable because no errors are thrown in 'do' block

Check warning on line 74 in packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsLogTransaction.swift

View workflow job for this annotation

GitHub Actions / iOS (release, 0)

'catch' block is unreachable because no errors are thrown in 'do' block

Check warning on line 74 in packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsLogTransaction.swift

View workflow job for this annotation

GitHub Actions / iOS (debug, 0)

'catch' block is unreachable because no errors are thrown in 'do' block
let nsError = error as NSError
await MainActor.run { reject(Self.kCode, error.localizedDescription, nsError) }
}
}
}
13 changes: 13 additions & 0 deletions packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#import <Firebase/Firebase.h>
#import <React/RCTUtils.h>

#import <RNFBAnalytics/RNFBAnalytics-Swift.h>
#import <RNFBApp/RNFBSharedUtils.h>
#import "RNFBAnalyticsModule.h"

Expand Down Expand Up @@ -212,6 +213,18 @@ - (dispatch_queue_t)methodQueue {
return resolve([NSNull null]);
}

RCT_EXPORT_METHOD(logTransaction
: (NSString *)transactionId resolver
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject) {
if (@available(iOS 15.0, macOS 12.0, *)) {
RNFBAnalyticsLogTransaction *handler = [[RNFBAnalyticsLogTransaction alloc] init];
[handler logTransactionWithTransactionId:transactionId resolve:resolve reject:reject];
} else {
reject(@"firebase_analytics", @"logTransaction() is only supported on iOS 15.0 or newer", nil);
}
}

RCT_EXPORT_METHOD(setConsent
: (NSDictionary *)consentSettings resolver
: (RCTPromiseResolveBlock)resolve rejecter
Expand Down
10 changes: 10 additions & 0 deletions packages/analytics/lib/modular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,16 @@ export function logEvent(
return analytics.logEvent.call(analytics, name, params, options, MODULAR_DEPRECATION_ARG);
}

/** Logs verified in-app purchase events in Google Analytics for Firebase
* after a purchase is successful.
* Modular API only; iOS only (StoreKit 2). On Android, the native module rejects with unimplemented.
*/
export function logTransaction(analytics: Analytics, transaction_id: string): Promise<void> {
return (
analytics as unknown as { native: { logTransaction(id: string): Promise<void> } }
).native.logTransaction(transaction_id);
}

/**
* If true, allows the device to collect analytical data and send it to Firebase. Useful for GDPR.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/analytics/lib/types/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@ export interface Analytics extends ReactNativeFirebase.FirebaseModule {
logShare(object: ShareEventParameters): Promise<void>;
logSignUp(object: SignUpEventParameters): Promise<void>;
logSpendVirtualCurrency(object: SpendVirtualCurrencyEventParameters): Promise<void>;
logTransaction(transaction_id: string): Promise<void>;
logTutorialBegin(): Promise<void>;
logTutorialComplete(): Promise<void>;
logUnlockAchievement(object: UnlockAchievementEventParameters): Promise<void>;
Expand Down
2 changes: 2 additions & 0 deletions packages/analytics/lib/types/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ export interface RNFBAnalyticsModule {
setSessionTimeoutDuration(): Promise<null>;
getAppInstanceId(): Promise<string | null>;
getSessionId(): Promise<null>;
/** Rejects with unimplemented; logTransaction is iOS-only (StoreKit 2). */
logTransaction(transactionId: string): Promise<null>;
}
5 changes: 5 additions & 0 deletions packages/analytics/lib/web/RNFBAnalyticsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,9 @@ export const RNFBAnalyticsModule: RNFBAnalyticsModuleType = {
return null;
});
},

logTransaction(_transactionId: string): Promise<null> {
// Unimplemented on web; iOS-only (StoreKit 2).
return Promise.reject(new Error('logTransaction is only available on iOS')) as Promise<null>;
},
};
4 changes: 2 additions & 2 deletions tests/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2600,15 +2600,15 @@ SPEC CHECKSUMS:
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce
RNDeviceInfo: 36d7f232bfe7c9b5c494cb7793230424ed32c388
RNFBAnalytics: 60b86a7ea41673260baa68840edddc62c1b1144d
RNFBAnalytics: f71966fbe78605d04fdc56bed2b2c7cb23313aac
RNFBApp: 395e085384fd7e9337db789bd712ecc02677b5a3
RNFBAppCheck: efbd45070934d05971ae3441dbeec70032078e68
RNFBAppDistribution: 0bab95533d4f38c26e7021d1612fe414fa04de0d
RNFBAuth: 0ce38d568fe61d56cad1b63fb8152701b7a16d40
RNFBCrashlytics: dcea974933abed951adb01ae1abed06a69e11ba2
RNFBDatabase: 29922d67fadf86b4ed27385dbd18897f736ee15a
RNFBFirestore: 67cfdbcfbb5ad453d572d982d7505ba23adc8f40
RNFBFunctions: 9b8b71cd156c4cc6d0a739cea333e1897a755888
RNFBFunctions: 05796dbcd082a53e320abf3fc139c0a0bbceec07
RNFBInAppMessaging: bc36ade0080fb8275af3b60f4faf0181dec4be99
RNFBInstallations: ab14ea26b8aaff7695a2eaeab616e7758a50e168
RNFBMessaging: d70c6ceef3f3a51734975e9cbaab3a7483274ff0
Expand Down
Loading