From b8675221885cebcde95af6ac48d0a845f0078db4 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Wed, 17 Feb 2021 21:57:26 +0100 Subject: [PATCH 01/17] Register device --- sdk/.gitignore | 5 + sdk/Package.swift | 28 +++ sdk/README.md | 3 + sdk/Sources/Sdk/APIError.swift | 26 ++ sdk/Sources/Sdk/Extensions/Date.swift | 21 ++ sdk/Sources/Sdk/Extensions/URLRequest.swift | 70 ++++++ sdk/Sources/Sdk/Sdk.swift | 235 ++++++++++++++++++ sdk/Sources/Sdk/X509.swift | 192 ++++++++++++++ sdk/Sources/Sdk/signer.swift | 97 ++++++++ sdk/Tests/LinuxMain.swift | 7 + sdk/Tests/SdkTests/SdkTests.swift | 15 ++ sdk/Tests/SdkTests/XCTestManifests.swift | 9 + wulkanowy.xcodeproj/project.pbxproj | 111 ++++++++- .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/swiftpm/Package.resolved | 16 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + wulkanowy/.DS_Store | Bin 0 -> 6148 bytes .../AccentColor.colorset/Contents.json | 9 + .../AppIcon.appiconset/Contents.json | 1 + .../AppIcon.appiconset/logo.jpg | Bin 0 -> 21309 bytes .../ComponentColor.colorset/Contents.json | 38 +++ .../Assets.xcassets/Colours/Contents.json | 6 + .../wulkanowy.imageset/Contents.json | 23 ++ .../wulkanowy.imageset/wulkanowy-1.svg | 9 + .../wulkanowy.imageset/wulkanowy-2.svg | 9 + .../wulkanowy.imageset/wulkanowy.svg | 9 + wulkanowy/ContentView.swift | 108 +++++++- wulkanowy/Info.plist | 7 + wulkanowy/VulcanStore.swift | 37 +++ wulkanowy/en.lproj/Localizable.strings | 13 + wulkanowy/pl-PL.lproj/Localizable.strings | 13 + wulkanowy/wulkanowyApp.swift | 2 + 33 files changed, 1134 insertions(+), 11 deletions(-) create mode 100644 sdk/.gitignore create mode 100644 sdk/Package.swift create mode 100644 sdk/README.md create mode 100644 sdk/Sources/Sdk/APIError.swift create mode 100644 sdk/Sources/Sdk/Extensions/Date.swift create mode 100644 sdk/Sources/Sdk/Extensions/URLRequest.swift create mode 100644 sdk/Sources/Sdk/Sdk.swift create mode 100644 sdk/Sources/Sdk/X509.swift create mode 100644 sdk/Sources/Sdk/signer.swift create mode 100644 sdk/Tests/LinuxMain.swift create mode 100644 sdk/Tests/SdkTests/SdkTests.swift create mode 100644 sdk/Tests/SdkTests/XCTestManifests.swift create mode 100644 wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 wulkanowy.xcworkspace/contents.xcworkspacedata create mode 100644 wulkanowy.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 wulkanowy/.DS_Store create mode 100644 wulkanowy/Assets.xcassets/AppIcon.appiconset/logo.jpg create mode 100644 wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json create mode 100644 wulkanowy/Assets.xcassets/Colours/Contents.json create mode 100644 wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json create mode 100644 wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-1.svg create mode 100644 wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg create mode 100644 wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg create mode 100644 wulkanowy/VulcanStore.swift create mode 100644 wulkanowy/en.lproj/Localizable.strings create mode 100644 wulkanowy/pl-PL.lproj/Localizable.strings 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..4fbaca6 --- /dev/null +++ b/sdk/Sources/Sdk/APIError.swift @@ -0,0 +1,26 @@ +// +// 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 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/Sdk.swift b/sdk/Sources/Sdk/Sdk.swift new file mode 100644 index 0000000..9a29b25 --- /dev/null +++ b/sdk/Sources/Sdk/Sdk.swift @@ -0,0 +1,235 @@ +// +// Sdk.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021. +// + + +import Foundation +import Combine +import os + +@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) +public class Sdk { + static private let libraryVersion: String = "v0-INTERNAL" + + private let loggerSubsystem: String = "xyz.shameful.VulcanKit" + private var cancellables: Set = [] + + 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 { + 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 { + 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) + } + + // 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) + } + + // 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 + } + + switch statusCode { + case 0: return nil + case 200: return APIError.wrongToken + 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/signer.swift b/sdk/Sources/Sdk/signer.swift new file mode 100644 index 0000000..e87f9c9 --- /dev/null +++ b/sdk/Sources/Sdk/signer.swift @@ -0,0 +1,97 @@ +// +// File.swift +// +// +// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021. +// + +import Foundation +import CryptoKit + +@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 813424b..d37f6f0 100644 --- a/wulkanowy.xcodeproj/project.pbxproj +++ b/wulkanowy.xcodeproj/project.pbxproj @@ -3,10 +3,14 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ + 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */; }; + 5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */ = {isa = PBXBuildFile; productRef = 5C9B6F4825D6C08D00C3F5F5 /* Sdk */; }; + 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 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9092544E17400F8903A /* ContentView.swift */; }; F4C6D90C2544E17500F8903A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C6D90B2544E17500F8903A /* Assets.xcassets */; }; @@ -32,7 +36,35 @@ }; /* 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 */ + 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VulcanStore.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 = ""; }; + 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 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -52,6 +84,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */, + 5CCAE31625DA4CDD00D87580 /* OpenSSL in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -72,12 +106,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5C9B6E4825D6ADFB00C3F5F5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5C9B6E4925D6ADFB00C3F5F5 /* NetworkExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; F4C6D8FB2544E17300F8903A = { isa = PBXGroup; children = ( + 5C9B6F4525D6C06D00C3F5F5 /* Sdk */, F4C6D9062544E17400F8903A /* wulkanowy */, F4C6D9182544E17500F8903A /* wulkanowyTests */, F4C6D9232544E17500F8903A /* wulkanowyUITests */, + 5C9B6E4825D6ADFB00C3F5F5 /* Frameworks */, F4C6D9052544E17400F8903A /* Products */, ); sourceTree = ""; @@ -100,6 +144,8 @@ F4C6D90B2544E17500F8903A /* Assets.xcassets */, F4C6D9102544E17500F8903A /* Info.plist */, F4C6D90D2544E17500F8903A /* Preview Content */, + 5CEA516D25D540B900DB45BD /* Localizable.strings */, + 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */, ); path = wulkanowy; sourceTree = ""; @@ -140,12 +186,18 @@ F4C6D9002544E17300F8903A /* Sources */, F4C6D9012544E17300F8903A /* Frameworks */, F4C6D9022544E17300F8903A /* Resources */, + 5C1F6D5F25D6891300AFDDD6 /* Embed App Extensions */, + 5C9B6EEC25D6B25200C3F5F5 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = wulkanowy; + packageProductDependencies = ( + 5C9B6F4825D6C08D00C3F5F5 /* Sdk */, + 5CCAE31525DA4CDD00D87580 /* OpenSSL */, + ); productName = wulkanowy; productReference = F4C6D9042544E17400F8903A /* wulkanowy.app */; productType = "com.apple.product-type.application"; @@ -192,7 +244,7 @@ F4C6D8FC2544E17300F8903A /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1210; + LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1210; TargetAttributes = { F4C6D9032544E17300F8903A = { @@ -215,8 +267,12 @@ knownRegions = ( en, Base, + "pl-PL", ); mainGroup = F4C6D8FB2544E17300F8903A; + packageReferences = ( + 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */, + ); productRefGroup = F4C6D9052544E17400F8903A /* Products */; projectDirPath = ""; projectRoot = ""; @@ -234,6 +290,7 @@ buildActionMask = 2147483647; files = ( F4C6D90F2544E17500F8903A /* Preview Assets.xcassets in Resources */, + 5CEA516B25D540B900DB45BD /* Localizable.strings in Resources */, F4C6D90C2544E17500F8903A /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -259,6 +316,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */, F4C6D90A2544E17400F8903A /* ContentView.swift in Sources */, F4C6D9082544E17400F8903A /* wulkanowyApp.swift in Sources */, ); @@ -295,11 +353,24 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 5CEA516D25D540B900DB45BD /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 5CEA516C25D540B900DB45BD /* en */, + 5CF81BD725D9D44400B12C4C /* pl-PL */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ F4C6D9272544E17500F8903A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -347,7 +418,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; @@ -361,6 +432,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -402,7 +474,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; @@ -419,14 +491,14 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"wulkanowy/Preview Content\""; + 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", ); - PRODUCT_BUNDLE_IDENTIFIER = io.github.wulkanowy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -440,14 +512,14 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"wulkanowy/Preview Content\""; + 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", ); - PRODUCT_BUNDLE_IDENTIFIER = io.github.wulkanowy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -574,6 +646,29 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/krzyzanowskim/OpenSSL"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.180; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 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.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..b88114c --- /dev/null +++ b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "OpenSSL", + "repositoryURL": "https://github.com/krzyzanowskim/OpenSSL", + "state": { + "branch": null, + "revision": "389296819a8d025ac10ddc9f22135a5518991fdc", + "version": "1.1.180" + } + } + ] + }, + "version": 1 +} 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/.DS_Store b/wulkanowy/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5f951724627016721602b62bdcaa9b339ebaa56a GIT binary patch literal 6148 zcmeHKyG{Z@6g{I9C>iyLPD(|BJDm1!V84xa7Vi7$LiV>y}Ow4+l{^pr<@0*G%0nYb*hETp`Ak+>y=un6UKB=OAG| z!5E8t1am~zdnQ8JG8>36!F3aA3Az=i@mv&G64hnlMbs(>o6RzThl0h3_pF?DEP z9n9<%fLLO)HJ0TgQ8R literal 0 HcmV?d00001 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 9221b9b..e1558a5 100644 --- a/wulkanowy/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/wulkanowy/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -31,6 +31,7 @@ "size" : "40x40" }, { + "filename" : "logo.jpg", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" diff --git a/wulkanowy/Assets.xcassets/AppIcon.appiconset/logo.jpg b/wulkanowy/Assets.xcassets/AppIcon.appiconset/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..14cb2b783f5f2f66996ecc7492afba597c3f47a6 GIT binary patch literal 21309 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 + + Shape + + + + + + \ No newline at end of file diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg b/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg new file mode 100644 index 0000000..de66e7a --- /dev/null +++ b/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg @@ -0,0 +1,9 @@ + + + Shape + + + + + + \ No newline at end of file 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/ContentView.swift b/wulkanowy/ContentView.swift index 72c5c11..8567a28 100644 --- a/wulkanowy/ContentView.swift +++ b/wulkanowy/ContentView.swift @@ -7,15 +7,117 @@ import SwiftUI +enum AvailableEndpoints: String, CaseIterable { + case vulcan = "Vulcan" + case fakelog = "Fakelog" +} + + struct ContentView: 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 = "" + + let cellHeight: CGFloat = 55 + let cornerRadius: CGFloat = 12 + let cellBackground: Color = Color(UIColor.systemGray6).opacity(0.5) + + private func login() { + vulcan.login(token: token, symbol: symbol, pin: pin, deviceModel: deviceModel) { error in + if let error = error { + print("error: \(error)") + } else { + print("success") + } + } + } + + var body: some View { - Text("Wulkanowy!") - .bold() + VStack { + VStack { + Image("wulkanowy") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 92) + .foregroundColor(.accentColor) + .padding(.bottom) + + Text("loginTitle") + .font(.largeTitle) + .fontWeight(.semibold) + } + .padding(.top, 50) + .padding(.bottom, -50) + + Spacer() + + TextField("token", text: $token) + .autocapitalization(.none) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .cornerRadius(cornerRadius) + + TextField("symbol", text: $symbol) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .cornerRadius(cornerRadius) + + TextField("pin", text: $pin) + .keyboardType(.numberPad) + .autocapitalization(.none) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .cornerRadius(cornerRadius) + + TextField("Device name", text: $deviceModel) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(Font.body.weight(Font.Weight.medium)) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .background(cellBackground) + .cornerRadius(cornerRadius) + + Spacer() + + Button("loginButton") {login()} + .font(.headline) + .multilineTextAlignment(.center) + .padding(.horizontal) + .frame(height: cellHeight) + .frame(maxWidth: .infinity) + .background(Color.accentColor.opacity(0.1)) + .cornerRadius(cornerRadius) + } + .padding() } } + struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() + Group { + ContentView() + + } + .preferredColorScheme(.light) } } diff --git a/wulkanowy/Info.plist b/wulkanowy/Info.plist index efc211a..bc627d2 100644 --- a/wulkanowy/Info.plist +++ b/wulkanowy/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Wulkanowy CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -20,6 +22,11 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/wulkanowy/VulcanStore.swift b/wulkanowy/VulcanStore.swift new file mode 100644 index 0000000..99ba8c3 --- /dev/null +++ b/wulkanowy/VulcanStore.swift @@ -0,0 +1,37 @@ +// +// VulcanStore.swift +// wulkanowy +// +// Created by Tomasz (copied from rrroyal/vulcan) on 16/02/2021. +// + +import Combine +import Sdk + +final class VulcanStore: ObservableObject { + static let shared: VulcanStore = VulcanStore() + + 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 + return + } + + 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) { error in + if error == nil { + // Success - save certificate + } else { + // Error - discard or try again + } + + completionHandler(error) + } + } +} + diff --git a/wulkanowy/en.lproj/Localizable.strings b/wulkanowy/en.lproj/Localizable.strings new file mode 100644 index 0000000..1221c78 --- /dev/null +++ b/wulkanowy/en.lproj/Localizable.strings @@ -0,0 +1,13 @@ +/* + Localizable.strings + wulkanowy + + Created by Tomasz on 11/02/2021. + +*/ + +"loginTitle" = "Log In"; +"token" = "Token"; +"symbol" = "Symbol"; +"pin" = "Pin"; +"loginButton" = "Login"; diff --git a/wulkanowy/pl-PL.lproj/Localizable.strings b/wulkanowy/pl-PL.lproj/Localizable.strings new file mode 100644 index 0000000..674a967 --- /dev/null +++ b/wulkanowy/pl-PL.lproj/Localizable.strings @@ -0,0 +1,13 @@ +/* + Localizable.strings + wulkanowy + + Created by Tomasz on 11/02/2021. + +*/ + +"loginTitle" = "Logowanie"; +"token" = "Token"; +"symbol" = "Symbol"; +"pin" = "Pin"; +"loginButton" = "Zaloguj"; diff --git a/wulkanowy/wulkanowyApp.swift b/wulkanowy/wulkanowyApp.swift index ce32d9e..f9719d9 100644 --- a/wulkanowy/wulkanowyApp.swift +++ b/wulkanowy/wulkanowyApp.swift @@ -6,6 +6,8 @@ // import SwiftUI +import Sdk +import Combine @main struct wulkanowyApp: App { From 1b689343eaaa84612ebe9a391203057c2f2141ae Mon Sep 17 00:00:00 2001 From: Pengwius Date: Thu, 18 Feb 2021 07:41:33 +0100 Subject: [PATCH 02/17] Fixes --- .gitignore | 55 ++++++++++++++++++ sdk/Sources/Sdk/Sdk.swift | 4 +- wulkanowy/.DS_Store | Bin 6148 -> 6148 bytes .../wulkanowy.imageset/Contents.json | 2 - .../wulkanowy.imageset/wulkanowy-2.svg | 9 --- .../wulkanowy.imageset/wulkanowy.svg | 9 --- wulkanowy/Info.plist | 5 -- 7 files changed, 57 insertions(+), 27 deletions(-) delete mode 100644 wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg delete mode 100644 wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg 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/Sources/Sdk/Sdk.swift b/sdk/Sources/Sdk/Sdk.swift index 9a29b25..f91898d 100644 --- a/sdk/Sources/Sdk/Sdk.swift +++ b/sdk/Sources/Sdk/Sdk.swift @@ -12,9 +12,9 @@ import os @available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) public class Sdk { - static private let libraryVersion: String = "v0-INTERNAL" + static private let libraryVersion: String = "0.0.1" - private let loggerSubsystem: String = "xyz.shameful.VulcanKit" + private let loggerSubsystem: String = "com.wulkanowy-ios.Sdk" private var cancellables: Set = [] public let certificate: X509 diff --git a/wulkanowy/.DS_Store b/wulkanowy/.DS_Store index 5f951724627016721602b62bdcaa9b339ebaa56a..9230ab71f941da8aa18b8d409929897c5b7ffd1c 100644 GIT binary patch delta 18 ZcmZoMXffDe%fuSMz`&rj*@@|pC;%?x1f>7~ delta 18 ZcmZoMXffDe%f#x#z`zi&*@@|pC;%^@1jhgX diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json index 7ddfe15..055332f 100644 --- a/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json +++ b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json @@ -6,12 +6,10 @@ "scale" : "1x" }, { - "filename" : "wulkanowy.svg", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "wulkanowy-2.svg", "idiom" : "universal", "scale" : "3x" } diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg b/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg deleted file mode 100644 index de66e7a..0000000 --- a/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-2.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - Shape - - - - - - \ No newline at end of file diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg b/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg deleted file mode 100644 index de66e7a..0000000 --- a/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - Shape - - - - - - \ No newline at end of file diff --git a/wulkanowy/Info.plist b/wulkanowy/Info.plist index bc627d2..e608f0b 100644 --- a/wulkanowy/Info.plist +++ b/wulkanowy/Info.plist @@ -22,11 +22,6 @@ 1 LSRequiresIPhoneOS - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - UIApplicationSceneManifest UIApplicationSupportsMultipleScenes From 60f9eb8a8a32ded5ba9d982e350ec5075e4b8c7e Mon Sep 17 00:00:00 2001 From: Pengwius Date: Thu, 18 Feb 2021 12:56:11 +0100 Subject: [PATCH 03/17] Next fixes --- .../wulkanowy.imageset/Contents.json | 8 -------- wulkanowy/Info.plist | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json index 055332f..c93d7f9 100644 --- a/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json +++ b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json @@ -4,14 +4,6 @@ "filename" : "wulkanowy-1.svg", "idiom" : "universal", "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" } ], "info" : { diff --git a/wulkanowy/Info.plist b/wulkanowy/Info.plist index e608f0b..9620cf4 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 From 1ac56775ae10004380a182147e8e6a69134116de Mon Sep 17 00:00:00 2001 From: Pengwius Date: Sat, 20 Feb 2021 20:52:47 +0100 Subject: [PATCH 04/17] Reaction for null input --- wulkanowy/ContentView.swift | 79 +++++++++++++++++++++++++++++++------ wulkanowy/Info.plist | 1 + 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/wulkanowy/ContentView.swift b/wulkanowy/ContentView.swift index 8567a28..ae49c88 100644 --- a/wulkanowy/ContentView.swift +++ b/wulkanowy/ContentView.swift @@ -5,6 +5,7 @@ // Created by Mikołaj on 25/10/2020. // + import SwiftUI enum AvailableEndpoints: String, CaseIterable { @@ -22,20 +23,65 @@ struct ContentView: View { @State private var pin: String = "" @State private var deviceModel: String = "" + @State private var clicked: Bool = 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() { - vulcan.login(token: token, symbol: symbol, pin: pin, deviceModel: deviceModel) { error in - if let error = error { - print("error: \(error)") - } else { - print("success") - } + 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)") + } 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 { @@ -64,7 +110,10 @@ struct ContentView: View { .padding(.horizontal) .frame(height: cellHeight) .background(cellBackground) - .cornerRadius(cornerRadius) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "token"), lineWidth: 2) + ) TextField("symbol", text: $symbol) .autocapitalization(.none) @@ -74,7 +123,10 @@ struct ContentView: View { .padding(.horizontal) .frame(height: cellHeight) .background(cellBackground) - .cornerRadius(cornerRadius) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "symbol"), lineWidth: 2) + ) TextField("pin", text: $pin) .keyboardType(.numberPad) @@ -84,7 +136,10 @@ struct ContentView: View { .padding(.horizontal) .frame(height: cellHeight) .background(cellBackground) - .cornerRadius(cornerRadius) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "pin"), lineWidth: 2) + ) TextField("Device name", text: $deviceModel) .autocapitalization(.none) @@ -94,7 +149,10 @@ struct ContentView: View { .padding(.horizontal) .frame(height: cellHeight) .background(cellBackground) - .cornerRadius(cornerRadius) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(setColor(input: "deviceName"), lineWidth: 2) + ) Spacer() @@ -116,7 +174,6 @@ struct ContentView_Previews: PreviewProvider { static var previews: some View { Group { ContentView() - } .preferredColorScheme(.light) } diff --git a/wulkanowy/Info.plist b/wulkanowy/Info.plist index 9620cf4..3c0a77b 100644 --- a/wulkanowy/Info.plist +++ b/wulkanowy/Info.plist @@ -55,6 +55,7 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown UISupportedInterfaceOrientations~ipad From 6392f7220dbcebcaacf887babddfe7093a2da6aa Mon Sep 17 00:00:00 2001 From: Pengwius Date: Sat, 20 Feb 2021 21:56:30 +0100 Subject: [PATCH 05/17] Error handling --- wulkanowy/ContentView.swift | 19 +++++++++++++++++-- wulkanowy/en.lproj/Localizable.strings | 4 ++++ wulkanowy/pl-PL.lproj/Localizable.strings | 5 +++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/wulkanowy/ContentView.swift b/wulkanowy/ContentView.swift index ae49c88..bb2b36c 100644 --- a/wulkanowy/ContentView.swift +++ b/wulkanowy/ContentView.swift @@ -24,6 +24,8 @@ struct ContentView: View { @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 = "" let cellHeight: CGFloat = 55 let cornerRadius: CGFloat = 12 @@ -37,10 +39,23 @@ struct ContentView: View { 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 "wrongPin": + buttonValue = String(format: NSLocalizedString("\(error)", comment: "loginButton")) + + default: + buttonValue = String(format: NSLocalizedString("invalidData", comment: "loginButton")) + } } else { print("success") + buttonValue = String(format: NSLocalizedString("success", comment: "loginButton")) } } + + } } @@ -156,7 +171,7 @@ struct ContentView: View { Spacer() - Button("loginButton") {login()} + Button(buttonValue) {login()} .font(.headline) .multilineTextAlignment(.center) .padding(.horizontal) @@ -175,6 +190,6 @@ struct ContentView_Previews: PreviewProvider { Group { ContentView() } - .preferredColorScheme(.light) + .preferredColorScheme(.dark) } } diff --git a/wulkanowy/en.lproj/Localizable.strings b/wulkanowy/en.lproj/Localizable.strings index 1221c78..c19b8f0 100644 --- a/wulkanowy/en.lproj/Localizable.strings +++ b/wulkanowy/en.lproj/Localizable.strings @@ -11,3 +11,7 @@ "symbol" = "Symbol"; "pin" = "Pin"; "loginButton" = "Login"; +"wrongToken" = "Wrong token"; +"wrongPin" = "Wrong pin"; +"invalidData" = "Wrong token, symbol or pin"; +"success" = "Success"; diff --git a/wulkanowy/pl-PL.lproj/Localizable.strings b/wulkanowy/pl-PL.lproj/Localizable.strings index 674a967..fe81ea3 100644 --- a/wulkanowy/pl-PL.lproj/Localizable.strings +++ b/wulkanowy/pl-PL.lproj/Localizable.strings @@ -11,3 +11,8 @@ "symbol" = "Symbol"; "pin" = "Pin"; "loginButton" = "Zaloguj"; +"wrongToken" = "Zły token"; +"wrongPin" = "Zły pin"; +"invalidData" = "Zły token, symbol lub pin"; +"success" = "Sukces"; + From 9d5fa7d15e14bc674b20f8a15698ab7d02a14981 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Sat, 20 Feb 2021 22:20:53 +0100 Subject: [PATCH 06/17] Handle wrong symbol error --- sdk/Sources/Sdk/APIError.swift | 1 + sdk/Sources/Sdk/Sdk.swift | 5 ++++- wulkanowy/ContentView.swift | 3 +++ wulkanowy/en.lproj/Localizable.strings | 1 + wulkanowy/pl-PL.lproj/Localizable.strings | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sdk/Sources/Sdk/APIError.swift b/sdk/Sources/Sdk/APIError.swift index 4fbaca6..ae237de 100644 --- a/sdk/Sources/Sdk/APIError.swift +++ b/sdk/Sources/Sdk/APIError.swift @@ -20,6 +20,7 @@ public extension Sdk { case urlError case wrongToken + case wrongSymbol case wrongPin } } diff --git a/sdk/Sources/Sdk/Sdk.swift b/sdk/Sources/Sdk/Sdk.swift index f91898d..0f430df 100644 --- a/sdk/Sources/Sdk/Sdk.swift +++ b/sdk/Sources/Sdk/Sdk.swift @@ -224,10 +224,13 @@ public class Sdk { 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/wulkanowy/ContentView.swift b/wulkanowy/ContentView.swift index bb2b36c..26d5711 100644 --- a/wulkanowy/ContentView.swift +++ b/wulkanowy/ContentView.swift @@ -42,6 +42,9 @@ struct ContentView: View { 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")) diff --git a/wulkanowy/en.lproj/Localizable.strings b/wulkanowy/en.lproj/Localizable.strings index c19b8f0..0b158b8 100644 --- a/wulkanowy/en.lproj/Localizable.strings +++ b/wulkanowy/en.lproj/Localizable.strings @@ -12,6 +12,7 @@ "pin" = "Pin"; "loginButton" = "Login"; "wrongToken" = "Wrong token"; +"wrongSymbol" = "Wrong symbol"; "wrongPin" = "Wrong pin"; "invalidData" = "Wrong token, symbol or pin"; "success" = "Success"; diff --git a/wulkanowy/pl-PL.lproj/Localizable.strings b/wulkanowy/pl-PL.lproj/Localizable.strings index fe81ea3..6ca907e 100644 --- a/wulkanowy/pl-PL.lproj/Localizable.strings +++ b/wulkanowy/pl-PL.lproj/Localizable.strings @@ -12,6 +12,7 @@ "pin" = "Pin"; "loginButton" = "Zaloguj"; "wrongToken" = "Zły token"; +"wrongSymbol" = "Zły symbol"; "wrongPin" = "Zły pin"; "invalidData" = "Zły token, symbol lub pin"; "success" = "Sukces"; From b898042784b38b778eccd6e5ebf6b5cee1e3fef8 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Tue, 23 Feb 2021 13:39:46 +0100 Subject: [PATCH 07/17] Views --- wulkanowy.xcodeproj/project.pbxproj | 68 ++++++++++++++++-- wulkanowy/.DS_Store | Bin 6148 -> 6148 bytes wulkanowy/{ => App}/wulkanowyApp.swift | 2 +- .../en.lproj/Localizable.strings | 0 .../pl-PL.lproj/Localizable.strings | 0 wulkanowy/Views/Content/Dashboard.swift | 34 +++++++++ .../Login/LoginView.swift} | 13 ++-- wulkanowy/Views/Navigation/navigation.swift | 31 ++++++++ 8 files changed, 134 insertions(+), 14 deletions(-) rename wulkanowy/{ => App}/wulkanowyApp.swift (87%) rename wulkanowy/{ => Resources}/en.lproj/Localizable.strings (100%) rename wulkanowy/{ => Resources}/pl-PL.lproj/Localizable.strings (100%) create mode 100644 wulkanowy/Views/Content/Dashboard.swift rename wulkanowy/{ContentView.swift => Views/Login/LoginView.swift} (95%) create mode 100644 wulkanowy/Views/Navigation/navigation.swift diff --git a/wulkanowy.xcodeproj/project.pbxproj b/wulkanowy.xcodeproj/project.pbxproj index d37f6f0..a43548e 100644 --- a/wulkanowy.xcodeproj/project.pbxproj +++ b/wulkanowy.xcodeproj/project.pbxproj @@ -9,10 +9,12 @@ /* Begin PBXBuildFile section */ 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */; }; 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 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9092544E17400F8903A /* ContentView.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 */; }; @@ -63,11 +65,13 @@ 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VulcanStore.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 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.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 = ""; }; @@ -106,6 +110,40 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5C1B848625E1B6740074F29D /* App */ = { + isa = PBXGroup; + children = ( + F4C6D9072544E17400F8903A /* wulkanowyApp.swift */, + ); + path = App; + sourceTree = ""; + }; + 5C1B848925E1B6910074F29D /* Views */ = { + isa = PBXGroup; + children = ( + 5CC2EAAC25E5269E00B6183E /* Navigation */, + 5CC2EAA325E516DD00B6183E /* Content */, + 5C1B849F25E1B7A30074F29D /* Login */, + ); + path = Views; + sourceTree = ""; + }; + 5C1B848E25E1B6FA0074F29D /* Resources */ = { + isa = PBXGroup; + children = ( + 5CEA516D25D540B900DB45BD /* Localizable.strings */, + ); + path = Resources; + sourceTree = ""; + }; + 5C1B849F25E1B7A30074F29D /* Login */ = { + isa = PBXGroup; + children = ( + F4C6D9092544E17400F8903A /* LoginView.swift */, + ); + path = Login; + sourceTree = ""; + }; 5C9B6E4825D6ADFB00C3F5F5 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -114,6 +152,22 @@ name = Frameworks; sourceTree = ""; }; + 5CC2EAA325E516DD00B6183E /* Content */ = { + isa = PBXGroup; + children = ( + 5CC2EAA425E516F100B6183E /* Dashboard.swift */, + ); + path = Content; + sourceTree = ""; + }; + 5CC2EAAC25E5269E00B6183E /* Navigation */ = { + isa = PBXGroup; + children = ( + 5CC2EAAD25E526B500B6183E /* navigation.swift */, + ); + path = Navigation; + sourceTree = ""; + }; F4C6D8FB2544E17300F8903A = { isa = PBXGroup; children = ( @@ -139,12 +193,12 @@ F4C6D9062544E17400F8903A /* wulkanowy */ = { isa = PBXGroup; children = ( - F4C6D9072544E17400F8903A /* wulkanowyApp.swift */, - F4C6D9092544E17400F8903A /* ContentView.swift */, + 5C1B848E25E1B6FA0074F29D /* Resources */, + 5C1B848925E1B6910074F29D /* Views */, + 5C1B848625E1B6740074F29D /* App */, F4C6D90B2544E17500F8903A /* Assets.xcassets */, F4C6D9102544E17500F8903A /* Info.plist */, F4C6D90D2544E17500F8903A /* Preview Content */, - 5CEA516D25D540B900DB45BD /* Localizable.strings */, 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */, ); path = wulkanowy; @@ -317,7 +371,9 @@ buildActionMask = 2147483647; files = ( 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */, - F4C6D90A2544E17400F8903A /* ContentView.swift in Sources */, + F4C6D90A2544E17400F8903A /* LoginView.swift in Sources */, + 5CC2EAAE25E526B500B6183E /* navigation.swift in Sources */, + 5CC2EAA525E516F100B6183E /* Dashboard.swift in Sources */, F4C6D9082544E17400F8903A /* wulkanowyApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/wulkanowy/.DS_Store b/wulkanowy/.DS_Store index 9230ab71f941da8aa18b8d409929897c5b7ffd1c..4624469316bba9308838bc3e563e008ba7c5d218 100644 GIT binary patch delta 74 zcmZoMXfc=|#>B`mu~2NHo+2a5#DLw5ER%Vd94GH*GMTK&+_3o@vmnbRmamMP**W+* dfGRhCWd6=PnP0?`g8>K_85o#02Z(H81^@wr6B_^k delta 310 zcmZoMXfc=|#>B)qu~2NHo+2a1#DLw41(+BaIVSTkI@Sv^I5Xrkmq=LyXhD;!< zoI#JF7)WL^q%o9u=Hw?Q<>V(ZFfa%(FfghD>9zmC0LWrs-~j8?W5{7BU?>8bm4&LF zVFOT&5t4c?22Y?(X+V{F3 zpt@{^M4(CeKwc$-BhZWjJg)u&)NKrL#b!n(d6v!W9Q+)>PyuqjGf(ChvE*O``VFXv KVRL}U7G?kvdrMgW diff --git a/wulkanowy/wulkanowyApp.swift b/wulkanowy/App/wulkanowyApp.swift similarity index 87% rename from wulkanowy/wulkanowyApp.swift rename to wulkanowy/App/wulkanowyApp.swift index f9719d9..6f4f4be 100644 --- a/wulkanowy/wulkanowyApp.swift +++ b/wulkanowy/App/wulkanowyApp.swift @@ -13,7 +13,7 @@ import Combine struct wulkanowyApp: App { var body: some Scene { WindowGroup { - ContentView() + NavigationBarView() } } } diff --git a/wulkanowy/en.lproj/Localizable.strings b/wulkanowy/Resources/en.lproj/Localizable.strings similarity index 100% rename from wulkanowy/en.lproj/Localizable.strings rename to wulkanowy/Resources/en.lproj/Localizable.strings diff --git a/wulkanowy/pl-PL.lproj/Localizable.strings b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings similarity index 100% rename from wulkanowy/pl-PL.lproj/Localizable.strings rename to wulkanowy/Resources/pl-PL.lproj/Localizable.strings diff --git a/wulkanowy/Views/Content/Dashboard.swift b/wulkanowy/Views/Content/Dashboard.swift new file mode 100644 index 0000000..c270d9b --- /dev/null +++ b/wulkanowy/Views/Content/Dashboard.swift @@ -0,0 +1,34 @@ +// +// Dashboard.swift +// wulkanowy +// +// Created by Tomasz on 23/02/2021. +// + +import SwiftUI + +struct DashboardView: View { + var body: some View { + NavigationView { + VStack { + Text("You are not logged in") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + + } + } + } +} + + + +struct DashboardView_Previews: PreviewProvider { + static var previews: some View { + Group { + DashboardView() + } + .preferredColorScheme(.dark) + } +} + diff --git a/wulkanowy/ContentView.swift b/wulkanowy/Views/Login/LoginView.swift similarity index 95% rename from wulkanowy/ContentView.swift rename to wulkanowy/Views/Login/LoginView.swift index 26d5711..b3a8c41 100644 --- a/wulkanowy/ContentView.swift +++ b/wulkanowy/Views/Login/LoginView.swift @@ -14,7 +14,7 @@ enum AvailableEndpoints: String, CaseIterable { } -struct ContentView: View { +struct LoginView: View { @StateObject var vulcan: VulcanStore = VulcanStore.shared @@ -26,6 +26,7 @@ struct ContentView: View { @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 @@ -54,7 +55,7 @@ struct ContentView: View { } } else { print("success") - buttonValue = String(format: NSLocalizedString("success", comment: "loginButton")) + } } @@ -116,8 +117,6 @@ struct ContentView: View { .font(.largeTitle) .fontWeight(.semibold) } - .padding(.top, 50) - .padding(.bottom, -50) Spacer() @@ -182,16 +181,16 @@ struct ContentView: View { .frame(maxWidth: .infinity) .background(Color.accentColor.opacity(0.1)) .cornerRadius(cornerRadius) + Spacer() } .padding() } } - -struct ContentView_Previews: PreviewProvider { +struct LoginView_Previews: PreviewProvider { static var previews: some View { Group { - ContentView() + LoginView() } .preferredColorScheme(.dark) } diff --git a/wulkanowy/Views/Navigation/navigation.swift b/wulkanowy/Views/Navigation/navigation.swift new file mode 100644 index 0000000..dfd3696 --- /dev/null +++ b/wulkanowy/Views/Navigation/navigation.swift @@ -0,0 +1,31 @@ +// +// navigation.swift +// wulkanowy +// +// Created by Tomasz on 23/02/2021. +// + +import SwiftUI + +struct NavigationBarView: View { + var body: some View { + TabView { + DashboardView() + .tabItem { + Label("Grades", systemImage: "rosette") + .accessibility(label: Text("Grades")) + } + } + } +} + + +struct NavigationBarView_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationBarView() + } + .preferredColorScheme(.dark) + } +} + From 002658499a940d1be07192f9ca30c7d9f4b4574a Mon Sep 17 00:00:00 2001 From: Pengwius Date: Wed, 24 Feb 2021 12:27:12 +0100 Subject: [PATCH 08/17] New views --- wulkanowy.xcodeproj/project.pbxproj | 24 +++++++++++--- wulkanowy/Views/Content/dashboard.swift | 33 +++++++++++++++++++ .../Content/{Dashboard.swift => exams.swift} | 16 ++++----- wulkanowy/Views/Content/grades.swift | 32 ++++++++++++++++++ wulkanowy/Views/Content/homework.swift | 32 ++++++++++++++++++ wulkanowy/Views/Content/more.swift | 32 ++++++++++++++++++ wulkanowy/Views/Login/LoginView.swift | 1 - wulkanowy/Views/Navigation/navigation.swift | 32 +++++++++++++++--- 8 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 wulkanowy/Views/Content/dashboard.swift rename wulkanowy/Views/Content/{Dashboard.swift => exams.swift} (59%) create mode 100644 wulkanowy/Views/Content/grades.swift create mode 100644 wulkanowy/Views/Content/homework.swift create mode 100644 wulkanowy/Views/Content/more.swift diff --git a/wulkanowy.xcodeproj/project.pbxproj b/wulkanowy.xcodeproj/project.pbxproj index a43548e..6049a31 100644 --- a/wulkanowy.xcodeproj/project.pbxproj +++ b/wulkanowy.xcodeproj/project.pbxproj @@ -7,9 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; 5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */ = {isa = PBXBuildFile; productRef = 5C9B6F4825D6C08D00C3F5F5 /* Sdk */; }; - 5CC2EAA525E516F100B6183E /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC2EAA425E516F100B6183E /* Dashboard.swift */; }; + 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 */; }; @@ -62,10 +66,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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 = ""; }; 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 = ""; }; + 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 = ""; }; @@ -155,7 +163,11 @@ 5CC2EAA325E516DD00B6183E /* Content */ = { isa = PBXGroup; children = ( - 5CC2EAA425E516F100B6183E /* Dashboard.swift */, + 5CC2EAA425E516F100B6183E /* dashboard.swift */, + 5C2D330F25E64F3C000253AC /* grades.swift */, + 5C2D331325E650EC000253AC /* exams.swift */, + 5C2D331725E651C4000253AC /* homework.swift */, + 5C2D331B25E651FB000253AC /* more.swift */, ); path = Content; sourceTree = ""; @@ -370,10 +382,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5C2D331425E650EC000253AC /* exams.swift in Sources */, 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */, + 5C2D331025E64F3C000253AC /* grades.swift in Sources */, + 5C2D331C25E651FB000253AC /* more.swift in Sources */, F4C6D90A2544E17400F8903A /* LoginView.swift in Sources */, 5CC2EAAE25E526B500B6183E /* navigation.swift in Sources */, - 5CC2EAA525E516F100B6183E /* Dashboard.swift in Sources */, + 5C2D331825E651C4000253AC /* homework.swift in Sources */, + 5CC2EAA525E516F100B6183E /* dashboard.swift in Sources */, F4C6D9082544E17400F8903A /* wulkanowyApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/wulkanowy/Views/Content/dashboard.swift b/wulkanowy/Views/Content/dashboard.swift new file mode 100644 index 0000000..d8acaa3 --- /dev/null +++ b/wulkanowy/Views/Content/dashboard.swift @@ -0,0 +1,33 @@ +// +// Dashboard.swift +// wulkanowy +// +// Created by Tomasz on 23/02/2021. +// + +import SwiftUI + +struct DashboardView: View { + 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/Dashboard.swift b/wulkanowy/Views/Content/exams.swift similarity index 59% rename from wulkanowy/Views/Content/Dashboard.swift rename to wulkanowy/Views/Content/exams.swift index c270d9b..147c4bc 100644 --- a/wulkanowy/Views/Content/Dashboard.swift +++ b/wulkanowy/Views/Content/exams.swift @@ -1,34 +1,32 @@ // -// Dashboard.swift +// attendance.swift // wulkanowy // -// Created by Tomasz on 23/02/2021. +// Created by Tomasz on 24/02/2021. // import SwiftUI -struct DashboardView: View { +struct ExamsView: View { var body: some View { NavigationView { VStack { - Text("You are not logged in") + Text("You are not logged in (Exams)") NavigationLink(destination: LoginView()) { Text("Log in") } - - } + }.padding() } } } -struct DashboardView_Previews: PreviewProvider { +struct ExamsView_Previews: PreviewProvider { static var previews: some View { Group { - DashboardView() + 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/more.swift b/wulkanowy/Views/Content/more.swift new file mode 100644 index 0000000..893845b --- /dev/null +++ b/wulkanowy/Views/Content/more.swift @@ -0,0 +1,32 @@ +// +// more.swift +// wulkanowy +// +// Created by Tomasz on 24/02/2021. +// + +import SwiftUI + +struct MoreView: View { + var body: some View { + NavigationView { + VStack { + Text("You are not logged in (more)") + NavigationLink(destination: LoginView()) { + Text("Log in") + } + }.padding() + } + } +} + + + +struct MoreView_Previews: PreviewProvider { + static var previews: some View { + Group { + MoreView() + } + .preferredColorScheme(.dark) + } +} diff --git a/wulkanowy/Views/Login/LoginView.swift b/wulkanowy/Views/Login/LoginView.swift index b3a8c41..c1b3b3c 100644 --- a/wulkanowy/Views/Login/LoginView.swift +++ b/wulkanowy/Views/Login/LoginView.swift @@ -181,7 +181,6 @@ struct LoginView: View { .frame(maxWidth: .infinity) .background(Color.accentColor.opacity(0.1)) .cornerRadius(cornerRadius) - Spacer() } .padding() } diff --git a/wulkanowy/Views/Navigation/navigation.swift b/wulkanowy/Views/Navigation/navigation.swift index dfd3696..4618476 100644 --- a/wulkanowy/Views/Navigation/navigation.swift +++ b/wulkanowy/Views/Navigation/navigation.swift @@ -9,11 +9,35 @@ import SwiftUI struct NavigationBarView: View { var body: some View { - TabView { + TabView() { DashboardView() - .tabItem { - Label("Grades", systemImage: "rosette") - .accessibility(label: Text("Grades")) + .tabItem { + Label("Dashboard", systemImage: "rectangle.on.rectangle") + .accessibility(label: Text("Dashboard")) + } + + GradesView() + .tabItem { + Label("Grades", systemImage: "rosette") + .accessibility(label: Text("Grades")) + } + + ExamsView() + .tabItem { + Label("Exams", systemImage: "calendar") + .accessibility(label: Text("Exams")) + } + + HomeworksView() + .tabItem { + Label("Homework", systemImage: "note.text") + .accessibility(label: Text("Homework")) + } + + MoreView() + .tabItem { + Label("More", systemImage: "ellipsis.circle") + .accessibility(label: Text("More")) } } } From 87556ea87070960e7c4bb8f5dce757708b4aad9a Mon Sep 17 00:00:00 2001 From: Pengwius Date: Fri, 26 Feb 2021 10:30:54 +0100 Subject: [PATCH 09/17] Translation --- wulkanowy/Resources/en.lproj/Localizable.strings | 8 ++++++++ wulkanowy/Resources/pl-PL.lproj/Localizable.strings | 8 ++++++++ wulkanowy/Views/Content/exams.swift | 2 +- wulkanowy/Views/Login/LoginView.swift | 3 +-- wulkanowy/Views/Navigation/navigation.swift | 10 +++++----- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/wulkanowy/Resources/en.lproj/Localizable.strings b/wulkanowy/Resources/en.lproj/Localizable.strings index 0b158b8..862a544 100644 --- a/wulkanowy/Resources/en.lproj/Localizable.strings +++ b/wulkanowy/Resources/en.lproj/Localizable.strings @@ -6,6 +6,7 @@ */ +//LOGIN SCREEN "loginTitle" = "Log In"; "token" = "Token"; "symbol" = "Symbol"; @@ -16,3 +17,10 @@ "wrongPin" = "Wrong pin"; "invalidData" = "Wrong token, symbol or pin"; "success" = "Success"; + +//NAVIGATION +"dashboardButton" = "Dashboard"; +"gradesButton" = "Grades"; +"examsButton" = "Exams"; +"homeworkButton" = "Homework"; +"moreButton" = "More"; diff --git a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings index 6ca907e..141c13e 100644 --- a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings +++ b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings @@ -6,6 +6,7 @@ */ +//LOGIN SCREEN "loginTitle" = "Logowanie"; "token" = "Token"; "symbol" = "Symbol"; @@ -17,3 +18,10 @@ "invalidData" = "Zły token, symbol lub pin"; "success" = "Sukces"; +//NAVIGATION +"dashboardButton" = "Start"; +"gradesButton" = "Oceny"; +"examsButton" = "Sprawdziany"; +"homeworkButton" = "Zadania"; +"moreButton" = "Więcej"; + diff --git a/wulkanowy/Views/Content/exams.swift b/wulkanowy/Views/Content/exams.swift index 147c4bc..01a764e 100644 --- a/wulkanowy/Views/Content/exams.swift +++ b/wulkanowy/Views/Content/exams.swift @@ -11,7 +11,7 @@ struct ExamsView: View { var body: some View { NavigationView { VStack { - Text("You are not logged in (Exams)") + Text("You are not logged in (exams)") NavigationLink(destination: LoginView()) { Text("Log in") } diff --git a/wulkanowy/Views/Login/LoginView.swift b/wulkanowy/Views/Login/LoginView.swift index c1b3b3c..0ec6247 100644 --- a/wulkanowy/Views/Login/LoginView.swift +++ b/wulkanowy/Views/Login/LoginView.swift @@ -104,7 +104,6 @@ struct LoginView: View { var body: some View { VStack { - VStack { Image("wulkanowy") .renderingMode(.template) .resizable() @@ -116,7 +115,6 @@ struct LoginView: View { Text("loginTitle") .font(.largeTitle) .fontWeight(.semibold) - } Spacer() @@ -183,6 +181,7 @@ struct LoginView: View { .cornerRadius(cornerRadius) } .padding() + Spacer() } } diff --git a/wulkanowy/Views/Navigation/navigation.swift b/wulkanowy/Views/Navigation/navigation.swift index 4618476..58faede 100644 --- a/wulkanowy/Views/Navigation/navigation.swift +++ b/wulkanowy/Views/Navigation/navigation.swift @@ -12,31 +12,31 @@ struct NavigationBarView: View { TabView() { DashboardView() .tabItem { - Label("Dashboard", systemImage: "rectangle.on.rectangle") + Label("dashboardButton", systemImage: "rectangle.on.rectangle") .accessibility(label: Text("Dashboard")) } GradesView() .tabItem { - Label("Grades", systemImage: "rosette") + Label("gradesButton", systemImage: "rosette") .accessibility(label: Text("Grades")) } ExamsView() .tabItem { - Label("Exams", systemImage: "calendar") + Label("examsButton", systemImage: "calendar") .accessibility(label: Text("Exams")) } HomeworksView() .tabItem { - Label("Homework", systemImage: "note.text") + Label("homeworkButton", systemImage: "note.text") .accessibility(label: Text("Homework")) } MoreView() .tabItem { - Label("More", systemImage: "ellipsis.circle") + Label("moreButton", systemImage: "ellipsis.circle") .accessibility(label: Text("More")) } } From 2abb4783665ed45da4d51a638bb502ad0b63f385 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Fri, 26 Feb 2021 11:07:37 +0100 Subject: [PATCH 10/17] More views --- wulkanowy.xcodeproj/project.pbxproj | 16 +++++++++++++ wulkanowy/Views/Content/about.swift | 26 +++++++++++++++++++++ wulkanowy/Views/Content/messages.swift | 32 ++++++++++++++++++++++++++ wulkanowy/Views/Content/more.swift | 26 ++++++++++++++++----- wulkanowy/Views/Content/notes.swift | 32 ++++++++++++++++++++++++++ wulkanowy/Views/Content/settings.swift | 25 ++++++++++++++++++++ 6 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 wulkanowy/Views/Content/about.swift create mode 100644 wulkanowy/Views/Content/messages.swift create mode 100644 wulkanowy/Views/Content/notes.swift create mode 100644 wulkanowy/Views/Content/settings.swift diff --git a/wulkanowy.xcodeproj/project.pbxproj b/wulkanowy.xcodeproj/project.pbxproj index 6049a31..ffa5728 100644 --- a/wulkanowy.xcodeproj/project.pbxproj +++ b/wulkanowy.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; 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 */; }; @@ -66,6 +70,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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 = ""; }; 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 = ""; }; @@ -168,6 +176,10 @@ 5C2D331325E650EC000253AC /* exams.swift */, 5C2D331725E651C4000253AC /* homework.swift */, 5C2D331B25E651FB000253AC /* more.swift */, + 5C1794B325E8FDFB007AD91A /* messages.swift */, + 5C1794B725E8FE08007AD91A /* notes.swift */, + 5C1794BB25E8FE19007AD91A /* settings.swift */, + 5C1794BF25E8FE27007AD91A /* about.swift */, ); path = Content; sourceTree = ""; @@ -384,12 +396,16 @@ files = ( 5C2D331425E650EC000253AC /* exams.swift in Sources */, 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */, + 5C1794BC25E8FE19007AD91A /* settings.swift in Sources */, 5C2D331025E64F3C000253AC /* grades.swift in Sources */, 5C2D331C25E651FB000253AC /* more.swift in Sources */, + 5C1794C025E8FE27007AD91A /* about.swift in Sources */, F4C6D90A2544E17400F8903A /* LoginView.swift in Sources */, 5CC2EAAE25E526B500B6183E /* navigation.swift in Sources */, 5C2D331825E651C4000253AC /* homework.swift in Sources */, + 5C1794B425E8FDFB007AD91A /* messages.swift in Sources */, 5CC2EAA525E516F100B6183E /* dashboard.swift in Sources */, + 5C1794B825E8FE08007AD91A /* notes.swift in Sources */, F4C6D9082544E17400F8903A /* wulkanowyApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/wulkanowy/Views/Content/about.swift b/wulkanowy/Views/Content/about.swift new file mode 100644 index 0000000..814a3ae --- /dev/null +++ b/wulkanowy/Views/Content/about.swift @@ -0,0 +1,26 @@ +// +// about.swift +// wulkanowy +// +// Created by Tomasz on 26/02/2021. +// + +import SwiftUI + +struct AboutView: View { + var body: some View { + Text("Here are some info about application (in my imagination)") + } +} + + + +struct AboutView_Previews: PreviewProvider { + static var previews: some View { + Group { + AboutView() + } + .preferredColorScheme(.dark) + } +} + 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 index 893845b..9e1689f 100644 --- a/wulkanowy/Views/Content/more.swift +++ b/wulkanowy/Views/Content/more.swift @@ -10,13 +10,27 @@ import SwiftUI struct MoreView: View { var body: some View { NavigationView { - VStack { - Text("You are not logged in (more)") - NavigationLink(destination: LoginView()) { - Text("Log in") - } - }.padding() + Form { + Section { + NavigationLink(destination: MessagesView()) { + Text("Messages") + } + NavigationLink(destination: NotesView()) { + Text("Notes and achievements") + } } + + Section { + NavigationLink(destination: SettingsView()) { + Text("Settings") + } + NavigationLink(destination: AboutView()) { + Text("About") + } + } + } + .navigationBarTitle("Wulkanowy - more", displayMode: .inline) + } } } 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) + } +} From 1483dd8592317368287435cbe09d7ef11830da97 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Fri, 26 Feb 2021 11:26:35 +0100 Subject: [PATCH 11/17] Translations & icons --- wulkanowy/Resources/en.lproj/Localizable.strings | 6 ++++++ .../Resources/pl-PL.lproj/Localizable.strings | 5 +++++ wulkanowy/Views/Content/more.swift | 14 +++++++++----- wulkanowy/Views/Navigation/navigation.swift | 10 +++++----- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/wulkanowy/Resources/en.lproj/Localizable.strings b/wulkanowy/Resources/en.lproj/Localizable.strings index 862a544..bd2e486 100644 --- a/wulkanowy/Resources/en.lproj/Localizable.strings +++ b/wulkanowy/Resources/en.lproj/Localizable.strings @@ -24,3 +24,9 @@ "examsButton" = "Exams"; "homeworkButton" = "Homework"; "moreButton" = "More"; + +//MORE +"messagesButton" = "Messages"; +"notesButton" = "Notes and achievements"; +"settingsButton" = "Settings"; +"aboutButton" = "About"; diff --git a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings index 141c13e..0096c50 100644 --- a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings +++ b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings @@ -25,3 +25,8 @@ "homeworkButton" = "Zadania"; "moreButton" = "Więcej"; +//MORE +"messagesButton" = "Wiadomości"; +"notesButton" = "Uwagi i osiągnięcia"; +"settingsButton" = "Ustawienia"; +"aboutButton" = "O aplikacji"; diff --git a/wulkanowy/Views/Content/more.swift b/wulkanowy/Views/Content/more.swift index 9e1689f..cfd7ef5 100644 --- a/wulkanowy/Views/Content/more.swift +++ b/wulkanowy/Views/Content/more.swift @@ -13,23 +13,27 @@ struct MoreView: View { Form { Section { NavigationLink(destination: MessagesView()) { - Text("Messages") + Label("messagesButton", systemImage: "envelope") + .accessibility(label: Text("messagesButton")) } NavigationLink(destination: NotesView()) { - Text("Notes and achievements") + Label("notesButton", systemImage: "graduationcap") + .accessibility(label: Text("notesButton")) } } Section { NavigationLink(destination: SettingsView()) { - Text("Settings") + Label("settingsButton", systemImage: "gear") + .accessibility(label: Text("settingsButton")) } NavigationLink(destination: AboutView()) { - Text("About") + Label("aboutButton", systemImage: "info.circle") + .accessibility(label: Text("aboutButton")) } } } - .navigationBarTitle("Wulkanowy - more", displayMode: .inline) + .navigationBarTitle("moreButton", displayMode: .inline) } } } diff --git a/wulkanowy/Views/Navigation/navigation.swift b/wulkanowy/Views/Navigation/navigation.swift index 58faede..76158ae 100644 --- a/wulkanowy/Views/Navigation/navigation.swift +++ b/wulkanowy/Views/Navigation/navigation.swift @@ -13,31 +13,31 @@ struct NavigationBarView: View { DashboardView() .tabItem { Label("dashboardButton", systemImage: "rectangle.on.rectangle") - .accessibility(label: Text("Dashboard")) + .accessibility(label: Text("dashboardButton")) } GradesView() .tabItem { Label("gradesButton", systemImage: "rosette") - .accessibility(label: Text("Grades")) + .accessibility(label: Text("gradesButton")) } ExamsView() .tabItem { Label("examsButton", systemImage: "calendar") - .accessibility(label: Text("Exams")) + .accessibility(label: Text("examsButton")) } HomeworksView() .tabItem { Label("homeworkButton", systemImage: "note.text") - .accessibility(label: Text("Homework")) + .accessibility(label: Text("homeworkButton")) } MoreView() .tabItem { Label("moreButton", systemImage: "ellipsis.circle") - .accessibility(label: Text("More")) + .accessibility(label: Text("moreButton")) } } } From ee6569732477fdc5c83a2d94859eb09634a71758 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Fri, 26 Feb 2021 14:42:16 +0100 Subject: [PATCH 12/17] Saving private key --- wulkanowy.xcodeproj/project.pbxproj | 17 +++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++++ .../Resources/en.lproj/Localizable.strings | 1 + .../Resources/pl-PL.lproj/Localizable.strings | 1 + wulkanowy/Views/Login/LoginView.swift | 2 +- wulkanowy/VulcanStore.swift | 38 ++++++++++++++++--- 6 files changed, 62 insertions(+), 6 deletions(-) diff --git a/wulkanowy.xcodeproj/project.pbxproj b/wulkanowy.xcodeproj/project.pbxproj index ffa5728..0e69906 100644 --- a/wulkanowy.xcodeproj/project.pbxproj +++ b/wulkanowy.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 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 */; }; 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 */; }; @@ -106,6 +107,7 @@ files = ( 5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */, 5CCAE31625DA4CDD00D87580 /* OpenSSL in Frameworks */, + 5C1794CD25E90DBD007AD91A /* KeychainAccess in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -275,6 +277,7 @@ packageProductDependencies = ( 5C9B6F4825D6C08D00C3F5F5 /* Sdk */, 5CCAE31525DA4CDD00D87580 /* OpenSSL */, + 5C1794CC25E90DBD007AD91A /* KeychainAccess */, ); productName = wulkanowy; productReference = F4C6D9042544E17400F8903A /* wulkanowy.app */; @@ -350,6 +353,7 @@ mainGroup = F4C6D8FB2544E17300F8903A; packageReferences = ( 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */, + 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */, ); productRefGroup = F4C6D9052544E17400F8903A /* Products */; projectDirPath = ""; @@ -736,6 +740,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.2.1; + }; + }; 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/krzyzanowskim/OpenSSL"; @@ -747,6 +759,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 5C1794CC25E90DBD007AD91A /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; 5C9B6F4825D6C08D00C3F5F5 /* Sdk */ = { isa = XCSwiftPackageProductDependency; productName = Sdk; diff --git a/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b88114c..ecc9662 100644 --- a/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "KeychainAccess", + "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", + "state": { + "branch": null, + "revision": "654d52d30f3dd4592e944c3e0bccb53178c992f6", + "version": "4.2.1" + } + }, { "package": "OpenSSL", "repositoryURL": "https://github.com/krzyzanowskim/OpenSSL", diff --git a/wulkanowy/Resources/en.lproj/Localizable.strings b/wulkanowy/Resources/en.lproj/Localizable.strings index bd2e486..fc7683f 100644 --- a/wulkanowy/Resources/en.lproj/Localizable.strings +++ b/wulkanowy/Resources/en.lproj/Localizable.strings @@ -11,6 +11,7 @@ "token" = "Token"; "symbol" = "Symbol"; "pin" = "Pin"; +"deviceName" = "Device name"; "loginButton" = "Login"; "wrongToken" = "Wrong token"; "wrongSymbol" = "Wrong symbol"; diff --git a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings index 0096c50..942c416 100644 --- a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings +++ b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings @@ -11,6 +11,7 @@ "token" = "Token"; "symbol" = "Symbol"; "pin" = "Pin"; +"deviceName" = "Nazwa urządzenia"; "loginButton" = "Zaloguj"; "wrongToken" = "Zły token"; "wrongSymbol" = "Zły symbol"; diff --git a/wulkanowy/Views/Login/LoginView.swift b/wulkanowy/Views/Login/LoginView.swift index 0ec6247..7e1a2b3 100644 --- a/wulkanowy/Views/Login/LoginView.swift +++ b/wulkanowy/Views/Login/LoginView.swift @@ -156,7 +156,7 @@ struct LoginView: View { .stroke(setColor(input: "pin"), lineWidth: 2) ) - TextField("Device name", text: $deviceModel) + TextField("deviceName", text: $deviceModel) .autocapitalization(.none) .disableAutocorrection(true) .font(Font.body.weight(Font.Weight.medium)) diff --git a/wulkanowy/VulcanStore.swift b/wulkanowy/VulcanStore.swift index 99ba8c3..b173e05 100644 --- a/wulkanowy/VulcanStore.swift +++ b/wulkanowy/VulcanStore.swift @@ -7,27 +7,55 @@ 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) { error in - if error == nil { - // Success - save certificate + 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 { - // Error - discard or try again + 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"] = base + + let token = keychain["privateKey"] + print("Encoded: \(token)") + } } completionHandler(error) From b17cd82d21561f77c3ab2a49da96f57ab8b8f1c0 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Fri, 26 Feb 2021 14:45:32 +0100 Subject: [PATCH 13/17] Fix --- wulkanowy/VulcanStore.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wulkanowy/VulcanStore.swift b/wulkanowy/VulcanStore.swift index b173e05..aa6421f 100644 --- a/wulkanowy/VulcanStore.swift +++ b/wulkanowy/VulcanStore.swift @@ -51,10 +51,10 @@ final class VulcanStore: ObservableObject { if let base64Encoded = utf8str?.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) { let keychain = Keychain() - keychain[string: "privateKey"] = base + keychain[string: "privateKey"] = base64Encoded - let token = keychain["privateKey"] - print("Encoded: \(token)") + let key = keychain["privateKey"] + print("Encoded: \(key)") } } From 47151e602df70660ba5738ff34c8d06c9f8251b2 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Sat, 27 Feb 2021 14:44:22 +0100 Subject: [PATCH 14/17] About app view & splash screen --- wulkanowy.xcodeproj/project.pbxproj | 25 +++ .../xcshareddata/swiftpm/Package.resolved | 9 + wulkanowy/.DS_Store | Bin 6148 -> 6148 bytes .../ComponentColor.colorset/Contents.json | 6 +- .../Assets.xcassets/Colours/Contents.json | 3 + .../LaunchColor.colorset/Contents.json | 38 ++++ .../customControlColor.colorset/Contents.json | 33 +++ .../Logo.imageset/Contents.json | 21 ++ .../Assets.xcassets/Logo.imageset/logo.png | Bin 0 -> 13885 bytes .../splash.imageset/Contents.json | 24 +++ .../splash.imageset/wulkanowy-logo.png | Bin 0 -> 11726 bytes .../wulkanowy.imageset/Contents.json | 13 +- .../{wulkanowy-1.svg => wulkanowy.svg} | 0 wulkanowy/Info.plist | 7 +- wulkanowy/Views/Content/about.swift | 100 ++++++++- wulkanowy/Views/Content/licenses.swift | 190 ++++++++++++++++++ wulkanowy/Views/ghImage.swift | 123 ++++++++++++ wulkanowy/VulcanStore.swift | 2 +- 18 files changed, 587 insertions(+), 7 deletions(-) create mode 100644 wulkanowy/Assets.xcassets/Colours/LaunchColor.colorset/Contents.json create mode 100644 wulkanowy/Assets.xcassets/Colours/customControlColor.colorset/Contents.json create mode 100644 wulkanowy/Assets.xcassets/Logo.imageset/Contents.json create mode 100644 wulkanowy/Assets.xcassets/Logo.imageset/logo.png create mode 100644 wulkanowy/Assets.xcassets/splash.imageset/Contents.json create mode 100644 wulkanowy/Assets.xcassets/splash.imageset/wulkanowy-logo.png rename wulkanowy/Assets.xcassets/wulkanowy.imageset/{wulkanowy-1.svg => wulkanowy.svg} (100%) create mode 100644 wulkanowy/Views/Content/licenses.swift create mode 100644 wulkanowy/Views/ghImage.swift diff --git a/wulkanowy.xcodeproj/project.pbxproj b/wulkanowy.xcodeproj/project.pbxproj index 0e69906..11b2768 100644 --- a/wulkanowy.xcodeproj/project.pbxproj +++ b/wulkanowy.xcodeproj/project.pbxproj @@ -12,11 +12,14 @@ 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 */; }; @@ -75,11 +78,13 @@ 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 = ""; }; @@ -108,6 +113,7 @@ 5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */, 5CCAE31625DA4CDD00D87580 /* OpenSSL in Frameworks */, 5C1794CD25E90DBD007AD91A /* KeychainAccess in Frameworks */, + 5C89C90625EA7996000B5816 /* SwiftUIEKtensions in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -142,6 +148,7 @@ 5CC2EAAC25E5269E00B6183E /* Navigation */, 5CC2EAA325E516DD00B6183E /* Content */, 5C1B849F25E1B7A30074F29D /* Login */, + 5C1CFA7925EA32AE0047286F /* ghImage.swift */, ); path = Views; sourceTree = ""; @@ -182,6 +189,7 @@ 5C1794B725E8FE08007AD91A /* notes.swift */, 5C1794BB25E8FE19007AD91A /* settings.swift */, 5C1794BF25E8FE27007AD91A /* about.swift */, + 5C89C8F425EA6AA4000B5816 /* licenses.swift */, ); path = Content; sourceTree = ""; @@ -278,6 +286,7 @@ 5C9B6F4825D6C08D00C3F5F5 /* Sdk */, 5CCAE31525DA4CDD00D87580 /* OpenSSL */, 5C1794CC25E90DBD007AD91A /* KeychainAccess */, + 5C89C90525EA7996000B5816 /* SwiftUIEKtensions */, ); productName = wulkanowy; productReference = F4C6D9042544E17400F8903A /* wulkanowy.app */; @@ -354,6 +363,7 @@ packageReferences = ( 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */, 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */, + 5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */, ); productRefGroup = F4C6D9052544E17400F8903A /* Products */; projectDirPath = ""; @@ -400,11 +410,13 @@ files = ( 5C2D331425E650EC000253AC /* exams.swift in Sources */, 5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */, + 5C1CFA7A25EA32AE0047286F /* ghImage.swift in Sources */, 5C1794BC25E8FE19007AD91A /* settings.swift in Sources */, 5C2D331025E64F3C000253AC /* grades.swift in Sources */, 5C2D331C25E651FB000253AC /* more.swift in Sources */, 5C1794C025E8FE27007AD91A /* about.swift in Sources */, F4C6D90A2544E17400F8903A /* LoginView.swift in Sources */, + 5C89C8F525EA6AA4000B5816 /* licenses.swift in Sources */, 5CC2EAAE25E526B500B6183E /* navigation.swift in Sources */, 5C2D331825E651C4000253AC /* homework.swift in Sources */, 5C1794B425E8FDFB007AD91A /* messages.swift in Sources */, @@ -748,6 +760,14 @@ 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"; @@ -764,6 +784,11 @@ package = 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; + 5C89C90525EA7996000B5816 /* SwiftUIEKtensions */ = { + isa = XCSwiftPackageProductDependency; + package = 5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */; + productName = SwiftUIEKtensions; + }; 5C9B6F4825D6C08D00C3F5F5 /* Sdk */ = { isa = XCSwiftPackageProductDependency; productName = Sdk; diff --git a/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ecc9662..999d53a 100644 --- a/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/wulkanowy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,6 +18,15 @@ "revision": "389296819a8d025ac10ddc9f22135a5518991fdc", "version": "1.1.180" } + }, + { + "package": "SwiftUIEKtensions", + "repositoryURL": "https://github.com/EnesKaraosman/SwiftUIEKtensions", + "state": { + "branch": null, + "revision": "cafd7ad82796528b392107874f07a7279dec7fcd", + "version": "0.1.8" + } } ] }, diff --git a/wulkanowy/.DS_Store b/wulkanowy/.DS_Store index 4624469316bba9308838bc3e563e008ba7c5d218..141b20df7c16d790e6405e1c63bc628545142405 100644 GIT binary patch delta 83 zcmZoMXfc=|#>B)qu~2NHo+2ab#DLw41(+BaSts)_I!@+il;0$^0Ug9E?CIpftng0Ff=s08KO!Y5)KL delta 66 zcmZoMXfc=|#>B`mu~2NHo+2a5#DLw5ER%Vd95+`p^{{Si&|}=p&cV+CRI%BR`8)Gu Uei2I!1|VQ$U|`xDAhLxS05p^jNB{r; diff --git a/wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json b/wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json index 9fea250..135c479 100644 --- a/wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json +++ b/wulkanowy/Assets.xcassets/Colours/ComponentColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.184", - "green" : "0.184", - "red" : "0.827" + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0xD2" } }, "idiom" : "universal" diff --git a/wulkanowy/Assets.xcassets/Colours/Contents.json b/wulkanowy/Assets.xcassets/Colours/Contents.json index 73c0059..7f73912 100644 --- a/wulkanowy/Assets.xcassets/Colours/Contents.json +++ b/wulkanowy/Assets.xcassets/Colours/Contents.json @@ -2,5 +2,8 @@ "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 0000000000000000000000000000000000000000..291b06ff45014e7e21bacf150cd6e3d401d6910c GIT binary patch literal 13885 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 Q8UR3tY!5y=;Qr750rau+E&u=k literal 0 HcmV?d00001 diff --git a/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json index c93d7f9..6b07939 100644 --- a/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json +++ b/wulkanowy/Assets.xcassets/wulkanowy.imageset/Contents.json @@ -1,13 +1,24 @@ { "images" : [ { - "filename" : "wulkanowy-1.svg", + "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-1.svg b/wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg similarity index 100% rename from wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy-1.svg rename to wulkanowy/Assets.xcassets/wulkanowy.imageset/wulkanowy.svg diff --git a/wulkanowy/Info.plist b/wulkanowy/Info.plist index 3c0a77b..74818e2 100644 --- a/wulkanowy/Info.plist +++ b/wulkanowy/Info.plist @@ -45,7 +45,12 @@ UIApplicationSupportsIndirectInputEvents UILaunchScreen - + + UIColorName + LaunchColor + UIImageName + splash + UIRequiredDeviceCapabilities armv7 diff --git a/wulkanowy/Views/Content/about.swift b/wulkanowy/Views/Content/about.swift index 814a3ae..0aa375d 100644 --- a/wulkanowy/Views/Content/about.swift +++ b/wulkanowy/Views/Content/about.swift @@ -5,11 +5,109 @@ // 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 { - Text("Here are some info about application (in my imagination)") + Form { + Section { + DisclosureGroup("App version") { + Text("You actually version is alpha 0.1") + .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("Licenses") + } + + Link("FAQ", destination: URL(string: "https://wulkanowy.github.io/czesto-zadawane-pytania")!) + .foregroundColor(Color("customControlColor")) + + Link("Privacy policy", destination: URL(string: "https://wulkanowy.github.io/polityka-prywatnosci")!) + .foregroundColor(Color("customControlColor")) + } + Section { + Link("Join the Discord serwer", destination: URL(string: "https://discord.com/invite/vccAQBr")!) + .foregroundColor(Color("customControlColor")) + + Link("Facebook fanpage", 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("Report a bug") + .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")) + + Link("Donate us!", destination: URL(string: "https://www.paypal.com/paypalme/wulkanowy")!) + .foregroundColor(Color("customControlColor")) + } } } diff --git a/wulkanowy/Views/Content/licenses.swift b/wulkanowy/Views/Content/licenses.swift new file mode 100644 index 0000000..830132c --- /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("No license") + .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/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 index aa6421f..5f4737c 100644 --- a/wulkanowy/VulcanStore.swift +++ b/wulkanowy/VulcanStore.swift @@ -54,7 +54,7 @@ final class VulcanStore: ObservableObject { keychain[string: "privateKey"] = base64Encoded let key = keychain["privateKey"] - print("Encoded: \(key)") + print("Encoded: \(String(describing: key))") } } From e4bff041a4cd0fec8a2a25d1205d39c29b1479e7 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Sat, 27 Feb 2021 20:20:30 +0100 Subject: [PATCH 15/17] Deleting paypal --- wulkanowy/Views/Content/about.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/wulkanowy/Views/Content/about.swift b/wulkanowy/Views/Content/about.swift index 0aa375d..16b4785 100644 --- a/wulkanowy/Views/Content/about.swift +++ b/wulkanowy/Views/Content/about.swift @@ -104,9 +104,6 @@ struct AboutView: View { Link("Github", destination: URL(string: "https://github.com/wulkanowy/wulkanowy-ios")!) .foregroundColor(Color("customControlColor")) - - Link("Donate us!", destination: URL(string: "https://www.paypal.com/paypalme/wulkanowy")!) - .foregroundColor(Color("customControlColor")) } } } From 5b2bda83f800bd68338981e730f34f5514674266 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Sat, 27 Feb 2021 20:36:40 +0100 Subject: [PATCH 16/17] Translations --- .../Resources/en.lproj/Localizable.strings | 14 ++++++++++++++ .../Resources/pl-PL.lproj/Localizable.strings | 14 ++++++++++++++ wulkanowy/Views/Content/about.swift | 18 +++++++++--------- wulkanowy/Views/Content/licenses.swift | 2 +- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/wulkanowy/Resources/en.lproj/Localizable.strings b/wulkanowy/Resources/en.lproj/Localizable.strings index fc7683f..ea3da8a 100644 --- a/wulkanowy/Resources/en.lproj/Localizable.strings +++ b/wulkanowy/Resources/en.lproj/Localizable.strings @@ -31,3 +31,17 @@ "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 index 942c416..68cee08 100644 --- a/wulkanowy/Resources/pl-PL.lproj/Localizable.strings +++ b/wulkanowy/Resources/pl-PL.lproj/Localizable.strings @@ -31,3 +31,17 @@ "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 index 16b4785..04713c3 100644 --- a/wulkanowy/Views/Content/about.swift +++ b/wulkanowy/Views/Content/about.swift @@ -19,12 +19,12 @@ struct AboutView: View { var body: some View { Form { Section { - DisclosureGroup("App version") { - Text("You actually version is alpha 0.1") + DisclosureGroup("appVersion") { + Text("appVersionContent") .font(.system(.body, design: .monospaced)) } - DisclosureGroup("Contributors") { + DisclosureGroup("contributors") { HStack { AsyncImage(url: URL(string: "https://avatars.githubusercontent.com/u/55411338?s=460&v=4")!, placeholder: { Image(systemName: "circle.dashed") }, @@ -54,20 +54,20 @@ struct AboutView: View { } NavigationLink(destination: LicensesView()) { - Text("Licenses") + Text("licensesButton") } Link("FAQ", destination: URL(string: "https://wulkanowy.github.io/czesto-zadawane-pytania")!) .foregroundColor(Color("customControlColor")) - Link("Privacy policy", destination: URL(string: "https://wulkanowy.github.io/polityka-prywatnosci")!) + Link("privacyPolicy", destination: URL(string: "https://wulkanowy.github.io/polityka-prywatnosci")!) .foregroundColor(Color("customControlColor")) } Section { - Link("Join the Discord serwer", destination: URL(string: "https://discord.com/invite/vccAQBr")!) + Link("discordButton", destination: URL(string: "https://discord.com/invite/vccAQBr")!) .foregroundColor(Color("customControlColor")) - Link("Facebook fanpage", destination: URL(string: "https://www.facebook.com/wulkanowy")!) + Link("fbButton", destination: URL(string: "https://www.facebook.com/wulkanowy")!) .foregroundColor(Color("customControlColor")) Link("Reddit", destination: URL(string: "https://www.reddit.com/r/wulkanowy/")!) @@ -86,7 +86,7 @@ struct AboutView: View { } }) { HStack { - Text("Report a bug") + Text("reportBug") .foregroundColor(Color("customControlColor")) } } @@ -99,7 +99,7 @@ struct AboutView: View { } } - Link("Homepage", destination: URL(string: "https://wulkanowy.github.io/")!) + Link("homepage", destination: URL(string: "https://wulkanowy.github.io/")!) .foregroundColor(Color("customControlColor")) Link("Github", destination: URL(string: "https://github.com/wulkanowy/wulkanowy-ios")!) diff --git a/wulkanowy/Views/Content/licenses.swift b/wulkanowy/Views/Content/licenses.swift index 830132c..bc12005 100644 --- a/wulkanowy/Views/Content/licenses.swift +++ b/wulkanowy/Views/Content/licenses.swift @@ -169,7 +169,7 @@ struct LicensesView: View { // SwiftUIEKtensions DisclosureGroup("SwiftUIEKtensions") { - Text("No license") + Text("noLicence") .font(.system(.body, design: .monospaced)) .onTapGesture { guard let url = SwiftUIEKtensionsURL else { return } From 4a7319aab8c0618ffd525aa3bc74efd890d61168 Mon Sep 17 00:00:00 2001 From: Pengwius Date: Tue, 2 Mar 2021 11:05:42 +0100 Subject: [PATCH 17/17] Request to /api/mobile/register/hebe --- .../Sdk/Extensions/getSignatures.swift | 50 ++++++++++++++++ sdk/Sources/Sdk/Sdk.swift | 59 ++++++++++++++++++- sdk/Sources/Sdk/data/luckyNumber.swift | 12 ++++ sdk/Sources/Sdk/signer.swift | 1 + wulkanowy/Views/Content/dashboard.swift | 11 ++++ wulkanowy/Views/Login/LoginView.swift | 1 - wulkanowy/VulcanStore.swift | 4 -- wulkanowyTests/wulkanowyTests.swift | 1 + 8 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 sdk/Sources/Sdk/Extensions/getSignatures.swift create mode 100644 sdk/Sources/Sdk/data/luckyNumber.swift 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 index 0f430df..2065973 100644 --- a/sdk/Sources/Sdk/Sdk.swift +++ b/sdk/Sources/Sdk/Sdk.swift @@ -9,6 +9,7 @@ import Foundation import Combine import os +import KeychainAccess @available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) public class Sdk { @@ -17,6 +18,9 @@ public class Sdk { private let loggerSubsystem: String = "com.wulkanowy-ios.Sdk" private var cancellables: Set = [] + var firebaseToken: String! + var endpointURL: String! + public let certificate: X509 // MARK: - Init @@ -136,6 +140,7 @@ public class Sdk { let parsedError = self.parseResponse(response) { completionHandler(parsedError) } else { + self.getStudents(symbol: symbol, deviceModel: deviceModel) completionHandler(nil) } }) @@ -155,6 +160,8 @@ public class Sdk { /// - 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)? @@ -179,7 +186,10 @@ public class Sdk { return dateFormatter.string(from: now) } - + + let keychain = Keychain() + keychain[string: "keyFingerprint"] = keyFingerprint + // Body let body: [String: Encodable?] = [ "AppName": "DzienniczekPlus 2.0", @@ -201,7 +211,7 @@ public class Sdk { "Timestamp": now.millisecondsSince1970, "TimestampFormatted": "\(timestampFormatted) GMT" ] - + request.httpBody = try? JSONSerialization.data(withJSONObject: body) request.allHTTPHeaderFields = [ @@ -211,9 +221,54 @@ public class Sdk { ] 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 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 index e87f9c9..9d2a49d 100644 --- a/sdk/Sources/Sdk/signer.swift +++ b/sdk/Sources/Sdk/signer.swift @@ -7,6 +7,7 @@ import Foundation import CryptoKit +import KeychainAccess @available (iOS 14, macOS 11, watchOS 7, tvOS 14, *) public extension Sdk { diff --git a/wulkanowy/Views/Content/dashboard.swift b/wulkanowy/Views/Content/dashboard.swift index d8acaa3..a937ac3 100644 --- a/wulkanowy/Views/Content/dashboard.swift +++ b/wulkanowy/Views/Content/dashboard.swift @@ -6,8 +6,19 @@ // 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 { diff --git a/wulkanowy/Views/Login/LoginView.swift b/wulkanowy/Views/Login/LoginView.swift index 7e1a2b3..12cfed2 100644 --- a/wulkanowy/Views/Login/LoginView.swift +++ b/wulkanowy/Views/Login/LoginView.swift @@ -55,7 +55,6 @@ struct LoginView: View { } } else { print("success") - } } diff --git a/wulkanowy/VulcanStore.swift b/wulkanowy/VulcanStore.swift index 5f4737c..8a16a1e 100644 --- a/wulkanowy/VulcanStore.swift +++ b/wulkanowy/VulcanStore.swift @@ -52,9 +52,6 @@ final class VulcanStore: ObservableObject { if let base64Encoded = utf8str?.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) { let keychain = Keychain() keychain[string: "privateKey"] = base64Encoded - - let key = keychain["privateKey"] - print("Encoded: \(String(describing: key))") } } @@ -62,4 +59,3 @@ final class VulcanStore: ObservableObject { } } } - 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 {