-
Notifications
You must be signed in to change notification settings - Fork 316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrates RCEntitlementInfo #596
Conversation
… tester for EntitlementInfo
…r now. - Issue #597 filed for when we should re-enable them.
@taquitos is this ready for review? |
Just resolved conflicts, and the PR it was blocked on has merged, we're good to review 🎉 |
self.productIdentifier = productIdString! | ||
|
||
let isSandbox: Bool | ||
if maybeSandbox?.responds(to: #selector(getter: NSNumber.boolValue)) ?? false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a fun one, NSNumber and NSString have the same -boolValue
method so in ObjC the message passing worked regardless of type because types are just a suggestion anyways, right? In swift, I had to decide what type it was. First I was like "yeah, this is probably a string" but that didn't work. I'm not going to assume it's an NSNumber or an NSString, so I'll just check for NSNumber
first (because that's the most likely and I like to pre-optimize).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a comment before reading this. I think we should be able to figure out its concrete type since it's coming from our own rest api
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just asked in our company chat.
You'll notice a few TODOs, #599 |
37E35D29AD7BE9B81DBF6907 /* RCPromotionalOffer+Protected.h */, | ||
3589D15524C21DBD00A65CBB /* RCAttributionTypeFactory+Protected.h */, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this only alphabetization? or are there files added / removed aside from RCEntitlementInfo headers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alphabetization. It's an old habit of mine... everything gets alphabetized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love having stuff alphabetized, was just asking because it's hard to figure out what the actual diff is, though.
@objc(RCStore) public enum Store: Int { | ||
/// For entitlements granted via Apple App Store. | ||
@objc(RCAppStore) case appStore = 0 | ||
/// For entitlements granted via Apple Mac App Store. | ||
@objc(RCMacAppStore) case macAppStore = 1 | ||
/// For entitlements granted via Google Play Store. | ||
@objc(RCPlayStore) case playStore = 2 | ||
/// For entitlements granted via Stripe. | ||
@objc(RCStripe) case stripe = 3 | ||
/// For entitlements granted via a promo in RevenueCat. | ||
@objc(RCPromotional) case promotional = 4 | ||
/// For entitlements granted via an unknown store. | ||
@objc(RCUnknownStore) case unknownStore = 5 | ||
} | ||
|
||
/** | ||
Enum of supported period types for an entitlement. | ||
*/ | ||
@objc(RCPeriodType) public enum PeriodType: Int { | ||
/// If the entitlement is not under an introductory or trial period. | ||
@objc(RCNormal) case normal = 0 | ||
/// If the entitlement is under a introductory price period. | ||
@objc(RCIntro) case intro = 1 | ||
/// If the entitlement is under a trial period. | ||
@objc(RCTrial) case trial = 2 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we take the opportunity to move these out to their own files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See: #596 (comment)
@objc(RCTrial) case trial = 2 | ||
} | ||
|
||
@objcMembers @objc(RCEntitlementInfo) public class EntitlementInfo: NSObject { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, didn't know about @objcMembers
but it seems super useful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume it only applies to the current declaration? i.e.: it wouldn't apply for extensions unless we also mark those as @objcMembers
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, perhaps we should strive to only add in this declaration the stuff we actually want to expose to obj-c, and move the class funcs to an extension, since they don't even need to be obj-c compatible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aboedo what wouldn't need to be exposed to obj-c? anything we wouldn't want exposed to users? in that case, could we not just make it private?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like with everything, we should only expose what we absolutely need to.
In this case, we need to expose whatever was a part of the original API in objective-c.
and yes, making it either private
or internal
would do the trick, I left a comment related to that in one of the methods that might not need to be exposed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@objcMembers
:
Apply this attribute to a class declaration, to implicitly apply the objc attribute to all Objective-C compatible members of the class, its extensions, its subclasses, and all of the extensions of its subclasses.
Yeah, so maybe I'll do it member-wise, instead 😄
/** | ||
The latest purchase or renewal date for the entitlement. | ||
*/ | ||
public let latestPurchaseDate: Date? // TODO: This used to be NON-NULL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you elaborate on why they're optional now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our date parsing code returns either a date or a nil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ouch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, this is a tricky case, but we might as well solve it since it's bound to come up again:
- in obj-c, we're declaring this as non-null, but it's actually nullable.
- it's nullable in the rest API, so we should try to reflect that (i.e.: the bug is in the declaration, not the implementation)
Currently, this means:
- for obj-c clients, this is bad, but it's objc, what's nullability anyway in obj-c land.
- for swift clients, this is terrible. The property will show up as non-null, they won't be able to check for nullability, but if it's
nil
, app crashes.
So I'd say we should just go ahead and make it optional, without even the comment. It's technically a change of the API, but it's more of a fix than an actual change.
We had a similar thing happen with the shared
instance of purchases recently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I 100% agree with this approach. It changes the Swift API, but it is a fix. I also think we should talk about revving RC a major version, but with enough comms that folks understand it was additive for ObjC, and some small API fixes around nullablility for swift.
The expiration date for the entitlement, can be `nil` for lifetime access. | ||
If the `periodType` is `trial`, this is the trial expiration date. | ||
*/ | ||
public private(set) var expirationDate: Date? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expirationDate
is only set at init
, right? I don't think this needs to be var
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same for unsubscribeDetectedAt
and billingIssueDetectedAt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
let originalPurchaseDateString = productData["original_purchase_date"] as? String | ||
let productExpiresDateString = productData["expires_date"] as? String | ||
let storeString = productData["store"] as? String | ||
let maybeSandbox = productData["is_sandbox"] as? NSObject // This could be a String or NSNumber |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preeetty sure we can trace it back to find out its actual type. This is a value that comes from our REST API.
So if you debug the value as it comes in you can see whether it gets translated into String
or NSNumber
(I don't recall which one it is tbh)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
let isSandbox: Bool | ||
if maybeSandbox?.responds(to: #selector(getter: NSNumber.boolValue)) ?? false, | ||
let unwrapped = maybeSandbox as? NSNumber { | ||
isSandbox = unwrapped.boolValue | ||
} else if maybeSandbox?.responds(to: #selector(getter: NSString.boolValue)) ?? false, | ||
let unwrapped = maybeSandbox as? NSString { | ||
isSandbox = unwrapped.boolValue | ||
} else { | ||
isSandbox = false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although I love how thoughtful this translation is, and it is absolutely correct, I think we should just figure out which type it is so that we can remove the complexity here, since it's a value that is under our control (if this was coming from a third-party API, or something outside our control, I'd advocate for keeping this logic)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will update since we can see it's definitely returned as a bool
and not a string 🎉
return description | ||
} | ||
|
||
public override func isEqual(_ object: Any?) -> Bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not needed for this PR, but we might as well add conformity to Equatable
and use this method for both
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// EntitlementInfoEnums.swift | ||
// PurchasesCoreSwift | ||
// | ||
// Created by Joshua Liebowitz on 6/24/21. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the intent of moving this into the other file to keep the same file structure that we had before? I actually liked having these separate, since the other file is subject to grow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent is to maintain the same file structure as before. There's enough moving pieces I want to be sure our first pass is just migration. We definitely should consider organization as an optimization step 😄
@@ -441,6 +441,30 @@ class EntitlementInfosTests: XCTestCase { | |||
verifyProduct() | |||
} | |||
|
|||
func testSubscriptionIsSandboxString() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- removed since we now are sure we're getting a
bool
and not a string for this value.
@RevenueCat/sdk ready for re-review |
Getting into git rebase trouble with some review chains. Merging, but feel free to leave any more comments and I'll continue to address them. |
return !(isPromo || isLifetime || hasUnsubscribed || hasBillingIssues) | ||
} | ||
|
||
public override var description: String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually try to put public stuff at the top, then internal, then private, so that most readers get to all the public stuff right away.
For swift I usually make an exception for protocol conformance, which I do in an extension, to keep the protocol methods in one place.
And for purely private stuff I usually end up moving it to a private extension as well for the sake of keeping the main declaration as clean as possible... with the obvious limitation being that stored properties have to be in the main declaration as well, so it ends up being a bit mixed.
I don't think it's urgent, but perhaps we could come up with a consistent pattern and use that going forward for swift files?
let entitlements: [String : Purchases.EntitlementInfo] = purchaserInfoWithoutRequestData!.entitlements.active | ||
let entitlements: [String : EntitlementInfo] = purchaserInfoWithoutRequestData!.entitlements.active |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm somewhat concerned about this bit - we have a bit of a conflict where both the framework and its main class are called Purchases
, so folks had to disambiguate using Purchases.
, which won't be needed anymore... and I'm not sure it would compile if you do add it? We should try to figure out what implications this'll have (if any) for devs
Fixes #553
Blocked on #595