Skip to content

Commit

Permalink
Merge pull request #268 from superwall/develop
Browse files Browse the repository at this point in the history
3.12.0
  • Loading branch information
yusuftor authored Nov 22, 2024
2 parents 490de8c + 0705cb5 commit a41f529
Show file tree
Hide file tree
Showing 48 changed files with 935 additions and 362 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub.

## 3.12.0

### Enhancements

- Adds the `SuperwallOption` `shouldObservePurchases`. Set this to `true` to allow us to observe StoreKit 1 transactions you make with your app outside of Superwall. When this is enabled Superwall will not finish your external transactions. StoreKit 2 is not supported... yet.
- Adds Apple Search Ads attribution data to user attributes, which is visible on the user's page in Superwall. Attribution data will be collected if you have enabled Basic or Advanced Apple Search Ads in the Superwall dashboard settings. Advanced attribution data includes the keyword name, campaign name, bid amount, match type, and more. Otherwise, the basic attribution data will be collected, which is mostly IDs. This data will soon be added to Charts.
- Adds `isSubscribed` to product attributes so that you can use `products.primary.isSubscribed` as a dynamic value in the paywall editor.
- Adds `device.appVersionPadded` to the device properties that you can use in audience filters.
- Adds a `notificationPermissionsDenied` `PaywallOption`, which you can set to show an alert after a user denies notification permissions.

### Fixes

- Fixes issue where network requests that returned an error code weren't being retried.
- Fixes date formatting on a device property.

## 3.11.3

### Enhancements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
"repositoryURL": "https://github.com/RevenueCat/purchases-ios.git",
"state": {
"branch": null,
"revision": "c7058bfd80d7f42ca6aa392bf1fab769d1158bf1",
"version": "5.5.0"
"revision": "7b2855bbe96980952c9ebe686df1ead1b38f1f11",
"version": "5.9.0"
}
},
{
"package": "Superscript",
"repositoryURL": "https://github.com/superwall/Superscript-iOS",
"state": {
"branch": null,
"revision": "4e08d383977883aaee406b7f0e52afb81a4ea66f",
"version": "0.1.12"
"revision": "a917ad513e171e658a73f9ae7329f3cffa42f173",
"version": "0.1.16"
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ final class RCPurchaseController: PurchaseController {
Task {
for await customerInfo in Purchases.shared.customerInfoStream {
// Gets called whenever new CustomerInfo is available
let hasActiveSubscription = !customerInfo.entitlements.active.isEmpty // Why? -> https://www.revenuecat.com/docs/entitlements#entitlements
let hasActiveSubscription = !customerInfo.entitlements.activeInCurrentEnvironment.isEmpty // Why? -> https://www.revenuecat.com/docs/entitlements#entitlements
if hasActiveSubscription {
Superwall.shared.subscriptionStatus = .active
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
//

import Foundation
import Combine

final class AttributionPoster {
private let attributionFetcher = AttributionFetcher()
private let collectAdServicesAttribution: Bool
private var isCollecting = false

private unowned let storage: Storage
private unowned let network: Network
private unowned let configManager: ConfigManager
private var cancellables: [AnyCancellable] = []

private var adServicesTokenToPostIfNeeded: String? {
get async throws {
Expand All @@ -34,11 +37,17 @@ final class AttributionPoster {
}

init(
collectAdServicesAttribution: Bool,
storage: Storage
storage: Storage,
network: Network,
configManager: ConfigManager
) {
self.collectAdServicesAttribution = collectAdServicesAttribution
self.storage = storage
self.network = network
self.configManager = configManager

if #available(iOS 14.3, *) {
listenToConfig()
}

NotificationCenter.default.addObserver(
self,
Expand All @@ -48,15 +57,32 @@ final class AttributionPoster {
)
}


@available(iOS 14.3, *)
private func listenToConfig() {
configManager.configState
.compactMap { $0.getConfig() }
.first { config in
config.attribution?.appleSearchAds?.enabled == true
}
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in
Task { [weak self] in
await self?.getAdServicesTokenIfNeeded()
}
}
)
.store(in: &cancellables)
}

@objc
private func applicationWillEnterForeground() {
#if os(iOS) || os(macOS) || os(visionOS)
guard Superwall.isInitialized else {
return
}
if isCollecting {
return
}

Task(priority: .background) {
if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) {
await getAdServicesTokenIfNeeded()
Expand All @@ -70,12 +96,15 @@ final class AttributionPoster {
@available(tvOS, unavailable)
@available(watchOS, unavailable)
func getAdServicesTokenIfNeeded() async {
if isCollecting {
return
}
defer {
isCollecting = false
}
do {
isCollecting = true
guard collectAdServicesAttribution else {
guard configManager.config?.attribution?.appleSearchAds?.enabled == true else {
return
}
guard let token = try await adServicesTokenToPostIfNeeded else {
Expand All @@ -87,6 +116,9 @@ final class AttributionPoster {
await Superwall.shared.track(
InternalSuperwallEvent.AdServicesTokenRetrieval(state: .complete(token))
)

let data = await network.sendToken(token)
Superwall.shared.setUserAttributes(data)
} catch {
await Superwall.shared.track(
InternalSuperwallEvent.AdServicesTokenRetrieval(state: .fail(error))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ enum InternalSuperwallEvent {
case start(StoreProduct)
case fail(TransactionError)
case abandon(StoreProduct)
case complete(StoreProduct, StoreTransaction?)
case complete(StoreProduct, StoreTransaction?, TransactionType)
case restore(RestoreType)
case timeout
}
Expand All @@ -544,7 +544,8 @@ enum InternalSuperwallEvent {
product: product,
paywallInfo: paywallInfo
)
case let .complete(product, model):
case let .complete(product, model, _):
// TODO: In v4 add in type:
return .transactionComplete(
transaction: model,
product: product,
Expand All @@ -563,16 +564,30 @@ enum InternalSuperwallEvent {
case storeKit1 = "STOREKIT1"
case storeKit2 = "STOREKIT2"
}
enum TransactionSource: String {
enum Source: String {
case `internal` = "SUPERWALL"
case external = "APP"
}
let paywallInfo: PaywallInfo
let product: StoreProduct?
let model: StoreTransaction?
let source: TransactionSource
let transaction: StoreTransaction?
let source: Source
let isObserved: Bool
let storeKitVersion: StoreKitVersion

enum TransactionType: String {
case nonRecurringProductPurchase = "NON_RECURRING_PRODUCT_PURCHASE"
case freeTrialStart = "FREE_TRIAL_START"
case subscriptionStart = "SUBSCRIPTION_START"
}

var canImplicitlyTriggerPaywall: Bool {
if isObserved {
return false
}
return superwallEvent.canImplicitlyTriggerPaywall
}

var audienceFilterParams: [String: Any] {
switch state {
case .abandon(let product):
Expand All @@ -585,6 +600,12 @@ enum InternalSuperwallEvent {
}

func getSuperwallParameters() async -> [String: Any] {
var storefrontCountryCode = ""
var storefrontId = ""
if #available(iOS 15.0, *) {
storefrontCountryCode = await Storefront.current?.countryCode ?? ""
storefrontId = await Storefront.current?.id ?? ""
}
var eventParams: [String: Any] = [
"store": "APP_STORE",
"source": source.rawValue,
Expand All @@ -594,17 +615,25 @@ enum InternalSuperwallEvent {
switch state {
case .restore:
eventParams += await paywallInfo.eventParams(forProduct: product)
if let transactionDict = model?.dictionary(withSnakeCase: true) {
if let transactionDict = transaction?.dictionary(withSnakeCase: true) {
eventParams += transactionDict
}
eventParams["restore_via_purchase_attempt"] = model != nil
eventParams["restore_via_purchase_attempt"] = transaction != nil
return eventParams
case .complete(_, _, let type):
eventParams += [
"storefront_countryCode": storefrontCountryCode,
"storefront_id": storefrontId,
"transaction_type": type.rawValue
]
let appleSearchAttributes = Superwall.shared.userAttributes.filter { $0.key.hasPrefix("apple_search_ads_") }
eventParams += appleSearchAttributes
fallthrough
case .start,
.abandon,
.complete,
.timeout:
eventParams += await paywallInfo.eventParams(forProduct: product)
if let transactionDict = model?.dictionary(withSnakeCase: true) {
if let transactionDict = transaction?.dictionary(withSnakeCase: true) {
eventParams += transactionDict
}
return eventParams
Expand All @@ -628,12 +657,17 @@ enum InternalSuperwallEvent {
}
let paywallInfo: PaywallInfo
let product: StoreProduct
let transaction: StoreTransaction?
var audienceFilterParams: [String: Any] {
return paywallInfo.audienceFilterParams()
}

func getSuperwallParameters() async -> [String: Any] {
return await paywallInfo.eventParams(forProduct: product)
var params = await paywallInfo.eventParams(forProduct: product)
if let transactionDict = transaction?.dictionary(withSnakeCase: true) {
params += transactionDict
}
return params
}
}

Expand All @@ -652,12 +686,17 @@ enum InternalSuperwallEvent {
}
let paywallInfo: PaywallInfo
let product: StoreProduct
let transaction: StoreTransaction?
var audienceFilterParams: [String: Any] {
return paywallInfo.audienceFilterParams()
}

func getSuperwallParameters() async -> [String: Any] {
return await paywallInfo.eventParams(forProduct: product)
var params = await paywallInfo.eventParams(forProduct: product)
if let transactionDict = transaction?.dictionary(withSnakeCase: true) {
params += transactionDict
}
return params
}
}

Expand All @@ -670,12 +709,17 @@ enum InternalSuperwallEvent {
}
let paywallInfo: PaywallInfo
let product: StoreProduct
let transaction: StoreTransaction?
var audienceFilterParams: [String: Any] {
return paywallInfo.audienceFilterParams()
}

func getSuperwallParameters() async -> [String: Any] {
return await paywallInfo.eventParams(forProduct: product)
var params = await paywallInfo.eventParams(forProduct: product)
if let transactionDict = transaction?.dictionary(withSnakeCase: true) {
params += transactionDict
}
return params
}
}

Expand Down
16 changes: 16 additions & 0 deletions Sources/SuperwallKit/Config/Models/Attribution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// File.swift
// SuperwallKit
//
// Created by Yusuf Tör on 20/11/2024.
//

import Foundation

struct Attribution: Codable, Equatable {
let appleSearchAds: AppleSearchAds?
}

struct AppleSearchAds: Codable, Equatable {
let enabled: Bool
}
11 changes: 9 additions & 2 deletions Sources/SuperwallKit/Config/Models/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct Config: Codable, Equatable {
var featureFlags: FeatureFlags
var preloadingDisabled: PreloadingDisabled
var requestId: String?
var attribution: Attribution?
var allComputedProperties: [ComputedPropertyRequest] {
return triggers.flatMap {
$0.rules.flatMap {
Expand All @@ -35,6 +36,7 @@ struct Config: Codable, Equatable {
case appSessionTimeout = "appSessionTimeoutMs"
case featureFlags = "toggles"
case preloadingDisabled = "disablePreload"
case attribution = "attributionOptions"
}

init(from decoder: Decoder) throws {
Expand All @@ -47,6 +49,7 @@ struct Config: Codable, Equatable {
appSessionTimeout = try values.decode(Milliseconds.self, forKey: .appSessionTimeout)
featureFlags = try FeatureFlags(from: decoder)
preloadingDisabled = try values.decode(PreloadingDisabled.self, forKey: .preloadingDisabled)
attribution = try values.decodeIfPresent(Attribution.self, forKey: .attribution)

let localization = try values.decode(LocalizationConfig.self, forKey: .localization)
locales = Set(localization.locales.map { $0.locale })
Expand All @@ -62,6 +65,7 @@ struct Config: Codable, Equatable {
try container.encode(logLevel, forKey: .logLevel)
try container.encode(appSessionTimeout, forKey: .appSessionTimeout)
try container.encode(preloadingDisabled, forKey: .preloadingDisabled)
try container.encodeIfPresent(attribution, forKey: .attribution)

let localizationConfig = LocalizationConfig(locales: locales.map { LocalizationConfig.LocaleConfig(locale: $0) })
try container.encode(localizationConfig, forKey: .localization)
Expand All @@ -78,7 +82,8 @@ struct Config: Codable, Equatable {
locales: Set<String>,
appSessionTimeout: Milliseconds,
featureFlags: FeatureFlags,
preloadingDisabled: PreloadingDisabled
preloadingDisabled: PreloadingDisabled,
attribution: Attribution
) {
self.buildId = buildId
self.triggers = triggers
Expand All @@ -88,6 +93,7 @@ struct Config: Codable, Equatable {
self.appSessionTimeout = appSessionTimeout
self.featureFlags = featureFlags
self.preloadingDisabled = preloadingDisabled
self.attribution = attribution
}
}

Expand All @@ -102,7 +108,8 @@ extension Config: Stubbable {
locales: [],
appSessionTimeout: 3600000,
featureFlags: .stub(),
preloadingDisabled: .stub()
preloadingDisabled: .stub(),
attribution: .init(appleSearchAds: .init(enabled: true))
)
}
}
Loading

0 comments on commit a41f529

Please sign in to comment.