Initial commit

This commit is contained in:
oscarz
2024-08-12 10:49:20 +08:00
parent 3002510aaf
commit 00fd0adf89
331 changed files with 53210 additions and 130 deletions

View File

@ -6,3 +6,16 @@
//
import Foundation
import SwiftUI
extension Color {
static func hex(_ hex: UInt, alpha: Double = 1.0) -> Color {
return Color(
red: Double((hex >> 16) & 0xFF) / 255.0,
green: Double((hex >> 8) & 0xFF) / 255.0,
blue: Double(hex & 0xFF) / 255.0,
opacity: alpha
)
}
}

View File

@ -6,3 +6,55 @@
//
import Foundation
class CommonFunc {
static let shared = CommonFunc()
private let profanityList: [String] = ["fuck", "shit", "porn", "习近平", "鸡巴", "阴茎"] // Example profanity words
private init() {} // Private initializer to ensure singleton usage
func validateInputEng(input: String, isSingleWord: Bool = false) -> (isValid: Bool, message: String) {
// Check for profanity
for badWord in profanityList {
if input.lowercased().contains(badWord) {
return (false, globalEnvironment.DirtyInputErrToast)
}
}
// Check characters based on the isSingleWord flag
let pattern = isSingleWord ? "^[A-Za-z]+$" : "^[A-Za-z0-9 .,;:!?'\"@-]+$"
let regex = try! NSRegularExpression(pattern: pattern)
let range = NSRange(location: 0, length: input.utf16.count)
if regex.firstMatch(in: input, options: [], range: range) == nil {
let characterSet = isSingleWord ? "english letters" : "english letters, numbers, and punctuation"
return (false, "Input should only contain \(characterSet).")
}
return (true, "Input is valid.")
}
func validateChineseInput(input: String) -> (isValid: Bool, message: String) {
// Check for profanity
for word in profanityList {
if input.contains(word) {
return (false, globalEnvironment.DirtyInputErrToast)
}
}
// Check if all characters are valid Chinese characters or punctuation
for character in input {
if !character.isPunctuation && !(0x4E00...0x9FFF).contains(character.unicodeScalars.first!.value) &&
!(0x3400...0x4DBF).contains(character.unicodeScalars.first!.value) &&
!(0x20000...0x2A6DF).contains(character.unicodeScalars.first!.value) {
//
let res = self.validateInputEng(input: input)
if !res.isValid{
return (false, "Input contains invalid characters.")
}
}
}
return (true, "Input is valid.")
}
}

View File

@ -6,3 +6,228 @@
//
import Foundation
import StoreKit
import SwiftUI
import SwiftyBeaver
enum IAPProduct: String, CaseIterable {
case premiumFeature1 = "grammar_1_week"
case premiumFeature2 = "grammar_1_month"
case premiumFeature3 = "grammar_1_year"
var weight: Int {
switch self {
case .premiumFeature1:
return 1
case .premiumFeature2:
return 2
case .premiumFeature3:
return 3
}
}
}
class IAPManager: ObservableObject {
@Published var products: [Product] = [] //
@Published var purchasedProducts: [Product] = [] //
private var processedTransactionIDs = Set<UInt64>() // ID
init() {
Task {
await requestProducts()
await updatePurchasedProducts()
await listenForTransactions()
}
}
//
func requestProducts() async {
do {
let products = try await Product.products(for: IAPProduct.allCases.map { $0.rawValue })
DispatchQueue.main.async {
//
self.products = products.sorted { product1, product2 in
let weight1 = IAPProduct(rawValue: product1.id)?.weight ?? 0
let weight2 = IAPProduct(rawValue: product2.id)?.weight ?? 0
return weight1 < weight2
}
//
for product in self.products {
if let receiptData = try? product.jsonRepresentation {
if let jsonString = String(data: receiptData, encoding: .utf8) {
logger.info("product details for \(product.id): \(jsonString)")
} else {
logger.info("product details for \(product.id): ", context: ["Tile": product.displayName, "Price": product.price, "DisplayPrice": product.displayPrice])
}
}else {
logger.info("product details for \(product.id): ", context: ["Tile": product.displayName, "Price": product.price, "DisplayPrice": product.displayPrice])
}
}
}
} catch {
logger.error("Failed to fetch products: \(error.localizedDescription)")
}
}
//
func buy(product: Product, completion: @escaping (Result<String, Error>) -> Void) async {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
//
let appAccountToken = transaction.appAccountToken
let productId = transaction.productID
let transactionId = transaction.id
//
if let receiptData = try? transaction.jsonRepresentation {
if let jsonString = String(data: receiptData, encoding: .utf8) {
logger.info("Transaction details for \(transactionId): \(jsonString)")
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
//
await validateReceipt(receiptData: transaction.jsonRepresentation, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: transaction.environment.rawValue)
await transaction.finish()
await updatePurchasedProducts()
DispatchQueue.main.async {
//
completion(.success("Purchase Successful"))
}
}
case .userCancelled:
//
logger.error("User cancelled the purchase.")
DispatchQueue.main.async {
//
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Cancelled"])))
}
case .pending:
//
logger.error("Purchase is pending.")
DispatchQueue.main.async {
//
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Pending"])))
}
default:
break
}
} catch {
logger.error("Failed to purchase product: \(error.localizedDescription)")
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
// https://developer.apple.com/documentation/storekit/transaction/3851204-currententitlements
// A sequence of the latest transactions that entitle a user to in-app purchases and subscriptions.
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
if let product = products.first(where: { $0.id == transaction.productID }) {
DispatchQueue.main.async {
self.purchasedProducts.append(product)
}
}
}
}
}
// https://developer.apple.com/documentation/storekit/transaction/3851206-updates
// The asynchronous sequence that emits a transaction when the system creates or updates transactions that occur outside of the app or on other devices.
// Note that after a successful in-app purchase on the same device, StoreKit returns the transaction through Product.PurchaseResult.success(_:).
func listenForTransactions() async {
for await transactionResult in Transaction.updates {
if case .verified(let transaction) = transactionResult {
if let product = products.first(where: { $0.id == transaction.productID }) {
DispatchQueue.main.async {
self.purchasedProducts.append(product)
}
}
//
let appAccountToken = transaction.appAccountToken
let productId = transaction.productID
let transactionId = transaction.id
//
if let receiptData = try? transaction.jsonRepresentation {
if let jsonString = String(data: receiptData, encoding: .utf8) {
logger.info("Transaction details for \(transactionId): \(jsonString)")
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
//
await validateReceipt(receiptData: transaction.jsonRepresentation, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: transaction.environment.rawValue)
await transaction.finish()
}
}
}
// https://developer.apple.com/documentation/storekit/appstore/3791906-sync
// Synchronizes your apps transaction information and subscription status with information from the App Store.
func restorePurchases(completion: @escaping (Result<String, Error>) -> Void) async {
do {
try await AppStore.sync()
await updatePurchasedProducts()
completion(.success("Purchase Successful"))
logger.info("Purchases restored")
} catch {
logger.error("Failed to restore purchases: \(error.localizedDescription)")
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Pending"])))
}
}
//
func validateReceipt(receiptData: Data, appAccountToken: UUID?, productId: String, transactionId: UInt64, env: String) async {
// NetworkManagerIapVerify
NetworkManager.shared.IapVerify(receiptData: receiptData, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: env) { result in
switch result {
case .success(let response):
DispatchQueue.main.async {
logger.info("Receipt verification succeeded: \(response.ret)", context: ["ProductID":productId, "TransactionID":transactionId])
//
self.updatePurchasedStatus(productId: productId)
}
case .failure(let error):
//
DispatchQueue.main.async {
switch error {
case .businessError(let ret, let message):
logger.error("Business error - Ret: \(ret), Message: \(message)", context: ["ProductID":productId, "TransactionID":transactionId])
case .other(let error):
logger.error("network occurred: \(error.localizedDescription)", context: ["ProductID":productId, "TransactionID":transactionId])
}
}
}
}
}
//
private func updatePurchasedStatus(productId: String) {
//
}
}

View File

@ -6,5 +6,88 @@
//
import Foundation
import Alamofire
import TrustDecision
import SwiftJWT
import SwiftyBeaver
class InitApp {
static let shared = InitApp(environment: globalEnvironment)
private let userDefaults = UserDefaults.standard
private let deviceIDKey = "DeviceID"
private let vipStatusKey = "VIPStatus"
private var deviceID: String = ""
private var vipStatus: Bool = false
var environment: GlobalEnvironment
init(environment: GlobalEnvironment) {
self.environment = environment
initializeApp()
}
private func initializeApp() {
fetchDeviceID()
}
private func fetchDeviceID() {
if let savedDeviceID = userDefaults.string(forKey: deviceIDKey) {
self.environment.deviceID = savedDeviceID
logger.info("DeviceID from UserDefaults: \(savedDeviceID)")
//
getUser()
} else {
getDeviceIDFromTrustDecision()
}
}
private func getDeviceIDFromTrustDecision() {
var options = [String : NSObject]()
let responseCallback: ([String : Any]) -> Void = { response in
DispatchQueue.main.async { // 线UI
if let deviceId = response["device_id"] as? String {
self.deviceID = deviceId
self.userDefaults.set(deviceId, forKey: self.deviceIDKey)
self.environment.deviceID = deviceId
logger.info("Device ID from TrustDecision: \(self.deviceID)")
// VIP
self.getUser()
}
}
}
options["callback"] = unsafeBitCast(responseCallback as @convention(block) ([String : Any]) -> Void, to: AnyObject.self) as? NSObject
let manager = TDMobRiskManager.sharedManager()
manager?.pointee.initWithOptions(options)
}
private func getUser() {
NetworkManager.shared.getUserProfile() { result in
DispatchQueue.main.async {
switch result {
case .success(let userData):
self.vipStatus = userData.vip == 1
self.environment.GID = userData.id
self.environment.userID = userData.userid
self.environment.userName = userData.username
self.environment.isVip = userData.vip == 1
logger.info("getUserProfile: ID: \(userData.id), userID: \(userData.userid), userName: \(userData.username), vip: \(userData.vip)")
case .failure(let error):
switch error {
case .businessError(let ret, let message):
logger.error("Business error - Ret: \(ret), Message: \(message)")
case .other(let error):
logger.error("network occurred: \(error.localizedDescription)")
}
}
}
}
}
func refreshUserInfo(){
getUser()
}
}

View File

@ -6,3 +6,38 @@
//
import Foundation
import SwiftyBeaver
let logger = SwiftyBeaver.self
func setupLogging() {
let console = ConsoleDestination()
let file = FileDestination()
//file.logFileURL = URL(fileURLWithPath: "/path/to/your/log/file.log")
// use custom format and set console output to short time, log level & message
// console.format = "$DHH:mm:ss$d $L $M"
// or use this for JSON output:
// console.format = "$J"
// In Xcode 15, specifying the logging method as .logger to display color, subsystem, and category information in the console.(Relies on the OSLog API)
//console.logPrintWay = .logger(subsystem: "Main", category: "UI")
// If you prefer not to use the OSLog API, you can use print instead.
// console.logPrintWay = .print
console.format = "$DHH:mm:ss$d $C$L$c $N.$F:$l - $M"
//
console.levelColor.verbose = "⚪️ " // White
console.levelColor.debug = "🔵 " // Blue
console.levelColor.info = "🟢 " // Green
console.levelColor.warning = "🟡 " // Yellow
console.levelColor.error = "🔴 " // Red
logger.addDestination(console)
logger.addDestination(file)
}

View File

@ -10,35 +10,50 @@ import Alamofire
import SwiftJWT
import SwiftyBeaver
// jwt
struct MyClaims: Claims {
var deviceID: String
var gid: Int
var exp1: Int
}
// {"ret":%d, "message":"%s", "data":jsonData}
struct APIResponse<T: Decodable>: Decodable {
let ret: Int
let message: String
let data: T?
}
//
enum NetworkError: Error {
case businessError(ret: Int, message: String)
case other(Error)
}
// 使
struct GrammarCheckRsp : Codable{
let data: [GrammarRes]
}
//
struct Translation: Identifiable, Codable, Equatable {
var id = UUID()
var input: String
var translation: String
}
//
struct TranslationResponse: Codable {
let translation: String
}
//
struct WordDetails {
var word: String = ""
var explanations: [String] = []
var phrases: [String] = []
var synonyms: [String] = []
}
//
struct WordDetailsResponse: Codable {
let word: String
let explain: [String]?
@ -56,6 +71,7 @@ struct WordDetailsResponse: Codable {
}
}
// 使
struct VIPStatusResponse: Codable {
let id: Int
let userid: String
@ -63,73 +79,122 @@ struct VIPStatusResponse: Codable {
let vip: Int
}
// 使
struct IAPVerifyRsp: Codable {
let ret: String
//let productType : String
}
//
struct NetworkManager {
static let shared = NetworkManager()
private let jwtSecret = globalEnvironment.jwtSecret
//
func getHeaderTimezoneInfo() -> [String: String] {
let timezone = TimeZone.current
let timezoneIdentifier = timezone.identifier // "America/New_York"
let secondsFromGMT = timezone.secondsFromGMT() // GMT
return [
"timezone": timezoneIdentifier,
"secondsfromgmt": String(secondsFromGMT)
]
}
//
func checkGrammar(inputText: String, completion: @escaping ([GrammarRes]?, Error?) -> Void) {
func checkGrammar(inputText: String, completion: @escaping (Result<[GrammarRes], NetworkError>) -> Void) {
let url = globalEnvironment.grammarURL
let headers: HTTPHeaders = createAuthorizationHeader()
var headers: HTTPHeaders = createAuthorizationHeader()
let timezoneHeaders = getHeaderTimezoneInfo()
headers.add(name: "timezone", value: timezoneHeaders["timezone"]!)
headers.add(name: "secondsfromgmt", value: timezoneHeaders["secondsfromgmt"]!)
let parameters: [String: Any] = [
"input": inputText,
"lang": "eng"
]
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers)
.responseDecodable(of: [GrammarRes].self) { response in
switch response.result {
case .success(let results):
completion(results, nil)
case .failure(let error):
completion(nil, error)
// 使
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers,
completion: { (result: Result<[GrammarRes], NetworkError>) in
switch result {
case .success(let results):
completion(.success(results))
case .failure(let error):
//
completion(.failure(error))
}
}
}
)
}
//
func fetchWordDetails(inputText: String, lang: String = "eng", completion: @escaping (Result<WordDetails, Error>) -> Void) {
func fetchWordDetails(inputText: String, lang: String = "eng", completion: @escaping (Result<WordDetails, NetworkError>) -> Void) {
guard !inputText.isEmpty else { return }
let parameters: [String: Any] = ["input": inputText, "lang": lang]
let url = globalEnvironment.dictURL
let headers: HTTPHeaders = createAuthorizationHeader()
var headers: HTTPHeaders = createAuthorizationHeader()
let timezoneHeaders = getHeaderTimezoneInfo()
headers.add(name: "timezone", value: timezoneHeaders["timezone"]!)
headers.add(name: "secondsfromgmt", value: timezoneHeaders["secondsfromgmt"]!)
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers)
.responseDecodable(of: WordDetailsResponse.self) { response in
switch response.result {
// 使
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers,
completion: { (result: Result<WordDetailsResponse, NetworkError>) in
switch result {
case .success(let detailsResponse):
print("Success: Received data for \(detailsResponse.word)")
let details = detailsResponse.toWordDetails() // Convert here
completion(.success(details))
case .failure(let error):
print("Error: \(error)")
print("Response Data: \(String(data: response.data ?? Data(), encoding: .utf8) ?? "No data")")
//
completion(.failure(error))
}
}
)
}
//
func translate(inputText: String, lang: String = "chs", completion: @escaping (Result<Translation, Error>) -> Void) {
func translate(inputText: String, lang: String = "chs", completion: @escaping (Result<Translation, NetworkError>) -> Void) {
guard !inputText.isEmpty else { return }
let url = globalEnvironment.translateURL
let parameters: [String: Any] = ["input": inputText, "lang": lang]
let headers: HTTPHeaders = createAuthorizationHeader()
var headers: HTTPHeaders = createAuthorizationHeader()
let timezoneHeaders = getHeaderTimezoneInfo()
headers.add(name: "timezone", value: timezoneHeaders["timezone"]!)
headers.add(name: "secondsfromgmt", value: timezoneHeaders["secondsfromgmt"]!)
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers)
.responseDecodable(of: TranslationResponse.self) { response in
switch response.result {
// 使
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers,
completion: { (result: Result<TranslationResponse, NetworkError>) in
switch result {
case .success(let translationResponse):
let newTranslation = Translation(input: inputText, translation: translationResponse.translation)
completion(.success(newTranslation))
case .failure(let error):
//
completion(.failure(error))
}
}
)
}
//
@ -146,16 +211,15 @@ struct NetworkManager {
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers).response { response in
switch response.result {
case .success(let data):
print("Feedback sent successfully: \(String(describing: data))")
logger.info("Feedback sent successfully.", context: ["input":input, "isPositive": isPositive])
case .failure(let error):
print("Error sending feedback: \(error)")
logger.error("Error sending feedback: \(error)", context: ["input":input, "isPositive": isPositive])
}
}
}
// VIP
func getUserProfile(completion: @escaping (Result<VIPStatusResponse, Error>) -> Void) {
func getUserProfile(completion: @escaping (Result<VIPStatusResponse, NetworkError>) -> Void) {
let url = globalEnvironment.userURL
let parameters: [String: Any] = [:]
@ -167,27 +231,52 @@ struct NetworkManager {
method: .post,
encoding: URLEncoding.httpBody,
headers: headers, // 使JWT token
completion: { (result: Result<VIPStatusResponse, Error>) in
completion: { (result: Result<VIPStatusResponse, NetworkError>) in
switch result {
case .success(let vipData):
logger.info("VIP Status: \(vipData.vip)")
completion(.success(vipData))
case .failure(let error):
logger.error("Error: \(error.localizedDescription)")
//
completion(.failure(error))
}
}
)
}
// VIP
func IapVerify(receiptData: Data, appAccountToken: UUID?, productId: String, transactionId: UInt64, env: String, completion: @escaping (Result<IAPVerifyRsp, NetworkError>) -> Void) {
let url = globalEnvironment.iapVerifyURL
let parameters: [String: Any] = ["transid":transactionId, "appaccounttoken": appAccountToken ?? "", "productid":productId, "receiptdata":receiptData, "env": env]
let headers: HTTPHeaders = createAuthorizationHeader()
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers, // 使JWT token
completion: { (result: Result<IAPVerifyRsp, NetworkError>) in
switch result {
case .success(let rsp):
completion(.success(rsp))
case .failure(let error):
//
completion(.failure(error))
}
}
)
}
//
func performRequest<T: Decodable>(
endpoint: String,
parameters: Parameters, // 使 Alamofire Parameters
method: HTTPMethod = .post,
encoding: URLEncoding = .httpBody,
headers: HTTPHeaders? = nil,
completion: @escaping (Result<T, Error>) -> Void
completion: @escaping (Result<T, NetworkError>) -> Void
) {
let url = endpoint
let defaultHeaders: HTTPHeaders = [.contentType("application/x-www-form-urlencoded")]
@ -200,15 +289,18 @@ struct NetworkManager {
if apiResponse.ret == 0, let data = apiResponse.data {
completion(.success(data))
} else {
let error = NSError(domain: "", code: apiResponse.ret, userInfo: [NSLocalizedDescriptionKey: apiResponse.message])
completion(.failure(error))
completion(.failure(.businessError(ret: apiResponse.ret, message: apiResponse.message)))
}
case .failure(let error):
completion(.failure(error))
if let httpResponse = response.response {
logger.error("network erorr.", context: ["url":url, "StatusCode":httpResponse.statusCode])
}
completion(.failure(.other(error)))
}
}
}
// jwt token
private func createAuthorizationHeader() -> HTTPHeaders {
// Generate JWT and return headers
guard let jwtToken = generateJWT(deviceID: globalEnvironment.deviceID, gID: globalEnvironment.GID, jwtSecret: jwtSecret) else {