Skip to content

Commit 4a9365c

Browse files
committed
feat:iap
1 parent a4428da commit 4a9365c

31 files changed

+763
-159
lines changed

Guozaoke.xcodeproj/project.pbxproj

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
322627D92D35608300F4AAA8 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 322627D82D35608300F4AAA8 /* Alamofire */; };
1414
32370F552D56665200EFE597 /* RichText in Frameworks */ = {isa = PBXBuildFile; productRef = 32370F542D56665200EFE597 /* RichText */; };
1515
328B0EA32D448EEB0023EBE2 /* JDStatusBarNotification in Frameworks */ = {isa = PBXBuildFile; productRef = 328B0EA22D448EEB0023EBE2 /* JDStatusBarNotification */; };
16+
329B21292D81BDA1009ABF72 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 329B21282D81BDA1009ABF72 /* StoreKit.framework */; };
1617
/* End PBXBuildFile section */
1718

1819
/* Begin PBXContainerItemProxy section */
@@ -36,6 +37,7 @@
3637
322627622D3393AC00F4AAA8 /* Guozaoke.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Guozaoke.app; sourceTree = BUILT_PRODUCTS_DIR; };
3738
322627722D3393AF00F4AAA8 /* GuozaokeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GuozaokeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3839
3226277C2D3393AF00F4AAA8 /* GuozaokeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GuozaokeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
40+
329B21282D81BDA1009ABF72 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
3941
/* End PBXFileReference section */
4042

4143
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -75,6 +77,7 @@
7577
buildActionMask = 2147483647;
7678
files = (
7779
322627D42D35566700F4AAA8 /* Kingfisher in Frameworks */,
80+
329B21292D81BDA1009ABF72 /* StoreKit.framework in Frameworks */,
7881
322627D92D35608300F4AAA8 /* Alamofire in Frameworks */,
7982
32370F552D56665200EFE597 /* RichText in Frameworks */,
8083
32113EA42D60419000D9B331 /* MarkdownUI in Frameworks */,
@@ -106,6 +109,7 @@
106109
322627642D3393AC00F4AAA8 /* Guozaoke */,
107110
322627752D3393AF00F4AAA8 /* GuozaokeTests */,
108111
3226277F2D3393AF00F4AAA8 /* GuozaokeUITests */,
112+
329B21272D81BDA1009ABF72 /* Frameworks */,
109113
322627632D3393AC00F4AAA8 /* Products */,
110114
);
111115
sourceTree = "<group>";
@@ -120,6 +124,14 @@
120124
name = Products;
121125
sourceTree = "<group>";
122126
};
127+
329B21272D81BDA1009ABF72 /* Frameworks */ = {
128+
isa = PBXGroup;
129+
children = (
130+
329B21282D81BDA1009ABF72 /* StoreKit.framework */,
131+
);
132+
name = Frameworks;
133+
sourceTree = "<group>";
134+
};
123135
/* End PBXGroup section */
124136

125137
/* Begin PBXNativeTarget section */
@@ -437,9 +449,9 @@
437449
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
438450
CODE_SIGN_IDENTITY = "Apple Development";
439451
CODE_SIGN_STYLE = Automatic;
440-
CURRENT_PROJECT_VERSION = 121;
452+
CURRENT_PROJECT_VERSION = 125;
441453
DEVELOPMENT_ASSET_PATHS = "\"Guozaoke/Preview Content\"";
442-
DEVELOPMENT_TEAM = W3H3KU6C4U;
454+
DEVELOPMENT_TEAM = 63K948ZA2P;
443455
ENABLE_PREVIEWS = YES;
444456
GENERATE_INFOPLIST_FILE = YES;
445457
INFOPLIST_FILE = Guozaoke/Info.plist;
@@ -456,8 +468,8 @@
456468
"$(inherited)",
457469
"@executable_path/Frameworks",
458470
);
459-
MARKETING_VERSION = 1.5;
460-
PRODUCT_BUNDLE_IDENTIFIER = com.youyu.meetyou;
471+
MARKETING_VERSION = 1.5.1;
472+
PRODUCT_BUNDLE_IDENTIFIER = com.guozaoke.app.ios;
461473
PRODUCT_NAME = "$(TARGET_NAME)";
462474
PROVISIONING_PROFILE_SPECIFIER = "";
463475
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -476,7 +488,7 @@
476488
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
477489
CODE_SIGN_IDENTITY = "Apple Development";
478490
CODE_SIGN_STYLE = Automatic;
479-
CURRENT_PROJECT_VERSION = 121;
491+
CURRENT_PROJECT_VERSION = 125;
480492
DEVELOPMENT_ASSET_PATHS = "\"Guozaoke/Preview Content\"";
481493
DEVELOPMENT_TEAM = 63K948ZA2P;
482494
ENABLE_PREVIEWS = YES;
@@ -495,7 +507,7 @@
495507
"$(inherited)",
496508
"@executable_path/Frameworks",
497509
);
498-
MARKETING_VERSION = 1.5;
510+
MARKETING_VERSION = 1.5.1;
499511
PRODUCT_BUNDLE_IDENTIFIER = com.guozaoke.app.ios;
500512
PRODUCT_NAME = "$(TARGET_NAME)";
501513
PROVISIONING_PROFILE_SPECIFIER = "";

Guozaoke/APIService/APIService.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ enum MyTopicEnum: String {
5050
}
5151
}
5252

53+
struct DeveloperInfo {
54+
static let username = "isau"
55+
static let email = "268144637@qq.com"
56+
57+
}
58+
5359
/// App 版本信息
5460
struct AppInfo {
5561
static let appIntro = "过早客是源自武汉的高端社交网络,这里有关于创业、创意、IT、金融等最热话题的交流,也有招聘问答、活动交友等最新资讯的发布。"
@@ -59,15 +65,15 @@ struct AppInfo {
5965
static let AppStoreReviewUrl = "itms-apps://itunes.apple.com/app/id\(AppId)?action=write-review"
6066

6167
static var appName: String {
62-
Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Guozaoke"
68+
return Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "过早客"
6369
}
6470

6571
static var appVersion: String {
66-
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
72+
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
6773
}
6874

6975
static var buildNumber: String {
70-
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "100"
76+
return Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "100"
7177
}
7278

7379

Guozaoke/AppDelegate.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import UIKit
1212
import UserNotifications
1313

1414
class AppDelegate: UIResponder, UIApplicationDelegate {
15-
//var notificationManager = NotificationManager()
16-
15+
//var notificationManager = NotificationManager()
1716
// 应用启动完成时调用
1817
func application(
1918
_ application: UIApplication,
2019
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
2120
) -> Bool {
21+
2222
print("App 启动完成")
2323
NotificationManager.shared.requestNotificationPermission()
2424
NotificationManager.shared.scheduleDailyNotification()

Guozaoke/Common/AppearanceManager.swift

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,39 @@ let subTitleFontSize = titleFontSize-2
1818
let titleFontSize = UserDefaultsKeys.settingFontSize
1919

2020
let usernameFontSize = 13.0
21-
let menuFontSize = 15.0
21+
let menuFontSize = 15.0
22+
23+
/// 推荐字体
24+
enum RecommandFontOption: CaseIterable, Identifiable {
25+
case system
26+
case pingFangSCThin
27+
case pingFangSCLight
28+
case pingFangSCRegular
29+
case pingFangSCMedium
30+
31+
var id: String {
32+
name
33+
}
34+
35+
var name: String {
36+
switch self {
37+
case .system:
38+
return "系统默认"
39+
case .pingFangSCThin:
40+
return "PingFangSC-Thin"
41+
case .pingFangSCLight:
42+
return "PingFangSC-Light"
43+
case .pingFangSCRegular:
44+
return "PingFangSC-Regular"
45+
case .pingFangSCMedium:
46+
return "PingFangSC-Medium"
47+
}
48+
}
49+
50+
var size: CGFloat {
51+
return UserDefaultsKeys.fontSize16
52+
}
53+
}
2254

2355
struct UserDefaultsKeys {
2456
static let pushNotificationsEnabled = "pushNotificationsEnabled"
@@ -30,11 +62,7 @@ struct UserDefaultsKeys {
3062
/// 18
3163
static let fontSize16 = 18.0
3264
static let fontName = UIFont.systemFont(ofSize: settingFontSize).fontName
33-
static let pingFangSCThin = "PingFangSC-Thin"
34-
static let pingFangSCLight = "PingFangSC-Light"
35-
static let pingFangSCRegular = "PingFangSC-Regular"
36-
static let pingFangSCMedium = "PingFangSC-Medium"
37-
65+
3866
static var settingPushNotificationsEnabled: Bool {
3967
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.pushNotificationsEnabled)
4068
}

Guozaoke/Common/CustomViewModifier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func DynamicContextMenuContent(userInfo: String, showSafari: Binding<Bool>, show
183183
if let email = userInfo.extractEmails.first, email.contains("@") {
184184
if email != userInfo {
185185
Button(action: {
186-
showSafari.wrappedValue.toggle()
186+
email.copyToClipboard()
187187
}) {
188188
Text("拷贝邮箱")
189189
SFSymbol.copy

Guozaoke/Common/Extensions/Extentions.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ extension String {
1515
static let `default`: String = ""
1616
public static let empty = `default`
1717

18-
1918
var isBlank: Bool {
2019
return self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
2120
}
@@ -326,6 +325,17 @@ struct DeviceUtils {
326325
let safeAreaInsets = getSafeAreaInsets()
327326
return safeAreaInsets.top > 20
328327
}
328+
329+
static var getDeviceModel: String {
330+
var systemInfo = utsname()
331+
uname(&systemInfo)
332+
let machineMirror = Mirror(reflecting: systemInfo.machine)
333+
let identifier = machineMirror.children.reduce("") { identifier, element in
334+
guard let value = element.value as? Int8, value != 0 else { return identifier }
335+
return identifier + String(UnicodeScalar(UInt8(value)))
336+
}
337+
return identifier
338+
}
329339
}
330340

331341

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//
2+
// KeychainHelper.swift
3+
// Guozaoke
4+
//
5+
// Created by scy on 2025/3/12.
6+
//
7+
8+
import Foundation
9+
import Security
10+
11+
struct KeychainKeys {
12+
static let purchaseGuozaokeKey = "purchaseGuozaokeKey"
13+
}
14+
15+
/// 1.5.2
16+
let purchasedVersion = "1.5.2"
17+
18+
// MARK: - PurchaseAppState
19+
20+
class PurchaseAppState: ObservableObject {
21+
@Published var isPurchased: Bool = false
22+
23+
private let purchaseKey = KeychainKeys.purchaseGuozaokeKey
24+
25+
init() {
26+
checkAndSavePurchaseStatus()
27+
}
28+
29+
func checkAndSavePurchaseStatus() {
30+
let currentVersion = AppInfo.appVersion
31+
if currentVersion <= purchasedVersion {
32+
if let _ = KeychainHelper.retrieve(key: purchaseKey) {
33+
} else {
34+
savePurchaseStatus(isPurchased: self.isPurchased)
35+
}
36+
self.isPurchased = true
37+
} else if let savedStatus = KeychainHelper.retrieve(key: purchaseKey) {
38+
self.isPurchased = savedStatus == "purchased".data(using: .utf8)
39+
} else {
40+
self.isPurchased = false
41+
}
42+
log("[app][version][iap] currentVersion \(currentVersion) purchasedVersion \(purchasedVersion) isPurchased \(self.isPurchased)")
43+
}
44+
45+
func savePurchaseStatus(isPurchased: Bool) {
46+
self.isPurchased = isPurchased
47+
let status = isPurchased ? "purchased" : "not_purchased"
48+
if let data = status.data(using: .utf8) {
49+
let success = KeychainHelper.save(key: purchaseKey, data: data)
50+
if success {
51+
log("[iap][purchase] Purchase status saved successfully")
52+
} else {
53+
log("[iap][purchase] Failed to save purchase status")
54+
}
55+
}
56+
}
57+
58+
func clear() {
59+
isPurchased = false
60+
KeychainHelper.clearPurchaseStatus()
61+
}
62+
}
63+
64+
// MARK: - KeychainHelper
65+
class KeychainHelper {
66+
67+
static func save(key: String, data: Data) -> Bool {
68+
let query: [String: Any] = [
69+
kSecClass as String: kSecClassGenericPassword,
70+
kSecAttrAccount as String: key,
71+
kSecValueData as String: data
72+
]
73+
SecItemDelete(query as CFDictionary)
74+
let status = SecItemAdd(query as CFDictionary, nil)
75+
return status == errSecSuccess
76+
}
77+
78+
// Retrieve data from Keychain
79+
static func retrieve(key: String) -> Data? {
80+
let query: [String: Any] = [
81+
kSecClass as String: kSecClassGenericPassword,
82+
kSecAttrAccount as String: key,
83+
kSecReturnData as String: kCFBooleanTrue!,
84+
kSecMatchLimit as String: kSecMatchLimitOne
85+
]
86+
87+
var dataTypeRef: AnyObject? = nil
88+
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
89+
90+
if status == errSecSuccess {
91+
return dataTypeRef as? Data
92+
}
93+
94+
return nil
95+
}
96+
97+
static func update(key: String, data: Data) -> Bool {
98+
let query: [String: Any] = [
99+
kSecClass as String: kSecClassGenericPassword,
100+
kSecAttrAccount as String: key
101+
]
102+
103+
let attributes: [String: Any] = [
104+
kSecValueData as String: data
105+
]
106+
107+
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
108+
return status == errSecSuccess
109+
}
110+
111+
static func delete(key: String) -> Bool {
112+
let query: [String: Any] = [
113+
kSecClass as String: kSecClassGenericPassword,
114+
kSecAttrAccount as String: key
115+
]
116+
117+
let status = SecItemDelete(query as CFDictionary)
118+
return status == errSecSuccess
119+
}
120+
121+
static func clearPurchaseStatus() {
122+
let clear = delete(key: KeychainKeys.purchaseGuozaokeKey)
123+
log("[iap][clear] \(clear)")
124+
}
125+
}

0 commit comments

Comments
 (0)