diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cc3c6ce..250388b 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "location" : "git@github.com:QuickVerse/quickverse-ios-sdk.git", "state" : { "branch" : "main", - "revision" : "c9c46a394f7debf8031302b280c5e32c5f407b4d" + "revision" : "5d4883c655a0821b94209f3f265c11fba6a33325" } } ], diff --git a/QuickVerse.podspec b/QuickVerse.podspec index e56e604..97a8169 100644 --- a/QuickVerse.podspec +++ b/QuickVerse.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "QuickVerse" - spec.version = "1.4.0" + spec.version = "1.4.1" spec.summary = "Effortlessly integrate your quickverse.io localisations into your iOS app, for instant, over-the-air updates & more." spec.description = <<-DESC QuickVerse lets you translate your web and mobile apps with ease. Powered by instant, over-the-air updates, you can change your app copy anytime, anywhere. diff --git a/README.md b/README.md index 5ee0e0e..0880f2f 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,13 @@ The library should have been added to the Swift Package Dependencies section, an ``` pod 'QuickVerse' // Always use the latest version -pod 'QuickVerse', '~> 1.4.0' // Or pin to a specific version +pod 'QuickVerse', '~> 1.4.1' // Or pin to a specific version ``` 2. In a terminal window, navigate to the directory of your `Podfile`, and run `pod install --repo-update` ### Carthage -We recommend SPM for CocoaPods, but please contact us for integration guidance if you wish to use Carthage. +We recommend integrating with SPM or CocoaPods, but please contact us for integration guidance if you wish to use Carthage. ## Usage diff --git a/Sources/QuickVerse/Extensions/Data+Extensions.swift b/Sources/QuickVerse/Internal/Extensions/Data+Extensions.swift similarity index 100% rename from Sources/QuickVerse/Extensions/Data+Extensions.swift rename to Sources/QuickVerse/Internal/Extensions/Data+Extensions.swift diff --git a/Sources/QuickVerse/Internal/Managers/LocalizationManager.swift b/Sources/QuickVerse/Internal/Managers/LocalizationManager.swift new file mode 100644 index 0000000..975f887 --- /dev/null +++ b/Sources/QuickVerse/Internal/Managers/LocalizationManager.swift @@ -0,0 +1,20 @@ +import Foundation + +class LocalizationManager { + private let apiClient: API + init(apiClient: API) { + self.apiClient = apiClient + } +} + +extension LocalizationManager { + func getLocalizationsFor(languageCode: String, completion: @escaping (Result) -> Void) { + guard let url = URL(string: RequestBuilder.buildLocalizationRequest(languageCode: languageCode)) else { + return completion(.failure(.invalidURL)) + } + let request = Request(url: url, httpMethod: .get, body: nil) + apiClient.makeRequest(request: request) { (result: Result) in + completion(result) + } + } +} diff --git a/Sources/QuickVerse/Logging/QuickVerseLogger.swift b/Sources/QuickVerse/Internal/Managers/LoggingManager.swift similarity index 69% rename from Sources/QuickVerse/Logging/QuickVerseLogger.swift rename to Sources/QuickVerse/Internal/Managers/LoggingManager.swift index 975285f..56c839e 100644 --- a/Sources/QuickVerse/Logging/QuickVerseLogger.swift +++ b/Sources/QuickVerse/Internal/Managers/LoggingManager.swift @@ -1,8 +1,8 @@ -struct QuickVerseLogger { - static func logStatement(_ string: String) { +struct LoggingManager { + static func log(_ string: String) { print("QuickVerse: \(string)") } - static func printCodeForAvailableLocalizations(_ localizations: [QuickVerseLocalization]) { + static func logCodeForAvailableLocalizations(_ localizations: [QuickVerseLocalization]) { var casesString = "" for (index, localization) in localizations.enumerated() { let propertyName = localization.key.replacingOccurrences(of: ".", with: "_") @@ -20,10 +20,10 @@ struct QuickVerseLogger { \(casesString) } - Example: QuickVerse.shared.stringFor(key: QVKey.\(localizations.last?.key.replacingOccurrences(of: " .,-", with: "", options: [.regularExpression]) ?? "")) + Example: QuickVerse.stringFor(key: QVKey.\(localizations.last?.key.replacingOccurrences(of: " .,-", with: "", options: [.regularExpression]) ?? "")) ℹ️ℹ️ℹ️ DEBUG: END AVAILABLE LOCALIZATION KEYS ℹ️ℹ️ℹ️ """ - QuickVerseLogger.logStatement(logString) + LoggingManager.log(logString) } } diff --git a/Sources/QuickVerse/Internal/Managers/ReportingManager.swift b/Sources/QuickVerse/Internal/Managers/ReportingManager.swift new file mode 100644 index 0000000..b5d7089 --- /dev/null +++ b/Sources/QuickVerse/Internal/Managers/ReportingManager.swift @@ -0,0 +1,83 @@ +import Foundation + +class ReportingManager { + private let apiClient: API + init(apiClient: API) { + self.apiClient = apiClient + } + + struct UtilisedKey { + let key: String + var count: Int + } + struct MissingKey { + let key: String + let defaultValue: String + } + + private let keyLimit = 4 + private var missingKeys: [MissingKey] = [] + private var utilisedKeys: [UtilisedKey] = [] + private var needsTransmission: Bool { + if !missingKeys.isEmpty { + return true + } else { + let utilisedCount = utilisedKeys.compactMap { $0.count }.reduce(0, +) + return (missingKeys.count + utilisedCount) >= keyLimit + } + } + private var requestInFlight: Bool = false +} + +extension ReportingManager { + func logUtilisedKey(_ key: String) { + let existingCount = utilisedKeys.first(where: { $0.key == key })?.count ?? 0 + let newCount = existingCount + 1 + utilisedKeys.removeAll(where: { $0.key == key }) + utilisedKeys.append(UtilisedKey(key: key, count: newCount)) + uploadKeyDataIfNecessary() + } + func logMissingKey(_ key: String, defaultValue: String?) { + missingKeys.removeAll(where: { $0.key == key }) + missingKeys.append(MissingKey(key: key, defaultValue: defaultValue ?? "")) + uploadKeyDataIfNecessary() + } +} + +extension ReportingManager { + func uploadKeyDataIfNecessary() { + guard + needsTransmission, + !requestInFlight else { return } + + var missing: [[String: Any]] = [[:]] + missingKeys.forEach { missingKey in + missing.append(["key": missingKey.key, "default_value": missingKey.defaultValue]) + } + var utilised: [[String: Any]] = [[:]] + utilisedKeys.forEach { utilisedKey in + utilised.append(["key": utilisedKey.key, "count": utilisedKey.count]) + } + let json: [String: Any] = ["missing_keys": missing, "utilised_keys": utilised] + + guard + let jsonData = try? JSONSerialization.data(withJSONObject: json), + let url = URL(string: RequestBuilder.buildAnalyticsRequest()) else { return } + + requestInFlight = true + + let request = Request(url: url, httpMethod: .post, body: jsonData) + apiClient.makeRequest(request: request) { [weak self] (result: Result) in + guard let self else { return } + switch result { + case .success: + missingKeys.removeAll() + utilisedKeys.removeAll() + case .failure: + // Request failed, do not clear keys, retry on next + break + } + requestInFlight = false + } + } +} diff --git a/Sources/QuickVerse/Model/QuickVerseLocalization.swift b/Sources/QuickVerse/Internal/Model/QuickVerseLocalization.swift similarity index 100% rename from Sources/QuickVerse/Model/QuickVerseLocalization.swift rename to Sources/QuickVerse/Internal/Model/QuickVerseLocalization.swift diff --git a/Sources/QuickVerse/Model/QuickVerseLocalizationData.swift b/Sources/QuickVerse/Internal/Model/QuickVerseLocalizationData.swift similarity index 100% rename from Sources/QuickVerse/Model/QuickVerseLocalizationData.swift rename to Sources/QuickVerse/Internal/Model/QuickVerseLocalizationData.swift diff --git a/Sources/QuickVerse/Model/QuickVerseResponse.swift b/Sources/QuickVerse/Internal/Model/QuickVerseResponse.swift similarity index 100% rename from Sources/QuickVerse/Model/QuickVerseResponse.swift rename to Sources/QuickVerse/Internal/Model/QuickVerseResponse.swift diff --git a/Sources/QuickVerse/Internal/Model/Request/RequestBuilder.swift b/Sources/QuickVerse/Internal/Model/Request/RequestBuilder.swift new file mode 100644 index 0000000..4cedf12 --- /dev/null +++ b/Sources/QuickVerse/Internal/Model/Request/RequestBuilder.swift @@ -0,0 +1,10 @@ +import Foundation + +struct RequestBuilder { + static func buildLocalizationRequest(languageCode: String) -> String { + return RequestEndpoint.base + RequestType.localizations.path + languageCode + } + static func buildAnalyticsRequest() -> String { + return RequestEndpoint.base + RequestType.reporting.path + } +} diff --git a/Sources/QuickVerse/Internal/Model/Request/RequestEndpoint.swift b/Sources/QuickVerse/Internal/Model/Request/RequestEndpoint.swift new file mode 100644 index 0000000..83ab29f --- /dev/null +++ b/Sources/QuickVerse/Internal/Model/Request/RequestEndpoint.swift @@ -0,0 +1,3 @@ +struct RequestEndpoint { + static let base = "https://quickverse.io/sdk/api/" +} diff --git a/Sources/QuickVerse/Internal/Model/Request/RequestType.swift b/Sources/QuickVerse/Internal/Model/Request/RequestType.swift new file mode 100644 index 0000000..1bca43f --- /dev/null +++ b/Sources/QuickVerse/Internal/Model/Request/RequestType.swift @@ -0,0 +1,11 @@ +enum RequestType { + case localizations + case reporting + + var path: String { + switch self { + case .localizations: return "localisation/" + case .reporting: return "report" + } + } +} diff --git a/Sources/QuickVerse/Internal/Networking/APIClient.swift b/Sources/QuickVerse/Internal/Networking/APIClient.swift new file mode 100644 index 0000000..769471c --- /dev/null +++ b/Sources/QuickVerse/Internal/Networking/APIClient.swift @@ -0,0 +1,91 @@ +import Foundation +import UIKit + +protocol API { + func makeRequest(request: Request, completion: @escaping (Result) -> Void) + func makeRequest(request: Request, completion: @escaping (Result) -> Void) +} + +class APIClient: API { + var apiKey: String! + private let sdkVersion = "1.4.1" + + private let session: URLSession + init(session: URLSession) { + self.session = session + } +} + +extension APIClient { + func makeRequest(request: Request, completion: @escaping (Result) -> Void) { + + guard let apiKey, !apiKey.isEmpty else { return completion (.failure(.noAPIKey)) } + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return completion (.failure(.noBundleID)) } + + let tokenString = "\(bundleIdentifier):\(apiKey)" + guard let tokenData = tokenString.data(using: .utf8) else { return completion (.failure(.invalidToken)) } + let tokenEncoded = tokenData.base64EncodedString() + + let urlRequest = buildURLRequestWith(request: request, token: tokenEncoded) + + session.dataTask(with: urlRequest) { data, response, error in + guard let response = response as? HTTPURLResponse else { return completion(.failure(.noResponse)) } + guard let data else { return completion(.failure(.noData)) } + + guard (200...300).contains(response.statusCode) else { + switch response.statusCode { + case 401: return completion(.failure(.unauthorized)) + default: return completion(.failure(.generic(response.statusCode))) + } + } + do { + let decoder = JSONDecoder() + try completion(.success(decoder.decode(T.self, from: data))) + } catch { + completion(.failure(.decoding)) + } + + }.resume() + } + func makeRequest(request: Request, completion: @escaping (Result) -> Void) { + + guard let apiKey, !apiKey.isEmpty else { return completion (.failure(.noAPIKey)) } + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return completion (.failure(.noBundleID)) } + + let tokenString = "\(bundleIdentifier):\(apiKey)" + guard let tokenData = tokenString.data(using: .utf8) else { return completion (.failure(.invalidToken)) } + let tokenEncoded = tokenData.base64EncodedString() + + let urlRequest = buildURLRequestWith(request: request, token: tokenEncoded) + + session.dataTask(with: urlRequest) { data, response, error in + guard let response = response as? HTTPURLResponse else { return completion(.failure(.noResponse)) } + + guard (200...300).contains(response.statusCode) else { + switch response.statusCode { + case 401: return completion(.failure(.unauthorized)) + default: return completion(.failure(.generic(response.statusCode))) + } + } + completion(.success(())) + }.resume() + } +} + +extension APIClient { + private func buildURLRequestWith(request: Request, token: String) -> URLRequest { + var urlRequest = URLRequest(url: request.url) + urlRequest.httpMethod = request.httpMethod.rawValue + urlRequest.httpBody = request.body + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + urlRequest.setValue("Apple", forHTTPHeaderField: "Platform") + urlRequest.setValue(sdkVersion, forHTTPHeaderField: "X_QUICKVERSE_VERSION") + + // NOTE: This is *not* an advertising identifier. It has no implication for privacy declarations. + let vendorIdentifier = UIDevice.current.identifierForVendor?.uuidString ?? "" + urlRequest.setValue(vendorIdentifier, forHTTPHeaderField: "X-QUICKVERSE-DEVICEID") + + return urlRequest + } +} diff --git a/Sources/QuickVerse/Internal/Networking/APIError.swift b/Sources/QuickVerse/Internal/Networking/APIError.swift new file mode 100644 index 0000000..6e86781 --- /dev/null +++ b/Sources/QuickVerse/Internal/Networking/APIError.swift @@ -0,0 +1,11 @@ +enum APIError: Error { + case noAPIKey + case noBundleID + case invalidToken + case invalidURL + case noResponse + case noData + case unauthorized // 401 + case generic(Int) // Returns error code for inspection + case decoding +} diff --git a/Sources/QuickVerse/Internal/Networking/HTTPMethod.swift b/Sources/QuickVerse/Internal/Networking/HTTPMethod.swift new file mode 100644 index 0000000..4c91929 --- /dev/null +++ b/Sources/QuickVerse/Internal/Networking/HTTPMethod.swift @@ -0,0 +1,4 @@ +enum HTTPMethod: String { + case get = "GET" + case post = "POST" +} diff --git a/Sources/QuickVerse/Internal/Networking/Request.swift b/Sources/QuickVerse/Internal/Networking/Request.swift new file mode 100644 index 0000000..2ef1703 --- /dev/null +++ b/Sources/QuickVerse/Internal/Networking/Request.swift @@ -0,0 +1,6 @@ +import Foundation +struct Request { + let url: URL + let httpMethod: HTTPMethod + let body: Data? +} diff --git a/Sources/QuickVerse/Manager/QuickVerseManager.swift b/Sources/QuickVerse/Manager/QuickVerseManager.swift deleted file mode 100644 index 09d98b5..0000000 --- a/Sources/QuickVerse/Manager/QuickVerseManager.swift +++ /dev/null @@ -1,158 +0,0 @@ -import Foundation - -/// Convenience global accessor, allowing you to call QuickVerse methods with shorter footprint, for example: QuickVerse.getLocalizations( -public let QuickVerse = QuickVerseManager.shared - -public class QuickVerseManager { - private init() {} - public static let shared = QuickVerseManager() - - private var apiKey: String! - public var isDebugEnabled: Bool = false - - private var localizations = [QuickVerseLocalization]() - private let sdkVersion = "1.3.3" -} - -// MARK: - Public Methods - -extension QuickVerseManager { - /// Configures the SDK with the API key from your quickverse.io account. Must be called before you can use the SDK. We strongly recommend you call this on app initialisation, e.g. AppDelegate. - public func configure(apiKey: String) { - guard !apiKey.isEmpty else { - fatalError("🚨 API Key not provided. Please call this method with your API key from https://quickverse.io.") - } - self.apiKey = apiKey - } -} - -/** - Use these methods to fetch the localizations you have created on quickverse.io. - You will typically want to call this on launch, before you display any copy. - */ -extension QuickVerseManager { - /// Fetches your quickverse localizations for the user's device language setting. Unless you have a very specific use case, this is the method you'll want you use. - public func getLocalizations(completion: @escaping (_ success: Bool) ->()) { - let languageCode = retrieveDeviceLanguageCode() - getLocalizationsFor(languageCode: languageCode, completion: completion) - } - /// Fetches your quickverse localizations for a specific language code you provide. This must be a two-letter ISO 639-1 code: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes - public func getSpecificLocalizations(languageCode: String, completion: @escaping (_ success: Bool) ->()) { - getLocalizationsFor(languageCode: languageCode, completion: completion) - } -} - -/** -Use these methods to retrieve values for the localizations you fetched using one of the "get" methods above. -You can call these from anywhere in your app, e.g. Quickverse.stringFor(key: "Onboarding.Welcome.Title") -*/ -extension QuickVerseManager { - /// Returns the value for a specific key, falling back to a default value - public func stringFor(key: String, defaultValue: String) -> String { - let value = stringFor(key: key) - if value == nil { - logStringKeyNotFound(key) - } - return value ?? defaultValue - } - /// Returns the value for a specific key, or null if one does not exist - public func stringFor(key: String) -> String? { - return localizations.first(where: { $0.key == key })?.value - } -} - -// MARK: - Internal Methods -extension QuickVerseManager { - private func getLocalizationsFor(languageCode: String, completion: @escaping (_ success: Bool) ->()) { - guard let apiKey else { - fatalError("🚨 API Key not configured. Please configure the SDK on your app startup (usually AppDelegate) with your API key from https://quickverse.io.") - } - guard let bundleIdentifier = Bundle.main.bundleIdentifier else { - fatalError("🚨 No bundle ID found. QuickVerse requires a valid Bundle ID to authenticate requests. If this is unexpected, please contact team@quickverse.io.") - } - let tokenString = "\(bundleIdentifier):\(apiKey)" - guard let tokenData = tokenString.data(using: .utf8) else { - fatalError("🚨 Unable to generate auth token. Please contact team@quickverse.io.") - } - let tokenEncoded = tokenData.base64EncodedString() - - if QuickVerseManager.shared.isDebugEnabled { - QuickVerseLogger.logStatement("ℹ️ Retrieving localizations for language code: \(languageCode)") - } - - guard let url = URL(string: "https://quickverse.io/sdk/api/localisation/\(languageCode)") else { - fatalError("🚨 Unable to create request url. Please contact team@quickverse.io.") - } - - let request = buildURLRequestWith(url: url, token: tokenEncoded) - - URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in - guard - let response = response as? HTTPURLResponse, - let data = data else { - QuickVerseLogger.logStatement("🚨 WARN: No response received. Have you added at least one localization in your quickverse.io account?") - return completion(false) - } - - guard response.statusCode == 200 else { - switch response.statusCode { - case 401: - QuickVerseLogger.logStatement("🚨 WARN: API Key incorrect, or application has been been added to your quickverse.io account.") - default: break - } - return completion(false) - } - - guard let localizationResponse = try? JSONDecoder().decode(QuickVerseResponse.self, from: data) else { - QuickVerseLogger.logStatement("🚨 WARN: Localizations parse failed. Please contact support at team@quickverse.io.") - return completion(false) - } - - let localizations = localizationResponse.data.localisations - if localizations.isEmpty { - QuickVerseLogger.logStatement("🚨 WARN: Localizations empty. Please add at least one localization entry to your account on quickverse.io.") - } - if QuickVerseManager.shared.isDebugEnabled { - QuickVerseLogger.printCodeForAvailableLocalizations(localizations) - } - - QuickVerseManager.shared.localizations = localizations - return completion(true) - }).resume() - } -} - -extension QuickVerseManager { - // You shouldn't need to access this, but we've left it open for specific use cases - public func retrieveDeviceLanguageCode() -> String { - var languageCode = Locale.preferredLanguages.first - if languageCode == nil { - if #available(iOS 16, macOS 13, *) { - languageCode = Locale.current.language.languageCode?.identifier - } else { - languageCode = Locale.current.languageCode - } - } - return languageCode ?? "en" - } - - private func buildURLRequestWith(url: URL, token: String) -> URLRequest { - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - request.setValue("Apple", forHTTPHeaderField: "Platform") - request.setValue(sdkVersion, forHTTPHeaderField: "X_QUICKVERSE_VERSION") - return request - } -} - -extension QuickVerseManager { - private func logStringKeyNotFound(_ key: String) { - if localizations.isEmpty { - QuickVerseLogger.logStatement("🚨 WARN: No localizations have been received. Have you added at least one localization to your quickverse account? If yes, did your fetchLocaliZations request succeed?") - } else { - QuickVerseLogger.logStatement("🚨 WARN: Value not found for referenced key: \(key). Please check this key exists in your quickverse.io account.") - } - } -} diff --git a/Sources/QuickVerse/Public/QuickVerseManager.swift b/Sources/QuickVerse/Public/QuickVerseManager.swift new file mode 100644 index 0000000..809d57c --- /dev/null +++ b/Sources/QuickVerse/Public/QuickVerseManager.swift @@ -0,0 +1,142 @@ +import Foundation + +/// Convenience global accessor, allowing you to call QuickVerse methods with shorter footprint, for example: QuickVerse.getLocalizations( +public let QuickVerse = QuickVerseManager.shared + +public class QuickVerseManager { + public static let shared = QuickVerseManager() + + private let apiClient: APIClient + private let localizationManager: LocalizationManager + private let reportingManager: ReportingManager + private init() { + apiClient = APIClient(session: URLSession.shared) + localizationManager = LocalizationManager(apiClient: apiClient) + reportingManager = ReportingManager(apiClient: apiClient) + } + + public var isDebugEnabled: Bool = false + private var localizations = [QuickVerseLocalization]() +} + +// MARK: - Public Methods + +extension QuickVerseManager { + /// Configures the SDK with the API key from your quickverse.io account. Must be called before you can use the SDK. We strongly recommend you call this on app initialisation, e.g. AppDelegate. + public func configure(apiKey: String) { + guard !apiKey.isEmpty else { + fatalError("🚨 API Key not provided. Please call this method with your API key from https://quickverse.io.") + } + apiClient.apiKey = apiKey + } +} + +/** + Use these methods to fetch the localizations you have created on quickverse.io. + You will typically want to call this on launch, before you display any copy. + */ +extension QuickVerseManager { + /// Fetches your quickverse localizations for the user's device language setting. Unless you have a very specific use case, this is the method you'll want you use. + public func getLocalizations(completion: @escaping (_ success: Bool) ->()) { + let languageCode = retrieveDeviceLanguageCode() + getLocalizationsFor(languageCode: languageCode, completion: completion) + } + /// Fetches your quickverse localizations for a specific language code you provide. This must be a two-letter ISO 639-1 code: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + public func getSpecificLocalizations(languageCode: String, completion: @escaping (_ success: Bool) ->()) { + getLocalizationsFor(languageCode: languageCode, completion: completion) + } +} + +/** +Use these methods to retrieve values for the localizations you fetched using one of the "get" methods above. +You can call these from anywhere in your app, e.g. Quickverse.stringFor(key: "Onboarding.Welcome.Title") +*/ +extension QuickVerseManager { + /// Returns the value for a specific key, falling back to a default value + public func stringFor(key: String, defaultValue: String) -> String { + let value = stringFor(key: key) + return value ?? defaultValue + } + /// Returns the value for a specific key, or null if one does not exist + public func stringFor(key: String) -> String? { + let value = localizations.first(where: { $0.key == key })?.value + logRequestedKey(key, defaultValue: "", wasPresent: value != nil) + return value + } +} + +// MARK: - Internal Methods +private extension QuickVerseManager { + func getLocalizationsFor(languageCode: String, completion: @escaping (_ success: Bool) ->()) { + if QuickVerseManager.shared.isDebugEnabled { + LoggingManager.log("ℹ️ Retrieving localizations for language code: \(languageCode)") + } + localizationManager.getLocalizationsFor(languageCode: languageCode) { result in + switch result { + case .success(let localizationResponse): + + let localizations = localizationResponse.data.localisations + if localizations.isEmpty { + LoggingManager.log("🚨 WARN: Localizations empty. Please add at least one localization entry to your account on quickverse.io.") + } + if QuickVerseManager.shared.isDebugEnabled { + LoggingManager.logCodeForAvailableLocalizations(localizations) + } + + QuickVerseManager.shared.localizations = localizations + return completion(true) + + case .failure(let apiError): + switch apiError { + case .noAPIKey: + fatalError("🚨 API Key not configured. Please configure the SDK on your app startup (usually AppDelegate) with your API key from https://quickverse.io.") + case .noBundleID: + fatalError("🚨 No bundle ID found. QuickVerse requires a valid Bundle ID to authenticate requests. If this is unexpected, please contact team@quickverse.io.") + case .invalidURL: + fatalError("🚨 Unable to create request url. Please contact team@quickverse.io.") + case .invalidToken: + fatalError("🚨 Unable to generate auth token. Please contact team@quickverse.io.") + case .noResponse, .noData: + LoggingManager.log("🚨 WARN: No response received. Have you added at least one localization in your quickverse.io account?") + case .unauthorized: + LoggingManager.log("🚨 WARN: API Key incorrect, or application has been been added to your quickverse.io account.") + case .generic(let statusCode): + LoggingManager.log("🚨 WARN: Network error. Status code: \(statusCode). Please contact team@quickverse.io if this is unexpected.") + case .decoding: + LoggingManager.log("🚨 WARN: Localizations parse failed. Please contact support at team@quickverse.io.") + } + completion(false) + } + } + } +} + +extension QuickVerseManager { + // You shouldn't need to access this, but we've left it open for specific use cases + public func retrieveDeviceLanguageCode() -> String { + var languageCode = Locale.preferredLanguages.first + if languageCode == nil { + if #available(iOS 16, macOS 13, *) { + languageCode = Locale.current.language.languageCode?.identifier + } else { + languageCode = Locale.current.languageCode + } + } + return languageCode ?? "en" + } +} + +private extension QuickVerseManager { + func logRequestedKey(_ key: String, defaultValue: String, wasPresent: Bool) { + if wasPresent { + reportingManager.logUtilisedKey(key) + } else { + if localizations.isEmpty { + LoggingManager.log("🚨 WARN: No localizations have been received. Have you added at least one localization to your quickverse account? If yes, did your fetchLocaliZations request succeed?") + } else { + LoggingManager.log("🚨 WARN: Value not found for referenced key: \(key). Please check this key exists in your quickverse.io account.") + } + reportingManager.logMissingKey(key, defaultValue: defaultValue) + } + } +}