From aed531f25f40ba04dedf979fd7f0595a9687bc8f Mon Sep 17 00:00:00 2001 From: xyzcancer Date: Thu, 3 Sep 2026 12:28:11 +0400 Subject: [PATCH] feat(tracking): send gift_package on purchase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEV-T-691. Optional `isGiftPackage` on `PurchaseTrackingRequest` reaches the wire as `gift_package` only when true, the way `tax_free` does. Unlike `tax_free`, the key is left unreserved inside `custom` — no host is broken by the upgrade. --- CHANGELOG.md | 1 + DemoApp/REES46Demo/MainViewController.swift | 3 +- .../TrackingNamespaceTests.swift | 43 +++++++++++++++++++ .../Model/PurchaseTrackingRequest.swift | 6 ++- .../Service/impl/TrackServiceImpl.swift | 4 ++ 5 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2fb86fa..932807a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +* Purchase tracking: optional `isGiftPackage` on `PurchaseTrackingRequest` — sent as `gift_package` only when `true`, the way `tax_free` is. * Strict purchase tracking: `trackPurchase(_:recommendedBy:completion:)` with `PurchaseTrackingRequest` / `PurchaseItemRequest` (camelCase API; snake_case on the wire). Validation before send; `tax_free` only when `isTaxFree` is true; optional properties omitted when unset. Demo buttons for minimal and full payloads. * The stories block collapses when a load brings nothing to show — a block switched off in the dashboard, or a failed request — instead of leaving an empty row on the screen. New `StoriesView.hasStories`, `onStoriesCollapse`, `StoriesView.defaultHeight` and `StoriesWidget.onCollapse(_:)` for hosts that pin the height themselves or draw their own header around the block. diff --git a/DemoApp/REES46Demo/MainViewController.swift b/DemoApp/REES46Demo/MainViewController.swift index 6ba9e853..87f49305 100644 --- a/DemoApp/REES46Demo/MainViewController.swift +++ b/DemoApp/REES46Demo/MainViewController.swift @@ -1079,7 +1079,8 @@ class MainViewController: UIViewController, UIScrollViewDelegate { custom: ["demo_custom": "ios_demo"], recommendedSource: ["source_key": "source_value"], stream: "demo-stream", - segment: "A" + segment: "A", + isGiftPackage: true ) sdk.tracking.purchase(request) { result in switch result { diff --git a/DemoApp/Rees46DemoTests/TrackingNamespaceTests.swift b/DemoApp/Rees46DemoTests/TrackingNamespaceTests.swift index 791583e7..fd1ed1cc 100644 --- a/DemoApp/Rees46DemoTests/TrackingNamespaceTests.swift +++ b/DemoApp/Rees46DemoTests/TrackingNamespaceTests.swift @@ -217,6 +217,49 @@ final class TrackingNamespaceTests: XCTestCase { XCTAssertEqual(items?.first?["price"] as? Double, 49.9) } + func test_purchaseGiftPackage_reachesTheWire() { + let sdk = MockPersonalizationSDK() + let service = TrackEventServiceImpl(sdk: sdk) + let expectation = expectation(description: "purchase tracked") + + TrackingAPIImpl(trackService: service, sourceService: TrackSourceServiceSpy()) + .purchase(Self.purchaseRequest(isGiftPackage: true)) { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0) + + XCTAssertEqual(sdk.lastPostPath, "push") + XCTAssertEqual(sdk.lastPostParams?["event"] as? String, "purchase") + XCTAssertEqual(sdk.lastPostParams?["gift_package"] as? Bool, true) + } + + func test_purchaseWithoutGiftPackage_omitsTheKey() { + let sdk = MockPersonalizationSDK() + let service = TrackEventServiceImpl(sdk: sdk) + let expectation = expectation(description: "purchase tracked") + + TrackingAPIImpl(trackService: service, sourceService: TrackSourceServiceSpy()) + .purchase(Self.purchaseRequest(isGiftPackage: false)) { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: 2.0) + + XCTAssertEqual(sdk.postCallCount, 1) + XCTAssertEqual(sdk.lastPostParams?["event"] as? String, "purchase") + XCTAssertNil(sdk.lastPostParams?["gift_package"]) + } + + private static func purchaseRequest(isGiftPackage: Bool) -> PurchaseTrackingRequest { + PurchaseTrackingRequest( + orderId: "order-1", + orderPrice: 100, + items: [PurchaseItemRequest(id: "sku-1", amount: 1, price: 100)], + isGiftPackage: isGiftPackage + ) + } + func test_searchResults_reachTheWireAsCommaSeparatedList() { let sdk = MockPersonalizationSDK() let service = TrackEventServiceImpl(sdk: sdk) diff --git a/REES46/Classes/Model/PurchaseTrackingRequest.swift b/REES46/Classes/Model/PurchaseTrackingRequest.swift index fb8a06eb..a2f4be50 100644 --- a/REES46/Classes/Model/PurchaseTrackingRequest.swift +++ b/REES46/Classes/Model/PurchaseTrackingRequest.swift @@ -27,6 +27,8 @@ public struct PurchaseTrackingRequest { public let stream: String? public let segment: String? + public let isGiftPackage: Bool + public init( orderId: String, orderPrice: Double, @@ -44,7 +46,8 @@ public struct PurchaseTrackingRequest { custom: [String: Any]? = nil, recommendedSource: [String: Any]? = nil, stream: String? = nil, - segment: String? = nil + segment: String? = nil, + isGiftPackage: Bool = false ) { self.orderId = orderId self.orderPrice = orderPrice @@ -63,5 +66,6 @@ public struct PurchaseTrackingRequest { self.recommendedSource = recommendedSource self.stream = stream self.segment = segment + self.isGiftPackage = isGiftPackage } } diff --git a/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift b/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift index 3e5d2e6a..f296bdff 100644 --- a/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift +++ b/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift @@ -60,6 +60,7 @@ class TrackEventServiceImpl: TrackEventServiceProtocol { static let removeWish = "remove_wish" static let paymentType = "payment_type" static let taxFree = "tax_free" + static let giftPackage = "gift_package" static let fullCart = "full_cart" static let fullWish = "full_wish" static let wish = "wish" @@ -441,6 +442,9 @@ class TrackEventServiceImpl: TrackEventServiceProtocol { if request.isTaxFree { params[Constants.taxFree] = true } + if request.isGiftPackage { + params[Constants.giftPackage] = true + } if let promocode = request.promocode?.trimmingCharacters(in: .whitespacesAndNewlines), !promocode.isEmpty { params[Constants.promocode] = promocode }