From d43ff3b153091b07dfe7cb5768b9560541e56286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jess=C3=A9=20Monteiro?= Date: Wed, 15 May 2024 11:56:20 -0300 Subject: [PATCH] feat/Create Offer class --- TikiClient/Classes/License/LicenseUse.swift | 33 +++ .../Classes/License/LicenseUseCase.swift | 57 ++++++ .../Classes/License/LicenseUseCaseEnum.swift | 39 ++++ TikiClient/Classes/Rewards.swift | 22 ++ TikiClient/Classes/TitleTag.swift | 81 ++++++++ TikiClient/Classes/TitleTagEnum.swift | 52 +++++ TikiClient/Classes/Trail/LicenseRecord.swift | 43 ++++ TikiClient/Classes/Trail/PayableRecord.swift | 29 +++ TikiClient/Classes/Trail/Rsp/RspLicense.swift | 29 +++ TikiClient/Classes/Trail/Rsp/RspPayable.swift | 29 +++ TikiClient/Classes/Trail/Rsp/RspTitle.swift | 24 +++ TikiClient/Classes/Trail/Tag.swift | 36 ++++ TikiClient/Classes/Trail/TagCommon.swift | 52 +++++ TikiClient/Classes/Trail/TitleRecord.swift | 54 +++++ TikiClient/Classes/UI/Bullet.swift | 20 ++ TikiClient/Classes/UI/Offer.swift | 113 +++++++++++ TikiClient/Classes/UI/OfferFlow.swift | 192 ++++++++++++++++++ TikiClient/Classes/UI/OfferFlowSteps.swift | 18 ++ .../Classes/UI/Screens/OfferPrompt.swift | 8 + TikiClient/Classes/Util/Rsp.swift | 22 ++ 20 files changed, 953 insertions(+) create mode 100644 TikiClient/Classes/License/LicenseUse.swift create mode 100644 TikiClient/Classes/License/LicenseUseCase.swift create mode 100644 TikiClient/Classes/License/LicenseUseCaseEnum.swift create mode 100644 TikiClient/Classes/Rewards.swift create mode 100644 TikiClient/Classes/TitleTag.swift create mode 100644 TikiClient/Classes/TitleTagEnum.swift create mode 100644 TikiClient/Classes/Trail/LicenseRecord.swift create mode 100644 TikiClient/Classes/Trail/PayableRecord.swift create mode 100644 TikiClient/Classes/Trail/Rsp/RspLicense.swift create mode 100644 TikiClient/Classes/Trail/Rsp/RspPayable.swift create mode 100644 TikiClient/Classes/Trail/Rsp/RspTitle.swift create mode 100644 TikiClient/Classes/Trail/Tag.swift create mode 100644 TikiClient/Classes/Trail/TagCommon.swift create mode 100644 TikiClient/Classes/Trail/TitleRecord.swift create mode 100644 TikiClient/Classes/UI/Bullet.swift create mode 100644 TikiClient/Classes/UI/Offer.swift create mode 100644 TikiClient/Classes/UI/OfferFlow.swift create mode 100644 TikiClient/Classes/UI/OfferFlowSteps.swift create mode 100644 TikiClient/Classes/UI/Screens/OfferPrompt.swift create mode 100644 TikiClient/Classes/Util/Rsp.swift diff --git a/TikiClient/Classes/License/LicenseUse.swift b/TikiClient/Classes/License/LicenseUse.swift new file mode 100644 index 0000000..eced1be --- /dev/null +++ b/TikiClient/Classes/License/LicenseUse.swift @@ -0,0 +1,33 @@ +import Foundation + +/// License use +/// +/// Define explicit uses for an asset. LicenseUses are extremely helpful in programmatic search +/// and enforcement of your LicenseRecords. +/// +/// usecases explicitly define HOW an asset may be used. Use either our list of common enumerations +/// or define your own using LicenseUsecase. +/// +/// destinations define WHO can use an asset. destinations narrow down usecases to a set of URLs, +/// categories of companies, or more. Use ECMAScript Regex to specify flexible and easily enforceable +/// rules. +public struct LicenseUse: Codable { + /// Usecases explicitly define HOW an asset may be used. + let usecases: [LicenseUsecase] + + /// Destinations explicitly define WHERE an asset may be used. + /// Destinations can be: a wildcard URL (*.your-co.com), + /// a string defining a category of + let destinations: [String]? + + /// Create an empty LicenseUse. + /// + /// - Parameters: + /// - usecases: Usecases explicitly define HOW an asset may be used. + /// - destinations: Destinations explicitly define WHERE an asset may be used. + public init(usecases: [LicenseUsecase], destinations: [String]? = nil) { + self.usecases = usecases + self.destinations = destinations + } + +} diff --git a/TikiClient/Classes/License/LicenseUseCase.swift b/TikiClient/Classes/License/LicenseUseCase.swift new file mode 100644 index 0000000..f4f9ac9 --- /dev/null +++ b/TikiClient/Classes/License/LicenseUseCase.swift @@ -0,0 +1,57 @@ +import Foundation + +/** + Use case for license. + + - Parameter the license use case enumeration. Default value is nil. + */ +public class LicenseUsecase: Codable { + + private var _value: String + + /// The String value + public var value: String{ + return _value + } + + public init(_ value: String){ + do { + let licenseUsecaseEnum = try LicenseUsecaseEnum.fromValue(value: value) + _value = licenseUsecaseEnum.rawValue + } catch { + _value = "custom:\(value)" + } + } + + public init(_ licenseUsecaseEnum: LicenseUsecaseEnum){ + _value = licenseUsecaseEnum.rawValue + } + + required public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + var value: String = try container.decode(String.self) + if(value.hasPrefix("custom:")){ + value = String(value.dropFirst("custom:".count)) + } + do { + let licenseUsecaseEnum = try LicenseUsecaseEnum.fromValue(value: value) + _value = licenseUsecaseEnum.rawValue + } catch { + _value = "custom:\(value)" + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value) + } + + static let attribution = LicenseUsecase(LicenseUsecaseEnum.attribution) + static let retargeting = LicenseUsecase(LicenseUsecaseEnum.retargeting) + static let personalization = LicenseUsecase(LicenseUsecaseEnum.personalization) + static let aiTraining = LicenseUsecase(LicenseUsecaseEnum.aiTraining) + static let distribution = LicenseUsecase(LicenseUsecaseEnum.distribution) + static let analytics = LicenseUsecase(LicenseUsecaseEnum.analytics) + static let support = LicenseUsecase(LicenseUsecaseEnum.support) + +} diff --git a/TikiClient/Classes/License/LicenseUseCaseEnum.swift b/TikiClient/Classes/License/LicenseUseCaseEnum.swift new file mode 100644 index 0000000..0dfdfd3 --- /dev/null +++ b/TikiClient/Classes/License/LicenseUseCaseEnum.swift @@ -0,0 +1,39 @@ +import Foundation + +/** + + Default accepted usecases + */ +public enum LicenseUsecaseEnum: String, Codable, CaseIterable { + case attribution = "attribution" + case retargeting = "retargeting" + case personalization = "personalization" + case aiTraining = "ai_training" + case distribution = "distribution" + case analytics = "analytics" + case support = "support" + + /** + Returns the string value for the enum + */ + var value: String { + return self.rawValue + } + + /// + /// Builds a `LicenseUsecaseEnum` from `value`. + /// + /// - Parameter value: string value of enum. + /// - Throws: `NSException` if `value` is not a valid `LicenseUsecaseEnum` value. + /// + /// - Returns: `LicenseUsecase + + static func fromValue(value: String) throws -> LicenseUsecaseEnum { + for type in LicenseUsecaseEnum.allCases { + if type.rawValue == value { + return type + } + } + throw NSError(domain: "Invalid LicenseUsecaseEnum value \(value)", code: 0, userInfo: nil) + } +} diff --git a/TikiClient/Classes/Rewards.swift b/TikiClient/Classes/Rewards.swift new file mode 100644 index 0000000..92c58d2 --- /dev/null +++ b/TikiClient/Classes/Rewards.swift @@ -0,0 +1,22 @@ +// +// Rewards.swift +// TikiClient +// +// Created by Jesse Monteiro Ferreira on 14/05/24. +// + +import Foundation + +public class Rewards { + let virtualCurrency: String + let exclusiveAccess: String + let upgrades: String + let custom: String + + public init(virtualCurrency: String, exclusiveAccess: String, upgrades: String, custom: String) { + self.virtualCurrency = virtualCurrency + self.exclusiveAccess = exclusiveAccess + self.upgrades = upgrades + self.custom = custom + } +} diff --git a/TikiClient/Classes/TitleTag.swift b/TikiClient/Classes/TitleTag.swift new file mode 100644 index 0000000..2e27d7e --- /dev/null +++ b/TikiClient/Classes/TitleTag.swift @@ -0,0 +1,81 @@ +import Foundation + + +/// Title tag +/// +/// Tags are included in the [TitleRecord] and describe the represented data asset. +/// Tags improve record searchability and come in handy when bulk searching and filtering licenses. +/// Use either our list of common enumerations or define your own using [customValue] as constructor +/// parameter. +public class TitleTag: Codable { + + private var _value: String + + /// The TitleTag String value + public var value: String{ + return _value + } + + public init(_ value: String){ + do { + let titleTagEnum = try TitleTagEnum.fromValue(value: value) + _value = titleTagEnum.rawValue + } catch { + _value = "custom:\(value)" + } + } + + public init(_ titleTagEnum: TitleTagEnum){ + _value = titleTagEnum.rawValue + } + + required public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + var value: String = try container.decode(String.self) + if(value.hasPrefix("custom:")){ + value = String(value.dropFirst("custom:".count)) + } + do { + let titleTagEnum = try TitleTagEnum.fromValue(value: value) + _value = titleTagEnum.rawValue + } catch { + _value = "custom:\(value)" + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value) + } + + public static let emailAddress = TitleTag(TitleTagEnum.emailAddress) + public static let phoneNumber = TitleTag(TitleTagEnum.phoneNumber) + public static let physicalAddress = TitleTag(TitleTagEnum.physicalAddress) + public static let contactInfo = TitleTag(TitleTagEnum.contactInfo) + public static let health = TitleTag(TitleTagEnum.health) + public static let fitness = TitleTag(TitleTagEnum.fitness) + public static let paymentInfo = TitleTag(TitleTagEnum.paymentInfo) + public static let creditInfo = TitleTag(TitleTagEnum.creditInfo) + public static let financialInfo = TitleTag(TitleTagEnum.financialInfo) + public static let preciseLocation = TitleTag(TitleTagEnum.preciseLocation) + public static let coarseLocation = TitleTag(TitleTagEnum.coarseLocation) + public static let sensitiveInfo = TitleTag(TitleTagEnum.sensitiveInfo) + public static let contacts = TitleTag(TitleTagEnum.contacts) + public static let messages = TitleTag(TitleTagEnum.messages) + public static let photoVideo = TitleTag(TitleTagEnum.photoVideo) + public static let audio = TitleTag(TitleTagEnum.audio) + public static let gameplayContent = TitleTag(TitleTagEnum.gameplayContent) + public static let customerSupport = TitleTag(TitleTagEnum.customerSupport) + public static let userContent = TitleTag(TitleTagEnum.userContent) + public static let browsingHistory = TitleTag(TitleTagEnum.browsingHistory) + public static let searchHistory = TitleTag(TitleTagEnum.searchHistory) + public static let userId = TitleTag(TitleTagEnum.userId) + public static let deviceId = TitleTag(TitleTagEnum.deviceId) + public static let purchaseHistory = TitleTag(TitleTagEnum.purchaseHistory) + public static let productInteraction = TitleTag(TitleTagEnum.productInteraction) + public static let advertisingData = TitleTag(TitleTagEnum.advertisingData) + public static let usageData = TitleTag(TitleTagEnum.usageData) + public static let crashData = TitleTag(TitleTagEnum.crashData) + public static let performanceData = TitleTag(TitleTagEnum.performanceData) + public static let diagnosticData = TitleTag(TitleTagEnum.diagnosticData) +} diff --git a/TikiClient/Classes/TitleTagEnum.swift b/TikiClient/Classes/TitleTagEnum.swift new file mode 100644 index 0000000..2f6bf1d --- /dev/null +++ b/TikiClient/Classes/TitleTagEnum.swift @@ -0,0 +1,52 @@ +import Foundation + +/** + * Default accepted tags. + * + * - value: The string value of the tag. + */ +public enum TitleTagEnum: String, Codable, CaseIterable { + case emailAddress = "email_address" + case phoneNumber = "phone_number" + case physicalAddress = "physical_address" + case contactInfo = "contact_info" + case health = "health" + case fitness = "fitness" + case paymentInfo = "payment_info" + case creditInfo = "credit_info" + case financialInfo = "financial_info" + case preciseLocation = "precise_location" + case coarseLocation = "coarse_location" + case sensitiveInfo = "sensitive_info" + case contacts = "contacts" + case messages = "messages" + case photoVideo = "photo_video" + case audio = "audio" + case gameplayContent = "gameplay_content" + case customerSupport = "customer_support" + case userContent = "user_content" + case browsingHistory = "browsing_history" + case searchHistory = "search_history" + case userId = "user_id" + case deviceId = "device_id" + case purchaseHistory = "purchase_history" + case productInteraction = "product_interaction" + case advertisingData = "advertising_data" + case usageData = "usage_data" + case crashData = "crash_data" + case performanceData = "performance_data" + case diagnosticData = "diagnostic_data" + + /** + * Builds a [TitleTagEnum] from [value] + * @throws [IllegalArgumentException] if value is not a valid [TitleTagEnum] value + */ + public static func fromValue(value: String) throws -> TitleTagEnum { + for type in TitleTagEnum.allCases { + if type.rawValue == value { + return type + } + } + throw NSError(domain: "Invalid TitleTagEnum value \(value)", code: 0, userInfo: nil) + } +} diff --git a/TikiClient/Classes/Trail/LicenseRecord.swift b/TikiClient/Classes/Trail/LicenseRecord.swift new file mode 100644 index 0000000..c78ab36 --- /dev/null +++ b/TikiClient/Classes/Trail/LicenseRecord.swift @@ -0,0 +1,43 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +/// A `LicenseRecord` describes the terms under which a data asset may be used and MUST contain a reference to the +/// corresponding `TitleRecord`. +/// +/// Learn more about `LicenseRecords` at https://docs.mytiki.com/docs/offer-customization. +public struct LicenseRecord { + + /// This record's unique identifier. + public var id: String? + + /// The `TitleRecord` associated with this license. + public var title: TitleRecord + + /// A list of `Use` instances describing how an asset can be used. + public var uses: [Use] + + /// The legal terms for the license. + public var terms: String + + /// A human-readable description of the license. + public var description: String? + + /// The date when the license expires. + public var expiry: Date? + + public init?(from: RspLicense){ + guard let titleRecord = TitleRecord(from: from.title!), from.id != nil, from.title != nil, from.uses != nil, from.terms != nil, from.title != nil else{ + return nil + } + self.id = from.id + self.title = titleRecord + self.uses = from.uses! + self.terms = from.terms! + self.description = from.description + self.expiry = from.expiry + } +} diff --git a/TikiClient/Classes/Trail/PayableRecord.swift b/TikiClient/Classes/Trail/PayableRecord.swift new file mode 100644 index 0000000..79e9120 --- /dev/null +++ b/TikiClient/Classes/Trail/PayableRecord.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +public struct PayableRecord{ + public let id: String + public let license: LicenseRecord + public let amount: String + public let type: String? + public let description: String? + public let expiry: Date? + public let reference: String? + + public init?(from: RspPayable){ + guard let licenseRecord = LicenseRecord(from: from.license!), from.id != nil, from.license != nil, from.amount != nil else{ + return nil + } + id = from.id! + license = licenseRecord + amount = from.amount! + type = from.type + description = from.description + expiry = from.expiry + reference = from.reference + } +} diff --git a/TikiClient/Classes/Trail/Rsp/RspLicense.swift b/TikiClient/Classes/Trail/Rsp/RspLicense.swift new file mode 100644 index 0000000..d355feb --- /dev/null +++ b/TikiClient/Classes/Trail/Rsp/RspLicense.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +public struct RspLicense: Rsp { + public let id: String? + public let title: RspTitle? + public let uses: [Use]? + public let terms: String? + public let description: String? + public let expiry: Date? + public let requestId: String + + public init(from: [String : Any?]) { + self.requestId = RspLicense.nullToNil(value: from["requestId"] as? String ?? nil) != nil ? from["requestId"] as! String : "" + self.id = from["id"] as? String + self.title = from["title"] != nil ? RspTitle(from: from["title"] as! [String: Any?]) : nil + self.uses = (from["uses"] as! [[String: Any]]).map{ use in + Use(from: use) + } as [Use] + self.terms = from["terms"] as? String + self.description = from["description"] as? String +// self.expiry = RspLicense.nullToNil(value: from["expiry"] as? Int64) != nil ? Date(milliseconds: from["expiry"] as! Int64) : nil + self.expiry = nil + } +} diff --git a/TikiClient/Classes/Trail/Rsp/RspPayable.swift b/TikiClient/Classes/Trail/Rsp/RspPayable.swift new file mode 100644 index 0000000..294a662 --- /dev/null +++ b/TikiClient/Classes/Trail/Rsp/RspPayable.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +public struct RspPayable: Rsp { + public let id: String? + public let license: RspLicense? + public let amount: String? + public let type: String? + public let description: String? + public let expiry: Date? + public let reference: String? + public let requestId: String + + public init(from: [String : Any?]) { + self.id = from["id"] as? String + self.license = from["license"] as? [String: Any?] != nil ? RspLicense(from: (from["license"] as! [String: Any?])) : nil + self.amount = from["amount"] as? String + self.type = from["type"] as? String + self.description = from["description"] as? String + self.expiry = nil +// self.expiry = from["expiry"] as? Int64 != nil ? Date(miliseconds: ) (milliseconds: (from["expiry"] as? Int64)) : nil + self.reference = from["reference"] as? String + self.requestId = RspPayable.nullToNil(value: from["requestId"] as? String) != nil ? from["requestId"] as! String : "" + } +} diff --git a/TikiClient/Classes/Trail/Rsp/RspTitle.swift b/TikiClient/Classes/Trail/Rsp/RspTitle.swift new file mode 100644 index 0000000..607eda9 --- /dev/null +++ b/TikiClient/Classes/Trail/Rsp/RspTitle.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +public struct RspTitle: Rsp { + public let id: String? + public let hashedPtr: String? + public let origin: String? + public let tags: [Tag]? + public let description: String? + public let requestId: String + + public init(from: [ String: Any? ]){ + self.id = from["id"] as? String + self.hashedPtr = from["hashedPtr"] as? String + self.origin = from["origin"] as? String + self.tags = RspTitle.nullToNil(value: from["tags"] as Any?) != nil ? (from["tags"] as! [String]).map{ tagValue in Tag.from(tag: tagValue) } : nil + self.description = from["description"] as? String + self.requestId = from["requestId"] as? String ?? "" + } +} diff --git a/TikiClient/Classes/Trail/Tag.swift b/TikiClient/Classes/Trail/Tag.swift new file mode 100644 index 0000000..70287b3 --- /dev/null +++ b/TikiClient/Classes/Trail/Tag.swift @@ -0,0 +1,36 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +public class Tag { + let value: String + + private init(value: String) { + self.value = value + } + + public convenience init(tag: TagCommon) { + self.init(value: tag.rawValue) + } + + public static func custom(tag: String) -> Tag { + return Tag(value: "custom:\(tag)") + } + + public static func from(tag: String) -> Tag { + if let common = TagCommon.from(value: tag) { + return Tag(value: common.rawValue) + } else if tag.starts(with: "custom:") { + return Tag(value: tag) + } else { + return custom(tag: tag) + } + } + + public func toString() -> String { + return value + } +} diff --git a/TikiClient/Classes/Trail/TagCommon.swift b/TikiClient/Classes/Trail/TagCommon.swift new file mode 100644 index 0000000..5cd56cf --- /dev/null +++ b/TikiClient/Classes/Trail/TagCommon.swift @@ -0,0 +1,52 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +public enum TagCommon: String, CaseIterable { + case EMAIL_ADDRESS = "email_address" + case PHONE_NUMBER = "phone_number" + case PHYSICAL_ADDRESS = "physical_address" + case CONTACT_INFO = "contact_info" + case HEALTH = "health" + case FITNESS = "fitness" + case PAYMENT_INFO = "payment_info" + case CREDIT_INFO = "credit_info" + case FINANCIAL_INFO = "financial_info" + case PRECISE_LOCATION = "precise_location" + case COARSE_LOCATION = "coarse_location" + case SENSITIVE_INFO = "sensitive_info" + case CONTACTS = "contacts" + case MESSAGES = "messages" + case PHOTO_VIDEO = "photo_video" + case AUDIO = "audio" + case GAMEPLAY_CONTENT = "gameplay_content" + case CUSTOMER_SUPPORT = "customer_support" + case USER_CONTENT = "user_content" + case BROWSING_HISTORY = "browsing_history" + case SEARCH_HISTORY = "search_history" + case USER_ID = "user_id" + case DEVICE_ID = "device_id" + case PURCHASE_HISTORY = "purchase_history" + case PRODUCT_INTERACTION = "product_interaction" + case ADVERTISING_DATA = "advertising_data" + case USAGE_DATA = "usage_data" + case CRASH_DATA = "crash_data" + case PERFORMANCE_DATA = "performance_data" + case DIAGNOSTIC_DATA = "diagnostic_data" + + /// Builds a `TagCommon` from `value`. + /// + /// - Parameter value: The string value of the tag. + /// - Returns: A `TagCommon` value corresponding to the given `value`. + public static func from(value: String) -> TagCommon? { + for type in TagCommon.allCases { + if type.rawValue == value { + return type + } + } + return nil + } +} diff --git a/TikiClient/Classes/Trail/TitleRecord.swift b/TikiClient/Classes/Trail/TitleRecord.swift new file mode 100644 index 0000000..9af9c69 --- /dev/null +++ b/TikiClient/Classes/Trail/TitleRecord.swift @@ -0,0 +1,54 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +/// A record describing a data asset, which contains a `PointerRecord` to your system. +/// Title Records are used to provide metadata about a data asset that TIKI clients can use to evaluate the value of the asset +/// for their use cases. A Title Record must contain a `PointerRecord` that identifies the asset in your system. +/// +/// Learn more about Title Records in [TIKI's documentation](https://docs.mytiki.com/docs/offer-customization). +public struct TitleRecord { + + /// This record's unique identifier. + public let id: String + + /// A hashed `PointerRecord` identifying the asset in your system. + public let hashedPtr: String + + /// A list of tags that describe the asset in a search-friendly way. + public let tags: [Tag] + + /// A human-readable description of the asset. + public let description: String? + + /// An optional field to override the default origin from which the data was generated. + public let origin: String? + + /// Initializes a new instance of `TitleRecord`. + + /// - Parameters: + /// - id: This record's unique identifier. + /// - hashedPtr: A hashed `PointerRecord` identifying the asset in your system. + /// - tags: A list of tags that describe the asset in a search-friendly way. + /// - description: A human-readable description of the asset. + /// - origin: An optional field to override the default origin from which the data was generated + public init(id: String, hashedPtr: String, tags: [Tag], description: String?, origin: String?) { + self.id = id + self.hashedPtr = hashedPtr + self.tags = tags + self.description = description + self.origin = origin + } + + public init?(from: RspTitle){ + guard from.id != nil, from.hashedPtr != nil, from.tags != nil else { + return nil + } + self.id = from.id! + self.hashedPtr = from.hashedPtr! + self.tags = from.tags! + self.description = from.description + self.origin = from.origin + } +} diff --git a/TikiClient/Classes/UI/Bullet.swift b/TikiClient/Classes/UI/Bullet.swift new file mode 100644 index 0000000..77ff190 --- /dev/null +++ b/TikiClient/Classes/UI/Bullet.swift @@ -0,0 +1,20 @@ + +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +/// An item that describes what can be done with the user data. +public struct Bullet: Hashable { + + public init(text: String, isUsed: Bool) { + self.text = text + self.isUsed = isUsed + } + + /// Description of the data usage. + public let text: String + + /// Whether it is used. + public let isUsed: Bool +} diff --git a/TikiClient/Classes/UI/Offer.swift b/TikiClient/Classes/UI/Offer.swift new file mode 100644 index 0000000..8087637 --- /dev/null +++ b/TikiClient/Classes/UI/Offer.swift @@ -0,0 +1,113 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import SwiftUI + +/// An Offer for creating a License for a Title identified by [ptr]. +public class Offer { + /// An image that represents the reward. + /// + /// It should have 300x86 size and include assets for all screen depths. + + + /// The bullets that describes how the user data will be used. + + /// The Pointer Record of the data stored. + + /// A human-readable description for the license. + + /// The legal terms of the offer. + + /// The Use cases for the license. + + /// The tags that describes the represented data asset. + + /// The expiration of the License. Null for no expiration. + + /// A list of device-specific [Permission] required for the license. + + public var _id: String? + public var ptr: String? + public var description: String? + public var terms: String? + public var reward: Image? + public var usedBullet = [Bullet]() + public var uses = [LicenseUse]() + public var tags = [TitleTag]() + public var permissions = [Permission]() + public var expiry: Date? + + /// The Offer unique identifier. If none is set, it creates a random UUID. + public var id: String { + if(_id == nil){ + _id = UUID().uuidString + } + return _id! + } + + /// Sets the [id] + public func id(_ id: String) -> Offer { + _id = id + return self + } + + /// Sets the [reward] + public func reward(_ reward: String) -> Offer { + self.reward = Image(reward) + return self + } + + /// Adds a [usedBullet] + public func bullet(text: String, isUsed: Bool) -> Offer { + usedBullet.append(Bullet(text: text, isUsed: isUsed)) + return self + } + + /// Sets the [ptr] + public func ptr(_ ptr: String) -> Offer { + self.ptr = ptr + return self + } + + /// Sets the [description] + public func description(_ description: String?) -> Offer { + self.description = description + return self + } + + /// Sets the [terms] + public func terms(_ filename: String) throws -> Offer { + terms = try String( + contentsOfFile: Bundle.main.path(forResource: filename, ofType: "md")!, + encoding: String.Encoding(rawValue: NSUTF8StringEncoding)) + return self + } + + /// Adds an item in the [uses] list. + public func use(usecases: [LicenseUsecase], destinations: [String]? = []) -> Offer { + uses.append(LicenseUse(usecases: usecases, destinations: destinations)) + return self + } + + /// Adds an item in the [tags + public func tag(_ tag: TitleTag) -> Offer { + tags.append(tag) + return self + } + + /// Sets the [expiry] based in the *timeInterval* + public func duration(_ timeInterval: TimeInterval) -> Offer { + let now: Int = Int(Date().timeIntervalSince1970) + expiry = Date(timeIntervalSince1970: Double(now)).addingTimeInterval(timeInterval) + return self + } + + /// Adds an item in the [requiredPermissions] list. + public func permission(_ permission: Permission) -> Offer { + permissions.append(permission) + return self + } + +} diff --git a/TikiClient/Classes/UI/OfferFlow.swift b/TikiClient/Classes/UI/OfferFlow.swift new file mode 100644 index 0000000..236aa7f --- /dev/null +++ b/TikiClient/Classes/UI/OfferFlow.swift @@ -0,0 +1,192 @@ +//import SwiftUI +// +//public struct OfferFlow: View{ +// +// @Environment(\.colorScheme) private var colorScheme +// +// @State var activeOffer: Offer +// @State var pendingPermissions: [Permission]? = nil +// @State var step: OfferFlowStep = .none +// @State var dragOffsetY: CGFloat = 0 +// +// let offers: [String: Offer] +// +// var onSettings: (() -> Void) +// var onDismiss: (() -> Void) +// var onAccept: ((Offer, LicenseRecord) -> Void)? +// var onDecline: ((Offer, LicenseRecord?) -> Void)? +// +// public var body: some View{ +// ZStack{ +// if(step == .prompt){ +// OfferPrompt( +// currentOffer: $activeOffer, +// offers: offers, +// onAccept: { offer in +// activeOffer = offer +// pendingPermissions = offer.permissions +// goTo(.terms) +// }, +// onDecline: { offer in +// decline(offer) +// goTo(.endingDeclined) +// }, +// onLearnMore: {goTo(.learnMore)} +// ).asBottomSheet( +// isShowing: isShowingBinding(.prompt), +// offset: $dragOffsetY, +// onDismiss: dismissSheet) +// .transition(.bottomSheet).zIndex(2) +// } +// if(step == .endingAccepted){ +// EndingAccepted( +// onSettings: onSettings, +// dismiss: dismissSheet +// ).asBottomSheet( +// isShowing: isShowingBinding(.endingAccepted), +// offset: $dragOffsetY, +// onDismiss: dismissSheet) +// .transition(.bottomSheet) +// } +// if(step == .endingDeclined){ +// EndingDeclined( +// onSettings: onSettings, +// dismiss: dismissSheet +// ).asBottomSheet( +// isShowing: isShowingBinding(.endingAccepted), +// offset: $dragOffsetY, +// onDismiss: dismissSheet +// ) +// .transition(.bottomSheet) +// } +// if(step == .endingError){ +// EndingError( +// pendingPermissions: $pendingPermissions, +// onAuthorized: { +// accept(activeOffer) +// goTo(.endingAccepted) +// } +// ).asBottomSheet( +// isShowing: isShowingBinding(.endingAccepted), +// offset: $dragOffsetY, +// onDismiss: dismissSheet +// ) +// .transition(.bottomSheet) +// } +// if(step == .terms){ +// Terms(onAccept: onAcceptTerms, terms: activeOffer.terms!) +// .asNavigationRoute( +// isShowing: isShowingBinding(.terms), +// title: "Terms and conditions", +// onDismiss: {goTo(.prompt)} +// ) +// .zIndex(1) +// .transition(.navigate) +// } +// if(step == .learnMore){ +// LearnMore() +// .asNavigationRoute( +// isShowing: isShowingBinding(.learnMore), +// title: "LearnMore", +// onDismiss: { goTo(.prompt) } +// ) +// .zIndex(1) +// .transition(.navigate) +// } +// }.onAppear{ +// if(step == .none){ +// withAnimation(.easeOut) { +// step = .prompt +// } +// } +// } +// .gesture(DragGesture(minimumDistance: 5, coordinateSpace: .global) +// .onChanged { value in +// if(step != .terms && step != .learnMore){ +// dragOffsetY = value.translation.height > 0 ? value.translation.height : 0 +// } +// } +// .onEnded{ value in +// if(step != .terms && step != .learnMore){ +// withAnimation(.easeOut) { +// step = .none +// } +// onDismiss() +// } +// } +// ) +// } +// +// func goTo(_ step: OfferFlowStep){ +// withAnimation(.easeOut){ +// self.step = step +// } +// } +// +// func isShowingBinding(_ step: OfferFlowStep) -> Binding{ +// return Binding( +// get: { +// self.step == step +// }, +// set: { isShowing in +// withAnimation(.easeOut){ +// self.step = isShowing ? step : .none +// } +// }) +// } +// +// func dismissSheet(){ +// withAnimation(.easeOut){ +// step = .none +// } +// onDismiss() +// } +// +// func onAcceptTerms(){ +// if(isPendingPermission()){ +// goTo(.endingError) +// }else{ +// accept(activeOffer) +// goTo(.endingAccepted) +// } +// } +// +// func isPendingPermission() -> Bool{ +// if(pendingPermissions == nil || pendingPermissions!.isEmpty){ +// return false +// }else{ +// var isPending = false +// pendingPermissions!.forEach{ permission in +// if(!permission.isAuthorized()){ +// isPending = true +// } +// } +// return isPending +// } +// } +// +// func accept(_ offer: Offer){ +// Task{ +// do{ +// let license: LicenseRecord = try await license(offer: offer) +// onAccept?(offer, license) +// print(license) +// goTo(.endingAccepted) +// }catch{ +// print(error) +// } +// } +// } +// +// func decline(_ offer: Offer){ +// Task{ +// onDecline?(offer, nil) +// goTo(.endingDeclined) +// } +// } +// +// func license(offer: Offer) async throws -> LicenseRecord { +// return try await TikiSdk.license( offer.ptr!, offer.uses, offer.terms!, tags: offer.tags, licenseDescription: offer.description,expiry: offer.expiry) +// } +// +//} diff --git a/TikiClient/Classes/UI/OfferFlowSteps.swift b/TikiClient/Classes/UI/OfferFlowSteps.swift new file mode 100644 index 0000000..79b457f --- /dev/null +++ b/TikiClient/Classes/UI/OfferFlowSteps.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by Ricardo on 20/03/23. +// + +import Foundation + +enum OfferFlowStep{ + case none, + prompt, + terms, + learnMore, + endingAccepted, + endingDeclined, + endingError +} diff --git a/TikiClient/Classes/UI/Screens/OfferPrompt.swift b/TikiClient/Classes/UI/Screens/OfferPrompt.swift new file mode 100644 index 0000000..ab238f2 --- /dev/null +++ b/TikiClient/Classes/UI/Screens/OfferPrompt.swift @@ -0,0 +1,8 @@ +// +// OfferPrompt.swift +// TikiClient +// +// Created by Jesse Monteiro Ferreira on 14/05/24. +// + +import Foundation diff --git a/TikiClient/Classes/Util/Rsp.swift b/TikiClient/Classes/Util/Rsp.swift new file mode 100644 index 0000000..e464087 --- /dev/null +++ b/TikiClient/Classes/Util/Rsp.swift @@ -0,0 +1,22 @@ +/* + * Copyright (c) TIKI Inc. + * MIT license. See LICENSE file in root directory. + */ + +import Foundation + +protocol Rsp { + var requestId: String { get } + init(from: [String:Any?]) + +} + +extension Rsp{ + static func nullToNil(value : Any?) -> Any? { + if value is NSNull { + return nil + } else { + return value + } + } +}