Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Messages/Interactions/OWSGiftBadge.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import LibSignalClient

public enum GiftBadgeError: Error {
    case noGiftBadge
    case malformed
}

@objc
public enum OWSGiftBadgeRedemptionState: Int {
    case pending = 1
    case redeemed = 2
    case opened = 3
    // TODO: (GB) Add a failure state.
}

@objc
public final class OWSGiftBadge: NSObject, NSSecureCoding, NSCopying {
    public static var supportsSecureCoding: Bool { true }

    public init?(coder: NSCoder) {
        self.redemptionCredential = coder.decodeObject(of: NSData.self, forKey: "redemptionCredential") as Data?
        self.redemptionState = (coder.decodeObject(of: NSNumber.self, forKey: "redemptionState")?.intValue).flatMap(OWSGiftBadgeRedemptionState.init(rawValue:)) ?? .pending
    }

    public func encode(with coder: NSCoder) {
        if let redemptionCredential {
            coder.encode(redemptionCredential, forKey: "redemptionCredential")
        }
        coder.encode(NSNumber(value: self.redemptionState.rawValue), forKey: "redemptionState")
    }

    override public var hash: Int {
        var hasher = Hasher()
        hasher.combine(redemptionCredential)
        hasher.combine(redemptionState)
        return hasher.finalize()
    }

    override public func isEqual(_ object: Any?) -> Bool {
        guard let object = object as? Self else { return false }
        guard type(of: self) == type(of: object) else { return false }
        guard self.redemptionCredential == object.redemptionCredential else { return false }
        guard self.redemptionState == object.redemptionState else { return false }
        return true
    }

    public func copy(with zone: NSZone? = nil) -> Any {
        return Self(
            redemptionCredential: redemptionCredential,
            redemptionState: redemptionState,
        )
    }

    @objc
    public let redemptionCredential: Data?
    public var redemptionState: OWSGiftBadgeRedemptionState

    public convenience init(redemptionCredential: Data) {
        self.init(
            redemptionCredential: redemptionCredential,
            redemptionState: .pending,
        )
    }

    private init(
        redemptionCredential: Data?,
        redemptionState: OWSGiftBadgeRedemptionState,
    ) {
        self.redemptionCredential = redemptionCredential
        self.redemptionState = redemptionState

        super.init()
    }

    public func getReceiptCredentialPresentation() throws -> ReceiptCredentialPresentation {
        guard let rcPresentationData = self.redemptionCredential else {
            throw GiftBadgeError.malformed
        }
        return try ReceiptCredentialPresentation(contents: rcPresentationData)
    }

    public struct Level: Hashable {
        public var rawLevel: UInt64

        public static let signalGift = Level(rawLevel: 100)
    }

    public func getReceiptDetails() throws -> (level: Level, expirationTime: Date) {
        let rcPresentation = try self.getReceiptCredentialPresentation()

        let receiptLevel = Level(rawLevel: try rcPresentation.getReceiptLevel())
        let receiptExpiration = try rcPresentation.getReceiptExpirationTime()

        return (receiptLevel, Date(timeIntervalSince1970: TimeInterval(receiptExpiration)))
    }

    // MARK: -

    public class func restoreFromBackup(
        receiptCredentialPresentation: Data?,
        redemptionState: OWSGiftBadgeRedemptionState,
    ) -> OWSGiftBadge {
        return OWSGiftBadge(
            redemptionCredential: receiptCredentialPresentation,
            redemptionState: redemptionState,
        )
    }

    // MARK: -

    @objc(maybeBuildFromDataMessage:)
    public class func maybeBuild(from dataMessage: SSKProtoDataMessage) -> OWSGiftBadge? {
        do {
            return try self.build(from: dataMessage)
        } catch GiftBadgeError.noGiftBadge {
            // this isn't an error -- it will be codepath for all non-gift messages
            return nil
        } catch {
            Logger.warn("Couldn't parse incoming gift badge: \(error)")
            return nil
        }
    }

    private class func build(from dataMessage: SSKProtoDataMessage) throws -> OWSGiftBadge {
        guard let giftBadge = dataMessage.giftBadge else {
            throw GiftBadgeError.noGiftBadge
        }
        guard let rcPresentationData = giftBadge.receiptCredentialPresentation else {
            throw GiftBadgeError.malformed
        }
        let result = OWSGiftBadge(redemptionCredential: rcPresentationData)
        // If we can't parse the details, drop the message.
        _ = try result.getReceiptDetails()
        return result
    }
}