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

Offering metadata #2498

Merged
merged 21 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/Networking/Responses/OfferingsResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ struct OfferingsResponse {
let identifier: String
let description: String
let packages: [Package]
@DefaultDecodable.EmptyDictionary
var metadata: [String: AnyDecodable]

}

Expand Down
33 changes: 32 additions & 1 deletion Sources/Purchasing/Offering.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ import Foundation
*/
@objc public let serverDescription: String

private let _metadata: Metadata

/**
Offering metadata defined in RevenueCat dashboard.
*/
@objc public var metadata: [String: Any] { self._metadata.data }
NachoSoto marked this conversation as resolved.
Show resolved Hide resolved

/**
Array of ``Package`` objects available for purchase.
*/
Expand Down Expand Up @@ -110,10 +117,11 @@ import Foundation
}

// swiftlint:disable:next cyclomatic_complexity
init(identifier: String, serverDescription: String, availablePackages: [Package]) {
init(identifier: String, serverDescription: String, metadata: [String: Any], availablePackages: [Package]) {
self.identifier = identifier
self.serverDescription = serverDescription
self.availablePackages = availablePackages
self._metadata = Metadata(data: metadata)

var foundPackages: [PackageType: Package] = [:]

Expand Down Expand Up @@ -165,6 +173,21 @@ import Foundation

}

extension Offering {
NachoSoto marked this conversation as resolved.
Show resolved Hide resolved

/**
- Returns: the `metadata` value associated to `key` for the expected type,
or `defaultValue` if not found, or it's not the expected type.
*/
public func getMetadataValue<T>(for key: String, default: T) -> T {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this also support null default values? Like let metadataInt: Int? = offering.getMetadataValue(for: "key", default: nil). I think that could be useful.

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 should be let me write another test for that! 😊

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, this works!

let optionalInt: Int? = offeringA.getMetadataValue(for: "optionalInt", default: nil)
expect(optionalInt).to(beNil())

guard let rawValue = self.metadata[key], let value = rawValue as? T else {
return `default`
}
return value
}

}

extension Offering: Identifiable {

/// The stable identity of the entity associated with this instance.
Expand All @@ -176,6 +199,14 @@ extension Offering: Sendable {}

// MARK: - Private

private extension Offering {

struct Metadata {
let data: [String: Any]
}

}

private extension Offering {

static func checkForNilAndLogReplacement(previousPackages: [PackageType: Package], newPackage: Package) {
Expand Down
1 change: 1 addition & 0 deletions Sources/Purchasing/OfferingsFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class OfferingsFactory {

return Offering(identifier: offering.identifier,
serverDescription: offering.description,
metadata: offering.metadata.mapValues(\.asAny),
availablePackages: availablePackages)
}

Expand Down
3 changes: 2 additions & 1 deletion Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOfferingAPI.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ + (void)checkAPI {
RCPackage *p = [o packageWithIdentifier:nil];
p = [o packageWithIdentifier:@""];
RCPackage *ok = [o objectForKeyedSubscript:@""];
NSDictionary<NSString *, id> *md = o.metadata;

NSLog(o, i, sd, a, l, an, s, t, tm, m, w, p, ok);
NSLog(o, i, sd, a, l, an, s, t, tm, m, w, p, ok, md);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func checkOfferingAPI() {
var pPack: Package? = off.package(identifier: "")
pPack = off.package(identifier: nil)
let package: Package? = off[""]
let metadata: [String: Any] = off.metadata
let metadataString: String = off.getMetadataValue(for: "", default: "")
let metadataInt: Int = off.getMetadataValue(for: "", default: 0)

print(off!, ident, sDesc, aPacks, lPack!, annPack!, smPack!, thmPack!, twmPack!, mPack!, wPack!, pPack!, package!)
print(off!, ident, sDesc, aPacks, lPack!, annPack!, smPack!, thmPack!,
twmPack!, mPack!, wPack!, pPack!, package!, metadata, metadataString, metadataInt)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
{
"description" : "standard set of packages",
"identifier" : "default",
"metadata" : {
"meta" : "data"
},
"packages" : [
{
"identifier" : "$rc_monthly",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
{
"description" : "standard set of packages",
"identifier" : "default",
"metadata" : {
Copy link
Contributor

Choose a reason for hiding this comment

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

Amazing

"meta" : "data"
},
"packages" : [
{
"identifier" : "$rc_monthly",
Expand All @@ -22,6 +25,9 @@
{
"description" : "Coins",
"identifier" : "coins",
"metadata" : {

},
"packages" : [
{
"identifier" : "10.coins",
Expand Down
1 change: 1 addition & 0 deletions Tests/UnitTests/Mocks/MockOfferingsFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class MockOfferingsFactory: OfferingsFactory {
"base": Offering(
identifier: "base",
serverDescription: "This is the base offering",
metadata: [:],
availablePackages: [
Package(identifier: "$rc_monthly",
packageType: PackageType.monthly,
Expand Down
31 changes: 31 additions & 0 deletions Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,37 @@
"platform_product_identifier": "com.revenuecat.other_product"
}
]
},
{
"description": "offering with metadata",
"identifier": "metadata",
"metadata": {
"int": 5,
"double": 5.5,
"boolean": true,
"string": "five",
"array": ["five"],
"dictionary": {
"string": "five"
}
},
"packages": [
{
"identifier": "$rc_lifetime",
"platform_product_identifier": "com.revenuecat.other_product"
}
]
},
{
"description": "offering with null metadata",
"identifier": "nullmetadata",
"metadata": null,
"packages": [
{
"identifier": "$rc_lifetime",
"platform_product_identifier": "com.revenuecat.other_product"
}
]
}
]
}
58 changes: 53 additions & 5 deletions Tests/UnitTests/Networking/Responses/OfferingsDecodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,19 @@ class OfferingsDecodingTests: BaseHTTPResponseTest {

func testDecodesAllOfferings() throws {
expect(self.response.currentOfferingId) == "default"
expect(self.response.offerings).to(haveCount(2))
expect(self.response.offerings).to(haveCount(4))
}

func testDecodesFirstOffering() throws {
let offering = try XCTUnwrap(self.response.offerings.first)

expect(offering.identifier) == "default"
expect(offering.description) == "standard set of packages"
expect(offering.metadata) == [:]
expect(offering.packages).to(haveCount(2))

let package1 = offering.packages[0]
let package2 = offering.packages[1]
let package1 = try XCTUnwrap(offering.packages.first)
let package2 = try XCTUnwrap(offering.packages[safe: 1])

expect(package1.identifier) == PackageType.monthly.description
expect(package1.platformProductIdentifier) == "com.revenuecat.monthly_4.99.1_week_intro"
Expand All @@ -48,13 +49,51 @@ class OfferingsDecodingTests: BaseHTTPResponseTest {
}

func testDecodesSecondOffering() throws {
let offering = try XCTUnwrap(self.response.offerings.last)
let offering = try XCTUnwrap(self.response.offerings[safe: 1])

expect(offering.identifier) == "alternate"
expect(offering.description) == "alternate offering"
expect(offering.metadata) == [:]
expect(offering.packages).to(haveCount(1))

let package = offering.packages[0]
let package = try XCTUnwrap(offering.packages.first)

expect(package.identifier) == PackageType.lifetime.description
expect(package.platformProductIdentifier) == "com.revenuecat.other_product"
}

func testDecodesMetadataOffering() throws {
let offering = try XCTUnwrap(self.response.offerings[safe: 2])

expect(offering.identifier) == "metadata"
expect(offering.description) == "offering with metadata"
expect(offering.metadata) == [
"int": 5,
"double": 5.5,
"boolean": true,
"string": "five",
"array": ["five"],
"dictionary": [
"string": "five"
]
]
expect(offering.packages).to(haveCount(1))

let package = try XCTUnwrap(offering.packages.first)

expect(package.identifier) == PackageType.lifetime.description
expect(package.platformProductIdentifier) == "com.revenuecat.other_product"
}

func testDecodesNullMetadataOffering() throws {
let offering = try XCTUnwrap(self.response.offerings[safe: 3])

expect(offering.identifier) == "nullmetadata"
expect(offering.description) == "offering with null metadata"
expect(offering.metadata) == [:]
expect(offering.packages).to(haveCount(1))

let package = try XCTUnwrap(offering.packages.first)

expect(package.identifier) == PackageType.lifetime.description
expect(package.platformProductIdentifier) == "com.revenuecat.other_product"
Expand All @@ -65,3 +104,12 @@ class OfferingsDecodingTests: BaseHTTPResponseTest {
}

}

private extension Collection {

/// Returns the element at the specified index if it exists, otherwise nil.
subscript (safe index: Index) -> Element? {
Copy link
Contributor

Choose a reason for hiding this comment

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

Cool this is exactly what I had in mind 👍🏻
I can extract this an add unit tests for it in a separate PR since I think it's useful to have.

return indices.contains(index) ? self[index] : nil
}

}
1 change: 1 addition & 0 deletions Tests/UnitTests/Purchasing/OfferingsManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ private extension OfferingsManagerTests {
Offering(
identifier: offering.identifier,
serverDescription: offering.description,
metadata: offering.metadata,
availablePackages: offering.packages.map { package in
.init(
identifier: package.identifier,
Expand Down
59 changes: 59 additions & 0 deletions Tests/UnitTests/Purchasing/OfferingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,65 @@ class OfferingsTests: TestCase {
expect(offerings.current) == offerings["offering_a"]
}

func testOfferingsWithMetadataIsCreated() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻

let metadata: [String: AnyDecodable] = [
"int": 5,
"double": 5.5,
"boolean": true,
"string": "five",
"array": ["five"],
"dictionary": [
"string": "five"
]
]

let annualProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.annual")
let monthlyProduct = MockSK1Product(mockProductIdentifier: "com.myproduct.monthly")
let products = [
"com.myproduct.annual": StoreProduct(sk1Product: annualProduct),
"com.myproduct.monthly": StoreProduct(sk1Product: monthlyProduct)
]
let offerings = try XCTUnwrap(
self.offeringsFactory.createOfferings(
from: products,
data: .init(
currentOfferingId: "offering_a",
offerings: [
.init(identifier: "offering_a",
description: "This is the base offering",
packages: [
.init(identifier: "$rc_six_month", platformProductIdentifier: "com.myproduct.annual")
],
metadata: .init(
wrappedValue: metadata
)),
.init(identifier: "offering_b",
description: "This is the base offering b",
packages: [
.init(identifier: "$rc_monthly", platformProductIdentifier: "com.myproduct.monthly")
])
]
)
)
)

expect(offerings["offering_a"]).toNot(beNil())
expect(offerings["offering_b"]).toNot(beNil())
expect(offerings.current) == offerings["offering_a"]

let offeringA = try XCTUnwrap(offerings["offering_a"])
expect(offeringA.metadata).to(haveCount(6))
expect(offeringA.getMetadataValue(for: "int", default: 0)) == 5
expect(offeringA.getMetadataValue(for: "double", default: 0.0)) == 5.5
expect(offeringA.getMetadataValue(for: "boolean", default: false)) == true
expect(offeringA.getMetadataValue(for: "string", default: "")) == "five"

expect(offeringA.getMetadataValue(for: "pizza", default: "no pizza")) == "no pizza"

let wrongMetadataType = offeringA.getMetadataValue(for: "string", default: 5.5)
expect(wrongMetadataType) == 5.5
}

func testLifetimePackage() throws {
try testPackageType(packageType: PackageType.lifetime)
}
Expand Down