Skip to content

Conversation

@JayShortway
Copy link
Member

@JayShortway JayShortway commented Dec 15, 2025

Motivation

Mini-spec

Description

This PR introduces a new public function Attribution.setAppsFlyerConversionData(_ data: [AnyHashable: Any]?) that processes AppsFlyer conversion data and sets corresponding RevenueCat subscriber attributes.

@cursor
Copy link

cursor bot commented Dec 15, 2025

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@JayShortway JayShortway self-assigned this Dec 15, 2025
@JayShortway JayShortway added the pr:feat A new feature label Dec 15, 2025
@JayShortway JayShortway changed the title AppsFlyer attribution integration Conveniently track AppsFlyer attribution data Dec 15, 2025
@JayShortway JayShortway marked this pull request as ready for review December 15, 2025 17:37
@JayShortway JayShortway requested a review from a team as a code owner December 15, 2025 17:37
Copy link
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me! 🚢

Comment on lines +188 to +189
if let creative = stringValue(from: data, forKey: "creative")
?? stringValue(from: data, forKey: "af_creative") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, for af_ad and af_keywords, we prioritize those keys over the other parameter, but here we prioritize creative over af_creative. Just making sure that's a conscious decission :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a conscious decision by @jefago in the spec 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, thanks to ChatGPT (and validating that with the AppsFlyer documentation)

Copy link
Contributor

@rickvdl rickvdl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this should make things a lot easier for developers 🙌

setReservedAttribute(.mediaSource, value: mediaSource, appUserID: appUserID)
}

if let campaign = stringValue(from: data, forKey: "campaign") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With these wrapped in if let's we don't support 'unsetting' or clearing the value. Is that expected? (Same with the data: [AnyHashable: Any]? object itself)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For posterity, linking to my other comment 😄 RevenueCat/purchases-android#2931 (comment)

/// the `nil` case gets boxed as `Optional<T>.none` inside `Any`, rather than being absent from the dictionary.
func isNilValue(_ value: Any) -> Bool {
let mirror = Mirror(reflecting: value)
return mirror.displayStyle == .optional && mirror.children.isEmpty
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a smart one 😀 Although I would think just doing the following in stringValue(from: data) would work as well (verified it with a playground).

//let value: Optional<String> = "ja"
let value: Optional<Int> = 1
let any = value as Any
print(any)

if let value = any as? String {
    print(value)
}

if let intValue = any as? Int {
    print(intValue)
}

Did this cause any issues for you?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, for the test case here:

let dataWithNilValues: [AnyHashable: Any] = [
    "media_source": nilValue as Any,
    "campaign": "valid_campaign"
]

it would end up setting $mediaSource to the literal string "nil", which is not what we want. I got the same issue with your code, but maybe I'm doing something wrong.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha that's interesting, I think the difference is the dictionary here. That's what causing it to loose the generic type information, and it's retrieved as Optional<Any> instead of Optional<String> when it's a nil value for an optional String, and just String when it's a non-nil string.

In my test I defined it without storing it in the dictionary, which is probably why it's smart enough to preserve it's type information.

LGTM then :)

Copy link
Member

@ajpallares ajpallares Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this?

func isNilValue(_ value: Any) -> Bool {
    return (value as Any?) == nil 
}

Casting from Any to Any? always works and preserves nil

@JayShortway JayShortway force-pushed the cursor/appsflyer-attribution-integration-e2df branch from 8af94b9 to d8f7c0f Compare December 16, 2025 09:49
@JayShortway JayShortway changed the title Conveniently track AppsFlyer attribution data Adds setAppsFlyerConversionData to conveniently track AppsFlyer conversion data Dec 16, 2025
Copy link
Member

@ajpallares ajpallares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! It does make sense to add this convenience API 👍

}
if let intValue = value as? Int { return String(intValue) }
if let doubleValue = value as? Double { return String(Int(doubleValue)) }
return String(describing: value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making sure we want this. This would convert any object into a string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants