PlatformHostApiImpl.swift 14.7 KB
import Foundation
import AuthenticationServices
import StoreKit
import UIKit
import WebKit

class PriceFormatter{
    static func formatPrice(_ price: Decimal, currencyCode: String) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.currencyCode = currencyCode
        formatter.minimumFractionDigits = 2
        formatter.maximumFractionDigits = 2

        return formatter.string(from: price as NSDecimalNumber) ?? "\(price)"
    }

    static func currencySymbol(for currencyCode: String) -> String {

        let formatter = NumberFormatter()

        formatter.numberStyle = .currency

        formatter.currencyCode = currencyCode

//        formatter.locale = Locale(identifier: "zh_CN")

        return formatter.currencySymbol

    }
}

extension WatchAppOtherInfo{
    func toParams() -> [String: Any]{
        var json: [String: Any] = [:]
        json["userId"] = userId
        json["nickname"] = nickname
        json["markName"] = markName
        return json
    }
}

/**
 * PlatformApi iOS implementation.
 *
 * 组装完整 User-Agent格式与 Android 端保持一致
 * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)`
 */
final class PlatformHostApiImpl: PlatformHostApi {
    func isDebugEnvoriment() throws -> Bool {
        #if DEBUG || CI_ENV
        return true
        #else
        return false
        #endif
    }

    func nativeHandleUrl(urlString: String) throws -> Bool {
        if let url = URL(string: urlString){
            UIApplication.shared.open(url)
            return true
        }
        return false
    }

    func requestUnhandedUrl() throws -> String? {
        let unhandedUrl = AppShared.shared.unhandedUrl
        AppShared.shared.unhandedUrl = nil
        return unhandedUrl
    }

    func jumpAppSetting() throws -> Bool {
        if let url = URL(string: UIApplication.openSettingsURLString){
            UIApplication.shared.open(url)
            return true
        }
        return false
    }

    func requestAppReview(completion: @escaping (Result<Bool, any Error>) -> Void) {
//        Task { @MainActor in
//            guard let windowScene = UIApplication.shared.connectedScenes
//                .compactMap({ $0 as? UIWindowScene })
//                .first(where: { $0.activationState == .foregroundActive }) else {
//                completion(.success(false))
//                return
//            }
//
//            // App Store controls whether the prompt is actually displayed.
//            AppStore.requestReview(in: windowScene)
//            completion(.success(true))
//        }
        let urlString = "itms-apps://itunes.apple.com/app/id\(kAppId)?action=write-review"
        guard let url = URL(string: urlString) else{
            return
        }
        if UIApplication.shared.canOpenURL(url){
            UIApplication.shared.open(url)
        }
    }
    
    func shareText(text: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        presentShareSheet(items: [text], completion: completion)
    }
    
    func shareFileData(databytes: FlutterStandardTypedData, completion: @escaping (Result<Bool, any Error>) -> Void) {
        let fileURL = FileManager.default.temporaryDirectory
            .appendingPathComponent("doublefeel-share-\(UUID().uuidString)")
            .appendingPathExtension("data")

        do {
            try databytes.data.write(to: fileURL, options: .atomic)
        } catch {
            print("[PlatformHostApiImpl.shareFileData] return error: \(error)")
            completion(.failure(error))
            return
        }

        presentShareSheet(items: [fileURL], cleanupURL: fileURL, completion: completion)
    }

    func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, any Error>) -> Void) {
        let obsResourceType: HuaweiOBSResourceType
        switch resourceType {
        case .image:
            obsResourceType = .image
        case .video:
            obsResourceType = .video
        }

        Task {
            do {
                let fileURL = try await HuaweiOBSUploader.shared.upload(
                    filePath: filePath,
                    resourceType: obsResourceType
                )
                completion(.success(fileURL))
            } catch {
                DebugLogger.log(desc: "uploadFile error: \(error.localizedDescription)")
                completion(.failure(error))
            }
        }
    }
    
    func performCropImage(imageUrl: String, maxKB: Int64?, width: Int64?, height: Int64?, completion: @escaping (Result<String?, any Error>) -> Void) {
        
    }
    

    // 登录
    func updateLoginInfo(jsonString: String, baseUrl: String) throws {
        if let data = jsonString.data(using: .utf8){
            do{
                let model = try JSONDecoder().decode(FlutterUserSummary.self, from: data)
                if let token = model.accessToken{
                    // 刷新用户信息
                    let watchUser = UserInfoModel(userId: model.meUserInfo?.id, token: token, baseUrl: baseUrl)
                    do{
                        let data = try JSONEncoder().encode(watchUser)
                        AppGroupConstants.defaults?.set(data, forKey: AppGroupConstants.Key.myUserInfo)
                        AppShared.shared.login(userInfo: watchUser)
                        _ = WatchConnectivityService.shared.sendLoginInfoMessage(userInfo: watchUser)

                    }catch{}

                    Task{
                        await AppShared.shared.reportDeviceInfo()
                        if await HealthKitService.shared.hasAnyReadableData() {
                            _ = await AnchoredHealthDataUploader.shared.uploadAll()
                        }
                    }
                }
            }catch{
                DebugLogger.log(desc: error.localizedDescription)
            }
        }
    }

    // 退出登录
    func logout() throws {
        // 清除所有数据
        AppShared.shared.logout()
        _ = WatchConnectivityService.shared.sendLoginInfoMessage(userInfo: nil)
    }
    // 刷新会员
    func refreshVip() throws {
        _ = WatchConnectivityService.shared.sendCommandMessage(AppGroupMessageKey.reloadVip)
    }
    // 刷新watch app 和表盘
    func refreshWatchAppAndWidgets() throws {
        _ = WatchConnectivityService.shared.sendCommandMessage(AppGroupMessageKey.reloadAll)
    }

    func performRestore(completion: @escaping (Result<Bool, any Error>) -> Void) {
        Task{
            let success = await AppShared.shared.payment.restore()
            completion(.success(success))
        }
    }

    func requestAppleProductInfo(productId: String, baseUnit: Int64, completion: @escaping (Result<AppleProductInfo?, any Error>) -> Void) {
        Task {
            do {
                let product = try await AppShared.shared.payment.requestProducts(productId)
                let isTrialPeriod = await AppShared.shared.payment.isFreeTrail(product: product)
                let originPrice = product.price
                var displayPrice = product.displayPrice
                var price = originPrice
                if let introductoryOffer = product.subscription?.introductoryOffer{
                    price = introductoryOffer.price
                    displayPrice = introductoryOffer.displayPrice
                }else if let offer = product.subscription?.promotionalOffers.first(where: { $0.id == product.id }){
                    price = offer.price
                    displayPrice = offer.displayPrice
                }

                let unitPrice = originPrice/Decimal(baseUnit)
                let currencyCode = product.priceFormatStyle.currencyCode

                let productInfo = AppleProductInfo(
                    productId: product.id,
                    originPriceDescription: product.displayPrice,
                    priceDescription: displayPrice,
                    originPrice: NSDecimalNumber(decimal: originPrice * 100).doubleValue,
                    price: NSDecimalNumber(decimal: price * 100).doubleValue,
                    unitPrice: PriceFormatter.formatPrice(unitPrice, currencyCode: currencyCode),
                    currencyCode: PriceFormatter.currencySymbol(for: currencyCode),
                    isTrialPeriod: isTrialPeriod
                )
                print("[PlatformHostApiImpl.requestAppleProductInfo] return: \(productInfo)")
                completion(.success(productInfo))
            } catch StoreKitError.productNotFound {
                print("[PlatformHostApiImpl.requestAppleProductInfo] return: nil")
                completion(.success(nil))
            } catch {
                print("[PlatformHostApiImpl.requestAppleProductInfo] error: \(error.localizedDescription)")
                completion(.failure(error))
            }
        }
    }

    func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, any Error>) -> Void) {
        Task {
            let result = await AppShared.shared.payment.purchase(productId, uuidString: uuid)
            print("[PlatformHostApiImpl.performApplePayment] return: \(result)")
            completion(.success(result))
        }
    }

    private var appleSignInCoordinator: AppleSignInCoordinator?
//    private var authorizationController: ASAuthorizationController?

    func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, any Error>) -> Void) {
        DispatchQueue.main.async { [weak self] in
            guard let self else {
                print("[PlatformHostApiImpl.requestAppleSignIn] error: \(PlatformHostApiError.deallocated.localizedDescription)")
                completion(.failure(PlatformHostApiError.deallocated))
                return
            }

            let provider = ASAuthorizationAppleIDProvider()
            let request = provider.createRequest()
            request.requestedScopes = [.fullName, .email]

            guard let presentationAnchor = AppleSignInCoordinator.currentPresentationAnchor() else {
                print("[PlatformHostApiImpl.requestAppleSignIn] error: \(PlatformHostApiError.missingPresentationAnchor.localizedDescription)")
                completion(.failure(PlatformHostApiError.missingPresentationAnchor))
                return
            }

            let coordinator = AppleSignInCoordinator(presentationAnchor: presentationAnchor) { [weak self] result in
                self?.appleSignInCoordinator = nil
                switch result {
                case .success(let model):
                    print("[PlatformHostApiImpl.requestAppleSignIn] return: \(String(describing: model))")
                case .failure(let error):
                    print("[PlatformHostApiImpl.requestAppleSignIn] error: \(error.localizedDescription)")
                }
                completion(result)
            }
            appleSignInCoordinator = coordinator

            let controller = ASAuthorizationController(authorizationRequests: [request])
            controller.delegate = coordinator
            controller.presentationContextProvider = coordinator
            controller.performRequests()
//            authorizationController = controller
        }
    }

    func getFullUserAgent() throws -> String {
        let userAgent = AppShared.shared.agent.finalUA
        print("[PlatformHostApiImpl.getFullUserAgent] return: \(userAgent)")
        return userAgent
    }
    
    func sendLocalNotification(title: String, content: String, link: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        Task {
            do {
                let sent = try await LocalNotificationSender.shared.send(
                    title: title,
                    body: content,
                    link: link
                )
                completion(.success(sent))
            } catch {
                completion(.failure(error))
            }
        }
    }
    
}

//MARK: - 分享
extension PlatformHostApiImpl{
    private func presentShareSheet(
        items: [Any],
        cleanupURL: URL? = nil,
        completion: @escaping (Result<Bool, any Error>) -> Void
    ) {
        DispatchQueue.main.async {
            guard let viewController = Self.topViewController() else {
                if let cleanupURL {
                    try? FileManager.default.removeItem(at: cleanupURL)
                }
                let error = NSError(
                    domain: "PlatformHostApiImpl.Share",
                    code: 1,
                    userInfo: [NSLocalizedDescriptionKey: "Unable to find a view controller for sharing."]
                )
                print("[PlatformHostApiImpl.share] return error: \(error)")
                completion(.failure(error))
                return
            }

            let activityController = UIActivityViewController(
                activityItems: items,
                applicationActivities: nil
            )
            activityController.popoverPresentationController?.sourceView = viewController.view
            activityController.popoverPresentationController?.sourceRect = CGRect(
                x: viewController.view.bounds.midX,
                y: viewController.view.bounds.midY,
                width: 1,
                height: 1
            )
            activityController.completionWithItemsHandler = { _, completed, _, error in
                if let cleanupURL {
                    try? FileManager.default.removeItem(at: cleanupURL)
                }
                if let error {
                    print("[PlatformHostApiImpl.share] return error: \(error)")
                    completion(.failure(error))
                } else {
                    print("[PlatformHostApiImpl.share] return: \(completed)")
                    completion(.success(completed))
                }
            }
            viewController.present(activityController, animated: true)
        }
    }

    private static func topViewController(
        from rootViewController: UIViewController? = UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first(where: { $0.activationState == .foregroundActive })?
            .windows
            .first(where: { $0.isKeyWindow })?
            .rootViewController
    ) -> UIViewController? {
        if let presented = rootViewController?.presentedViewController {
            return topViewController(from: presented)
        }
        if let navigationController = rootViewController as? UINavigationController {
            return topViewController(from: navigationController.visibleViewController)
        }
        if let tabBarController = rootViewController as? UITabBarController {
            return topViewController(from: tabBarController.selectedViewController)
        }
        return rootViewController
    }
}