diff --git a/.gitignore b/.gitignore index 041577f4e..7be6c0c08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Mac +.build .DS_Store .LSOverride diff --git a/BranchSDK.xcodeproj/project.pbxproj b/BranchSDK.xcodeproj/project.pbxproj index a07cf6616..5969a09d6 100644 --- a/BranchSDK.xcodeproj/project.pbxproj +++ b/BranchSDK.xcodeproj/project.pbxproj @@ -488,6 +488,9 @@ E52E5B0A2CC79E5C00F553EE /* BranchFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = E52E5B092CC79E5C00F553EE /* BranchFileLogger.m */; }; E52E5B0B2CC79E5C00F553EE /* BranchFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = E52E5B092CC79E5C00F553EE /* BranchFileLogger.m */; }; E563942E2CC7A8E600E18E65 /* BranchFileLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = E52E5B052CC79E4E00F553EE /* BranchFileLogger.h */; }; + E710E5E72EB48AB90051AE51 /* BranchEvent+StoreKit2.swift in Sources */ = {isa = PBXBuildFile; fileRef = E710E5E52EB48AB90051AE51 /* BranchEvent+StoreKit2.swift */; }; + E710E5E82EB48AB90051AE51 /* BranchEvent+StoreKit2.swift in Sources */ = {isa = PBXBuildFile; fileRef = E710E5E52EB48AB90051AE51 /* BranchEvent+StoreKit2.swift */; }; + E710E5E92EB48AB90051AE51 /* BranchEvent+StoreKit2.swift in Sources */ = {isa = PBXBuildFile; fileRef = E710E5E52EB48AB90051AE51 /* BranchEvent+StoreKit2.swift */; }; E71E396F2DD3A92900110F59 /* BNCInAppBrowser.h in Headers */ = {isa = PBXBuildFile; fileRef = E71E396D2DD3A92900110F59 /* BNCInAppBrowser.h */; }; E71E39712DD3A92900110F59 /* BNCInAppBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = E71E396E2DD3A92900110F59 /* BNCInAppBrowser.m */; }; E71E39722DD3A92900110F59 /* BNCInAppBrowser.h in Headers */ = {isa = PBXBuildFile; fileRef = E71E396D2DD3A92900110F59 /* BNCInAppBrowser.h */; }; @@ -713,6 +716,7 @@ 5FF2AFDF28E7C22100393216 /* BranchSDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BranchSDK.h; sourceTree = ""; }; E52E5B052CC79E4E00F553EE /* BranchFileLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchFileLogger.h; sourceTree = ""; }; E52E5B092CC79E5C00F553EE /* BranchFileLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchFileLogger.m; sourceTree = ""; }; + E710E5E52EB48AB90051AE51 /* BranchEvent+StoreKit2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BranchEvent+StoreKit2.swift"; sourceTree = ""; }; E71E396D2DD3A92900110F59 /* BNCInAppBrowser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BNCInAppBrowser.h; sourceTree = ""; }; E71E396E2DD3A92900110F59 /* BNCInAppBrowser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BNCInAppBrowser.m; sourceTree = ""; }; E73D027F2DEE8AE90076C3F1 /* BranchConfigurationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BranchConfigurationController.h; sourceTree = ""; }; @@ -773,6 +777,7 @@ isa = PBXGroup; children = ( 5FCDD36B2B7AC6A100EAF29F /* BranchSDK */, + E710E5E62EB48AB90051AE51 /* BranchSDK_Swift */, 5F2211702894A9C000C5B190 /* TestHost */, 5F2211872894A9C100C5B190 /* TestHostTests */, 5F2211912894A9C100C5B190 /* TestHostUITests */, @@ -1022,6 +1027,15 @@ path = Framework; sourceTree = ""; }; + E710E5E62EB48AB90051AE51 /* BranchSDK_Swift */ = { + isa = PBXGroup; + children = ( + E710E5E52EB48AB90051AE51 /* BranchEvent+StoreKit2.swift */, + ); + name = BranchSDK_Swift; + path = Sources/BranchSDK_Swift; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1660,6 +1674,7 @@ 5FCDD59A2B7AC6A400EAF29F /* BNCServerResponse.m in Sources */, 5FCDD5852B7AC6A400EAF29F /* BNCInitSessionResponse.m in Sources */, 5FCDD4412B7AC6A100EAF29F /* BNCPreferenceHelper.m in Sources */, + E710E5E92EB48AB90051AE51 /* BranchEvent+StoreKit2.swift in Sources */, 5FCDD5792B7AC6A400EAF29F /* BranchContentPathProperties.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1767,6 +1782,7 @@ 5FCDD59B2B7AC6A400EAF29F /* BNCServerResponse.m in Sources */, 5FCDD5862B7AC6A400EAF29F /* BNCInitSessionResponse.m in Sources */, 5FCDD4422B7AC6A100EAF29F /* BNCPreferenceHelper.m in Sources */, + E710E5E82EB48AB90051AE51 /* BranchEvent+StoreKit2.swift in Sources */, 5FCDD57A2B7AC6A400EAF29F /* BranchContentPathProperties.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1786,6 +1802,7 @@ 5FCDD4732B7AC6A100EAF29F /* BranchLATDRequest.m in Sources */, 5FCDD57E2B7AC6A400EAF29F /* BNCPasteboard.m in Sources */, 5F5FDA182B7DE2FE00F14A43 /* BranchLogger.m in Sources */, + E710E5E72EB48AB90051AE51 /* BranchEvent+StoreKit2.swift in Sources */, 5FCDD4612B7AC6A100EAF29F /* BNCRequestFactory.m in Sources */, 5FCDD41F2B7AC6A100EAF29F /* BNCDeepLinkViewControllerInstance.m in Sources */, 5FCDD58A2B7AC6A400EAF29F /* BNCLinkCache.m in Sources */, @@ -1884,6 +1901,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -1936,6 +1954,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -1945,6 +1964,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -1990,6 +2010,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -2027,6 +2048,7 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -2062,6 +2084,7 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -2069,20 +2092,24 @@ 5F2211692894A90500C5B190 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Debug; }; 5F22116A2894A90500C5B190 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -2344,6 +2371,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; }; @@ -2379,6 +2407,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; }; @@ -2387,60 +2416,72 @@ 5FF9DEEE28EE7C0D00D62DE1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Debug; }; 5FF9DEEF28EE7C0D00D62DE1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Release; }; 5FF9DEF228EE7C2200D62DE1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Debug; }; 5FF9DEF328EE7C2200D62DE1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Release; }; 5FF9DEF628EE7C3600D62DE1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Debug; }; 5FF9DEF728EE7C3600D62DE1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = R63EM248DP; IPHONEOS_DEPLOYMENT_TARGET = 12.0; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; }; name = Release; }; diff --git a/Package.swift b/Package.swift index 0ae916e32..2d4beb53b 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( products: [ .library( name: "BranchSDK", - targets: ["BranchSDK"]), + targets: ["BranchSDK", "BranchSwiftSDK"]), ], dependencies: [ ], @@ -37,5 +37,10 @@ let package = Package( .linkedFramework("AdServices", .when(platforms: [.iOS])) ] ), + .target( + name: "BranchSwiftSDK", + dependencies: ["BranchSDK"], + path: "Sources/BranchSDK_Swift" + ) ] ) diff --git a/Sources/BranchSDK/BranchEvent.m b/Sources/BranchSDK/BranchEvent.m index b16c813f8..2b345b458 100644 --- a/Sources/BranchSDK/BranchEvent.m +++ b/Sources/BranchSDK/BranchEvent.m @@ -157,7 +157,6 @@ + (BOOL)supportsSecureCoding { #pragma mark - BranchEvent @interface BranchEvent () -@property (nonatomic, copy) NSString* eventName; @property (strong, nonatomic) SKProductsRequest *request; @end diff --git a/Sources/BranchSDK/Public/BranchEvent.h b/Sources/BranchSDK/Public/BranchEvent.h index 1022f7599..0d52fd19e 100644 --- a/Sources/BranchSDK/Public/BranchEvent.h +++ b/Sources/BranchSDK/Public/BranchEvent.h @@ -88,7 +88,7 @@ typedef NS_ENUM(NSInteger, BranchEventAdType) { @property (nonatomic, copy) NSString*_Nullable affiliation; @property (nonatomic, copy) NSString*_Nullable eventDescription; @property (nonatomic, copy) NSString*_Nullable searchQuery; - +@property (nonatomic, copy) NSString*_Nullable eventName; @property (nonatomic, assign) BranchEventAdType adType; @property (nonatomic, strong) NSArray*_Nonnull contentItems; diff --git a/Sources/BranchSDK_Swift/BranchEvent+StoreKit2.swift b/Sources/BranchSDK_Swift/BranchEvent+StoreKit2.swift new file mode 100644 index 000000000..fd1200a56 --- /dev/null +++ b/Sources/BranchSDK_Swift/BranchEvent+StoreKit2.swift @@ -0,0 +1,117 @@ +// +// BranchEvent+StoreKit2.swift +// Branch-SDK +// +// Created by Nidhi Dixit on 09/30/25. +// Copyright 2024 Branch Metrics. All rights reserved. +// + +import Foundation +import StoreKit +import BranchSDK + +@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) +extension BranchEvent { + + /// This method extracts detailed product and transaction information from a StoreKit 2 transaction + /// and logs a Branch PURCHASE event with all the extracted information. + /// - Parameter transaction: The StoreKit 2 transaction + public func logEventForTransaction( transaction: Transaction) { + Task { + await logEventAsync(with: transaction) + } + } + + private func logEventAsync(with transaction: Transaction) async { + do { + let products = try await Product.products(for: [transaction.productID]) + guard let product = products.first else { + BranchLogger.shared().logError("Could not load product for transaction: \(transaction.productID)", error: nil) + return + } + self.populateBUO(with: transaction, product: product) + try await self.logEvent() + BranchLogger.shared().logDebug("Created and logged StoreKit 2 event: \(self.description)", error: nil) + } catch { + BranchLogger.shared().logError("Failed to load product for StoreKit 2 transaction: \(error.localizedDescription)", error: error) + } + } + + private func populateBUO(with transaction: Transaction, product: Product) { + let buo = BranchUniversalObject() + buo.canonicalIdentifier = product.id + buo.title = product.displayName + buo.contentDescription = product.description + buo.contentMetadata.quantity = Double(transaction.purchasedQuantity) + buo.contentMetadata.price = NSDecimalNumber(decimal: product.price) + buo.contentMetadata.currency = BNCCurrency(rawValue: product.priceFormatStyle.currencyCode) + buo.contentMetadata.productName = product.displayName + + var customMetadata: [String: Any] = [ + "logged_from_storekit2": true, + "product_type": product.type.rawValue, + "transaction_id": String(transaction.id), + "original_transaction_id": String(transaction.originalID), + "purchase_date": ISO8601DateFormatter().string(from: transaction.purchaseDate), + "purchased_quantity": transaction.purchasedQuantity + ] + + if let subscriptionInfo = product.subscription { + customMetadata["subscription_group_id"] = subscriptionInfo.subscriptionGroupID + customMetadata["subscription_period"] = formatSubscriptionPeriod(subscriptionInfo.subscriptionPeriod) + + if let introductoryOffer = subscriptionInfo.introductoryOffer { + customMetadata["introductory_offer_type"] = introductoryOffer.type.rawValue + customMetadata["introductory_offer_period"] = formatSubscriptionPeriod(introductoryOffer.period) + } + } + customMetadata["ownership_type"] = transaction.ownershipType.rawValue + + if let revocationDate = transaction.revocationDate { + customMetadata["revocation_date"] = ISO8601DateFormatter().string(from: revocationDate) + } + if let revocationReason = transaction.revocationReason { + customMetadata["revocation_reason"] = revocationReason.rawValue + } + + buo.contentMetadata.customMetadata = NSMutableDictionary(dictionary: customMetadata) + + self.contentItems = [buo] + self.eventName = "PURCHASE" + self.transactionID = String(transaction.id) + self.eventDescription = "StoreKit 2: \(product.displayName)" + self.currency = BNCCurrency(rawValue: product.priceFormatStyle.currencyCode) + self.revenue = NSDecimalNumber(decimal: product.price) + + switch product.type { + case .autoRenewable, .nonRenewable: + self.alias = "Subscription" + case .consumable, .nonConsumable: + self.alias = "IAP" + default: + self.alias = "IAP" + } + + var eventCustomData: [String: String] = [:] + eventCustomData["transaction_identifier"] = String(transaction.id) + eventCustomData["logged_from_storekit2"] = "true" + self.customData = eventCustomData + } + + private func formatSubscriptionPeriod(_ period: Product.SubscriptionPeriod) -> String { + let unitString: String + switch period.unit { + case .day: + unitString = "day" + case .week: + unitString = "week" + case .month: + unitString = "month" + case .year: + unitString = "year" + @unknown default: + unitString = "unknown" + } + return "\(period.value) \(unitString)\(period.value > 1 ? "s" : "")" + } +}