Skip to content
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

Merged
merged 7 commits into from
Jun 28, 2021
Merged

Conversation

taquitos
Copy link
Contributor

Fixes #553
Blocked on #595

@taquitos taquitos linked an issue Jun 26, 2021 that may be closed by this pull request
@taquitos taquitos mentioned this pull request Jun 26, 2021
…r now.

- Issue #597 filed for when we should re-enable them.
@aboedo
Copy link
Member

aboedo commented Jun 28, 2021

@taquitos is this ready for review?

Base automatically changed from entitlementinfo_tester to swift_migration June 28, 2021 16:52
@taquitos taquitos removed the blocked label Jun 28, 2021
@taquitos taquitos requested a review from a team June 28, 2021 16:59
@taquitos
Copy link
Contributor Author

@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,
Copy link
Contributor Author

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).

Copy link
Member

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

Copy link
Contributor Author

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.

@taquitos
Copy link
Contributor Author

You'll notice a few TODOs, #599

Comment on lines -1058 to +1053
37E35D29AD7BE9B81DBF6907 /* RCPromotionalOffer+Protected.h */,
3589D15524C21DBD00A65CBB /* RCAttributionTypeFactory+Protected.h */,
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Comment on lines +14 to +39
@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
}
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@objc(RCTrial) case trial = 2
}

@objcMembers @objc(RCEntitlementInfo) public class EntitlementInfo: NSObject {
Copy link
Member

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

Copy link
Member

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?

Copy link
Member

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

Copy link
Contributor

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?

Copy link
Member

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

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

Choose a reason for hiding this comment

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

ouch

Copy link
Member

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

Copy link
Contributor Author

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?
Copy link
Member

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

Copy link
Member

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

Copy link
Contributor Author

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
Copy link
Member

@aboedo aboedo Jun 28, 2021

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 158 to 167
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
}
Copy link
Member

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)

Copy link
Contributor Author

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 {
Copy link
Member

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

Copy link
Contributor Author

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.
Copy link
Member

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

Copy link
Contributor Author

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() {
Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link
Contributor Author

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.

@taquitos
Copy link
Contributor Author

@RevenueCat/sdk ready for re-review

@taquitos
Copy link
Contributor Author

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.

@taquitos taquitos merged commit db98a4a into swift_migration Jun 28, 2021
@taquitos taquitos deleted the entitlementinfo_migration branch June 28, 2021 21:31
return !(isPromo || isLifetime || hasUnsubscribed || hasBillingIssues)
}

public override var description: String {
Copy link
Member

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?

Comment on lines -309 to +310
let entitlements: [String : Purchases.EntitlementInfo] = purchaserInfoWithoutRequestData!.entitlements.active
let entitlements: [String : EntitlementInfo] = purchaserInfoWithoutRequestData!.entitlements.active
Copy link
Member

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

@taquitos taquitos mentioned this pull request Jul 1, 2021
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Migrate EntitlementInfo
3 participants