Resolve conflicts

This commit is contained in:
Pengwius 2021-03-02 11:15:00 +01:00
commit 08c27510ce
51 changed files with 2442 additions and 66 deletions

55
.gitignore vendored
View file

@ -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

5
sdk/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

28
sdk/Package.swift Normal file
View file

@ -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"]),
]
)

3
sdk/README.md Normal file
View file

@ -0,0 +1,3 @@
# Sdk
A description of this package.

View file

@ -0,0 +1,27 @@
//
// File.swift
//
//
// Created by Tomasz (copied from rrroyal/vulcan) on 15/02/2021.
//
import Foundation
@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *)
public extension Sdk {
enum APIError: Error {
case error(reason: String)
case jsonSerialization
case noEndpointURL
case noFirebaseToken
case noCertificate
case noPrivateKey
case noSignatureValues
case urlError
case wrongToken
case wrongSymbol
case wrongPin
}
}

View file

@ -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())
}
}

View file

@ -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
}
}

View file

@ -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
}

293
sdk/Sources/Sdk/Sdk.swift Normal file
View file

@ -0,0 +1,293 @@
//
// Sdk.swift
//
//
// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021.
//
import Foundation
import Combine
import os
import KeychainAccess
@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *)
public class Sdk {
static private let libraryVersion: String = "0.0.1"
private let loggerSubsystem: String = "com.wulkanowy-ios.Sdk"
private var cancellables: Set<AnyCancellable> = []
var firebaseToken: String!
var endpointURL: String!
public let certificate: X509
// MARK: - Init
public init(certificate: X509) {
self.certificate = certificate
}
// MARK: - Public functions
/// Logs in with supplied login data.
/// - Parameters:
/// - token: Login token
/// - symbol: Login symbol
/// - pin: Login PIN
/// - deviceName: Name of the device
/// - completionHandler: Callback
public func login(token: String, symbol: String, pin: String, deviceModel: String, completionHandler: @escaping (Error?) -> Void) {
let logger: Logger = Logger(subsystem: self.loggerSubsystem, category: "Login")
logger.debug("Logging in...")
let endpointPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "http://komponenty.vulcan.net.pl/UonetPlusMobile/RoutingRules.txt")!)
.mapError { $0 as Error }
// Firebase request
var firebaseRequest: URLRequest = URLRequest(url: URL(string: "https://android.googleapis.com/checkin")!)
firebaseRequest.httpMethod = "POST"
firebaseRequest.setValue("application/json", forHTTPHeaderField: "Content-type")
firebaseRequest.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
let firebaseRequestBody: [String: Any] = [
"locale": "pl_PL",
"digest": "",
"checkin": [
"iosbuild": [
"model": deviceModel,
"os_version": Self.libraryVersion
],
"last_checkin_msec": 0,
"user_number": 0,
"type": 2
],
"time_zone": TimeZone.current.identifier,
"user_serial_number": 0,
"id": 0,
"logging_id": 0,
"version": 2,
"security_token": 0,
"fragment": 0
]
firebaseRequest.httpBody = try? JSONSerialization.data(withJSONObject: firebaseRequestBody)
let firebasePublisher = URLSession.shared.dataTaskPublisher(for: firebaseRequest)
.receive(on: DispatchQueue.global(qos: .background))
.tryCompactMap { value -> AnyPublisher<Data, Error> in
guard let dictionary: [String: Any] = try? JSONSerialization.jsonObject(with: value.data) as? [String: Any] else {
throw APIError.jsonSerialization
}
var request: URLRequest = URLRequest(url: URL(string: "https://fcmtoken.googleapis.com/register")!)
request.httpMethod = "POST"
request.setValue("AidLogin \(dictionary["android_id"] as? Int ?? 0):\(dictionary["security_token"] as? Int ?? 0)", forHTTPHeaderField: "Authorization")
request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
let body: String = "device=\(dictionary["android_id"] as? Int ?? 0)&app=pl.edu.vulcan.hebe&sender=987828170337&X-subtype=987828170337&appid=dLIDwhjvE58&gmp_app_id=1:987828170337:ios:6b65a4ad435fba7f"
request.httpBody = body.data(using: .utf8)
return URLSession.shared.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.global(qos: .background))
.mapError { $0 as Error }
.map { $0.data }
.eraseToAnyPublisher()
}
.flatMap { $0 }
Publishers.Zip(endpointPublisher, firebasePublisher)
.tryMap { (endpoints, firebaseToken) -> (String, String) in
// Find endpointURL
let lines = String(data: endpoints.data, encoding: .utf8)?.split { $0.isNewline }
var endpointURL: String?
lines?.forEach { line in
let items = line.split(separator: ",")
if (token.starts(with: items[0])) {
endpointURL = String(items[1])
return
}
}
guard let finalEndpointURL: String = endpointURL else {
throw APIError.noEndpointURL
}
// Get Firebase token
guard let token: String = String(data: firebaseToken, encoding: .utf8)?.components(separatedBy: "token=").last else {
logger.error("Token empty! Response: \"\(firebaseToken.base64EncodedString(), privacy: .private)\"")
throw APIError.noFirebaseToken
}
return (finalEndpointURL, token)
}
.tryMap { endpointURL, firebaseToken in
try self.registerDevice(endpointURL: endpointURL, firebaseToken: firebaseToken, token: token, symbol: symbol, pin: pin, deviceModel: deviceModel)
.mapError { $0 as Error }
.map { $0.data }
.eraseToAnyPublisher()
}
.flatMap { $0 }
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
completionHandler(error)
}
}, receiveValue: { data in
if let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let parsedError = self.parseResponse(response) {
completionHandler(parsedError)
} else {
self.getStudents(symbol: symbol, deviceModel: deviceModel)
completionHandler(nil)
}
})
.store(in: &cancellables)
}
// MARK: - Private functions
/// Registers the device
/// - Parameters:
/// - endpointURL: API endpoint URL
/// - firebaseToken: FCM token
/// - token: Vulcan token
/// - symbol: Vulcan symbol
/// - pin: Vulcan PIN
/// - deviceModel: Device model
/// - Throws: Error
/// - Returns: URLSession.DataTaskPublisher
private func registerDevice(endpointURL: String, firebaseToken: String, token: String, symbol: String, pin: String, deviceModel: String) throws -> URLSession.DataTaskPublisher {
self.endpointURL = endpointURL
self.firebaseToken = firebaseToken
guard let keyFingerprint = certificate.getPrivateKeyFingerprint(format: .PEM)?.replacingOccurrences(of: ":", with: "").lowercased(),
let keyData = certificate.getPublicKeyData(),
let keyBase64 = String(data: keyData, encoding: .utf8)?
.split(separator: "\n") // Split by newline
.dropFirst() // Drop prefix
.dropLast() // Drop suffix
.joined() // Join
else {
throw APIError.noCertificate
}
// Request
let url = "\(endpointURL)/\(symbol)/api/mobile/register/new"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
let now: Date = Date()
var timestampFormatted: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
return dateFormatter.string(from: now)
}
let keychain = Keychain()
keychain[string: "keyFingerprint"] = keyFingerprint
// Body
let body: [String: Encodable?] = [
"AppName": "DzienniczekPlus 2.0",
"AppVersion": Self.libraryVersion,
"CertificateId": nil,
"Envelope": [
"OS": "iOS",
"PIN": pin,
"Certificate": keyBase64,
"CertificateType": "RSA_PEM",
"DeviceModel": deviceModel,
"SecurityToken": token,
"SelfIdentifier": UUID().uuidString.lowercased(),
"CertificateThumbprint": keyFingerprint
],
"FirebaseToken": firebaseToken,
"API": 1,
"RequestId": UUID().uuidString.lowercased(),
"Timestamp": now.millisecondsSince1970,
"TimestampFormatted": "\(timestampFormatted) GMT"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
request.allHTTPHeaderFields = [
"Content-Type": "application/json",
"Accept-Encoding": "gzip",
"vDeviceModel": deviceModel
]
let signedRequest = try request.signed(with: certificate)
return URLSession.shared.dataTaskPublisher(for: signedRequest)
}
private func getStudents(symbol: String, deviceModel: String) {
let url = "\(self.endpointURL!)/\(symbol)/api/mobile/register/hebe"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "GET"
let now = Date()
var vDate: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return "\(dateFormatter.string(from: now)) GMT"
}
let signatures = getSignatures(request: request, certificate: certificate)
request.setValue("\(signatures)", forHTTPHeaderField: "Signature")
request.allHTTPHeaderFields = [
"User-Agent": "wulkanowy/1 CFNetwork/1220.1 Darwin/20.1.0",
"vOS": "iOS",
"vDeviceModel": deviceModel,
"vAPI": "1",
"vDate": vDate,
"vCanonicalUrl": "api%2fmobile%2fregister%2fhebe"
]
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
if let error = error {
// Handle HTTP request error
print(error)
} else if let data = data {
// Handle HTTP request response
print(String(data: data, encoding: String.Encoding.utf8) as Any)
} else {
// Handle unexpected error
}
}
task.resume()
}
// MARK: - Helper functions
/// Parses the response
/// - Parameter response: Request response
/// - Returns: VulcanKit.APIError?
private func parseResponse(_ response: [String: Any]) -> APIError? {
guard let status = response["Status"] as? [String: Any],
let statusCode = status["Code"] as? Int else {
return nil
}
print("Response status code: \(statusCode)")
switch statusCode {
case 0: return nil
case 200: return APIError.wrongToken
case -1: return APIError.wrongSymbol //Ya, Vulcan returns -1 code
case 203: return APIError.wrongPin
default: return nil
}
}
}

192
sdk/Sources/Sdk/X509.swift Normal file
View file

@ -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<UInt8> = .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()
}
}

View file

@ -0,0 +1,12 @@
//
// luckyNumber.swift
//
//
// Created by Tomasz on 27/02/2021.
//
import Foundation
public func getLuckyNumber() -> Int {
return 7
}

View file

@ -0,0 +1,98 @@
//
// File.swift
//
//
// Created by Tomasz (copied from rrroyal/vulcan) on 14/02/2021.
//
import Foundation
import CryptoKit
import KeychainAccess
@available (iOS 14, macOS 11, watchOS 7, tvOS 14, *)
public extension Sdk {
struct Signer {
static public func getSignatureValues(body: Data?, url: String, date: Date = Date(), privateKey: SecKey, fingerprint: String) -> (digest: String?, canonicalURL: String, signature: String)? {
// Canonical URL
guard let canonicalURL = getCanonicalURL(url) else {
return nil
}
// Digest
let digest: String?
if let body = body {
digest = Data(SHA256.hash(data: body)).base64EncodedString()
} else {
digest = nil
}
// Headers & values
let headersList = getHeadersList(digest: digest, canonicalURL: canonicalURL, date: date)
// Signature value
guard let data = headersList.values.data(using: .utf8) else {
return nil
}
let signatureData = SecKeyCreateSignature(privateKey, .rsaSignatureMessagePKCS1v15SHA256, data as CFData, nil) as Data?
guard let signatureValue = signatureData?.base64EncodedString() else {
return nil
}
return (
digest,
canonicalURL,
"keyId=\"\(fingerprint.replacingOccurrences(of: ":", with: ""))\",headers=\"\(headersList.headers)\",algorithm=\"sha256withrsa\",signature=Base64(SHA256withRSA(\(signatureValue)))"
)
}
// MARK: - Private functions
/// Finds and encodes the first canonical URL match in the supplied URL.
/// - Parameter url: URL to find matches in
/// - Returns: Canonical URL
static internal func getCanonicalURL(_ url: String) -> String? {
guard let regex = try? NSRegularExpression(pattern: "(api/mobile/.+)") else {
return nil
}
let results = regex.matches(in: url, range: NSRange(url.startIndex..., in: url))
return results.compactMap {
guard let range = Range($0.range, in: url) else {
return nil
}
return String(url[range]).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)?.lowercased()
}
.first
}
/// Creates a tuple with formatted headers and values needed to sign the request.
/// - Parameters:
/// - body: Body of the request
/// - digest: Digest of the request
/// - canonicalURL: Canonical URL of the request
/// - date: Date of the request
/// - Returns: Formatted headers and values
static internal func getHeadersList(digest: String?, canonicalURL: String, date: Date) -> (headers: String, values: String) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
let dateString = "\(dateFormatter.string(from: date)) GMT"
let signData: [(key: String, value: String)] = [
("vCanonicalUrl", canonicalURL),
digest == nil ? nil : ("Digest", digest ?? ""),
("vDate", "\(dateString)")
]
.compactMap { $0 }
let headers = signData.map(\.key).joined(separator: " ")
let values = signData.map(\.value).joined()
return (headers, values)
}
}
}

View file

@ -0,0 +1,7 @@
import XCTest
import SdkTests
var tests = [XCTestCaseEntry]()
tests += SdkTests.allTests()
XCTMain(tests)

View file

@ -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),
]
}

View file

@ -0,0 +1,9 @@
import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(SdkTests.allTests),
]
}
#endif

View file

@ -3,16 +3,30 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
967B5B9825D813F5006ED944 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967B5B9725D813F5006ED944 /* LoginView.swift */; };
96A2D96325D6FEA6001CB109 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 96A2D96525D6FEA6001CB109 /* Localizable.strings */; };
96A2D97B25D7003F001CB109 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A2D97A25D7003F001CB109 /* OnboardingView.swift */; };
96A2D98825D73DCF001CB109 /* CustomButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A2D98725D73DCF001CB109 /* CustomButtonView.swift */; };
96A5571725D81BD20094BF48 /* CustomTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A5571625D81BD20094BF48 /* CustomTextFieldView.swift */; };
5C1794B425E8FDFB007AD91A /* messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794B325E8FDFB007AD91A /* messages.swift */; };
5C1794B825E8FE08007AD91A /* notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794B725E8FE08007AD91A /* notes.swift */; };
5C1794BC25E8FE19007AD91A /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794BB25E8FE19007AD91A /* settings.swift */; };
5C1794C025E8FE27007AD91A /* about.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1794BF25E8FE27007AD91A /* about.swift */; };
5C1794CD25E90DBD007AD91A /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 5C1794CC25E90DBD007AD91A /* KeychainAccess */; };
5C1CFA7A25EA32AE0047286F /* ghImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1CFA7925EA32AE0047286F /* ghImage.swift */; };
5C2D331025E64F3C000253AC /* grades.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D330F25E64F3C000253AC /* grades.swift */; };
5C2D331425E650EC000253AC /* exams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D331325E650EC000253AC /* exams.swift */; };
5C2D331825E651C4000253AC /* homework.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D331725E651C4000253AC /* homework.swift */; };
5C2D331C25E651FB000253AC /* more.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2D331B25E651FB000253AC /* more.swift */; };
5C478F3525DC742100ABEFB7 /* VulcanStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C478F3425DC742100ABEFB7 /* VulcanStore.swift */; };
5C89C8F525EA6AA4000B5816 /* licenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C89C8F425EA6AA4000B5816 /* licenses.swift */; };
5C89C90625EA7996000B5816 /* SwiftUIEKtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 5C89C90525EA7996000B5816 /* SwiftUIEKtensions */; };
5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */ = {isa = PBXBuildFile; productRef = 5C9B6F4825D6C08D00C3F5F5 /* Sdk */; };
5CC2EAA525E516F100B6183E /* dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC2EAA425E516F100B6183E /* dashboard.swift */; };
5CC2EAAE25E526B500B6183E /* navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC2EAAD25E526B500B6183E /* navigation.swift */; };
5CCAE31625DA4CDD00D87580 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 5CCAE31525DA4CDD00D87580 /* OpenSSL */; };
5CEA516B25D540B900DB45BD /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CEA516D25D540B900DB45BD /* Localizable.strings */; };
F4C6D9082544E17400F8903A /* wulkanowyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9072544E17400F8903A /* wulkanowyApp.swift */; };
F4C6D90A2544E17400F8903A /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9092544E17400F8903A /* LoginView.swift */; };
F4C6D90C2544E17500F8903A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C6D90B2544E17500F8903A /* Assets.xcassets */; };
F4C6D90F2544E17500F8903A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C6D90E2544E17500F8903A /* Preview Assets.xcassets */; };
F4C6D91A2544E17500F8903A /* wulkanowyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C6D9192544E17500F8903A /* wulkanowyTests.swift */; };
@ -36,15 +50,50 @@
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
5C1F6D5F25D6891300AFDDD6 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
5C9B6EEC25D6B25200C3F5F5 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
967B5B9725D813F5006ED944 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
96A2D96425D6FEA6001CB109 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
96A2D96925D6FEBB001CB109 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
96A2D97A25D7003F001CB109 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
96A2D98725D73DCF001CB109 /* CustomButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButtonView.swift; sourceTree = "<group>"; };
96A5571625D81BD20094BF48 /* CustomTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextFieldView.swift; sourceTree = "<group>"; };
5C1794B325E8FDFB007AD91A /* messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = messages.swift; sourceTree = "<group>"; };
5C1794B725E8FE08007AD91A /* notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notes.swift; sourceTree = "<group>"; };
5C1794BB25E8FE19007AD91A /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = "<group>"; };
5C1794BF25E8FE27007AD91A /* about.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = about.swift; sourceTree = "<group>"; };
5C1CFA7925EA32AE0047286F /* ghImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ghImage.swift; sourceTree = "<group>"; };
5C2D330F25E64F3C000253AC /* grades.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = grades.swift; sourceTree = "<group>"; };
5C2D331325E650EC000253AC /* exams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = exams.swift; sourceTree = "<group>"; };
5C2D331725E651C4000253AC /* homework.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = homework.swift; sourceTree = "<group>"; };
5C2D331B25E651FB000253AC /* more.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = more.swift; sourceTree = "<group>"; };
5C478F3425DC742100ABEFB7 /* VulcanStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VulcanStore.swift; sourceTree = "<group>"; };
5C89C8F425EA6AA4000B5816 /* licenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = licenses.swift; sourceTree = "<group>"; };
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 = "<group>"; };
5CC2EAA425E516F100B6183E /* dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dashboard.swift; sourceTree = "<group>"; };
5CC2EAAD25E526B500B6183E /* navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = navigation.swift; sourceTree = "<group>"; };
5CEA516C25D540B900DB45BD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
5CF81BD725D9D44400B12C4C /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
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 = "<group>"; };
F4C6D9092544E17400F8903A /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
F4C6D90B2544E17500F8903A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F4C6D90E2544E17500F8903A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
F4C6D9102544E17500F8903A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -61,6 +110,10 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C9B6F4925D6C08D00C3F5F5 /* Sdk in Frameworks */,
5CCAE31625DA4CDD00D87580 /* OpenSSL in Frameworks */,
5C1794CD25E90DBD007AD91A /* KeychainAccess in Frameworks */,
5C89C90625EA7996000B5816 /* SwiftUIEKtensions in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -81,16 +134,7 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
967B5B9625D813E2006ED944 /* Login */ = {
isa = PBXGroup;
children = (
967B5B9725D813F5006ED944 /* LoginView.swift */,
96A5571625D81BD20094BF48 /* CustomTextFieldView.swift */,
);
path = Login;
sourceTree = "<group>";
};
96A2D95625D6FE4D001CB109 /* App */ = {
5C1B848625E1B6740074F29D /* App */ = {
isa = PBXGroup;
children = (
F4C6D9072544E17400F8903A /* wulkanowyApp.swift */,
@ -98,46 +142,74 @@
path = App;
sourceTree = "<group>";
};
96A2D95A25D6FE81001CB109 /* Resources */ = {
5C1B848925E1B6910074F29D /* Views */ = {
isa = PBXGroup;
children = (
96A2D96525D6FEA6001CB109 /* Localizable.strings */,
);
path = Resources;
sourceTree = "<group>";
};
96A2D96D25D6FF29001CB109 /* Views */ = {
isa = PBXGroup;
children = (
96A5571B25D859460094BF48 /* Shared */,
967B5B9625D813E2006ED944 /* Login */,
96A2D97925D7002D001CB109 /* Onboarding */,
5CC2EAAC25E5269E00B6183E /* Navigation */,
5CC2EAA325E516DD00B6183E /* Content */,
5C1B849F25E1B7A30074F29D /* Login */,
5C1CFA7925EA32AE0047286F /* ghImage.swift */,
);
path = Views;
sourceTree = "<group>";
};
96A2D97925D7002D001CB109 /* Onboarding */ = {
5C1B848E25E1B6FA0074F29D /* Resources */ = {
isa = PBXGroup;
children = (
96A2D97A25D7003F001CB109 /* OnboardingView.swift */,
5CEA516D25D540B900DB45BD /* Localizable.strings */,
);
path = Onboarding;
path = Resources;
sourceTree = "<group>";
};
96A5571B25D859460094BF48 /* Shared */ = {
5C1B849F25E1B7A30074F29D /* Login */ = {
isa = PBXGroup;
children = (
96A2D98725D73DCF001CB109 /* CustomButtonView.swift */,
F4C6D9092544E17400F8903A /* LoginView.swift */,
);
path = Shared;
path = Login;
sourceTree = "<group>";
};
5C9B6E4825D6ADFB00C3F5F5 /* Frameworks */ = {
isa = PBXGroup;
children = (
5C9B6E4925D6ADFB00C3F5F5 /* NetworkExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
5CC2EAA325E516DD00B6183E /* Content */ = {
isa = PBXGroup;
children = (
5CC2EAA425E516F100B6183E /* dashboard.swift */,
5C2D330F25E64F3C000253AC /* grades.swift */,
5C2D331325E650EC000253AC /* exams.swift */,
5C2D331725E651C4000253AC /* homework.swift */,
5C2D331B25E651FB000253AC /* more.swift */,
5C1794B325E8FDFB007AD91A /* messages.swift */,
5C1794B725E8FE08007AD91A /* notes.swift */,
5C1794BB25E8FE19007AD91A /* settings.swift */,
5C1794BF25E8FE27007AD91A /* about.swift */,
5C89C8F425EA6AA4000B5816 /* licenses.swift */,
);
path = Content;
sourceTree = "<group>";
};
5CC2EAAC25E5269E00B6183E /* Navigation */ = {
isa = PBXGroup;
children = (
5CC2EAAD25E526B500B6183E /* navigation.swift */,
);
path = Navigation;
sourceTree = "<group>";
};
F4C6D8FB2544E17300F8903A = {
isa = PBXGroup;
children = (
5C9B6F4525D6C06D00C3F5F5 /* Sdk */,
F4C6D9062544E17400F8903A /* wulkanowy */,
F4C6D9182544E17500F8903A /* wulkanowyTests */,
F4C6D9232544E17500F8903A /* wulkanowyUITests */,
5C9B6E4825D6ADFB00C3F5F5 /* Frameworks */,
F4C6D9052544E17400F8903A /* Products */,
);
sourceTree = "<group>";
@ -155,12 +227,13 @@
F4C6D9062544E17400F8903A /* wulkanowy */ = {
isa = PBXGroup;
children = (
96A2D96D25D6FF29001CB109 /* Views */,
96A2D95A25D6FE81001CB109 /* Resources */,
96A2D95625D6FE4D001CB109 /* App */,
5C1B848E25E1B6FA0074F29D /* Resources */,
5C1B848925E1B6910074F29D /* Views */,
5C1B848625E1B6740074F29D /* App */,
F4C6D90B2544E17500F8903A /* Assets.xcassets */,
F4C6D9102544E17500F8903A /* Info.plist */,
F4C6D90D2544E17500F8903A /* Preview Content */,
5C478F3425DC742100ABEFB7 /* VulcanStore.swift */,
);
path = wulkanowy;
sourceTree = "<group>";
@ -201,12 +274,20 @@
F4C6D9002544E17300F8903A /* Sources */,
F4C6D9012544E17300F8903A /* Frameworks */,
F4C6D9022544E17300F8903A /* Resources */,
5C1F6D5F25D6891300AFDDD6 /* Embed App Extensions */,
5C9B6EEC25D6B25200C3F5F5 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = wulkanowy;
packageProductDependencies = (
5C9B6F4825D6C08D00C3F5F5 /* Sdk */,
5CCAE31525DA4CDD00D87580 /* OpenSSL */,
5C1794CC25E90DBD007AD91A /* KeychainAccess */,
5C89C90525EA7996000B5816 /* SwiftUIEKtensions */,
);
productName = wulkanowy;
productReference = F4C6D9042544E17400F8903A /* wulkanowy.app */;
productType = "com.apple.product-type.application";
@ -253,7 +334,7 @@
F4C6D8FC2544E17300F8903A /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1210;
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1210;
TargetAttributes = {
F4C6D9032544E17300F8903A = {
@ -276,9 +357,14 @@
knownRegions = (
en,
Base,
pl,
"pl-PL",
);
mainGroup = F4C6D8FB2544E17300F8903A;
packageReferences = (
5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */,
5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */,
5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */,
);
productRefGroup = F4C6D9052544E17400F8903A /* Products */;
projectDirPath = "";
projectRoot = "";
@ -296,7 +382,7 @@
buildActionMask = 2147483647;
files = (
F4C6D90F2544E17500F8903A /* Preview Assets.xcassets in Resources */,
96A2D96325D6FEA6001CB109 /* Localizable.strings in Resources */,
5CEA516B25D540B900DB45BD /* Localizable.strings in Resources */,
F4C6D90C2544E17500F8903A /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -362,11 +448,11 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
96A2D96525D6FEA6001CB109 /* Localizable.strings */ = {
5CEA516D25D540B900DB45BD /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
96A2D96425D6FEA6001CB109 /* en */,
96A2D96925D6FEBB001CB109 /* pl */,
5CEA516C25D540B900DB45BD /* en */,
5CF81BD725D9D44400B12C4C /* pl-PL */,
);
name = Localizable.strings;
sourceTree = "<group>";
@ -426,7 +512,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -482,7 +568,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@ -499,10 +585,10 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"wulkanowy/Preview Content\"";
DEVELOPMENT_TEAM = GTH776WFJL;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = wulkanowy/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -521,10 +607,10 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"wulkanowy/Preview Content\"";
DEVELOPMENT_TEAM = GTH776WFJL;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = wulkanowy/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -656,6 +742,55 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.2.1;
};
};
5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/EnesKaraosman/SwiftUIEKtensions";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.1.8;
};
};
5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.180;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
5C1794CC25E90DBD007AD91A /* KeychainAccess */ = {
isa = XCSwiftPackageProductDependency;
package = 5C1794CB25E90DBD007AD91A /* XCRemoteSwiftPackageReference "KeychainAccess" */;
productName = KeychainAccess;
};
5C89C90525EA7996000B5816 /* SwiftUIEKtensions */ = {
isa = XCSwiftPackageProductDependency;
package = 5C89C90425EA7996000B5816 /* XCRemoteSwiftPackageReference "SwiftUIEKtensions" */;
productName = SwiftUIEKtensions;
};
5C9B6F4825D6C08D00C3F5F5 /* Sdk */ = {
isa = XCSwiftPackageProductDependency;
productName = Sdk;
};
5CCAE31525DA4CDD00D87580 /* OpenSSL */ = {
isa = XCSwiftPackageProductDependency;
package = 5CCAE31025DA4CCA00D87580 /* XCRemoteSwiftPackageReference "OpenSSL" */;
productName = OpenSSL;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = F4C6D8FC2544E17300F8903A /* Project object */;
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:wulkanowy.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -6,12 +6,14 @@
//
import SwiftUI
import Sdk
import Combine
@main
struct wulkanowyApp: App {
var body: some Scene {
WindowGroup {
LoginView()
NavigationBarView()
}
}
}

View file

@ -1,6 +1,15 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x09",
"green" : "0x02",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],

View file

@ -37,7 +37,11 @@
"scale" : "3x"
},
{
<<<<<<< HEAD
"size" : "40x40",
=======
"filename" : "logo.jpg",
>>>>>>> feature/register-device
"idiom" : "iphone",
"filename" : "wulkanowy-40@2x.png",
"scale" : "2x"

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.184",
"green" : "0.184",
"red" : "0.827"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2E",
"green" : "0x2E",
"red" : "0xD2"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"compression-type" : "automatic"
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "wulkanowy-logo.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"compression-type" : "automatic"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "wulkanowy.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Shape</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="wulkanowy" fill="#000000" fill-rule="nonzero">
<path d="M295.17362,965.05417 C296.24282,968.52944 295.70822,972.27203 293.83692,975.34631 L268.03972,1017.1831 C265.50012,1021.3267 260.82192,1024 255.74272,1024 L14.345318,1024 C3.1176178,1024 -3.6991822,1012.2376 2.3157178,1003.4158 L157.76692,774.44928 C158.70252,773.11265 159.23732,771.64234 159.63822,770.17205 L230.88102,465.95272 C231.68312,462.34379 234.08912,459.26952 237.56432,457.39823 L334.06972,404.46727 C337.54502,402.59597 339.95092,399.5217 340.75292,395.77911 L353.71832,339.23923 C356.39162,327.47681 373.23342,324.93718 379.91642,335.22932 L384.59472,342.71451 C386.59962,345.92244 387.13432,349.93236 385.79782,353.54129 L297.84672,607.77024 C297.17842,609.77521 296.91122,611.91384 297.31212,614.05247 L319.23302,735.68673 C319.63402,738.09268 319.36642,740.63229 318.29732,742.90458 L266.03482,860.26158 C264.83182,863.06854 264.56442,866.14281 265.50012,868.94975 L295.17362,965.05417 Z M1009.7413,1024 L843.46322,1024 C838.65152,1024 834.24042,1021.5941 831.56732,1017.8515 L719.69042,860.52891 C719.02212,859.45961 718.35382,858.3903 717.95292,857.18731 L662.48222,695.18653 C661.41302,691.9786 658.87342,689.17164 655.66532,687.56767 L519.86272,618.99802 C516.12012,617.12675 513.44682,613.65147 512.64482,609.77521 L492.59532,510.32918 C492.32792,508.72522 491.65962,507.12125 490.59032,505.65094 L444.47622,437.88328 C441.93662,434.14069 441.53572,429.59611 443.40692,425.58618 L471.47632,365.57105 C473.61502,361.02648 478.29332,357.95219 483.63972,357.68486 L535.76872,354.61059 C538.84292,354.47689 541.64992,353.40763 543.92232,351.53632 L582.28382,321.72925 C589.50162,316.11537 600.46222,318.52131 604.33842,326.40749 L736.53212,595.3395 C737.06672,596.54247 737.46782,597.74545 737.73502,598.94844 L754.04222,707.08262 C754.44312,709.62225 755.51232,711.89452 757.25012,713.76582 L1020.5682,1001.9454 C1028.3207,1010.4999 1021.7712,1024 1009.7413,1024 L1009.7413,1024 Z M363.20822,182.58501 C363.20822,151.97594 382.58972,125.64413 410.39192,113.34703 C408.38692,110.00543 407.18402,106.39651 407.18402,102.52025 C407.18402,87.683545 424.29302,75.653785 445.27822,75.653785 L446.74862,75.653785 C455.43662,61.218065 472.01102,51.460605 490.99122,51.460605 C492.32792,51.460605 493.66452,51.460605 495.00122,51.594305 C496.73892,51.727995 498.47652,50.925975 499.41212,49.589335 C513.44682,28.203095 549.00152,12.965395 590.57112,12.965395 C605.94232,12.965395 620.51172,15.104025 633.47712,18.712955 C636.55152,13.633715 643.36822,10.158455 651.25442,10.158455 C660.21002,10.158455 667.82882,14.703035 670.10102,20.985235 C681.06162,8.287145 699.64082,-4.99999999e-06 720.75972,-4.99999999e-06 C754.44312,-4.99999999e-06 781.71052,21.252565 781.71052,47.450715 C781.71052,50.658645 781.30952,53.732915 780.50752,56.807185 C779.97292,58.945825 781.17582,61.084435 783.44822,61.886425 C804.96812,69.772605 819.53752,84.742975 819.53752,101.85196 C819.53752,121.36691 800.69092,138.07492 774.09172,144.62445 C771.95302,145.15911 770.61642,147.0304 770.61642,149.03537 L770.61642,149.43635 C770.61642,164.54039 755.64602,176.97115 736.39842,178.30779 C736.53172,178.97612 736.53172,179.64442 736.53172,180.44641 C736.53172,209.4515 681.32862,232.84271 613.29352,232.84271 C598.59062,232.84271 584.55582,231.77339 571.59042,229.76844 L571.59042,230.43676 C571.59042,242.46651 556.08532,252.22399 537.10502,252.22399 C536.03582,252.22399 535.10012,252.22399 534.16452,252.0903 C535.50122,255.0309 536.16952,258.10517 536.16952,261.31311 C536.16952,280.02607 512.51092,295.1301 483.23842,295.1301 C480.03052,295.1301 476.95632,294.9964 473.88212,294.59544 C471.47602,294.32813 469.20382,295.79843 468.66902,298.07073 C466.12942,307.4272 457.97602,314.24406 448.21862,314.24406 C436.45612,314.24406 427.09962,304.21926 427.09962,291.92217 C427.09962,286.17462 429.10462,280.96172 432.44622,277.08546 C433.64922,275.74883 434.05012,273.87753 433.24812,272.27355 C431.37682,268.79829 430.57482,265.18936 430.57482,261.31311 C430.57482,259.17449 428.83722,257.43685 426.69852,256.90221 C390.47572,248.88236 363.20822,218.67429 363.20822,182.58501 L363.20822,182.58501 Z M670.10102,908.64795 C670.63582,910.25193 670.76932,911.85591 670.76932,913.59353 L663.01682,1011.5693 C662.48222,1018.5198 656.33362,1024 648.84852,1024 L398.62952,1024 C393.28292,1024 388.33742,1020.9257 385.93132,1016.5148 L344.62922,939.79168 C344.36192,939.39068 344.22822,938.98969 344.09452,938.5887 L305.19832,844.35557 C303.72812,841.01397 303.99532,837.13772 305.59942,833.92976 L370.15902,707.61727 C371.76302,704.543 372.03022,700.93407 370.82732,697.72613 L339.28262,610.30987 C338.21332,607.23559 338.34702,603.76032 339.68372,600.81972 L392.34732,488.6756 C397.69372,477.44782 415.20382,478.38348 418.94642,490.14591 L435.38712,541.20556 L485.51112,675.40424 C486.84782,679.14684 490.05572,682.08744 494.06562,683.42409 L600.99682,719.91436 C605.14032,721.38468 608.34842,724.45894 609.68492,728.3352 L670.10102,908.64795 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -22,6 +22,21 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>komponenty.vulcan.net.pl</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
</dict>
</dict>
</dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
@ -30,7 +45,12 @@
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<dict>
<key>UIColorName</key>
<string>LaunchColor</string>
<key>UIImageName</key>
<string>splash</string>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
@ -40,6 +60,7 @@
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>

View file

@ -2,17 +2,46 @@
Localizable.strings
wulkanowy
Created by Karol Zientek on 12/02/2021.
Created by Tomasz on 11/02/2021.
*/
"title" = "Wulkanowy";
//LOGIN SCREEN
"loginTitle" = "Log In";
"token" = "Token";
"symbol" = "Symbol";
"pin" = "Pin";
"deviceName" = "Device name";
"loginButton" = "Login";
"wrongToken" = "Wrong token";
"wrongSymbol" = "Wrong symbol";
"wrongPin" = "Wrong pin";
"invalidData" = "Wrong token, symbol or pin";
"success" = "Success";
"onboarding.description.title" = "Keep track of your educational progress with Wulkanowy app!";
"onboarding.description.content" = "Wulkanowy allows you to view your grades, attendance, messages from teachers and many more!";
"onboarding.continue" = "Continue";
//NAVIGATION
"dashboardButton" = "Dashboard";
"gradesButton" = "Grades";
"examsButton" = "Exams";
"homeworkButton" = "Homework";
"moreButton" = "More";
"login.login" = "Log-in";
"login.token" = "Token";
"login.symbol" = "Symbol";
"login.pin" = "PIN";
//MORE
"messagesButton" = "Messages";
"notesButton" = "Notes and achievements";
"settingsButton" = "Settings";
"aboutButton" = "About";
//ABOUT
"appVersion" = "App version";
"appVersionContent" = "You actually version is alpha 0.1";
"contributors" = "Contributors";
"licensesButton" = "Licenses";
"privacyPolicy" = "Privacy policy";
"discordButton" = "Join the Discord serwer";
"fbButton" = "Facebook fanpage";
"reportBug" = "Report a bug";
"homepage" = "Homepage";
//LICENCES
"noLicence" = "No licence";

View file

@ -0,0 +1,47 @@
/*
Localizable.strings
wulkanowy
Created by Tomasz on 11/02/2021.
*/
//LOGIN SCREEN
"loginTitle" = "Logowanie";
"token" = "Token";
"symbol" = "Symbol";
"pin" = "Pin";
"deviceName" = "Nazwa urządzenia";
"loginButton" = "Zaloguj";
"wrongToken" = "Zły token";
"wrongSymbol" = "Zły symbol";
"wrongPin" = "Zły pin";
"invalidData" = "Zły token, symbol lub pin";
"success" = "Sukces";
//NAVIGATION
"dashboardButton" = "Start";
"gradesButton" = "Oceny";
"examsButton" = "Sprawdziany";
"homeworkButton" = "Zadania";
"moreButton" = "Więcej";
//MORE
"messagesButton" = "Wiadomości";
"notesButton" = "Uwagi i osiągnięcia";
"settingsButton" = "Ustawienia";
"aboutButton" = "O aplikacji";
//ABOUT
"appVersion" = "Wersja aplikacji";
"appVersionContent" = "Twoja aktualna wersja aplikacji to alpha 0.1";
"contributors" = "Twórcy";
"licensesButton" = "Licencje";
"privacyPolicy" = "Polityka prywatności";
"discordButton" = "Dołącz do serwera Discord";
"fbButton" = "Fanpage na Facebooku";
"reportBug" = "Zgłoś błąd";
"homepage" = "Strona domowa";
//LICENCES
"noLicence" = "Brak licencji";

View file

@ -0,0 +1,121 @@
//
// about.swift
// wulkanowy
//
// Created by Tomasz on 26/02/2021.
//
import Foundation
import SwiftUI
import UIKit
import Combine
import MessageUI
import SwiftUIEKtensions
struct AboutView: View {
@State private var result: Result<MFMailComposeResult, Error>? = nil
@State private var isShowingMailView = false
var body: some View {
Form {
Section {
DisclosureGroup("appVersion") {
Text("appVersionContent")
.font(.system(.body, design: .monospaced))
}
DisclosureGroup("contributors") {
HStack {
AsyncImage(url: URL(string: "https://avatars.githubusercontent.com/u/55411338?s=460&v=4")!,
placeholder: { Image(systemName: "circle.dashed") },
image: { Image(uiImage: $0).resizable() })
.frame(width: 38, height: 38)
Link("Pengwius", destination: URL(string: "https://github.com/pengwius")!)
.foregroundColor(Color("customControlColor"))
}
HStack {
AsyncImage(url: URL(string: "https://avatars.githubusercontent.com/u/23171377?s=460&u=ce615ffdaaea96b191b1c27fb915fd18d25eaebd&v=4")!,
placeholder: { Image(systemName: "circle.dashed") },
image: { Image(uiImage: $0).resizable() })
.frame(width: 38, height: 38)
Link("rrroyal", destination: URL(string: "https://github.com/rrroyal")!)
.foregroundColor(Color("customControlColor"))
}
HStack {
AsyncImage(url: URL(string: "https://avatars.githubusercontent.com/u/20373275?s=400&u=a59e3ca4656a7113a0021682b6733c27e6742e73&v=4")!,
placeholder: { Image(systemName: "circle.dashed") },
image: { Image(uiImage: $0).resizable() })
.frame(width: 38, height: 38)
Link("Karol Z.", destination: URL(string: "https://github.com/szakes1")!)
.foregroundColor(Color("customControlColor"))
}
}
NavigationLink(destination: LicensesView()) {
Text("licensesButton")
}
Link("FAQ", destination: URL(string: "https://wulkanowy.github.io/czesto-zadawane-pytania")!)
.foregroundColor(Color("customControlColor"))
Link("privacyPolicy", destination: URL(string: "https://wulkanowy.github.io/polityka-prywatnosci")!)
.foregroundColor(Color("customControlColor"))
}
Section {
Link("discordButton", destination: URL(string: "https://discord.com/invite/vccAQBr")!)
.foregroundColor(Color("customControlColor"))
Link("fbButton", destination: URL(string: "https://www.facebook.com/wulkanowy")!)
.foregroundColor(Color("customControlColor"))
Link("Reddit", destination: URL(string: "https://www.reddit.com/r/wulkanowy/")!)
.foregroundColor(Color("customControlColor"))
}
Section {
Button(action: {
if MFMailComposeViewController.canSendMail() {
self.isShowingMailView.toggle()
} else {
print("Can't send emails from this device")
}
if result != nil {
print("Result: \(String(describing: result))")
}
}) {
HStack {
Text("reportBug")
.foregroundColor(Color("customControlColor"))
}
}
// .disabled(!MFMailComposeViewController.canSendMail())
}
.sheet(isPresented: $isShowingMailView) {
MailView(result: $result) { composer in
composer.setSubject("")
composer.setToRecipients(["wulkanowyinc@gmail.com"])
}
}
Link("homepage", destination: URL(string: "https://wulkanowy.github.io/")!)
.foregroundColor(Color("customControlColor"))
Link("Github", destination: URL(string: "https://github.com/wulkanowy/wulkanowy-ios")!)
.foregroundColor(Color("customControlColor"))
}
}
}
struct AboutView_Previews: PreviewProvider {
static var previews: some View {
Group {
AboutView()
}
.preferredColorScheme(.dark)
}
}

View file

@ -0,0 +1,44 @@
//
// Dashboard.swift
// wulkanowy
//
// Created by Tomasz on 23/02/2021.
//
import SwiftUI
import KeychainAccess
import Sdk
struct DashboardView: View {
init() {
let keychain = Keychain()
let key = keychain["privateKey"]
let luckyNumber = getLuckyNumber()
print(luckyNumber)
}
var body: some View {
NavigationView {
VStack {
Text("You are not logged in (dashboard)")
NavigationLink(destination: LoginView()) {
Text("Log in")
}
}.padding()
}
}
}
struct DashboardView_Previews: PreviewProvider {
static var previews: some View {
Group {
DashboardView()
}
.preferredColorScheme(.dark)
}
}

View file

@ -0,0 +1,32 @@
//
// attendance.swift
// wulkanowy
//
// Created by Tomasz on 24/02/2021.
//
import SwiftUI
struct ExamsView: View {
var body: some View {
NavigationView {
VStack {
Text("You are not logged in (exams)")
NavigationLink(destination: LoginView()) {
Text("Log in")
}
}.padding()
}
}
}
struct ExamsView_Previews: PreviewProvider {
static var previews: some View {
Group {
ExamsView()
}
.preferredColorScheme(.dark)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,190 @@
//
// licenses.swift
// wulkanowy
//
// Created by Tomasz on 27/02/2021.
//
import SwiftUI
struct LicensesView: View {
let KeychainAccessLicense: String = "The MIT License (MIT)\nCopyright (c) 2014 kishikawa katsumi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
let KeychainAccessURL: URL? = URL(string: "https://github.com/kishikawakatsumi/KeychainAccess")!
let OpenSSLURL: URL? = URL(string: "https://github.com/krzyzanowskim/OpenSSL")!
let SwiftUIEKtensionsURL: URL? = URL(string: "https://github.com/EnesKaraosman/SwiftUIEKtensions")!
let OpenSSLLicense: String = """
LICENSE ISSUES
==============
The OpenSSL toolkit stays under a dual license, i.e. both the conditions of
the OpenSSL License and the original SSLeay license apply to the toolkit.
See below for the actual license texts. Actually both licenses are BSD-style
Open Source licenses. In case of any license issues related to OpenSSL
please contact openssl-core@openssl.org.
OpenSSL License
---------------
/* ====================================================================
* Copyright (c) 1998-2008 The OpenSSL Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. All advertising materials mentioning features or use of this
* software must display the following acknowledgment:
* "This product includes software developed by the OpenSSL Project
* for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
*
* 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For written permission, please contact
* openssl-core@openssl.org.
*
* 5. Products derived from this software may not be called "OpenSSL"
* nor may "OpenSSL" appear in their names without prior written
* permission of the OpenSSL Project.
*
* 6. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by the OpenSSL Project
* for use in the OpenSSL Toolkit (http://www.openssl.org/)"
*
* THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
* EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
* ====================================================================
*
* This product includes cryptographic software written by Eric Young
* (eay@cryptsoft.com). This product includes software written by Tim
* Hudson (tjh@cryptsoft.com).
*
*/
Original SSLeay License
-----------------------
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
* All rights reserved.
*
* This package is an SSL implementation written
* by Eric Young (eay@cryptsoft.com).
* The implementation was written so as to conform with Netscapes SSL.
*
* This library is free for commercial and non-commercial use as long as
* the following conditions are aheared to. The following conditions
* apply to all code found in this distribution, be it the RC4, RSA,
* lhash, DES, etc., code; not just the SSL code. The SSL documentation
* included with this distribution is covered by the same copyright terms
* except that the holder is Tim Hudson (tjh@cryptsoft.com).
*
* Copyright remains Eric Young's, and as such any Copyright notices in
* the code are not to be removed.
* If this package is used in a product, Eric Young should be given attribution
* as the author of the parts of the library used.
* This can be in the form of a textual message at program startup or
* in documentation (online or textual) provided with the package.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* "This product includes cryptographic software written by
* Eric Young (eay@cryptsoft.com)"
* The word 'cryptographic' can be left out if the rouines from the library
* being used are not cryptographic related :-).
* 4. If you include any Windows specific code (or a derivative thereof) from
* the apps directory (application code) you must include an acknowledgement:
* "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
*
* THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* The licence and distribution terms for any publically available version or
* derivative of this code cannot be changed. i.e. this code cannot simply be
* copied and put under another distribution licence
* [including the GNU Public Licence.]
*/
"""
var body: some View {
List {
// KeychainAccess
DisclosureGroup("KeychainAccess") {
Text(KeychainAccessLicense)
.font(.system(.body, design: .monospaced))
.onTapGesture {
guard let url = KeychainAccessURL else { return }
UIApplication.shared.open(url)
}
}
.padding(.vertical)
// OpenSSL
DisclosureGroup("OpenSSL") {
Text(OpenSSLLicense)
.font(.system(.body, design: .monospaced))
.onTapGesture {
guard let url = OpenSSLURL else { return }
UIApplication.shared.open(url)
}
}
.padding(.vertical)
// SwiftUIEKtensions
DisclosureGroup("SwiftUIEKtensions") {
Text("noLicence")
.font(.system(.body, design: .monospaced))
.onTapGesture {
guard let url = SwiftUIEKtensionsURL else { return }
UIApplication.shared.open(url)
}
}
.padding(.vertical)
}
.listStyle(InsetGroupedListStyle())
.navigationTitle(Text("Libraries"))
}
}
struct LicensesView_Previews: PreviewProvider {
static var previews: some View {
LicensesView()
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,50 @@
//
// more.swift
// wulkanowy
//
// Created by Tomasz on 24/02/2021.
//
import SwiftUI
struct MoreView: View {
var body: some View {
NavigationView {
Form {
Section {
NavigationLink(destination: MessagesView()) {
Label("messagesButton", systemImage: "envelope")
.accessibility(label: Text("messagesButton"))
}
NavigationLink(destination: NotesView()) {
Label("notesButton", systemImage: "graduationcap")
.accessibility(label: Text("notesButton"))
}
}
Section {
NavigationLink(destination: SettingsView()) {
Label("settingsButton", systemImage: "gear")
.accessibility(label: Text("settingsButton"))
}
NavigationLink(destination: AboutView()) {
Label("aboutButton", systemImage: "info.circle")
.accessibility(label: Text("aboutButton"))
}
}
}
.navigationBarTitle("moreButton", displayMode: .inline)
}
}
}
struct MoreView_Previews: PreviewProvider {
static var previews: some View {
Group {
MoreView()
}
.preferredColorScheme(.dark)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -1,4 +1,5 @@
//
<<<<<<< HEAD
// LoginView.swift
// wulkanowy
//
@ -41,11 +42,202 @@ struct LoginView: View {
.sheet(isPresented: $needsAppOnboarding) {
OnboardingView()
}
=======
// ContentView.swift
// wulkanowy
//
// Created by Mikołaj on 25/10/2020.
//
import SwiftUI
enum AvailableEndpoints: String, CaseIterable {
case vulcan = "Vulcan"
case fakelog = "Fakelog"
}
struct LoginView: View {
@StateObject var vulcan: VulcanStore = VulcanStore.shared
@State private var token: String = ""
@State private var symbol: String = ""
@State private var pin: String = ""
@State private var deviceModel: String = ""
@State private var clicked: Bool = false
@State private var buttonValue = String(format: NSLocalizedString("loginButton", comment: "loginButton"))
@State private var loginStatus: String = ""
@State private var willMoveToNextScreen = false
let cellHeight: CGFloat = 55
let cornerRadius: CGFloat = 12
let cellBackground: Color = Color(UIColor.systemGray6).opacity(0.5)
let nullColor: Color = Color.accentColor.opacity(0.4)
private func login() {
clicked = true
if(token != "" && symbol != "" && pin != "" && deviceModel != "") {
vulcan.login(token: token, symbol: symbol, pin: pin, deviceModel: deviceModel) { error in
if let error = error {
print("error: \(error)")
switch("\(error)"){
case "wrongToken":
buttonValue = String(format: NSLocalizedString("\(error)", comment: "loginButton"))
case "wrongSymbol":
buttonValue = String(format: NSLocalizedString("\(error)", comment: "loginButton"))
case "wrongPin":
buttonValue = String(format: NSLocalizedString("\(error)", comment: "loginButton"))
default:
buttonValue = String(format: NSLocalizedString("invalidData", comment: "loginButton"))
}
} else {
print("success")
}
}
}
}
private func setColor(input: String) -> Color {
if(clicked == true){
switch(input) {
case "token":
if (token == "") {
return nullColor
} else {
return cellBackground
}
case "symbol":
if (symbol == "") {
return nullColor
} else {
return cellBackground
}
case "pin":
if (pin == "") {
return nullColor
} else {
return cellBackground
}
case "deviceName":
if (deviceModel == "") {
return nullColor
} else {
return cellBackground
}
default:
return cellBackground
}
} else {
return cellBackground
}
}
var body: some View {
VStack {
Image("wulkanowy")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 92)
.foregroundColor(.accentColor)
.padding(.bottom)
Text("loginTitle")
.font(.largeTitle)
.fontWeight(.semibold)
Spacer()
TextField("token", text: $token)
.autocapitalization(.none)
.font(Font.body.weight(Font.Weight.medium))
.multilineTextAlignment(.center)
.padding(.horizontal)
.frame(height: cellHeight)
.background(cellBackground)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(setColor(input: "token"), lineWidth: 2)
)
TextField("symbol", text: $symbol)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(Font.body.weight(Font.Weight.medium))
.multilineTextAlignment(.center)
.padding(.horizontal)
.frame(height: cellHeight)
.background(cellBackground)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(setColor(input: "symbol"), lineWidth: 2)
)
TextField("pin", text: $pin)
.keyboardType(.numberPad)
.autocapitalization(.none)
.font(Font.body.weight(Font.Weight.medium))
.multilineTextAlignment(.center)
.padding(.horizontal)
.frame(height: cellHeight)
.background(cellBackground)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(setColor(input: "pin"), lineWidth: 2)
)
TextField("deviceName", text: $deviceModel)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(Font.body.weight(Font.Weight.medium))
.multilineTextAlignment(.center)
.padding(.horizontal)
.frame(height: cellHeight)
.background(cellBackground)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(setColor(input: "deviceName"), lineWidth: 2)
)
Spacer()
Button(buttonValue) {login()}
.font(.headline)
.multilineTextAlignment(.center)
.padding(.horizontal)
.frame(height: cellHeight)
.frame(maxWidth: .infinity)
.background(Color.accentColor.opacity(0.1))
.cornerRadius(cornerRadius)
}
.padding()
Spacer()
>>>>>>> feature/register-device
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
<<<<<<< HEAD
LoginView()
=======
Group {
LoginView()
}
.preferredColorScheme(.dark)
>>>>>>> feature/register-device
}
}

View file

@ -0,0 +1,55 @@
//
// navigation.swift
// wulkanowy
//
// Created by Tomasz on 23/02/2021.
//
import SwiftUI
struct NavigationBarView: View {
var body: some View {
TabView() {
DashboardView()
.tabItem {
Label("dashboardButton", systemImage: "rectangle.on.rectangle")
.accessibility(label: Text("dashboardButton"))
}
GradesView()
.tabItem {
Label("gradesButton", systemImage: "rosette")
.accessibility(label: Text("gradesButton"))
}
ExamsView()
.tabItem {
Label("examsButton", systemImage: "calendar")
.accessibility(label: Text("examsButton"))
}
HomeworksView()
.tabItem {
Label("homeworkButton", systemImage: "note.text")
.accessibility(label: Text("homeworkButton"))
}
MoreView()
.tabItem {
Label("moreButton", systemImage: "ellipsis.circle")
.accessibility(label: Text("moreButton"))
}
}
}
}
struct NavigationBarView_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationBarView()
}
.preferredColorScheme(.dark)
}
}

View file

@ -0,0 +1,123 @@
//
// ghImage.swift
// wulkanowy
//
// Created by Tomasz on 27/02/2021.
//
import Foundation
import SwiftUI
import UIKit
import Combine
struct AsyncImage<Placeholder: View>: 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<NSURL, UIImage>()
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 }
}
}

View file

@ -0,0 +1,61 @@
//
// VulcanStore.swift
// wulkanowy
//
// Created by Tomasz (copied from rrroyal/vulcan) on 16/02/2021.
//
import Combine
import Sdk
import Foundation
import KeychainAccess
final class VulcanStore: ObservableObject {
static let shared: VulcanStore = VulcanStore()
var privateKey: String?
let sdk: Sdk?
private init() {
// Check for stored certificate
guard let certificate: X509 = try? X509(serialNumber: 1, certificateEntries: ["CN": "APP_CERTIFICATE CA Certificate"]) else {
sdk = nil
privateKey = nil
return
}
guard let privateKeyRawData = certificate.getPrivateKeyData(format: .DER),
let privateKeyString = String(data: privateKeyRawData, encoding: .utf8)?
.split(separator: "\n")
.dropFirst()
.dropLast()
.joined() else {
privateKey = nil
sdk = nil
return
}
privateKey = privateKeyString
sdk = Sdk(certificate: certificate)
}
public func login(token: String, symbol: String, pin: String, deviceModel: String, completionHandler: @escaping (Error?) -> Void) {
sdk?.login(token: token, symbol: symbol, pin: pin, deviceModel: deviceModel) { [self] error in
if let error = error {
// Wyobraź sobie, że tutaj jest obsługa błędów. Wyobraź, bo mi sie jej robić nie chciało.
print(error)
} else {
let privateKeyToSave: String = privateKey ?? ""
let utf8str = privateKeyToSave.data(using: .utf8)
if let base64Encoded = utf8str?.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) {
let keychain = Keychain()
keychain[string: "privateKey"] = base64Encoded
}
}
completionHandler(error)
}
}
}

View file

@ -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 {