diff --git a/AsyncSwift.xcodeproj/project.pbxproj b/AsyncSwift.xcodeproj/project.pbxproj index f334825..7272948 100644 --- a/AsyncSwift.xcodeproj/project.pbxproj +++ b/AsyncSwift.xcodeproj/project.pbxproj @@ -7,8 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + B25E600C28D2400500E96C78 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25E600B28D2400500E96C78 /* KeyChain.swift */; }; B289943328CA69FF002B9F67 /* StampView+Observed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B289943228CA69FF002B9F67 /* StampView+Observed.swift */; }; B2E1083128C9CD6900C3DD59 /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E1083028C9CD6900C3DD59 /* AppData.swift */; }; + B2FC6F6328D309FF00D2ACBF /* Stamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FC6F6228D309FF00D2ACBF /* Stamp.swift */; }; C63A865F28CA70ED0064C417 /* EventDetailView+Observed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A865E28CA70ED0064C417 /* EventDetailView+Observed.swift */; }; C63A866328CB3D490064C417 /* View+.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A866228CB3D490064C417 /* View+.swift */; }; C63A866528CB3F6D0064C417 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A866428CB3F6D0064C417 /* DateFormatter+.swift */; }; @@ -36,8 +38,10 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + B25E600B28D2400500E96C78 /* KeyChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = ""; }; B289943228CA69FF002B9F67 /* StampView+Observed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StampView+Observed.swift"; sourceTree = ""; }; B2E1083028C9CD6900C3DD59 /* AppData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppData.swift; sourceTree = ""; }; + B2FC6F6228D309FF00D2ACBF /* Stamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stamp.swift; sourceTree = ""; }; C63A865E28CA70ED0064C417 /* EventDetailView+Observed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventDetailView+Observed.swift"; sourceTree = ""; }; C63A866228CB3D490064C417 /* View+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+.swift"; sourceTree = ""; }; C63A866428CB3F6D0064C417 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; @@ -150,6 +154,7 @@ C66C68D228D1B00A0091F960 /* EventModel.swift */, C66C68D428D1B0130091F960 /* SessionModel.swift */, E94F92C628D2505100D9E759 /* Ticketing.swift */, + B2FC6F6228D309FF00D2ACBF /* Stamp.swift */, ); path = Models; sourceTree = ""; @@ -160,6 +165,7 @@ C6F7798A28C9CBC60036773B /* EventView+Observed.swift */, C63A865E28CA70ED0064C417 /* EventDetailView+Observed.swift */, C66DAD4F28CF478700195DEB /* SessionView+Observed.swift */, + B25E600B28D2400500E96C78 /* KeyChain.swift */, B2E1083028C9CD6900C3DD59 /* AppData.swift */, B289943228CA69FF002B9F67 /* StampView+Observed.swift */, E9171EFF28D15426002FAF52 /* TicketingView+Observed.swift */, @@ -250,6 +256,7 @@ C66DAD5028CF478700195DEB /* SessionView+Observed.swift in Sources */, C6E744A028CA557100B7B2BD /* Color+.swift in Sources */, C68DE95128C77DDA00CA4CC8 /* TicketingView.swift in Sources */, + B25E600C28D2400500E96C78 /* KeyChain.swift in Sources */, C6F7798B28C9CBC60036773B /* EventView+Observed.swift in Sources */, C66C68D528D1B0130091F960 /* SessionModel.swift in Sources */, E9171F0028D15426002FAF52 /* TicketingView+Observed.swift in Sources */, @@ -265,6 +272,7 @@ C63A866528CB3F6D0064C417 /* DateFormatter+.swift in Sources */, C6F7798F28C9D1BF0036773B /* SessionView.swift in Sources */, C66C68D328D1B00A0091F960 /* EventModel.swift in Sources */, + B2FC6F6328D309FF00D2ACBF /* Stamp.swift in Sources */, B2E1083128C9CD6900C3DD59 /* AppData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -392,7 +400,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = AsyncSwift/AsyncSwift.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AsyncSwift/Preview Content\""; DEVELOPMENT_TEAM = 76AJ433CP5; @@ -415,6 +424,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.kim.AsyncSwift; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = AsyncSwiftProvisioningProfile; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -427,7 +437,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = AsyncSwift/AsyncSwift.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"AsyncSwift/Preview Content\""; DEVELOPMENT_TEAM = 76AJ433CP5; @@ -450,6 +461,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.kim.AsyncSwift; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = AsyncSwiftProvisioningProfile; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; diff --git a/AsyncSwift/AsyncSwiftApp.swift b/AsyncSwift/AsyncSwiftApp.swift index 8a40c6e..feb2e88 100644 --- a/AsyncSwift/AsyncSwiftApp.swift +++ b/AsyncSwift/AsyncSwiftApp.swift @@ -17,7 +17,11 @@ struct AsyncSwiftApp: App { MainTabView() .environmentObject(appData) .onOpenURL { url in - print(url) + if appData.checkLink(url: url) { + print("Success Link URL: \(url)") + } else { + print("Fail Link URL: \(url)") + } } } } diff --git a/AsyncSwift/Models/Stamp.swift b/AsyncSwift/Models/Stamp.swift new file mode 100644 index 0000000..d50375b --- /dev/null +++ b/AsyncSwift/Models/Stamp.swift @@ -0,0 +1,12 @@ +// +// Stamp.swift +// AsyncSwift +// +// Created by Inho Choi on 2022/09/15. +// + +import Foundation + +struct Stamp: Decodable { + let title: String +} diff --git a/AsyncSwift/Observed/AppData.swift b/AsyncSwift/Observed/AppData.swift index 306ea97..dc1eb26 100644 --- a/AsyncSwift/Observed/AppData.swift +++ b/AsyncSwift/Observed/AppData.swift @@ -9,8 +9,70 @@ import SwiftUI import UIKit final class AppData: ObservableObject { - @Published var currentTab: Tab = .event // Universal Link로 앱진입시 StampView 전환을 위한 변수 - var scannedSeminarQR = true // Universal Link로 진입시 QR코드 스캔 여부 + /// Universal Link로 앱진입시 StampView 전환을 위한 변수 + @Published var currentTab: Tab = .event + + private var currentStamp: Stamp? + lazy var isStampExist: Bool = { + KeyChain.shared.getItem(key: currentStamp?.title) != nil + }() + + init(){ + fetchCurrentStamp() + } + + func checkLink(url: URL) -> Bool { + // URL Example = https://www.asyncswift.info?tab=ticketing + // URL Example = https://www.asyncswift.info?tab=event + guard URLComponents(url: url, resolvingAgainstBaseURL: true)?.host != nil else { return false } + + var queries = [String: String]() + for item in URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems ?? [] { + queries[item.name] = item.value + } + + guard let currentStampName = currentStamp?.title else { return false } + + switch queries["tab"] { + case Tab.stamp.rawValue: + KeyChain.shared.addItem(key: currentStampName, pwd: "true") ? print("Adding Stamp History KeyChain is Success") : print("Adding Stamp History is Fail") + self.isStampExist = true + currentTab = .stamp + case Tab.event.rawValue: + currentTab = .event + default: + return false + } + + return true + } + + private func fetchCurrentStamp() { + guard + let url = URL(string: "https://raw.githubusercontent.com/Async-Swift/jsonstorage/main/stamp.json") + else { return } + + + let request = URLRequest(url: url) + let dataTask = URLSession.shared.dataTask(with: request) { data, response, _ in + guard + let response = response as? HTTPURLResponse, + response.statusCode == 200, + let data = data + else { return } + + DispatchQueue.main.async { [weak self] in + guard let self = self else {return } + do { + let stamp = try JSONDecoder().decode(Stamp.self, from: data) + self.currentStamp = stamp + } catch { + self.currentStamp = nil + } + } + } + dataTask.resume() + } } @@ -39,4 +101,4 @@ enum Tab: String, CaseIterable { case .stamp: StampView() } } - } +} diff --git a/AsyncSwift/Observed/KeyChain.swift b/AsyncSwift/Observed/KeyChain.swift new file mode 100644 index 0000000..704dcfc --- /dev/null +++ b/AsyncSwift/Observed/KeyChain.swift @@ -0,0 +1,76 @@ +// +// KeyChain.swift +// AsyncSwift +// +// Created by Inho Choi on 2022/09/15. +// + +import Foundation +import UIKit + +final class KeyChain { + static let shared = KeyChain() + + private init() { } + + func addItem(key: Any, pwd: Any) -> Bool { + let addQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key, + kSecValueData: (pwd as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any] + + let status = SecItemAdd(addQuery as CFDictionary, nil) + + switch status { + case errSecSuccess: + return true + case errSecDuplicateItem: + return updateItem(value: pwd, key: key) + default: + print("addItem Error : \(status.description))") + return false + } + } + + func getItem(key: Any) -> Any? { + let getQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key, + kSecReturnAttributes: true, + kSecReturnData: true] + var item: CFTypeRef? + let result = SecItemCopyMatching(getQuery as CFDictionary, &item) + + if result == errSecSuccess, + let existingItem = item as? [String: Any], + let data = existingItem[kSecValueData as String] as? Data, + let password = String(data: data, encoding: .utf8) { + return password + } + print("getItem Error : \(result.description)") + return nil + } + + func updateItem(value: Any, key: Any) -> Bool { + let prevQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key] + let updateQuery: [CFString: Any] = [kSecValueData: (value as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any] + + let result: Bool = { + let status = SecItemUpdate(prevQuery as CFDictionary, updateQuery as CFDictionary) + return status == errSecSuccess + }() + + return result + } + + func deleteItem(key: String) -> Bool { + let deleteQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key] + let status = SecItemDelete(deleteQuery as CFDictionary) + if status == errSecSuccess { + return true + } else { + print("deleteItem Error : \(status.description)") + return false + } + } +} diff --git a/AsyncSwift/Views/StampView.swift b/AsyncSwift/Views/StampView.swift index 52621ba..8698985 100644 --- a/AsyncSwift/Views/StampView.swift +++ b/AsyncSwift/Views/StampView.swift @@ -14,7 +14,7 @@ struct StampView: View { var body: some View { NavigationView { Group { - if appData.scannedSeminarQR { + if appData.isStampExist { ZStack { stampBack stampFront