diff --git a/.gitignore b/.gitignore index d5eaced..da50a18 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,58 @@ Dependencies/ # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ + +# Xcode & macOS + +# OS X temporary files that should never be committed +.DS_Store +*.swp +*.lock +profile + +# Xcode temporary files that should never be committed +*~.nib + +# Xcode build files +DerivedData/ +build/ +.build/ + +# Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) +*.pbxuser +*.mode1v3 +*.mode2v3 +.perspectivev3 +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 +xcuserdata +xcuserdata/**/ + +# Cocoapods: cocoapods.org +Pods/ +Podfile.lock + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +Packages/ +Package.pins +Package.resolved + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/.png +fastlane/test_output \ No newline at end of file diff --git a/sdk/.gitignore b/sdk/.gitignore new file mode 100644 index 0000000..95c4320 --- /dev/null +++ b/sdk/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/sdk/Package.swift b/sdk/Package.swift new file mode 100644 index 0000000..eca6fe6 --- /dev/null +++ b/sdk/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Sdk", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Sdk", + targets: ["Sdk"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Sdk", + dependencies: []), + .testTarget( + name: "SdkTests", + dependencies: ["Sdk"]), + ] +) diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..6f1a889 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,3 @@ +# Sdk + +A description of this package. diff --git a/sdk/Sources/Sdk/APIError.swift b/sdk/Sources/Sdk/APIError.swift new file mode 100644 index 0000000..ae237de --- /dev/null +++ b/sdk/Sources/Sdk/APIError.swift @@ -0,0 +1,27 @@ +// +// File.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 15/02/2021. +// + +import Foundation + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +public extension Sdk { + enum APIError: Error { + case error(reason: String) + case jsonSerialization + case noEndpointURL + case noFirebaseToken + case noCertificate + case noPrivateKey + case noSignatureValues + case urlError + + case wrongToken + case wrongSymbol + case wrongPin + } +} + diff --git a/sdk/Sources/Sdk/Extensions/Date.swift b/sdk/Sources/Sdk/Extensions/Date.swift new file mode 100644 index 0000000..1e04b76 --- /dev/null +++ b/sdk/Sources/Sdk/Extensions/Date.swift @@ -0,0 +1,21 @@ +// +// File.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 15/02/2021. +// + +import Foundation + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +extension Date { + func formattedString(_ format: String) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + return formatter.string(from: self) + } + + var millisecondsSince1970: Int64 { + Int64((self.timeIntervalSince1970 * 1000.0).rounded()) + } +} diff --git a/sdk/Sources/Sdk/Extensions/URLRequest.swift b/sdk/Sources/Sdk/Extensions/URLRequest.swift new file mode 100644 index 0000000..5835f63 --- /dev/null +++ b/sdk/Sources/Sdk/Extensions/URLRequest.swift @@ -0,0 +1,70 @@ +// +// File.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 15/02/2021. +// + +import Foundation + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +extension URLRequest { + func signed(with certificate: X509) throws -> URLRequest { + // Create request + var request = self + + // Signing stuff + guard let urlString = request.url?.absoluteString else { + throw Sdk.APIError.urlError + } + + // Get private key + guard let privateKeyRawData = certificate.getPrivateKeyData(format: .DER), + let privateKeyString = String(data: privateKeyRawData, encoding: .utf8)? + .split(separator: "\n") + .dropFirst() + .dropLast() + .joined() + .data(using: .utf8) else { + throw Sdk.APIError.noPrivateKey + } + + // Create SecKey + let attributes = [ + kSecAttrKeyType: kSecAttrKeyTypeRSA, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + ] + guard let privateKeyData = Data(base64Encoded: privateKeyString), + let secKey = SecKeyCreateWithData(privateKeyData as NSData, attributes as NSDictionary, nil) else { + throw Sdk.APIError.noPrivateKey + } + + // Get fingerprint + guard let signatureValues = Sdk.Signer.getSignatureValues(body: request.httpBody, url: urlString, privateKey: secKey, fingerprint: certificate.getCertificateFingerprint().lowercased()) else { + throw Sdk.APIError.noSignatureValues + } + + let now = Date() + var vDate: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + return "\(dateFormatter.string(from: now)) GMT" + } + + // Headers + request.setValue("iOS", forHTTPHeaderField: "vOS") + request.setValue("1", forHTTPHeaderField: "vAPI") + request.setValue(vDate, forHTTPHeaderField: "vDate") + request.setValue(signatureValues.canonicalURL, forHTTPHeaderField: "vCanonicalUrl") + request.setValue(signatureValues.signature, forHTTPHeaderField: "Signature") + + if let digest = signatureValues.digest { + request.setValue("SHA-256=\(digest)", forHTTPHeaderField: "Digest") + } + + return request + } +} diff --git a/sdk/Sources/Sdk/Extensions/getSignatures.swift b/sdk/Sources/Sdk/Extensions/getSignatures.swift new file mode 100644 index 0000000..1eb5a7e --- /dev/null +++ b/sdk/Sources/Sdk/Extensions/getSignatures.swift @@ -0,0 +1,50 @@ +// +// getSignatures.swift +// +// +// Created by Tomasz on 02/03/2021. +// + +import Foundation +import KeychainAccess + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +func getSignatures(request: URLRequest, certificate: X509) -> String { + guard let urlString = request.url?.absoluteString else { + return "\(Sdk.APIError.urlError)" + } + + // Get private key + guard let privateKeyRawData = certificate.getPrivateKeyData(format: .DER), + let privateKeyString = String(data: privateKeyRawData, encoding: .utf8)? + .split(separator: "\n") + .dropFirst() + .dropLast() + .joined() + .data(using: .utf8) else { + return "\(Sdk.APIError.noPrivateKey)" + } + + // Create SecKey + let attributes = [ + kSecAttrKeyType: kSecAttrKeyTypeRSA, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + ] + guard let privateKeyData = Data(base64Encoded: privateKeyString), + let secKey = SecKeyCreateWithData(privateKeyData as NSData, attributes as NSDictionary, nil) else { + return "\(Sdk.APIError.noPrivateKey)" + } + + // Get fingerprint + guard let signatureValues = Sdk.Signer.getSignatureValues(body: request.httpBody, url: urlString, privateKey: secKey, fingerprint: certificate.getCertificateFingerprint().lowercased()) else { + return "\(Sdk.APIError.noPrivateKey)" + } + + // Headers + let keychain = Keychain() + let fingerprint: String! = keychain["keyFingerprint"] + + let signature = "\(signatureValues.signature.replacingOccurrences(of: "nil", with: fingerprint))" + + return signature +} diff --git a/sdk/Sources/Sdk/Sdk.swift b/sdk/Sources/Sdk/Sdk.swift new file mode 100644 index 0000000..2065973 --- /dev/null +++ b/sdk/Sources/Sdk/Sdk.swift @@ -0,0 +1,293 @@ +// +// Sdk.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021. +// + + +import Foundation +import Combine +import os +import KeychainAccess + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +public class Sdk { + static private let libraryVersion: String = "0.0.1" + + private let loggerSubsystem: String = "com.wulkanowy-ios.Sdk" + private var cancellables: Set = [] + + var firebaseToken: String! + var endpointURL: String! + + public let certificate: X509 + + // MARK: - Init + public init(certificate: X509) { + self.certificate = certificate + } + + // MARK: - Public functions + + /// Logs in with supplied login data. + /// - Parameters: + /// - token: Login token + /// - symbol: Login symbol + /// - pin: Login PIN + /// - deviceName: Name of the device + /// - completionHandler: Callback + public func login(token: String, symbol: String, pin: String, deviceModel: String, completionHandler: @escaping (Error?) -> Void) { + let logger: Logger = Logger(subsystem: self.loggerSubsystem, category: "Login") + logger.debug("Logging in...") + + let endpointPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "http://komponenty.vulcan.net.pl/UonetPlusMobile/RoutingRules.txt")!) + .mapError { $0 as Error } + + // Firebase request + var firebaseRequest: URLRequest = URLRequest(url: URL(string: "https://android.googleapis.com/checkin")!) + firebaseRequest.httpMethod = "POST" + firebaseRequest.setValue("application/json", forHTTPHeaderField: "Content-type") + firebaseRequest.setValue("gzip", forHTTPHeaderField: "Accept-Encoding") + + let firebaseRequestBody: [String: Any] = [ + "locale": "pl_PL", + "digest": "", + "checkin": [ + "iosbuild": [ + "model": deviceModel, + "os_version": Self.libraryVersion + ], + "last_checkin_msec": 0, + "user_number": 0, + "type": 2 + ], + "time_zone": TimeZone.current.identifier, + "user_serial_number": 0, + "id": 0, + "logging_id": 0, + "version": 2, + "security_token": 0, + "fragment": 0 + ] + firebaseRequest.httpBody = try? JSONSerialization.data(withJSONObject: firebaseRequestBody) + + let firebasePublisher = URLSession.shared.dataTaskPublisher(for: firebaseRequest) + .receive(on: DispatchQueue.global(qos: .background)) + .tryCompactMap { value -> AnyPublisher in + guard let dictionary: [String: Any] = try? JSONSerialization.jsonObject(with: value.data) as? [String: Any] else { + throw APIError.jsonSerialization + } + + var request: URLRequest = URLRequest(url: URL(string: "https://fcmtoken.googleapis.com/register")!) + request.httpMethod = "POST" + request.setValue("AidLogin \(dictionary["android_id"] as? Int ?? 0):\(dictionary["security_token"] as? Int ?? 0)", forHTTPHeaderField: "Authorization") + request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding") + + let body: String = "device=\(dictionary["android_id"] as? Int ?? 0)&app=pl.edu.vulcan.hebe&sender=987828170337&X-subtype=987828170337&appid=dLIDwhjvE58&gmp_app_id=1:987828170337:ios:6b65a4ad435fba7f" + request.httpBody = body.data(using: .utf8) + + return URLSession.shared.dataTaskPublisher(for: request) + .receive(on: DispatchQueue.global(qos: .background)) + .mapError { $0 as Error } + .map { $0.data } + .eraseToAnyPublisher() + } + .flatMap { $0 } + + Publishers.Zip(endpointPublisher, firebasePublisher) + .tryMap { (endpoints, firebaseToken) -> (String, String) in + // Find endpointURL + let lines = String(data: endpoints.data, encoding: .utf8)?.split { $0.isNewline } + + var endpointURL: String? + lines?.forEach { line in + let items = line.split(separator: ",") + if (token.starts(with: items[0])) { + endpointURL = String(items[1]) + return + } + } + + guard let finalEndpointURL: String = endpointURL else { + throw APIError.noEndpointURL + } + + // Get Firebase token + guard let token: String = String(data: firebaseToken, encoding: .utf8)?.components(separatedBy: "token=").last else { + logger.error("Token empty! Response: \"\(firebaseToken.base64EncodedString(), privacy: .private)\"") + throw APIError.noFirebaseToken + } + + return (finalEndpointURL, token) + } + .tryMap { endpointURL, firebaseToken in + try self.registerDevice(endpointURL: endpointURL, firebaseToken: firebaseToken, token: token, symbol: symbol, pin: pin, deviceModel: deviceModel) + .mapError { $0 as Error } + .map { $0.data } + .eraseToAnyPublisher() + } + .flatMap { $0 } + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure(let error): + completionHandler(error) + } + }, receiveValue: { data in + if let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let parsedError = self.parseResponse(response) { + completionHandler(parsedError) + } else { + self.getStudents(symbol: symbol, deviceModel: deviceModel) + completionHandler(nil) + } + }) + .store(in: &cancellables) + } + + // MARK: - Private functions + + /// Registers the device + /// - Parameters: + /// - endpointURL: API endpoint URL + /// - firebaseToken: FCM token + /// - token: Vulcan token + /// - symbol: Vulcan symbol + /// - pin: Vulcan PIN + /// - deviceModel: Device model + /// - Throws: Error + /// - Returns: URLSession.DataTaskPublisher + private func registerDevice(endpointURL: String, firebaseToken: String, token: String, symbol: String, pin: String, deviceModel: String) throws -> URLSession.DataTaskPublisher { + self.endpointURL = endpointURL + self.firebaseToken = firebaseToken + guard let keyFingerprint = certificate.getPrivateKeyFingerprint(format: .PEM)?.replacingOccurrences(of: ":", with: "").lowercased(), + let keyData = certificate.getPublicKeyData(), + let keyBase64 = String(data: keyData, encoding: .utf8)? + .split(separator: "\n") // Split by newline + .dropFirst() // Drop prefix + .dropLast() // Drop suffix + .joined() // Join + else { + throw APIError.noCertificate + } + + // Request + let url = "\(endpointURL)/\(symbol)/api/mobile/register/new" + var request = URLRequest(url: URL(string: url)!) + request.httpMethod = "POST" + + let now: Date = Date() + var timestampFormatted: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + + return dateFormatter.string(from: now) + } + + let keychain = Keychain() + keychain[string: "keyFingerprint"] = keyFingerprint + + // Body + let body: [String: Encodable?] = [ + "AppName": "DzienniczekPlus 2.0", + "AppVersion": Self.libraryVersion, + "CertificateId": nil, + "Envelope": [ + "OS": "iOS", + "PIN": pin, + "Certificate": keyBase64, + "CertificateType": "RSA_PEM", + "DeviceModel": deviceModel, + "SecurityToken": token, + "SelfIdentifier": UUID().uuidString.lowercased(), + "CertificateThumbprint": keyFingerprint + ], + "FirebaseToken": firebaseToken, + "API": 1, + "RequestId": UUID().uuidString.lowercased(), + "Timestamp": now.millisecondsSince1970, + "TimestampFormatted": "\(timestampFormatted) GMT" + ] + + request.httpBody = try? JSONSerialization.data(withJSONObject: body) + + request.allHTTPHeaderFields = [ + "Content-Type": "application/json", + "Accept-Encoding": "gzip", + "vDeviceModel": deviceModel + ] + + let signedRequest = try request.signed(with: certificate) + + return URLSession.shared.dataTaskPublisher(for: signedRequest) + } + + private func getStudents(symbol: String, deviceModel: String) { + let url = "\(self.endpointURL!)/\(symbol)/api/mobile/register/hebe" + var request = URLRequest(url: URL(string: url)!) + request.httpMethod = "GET" + + let now = Date() + var vDate: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + return "\(dateFormatter.string(from: now)) GMT" + } + + let signatures = getSignatures(request: request, certificate: certificate) + request.setValue("\(signatures)", forHTTPHeaderField: "Signature") + + request.allHTTPHeaderFields = [ + "User-Agent": "wulkanowy/1 CFNetwork/1220.1 Darwin/20.1.0", + "vOS": "iOS", + "vDeviceModel": deviceModel, + "vAPI": "1", + "vDate": vDate, + "vCanonicalUrl": "api%2fmobile%2fregister%2fhebe" + ] + + let session = URLSession.shared + let task = session.dataTask(with: request) { (data, response, error) in + if let error = error { + // Handle HTTP request error + print(error) + } else if let data = data { + // Handle HTTP request response + print(String(data: data, encoding: String.Encoding.utf8) as Any) + } else { + // Handle unexpected error + } + } + + task.resume() + + } + + // MARK: - Helper functions + + /// Parses the response + /// - Parameter response: Request response + /// - Returns: VulcanKit.APIError? + private func parseResponse(_ response: [String: Any]) -> APIError? { + guard let status = response["Status"] as? [String: Any], + let statusCode = status["Code"] as? Int else { + return nil + } + + print("Response status code: \(statusCode)") + + switch statusCode { + case 0: return nil + case 200: return APIError.wrongToken + case -1: return APIError.wrongSymbol //Ya, Vulcan returns -1 code + case 203: return APIError.wrongPin + default: return nil + } + } +} diff --git a/sdk/Sources/Sdk/X509.swift b/sdk/Sources/Sdk/X509.swift new file mode 100644 index 0000000..a74bf2c --- /dev/null +++ b/sdk/Sources/Sdk/X509.swift @@ -0,0 +1,192 @@ +// +// File.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021. +// + +import Foundation +import CryptoKit +import OpenSSL + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +public class X509: ObservableObject { + enum X509Error: Error { + case errorGeneratingPKEY + } + + public enum KeyFormat { + case PEM + case DER + } + + let certificate: OpaquePointer + let pkey: OpaquePointer + + public init(serialNumber: Int, certificateEntries: [String: String]) throws { + let x509: OpaquePointer = X509_new() + + // serial number + ASN1_INTEGER_set(X509_get_serialNumber(x509), serialNumber) + + // version + X509_set_version(x509, 0x2) // v3 + + // validity date + X509_gmtime_adj(X509_getm_notBefore(x509), 0) + X509_gmtime_adj(X509_getm_notAfter(x509), 60 * 60 * 24 * 365 * 10) // 60 seconds * 60 minutes * 24 hours * 365 days * 10 years + + // key + guard let pkey = EVP_PKEY_new() else { + throw X509Error.errorGeneratingPKEY + } + + let exponent = BN_new() + BN_set_word(exponent, 0x10001) + + let rsa = RSA_new() + RSA_generate_key_ex(rsa, 2048, exponent, nil) + EVP_PKEY_set1_RSA(pkey, rsa) + + X509_set_pubkey(x509, pkey) + self.pkey = pkey + + // issuer + let subjectName = X509_get_subject_name(x509) + for (key, value) in certificateEntries { + X509_NAME_add_entry_by_txt(subjectName, key, MBSTRING_ASC, value, -1, -1, 0) + } + + X509_set_issuer_name(x509, subjectName) + + // sign the certificate + X509_sign(x509, pkey, EVP_sha256()) + + self.certificate = x509 + } + + /// Gets the private key used to sign the certificate data. + /// - Parameter format: Format of the returned key + /// - Returns: Private key data + public func getPrivateKeyData(format: KeyFormat) -> Data? { + let bio = BIO_new(BIO_s_mem()) + + switch format { + case .PEM: PEM_write_bio_PrivateKey(bio, self.pkey, nil, nil, 0, nil, nil) + case .DER: PEM_write_bio_PrivateKey_traditional(bio, self.pkey, nil, nil, 0, nil, nil) + } + + var pointer: UnsafeMutableRawPointer? + let len = BIO_ctrl(bio, BIO_CTRL_INFO, 0, &pointer) + + guard let nonEmptyPointer = pointer else { + return nil + } + + let data = Data(bytes: nonEmptyPointer, count: len) + BIO_vfree(bio) + + return data + } + + /// Gets the public key used to sign the certificate data. + /// - Returns: Public key data + public func getPublicKeyData() -> Data? { + let bio = BIO_new(BIO_s_mem()) + PEM_write_bio_PUBKEY(bio, self.pkey) + + var pointer: UnsafeMutableRawPointer? + let len = BIO_ctrl(bio, BIO_CTRL_INFO, 0, &pointer) + + guard let nonEmptyPointer = pointer else { + return nil + } + + let data = Data(bytes: nonEmptyPointer, count: len) + BIO_vfree(bio) + + return data + } + + /// Gets the generated certificate data. + /// - Returns: Certificate data + public func getCertificateData() -> Data? { + let bio = BIO_new(BIO_s_mem()) + PEM_write_bio_X509(bio, self.certificate) + + var pointer: UnsafeMutableRawPointer? + let len = BIO_ctrl(bio, BIO_CTRL_INFO, 0, &pointer) + + guard let nonEmptyPointer = pointer else { + return nil + } + + let data = Data(bytes: nonEmptyPointer, count: len) + BIO_vfree(bio) + + return data + } + + /// Get certificate thumbrint. + /// - Returns: Certificate fingerprint + public func getCertificateFingerprint() -> String { + let md: UnsafeMutablePointer = .allocate(capacity: Int(EVP_MAX_MD_SIZE)) + var n: UInt32 = 0 + + X509_digest(self.certificate, EVP_sha1(), md, &n) + return UnsafeMutableBufferPointer(start: md, count: Int(EVP_MAX_MD_SIZE)) + .prefix(Int(n)) + .makeIterator() + .map { + let string = String($0, radix: 16) + return ($0 < 16 ? "0" + string : string) + } + .joined(separator: ":") + .uppercased() + } + + /// Get public key fingerprint + /// - Returns: Public key fingerprint + public func getPublicKeyFingerprint() -> String? { + guard let keyData = self.getPublicKeyData(), + let rawKeyB64 = String(data: keyData, encoding: .utf8) else { + return nil + } + + let keyB64 = rawKeyB64 + .split(separator: "\n") // Split by newline + .dropFirst() // Drop prefix + .dropLast() // Drop suffix + .joined() // Combine + + guard let data = Data(base64Encoded: keyB64) else { + return nil + } + + let hash = Insecure.MD5.hash(data: data) + return hash.map { String(format: "%02hhx", $0) }.joined() + } + + /// Get private key fingerprint + /// - Parameter format: Format of the returned key + /// - Returns: Private key fingerprint + public func getPrivateKeyFingerprint(format: KeyFormat) -> String? { + guard let keyData = self.getPrivateKeyData(format: format), + let rawKeyB64 = String(data: keyData, encoding: .utf8) else { + return nil + } + + let keyB64 = rawKeyB64 + .split(separator: "\n") // Split by newline + .dropFirst() // Drop prefix + .dropLast() // Drop suffix + .joined() // Combine + + guard let data = Data(base64Encoded: keyB64) else { + return nil + } + + let hash = Insecure.MD5.hash(data: data) + return hash.map { String(format: "%02hhx", $0) }.joined() + } +} diff --git a/sdk/Sources/Sdk/data/luckyNumber.swift b/sdk/Sources/Sdk/data/luckyNumber.swift new file mode 100644 index 0000000..6b7cf5f --- /dev/null +++ b/sdk/Sources/Sdk/data/luckyNumber.swift @@ -0,0 +1,12 @@ +// +// luckyNumber.swift +// +// +// Created by Tomasz on 27/02/2021. +// + +import Foundation + +public func getLuckyNumber() -> Int { + return 7 +} diff --git a/sdk/Sources/Sdk/signer.swift b/sdk/Sources/Sdk/signer.swift new file mode 100644 index 0000000..9d2a49d --- /dev/null +++ b/sdk/Sources/Sdk/signer.swift @@ -0,0 +1,98 @@ +// +// File.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021. +// + +import Foundation +import CryptoKit +import KeychainAccess + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +public extension Sdk { + struct Signer { + static public func getSignatureValues(body: Data?, url: String, date: Date = Date(), privateKey: SecKey, fingerprint: String) -> (digest: String?, canonicalURL: String, signature: String)? { + // Canonical URL + guard let canonicalURL = getCanonicalURL(url) else { + return nil + } + + // Digest + let digest: String? + if let body = body { + digest = Data(SHA256.hash(data: body)).base64EncodedString() + } else { + digest = nil + } + + // Headers & values + let headersList = getHeadersList(digest: digest, canonicalURL: canonicalURL, date: date) + + // Signature value + guard let data = headersList.values.data(using: .utf8) else { + return nil + } + + let signatureData = SecKeyCreateSignature(privateKey, .rsaSignatureMessagePKCS1v15SHA256, data as CFData, nil) as Data? + guard let signatureValue = signatureData?.base64EncodedString() else { + return nil + } + + return ( + digest, + canonicalURL, + "keyId=\"\(fingerprint.replacingOccurrences(of: ":", with: ""))\",headers=\"\(headersList.headers)\",algorithm=\"sha256withrsa\",signature=Base64(SHA256withRSA(\(signatureValue)))" + ) + } + + // MARK: - Private functions + + /// Finds and encodes the first canonical URL match in the supplied URL. + /// - Parameter url: URL to find matches in + /// - Returns: Canonical URL + static internal func getCanonicalURL(_ url: String) -> String? { + guard let regex = try? NSRegularExpression(pattern: "(api/mobile/.+)") else { + return nil + } + + let results = regex.matches(in: url, range: NSRange(url.startIndex..., in: url)) + return results.compactMap { + guard let range = Range($0.range, in: url) else { + return nil + } + + return String(url[range]).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)?.lowercased() + } + .first + } + + /// Creates a tuple with formatted headers and values needed to sign the request. + /// - Parameters: + /// - body: Body of the request + /// - digest: Digest of the request + /// - canonicalURL: Canonical URL of the request + /// - date: Date of the request + /// - Returns: Formatted headers and values + static internal func getHeadersList(digest: String?, canonicalURL: String, date: Date) -> (headers: String, values: String) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + let dateString = "\(dateFormatter.string(from: date)) GMT" + + let signData: [(key: String, value: String)] = [ + ("vCanonicalUrl", canonicalURL), + digest == nil ? nil : ("Digest", digest ?? ""), + ("vDate", "\(dateString)") + ] + .compactMap { $0 } + + let headers = signData.map(\.key).joined(separator: " ") + let values = signData.map(\.value).joined() + + return (headers, values) + } + } +} diff --git a/sdk/Tests/LinuxMain.swift b/sdk/Tests/LinuxMain.swift new file mode 100644 index 0000000..b222f07 --- /dev/null +++ b/sdk/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import SdkTests + +var tests = [XCTestCaseEntry]() +tests += SdkTests.allTests() +XCTMain(tests) diff --git a/sdk/Tests/SdkTests/SdkTests.swift b/sdk/Tests/SdkTests/SdkTests.swift new file mode 100644 index 0000000..13dfcb2 --- /dev/null +++ b/sdk/Tests/SdkTests/SdkTests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import Sdk + +final class SdkTests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Sdk().text, "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/sdk/Tests/SdkTests/XCTestManifests.swift b/sdk/Tests/SdkTests/XCTestManifests.swift new file mode 100644 index 0000000..e5b28f6 --- /dev/null +++ b/sdk/Tests/SdkTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(SdkTests.allTests), + ] +} +#endif diff --git a/wulkanowy.xcodeproj/project.pbxproj b/wulkanowy.xcodeproj/project.pbxproj index 9c81c40..5e3decc 100644 --- a/wulkanowy.xcodeproj/project.pbxproj +++ b/wulkanowy.xcodeproj/project.pbxproj @@ -3,16 +3,30 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ - 967B5B9825D813F5006ED944 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967B5B9725D813F5006ED944 /* LoginView.swift */; }; - 96A2D96325D6FEA6001CB109 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 96A2D96525D6FEA6001CB109 /* Localizable.strings */; }; - 96A2D97B25D7003F001CB109 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A2D97A25D7003F001CB109 /* OnboardingView.swift */; }; - 96A2D98825D73DCF001CB109 /* CustomButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A2D98725D73DCF001CB109 /* CustomButtonView.swift */; }; - 96A5571725D81BD20094BF48 /* CustomTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A5571625D81BD20094BF48 /* CustomTextFieldView.swift */; }; + 5C1794B425E8FDFB007AD91A /* messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794B325E8FDFB007AD91A /* messages.swift */; }; + 5C1794B825E8FE08007AD91A /* notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794B725E8FE08007AD91A /* notes.swift */; }; + 5C1794BC25E8FE19007AD91A /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794BB25E8FE19007AD91A /* settings.swift */; }; + 5C1794C025E8FE27007AD91A /* about.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794BF25E8FE27007AD91A /* about.swift */; }; + 5C1794CD25E90DBD007AD91A /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 5C1794CC25E90DBD007AD91A /* KeychainAccess */; }; + 5C1CFA7A25EA32AE0047286F /* ghImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1CFA7925EA32AE0047286F /* ghImage.swift */; }; + 5C2D331025E64F3C000253AC /* grades.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D330F25E64F3C000253AC /* grades.swift */; }; + 5C2D331425E650EC000253AC /* exams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D331325E650EC000253AC /* exams.swift */; }; + 5C2D331825E651C4000253AC /* homework.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D331725E651C4000253AC /* homework.swift */; }; + 5C2D331C25E651FB000253AC /* more.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D331B25E651FB000253AC /* more.swift */; }; + 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */; }; + 5C89C8F525EA6AA4000B5816 /* licenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C89C8F425EA6AA4000B5816 /* licenses.swift */; }; + 5C89C90625EA7996000B5816 /* SwiftUIEKtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 5C89C90525EA7996000B5816 /* SwiftUIEKtensions */; }; + 5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */ = {isa = PBXBuildFile; productRef = 5C9B6F4825D6C08D00C3F5F5 /* Sdk */; }; + 5CC2EAA525E516F100B6183E /* dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC2EAA425E516F100B6183E /* dashboard.swift */; }; + 5CC2EAAE25E526B500B6183E /* navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC2EAAD25E526B500B6183E /* navigation.swift */; }; + 5CCAE31625DA4CDD00D87580 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 5CCAE31525DA4CDD00D87580 /* OpenSSL */; }; + 5CEA516B25D540B900DB45BD /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CEA516D25D540B900DB45BD /* Localizable.strings */; }; F4C6D9082544E17400F8903A /* wulkanowyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9072544E17400F8903A /* wulkanowyApp.swift */; }; + F4C6D90A2544E17400F8903A /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9092544E17400F8903A /* LoginView.swift */; }; F4C6D90C2544E17500F8903A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C6D90B2544E17500F8903A /* Assets.xcassets */; }; F4C6D90F2544E17500F8903A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C6D90E2544E17500F8903A /* Preview Assets.xcassets */; }; F4C6D91A2544E17500F8903A /* wulkanowyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9192544E17500F8903A /* wulkanowyTests.swift */; }; @@ -36,15 +50,50 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 5C1F6D5F25D6891300AFDDD6 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 5C9B6EEC25D6B25200C3F5F5 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ - 967B5B9725D813F5006ED944 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; - 96A2D96425D6FEA6001CB109 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 96A2D96925D6FEBB001CB109 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; - 96A2D97A25D7003F001CB109 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; - 96A2D98725D73DCF001CB109 /* CustomButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButtonView.swift; sourceTree = ""; }; - 96A5571625D81BD20094BF48 /* CustomTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextFieldView.swift; sourceTree = ""; }; + 5C1794B325E8FDFB007AD91A /* messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = messages.swift; sourceTree = ""; }; + 5C1794B725E8FE08007AD91A /* notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notes.swift; sourceTree = ""; }; + 5C1794BB25E8FE19007AD91A /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; }; + 5C1794BF25E8FE27007AD91A /* about.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = about.swift; sourceTree = ""; }; + 5C1CFA7925EA32AE0047286F /* ghImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ghImage.swift; sourceTree = ""; }; + 5C2D330F25E64F3C000253AC /* grades.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = grades.swift; sourceTree = ""; }; + 5C2D331325E650EC000253AC /* exams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = exams.swift; sourceTree = ""; }; + 5C2D331725E651C4000253AC /* homework.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = homework.swift; sourceTree = ""; }; + 5C2D331B25E651FB000253AC /* more.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = more.swift; sourceTree = ""; }; + 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VulcanStore.swift; sourceTree = ""; }; + 5C89C8F425EA6AA4000B5816 /* licenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = licenses.swift; sourceTree = ""; }; + 5C9B6E4925D6ADFB00C3F5F5 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; + 5C9B6F4525D6C06D00C3F5F5 /* Sdk */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Sdk; sourceTree = ""; }; + 5CC2EAA425E516F100B6183E /* dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dashboard.swift; sourceTree = ""; }; + 5CC2EAAD25E526B500B6183E /* navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = navigation.swift; sourceTree = ""; }; + 5CEA516C25D540B900DB45BD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 5CF81BD725D9D44400B12C4C /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = ""; }; F4C6D9042544E17400F8903A /* wulkanowy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = wulkanowy.app; sourceTree = BUILT_PRODUCTS_DIR; }; F4C6D9072544E17400F8903A /* wulkanowyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wulkanowyApp.swift; sourceTree = ""; }; + F4C6D9092544E17400F8903A /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; F4C6D90B2544E17500F8903A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F4C6D90E2544E17500F8903A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; F4C6D9102544E17500F8903A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -61,6 +110,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */, + 5CCAE31625DA4CDD00D87580 /* OpenSSL in Frameworks */, + 5C1794CD25E90DBD007AD91A /* KeychainAccess in Frameworks */, + 5C89C90625EA7996000B5816 /* SwiftUIEKtensions in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -81,16 +134,7 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 967B5B9625D813E2006ED944 /* Login */ = { - isa = PBXGroup; - children = ( - 967B5B9725D813F5006ED944 /* LoginView.swift */, - 96A5571625D81BD20094BF48 /* CustomTextFieldView.swift */, - ); - path = Login; - sourceTree = ""; - }; - 96A2D95625D6FE4D001CB109 /* App */ = { + 5C1B848625E1B6740074F29D /* App */ = { isa = PBXGroup; children = ( F4C6D9072544E17400F8903A /* wulkanowyApp.swift */, @@ -98,46 +142,74 @@ path = App; sourceTree = ""; }; - 96A2D95A25D6FE81001CB109 /* Resources */ = { + 5C1B848925E1B6910074F29D /* Views */ = { isa = PBXGroup; children = ( - 96A2D96525D6FEA6001CB109 /* Localizable.strings */, - ); - path = Resources; - sourceTree = ""; - }; - 96A2D96D25D6FF29001CB109 /* Views */ = { - isa = PBXGroup; - children = ( - 96A5571B25D859460094BF48 /* Shared */, - 967B5B9625D813E2006ED944 /* Login */, - 96A2D97925D7002D001CB109 /* Onboarding */, + 5CC2EAAC25E5269E00B6183E /* Navigation */, + 5CC2EAA325E516DD00B6183E /* Content */, + 5C1B849F25E1B7A30074F29D /* Login */, + 5C1CFA7925EA32AE0047286F /* ghImage.swift */, ); path = Views; sourceTree = ""; }; - 96A2D97925D7002D001CB109 /* Onboarding */ = { + 5C1B848E25E1B6FA0074F29D /* Resources */ = { isa = PBXGroup; children = ( - 96A2D97A25D7003F001CB109 /* OnboardingView.swift */, + 5CEA516D25D540B900DB45BD /* Localizable.strings */, ); - path = Onboarding; + path = Resources; sourceTree = ""; }; - 96A5571B25D859460094BF48 /* Shared */ = { + 5C1B849F25E1B7A30074F29D /* Login */ = { isa = PBXGroup; children = ( - 96A2D98725D73DCF001CB109 /* CustomButtonView.swift */, + F4C6D9092544E17400F8903A /* LoginView.swift */, ); - path = Shared; + path = Login; + sourceTree = ""; + }; + 5C9B6E4825D6ADFB00C3F5F5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5C9B6E4925D6ADFB00C3F5F5 /* NetworkExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5CC2EAA325E516DD00B6183E /* Content */ = { + isa = PBXGroup; + children = ( + 5CC2EAA425E516F100B6183E /* dashboard.swift */, + 5C2D330F25E64F3C000253AC /* grades.swift */, + 5C2D331325E650EC000253AC /* exams.swift */, + 5C2D331725E651C4000253AC /* homework.swift */, + 5C2D331B25E651FB000253AC /* more.swift */, + 5C1794B325E8FDFB007AD91A /* messages.swift */, + 5C1794B725E8FE08007AD91A /* notes.swift */, + 5C1794BB25E8FE19007AD91A /* settings.swift */, + 5C1794BF25E8FE27007AD91A /* about.swift */, + 5C89C8F425EA6AA4000B5816 /* licenses.swift */, + ); + path = Content; + sourceTree = ""; + }; + 5CC2EAAC25E5269E00B6183E /* Navigation */ = { + isa = PBXGroup; + children = ( + 5CC2EAAD25E526B500B6183E /* navigation.swift */, + ); + path = Navigation; sourceTree = ""; }; F4C6D8FB2544E17300F8903A = { isa = PBXGroup; children = ( + 5C9B6F4525D6C06D00C3F5F5 /* Sdk */, F4C6D9062544E17400F8903A /* wulkanowy */, F4C6D9182544E17500F8903A /* wulkanowyTests */, F4C6D9232544E17500F8903A /* wulkanowyUITests */, + 5C9B6E4825D6ADFB00C3F5F5 /* Frameworks */, F4C6D9052544E17400F8903A /* Products */, ); sourceTree = ""; @@ -155,12 +227,13 @@ F4C6D9062544E17400F8903A /* wulkanowy */ = { isa = PBXGroup; children = ( - 96A2D96D25D6FF29001CB109 /* Views */, - 96A2D95A25D6FE81001CB109 /* Resources */, - 96A2D95625D6FE4D001CB109 /* App */, + 5C1B848E25E1B6FA0074F29D /* Resources */, + 5C1B848925E1B6910074F29D /* Views */, + 5C1B848625E1B6740074F29D /* App */, F4C6D90B2544E17500F8903A /* Assets.xcassets */, F4C6D9102544E17500F8903A /* Info.plist */, F4C6D90D2544E17500F8903A /* Preview Content */, + 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */, ); path = wulkanowy; sourceTree = ""; @@ -201,12 +274,20 @@ F4C6D9002544E17300F8903A /* Sources */, F4C6D9012544E17300F8903A /* Frameworks */, F4C6D9022544E17300F8903A /* Resources */, + 5C1F6D5F25D6891300AFDDD6 /* Embed App Extensions */, + 5C9B6EEC25D6B25200C3F5F5 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = wulkanowy; + packageProductDependencies = ( + 5C9B6F4825D6C08D00C3F5F5 /* Sdk */, + 5CCAE31525DA4CDD00D87580 /* OpenSSL */, + 5C1794CC25E90DBD007AD91A /* KeychainAccess */, + 5C89C90525EA7996000B5816 /* SwiftUIEKtensions */, + ); productName = wulkanowy; productReference = F4C6D9042544E17400F8903A /* wulkanowy.app */; productType = "com.apple.product-type.application"; @@ -253,7 +334,7 @@ F4C6D8FC2544E17300F8903A /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1210; + LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1210; TargetAttributes = { F4C6D9032544E17300F8903A = { @@ -276,9 +357,14 @@ knownRegions = ( en, Base, - pl, + "pl-PL", ); mainGroup = F4C6D8FB2544E17300F8903A; + packageReferences = ( + 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */, + 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */, + 5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */, + ); productRefGroup = F4C6D9052544E17400F8903A /* Products */; projectDirPath = ""; projectRoot = ""; @@ -296,7 +382,7 @@ buildActionMask = 2147483647; files = ( F4C6D90F2544E17500F8903A /* Preview Assets.xcassets in Resources */, - 96A2D96325D6FEA6001CB109 /* Localizable.strings in Resources */, + 5CEA516B25D540B900DB45BD /* Localizable.strings in Resources */, F4C6D90C2544E17500F8903A /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -362,11 +448,11 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 96A2D96525D6FEA6001CB109 /* Localizable.strings */ = { + 5CEA516D25D540B900DB45BD /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( - 96A2D96425D6FEA6001CB109 /* en */, - 96A2D96925D6FEBB001CB109 /* pl */, + 5CEA516C25D540B900DB45BD /* en */, + 5CF81BD725D9D44400B12C4C /* pl-PL */, ); name = Localizable.strings; sourceTree = ""; @@ -426,7 +512,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -482,7 +568,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -499,10 +585,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"wulkanowy/Preview Content\""; - DEVELOPMENT_TEAM = GTH776WFJL; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = wulkanowy/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -521,10 +607,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"wulkanowy/Preview Content\""; - DEVELOPMENT_TEAM = GTH776WFJL; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = wulkanowy/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -656,6 +742,55 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.2.1; + }; + }; + 5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/EnesKaraosman/SwiftUIEKtensions"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.8; + }; + }; + 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/krzyzanowskim/OpenSSL"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.180; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 5C1794CC25E90DBD007AD91A /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; + 5C89C90525EA7996000B5816 /* SwiftUIEKtensions */ = { + isa = XCSwiftPackageProductDependency; + package = 5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */; + productName = SwiftUIEKtensions; + }; + 5C9B6F4825D6C08D00C3F5F5 /* Sdk */ = { + isa = XCSwiftPackageProductDependency; + productName = Sdk; + }; + 5CCAE31525DA4CDD00D87580 /* OpenSSL */ = { + isa = XCSwiftPackageProductDependency; + package = 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */; + productName = OpenSSL; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = F4C6D8FC2544E17300F8903A /* Project object */; } diff --git a/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/wulkanowy.xcworkspace/contents.xcworkspacedata b/wulkanowy.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..9d722e0 --- /dev/null +++ b/wulkanowy.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/wulkanowy.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/wulkanowy.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/wulkanowy.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/wulkanowy/App/wulkanowyApp.swift b/wulkanowy/App/wulkanowyApp.swift index 3b455e6..6f4f4be 100644 --- a/wulkanowy/App/wulkanowyApp.swift +++ b/wulkanowy/App/wulkanowyApp.swift @@ -6,12 +6,14 @@ // import SwiftUI +import Sdk +import Combine @main struct wulkanowyApp: App { var body: some Scene { WindowGroup { - LoginView() + NavigationBarView() } } } diff --git a/wulkanowy/Assets.xcassets/AccentColor.colorset/Contents.json b/wulkanowy/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..970f9de 100644 --- a/wulkanowy/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/wulkanowy/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x09", + "green" : "0x02", + "red" : "0xFF" + } + }, "idiom" : "universal" } ], diff --git a/wulkanowy/Assets.xcassets/AppIcon.appiconset/Contents.json b/wulkanowy/Assets.xcassets/AppIcon.appiconset/Contents.json index e220ac8..935d6f4 100644 --- a/wulkanowy/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/wulkanowy/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -37,7 +37,11 @@ "scale" : "3x" }, { +<<<<<<< HEAD "size" : "40x40", +======= + "filename" : "logo.jpg", +>>>>>>> feature/register-device "idiom" : "iphone", "filename" : "wulkanowy-40@2x.png", "scale" : "2x" diff --git a/wulkanowy/Assets.xcassets/AppIcon.appiconset/logo.jpg b/wulkanowy/Assets.xcassets/AppIcon.appiconset/logo.jpg new file mode 100644 index 0000000..14cb2b7 Binary files /dev/null and b/wulkanowy/Assets.xcassets/AppIcon.appiconset/logo.jpg differ diff --git a/wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json b/wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json new file mode 100644 index 0000000..135c479 --- /dev/null +++ b/wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.184", + "green" : "0.184", + "red" : "0.827" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0xD2" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/wulkanowy/Assets.xcassets/Colours/Contents.json b/wulkanowy/Assets.xcassets/Colours/Contents.json new file mode 100644 index 0000000..7f73912 --- /dev/null +++ b/wulkanowy/Assets.xcassets/Colours/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "compression-type" : "automatic" + } +} diff --git a/wulkanowy/Assets.xcassets/Colours/LaunchColor.colorset/Contents.json b/wulkanowy/Assets.xcassets/Colours/LaunchColor.colorset/Contents.json new file mode 100644 index 0000000..1bdf847 --- /dev/null +++ b/wulkanowy/Assets.xcassets/Colours/LaunchColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0xD2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0xD2" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/wulkanowy/Assets.xcassets/Colours/customControlColor.colorset/Contents.json b/wulkanowy/Assets.xcassets/Colours/customControlColor.colorset/Contents.json new file mode 100644 index 0000000..26ad8d4 --- /dev/null +++ b/wulkanowy/Assets.xcassets/Colours/customControlColor.colorset/Contents.json @@ -0,0 +1,33 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "darkTextColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/wulkanowy/Assets.xcassets/Logo.imageset/Contents.json b/wulkanowy/Assets.xcassets/Logo.imageset/Contents.json new file mode 100644 index 0000000..5f670ca --- /dev/null +++ b/wulkanowy/Assets.xcassets/Logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/wulkanowy/Assets.xcassets/Logo.imageset/logo.png b/wulkanowy/Assets.xcassets/Logo.imageset/logo.png new file mode 100644 index 0000000..291b06f Binary files /dev/null and b/wulkanowy/Assets.xcassets/Logo.imageset/logo.png differ diff --git a/wulkanowy/Assets.xcassets/splash.imageset/Contents.json b/wulkanowy/Assets.xcassets/splash.imageset/Contents.json new file mode 100644 index 0000000..848830a --- /dev/null +++ b/wulkanowy/Assets.xcassets/splash.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "wulkanowy-logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "compression-type" : "automatic" + } +} diff --git a/wulkanowy/Assets.xcassets/splash.imageset/wulkanowy-logo.png b/wulkanowy/Assets.xcassets/splash.imageset/wulkanowy-logo.png new file mode 100644 index 0000000..66012ba Binary files /dev/null and b/wulkanowy/Assets.xcassets/splash.imageset/wulkanowy-logo.png differ diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json new file mode 100644 index 0000000..6b07939 --- /dev/null +++ b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "wulkanowy.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg b/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg new file mode 100644 index 0000000..de66e7a --- /dev/null +++ b/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg @@ -0,0 +1,9 @@ + + + Shape + + + + + + \ No newline at end of file diff --git a/wulkanowy/Info.plist b/wulkanowy/Info.plist index e608f0b..74818e2 100644 --- a/wulkanowy/Info.plist +++ b/wulkanowy/Info.plist @@ -22,6 +22,21 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSExceptionDomains + + komponenty.vulcan.net.pl + + NSIncludesSubdomains + + NSTemporaryExceptionAllowsInsecureHTTPLoads + + NSTemporaryExceptionMinimumTLSVersion + TLSv1.1 + + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -30,7 +45,12 @@ UIApplicationSupportsIndirectInputEvents UILaunchScreen - + + UIColorName + LaunchColor + UIImageName + splash + UIRequiredDeviceCapabilities armv7 @@ -40,6 +60,7 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown UISupportedInterfaceOrientations~ipad diff --git a/wulkanowy/Resources/en.lproj/Localizable.strings b/wulkanowy/Resources/en.lproj/Localizable.strings index 82effe9..ea3da8a 100644 --- a/wulkanowy/Resources/en.lproj/Localizable.strings +++ b/wulkanowy/Resources/en.lproj/Localizable.strings @@ -2,17 +2,46 @@ Localizable.strings wulkanowy - Created by Karol Zientek on 12/02/2021. + Created by Tomasz on 11/02/2021. */ -"title" = "Wulkanowy"; +//LOGIN SCREEN +"loginTitle" = "Log In"; +"token" = "Token"; +"symbol" = "Symbol"; +"pin" = "Pin"; +"deviceName" = "Device name"; +"loginButton" = "Login"; +"wrongToken" = "Wrong token"; +"wrongSymbol" = "Wrong symbol"; +"wrongPin" = "Wrong pin"; +"invalidData" = "Wrong token, symbol or pin"; +"success" = "Success"; -"onboarding.description.title" = "Keep track of your educational progress with Wulkanowy app!"; -"onboarding.description.content" = "Wulkanowy allows you to view your grades, attendance, messages from teachers and many more!"; -"onboarding.continue" = "Continue"; +//NAVIGATION +"dashboardButton" = "Dashboard"; +"gradesButton" = "Grades"; +"examsButton" = "Exams"; +"homeworkButton" = "Homework"; +"moreButton" = "More"; -"login.login" = "Log-in"; -"login.token" = "Token"; -"login.symbol" = "Symbol"; -"login.pin" = "PIN"; +//MORE +"messagesButton" = "Messages"; +"notesButton" = "Notes and achievements"; +"settingsButton" = "Settings"; +"aboutButton" = "About"; + +//ABOUT +"appVersion" = "App version"; +"appVersionContent" = "You actually version is alpha 0.1"; +"contributors" = "Contributors"; +"licensesButton" = "Licenses"; +"privacyPolicy" = "Privacy policy"; +"discordButton" = "Join the Discord serwer"; +"fbButton" = "Facebook fanpage"; +"reportBug" = "Report a bug"; +"homepage" = "Homepage"; + +//LICENCES +"noLicence" = "No licence"; diff --git a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings new file mode 100644 index 0000000..68cee08 --- /dev/null +++ b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings @@ -0,0 +1,47 @@ +/* + Localizable.strings + wulkanowy + + Created by Tomasz on 11/02/2021. + +*/ + +//LOGIN SCREEN +"loginTitle" = "Logowanie"; +"token" = "Token"; +"symbol" = "Symbol"; +"pin" = "Pin"; +"deviceName" = "Nazwa urządzenia"; +"loginButton" = "Zaloguj"; +"wrongToken" = "Zły token"; +"wrongSymbol" = "Zły symbol"; +"wrongPin" = "Zły pin"; +"invalidData" = "Zły token, symbol lub pin"; +"success" = "Sukces"; + +//NAVIGATION +"dashboardButton" = "Start"; +"gradesButton" = "Oceny"; +"examsButton" = "Sprawdziany"; +"homeworkButton" = "Zadania"; +"moreButton" = "Więcej"; + +//MORE +"messagesButton" = "Wiadomości"; +"notesButton" = "Uwagi i osiągnięcia"; +"settingsButton" = "Ustawienia"; +"aboutButton" = "O aplikacji"; + +//ABOUT +"appVersion" = "Wersja aplikacji"; +"appVersionContent" = "Twoja aktualna wersja aplikacji to alpha 0.1"; +"contributors" = "Twórcy"; +"licensesButton" = "Licencje"; +"privacyPolicy" = "Polityka prywatności"; +"discordButton" = "Dołącz do serwera Discord"; +"fbButton" = "Fanpage na Facebooku"; +"reportBug" = "Zgłoś błąd"; +"homepage" = "Strona domowa"; + +//LICENCES +"noLicence" = "Brak licencji"; diff --git a/wulkanowy/Views/Content/about.swift b/wulkanowy/Views/Content/about.swift new file mode 100644 index 0000000..04713c3 --- /dev/null +++ b/wulkanowy/Views/Content/about.swift @@ -0,0 +1,121 @@ +// +// about.swift +// wulkanowy +// +// Created by Tomasz on 26/02/2021. +// + +import Foundation +import SwiftUI +import UIKit +import Combine +import MessageUI +import SwiftUIEKtensions + +struct AboutView: View { + @State private var result: Result? = nil + @State private var isShowingMailView = false + + var body: some View { + Form { + Section { + DisclosureGroup("appVersion") { + Text("appVersionContent") + .font(.system(.body, design: .monospaced)) + } + + DisclosureGroup("contributors") { + HStack { + AsyncImage(url: URL(string: "https://avatars.githubusercontent.com/u/55411338?s=460&v=4")!, + placeholder: { Image(systemName: "circle.dashed") }, + image: { Image(uiImage: $0).resizable() }) + .frame(width: 38, height: 38) + Link("Pengwius", destination: URL(string: "https://github.com/pengwius")!) + .foregroundColor(Color("customControlColor")) + } + + HStack { + AsyncImage(url: URL(string: "https://avatars.githubusercontent.com/u/23171377?s=460&u=ce615ffdaaea96b191b1c27fb915fd18d25eaebd&v=4")!, + placeholder: { Image(systemName: "circle.dashed") }, + image: { Image(uiImage: $0).resizable() }) + .frame(width: 38, height: 38) + Link("rrroyal", destination: URL(string: "https://github.com/rrroyal")!) + .foregroundColor(Color("customControlColor")) + } + + HStack { + AsyncImage(url: URL(string: "https://avatars.githubusercontent.com/u/20373275?s=400&u=a59e3ca4656a7113a0021682b6733c27e6742e73&v=4")!, + placeholder: { Image(systemName: "circle.dashed") }, + image: { Image(uiImage: $0).resizable() }) + .frame(width: 38, height: 38) + Link("Karol Z.", destination: URL(string: "https://github.com/szakes1")!) + .foregroundColor(Color("customControlColor")) + } + } + + NavigationLink(destination: LicensesView()) { + Text("licensesButton") + } + + Link("FAQ", destination: URL(string: "https://wulkanowy.github.io/czesto-zadawane-pytania")!) + .foregroundColor(Color("customControlColor")) + + Link("privacyPolicy", destination: URL(string: "https://wulkanowy.github.io/polityka-prywatnosci")!) + .foregroundColor(Color("customControlColor")) + } + Section { + Link("discordButton", destination: URL(string: "https://discord.com/invite/vccAQBr")!) + .foregroundColor(Color("customControlColor")) + + Link("fbButton", destination: URL(string: "https://www.facebook.com/wulkanowy")!) + .foregroundColor(Color("customControlColor")) + + Link("Reddit", destination: URL(string: "https://www.reddit.com/r/wulkanowy/")!) + .foregroundColor(Color("customControlColor")) + } + + Section { + Button(action: { + if MFMailComposeViewController.canSendMail() { + self.isShowingMailView.toggle() + } else { + print("Can't send emails from this device") + } + if result != nil { + print("Result: \(String(describing: result))") + } + }) { + HStack { + Text("reportBug") + .foregroundColor(Color("customControlColor")) + } + } + // .disabled(!MFMailComposeViewController.canSendMail()) + } + .sheet(isPresented: $isShowingMailView) { + MailView(result: $result) { composer in + composer.setSubject("") + composer.setToRecipients(["wulkanowyinc@gmail.com"]) + } + } + + Link("homepage", destination: URL(string: "https://wulkanowy.github.io/")!) + .foregroundColor(Color("customControlColor")) + + Link("Github", destination: URL(string: "https://github.com/wulkanowy/wulkanowy-ios")!) + .foregroundColor(Color("customControlColor")) + } + } +} + + + +struct AboutView_Previews: PreviewProvider { + static var previews: some View { + Group { + AboutView() + } + .preferredColorScheme(.dark) + } +} + diff --git a/wulkanowy/Views/Content/dashboard.swift b/wulkanowy/Views/Content/dashboard.swift new file mode 100644 index 0000000..a937ac3 --- /dev/null +++ b/wulkanowy/Views/Content/dashboard.swift @@ -0,0 +1,44 @@ +// +// Dashboard.swift +// wulkanowy +// +// Created by Tomasz on 23/02/2021. +// + +import SwiftUI +import KeychainAccess +import Sdk + +struct DashboardView: View { + init() { + let keychain = Keychain() + let key = keychain["privateKey"] + + let luckyNumber = getLuckyNumber() + print(luckyNumber) + + } + + var body: some View { + NavigationView { + VStack { + Text("You are not logged in (dashboard)") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + }.padding() + } + } +} + + + +struct DashboardView_Previews: PreviewProvider { + static var previews: some View { + Group { + DashboardView() + } + .preferredColorScheme(.dark) + } +} + diff --git a/wulkanowy/Views/Content/exams.swift b/wulkanowy/Views/Content/exams.swift new file mode 100644 index 0000000..01a764e --- /dev/null +++ b/wulkanowy/Views/Content/exams.swift @@ -0,0 +1,32 @@ +// +// attendance.swift +// wulkanowy +// +// Created by Tomasz on 24/02/2021. +// + +import SwiftUI + +struct ExamsView: View { + var body: some View { + NavigationView { + VStack { + Text("You are not logged in (exams)") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + }.padding() + } + } +} + + + +struct ExamsView_Previews: PreviewProvider { + static var previews: some View { + Group { + ExamsView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Content/grades.swift b/wulkanowy/Views/Content/grades.swift new file mode 100644 index 0000000..b5f8b9b --- /dev/null +++ b/wulkanowy/Views/Content/grades.swift @@ -0,0 +1,32 @@ +// +// grades.swift +// wulkanowy +// +// Created by Tomasz on 24/02/2021. +// + +import SwiftUI + +struct GradesView: View { + var body: some View { + NavigationView { + VStack { + Text("You are not logged in (grades)") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + }.padding() + } + } +} + + + +struct GradesView_Previews: PreviewProvider { + static var previews: some View { + Group { + GradesView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Content/homework.swift b/wulkanowy/Views/Content/homework.swift new file mode 100644 index 0000000..2498c5b --- /dev/null +++ b/wulkanowy/Views/Content/homework.swift @@ -0,0 +1,32 @@ +// +// homework.swift +// wulkanowy +// +// Created by Tomasz on 24/02/2021. +// + +import SwiftUI + +struct HomeworksView: View { + var body: some View { + NavigationView { + VStack { + Text("You are not logged in (homeworks)") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + }.padding() + } + } +} + + + +struct HomeworksView_Previews: PreviewProvider { + static var previews: some View { + Group { + HomeworksView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Content/licenses.swift b/wulkanowy/Views/Content/licenses.swift new file mode 100644 index 0000000..bc12005 --- /dev/null +++ b/wulkanowy/Views/Content/licenses.swift @@ -0,0 +1,190 @@ +// +// licenses.swift +// wulkanowy +// +// Created by Tomasz on 27/02/2021. +// + +import SwiftUI + +struct LicensesView: View { + let KeychainAccessLicense: String = "The MIT License (MIT)\nCopyright (c) 2014 kishikawa katsumi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + let KeychainAccessURL: URL? = URL(string: "https://github.com/kishikawakatsumi/KeychainAccess")! + + let OpenSSLURL: URL? = URL(string: "https://github.com/krzyzanowskim/OpenSSL")! + + let SwiftUIEKtensionsURL: URL? = URL(string: "https://github.com/EnesKaraosman/SwiftUIEKtensions")! + + let OpenSSLLicense: String = """ + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a dual license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. Actually both licenses are BSD-style + Open Source licenses. In case of any license issues related to OpenSSL + please contact openssl-core@openssl.org. + + OpenSSL License + --------------- + + /* ==================================================================== + * Copyright (c) 1998-2008 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + + /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ +""" + + var body: some View { + List { + // KeychainAccess + DisclosureGroup("KeychainAccess") { + Text(KeychainAccessLicense) + .font(.system(.body, design: .monospaced)) + .onTapGesture { + guard let url = KeychainAccessURL else { return } + UIApplication.shared.open(url) + } + } + .padding(.vertical) + + // OpenSSL + DisclosureGroup("OpenSSL") { + Text(OpenSSLLicense) + .font(.system(.body, design: .monospaced)) + .onTapGesture { + guard let url = OpenSSLURL else { return } + UIApplication.shared.open(url) + } + } + .padding(.vertical) + + // SwiftUIEKtensions + DisclosureGroup("SwiftUIEKtensions") { + Text("noLicence") + .font(.system(.body, design: .monospaced)) + .onTapGesture { + guard let url = SwiftUIEKtensionsURL else { return } + UIApplication.shared.open(url) + } + } + .padding(.vertical) + } + .listStyle(InsetGroupedListStyle()) + .navigationTitle(Text("Libraries")) + } +} + +struct LicensesView_Previews: PreviewProvider { + static var previews: some View { + LicensesView() + } +} diff --git a/wulkanowy/Views/Content/messages.swift b/wulkanowy/Views/Content/messages.swift new file mode 100644 index 0000000..14a4865 --- /dev/null +++ b/wulkanowy/Views/Content/messages.swift @@ -0,0 +1,32 @@ +// +// messages.swift +// wulkanowy +// +// Created by Tomasz on 26/02/2021. +// + +import SwiftUI + +struct MessagesView: View { + var body: some View { + NavigationView { + VStack { + Text("You are not logged in (messages)") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + }.padding() + } + } +} + + + +struct MessagesView_Previews: PreviewProvider { + static var previews: some View { + Group { + MessagesView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Content/more.swift b/wulkanowy/Views/Content/more.swift new file mode 100644 index 0000000..cfd7ef5 --- /dev/null +++ b/wulkanowy/Views/Content/more.swift @@ -0,0 +1,50 @@ +// +// more.swift +// wulkanowy +// +// Created by Tomasz on 24/02/2021. +// + +import SwiftUI + +struct MoreView: View { + var body: some View { + NavigationView { + Form { + Section { + NavigationLink(destination: MessagesView()) { + Label("messagesButton", systemImage: "envelope") + .accessibility(label: Text("messagesButton")) + } + NavigationLink(destination: NotesView()) { + Label("notesButton", systemImage: "graduationcap") + .accessibility(label: Text("notesButton")) + } + } + + Section { + NavigationLink(destination: SettingsView()) { + Label("settingsButton", systemImage: "gear") + .accessibility(label: Text("settingsButton")) + } + NavigationLink(destination: AboutView()) { + Label("aboutButton", systemImage: "info.circle") + .accessibility(label: Text("aboutButton")) + } + } + } + .navigationBarTitle("moreButton", displayMode: .inline) + } + } +} + + + +struct MoreView_Previews: PreviewProvider { + static var previews: some View { + Group { + MoreView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Content/notes.swift b/wulkanowy/Views/Content/notes.swift new file mode 100644 index 0000000..fbcab5a --- /dev/null +++ b/wulkanowy/Views/Content/notes.swift @@ -0,0 +1,32 @@ +// +// notes.swift +// wulkanowy +// +// Created by Tomasz on 26/02/2021. +// + +import SwiftUI + +struct NotesView: View { + var body: some View { + NavigationView { + VStack { + Text("You are not logged in (notes)") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + }.padding() + } + } +} + + + +struct NotesView_Previews: PreviewProvider { + static var previews: some View { + Group { + NotesView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Content/settings.swift b/wulkanowy/Views/Content/settings.swift new file mode 100644 index 0000000..0220c6b --- /dev/null +++ b/wulkanowy/Views/Content/settings.swift @@ -0,0 +1,25 @@ +// +// settings.swift +// wulkanowy +// +// Created by Tomasz on 26/02/2021. +// + +import SwiftUI + +struct SettingsView: View { + var body: some View { + Text("Here are settings (in my imagination)") + } +} + + + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + Group { + SettingsView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Login/LoginView.swift b/wulkanowy/Views/Login/LoginView.swift index 6c60908..f4ce3bd 100644 --- a/wulkanowy/Views/Login/LoginView.swift +++ b/wulkanowy/Views/Login/LoginView.swift @@ -1,4 +1,5 @@ // +<<<<<<< HEAD // LoginView.swift // wulkanowy // @@ -41,11 +42,202 @@ struct LoginView: View { .sheet(isPresented: $needsAppOnboarding) { OnboardingView() } +======= +// ContentView.swift +// wulkanowy +// +// Created by Mikołaj on 25/10/2020. +// + + +import SwiftUI + +enum AvailableEndpoints: String, CaseIterable { + case vulcan = "Vulcan" + case fakelog = "Fakelog" +} + + +struct LoginView: View { + + @StateObject var vulcan: VulcanStore = VulcanStore.shared + + @State private var token: String = "" + @State private var symbol: String = "" + @State private var pin: String = "" + @State private var deviceModel: String = "" + + @State private var clicked: Bool = false + @State private var buttonValue = String(format: NSLocalizedString("loginButton", comment: "loginButton")) + @State private var loginStatus: String = "" + @State private var willMoveToNextScreen = false + + let cellHeight: CGFloat = 55 + let cornerRadius: CGFloat = 12 + let cellBackground: Color = Color(UIColor.systemGray6).opacity(0.5) + + let nullColor: Color = Color.accentColor.opacity(0.4) + + private func login() { + clicked = true + if(token != "" && symbol != "" && pin != "" && deviceModel != "") { + vulcan.login(token: token, symbol: symbol, pin: pin, deviceModel: deviceModel) { error in + if let error = error { + print("error: \(error)") + switch("\(error)"){ + case "wrongToken": + buttonValue = String(format: NSLocalizedString("\(error)", comment: "loginButton")) + + case "wrongSymbol": + buttonValue = String(format: NSLocalizedString("\(error)", comment: "loginButton")) + + case "wrongPin": + buttonValue = String(format: NSLocalizedString("\(error)", comment: "loginButton")) + + default: + buttonValue = String(format: NSLocalizedString("invalidData", comment: "loginButton")) + } + } else { + print("success") + } + } + + + } + } + + private func setColor(input: String) -> Color { + if(clicked == true){ + switch(input) { + case "token": + if (token == "") { + return nullColor + } else { + return cellBackground + } + + case "symbol": + if (symbol == "") { + return nullColor + } else { + return cellBackground + } + + case "pin": + if (pin == "") { + return nullColor + } else { + return cellBackground + } + + case "deviceName": + if (deviceModel == "") { + return nullColor + } else { + return cellBackground + } + + default: + return cellBackground + } + } else { + return cellBackground + } + } + + var body: some View { + VStack { + Image("wulkanowy") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 92) + .foregroundColor(.accentColor) + .padding(.bottom) + + Text("loginTitle") + .font(.largeTitle) + .fontWeight(.semibold) + + Spacer() + + TextField("token", text: $token) + .autocapitalization(.none) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "token"), lineWidth: 2) + ) + + TextField("symbol", text: $symbol) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "symbol"), lineWidth: 2) + ) + + TextField("pin", text: $pin) + .keyboardType(.numberPad) + .autocapitalization(.none) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "pin"), lineWidth: 2) + ) + + TextField("deviceName", text: $deviceModel) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "deviceName"), lineWidth: 2) + ) + + Spacer() + + Button(buttonValue) {login()} + .font(.headline) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .frame(maxWidth: .infinity) + .background(Color.accentColor.opacity(0.1)) + .cornerRadius(cornerRadius) + } + .padding() + Spacer() +>>>>>>> feature/register-device } } struct LoginView_Previews: PreviewProvider { static var previews: some View { +<<<<<<< HEAD LoginView() +======= + Group { + LoginView() + } + .preferredColorScheme(.dark) +>>>>>>> feature/register-device } } diff --git a/wulkanowy/Views/Navigation/navigation.swift b/wulkanowy/Views/Navigation/navigation.swift new file mode 100644 index 0000000..76158ae --- /dev/null +++ b/wulkanowy/Views/Navigation/navigation.swift @@ -0,0 +1,55 @@ +// +// navigation.swift +// wulkanowy +// +// Created by Tomasz on 23/02/2021. +// + +import SwiftUI + +struct NavigationBarView: View { + var body: some View { + TabView() { + DashboardView() + .tabItem { + Label("dashboardButton", systemImage: "rectangle.on.rectangle") + .accessibility(label: Text("dashboardButton")) + } + + GradesView() + .tabItem { + Label("gradesButton", systemImage: "rosette") + .accessibility(label: Text("gradesButton")) + } + + ExamsView() + .tabItem { + Label("examsButton", systemImage: "calendar") + .accessibility(label: Text("examsButton")) + } + + HomeworksView() + .tabItem { + Label("homeworkButton", systemImage: "note.text") + .accessibility(label: Text("homeworkButton")) + } + + MoreView() + .tabItem { + Label("moreButton", systemImage: "ellipsis.circle") + .accessibility(label: Text("moreButton")) + } + } + } +} + + +struct NavigationBarView_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationBarView() + } + .preferredColorScheme(.dark) + } +} + diff --git a/wulkanowy/Views/ghImage.swift b/wulkanowy/Views/ghImage.swift new file mode 100644 index 0000000..621e51d --- /dev/null +++ b/wulkanowy/Views/ghImage.swift @@ -0,0 +1,123 @@ +// +// ghImage.swift +// wulkanowy +// +// Created by Tomasz on 27/02/2021. +// + +import Foundation +import SwiftUI +import UIKit +import Combine + +struct AsyncImage: View { + @StateObject private var loader: ImageLoader + private let placeholder: Placeholder + private let image: (UIImage) -> Image + + init( + url: URL, + @ViewBuilder placeholder: () -> Placeholder, + @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:) + ) { + self.placeholder = placeholder() + self.image = image + _loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue)) + } + + var body: some View { + content + .onAppear(perform: loader.load) + } + + private var content: some View { + Group { + if loader.image != nil { + image(loader.image!) + } else { + placeholder + } + } + } +} + +protocol ImageCache { + subscript(_ url: URL) -> UIImage? { get set } +} + +struct TemporaryImageCache: ImageCache { + private let cache = NSCache() + + subscript(_ key: URL) -> UIImage? { + get { cache.object(forKey: key as NSURL) } + set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) } + } +} + +class ImageLoader: ObservableObject { + @Published var image: UIImage? + + private(set) var isLoading = false + + private let url: URL + private var cache: ImageCache? + private var cancellable: AnyCancellable? + + private static let imageProcessingQueue = DispatchQueue(label: "image-processing") + + init(url: URL, cache: ImageCache? = nil) { + self.url = url + self.cache = cache + } + + deinit { + cancel() + } + + func load() { + guard !isLoading else { return } + + if let image = cache?[url] { + self.image = image + return + } + + cancellable = URLSession.shared.dataTaskPublisher(for: url) + .map { UIImage(data: $0.data) } + .replaceError(with: nil) + .handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() }, + receiveOutput: { [weak self] in self?.cache($0) }, + receiveCompletion: { [weak self] _ in self?.onFinish() }, + receiveCancel: { [weak self] in self?.onFinish() }) + .subscribe(on: Self.imageProcessingQueue) + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.image = $0 } + } + + func cancel() { + cancellable?.cancel() + } + + private func onStart() { + isLoading = true + } + + private func onFinish() { + isLoading = false + } + + private func cache(_ image: UIImage?) { + image.map { cache?[url] = $0 } + } +} + +struct ImageCacheKey: EnvironmentKey { + static let defaultValue: ImageCache = TemporaryImageCache() +} + +extension EnvironmentValues { + var imageCache: ImageCache { + get { self[ImageCacheKey.self] } + set { self[ImageCacheKey.self] = newValue } + } +} diff --git a/wulkanowy/VulcanStore.swift b/wulkanowy/VulcanStore.swift new file mode 100644 index 0000000..8a16a1e --- /dev/null +++ b/wulkanowy/VulcanStore.swift @@ -0,0 +1,61 @@ +// +// VulcanStore.swift +// wulkanowy +// +// Created by Tomasz (copied from rrroyal/vulcan) on 16/02/2021. +// + +import Combine +import Sdk +import Foundation +import KeychainAccess + +final class VulcanStore: ObservableObject { + static let shared: VulcanStore = VulcanStore() + + var privateKey: String? + let sdk: Sdk? + private init() { + // Check for stored certificate + guard let certificate: X509 = try? X509(serialNumber: 1, certificateEntries: ["CN": "APP_CERTIFICATE CA Certificate"]) else { + sdk = nil + privateKey = nil + return + } + + guard let privateKeyRawData = certificate.getPrivateKeyData(format: .DER), + let privateKeyString = String(data: privateKeyRawData, encoding: .utf8)? + .split(separator: "\n") + .dropFirst() + .dropLast() + .joined() else { + privateKey = nil + sdk = nil + return + } + + privateKey = privateKeyString + + sdk = Sdk(certificate: certificate) + } + + public func login(token: String, symbol: String, pin: String, deviceModel: String, completionHandler: @escaping (Error?) -> Void) { + sdk?.login(token: token, symbol: symbol, pin: pin, deviceModel: deviceModel) { [self] error in + if let error = error { + // Wyobraź sobie, że tutaj jest obsługa błędów. Wyobraź, bo mi sie jej robić nie chciało. + print(error) + } else { + let privateKeyToSave: String = privateKey ?? "" + + let utf8str = privateKeyToSave.data(using: .utf8) + + if let base64Encoded = utf8str?.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) { + let keychain = Keychain() + keychain[string: "privateKey"] = base64Encoded + } + } + + completionHandler(error) + } + } +} diff --git a/wulkanowyTests/wulkanowyTests.swift b/wulkanowyTests/wulkanowyTests.swift index 0c35660..c88abd1 100644 --- a/wulkanowyTests/wulkanowyTests.swift +++ b/wulkanowyTests/wulkanowyTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import wulkanowy +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) class wulkanowyTests: XCTestCase { override func setUpWithError() throws {