feat: bug

This commit is contained in:
Junhui Chen 2025-09-01 13:24:12 +08:00 committed by jinyaqiu
parent 54a451fe6e
commit 94597f2d5a
43 changed files with 1376 additions and 529 deletions

View File

@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693C92E65C94400BCAAC1 /* SVGKit */; };
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */; };
AB6695272E67015600BCAAC1 /* WaterfallGrid in Frameworks */ = {isa = PBXBuildFile; productRef = AB6695262E67015600BCAAC1 /* WaterfallGrid */; };
AB8773632E4E04400071CB53 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = AB8773622E4E040E0071CB53 /* .gitignore */; };
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = ABC150C02E5DB39A00A1F970 /* Lottie */; };
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = ABE8998D2E533A7100CD7BA6 /* Alamofire */; };
@ -57,7 +60,10 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */,
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */,
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */,
AB6695272E67015600BCAAC1 /* WaterfallGrid in Frameworks */,
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -114,6 +120,9 @@
packageProductDependencies = (
ABE8998D2E533A7100CD7BA6 /* Alamofire */,
ABC150C02E5DB39A00A1F970 /* Lottie */,
AB6693C92E65C94400BCAAC1 /* SVGKit */,
AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */,
AB6695262E67015600BCAAC1 /* WaterfallGrid */,
);
productName = wake;
productReference = ABB4E2082E4B75D900660198 /* wake.app */;
@ -146,6 +155,8 @@
packageReferences = (
ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */,
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */,
AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */,
AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = ABB4E2092E4B75D900660198 /* Products */;
@ -305,6 +316,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = wake/wake.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@ -313,17 +325,20 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = wake/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSCameraUsageDescription = "We need access to your camera to take photos";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "We need access to your photo library to select photos";
INFOPLIST_KEY_NSUserTrackingUsageDescription = "我们需要访问您的Apple ID信息来为您提供登录服务";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.fairclip2025;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -336,6 +351,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = wake/wake.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@ -344,17 +360,20 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = wake/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSCameraUsageDescription = "We need access to your camera to take photos";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "We need access to your photo library to select photos";
INFOPLIST_KEY_NSUserTrackingUsageDescription = "我们需要访问您的Apple ID信息来为您提供登录服务";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.fairclip2025;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -386,6 +405,22 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SVGKit/SVGKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.0.0;
};
};
AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/paololeonardi/WaterfallGrid.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.0;
};
};
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
@ -405,6 +440,21 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
AB6693C92E65C94400BCAAC1 /* SVGKit */ = {
isa = XCSwiftPackageProductDependency;
package = AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKit;
};
AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */ = {
isa = XCSwiftPackageProductDependency;
package = AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKitSwift;
};
AB6695262E67015600BCAAC1 /* WaterfallGrid */ = {
isa = XCSwiftPackageProductDependency;
package = AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */;
productName = WaterfallGrid;
};
ABC150C02E5DB39A00A1F970 /* Lottie */ = {
isa = XCSwiftPackageProductDependency;
package = ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */;

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@ -1,5 +1,5 @@
{
"originHash" : "0e95cd18402f001189cea942918f7d0c4c8b04175c6c482029650c892d28d55a",
"originHash" : "7ea295cc5e3eb8ef644b89ce2b47a7600994b67c8582ee354b643cd63250740d",
"pins" : [
{
"identity" : "alamofire",
@ -10,6 +10,15 @@
"version" : "5.10.2"
}
},
{
"identity" : "cocoalumberjack",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
"state" : {
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
"version" : "3.9.0"
}
},
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
@ -18,6 +27,33 @@
"revision" : "04f2fd18cc9404a0a0917265a449002674f24ec9",
"version" : "4.5.2"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit.git",
"state" : {
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
"version" : "3.0.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log",
"state" : {
"revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
"version" : "1.6.4"
}
},
{
"identity" : "waterfallgrid",
"kind" : "remoteSourceControl",
"location" : "https://github.com/paololeonardi/WaterfallGrid.git",
"state" : {
"revision" : "c7c08652c3540adf8e48409c351879b4caea7e89",
"version" : "1.1.0"
}
}
],
"version" : 3

BIN
wake/Assets/Png/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -20,6 +20,8 @@ struct ReturnButton: View {
.font(Typography.font(for: iconSize))
.fontWeight(.medium)
.foregroundColor(iconColor)
.padding(10) //
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle())
}

View File

@ -182,6 +182,8 @@ struct BlindBoxView: View {
let mediaType: BlindBoxMediaType
@State private var showModal = false //
@State private var showSettings = false //
@State private var isMember = false //
@State private var memberDate = "" //
@State private var showLogin = false
@State private var memberProfile: MemberProfile? = nil
@State private var blindCount: BlindCount? = nil
@ -275,6 +277,8 @@ struct BlindBoxView: View {
switch result {
case .success(let response):
self.memberProfile = response.data
self.isMember = response.data.membershipLevel == "Pioneer"
self.memberDate = response.data.membershipEndAt ?? ""
print("✅ 成功获取会员信息:", response.data)
print("✅ 用户ID:", response.data.userInfo.userId)
case .failure(let error):
@ -641,7 +645,10 @@ struct BlindBoxView: View {
Button(action: showUserProfile) {
SVGImage(svgName: "User")
.frame(width: 24, height: 24)
.padding(13) // Increases tap area while keeping visual size
.contentShape(Rectangle()) // Makes the padded area tappable
}
.buttonStyle(PlainButtonStyle()) // Prevents the button from affecting the layout
Spacer()
// //
@ -705,13 +712,9 @@ struct BlindBoxView: View {
ZStack {
// 1. SVG
if !showScalingOverlay {
SVGImage(svgName: "BlindBg")
.frame(
width: UIScreen.main.bounds.width * 1.8,
height: UIScreen.main.bounds.height * 0.85
)
.position(x: UIScreen.main.bounds.width / 2,
y: UIScreen.main.bounds.height * 0.325)
SVGImage(svgName: "BlindBg", contentMode: .fit)
// .position(x: UIScreen.main.bounds.width / 2,
// y: UIScreen.main.bounds.height * 0.325)
.opacity(showScalingOverlay ? 0 : 1)
.animation(.easeOut(duration: 1.5), value: showScalingOverlay)
}
@ -830,6 +833,7 @@ struct BlindBoxView: View {
.offset(x: -10, y: UIScreen.main.bounds.height * 0.2)
}
}
.padding()
.frame(
maxWidth: .infinity,
maxHeight: UIScreen.main.bounds.height * 0.65
@ -897,10 +901,11 @@ struct BlindBoxView: View {
) {
UserProfileModal(
showModal: $showModal,
showSettings: $showSettings
showSettings: $showSettings,
isMember: $isMember,
memberDate: $memberDate
)
}
.shadow(color: .black.opacity(0.3), radius: 10, x: 5, y: 0)
.offset(x: showSettings ? UIScreen.main.bounds.width : 0)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings)

View File

@ -20,10 +20,6 @@
</array>
<key>NSAppleIDUsageDescription</key>
<string>Sign in with Apple is used to authenticate your account</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera to take photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to select photos</string>
<key>UIAppFonts</key>
<array>
<string>Inter.ttf</string>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,158 @@
{
"images" : [
{
"filename" : "40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

101
wake/Models/OrderInfo.swift Normal file
View File

@ -0,0 +1,101 @@
import Foundation
///
struct OrderInfo: Codable, Identifiable {
let id: String
let userId: String
let totalAmount: Amount
let status: String
let items: [OrderItem]
let paymentInfo: PaymentInfo?
let createdAt: String
let updatedAt: String
let expiredAt: String
enum CodingKeys: String, CodingKey {
case id
case userId = "user_id"
case totalAmount = "total_amount"
case status
case items
case paymentInfo = "payment_info"
case createdAt = "created_at"
case updatedAt = "updated_at"
case expiredAt = "expired_at"
}
}
///
struct PaymentInfo: Codable {
let id: String
let paymentMethod: String
let paymentStatus: String
let paymentAmount: Amount
let transactionId: String?
let thirdPartyTransactionId: String?
let paidAt: String?
let createdAt: String
let updatedAt: String
enum CodingKeys: String, CodingKey {
case id
case paymentMethod = "payment_method"
case paymentStatus = "payment_status"
case paymentAmount = "payment_amount"
case transactionId = "transaction_id"
case thirdPartyTransactionId = "third_party_transaction_id"
case paidAt = "paid_at"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
///
struct Amount: Codable {
let amount: String
let currency: String
}
///
struct OrderItem: Codable, Identifiable {
let id: String
let productId: Int
let productType: String
let productCode: String
let productName: String
let unitPrice: Amount
let discountAmount: Amount
let quantity: Int
let totalPrice: Amount
enum CodingKeys: String, CodingKey {
case id
case productId = "product_id"
case productType = "product_type"
case productCode = "product_code"
case productName = "product_name"
case unitPrice = "unit_price"
case discountAmount = "discount_amount"
case quantity
case totalPrice = "total_price"
}
}
///
enum OrderStatus: Int, Codable {
case pending = 0 //
case paid = 1 //
case completed = 2 //
case cancelled = 3 //
case refunded = 4 // 退
var description: String {
switch self {
case .pending: return "待支付"
case .paid: return "已支付"
case .completed: return "已完成"
case .cancelled: return "已取消"
case .refunded: return "已退款"
}
}
}

View File

@ -34,13 +34,12 @@ final class IAPManager: ObservableObject {
}
// Trigger App Store purchase sheet
func purchasePioneer() async {
guard !isPurchasing else { return }
func purchasePioneer() async throws -> String {
guard !isPurchasing else { throw NSError(domain: "IAPError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Purchase already in progress"]) }
guard let product = pioneerProduct else {
// Surface an actionable error so the UI can inform the user
self.errorMessage = "Subscription product unavailable. Please try again later."
return
throw NSError(domain: "IAPError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Subscription product unavailable"])
}
isPurchasing = true
defer { isPurchasing = false }
@ -50,21 +49,26 @@ final class IAPManager: ObservableObject {
case .success(let verification):
switch verification {
case .unverified(_, let error):
self.errorMessage = "Purchase unverified: \(error.localizedDescription)"
throw error
case .verified(let transaction):
// Update entitlement for the purchased product
print("🎉 订阅成功!", transaction)
print("🔄 交易验证通过 - ID: \(transaction.id), 原始ID: \(transaction.originalID), 产品ID: \(transaction.productID)")
updateEntitlement(from: transaction)
let transactionID = String(transaction.id)
print("📝 使用交易ID: \(transactionID)")
await transaction.finish()
return transactionID
}
case .userCancelled:
break
throw NSError(domain: "IAPError", code: -3, userInfo: [NSLocalizedDescriptionKey: "Purchase was cancelled"])
case .pending:
break
throw NSError(domain: "IAPError", code: -4, userInfo: [NSLocalizedDescriptionKey: "Purchase is pending approval"])
@unknown default:
break
throw NSError(domain: "IAPError", code: -5, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"])
}
} catch {
self.errorMessage = "Purchase failed: \(error.localizedDescription)"
throw error
}
}

View File

@ -1,92 +1,148 @@
import SwiftUI
import WebKit
import SVGKit
struct SVGImage: UIViewRepresentable {
let svgName: String
var shouldFill: Bool = false
var contentMode: ContentMode = .fit
var tintColor: Color?
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.isScrollEnabled = false
webView.scrollView.contentInsetAdjustmentBehavior = .never
guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
print("❌ 无法找到 SVG 文件: \(svgName).svg")
return webView
private var svgPath: String {
return svgName
}
let fileURL = URL(fileURLWithPath: path)
let svgStyle = shouldFill ? """
width: 100%;
height: 100%;
object-fit: cover;
""" : """
max-width: 100%;
max-height: 100%;
object-fit: contain;
private func createImageView() -> SVGKFastImageView {
let emptySVGString = """
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet">
<rect width="1" height="1" fill="transparent"/>
</svg>
"""
let htmlString = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
.container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
svg {
\(svgStyle)
display: block;
}
</style>
</head>
<body>
<div class="container">
<object type="image/svg+xml" data="\(fileURL.lastPathComponent)"></object>
</div>
</body>
</html>
"""
webView.loadHTMLString(htmlString, baseURL: fileURL.deletingLastPathComponent())
return webView
if let data = emptySVGString.data(using: .utf8),
let svgImage = SVGKImage(data: data) {
let imageView = SVGKFastImageView(svgkImage: svgImage) ?? SVGKFastImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .clear
return imageView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
let fallbackView = SVGKFastImageView()
fallbackView.backgroundColor = .clear
return fallbackView
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: WKWebView, context: Context) -> CGSize? {
func makeUIView(context: Context) -> SVGKFastImageView {
print("🔄 开始加载SVG: \(svgName)")
let imageView = createImageView()
loadSVG(into: imageView)
configureView(imageView)
return imageView
}
private func loadSVG(into imageView: SVGKFastImageView) {
guard let path = Bundle.main.path(forResource: svgPath, ofType: "svg") else {
print("⚠️ 在main bundle中找不到文件: \(svgPath).svg")
return
}
let url = URL(fileURLWithPath: path)
guard let svgImage = SVGKImage(contentsOf: url) else {
print("❌ 无法从URL创建SVG: \(path)")
return
}
// SVG
let containerSize = imageView.bounds.size
if containerSize != .zero {
svgImage.size = containerSize
} else {
//
svgImage.size = CGSize(width: 100, height: 100)
}
print("✅ 成功加载SVG: \(svgName), 尺寸: \(svgImage.size)")
DispatchQueue.main.async {
imageView.image = svgImage
imageView.setNeedsLayout()
imageView.layoutIfNeeded()
}
}
private func configureView(_ imageView: SVGKFastImageView) {
imageView.contentMode = contentMode == .fit ? .scaleAspectFit : .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .clear
//
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
if let tintColor = tintColor?.uiColor {
imageView.tintColor = tintColor
DispatchQueue.main.async {
self.applyTintColor(tintColor, to: imageView.layer)
}
}
}
private func applyTintColor(_ color: UIColor, to layer: CALayer) {
if let shapeLayer = layer as? CAShapeLayer {
shapeLayer.fillColor = color.cgColor
}
layer.sublayers?.forEach { sublayer in
applyTintColor(color, to: sublayer)
}
}
func updateUIView(_ uiView: SVGKFastImageView, context: Context) {
loadSVG(into: uiView)
if let tintColor = tintColor?.uiColor {
uiView.tintColor = tintColor
DispatchQueue.main.async {
self.applyTintColor(tintColor, to: uiView.layer)
}
}
uiView.contentMode = contentMode == .fit ? .scaleAspectFit : .scaleAspectFill
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: SVGKFastImageView, context: Context) -> CGSize? {
return nil
}
}
// MARK: - ContentMode
extension SVGImage {
enum ContentMode {
case fit //
case fill //
}
}
// MARK: - Preview
#Preview {
VStack {
Text("Filled SVG")
SVGImage(svgName: "YourSVGName", shouldFill: true)
.frame(width: 200, height: 100)
VStack(spacing: 20) {
Text("IP SVG")
SVGImage(svgName: "IP")
.frame(width: 100, height: 100)
.background(Color.gray.opacity(0.2))
.border(Color.red, width: 1)
Text("Intrinsic Size SVG")
SVGImage(svgName: "YourSVGName", shouldFill: false)
.frame(width: 200, height: 100)
Text("Pioneer SVG")
SVGImage(svgName: "Pioneer", contentMode: .fill)
.frame(width: 100, height: 50)
.background(Color.gray.opacity(0.2))
.border(Color.blue, width: 1)
.clipped()
}
.padding()
}
// MARK: - Color Extension
private extension Color {
var uiColor: UIColor {
return UIColor(self)
}
}

View File

@ -0,0 +1,71 @@
import SwiftUI
import WebKit
struct SVGImageHtml: UIViewRepresentable {
let svgName: String
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.isScrollEnabled = false
webView.scrollView.contentInsetAdjustmentBehavior = .never
// 1. Get the URL for the SVG file
guard let path = Bundle.main.path(forResource: svgName, ofType: "svg") else {
print("❌ Cannot find SVG file: \(svgName).svg in bundle")
return webView
}
let fileURL = URL(fileURLWithPath: path)
do {
// 2. Read the SVG content directly
let svgString = try String(contentsOf: fileURL, encoding: .utf8)
// 3. Create HTML with inline SVG for better reliability
let htmlString = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
}
svg {
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
\(svgString)
</body>
</html>
"""
// 4. Load the HTML with base URL as the main bundle's resource path
if let resourcePath = Bundle.main.resourceURL {
webView.loadHTMLString(htmlString, baseURL: resourcePath)
} else {
webView.loadHTMLString(htmlString, baseURL: nil)
}
} catch {
print("❌ Error loading SVG file: \(error.localizedDescription)")
}
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
}

View File

@ -2,7 +2,6 @@ import SwiftUI
import AVKit
import os.log
/// A view that displays either an image or a video with fullscreen support
struct BlindOutcomeView: View {
let media: MediaType
let time: String?
@ -12,6 +11,7 @@ struct BlindOutcomeView: View {
@State private var isPlaying = false
@State private var showControls = true
@State private var showIPListModal = false
@State private var player: AVPlayer?
init(media: MediaType, time: String? = nil, description: String? = nil) {
self.media = media
@ -28,7 +28,6 @@ struct BlindOutcomeView: View {
//
HStack {
Button(action: {
//
presentationMode.wrappedValue.dismiss()
}) {
HStack(spacing: 4) {
@ -47,7 +46,6 @@ struct BlindOutcomeView: View {
Spacer()
//
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.opacity(0)
@ -56,7 +54,7 @@ struct BlindOutcomeView: View {
}
.padding(.vertical, 12)
.background(Color.themeTextWhiteSecondary)
.zIndex(1) //
.zIndex(1)
Spacer()
.frame(height: 30)
@ -65,7 +63,6 @@ struct BlindOutcomeView: View {
GeometryReader { geometry in
VStack(spacing: 16) {
ZStack {
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.white)
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
@ -86,33 +83,28 @@ struct BlindOutcomeView: View {
}
case .video(let url, _):
VideoPlayerView(url: url, isPlaying: $isPlaying)
VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player)
.frame(width: UIScreen.main.bounds.width - 40)
.background(Color.clear)
.cornerRadius(10)
.clipped()
.onAppear {
// Auto-play the video when it appears
isPlaying = true
}
.onDisappear {
isPlaying = false
player?.pause()
}
.onTapGesture {
withAnimation {
showControls.toggle()
}
}
.fullScreenCover(isPresented: $isFullscreen) {
FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: nil)
FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: player)
}
.overlay(
showControls ? VideoControls(
isPlaying: $isPlaying,
onClose: { isFullscreen = false }
) : nil
)
}
VStack(alignment: .leading, spacing: 8) {
if let description = description, !description.isEmpty {
VStack(alignment: .leading, spacing: 2) {
Text("Description")
@ -127,8 +119,6 @@ struct BlindOutcomeView: View {
.padding(.bottom, 12)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.top, 8)
}
}
@ -136,12 +126,13 @@ struct BlindOutcomeView: View {
.padding(.bottom, 20)
}
.padding(.horizontal)
Spacer()
// Button at bottom
VStack {
Spacer()
Button(action: {
// video
if case .video = media {
withAnimation {
showIPListModal = true
@ -162,22 +153,20 @@ struct BlindOutcomeView: View {
}
.padding(.bottom, 20)
}
.onDisappear {
// Clean up video player when view disappears
if case .video = media {
isPlaying = false
}
}
}
.navigationBarHidden(true) //
.navigationBarBackButtonHidden(true) //
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: isFullscreen)
}
.navigationViewStyle(StackNavigationViewStyle()) // iPad
.navigationBarHidden(true) //
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarHidden(true)
.overlay(
JoinModal(isPresented: $showIPListModal)
)
.onDisappear {
player?.pause()
player = nil
}
}
}
@ -187,22 +176,19 @@ private struct FullscreenMediaView: View {
@Binding var isPresented: Bool
@Binding var isPlaying: Bool
@State private var showControls = true
@State private var player: AVPlayer?
private let player: AVPlayer?
init(media: MediaType, isPresented: Binding<Bool>, isPlaying: Binding<Bool>, player: AVPlayer?) {
self.media = media
self._isPresented = isPresented
self._isPlaying = isPlaying
if let player = player {
self._player = State(initialValue: player)
}
self.player = player
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
// Media content
ZStack {
switch media {
case .image(let uiImage):
@ -216,25 +202,21 @@ private struct FullscreenMediaView: View {
}
}
case .video(let url, _):
VideoPlayerView(url: url, isPlaying: $isPlaying)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
withAnimation {
showControls.toggle()
case .video(_, _):
if let player = player {
CustomVideoPlayer(player: player)
.onAppear {
player.play()
isPlaying = true
}
.onDisappear {
player.pause()
isPlaying = false
}
}
.overlay(
showControls ? VideoControls(
isPlaying: $isPlaying,
onClose: { isPresented = false }
) : nil
)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Close button (always visible)
VStack {
HStack {
Button(action: { isPresented = false }) {
@ -251,42 +233,22 @@ private struct FullscreenMediaView: View {
Spacer()
}
}
.onAppear {
if case .video = media {
if isPlaying {
// player?.play()
}
}
}
.onDisappear {
if case .video = media {
// player?.pause()
// player?.replaceCurrentItem(with: nil)
// player = nil
}
player?.pause()
}
}
}
// MARK: - Video Controls
private struct VideoControls: View {
@Binding var isPlaying: Bool
let onClose: () -> Void
var body: some View {
// Empty view - no controls shown
EmptyView()
}
}
// MARK: - Video Player with Dynamic Aspect Ratio
// MARK: - Video Player View
struct VideoPlayerView: UIViewRepresentable {
let url: URL
@Binding var isPlaying: Bool
@Binding var player: AVPlayer?
func makeUIView(context: Context) -> PlayerView {
let view = PlayerView()
view.setupPlayer(url: url)
let player = view.setupPlayer(url: url)
self.player = player
return view
}
@ -299,39 +261,56 @@ struct VideoPlayerView: UIViewRepresentable {
}
}
// MARK: - Custom Video Player
@available(iOS 14.0, *)
struct CustomVideoPlayer: UIViewControllerRepresentable {
let player: AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
controller.videoGravity = .resizeAspect
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
uiViewController.player = player
}
}
// MARK: - Player View
class PlayerView: UIView {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
private var playerItem: AVPlayerItem?
private var playerItemObserver: NSKeyValueObservation?
func setupPlayer(url: URL) {
// Clean up existing resources
@discardableResult
func setupPlayer(url: URL) -> AVPlayer {
cleanup()
// Create new player
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
self.playerItem = playerItem
player = AVPlayer(playerItem: playerItem)
// Setup player layer
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspect
layer.addSublayer(playerLayer)
self.playerLayer = playerLayer
// Layout
playerLayer.frame = bounds
// Add observer for video end
NotificationCenter.default.addObserver(
self,
selector: #selector(playerItemDidReachEnd),
name: .AVPlayerItemDidPlayToEndTime,
object: playerItem
)
return player!
}
func play() {
@ -343,21 +322,17 @@ class PlayerView: UIView {
}
private func cleanup() {
// Remove observers
if let playerItem = playerItem {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
}
// Pause and clean up player
player?.pause()
player?.replaceCurrentItem(with: nil)
player = nil
// Remove player layer
playerLayer?.removeFromSuperlayer()
playerLayer = nil
// Release player item
playerItem?.cancelPendingSeeks()
playerItem?.asset.cancelLoading()
playerItem = nil
@ -377,24 +352,3 @@ class PlayerView: UIView {
cleanup()
}
}
// MARK: - Preview
struct BlindOutcomeView_Previews: PreviewProvider {
static var previews: some View {
// Preview with image and details
BlindOutcomeView(
media: .image(UIImage(systemName: "photo")!),
time: "2:30",
description: "This is a sample description for the preview. It shows how the text will wrap and display below the media content."
)
// Preview with video and details
if let url = URL(string: "https://example.com/sample.mp4") {
BlindOutcomeView(
media: .video(url, nil),
time: "1:45",
description: "Video content with time and description"
)
}
}
}

View File

@ -22,7 +22,7 @@ struct JoinModal: View {
// IP Image peeking from top
HStack {
// Make sure you have an image named "IP" in your assets
SVGImage(svgName: "IP1")
SVGImageHtml(svgName: "IP1")
.frame(width: 116, height: 65)
.offset(x: 30)
Spacer()

View File

@ -8,7 +8,7 @@ struct SlideInModal<Content: View>: View {
// -
private let animation = Animation.spring(
response: 0.8, // 使
dampingFraction: 0.6, // 使
dampingFraction: 1, // 使
blendDuration: 0.8 // 使
)
@ -28,6 +28,8 @@ struct SlideInModal<Content: View>: View {
}
}
//
ZStack(alignment: .leading) {
//
VStack(spacing: 0) {
//
@ -43,6 +45,18 @@ struct SlideInModal<Content: View>: View {
.frame(maxHeight: .infinity)
.background(Color(.systemBackground))
.edgesIgnoringSafeArea(.vertical)
}
//
.background(
RoundedRectangle(cornerRadius: 0)
.fill(Color(.systemBackground))
.shadow(
color: .black.opacity(0.3),
radius: 10,
x: 5,
y: 0
)
)
.offset(x: isPresented ? 0 : -UIScreen.main.bounds.width)
.zIndex(2)
.transition(.move(edge: .leading))

View File

@ -305,7 +305,7 @@ public class ImageUploaderGetID: ObservableObject {
}
print("✅ 成功获取上传URL")
print(" - 文件ID: \(fileId)")
print(" ❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️❗️ - 文件ID: \(fileId)")
print(" - 上传URL: \(uploadURLString)")
completion(.success((fileId: fileId, uploadURL: uploadURL)))

View File

@ -25,10 +25,20 @@ struct APIResponse<T: Codable>: Codable {
struct UserProfileModal: View {
@Binding var showModal: Bool
@Binding var showSettings: Bool
@Binding var isMember: Bool
@Binding var memberDate: String
@State private var userProfile: UserProfile?
@State private var isLoading = false
@State private var errorMessage: String?
@State private var isCopied = false
@State private var isContentReady = false
init(showModal: Binding<Bool>, showSettings: Binding<Bool>, isMember: Binding<Bool>, memberDate: Binding<String>) {
self._showModal = showModal
self._showSettings = showSettings
self._isMember = isMember
self._memberDate = memberDate
}
var body: some View {
VStack(spacing: 20) {
@ -42,10 +52,35 @@ struct UserProfileModal: View {
Text(error)
.foregroundColor(.red)
.padding()
} else if isContentReady, let userProfile = userProfile {
userProfileView(userProfile: userProfile)
.opacity(isContentReady ? 1 : 0)
.animation(.easeInOut(duration: 0.3), value: isContentReady)
} else {
// Empty view with same dimensions to prevent layout shifts
Color.clear
.frame(height: UIScreen.main.bounds.height * 0.7)
}
}
.frame(width: UIScreen.main.bounds.width * 0.8)
.background(Color.themeTextWhiteSecondary)
.edgesIgnoringSafeArea(.all)
.onAppear {
fetchUserInfo()
// Delay content appearance to sync with modal animation
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation {
isContentReady = true
}
}
}
}
@ViewBuilder
private func userProfileView(userProfile: UserProfile) -> some View {
VStack(spacing: 20) {
HStack(alignment: .center, spacing: 16) {
if let avatarUrl = userProfile?.avatarUrl, !avatarUrl.isEmpty, let url = URL(string: avatarUrl) {
if let avatarUrl = userProfile.avatarUrl, !avatarUrl.isEmpty, let url = URL(string: avatarUrl) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
@ -62,6 +97,9 @@ struct UserProfileModal: View {
.foregroundColor(.blue)
}
}
.onTapGesture {
Router.shared.navigate(to: .userInfo)
}
} else {
Image(systemName: "person.circle.fill")
.resizable()
@ -71,12 +109,12 @@ struct UserProfileModal: View {
}
VStack(alignment: .leading, spacing: 10) {
Text(userProfile?.nickname ?? "Name")
Text(userProfile.nickname)
.font(Typography.font(for: .body))
.fontWeight(.bold)
.foregroundColor(.themeTextMessageMain)
HStack(spacing: 4) {
Text("ID: \(userProfile?.userId ?? "")")
Text("ID: \(userProfile.userId)")
.font(.system(size: 14))
.foregroundColor(.themeTextMessageMain)
.lineLimit(1)
@ -84,21 +122,7 @@ struct UserProfileModal: View {
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: 120)
Button(action: {
print("Copy ID button tapped")
UIPasteboard.general.string = userProfile?.userId
print("Copied to clipboard:", userProfile?.userId ?? "nil")
withAnimation {
isCopied = true
// Reset after 2 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
isCopied = false
print("Reset copy button state")
}
}
}
}) {
Button(action: copyUserId) {
if isCopied {
Image(systemName: "checkmark")
.foregroundColor(.themePrimary)
@ -109,8 +133,8 @@ struct UserProfileModal: View {
.font(.system(size: 12))
.foregroundColor(.themeTextMessageMain)
.animation(.easeInOut, value: isCopied)
.contentShape(Rectangle()) // Make the entire button area tappable
.frame(width: 24, height: 24) // Ensure minimum touch target size
.contentShape(Rectangle())
.frame(width: 24, height: 24)
}
}
@ -124,43 +148,8 @@ struct UserProfileModal: View {
)
.padding(.horizontal)
Button(action: {
Router.shared.navigate(to: .subscribe)
}) {
ZStack(alignment: .center) {
// SVG -
SVGImage(svgName: "Pioneer")
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: .infinity)
//
VStack(alignment: .leading, spacing: 6) {
Text("Pioneer")
.font(Typography.font(for: .title3))
.fontWeight(.bold)
.foregroundColor(.themeTextMessageMain)
.padding(.top, 6)
Text("Expires on :")
.font(.system(size: 12))
.foregroundColor(.themeTextMessageMain)
.padding(.top, 2)
Text("March 15, 2025")
.font(.system(size: 12))
.fontWeight(.bold)
.foregroundColor(.themeTextMessageMain)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 32)
// 使
.padding(.vertical, 12)
}
.frame(height: 112)
.frame(maxWidth: .infinity)
.cornerRadius(16)
.clipped() //
}
//
currentSubscriptionCard
VStack(spacing: 12) {
// upload
@ -208,26 +197,26 @@ struct UserProfileModal: View {
.buttonStyle(PlainButtonStyle())
// Box
Button(action: {
Router.shared.navigate(to: .mediaUpload)
}) {
HStack(spacing: 16) {
SVGImage(svgName: "Box")
.foregroundColor(.orange)
.frame(width: 20, height: 20)
// Button(action: {
// Router.shared.navigate(to: .mediaUpload)
// }) {
// HStack(spacing: 16) {
// SVGImage(svgName: "Box")
// .foregroundColor(.orange)
// .frame(width: 20, height: 20)
Text("My Blind Box")
.font(Typography.font(for: .body))
.fontWeight(.bold)
.foregroundColor(.themeTextMessageMain)
// Text("My Blind Box")
// .font(Typography.font(for: .body))
// .fontWeight(.bold)
// .foregroundColor(.themeTextMessageMain)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
// Spacer()
// }
// .padding()
// .cornerRadius(10)
// .contentShape(Rectangle())
// }
// .buttonStyle(PlainButtonStyle())
// setting
Button(action: {
@ -262,12 +251,42 @@ struct UserProfileModal: View {
.padding(.horizontal)
Spacer()
}
.frame(width: UIScreen.main.bounds.width * 0.8)
.background(Color.themeTextWhiteSecondary)
.edgesIgnoringSafeArea(.all)
.onAppear {
fetchUserInfo()
}
private func copyUserId() {
UIPasteboard.general.string = userProfile?.userId
withAnimation {
isCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
isCopied = false
}
}
}
}
// MARK: -
private var currentSubscriptionCard: some View {
let status: SubscriptionStatus = {
if isMember {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let expiryDate = dateFormatter.date(from: memberDate) ?? Date()
return .pioneer(expiryDate: expiryDate)
} else {
return .free
}
}()
return SubscriptionStatusBar(
status: status,
height: 112,
onSubscribeTap: {
//
Router.shared.navigate(to: .subscribe)
}
)
.padding(.horizontal, Theme.Spacing.xl)
}
private func fetchUserInfo() {
@ -295,5 +314,5 @@ struct UserProfileModal: View {
}
#Preview {
UserProfileModal(showModal: .constant(true), showSettings: .constant(false))
UserProfileModal(showModal: .constant(true), showSettings: .constant(false), isMember: .constant(true), memberDate: .constant(""))
}

View File

@ -32,8 +32,8 @@ struct CreditsInfoCard: View {
mainCreditsSection
}
.buttonStyle(PlainButtonStyle())
.background(Theme.Colors.primaryLight)
.cornerRadius(Theme.CornerRadius.extraLarge)
.background(Color.themeTextWhite)
.cornerRadius(Theme.CornerRadius.round)
// .shadow(color: Theme.Shadows.small, radius: Theme.Shadows.cardShadow.radius, x: Theme.Shadows.cardShadow.x, y: Theme.Shadows.cardShadow.y)
}

View File

@ -1,5 +1,6 @@
import SwiftUI
import AVKit
import WaterfallGrid
// MARK: - API Response Models
struct MaterialResponse: Decodable {
@ -27,7 +28,7 @@ struct MemoryItem: Identifiable, Decodable {
var title: String { name ?? "Untitled" }
var subtitle: String { description ?? "" }
var mediaType: MemoryMediaType {
let url = fileInfo.url.lowercased()
let url = fileInfo.fileName.lowercased()
if url.hasSuffix(".mp4") || url.hasSuffix(".mov") {
return .video(url: fileInfo.url, previewUrl: previewFileInfo.url)
} else {
@ -98,30 +99,17 @@ struct MemoriesView: View {
ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea()
Group {
if isLoading {
ProgressView()
.scaleEffect(1.5)
} else if let error = errorMessage {
Text("Error: \(error)")
.foregroundColor(.red)
} else {
ScrollView {
LazyVGrid(columns: columns, spacing: 4) {
ForEach(memories) { memory in
WaterfallGrid(memories) { memory in
MemoryCard(memory: memory)
.padding(.horizontal, 2)
.onTapGesture {
withAnimation(.spring()) {
selectedMemory = memory
}
}
}
}
.padding(.top, 4)
.padding(.horizontal, 4)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
}
}
@ -172,7 +160,23 @@ struct FullScreenMediaView: View {
@State private var isVideoPlaying = false
@State private var showControls = true
@State private var controlsTimer: Timer? = nil
@State private var player: AVPlayer? = nil
@State private var imageAspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
imageAspectRatio = 16/9 // Default to 16:9 if we can't determine the aspect ratio
isLoading = false
return
}
imageAspectRatio = width / height
isLoading = false
}
var body: some View {
ZStack {
@ -182,18 +186,34 @@ struct FullScreenMediaView: View {
// Media content with back button overlay
ZStack {
// Media content
GeometryReader { geometry in
switch memory.mediaType {
case .image(let url):
if let imageURL = URL(string: url) {
AsyncImage(url: imageURL) { phase in
switch phase {
case .success(let image):
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
.edgesIgnoringSafeArea(.all)
.scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear {
if let uiImage = image.asUIImage() {
let size = uiImage.size
imageAspectRatio = size.width / size.height
isLoading = false
}
}
case .failure(_):
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.red)
@ -205,22 +225,27 @@ struct FullScreenMediaView: View {
}
}
case .video(let url, let previewUrl):
if let videoURL = URL(string: url) {
VideoPlayer(player: player)
case .video(_, let previewUrl):
GeometryReader { geometry in
ZStack {
Color.clear
VideoPlayer(url: memory.mediaType.url, isPlaying: $isVideoPlaying)
.aspectRatio(imageAspectRatio, contentMode: .fit)
.frame(
width: min(geometry.size.width, geometry.size.height * imageAspectRatio),
height: min(geometry.size.height, geometry.size.width / imageAspectRatio)
)
.onAppear {
self.player = AVPlayer(url: videoURL)
self.player?.play()
self.isVideoPlaying = true
if let previewUrl = URL(string: previewUrl) {
loadAspectRatio(from: previewUrl)
}
isVideoPlaying = true
}
.onDisappear {
self.player?.pause()
self.player = nil
isVideoPlaying = false
}
.frame(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
.onTapGesture {
togglePlayPause()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
}
@ -231,13 +256,14 @@ struct FullScreenMediaView: View {
Button(action: {
withAnimation(.spring()) {
isPresented = nil
pauseVideo()
// pauseVideo()
}
}) {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.padding(12)
.background(Circle().fill(Color.black.opacity(0.4)))
}
.padding(.leading, 16)
.padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
@ -247,28 +273,6 @@ struct FullScreenMediaView: View {
Spacer()
}
.zIndex(2) // Higher z-index to keep it above media content
// Video controls overlay (only for video)
if case .video = memory.mediaType, showControls {
VStack {
Spacer()
// Play/pause button
Button(action: {
togglePlayPause()
}) {
Image(systemName: isVideoPlaying ? "pause.circle.fill" : "play.circle.fill")
.font(.system(size: 70))
.foregroundColor(.white.opacity(0.9))
.shadow(radius: 3)
}
.padding(.bottom, 30)
}
.transition(.opacity)
.onAppear {
resetControlsTimer()
}
.zIndex(3) // Highest z-index for controls
}
}
.ignoresSafeArea()
.statusBar(hidden: true)
@ -276,116 +280,141 @@ struct FullScreenMediaView: View {
.onTapGesture {
if case .video = memory.mediaType {
withAnimation(.easeInOut) {
showControls.toggle()
}
if showControls {
resetControlsTimer()
// showControls.toggle()
}
// if showControls {
// resetControlsTimer()
// }
}
}
.statusBar(hidden: true)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
if case .video = memory.mediaType {
setupVideoPlayer()
// setupVideoPlayer()
}
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
controlsTimer?.invalidate()
pauseVideo()
// pauseVideo()
}
}
private func setupVideoPlayer() {
if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) {
self.player = AVPlayer(url: videoURL)
self.player?.play()
self.isVideoPlaying = true
// private func setupVideoPlayer() {
// if case .video(let url, _) = memory.mediaType, let videoURL = URL(string: url) {
// // No need to set up player here
// }
// }
// Add observer for playback end
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: self.player?.currentItem,
queue: .main
) { _ in
self.player?.seek(to: .zero) { _ in
self.player?.play()
}
}
}
}
// private func togglePlayPause() {
// if isVideoPlaying {
// pauseVideo()
// } else {
// playVideo()
// }
// withAnimation {
// showControls = true
// }
// resetControlsTimer()
// }
private func togglePlayPause() {
if isVideoPlaying {
pauseVideo()
} else {
playVideo()
}
withAnimation {
showControls = true
}
resetControlsTimer()
}
// private func playVideo() {
// // No need to play video here
// }
private func playVideo() {
player?.play()
isVideoPlaying = true
}
// private func pauseVideo() {
// // No need to pause video here
// }
private func pauseVideo() {
player?.pause()
isVideoPlaying = false
}
private func resetControlsTimer() {
controlsTimer?.invalidate()
controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
withAnimation(.easeInOut) {
showControls = false
}
}
}
// private func resetControlsTimer() {
// controlsTimer?.invalidate()
// controlsTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
// withAnimation(.easeInOut) {
// showControls = false
// }
// }
// }
}
struct VideoPlayer: UIViewRepresentable {
let player: AVPlayer?
struct VideoPlayer: UIViewControllerRepresentable {
let url: String
@Binding var isPlaying: Bool
func makeUIView(context: Context) -> UIView {
let view = UIView()
if let player = player {
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = UIScreen.main.bounds
playerLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(playerLayer)
}
return view
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let player = AVPlayer(url: URL(string: url)!)
controller.player = player
controller.showsPlaybackControls = true
controller.videoGravity = .resizeAspect
// Make the background transparent
controller.view.backgroundColor = .clear
controller.view.isOpaque = false
return controller
}
func updateUIView(_ uiView: UIView, context: Context) {
if let player = player, let playerLayer = uiView.layer.sublayers?.first as? AVPlayerLayer {
playerLayer.player = player
playerLayer.frame = UIScreen.main.bounds
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if isPlaying {
uiViewController.player?.play()
} else {
uiViewController.player?.pause()
}
}
}
struct MemoryCard: View {
let memory: MemoryItem
@State private var aspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
aspectRatio = 16/9 // Default to 16:9 if we can't determine the aspect ratio
isLoading = false
return
}
aspectRatio = width / height
isLoading = false
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
ZStack {
// Media content
Group {
switch memory.mediaType {
case .image(let url):
if let url = URL(string: url) {
AsyncImage(url: url) { phase in
Group {
if let image = phase.image {
GeometryReader { geometry in
ZStack {
Color.black
image
.resizable()
.aspectRatio(contentMode: .fill)
.scaledToFit()
.frame(
width: min(geometry.size.width, geometry.size.height * aspectRatio),
height: min(geometry.size.height, geometry.size.width / aspectRatio)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.aspectRatio(aspectRatio, contentMode: aspectRatio > 1 ? .fit : .fill)
.onAppear {
if let uiImage = image.asUIImage() {
let size = uiImage.size
aspectRatio = size.width / size.height
isLoading = false
}
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
@ -393,31 +422,38 @@ struct MemoryCard: View {
}
}
}
}
case .video(_, let previewUrl):
if let previewUrl = URL(string: previewUrl) {
AsyncImage(url: previewUrl) { phase in
Group {
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear {
loadAspectRatio(from: previewUrl)
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
}
}
}
} else {
Color.gray.opacity(0.3)
}
}
}
.frame(width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio))
.frame(
width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) / (isLoading ? 1 : aspectRatio)
)
.clipped()
.cornerRadius(12)
// Show play button for videos
if case .video = memory.mediaType {
Image(systemName: "play.circle.fill")
.font(.system(size: 40))
@ -426,8 +462,7 @@ struct MemoryCard: View {
}
}
// Title and Subtitle
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Text(memory.title)
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
@ -444,6 +479,40 @@ struct MemoryCard: View {
}
}
// Add this extension to get UIImage from Image
extension View {
func asUIImage() -> UIImage? {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
// Add this extension to MemoryMediaType to get the URL
private extension MemoryMediaType {
var isVideo: Bool {
if case .video = self { return true }
return false
}
var url: String {
switch self {
case .image(let url):
return url
case .video(let url, _):
return url
}
}
}
#Preview {
MemoriesView()
}

View File

@ -50,18 +50,17 @@ public struct AvatarPicker: View {
if let selectedImage = selectedImage {
Image(uiImage: selectedImage)
.resizable()
.scaledToFill()
.aspectRatio(contentMode: .fill)
.frame(width: 225, height: 225)
.scaleEffect(scaleFactor)
.clipShape(RoundedRectangle(cornerRadius: 20 * scaleFactor))
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.themePrimary, lineWidth: borderWidth)
.scaleEffect(scaleFactor)
)
.scaleEffect(scaleFactor)
} else {
// Default SVG avatar with animated dashed border
SVGImage(svgName: "IP")
SVGImageHtml(svgName: "IP")
.frame(width: 225, height: 225)
.scaleEffect(scaleFactor)
.contentShape(Rectangle())

View File

@ -53,50 +53,50 @@ enum SubscriptionStatus {
struct SubscriptionStatusBar: View {
let status: SubscriptionStatus
let onSubscribeTap: (() -> Void)?
private let height: CGFloat
init(status: SubscriptionStatus, onSubscribeTap: (() -> Void)? = nil) {
init(status: SubscriptionStatus, height: CGFloat? = nil, onSubscribeTap: (() -> Void)? = nil) {
self.status = status
self.height = height ?? 155 // 155
self.onSubscribeTap = onSubscribeTap
}
var body: some View {
ZStack(alignment: .topLeading) {
ZStack(alignment: .leading) {
// Background SVG - First layer
SVGImage(svgName: status.backgroundImageName, shouldFill: true)
SVGImage(svgName: status.backgroundImageName)
.frame(maxWidth: .infinity, minHeight: 120)
.clipped()
// Content - Second layer
// Main content container
VStack(alignment: .leading, spacing: 0) {
Spacer()
// Subscription title
// Title - Centered vertically
Text(status.title)
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundColor(status.textColor)
.padding(.leading, 24)
.frame(maxHeight: .infinity, alignment: .center) // Center vertically
.padding(.leading, 12)
.padding(.top, height < 155 ? 30 : 40)
// Expiry date or subscribe button
// Expiry date - Bottom left
if case .pioneer(let expiryDate) = status {
VStack(alignment: .leading, spacing: 4) {
Text("Expires on:")
.font(.system(size: 14, weight: .medium))
.foregroundColor(status.textColor.opacity(0.9))
Text("Expires on :")
.font(.system(size: 12))
.foregroundColor(.themeTextMessageMain)
Text(formatDate(expiryDate))
.font(.system(size: 16, weight: .semibold))
.foregroundColor(status.textColor)
.font(.system(size: 12))
.fontWeight(.bold)
.foregroundColor(.themeTextMessageMain)
}
.padding(.leading, 24)
.padding(.bottom, 20)
.padding(.leading, 12)
.padding(.bottom, 12)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
.frame(height: 155)
.frame(maxWidth: .infinity)
.cornerRadius(20)
.clipped()
.frame(height: height)
}
// MARK: -

View File

@ -7,6 +7,7 @@
import SwiftUI
import StoreKit
import Network
// MARK: -
enum SubscriptionPlan: String, CaseIterable {
@ -46,6 +47,7 @@ struct SubscribeView: View {
@State private var showErrorAlert = false
@State private var errorText = ""
@State private var memberProfile: MemberProfile?
@State private var showSuccessAlert = false
//
private let features = [
@ -61,7 +63,6 @@ struct SubscribeView: View {
dismiss()
}
.background(Color.themeTextWhiteSecondary)
.padding(.bottom, Theme.Spacing.lg)
ScrollView {
VStack(spacing: 0) {
@ -123,12 +124,17 @@ struct SubscribeView: View {
} message: {
Text(errorText)
}
.alert("Purchase Success", isPresented: $showSuccessAlert) {
Button("OK", role: .cancel) { }
} message: {
Text("购买成功!")
}
}
// MARK: -
private var currentSubscriptionCard: some View {
let status: SubscriptionStatus = {
if memberProfile?.membershipLevel == "pioneer" {
if memberProfile?.membershipLevel == "Pioneer" {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let expiryDate = memberProfile.flatMap { dateFormatter.date(from: $0.membershipEndAt) } ?? Date()
@ -139,7 +145,7 @@ struct SubscribeView: View {
}()
return SubscriptionStatusBar(
status: .pioneer(expiryDate: Date()),
status: status,
onSubscribeTap: {
//
handleSubscribe()
@ -232,6 +238,9 @@ struct SubscribeView: View {
HStack(spacing: 8) {
Button(action: {
//
if let url = URL(string: "https://memorywake.com/privacy-policy") {
UIApplication.shared.open(url)
}
}) {
Text("Terms of Service")
.underline()
@ -242,6 +251,9 @@ struct SubscribeView: View {
Button(action: {
//
if let url = URL(string: "https://memorywake.com/privacy-policy") {
UIApplication.shared.open(url)
}
}) {
Text("Privacy Policy")
.underline()
@ -250,13 +262,24 @@ struct SubscribeView: View {
Text("|")
.foregroundColor(.secondary)
// Button(action: {
// Task { await store.restorePurchases() }
// }) {
// Text("Restore Purchase")
// .underline()
// }
Button(action: {
Task { await store.restorePurchases() }
//
if let url = URL(string: "https://memorywake.com/privacy-policy") {
UIApplication.shared.open(url)
}
}) {
Text("Restore Purchase")
Text("AI Usage Guidelines")
.underline()
}
}
.font(Typography.font(for: .caption, family: .quicksandRegular))
.foregroundColor(.secondary)
.padding(.top, Theme.Spacing.sm)
@ -264,7 +287,286 @@ struct SubscribeView: View {
// MARK: -
private func handleSubscribe() {
Task { await store.purchasePioneer() }
isLoading = true
Task {
do {
print("🔄 开始订阅流程...")
// 1.
print("🔄 正在创建订单...")
let orderInfo = try await createOrder()
// 2. id
print("🔄 正在创建支付...")
let paymentInfo = try await createPayment(orderId: orderInfo.id)
// 3. 使
print("🔄 开始苹果内购流程...")
do {
//
let transactionId = try await store.purchasePioneer()
print("✅ 苹果内购成功交易ID: \(transactionId)")
// 4.
print("🔄 正在通知服务器支付处理中...")
_ = try await notifyPaymentProcessing(
transactionId: paymentInfo.transactionId ?? paymentInfo.id,
// thirdPartyTransactionId: transactionId
)
print("🔄 正在通知服务器支付成功...")
_ = try await notifyPaymentSuccess(
transactionId: paymentInfo.transactionId ?? paymentInfo.id,
// thirdPartyTransactionId: transactionId
)
print("✅ 订阅流程完成")
// 5.
await MainActor.run {
self.isLoading = false
self.dismiss()
}
} catch let purchaseError as NSError {
print("❌ 苹果内购失败: \(purchaseError.localizedDescription)")
//
print("🔄 正在通知服务器支付失败...")
_ = try? await notifyPaymentFailure(
transactionId: paymentInfo.transactionId ?? paymentInfo.id,
reason: purchaseError.localizedDescription
)
// 便
throw purchaseError
}
} catch let error as NSError {
print("❌ 订阅失败: \(error.localizedDescription)")
//
var errorMessage = error.localizedDescription
if error.domain == "NetworkError" {
errorMessage = "网络连接失败,请检查您的网络设置"
} else if error.domain == "APIError" {
errorMessage = "请求失败,请稍后重试 (错误码: \(error.code))"
} else if error.domain == NSURLErrorDomain {
switch error.code {
case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost:
errorMessage = "网络连接已断开,请检查您的网络设置"
case NSURLErrorTimedOut:
errorMessage = "请求超时,请稍后重试"
case NSURLErrorCannotConnectToHost, NSURLErrorCannotFindHost:
errorMessage = "无法连接到服务器,请稍后重试"
default:
errorMessage = "网络错误: \(error.localizedDescription)"
}
}
// 线UI
await MainActor.run {
self.isLoading = false
self.errorText = errorMessage
self.showErrorAlert = true
print("❌ 错误提示: \(errorMessage)")
}
}
}
}
//
private func createOrder() async throws -> OrderInfo {
return try await withCheckedThrowingContinuation { continuation in
let parameters: [String: Any] = [
"items": [
[
"product_item_id": 5,
"quantity": 1
]
]
]
print("🔄 开始创建订单请求,参数:\(parameters)")
//
let monitor = NWPathMonitor()
let queue = DispatchQueue(label: "NetworkMonitor")
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
//
NetworkService.shared.postWithToken(
path: "/order/create",
parameters: parameters
) { (result: Result<APIResponse<OrderInfo>, NetworkError>) in
switch result {
case .success(let response):
print("✅ 请求成功,状态码:\(response.code)")
print("📦 返回数据:\(String(describing: response.data))")
if response.code == 0 {
continuation.resume(returning: response.data)
} else {
let errorMessage = "创建订单失败,状态码:\(response.code)"
print("\(errorMessage)")
continuation.resume(throwing: NSError(
domain: "APIError",
code: response.code,
userInfo: [NSLocalizedDescriptionKey: errorMessage]
))
}
case .failure(let error):
print("❌ 请求异常:\(error.localizedDescription)")
print("🔍 错误详情:\(error)")
if let urlError = error as? URLError {
print("🌐 URL错误: \(urlError.code.rawValue) - \(urlError.localizedDescription)")
print("🔗 失败URL: \(urlError.failingURL?.absoluteString ?? "未知")")
}
continuation.resume(throwing: error)
}
}
} else {
//
let errorMessage = "网络连接不可用,请检查网络设置"
print("\(errorMessage)")
continuation.resume(throwing: NSError(domain: "NetworkError", code: 0, userInfo: [NSLocalizedDescriptionKey: errorMessage]))
}
}
monitor.start(queue: queue)
}
}
//
private func createPayment(orderId: String) async throws -> PaymentInfo {
return try await withCheckedThrowingContinuation { continuation in
let parameters: [String: Any] = [
"order_id": orderId,
"payment_method": "ApplePay"
]
print("🔄 开始创建支付请求,参数:\(parameters)")
NetworkService.shared.postWithToken(
path: "/order/pay",
parameters: parameters
) { (result: Result<APIResponse<PaymentInfo>, NetworkError>) in
switch result {
case .success(let response):
print("✅ 请求成功,状态码:\(response.code)")
print("📦 返回数据:\(String(describing: response.data))")
if response.code == 0 {
continuation.resume(returning: response.data)
} else {
let errorMessage = "创建支付失败,状态码:\(response.code)"
print("\(errorMessage)")
continuation.resume(throwing: NSError(
domain: "APIError",
code: response.code,
userInfo: [NSLocalizedDescriptionKey: errorMessage]
))
}
case .failure(let error):
print("❌ 请求异常:\(error.localizedDescription)")
print("🔍 错误详情:\(error)")
if let urlError = error as? URLError {
print("🌐 URL错误: \(urlError.code.rawValue) - \(urlError.localizedDescription)")
print("🔗 失败URL: \(urlError.failingURL?.absoluteString ?? "未知")")
}
continuation.resume(throwing: error)
}
}
}
}
// MARK: -
///
/// - Parameter transactionId: ID
/// - Parameter thirdPartyTransactionId: ID
private func notifyPaymentProcessing(transactionId: String, thirdPartyTransactionId: String? = nil) async throws -> Bool {
return try await withCheckedThrowingContinuation { continuation in
var parameters: [String: Any] = ["transaction_id": transactionId]
// ID
if let thirdPartyId = thirdPartyTransactionId {
parameters["third_party_transaction_id"] = thirdPartyId
}
print("🔄 通知服务器支付处理中,参数:\(parameters)")
NetworkService.shared.postWithToken(
path: "/order/pay-processing",
parameters: parameters
) { (result: Result<APIResponse<[String: String]?>, NetworkError>) in
switch result {
case .success(let response):
print("✅ 支付处理通知发送成功,状态码:\(response.code)")
continuation.resume(returning: response.code == 0)
case .failure(let error):
print("❌ 支付处理通知发送失败:\(error.localizedDescription)")
continuation.resume(throwing: error)
}
}
}
}
///
/// - Parameter transactionId: ID
/// - Parameter thirdPartyTransactionId: ID
private func notifyPaymentSuccess(transactionId: String, thirdPartyTransactionId: String? = nil) async throws -> Bool {
return try await withCheckedThrowingContinuation { continuation in
var parameters: [String: Any] = ["transaction_id": transactionId]
// ID
if let thirdPartyId = thirdPartyTransactionId {
parameters["third_party_transaction_id"] = thirdPartyId
}
print("🔄 通知服务器支付成功,参数:\(parameters)")
NetworkService.shared.postWithToken(
path: "/order/pay-success",
parameters: parameters
) { (result: Result<APIResponse<[String: String]?>, NetworkError>) in
switch result {
case .success(let response):
print("✅ 支付成功通知发送成功,状态码:\(response.code)")
continuation.resume(returning: response.code == 0)
case .failure(let error):
print("❌ 支付成功通知发送失败:\(error.localizedDescription)")
continuation.resume(throwing: error)
}
}
}
}
///
/// - Parameter transactionId: ID
/// - Parameter reason:
private func notifyPaymentFailure(transactionId: String, reason: String) async throws -> Bool {
return try await withCheckedThrowingContinuation { continuation in
let parameters: [String: Any] = [
"transaction_id": transactionId,
"reason": reason
]
print("🔄 通知服务器支付失败,参数:\(parameters)")
NetworkService.shared.postWithToken(
path: "/order/pay-failure",
parameters: parameters
) { (result: Result<APIResponse<[String: String]?>, NetworkError>) in
switch result {
case .success(let response):
print("✅ 支付失败通知发送成功,状态码:\(response.code)")
continuation.resume(returning: response.code == 0)
case .failure(let error):
print("❌ 支付失败通知发送失败:\(error.localizedDescription)")
continuation.resume(throwing: error)
}
}
}
}
// MARK: - Helper Methods

View File

@ -599,7 +599,7 @@ struct UploadPromptView: View {
var body: some View {
Button(action: { showMediaPicker = true }) {
//
SVGImage(svgName: "IP")
SVGImageHtml(svgName: "IP")
.frame(width: 225, height: 225)
.contentShape(Rectangle())
.overlay(