diff --git a/.gitignore b/.gitignore index cbe0c791a..8c8b51070 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,6 @@ Package.resolved BuildTools/.build BuildTools/.swiftpm - # Sensitive Partner API Modules/litewallet-partner-api-ios litewallet-partner-api-ios diff --git a/litewallet.xcodeproj/xcshareddata/xcschemes/litewallet.xcscheme b/litewallet.xcodeproj/xcshareddata/xcschemes/litewallet.xcscheme index 4a857a40e..172685120 100644 --- a/litewallet.xcodeproj/xcshareddata/xcschemes/litewallet.xcscheme +++ b/litewallet.xcodeproj/xcshareddata/xcschemes/litewallet.xcscheme @@ -49,7 +49,6 @@ ) { + self.navigateStart = navigateStart + languagePref = language + _didTapContinue = didTapContinue + } + + var body: some View { + GeometryReader { geometry in + + let width = geometry.size.width + let height = geometry.size.height + + ZStack { + Color.liteWalletDarkBlue.edgesIgnoringSafeArea(.all) + VStack { + Text(S.Notifications.emailTitle.localize()) + .font(hugeFont) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(idealHeight: buttonHeight) + .foregroundColor(.white) + .padding(.all, genericPad) + .padding(.top, height * 0.08) + + Text(S.Notifications.pitchMessage.localize()) + .font(buttonLightFont) + .kerning(0.3) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(.white) + .padding(.all, genericPad) + .onTapGesture { + isEmailFieldFocused.toggle() + } + + SignupWebViewRepresentable(userAction: $didComplete, + urlString: C.signupURL) + .frame(width: width) + .frame(height: pageHeight) + .edgesIgnoringSafeArea(.all) + .padding(.bottom, smallLabelPad) + .onChange(of: didComplete) { updateValue in + if updateValue { + switch navigateStart { + case .create: + viewModel.didTapCreate!() + case .recover: + viewModel.didTapRecover!() + } + } + } + + Spacer() + Button { + /// Reuse this struct for Create or Recover + switch navigateStart { + case .create: + viewModel.didTapCreate!() + case .recover: + viewModel.didTapRecover!() + } + + } label: { + ZStack { + RoundedRectangle(cornerRadius: bigButtonCornerRadius) + .frame(width: width * 0.9, height: 45, alignment: .center) + .foregroundColor(.litewalletDarkBlue) + + Text(S.Notifications.signupCancel.localize()) + .frame(width: width * 0.9, height: 45, alignment: .center) + .font(buttonLightFont) + .foregroundColor(.white) + .overlay( + RoundedRectangle(cornerRadius: bigButtonCornerRadius) + .stroke(.white, lineWidth: 0.5) + ) + } + } + .padding(.bottom, genericPad) + } + } + } + .edgesIgnoringSafeArea(.top) + } +} + +/// Crashes when env Obj is added +// #Preview { +// AnnounceUpdatesView(navigateStart: .create, +// didTapContinue: .constant(false)) +// } diff --git a/litewallet/AppDelegate.swift b/litewallet/AppDelegate.swift new file mode 100644 index 000000000..f97f7fe46 --- /dev/null +++ b/litewallet/AppDelegate.swift @@ -0,0 +1,120 @@ +import Firebase +import LocalAuthentication +import PushNotifications +import SwiftUI +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + let applicationController = ApplicationController() + let pushNotifications = PushNotifications.shared + func application(_ application: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool + { + setFirebaseConfiguration() + + updateCurrentUserLocale(localeId: Locale.current.identifier) + + guard let thisWindow = window else { return false } + + thisWindow.tintColor = .liteWalletBlue + + UIView.swizzleSetFrame() + + applicationController.launch(application: application, window: thisWindow) + + LWAnalytics.logEventWithParameters(itemName: ._20191105_AL) + + Bundle.setLanguage(UserDefaults.selectedLanguage) + + // Pusher + pushNotifications.start(instanceId: Partner.partnerKeyPath(name: .pusherStaging)) + // pushNotifications.registerForRemoteNotifications() + let generaliOSInterest = "general-ios" + let debugGeneraliOSInterest = "debug-general-ios" + + try? pushNotifications + .addDeviceInterest(interest: generaliOSInterest) + try? pushNotifications + .addDeviceInterest(interest: debugGeneraliOSInterest) + + let interests = pushNotifications.getDeviceInterests()?.joined(separator: "|") ?? "" + let device = UIDevice.current.identifierForVendor?.uuidString ?? "ID" + let interestesDict: [String: String] = ["device_id": device, + "pusher_interests": interests] + + LWAnalytics.logEventWithParameters(itemName: ._20231202_RIGI, properties: interestesDict) + + return true + } + + func applicationDidBecomeActive(_: UIApplication) { + UIApplication.shared.applicationIconBadgeNumber = 0 + } + + func applicationWillEnterForeground(_: UIApplication) { + applicationController.willEnterForeground() + } + + func applicationDidEnterBackground(_: UIApplication) { + applicationController.didEnterBackground() + } + + func application(_: UIApplication, shouldAllowExtensionPointIdentifier _: UIApplication.ExtensionPointIdentifier) -> Bool + { + return false // disable extensions such as custom keyboards for security purposes + } + + func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool + { + return applicationController.open(url: url) + } + + func application(_: UIApplication, shouldSaveApplicationState _: NSCoder) -> Bool { + return true + } + + func application(_: UIApplication, shouldRestoreApplicationState _: NSCoder) -> Bool { + return true + } + + /// Pusher Related funcs + func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + pushNotifications.registerDeviceToken(deviceToken) + + let acceptanceDict: [String: String] = ["did_accept": "true", + "date_accepted": Date().ISO8601Format()] + LWAnalytics.logEventWithParameters(itemName: ._20231225_UAP, properties: acceptanceDict) + } + + func application(_: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler _: @escaping (UIBackgroundFetchResult) -> Void) + { + pushNotifications.handleNotification(userInfo: userInfo) + } + + /// Sets the correct Google Services plist file + private func setFirebaseConfiguration() { + // Load a Firebase debug config file. + // let filePath = Bundle.main.path(forResource: "Debug-GoogleService-Info", ofType: "plist") + + let filePath = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist") + + if let fboptions = FirebaseOptions(contentsOfFile: filePath!) { + FirebaseApp.configure(options: fboptions) + } else { + assertionFailure("Couldn't load Firebase config file") + } + } + + /// Check Locale + func updateCurrentUserLocale(localeId: String) { + let suffix = String(localeId.suffix(3)) + + if suffix == "_US" { + UserDefaults.userIsInUSA = true + } else { + UserDefaults.userIsInUSA = false + } + } +} diff --git a/litewallet/ApplicationController.swift b/litewallet/ApplicationController.swift new file mode 100644 index 000000000..ae3de074d --- /dev/null +++ b/litewallet/ApplicationController.swift @@ -0,0 +1,318 @@ +import StoreKit +import UIKit + +let timeSinceLastExitKey = "TimeSinceLastExit" +let shouldRequireLoginTimeoutKey = "ShouldRequireLoginTimeoutKey" +let numberOfLitewalletLaunches = "NumberOfLitewalletLaunches" +let hasSeenAnnounceView = "HasSeedAnnounceView" +let LITEWALLET_APP_STORE_ID = 1_119_332_592 + +class ApplicationController: Subscriber, Trackable { + // Ideally the window would be private, but is unfortunately required + // by the UIApplicationDelegate Protocol + + var window: UIWindow? + fileprivate let store = Store() + private var startFlowController: StartFlowPresenter? + private var modalPresenter: ModalPresenter? + fileprivate var walletManager: WalletManager? + private var walletCoordinator: WalletCoordinator? + private var exchangeUpdater: ExchangeUpdater? + private var feeUpdater: FeeUpdater? + private let transitionDelegate: ModalTransitionDelegate + private var kvStoreCoordinator: KVStoreCoordinator? + private var mainViewController: MainViewController? + fileprivate var application: UIApplication? + private var urlController: URLController? + private var defaultsUpdater: UserDefaultsUpdater? + private var reachability = ReachabilityMonitor() + private let noAuthApiClient = BRAPIClient(authenticator: NoAuthAuthenticator()) + private var fetchCompletionHandler: ((UIBackgroundFetchResult) -> Void)? + private var launchURL: URL? + private var hasPerformedWalletDependentInitialization = false + private var didInitWallet = false + + init() { + transitionDelegate = ModalTransitionDelegate(type: .transactionDetail, store: store) + DispatchQueue.walletQueue.async { + guardProtected(queue: DispatchQueue.walletQueue) { + self.initWallet() + } + } + } + + private func initWallet() { + walletManager = try? WalletManager(store: store, dbPath: nil) + _ = walletManager?.wallet // attempt to initialize wallet + DispatchQueue.main.async { + self.didInitWallet = true + if !self.hasPerformedWalletDependentInitialization { + self.didInitWalletManager() + } + } + } + + func launch(application: UIApplication, window: UIWindow?) { + self.application = application + self.window = window + application.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) + setup() + reachability.didChange = { isReachable in + if !isReachable { + self.reachability.didChange = { isReachable in + if isReachable { + self.retryAfterIsReachable() + } + } + } + } + + if !hasPerformedWalletDependentInitialization, didInitWallet { + didInitWalletManager() + } + } + + private func setup() { + setupRootViewController() + window?.makeKeyAndVisible() + offMainInitialization() + store.subscribe(self, name: .reinitWalletManager(nil), callback: { + guard let trigger = $0 else { return } + if case let .reinitWalletManager(callback) = trigger { + if let callback = callback { + self.store.removeAllSubscriptions() + self.store.perform(action: Reset()) + self.setup() + DispatchQueue.walletQueue.async { + do { + self.walletManager = try WalletManager(store: self.store, dbPath: nil) + _ = self.walletManager?.wallet // attempt to initialize wallet + } catch { + assertionFailure("Error creating new wallet: \(error)") + } + DispatchQueue.main.async { + self.didInitWalletManager() + callback() + } + } + } + } + }) + + TransactionManager.sharedInstance.fetchTransactionData(store: store) + } + + func willEnterForeground() { + guard let walletManager = walletManager else { return } + guard !walletManager.noWallet else { return } + if shouldRequireLogin() { + store.perform(action: RequireLogin()) + } + DispatchQueue.walletQueue.async { + walletManager.peerManager?.connect() + } + exchangeUpdater?.refresh(completion: {}) + feeUpdater?.refresh() + walletManager.apiClient?.kv?.syncAllKeys { print("KV finished syncing. err: \(String(describing: $0))") } + walletManager.apiClient?.updateFeatureFlags() + if modalPresenter?.walletManager == nil { + modalPresenter?.walletManager = walletManager + } + } + + func retryAfterIsReachable() { + guard let walletManager = walletManager else { return } + guard !walletManager.noWallet else { return } + DispatchQueue.walletQueue.async { + walletManager.peerManager?.connect() + } + exchangeUpdater?.refresh(completion: {}) + feeUpdater?.refresh() + walletManager.apiClient?.kv?.syncAllKeys { print("KV finished syncing. err: \(String(describing: $0))") } + walletManager.apiClient?.updateFeatureFlags() + if modalPresenter?.walletManager == nil { + modalPresenter?.walletManager = walletManager + } + } + + func didEnterBackground() { + if store.state.walletState.syncState == .success { + DispatchQueue.walletQueue.async { + self.walletManager?.peerManager?.disconnect() + } + } + // Save the backgrounding time if the user is logged in + if !store.state.isLoginRequired { + UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: timeSinceLastExitKey) + } + walletManager?.apiClient?.kv?.syncAllKeys { print("KV finished syncing. err: \(String(describing: $0))") } + } + + func performFetch(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + fetchCompletionHandler = completionHandler + } + + func open(url: URL) -> Bool { + if let urlController = urlController { + return urlController.handleUrl(url) + } else { + launchURL = url + return false + } + } + + private func didInitWalletManager() { + guard let walletManager = walletManager else { assertionFailure("WalletManager should exist!"); return } + guard let rootViewController = window?.rootViewController else { return } + guard let window = window else { return } + + hasPerformedWalletDependentInitialization = true + store.perform(action: PinLength.set(walletManager.pinLength)) + walletCoordinator = WalletCoordinator(walletManager: walletManager, store: store) + modalPresenter = ModalPresenter(store: store, walletManager: walletManager, window: window, apiClient: noAuthApiClient) + exchangeUpdater = ExchangeUpdater(store: store, walletManager: walletManager) + feeUpdater = FeeUpdater(walletManager: walletManager, store: store) + startFlowController = StartFlowPresenter(store: store, walletManager: walletManager, rootViewController: rootViewController) + mainViewController?.walletManager = walletManager + defaultsUpdater = UserDefaultsUpdater(walletManager: walletManager) + urlController = URLController(store: store, walletManager: walletManager) + if let url = launchURL { + _ = urlController?.handleUrl(url) + launchURL = nil + } + + if UIApplication.shared.applicationState != .background { + if walletManager.noWallet { + addWalletCreationListener() + store.perform(action: ShowStartFlow()) + } else { + modalPresenter?.walletManager = walletManager + DispatchQueue.walletQueue.async { + walletManager.peerManager?.connect() + } + startDataFetchers() + } + + // For when watch app launches app in background + } else { + DispatchQueue.walletQueue.async { [weak self] in + walletManager.peerManager?.connect() + if self?.fetchCompletionHandler != nil { + self?.performBackgroundFetch() + } + } + + exchangeUpdater?.refresh(completion: { + NSLog("Rates were updated") + }) + } + } + + private func shouldRequireLogin() -> Bool { + let then = UserDefaults.standard.double(forKey: timeSinceLastExitKey) + let timeout = UserDefaults.standard.double(forKey: shouldRequireLoginTimeoutKey) + let now = Date().timeIntervalSince1970 + return now - then > timeout + } + + private func setupRootViewController() { + mainViewController = MainViewController(store: store) + window?.rootViewController = mainViewController + } + + private func startDataFetchers() { + walletManager?.apiClient?.updateFeatureFlags() + initKVStoreCoordinator() + feeUpdater?.refresh() + defaultsUpdater?.refresh() + walletManager?.apiClient?.events?.up() + + exchangeUpdater?.refresh(completion: { + NSLog("::: Refreshed fiat rates") + }) + } + + private func addWalletCreationListener() { + store.subscribe(self, name: .didCreateOrRecoverWallet, callback: { _ in + self.modalPresenter?.walletManager = self.walletManager + self.startDataFetchers() + self.mainViewController?.didUnlockLogin() + }) + } + + private func initKVStoreCoordinator() { + guard let kvStore = walletManager?.apiClient?.kv + else { + NSLog("kvStore not initialized") + return + } + + guard kvStoreCoordinator == nil + else { + NSLog("kvStoreCoordinator not initialized") + return + } + + kvStore.syncAllKeys { error in + print("KV finished syncing. err: \(String(describing: error))") + self.walletCoordinator?.kvStore = kvStore + self.kvStoreCoordinator = KVStoreCoordinator(store: self.store, kvStore: kvStore) + self.kvStoreCoordinator?.retreiveStoredWalletInfo() + self.kvStoreCoordinator?.listenForWalletChanges() + } + } + + private func offMainInitialization() { + DispatchQueue.global(qos: .background).async { + _ = Rate.symbolMap // Initialize currency symbol map + } + } + + func performBackgroundFetch() { + saveEvent("appController.performBackgroundFetch") + let group = DispatchGroup() + if let peerManager = walletManager?.peerManager, peerManager.syncProgress(fromStartHeight: peerManager.lastBlockHeight) < 1.0 + { + group.enter() + LWAnalytics.logEventWithParameters(itemName: ._20200111_DEDG) + + store.lazySubscribe(self, selector: { $0.walletState.syncState != $1.walletState.syncState }, callback: { state in + if self.fetchCompletionHandler != nil { + if state.walletState.syncState == .success { + DispatchQueue.walletConcurrentQueue.async { + peerManager.disconnect() + self.saveEvent("appController.peerDisconnect") + DispatchQueue.main.async { + LWAnalytics.logEventWithParameters(itemName: ._20200111_DLDG) + group.leave() + } + } + } + } + }) + } + + group.enter() + LWAnalytics.logEventWithParameters(itemName: ._20200111_DEDG) + Async.parallel(callbacks: [ + { self.exchangeUpdater?.refresh(completion: $0) }, + { self.feeUpdater?.refresh(completion: $0) }, + { self.walletManager?.apiClient?.events?.sync(completion: $0) }, + { self.walletManager?.apiClient?.updateFeatureFlags(); $0() }, + ], completion: { + LWAnalytics.logEventWithParameters(itemName: ._20200111_DLDG) + group.leave() + }) + + DispatchQueue.global(qos: .utility).async { + if group.wait(timeout: .now() + 25.0) == .timedOut { + self.saveEvent("appController.backgroundFetchFailed") + self.fetchCompletionHandler?(.failed) + } else { + self.saveEvent("appController.backgroundFetchNewData") + self.fetchCompletionHandler?(.newData) + } + self.fetchCompletionHandler = nil + } + } +} diff --git a/litewallet/Assets.xcassets/Branding/NewLogo/newLogotyoe-white.imageset/Contents.json b/litewallet/Assets.xcassets/Branding/NewLogo/newLogotyoe-white.imageset/Contents.json new file mode 100644 index 000000000..2d4119bf2 --- /dev/null +++ b/litewallet/Assets.xcassets/Branding/NewLogo/newLogotyoe-white.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Screenshot 2023-12-12 at 12.16.04 PM.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/litewallet/Assets.xcassets/Branding/NewLogo/newLogotyoe-white.imageset/Litewallet-fin_white-65.png b/litewallet/Assets.xcassets/Branding/NewLogo/newLogotyoe-white.imageset/Litewallet-fin_white-65.png new file mode 100644 index 000000000..7763a3b8c Binary files /dev/null and b/litewallet/Assets.xcassets/Branding/NewLogo/newLogotyoe-white.imageset/Litewallet-fin_white-65.png differ diff --git a/litewallet/Assets.xcassets/TabBar/Contents.json b/litewallet/Assets.xcassets/TabBar/Contents.json index da4a164c9..2d92bd53f 100644 --- a/litewallet/Assets.xcassets/TabBar/Contents.json +++ b/litewallet/Assets.xcassets/TabBar/Contents.json @@ -3,4 +3,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/litewallet/Assets.xcassets/TabBar/receiveArrowIcon.imageset/Contents.json b/litewallet/Assets.xcassets/TabBar/receiveArrowIcon.imageset/Contents.json new file mode 100644 index 000000000..417af11fe --- /dev/null +++ b/litewallet/Assets.xcassets/TabBar/receiveArrowIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LW_RecieveV2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/litewallet/Assets.xcassets/TabBar/receiveArrowIcon.imageset/LW_RecieveV2.png b/litewallet/Assets.xcassets/TabBar/receiveArrowIcon.imageset/LW_RecieveV2.png new file mode 100644 index 000000000..776206531 Binary files /dev/null and b/litewallet/Assets.xcassets/TabBar/receiveArrowIcon.imageset/LW_RecieveV2.png differ diff --git a/litewallet/BRAPIClient+Events.swift b/litewallet/BRAPIClient+Events.swift index e9ffe12a7..fd0490100 100644 --- a/litewallet/BRAPIClient+Events.swift +++ b/litewallet/BRAPIClient+Events.swift @@ -98,7 +98,6 @@ class EventManager { self?.saveEvent(key) if note.name == UIScene.didEnterBackgroundNotification { self?.persistToDisk() - self?.sendToServer() } }) } @@ -131,9 +130,8 @@ class EventManager { return UserDefaults.hasAquiredShareDataPermission } - func sync(completion: @escaping () -> Void) { + func sync(completion _: @escaping () -> Void) { guard shouldRecordData else { removeData(); return } - sendToServer(completion: completion) } private func pushEvent(eventName: String, attributes: [String: String]) { @@ -176,62 +174,6 @@ class EventManager { } } - private func sendToServer(completion: (() -> Void)? = nil) { - queue.addOperation { [weak self] in - guard let myself = self else { return } - let dataDirectory = myself.unsentDataDirectory - - do { - try FileManager.default.contentsOfDirectory(atPath: dataDirectory) - } catch { - print("error: \(error)") - } - - guard let files = try? FileManager.default.contentsOfDirectory(atPath: dataDirectory) else { print("Unable to read event data directory"); return } - files.forEach { baseName in - // 1: read the json in - let fileName = NSString(string: dataDirectory).appendingPathComponent("/\(baseName)") - guard let inputStream = InputStream(fileAtPath: fileName) else { return } - inputStream.open() - guard let fileContents = try? JSONSerialization.jsonObject(with: inputStream, options: []) as? [[String: Any]] else { return } - guard let inArray = fileContents else { return } - // 2: transform it into the json data the server expects - let eventDump = myself.eventTupleArrayToDictionary(inArray) - guard let body = try? JSONSerialization.data(withJSONObject: eventDump, options: []) else { return } - - // 3: send off the request and await response - var request = URLRequest(url: myself.adaptor.url("/events", args: nil)) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = body - - myself.adaptor.dataTaskWithRequest(request, authenticated: true, retryCount: 0, handler: { data, resp, err in - if let resp = resp { - if resp.statusCode != 200 { - if let data = data { - print("[EventManager] Error uploading event data to server: STATUS=\(resp.statusCode), connErr=\(String(describing: err)), data=\(String(describing: String(data: data, encoding: .utf8)))") - } - } else { - if let data = data { - print("[EventManager] Successfully sent \(eventDump.count) events to server \(fileName) => \(resp.statusCode) data=\(String(describing: String(data: data, encoding: .utf8)))") - } - } - } - - // 4. remove the file from disk since we no longer need it - myself.queue.addOperation { - do { - try FileManager.default.removeItem(atPath: fileName) - } catch { - print("[EventManager] Unable to remove evnets file at path \(fileName) \(error)") - } - } - completion?() - }).resume() - } - } - } - private func removeData() { queue.addOperation { [weak self] in guard let myself = self else { return } diff --git a/litewallet/BRAPIClient+Features.swift b/litewallet/BRAPIClient+Features.swift index 1fe0e9eff..f0384b7e4 100644 --- a/litewallet/BRAPIClient+Features.swift +++ b/litewallet/BRAPIClient+Features.swift @@ -33,10 +33,4 @@ extension BRAPIClient { } }.resume() } - - static func featureEnabled(_ flag: BRFeatureFlags) -> Bool { - if E.isDebug || E.isTestFlight { return true } - let defaults = UserDefaults.standard - return defaults.bool(forKey: BRAPIClient.defaultsKeyForFeatureFlag(flag.description)) - } } diff --git a/litewallet/BRCore.swift b/litewallet/BRCore.swift new file mode 100644 index 000000000..963128ae8 --- /dev/null +++ b/litewallet/BRCore.swift @@ -0,0 +1,160 @@ +import BRCore +import Foundation + +typealias BRTxRef = UnsafeMutablePointer +typealias BRBlockRef = UnsafeMutablePointer + +/// BRPeerManagerError: Error +enum BRPeerManagerError: Error { + case posixError(errorCode: Int32, description: String) +} + +// MARK: - BRCore Protocols + +protocol BRWalletListener { + func balanceChanged(_ balance: UInt64) + func txAdded(_ tx: BRTxRef) + func txUpdated(_ txHashes: [UInt256], blockHeight: UInt32, timestamp: UInt32) + func txDeleted(_ txHash: UInt256, notifyUser: Bool, recommendRescan: Bool) +} + +protocol BRPeerManagerListener { + func syncStarted() + func syncStopped(_ error: BRPeerManagerError?) + func txStatusUpdate() + func saveBlocks(_ replace: Bool, _ blocks: [BRBlockRef?]) + func savePeers(_ replace: Bool, _ peers: [BRPeer]) + func networkIsReachable() -> Bool +} + +private func secureAllocate(allocSize: CFIndex, hint _: CFOptionFlags, info _: UnsafeMutableRawPointer?) + -> UnsafeMutableRawPointer? +{ + guard let ptr = malloc(MemoryLayout.stride + allocSize) else { return nil } + // keep track of the size of the allocation so it can be cleansed before deallocation + ptr.storeBytes(of: allocSize, as: CFIndex.self) + return ptr.advanced(by: MemoryLayout.stride) +} + +private func secureDeallocate(ptr: UnsafeMutableRawPointer?, info _: UnsafeMutableRawPointer?) { + guard let ptr = ptr else { return } + let allocSize = ptr.load(fromByteOffset: -MemoryLayout.stride, as: CFIndex.self) + memset(ptr, 0, allocSize) // cleanse allocated memory + free(ptr.advanced(by: -MemoryLayout.stride)) +} + +private func secureReallocate(ptr: UnsafeMutableRawPointer?, newsize: CFIndex, hint: CFOptionFlags, + info: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? +{ + // there's no way to tell ahead of time if the original memory will be deallocted even if the new size is smaller + // than the old size, so just cleanse and deallocate every time + guard let ptr = ptr else { return nil } + let newptr = secureAllocate(allocSize: newsize, hint: hint, info: info) + let allocSize = ptr.load(fromByteOffset: -MemoryLayout.stride, as: CFIndex.self) + if newptr != nil { memcpy(newptr, ptr, (allocSize < newsize) ? allocSize : newsize) } + secureDeallocate(ptr: ptr, info: info) + return newptr +} + +/// Converts CChar to Int8 and String +/// - Parameter characterArray: [CChar] +/// - Returns: String +public func charInt8ToString(charArray: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar)) -> String { + let addressCharArray = [unichar(charArray.0), unichar(charArray.1), unichar(charArray.2), unichar(charArray.3), unichar(charArray.4), + unichar(charArray.5), unichar(charArray.6), unichar(charArray.7), unichar(charArray.8), unichar(charArray.9), + unichar(charArray.10), unichar(charArray.11), unichar(charArray.12), unichar(charArray.13), unichar(charArray.14), + unichar(charArray.15), unichar(charArray.16), unichar(charArray.17), unichar(charArray.18), unichar(charArray.19), + unichar(charArray.20), unichar(charArray.21), unichar(charArray.22), unichar(charArray.23), unichar(charArray.24), + unichar(charArray.25), unichar(charArray.26), unichar(charArray.27), unichar(charArray.28), unichar(charArray.29), + unichar(charArray.30), unichar(charArray.31), unichar(charArray.32), unichar(charArray.33), unichar(charArray.34), + unichar(charArray.35), unichar(charArray.36), unichar(charArray.37), unichar(charArray.38), unichar(charArray.39), + unichar(charArray.40), unichar(charArray.41), unichar(charArray.42), unichar(charArray.43), unichar(charArray.44), + unichar(charArray.45), unichar(charArray.46), unichar(charArray.47), unichar(charArray.48), unichar(charArray.49), + unichar(charArray.50), unichar(charArray.51), unichar(charArray.52), unichar(charArray.53), unichar(charArray.54), + unichar(charArray.55), unichar(charArray.56), unichar(charArray.57), unichar(charArray.58), unichar(charArray.59), + unichar(charArray.60), unichar(charArray.61), unichar(charArray.62), unichar(charArray.63), unichar(charArray.64), + unichar(charArray.65), unichar(charArray.66), unichar(charArray.67), unichar(charArray.68), unichar(charArray.69), + unichar(charArray.70), unichar(charArray.71), unichar(charArray.72), unichar(charArray.73), unichar(charArray.74)] + + let length = addressCharArray.reduce(0) { $1 != 0 ? $0 + 1 : $0 } + return String(NSString(characters: addressCharArray, length: length)) +} + +/// Converts String to CChar +/// - Parameter String +/// - Returns: characterArray: [CChar] +public func stringToCharArray(addressString: String) -> [CChar] { + let arrayLength = 75 + + var charArray = [CChar]() + + let stringArray = Array(addressString) + + for index in 0 ... arrayLength - 1 { + if index < stringArray.count { + let value = Int8(stringArray[index] + .unicodeScalars + .map { $0.value }.reduce(0, +)) + charArray.append(CChar(value)) + } else { + charArray.append(0) + } + } + + return charArray +} + +// since iOS does not page memory to disk, all we need to do is cleanse allocated memory prior to deallocation +public let secureAllocator: CFAllocator = { + var context = CFAllocatorContext() + context.version = 0 + CFAllocatorGetContext(kCFAllocatorDefault, &context) + context.allocate = secureAllocate + context.reallocate = secureReallocate + context.deallocate = secureDeallocate + return CFAllocatorCreate(kCFAllocatorDefault, &context).takeRetainedValue() +}() + +// 8 element tuple equatable +public func == (l: (A, B, C, D, E, F, G, H), r: (A, B, C, D, E, F, G, H)) -> Bool +{ + return l.0 == r.0 && l.1 == r.1 && l.2 == r.2 && l.3 == r.3 && l.4 == r.4 && l.5 == r.5 && l.6 == r.6 && l.7 == r.7 +} + +public func != (l: (A, B, C, D, E, F, G, H), r: (A, B, C, D, E, F, G, H)) -> Bool +{ + return l.0 != r.0 || l.1 != r.1 || l.2 != r.2 || l.3 != r.3 || l.4 != r.4 || l.5 != r.5 || l.6 != r.6 || l.7 != r.7 +} + +// 33 element tuple equatable +public func == +(l: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, a, b, c, d, e, f, g), + r: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, a, b, c, d, e, f, g)) -> Bool +{ + return l.0 == r.0 && l.1 == r.1 && l.2 == r.2 && l.3 == r.3 && l.4 == r.4 && l.5 == r.5 && l.6 == r.6 && + l.7 == r.7 && l.8 == r.8 && l.9 == r.9 && l.10 == r.10 && l.11 == r.11 && l.12 == r.12 && l.13 == r.13 && + l.14 == r.14 && l.15 == r.15 && l.16 == r.16 && l.17 == r.17 && l.18 == r.18 && l.19 == r.19 && l.20 == r.20 && + l.21 == r.21 && l.22 == r.22 && l.23 == r.23 && l.24 == r.24 && l.25 == r.25 && l.26 == r.26 && l.27 == r.27 && + l.28 == r.28 && l.29 == r.29 && l.30 == r.30 && l.31 == r.31 && l.32 == r.32 +} + +public func != +(l: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, a, b, c, d, e, f, g), + r: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, a, b, c, d, e, f, g)) -> Bool +{ + return l.0 != r.0 || l.1 != r.1 || l.2 != r.2 || l.3 != r.3 || l.4 != r.4 || l.5 != r.5 || l.6 != r.6 || + l.7 != r.7 || l.8 != r.8 || l.9 != r.9 || l.10 != r.10 || l.11 != r.11 || l.12 != r.12 || l.13 != r.13 || + l.14 != r.14 || l.15 != r.15 || l.16 != r.16 || l.17 != r.17 || l.18 != r.18 || l.19 != r.19 || l.20 != r.20 || + l.21 != r.21 || l.22 != r.22 || l.23 != r.23 || l.24 != r.24 || l.25 != r.25 || l.26 != r.26 || l.27 != r.27 || + l.28 != r.28 || l.29 != r.29 || l.30 != r.30 || l.31 != r.31 || l.32 != r.32 +} diff --git a/litewallet/BRPeerManager.swift b/litewallet/BRPeerManager.swift index f6d1853b5..8e58da18c 100644 --- a/litewallet/BRPeerManager.swift +++ b/litewallet/BRPeerManager.swift @@ -56,7 +56,7 @@ class BRPeerManager { return BRPeerManagerConnectStatus(cPtr) == BRPeerStatusConnected } - // connect to bitcoin peer-to-peer network (also call this whenever networkIsReachable() status changes) + // connect to litecoin peer-to-peer network (also call this whenever networkIsReachable() status changes) func connect() { if let fixedAddress = UserDefaults.customNodeIP { setFixedPeer(address: fixedAddress, port: UserDefaults.customNodePort ?? C.standardPort) @@ -64,7 +64,7 @@ class BRPeerManager { BRPeerManagerConnect(cPtr) } - // disconnect from bitcoin peer-to-peer network + // disconnect from litecoin peer-to-peer network func disconnect() { BRPeerManagerDisconnect(cPtr) } @@ -113,7 +113,7 @@ class BRPeerManager { return String(cString: BRPeerManagerDownloadPeerName(cPtr)) } - // publishes tx to bitcoin network + // publishes tx to litecoin network func publishTx(_ tx: BRTxRef, completion: @escaping (Bool, BRPeerManagerError?) -> Void) { BRPeerManagerPublishTx(cPtr, tx, Unmanaged.passRetained(CompletionWrapper(completion)).toOpaque()) { info, error in diff --git a/litewallet/Base.lproj/Localizable.strings b/litewallet/Base.lproj/Localizable.strings index 1b65724ac..b34693ebb 100644 --- a/litewallet/Base.lproj/Localizable.strings +++ b/litewallet/Base.lproj/Localizable.strings @@ -1470,4 +1470,3 @@ /* Fees Blank: */ "Send.feeBlank" = "Fees:"; - diff --git a/litewallet/BuyTableViewController.swift b/litewallet/BuyTableViewController.swift index 41e936009..c09af86aa 100644 --- a/litewallet/BuyTableViewController.swift +++ b/litewallet/BuyTableViewController.swift @@ -58,7 +58,6 @@ class BuyTableViewController: UITableViewController, SFSafariViewControllerDeleg vcWKVC.currentWalletAddress = currentWalletAddress vcWKVC.uuidString = uuidString vcWKVC.timestamp = Int(Date().timeIntervalSince1970) - addChild(vcWKVC) view.addSubview(vcWKVC.view) vcWKVC.didMove(toParent: self) diff --git a/litewallet/Constants+Events.swift b/litewallet/Constants+Events.swift new file mode 100644 index 000000000..b514b7e3b --- /dev/null +++ b/litewallet/Constants+Events.swift @@ -0,0 +1,238 @@ +import UIKit + +let π: CGFloat = .pi +let customUserAgent: String = "litewallet-ios" +let swiftUICellPadding = 12.0 +let bigButtonCornerRadius = 15.0 + +struct FoundationSupport { + static let dashboard = "https://support.litewallet.io/" +} + +struct APIServer { + static let baseUrl = "https://api-prod.lite-wallet.org/" +} + +struct Padding { + subscript(multiplier: Int) -> CGFloat { + return CGFloat(multiplier) * 8.0 + } + + subscript(multiplier: Double) -> CGFloat { + return CGFloat(multiplier) * 8.0 + } +} + +struct C { + static let padding = Padding() + struct Sizes { + static let buttonHeight: CGFloat = 48.0 + static let sendButtonHeight: CGFloat = 165.0 + static let headerHeight: CGFloat = 48.0 + static let largeHeaderHeight: CGFloat = 220.0 + } + + static var defaultTintColor: UIColor = UIView().tintColor + + static let animationDuration: TimeInterval = 0.3 + static let secondsInDay: TimeInterval = 86400 + static let maxMoney: UInt64 = 84_000_000 * 100_000_000 + static let satoshis: UInt64 = 100_000_000 + static let walletQueue = "com.litecoin.walletqueue" + static let btcCurrencyCode = "LTC" + static let null = "(null)" + static let maxMemoLength = 250 + static let feedbackEmail = "feedback@litecoinfoundation.zendesk.com" + static let supportEmail = "support@litecoinfoundation.zendesk.com" + + static let reviewLink = "https://itunes.apple.com/app/loafwallet-litecoin-wallet/id1119332592?action=write-review" + static let signupURL = "https://litewallet.io/mobile-signup/signup.html" + static let stagingSignupURL = "https://staging-litewallet-io.webflow.io/mobile-signup/signup" + + static var standardPort: Int { + return E.isTestnet ? 19335 : 9333 + } + + static let troubleshootingQuestions = """ + + + + + + + + + + + +
+
+ +
Please reply to this email with the following information so that we can prepare to help you solve your Litewallet issue.
+
+
1. What version of software running on your mobile device (e.g.; iOS 13.7 or iOS 14)?
+
+
+
2. What version of Litewallet software is on your mobile device (found on the login view)?
+
+
+
3. What type of iPhone do you have?
+
+
+
4. How we can help?
+
+
+
+
+ + + """ +} + +struct AppVersion { + static let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + static let versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + static let string = "v" + versionNumber! + " (\(buildNumber!))" +} + +/// False Positive Rates +/// The rate at which the requested numner of false +/// addresses are sent to the syncing node. The more +/// fp sent the less likely that the node cannot +/// identify the Litewallet user. Used when deploying the +/// Bloom Filter. The 3 options are from testing ideal +/// rates. +enum FalsePositiveRates: Double { + case lowPrivacy = 0.00005 + case semiPrivate = 0.00008 + case anonymous = 0.0005 +} + +/// Custom Event Enum: Events related to different user based actions +enum CustomEvent: String { + /// App Launched + case _20191105_AL = "app_launched" + + /// Visit Receive Controller + case _20202116_VRC = "visited_received_controller" + + /// Visit Send Controller + case _20191105_VSC = "visited_send_controller" + + /// Did Tap Buy Tab Controller + case _20191105_DTBT = "did_tap_buy_tab" + + /// Did Send LTC + case _20191105_DSL = "did_send_ltc" + + /// Did Tap Support + case _20201118_DTS = "did_tap_support" + + /// Entered dispatch group + case _20200111_DEDG = "did_enter_dispatch_group" + + /// Left dispatch group + case _20200111_DLDG = "did_leave_dispatch_group" + + /// Rate not initialized + case _20200111_RNI = "rate_not_initialized" + + /// Fee per kb not initialized + case _20200111_FNI = "feeperkb_not_initialized" + + /// Transaction not initialized + case _20200111_TNI = "transaction_not_initialized" + + /// Wallet not initialized + case _20200111_WNI = "wallet_not_initialized" + + /// Phrase not initialized + case _20200111_PNI = "phrase_not_initialized" + + /// Unable to sign transaction + case _20200111_UTST = "unable_to_sign_transaction" + + /// Generalized Error + case _20200112_ERR = "error" + + /// Keychain Lookup + case _20210804_ERR_KLF = "error_key_lookup_failure" + + /// Started resync + case _20200112_DSR = "did_start_resync" + + /// Showed review request + case _20200125_DSRR = "did_show_review_request" + + /// Unlocked in with PIN + case _20200217_DUWP = "did_unlock_with_pin" + + /// App Launched + case _20200217_DUWB = "did_unlock_with_biometrics" + + /// Did use default fee per kb + case _20200301_DUDFPK = "did_use_default_fee_per_kb" + + /// User tapped support LF + case _20201118_DTGS = "did_tap_get_support" + + /// Started IFPS Lookup + case _20201121_SIL = "started_IFPS_lookup" + + /// Resolved IPFS Address + case _20201121_DRIA = "did_resolve_IPFS_address" + + /// Failed to resolve IPFS Address + case _20201121_FRIA = "failed_resolve_IPFS_address" + + /// User tapped balance + case _20200207_DTHB = "did_tap_header_balance" + + /// Ternio API Wallet details failure + case _20210405_TAWDF = "ternio_api_wallet_details_failure" + + /// Ternio API Authenticate Enable 2FA change + case _20210804_TAA2FAC = "ternio_API_auth_2FA_change" + + /// Ternio API Wallet details success + case _20210804_TAWDS = "ternio_API_wallet_details_success" + + /// Ternio API Login + case _20210804_TAULI = "ternio_API_user_log_in" + + /// Ternio API Logout + case _20210804_TAULO = "ternio_API_user_log_out" + + /// Heartbeat check If event even happens + case _20210427_HCIEEH = "heartbeat_check_if_event_even_happens" + + /// User Tapped on UD Image + case _20220822_UTOU = "user_tapped_on_ud" + + /// User registered Pusher interest + case _20231202_RIGI = "registered_ios_general_interest" + + /// User accepted pushes + case _20231225_UAP = "user_accepted_push" + + /// User signup + case _20240101_US = "user_signup" +} diff --git a/litewallet/Constants/ArticleIds.swift b/litewallet/Constants/ArticleIds.swift new file mode 100644 index 000000000..94cfbeec1 --- /dev/null +++ b/litewallet/Constants/ArticleIds.swift @@ -0,0 +1,5 @@ +import Foundation + +enum ArticleIds { + static let nothing = "nothing" +} diff --git a/litewallet/Constants/Constants+Events.swift b/litewallet/Constants/Constants+Events.swift new file mode 100644 index 000000000..e12849d1e --- /dev/null +++ b/litewallet/Constants/Constants+Events.swift @@ -0,0 +1,226 @@ +import UIKit + +let π: CGFloat = .pi +let customUserAgent: String = "litewallet-ios" +let swiftUICellPadding = 12.0 +let bigButtonCornerRadius = 15.0 + +struct FoundationSupport { + static let dashboard = "https://support.litewallet.io/" +} + +struct APIServer { + static let baseUrl = "https://api-prod.lite-wallet.org/" +} + +struct Padding { + subscript(multiplier: Int) -> CGFloat { + return CGFloat(multiplier) * 8.0 + } + + subscript(multiplier: Double) -> CGFloat { + return CGFloat(multiplier) * 8.0 + } +} + +struct C { + static let padding = Padding() + struct Sizes { + static let buttonHeight: CGFloat = 48.0 + static let sendButtonHeight: CGFloat = 165.0 + static let headerHeight: CGFloat = 48.0 + static let largeHeaderHeight: CGFloat = 220.0 + } + + static var defaultTintColor: UIColor = UIView().tintColor + + static let animationDuration: TimeInterval = 0.3 + static let secondsInDay: TimeInterval = 86400 + static let maxMoney: UInt64 = 84_000_000 * 100_000_000 + static let satoshis: UInt64 = 100_000_000 + static let walletQueue = "com.litecoin.walletqueue" + static let btcCurrencyCode = "LTC" + static let null = "(null)" + static let maxMemoLength = 250 + static let feedbackEmail = "feedback@litecoinfoundation.zendesk.com" + static let supportEmail = "support@litecoinfoundation.zendesk.com" + + static let reviewLink = "https://itunes.apple.com/app/loafwallet-litecoin-wallet/id1119332592?action=write-review" + static let signupURL = "https://litewallet.io/mobile-signup/signup.html" + static let stagingSignupURL = "https://staging-litewallet-io.webflow.io/mobile-signup/signup" + + static var standardPort: Int { + return E.isTestnet ? 19335 : 9333 + } + + static let troubleshootingQuestions = """ + + + + + + + + + + + +
+
+ +
Please reply to this email with the following information so that we can prepare to help you solve your Litewallet issue.
+
+
1. What version of software running on your mobile device (e.g.; iOS 13.7 or iOS 14)?
+
+
+
2. What version of Litewallet software is on your mobile device (found on the login view)?
+
+
+
3. What type of iPhone do you have?
+
+
+
4. How we can help?
+
+
+
+
+ + + """ +} + +struct AppVersion { + static let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + static let versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + static let string = "v" + versionNumber! + " (\(buildNumber!))" +} + +/// False Positive Rates +/// The rate at which the requested numner of false +/// addresses are sent to the syncing node. The more +/// fp sent the less likely that the node cannot +/// identify the Litewallet user. Used when deploying the +/// Bloom Filter. The 3 options are from testing ideal +/// rates. +enum FalsePositiveRates: Double { + case lowPrivacy = 0.00005 + case semiPrivate = 0.00008 + case anonymous = 0.0005 +} + +/// Custom Event Enum: Events related to different user based actions +enum CustomEvent: String { + /// App Launched + case _20191105_AL = "app_launched" + + /// Visit Receive Controller + case _20202116_VRC = "visited_received_controller" + + /// Visit Send Controller + case _20191105_VSC = "visited_send_controller" + + /// Did Tap Buy Tab Controller + case _20191105_DTBT = "did_tap_buy_tab" + + /// Did Send LTC + case _20191105_DSL = "did_send_ltc" + + /// Did Tap Support + case _20201118_DTS = "did_tap_support" + + /// Entered dispatch group + case _20200111_DEDG = "did_enter_dispatch_group" + + /// Left dispatch group + case _20200111_DLDG = "did_leave_dispatch_group" + + /// Rate not initialized + case _20200111_RNI = "rate_not_initialized" + + /// Fee per kb not initialized + case _20200111_FNI = "feeperkb_not_initialized" + + /// Transaction not initialized + case _20200111_TNI = "transaction_not_initialized" + + /// Wallet not initialized + case _20200111_WNI = "wallet_not_initialized" + + /// Phrase not initialized + case _20200111_PNI = "phrase_not_initialized" + + /// Unable to sign transaction + case _20200111_UTST = "unable_to_sign_transaction" + + /// Generalized Error + case _20200112_ERR = "error" + + /// Keychain Lookup + case _20210804_ERR_KLF = "error_key_lookup_failure" + + /// Started resync + case _20200112_DSR = "did_start_resync" + + /// Showed review request + case _20200125_DSRR = "did_show_review_request" + + /// Unlocked in with PIN + case _20200217_DUWP = "did_unlock_with_pin" + + /// App Launched + case _20200217_DUWB = "did_unlock_with_biometrics" + + /// Did use default fee per kb + case _20200301_DUDFPK = "did_use_default_fee_per_kb" + + /// User tapped support LF + case _20201118_DTGS = "did_tap_get_support" + + /// Started IFPS Lookup + case _20201121_SIL = "started_IFPS_lookup" + + /// Resolved IPFS Address + case _20201121_DRIA = "did_resolve_IPFS_address" + + /// Failed to resolve IPFS Address + case _20201121_FRIA = "failed_resolve_IPFS_address" + + /// User tapped balance + case _20200207_DTHB = "did_tap_header_balance" + + /// Heartbeat check If event even happens + case _20210427_HCIEEH = "heartbeat_check_if_event_even_happens" + + /// User Tapped on UD Image + case _20220822_UTOU = "user_tapped_on_ud" + + /// User registered Pusher interest + case _20231202_RIGI = "registered_ios_general_interest" + + /// User accepted pushes + case _20231225_UAP = "user_accepted_push" + + /// User signup + case _20240101_US = "user_signup" + + /// Transactions info + case _20240214_TI = "transactions_info" +} diff --git a/litewallet/Constants/Constants.swift b/litewallet/Constants/Constants.swift new file mode 100644 index 000000000..def14cb0b --- /dev/null +++ b/litewallet/Constants/Constants.swift @@ -0,0 +1,127 @@ +import UIKit + +let π: CGFloat = .pi +let customUserAgent: String = "litewallet-ios" +let swiftUICellPadding = 12.0 +let bigButtonCornerRadius = 15.0 + +struct FoundationSupport { + static let dashboard = "https://support.litewallet.io/" +} + +struct APIServer { + static let baseUrl = "https://api-prod.lite-wallet.org/" +} + +struct Padding { + subscript(multiplier: Int) -> CGFloat { + return CGFloat(multiplier) * 8.0 + } + + subscript(multiplier: Double) -> CGFloat { + return CGFloat(multiplier) * 8.0 + } +} + +struct C { + static let padding = Padding() + struct Sizes { + static let buttonHeight: CGFloat = 48.0 + static let sendButtonHeight: CGFloat = 165.0 + static let headerHeight: CGFloat = 48.0 + static let largeHeaderHeight: CGFloat = 220.0 + } + + static var defaultTintColor: UIColor = UIView().tintColor + + static let animationDuration: TimeInterval = 0.3 + static let secondsInDay: TimeInterval = 86400 + static let maxMoney: UInt64 = 84_000_000 * 100_000_000 + static let satoshis: UInt64 = 100_000_000 + static let walletQueue = "com.litecoin.walletqueue" + static let btcCurrencyCode = "LTC" + static let null = "(null)" + static let maxMemoLength = 250 + static let feedbackEmail = "feedback@litecoinfoundation.zendesk.com" + static let supportEmail = "support@litecoinfoundation.zendesk.com" + + static let reviewLink = "https://itunes.apple.com/app/loafwallet-litecoin-wallet/id1119332592?action=write-review" + static let signupURL = "https://litewallet.io/mobile-signup/signup.html" + static let stagingSignupURL = "https://staging-litewallet-io.webflow.io/mobile-signup/signup" + + static var standardPort: Int { + return E.isTestnet ? 19335 : 9333 + } + + static let troubleshootingQuestions = """ + + + + + + + + + + + +
+
+ +
Please reply to this email with the following information so that we can prepare to help you solve your Litewallet issue.
+
+
1. What version of software running on your mobile device (e.g.; iOS 13.7 or iOS 14)?
+
+
+
2. What version of Litewallet software is on your mobile device (found on the login view)?
+
+
+
3. What type of iPhone do you have?
+
+
+
4. How we can help?
+
+
+
+
+ + + """ +} + +struct AppVersion { + static let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + static let versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + static let string = "v" + versionNumber! + " (\(buildNumber!))" +} + +/// False Positive Rates +/// The rate at which the requested numner of false +/// addresses are sent to the syncing node. The more +/// fp sent the less likely that the node cannot +/// identify the Litewallet user. Used when deploying the +/// Bloom Filter. The 3 options are from testing ideal +/// rates. +enum FalsePositiveRates: Double { + case lowPrivacy = 0.00005 + case semiPrivate = 0.00008 + case anonymous = 0.0005 +} diff --git a/litewallet/Constants/Functions.swift b/litewallet/Constants/Functions.swift new file mode 100644 index 000000000..99e2d5296 --- /dev/null +++ b/litewallet/Constants/Functions.swift @@ -0,0 +1,54 @@ +import UIKit + +func guardProtected(queue: DispatchQueue, callback: @escaping () -> Void) { + DispatchQueue.main.async { + if UIApplication.shared.isProtectedDataAvailable { + callback() + } else { + var observer: Any? + observer = NotificationCenter + .default + .addObserver(forName: UIApplication.protectedDataDidBecomeAvailableNotification, + object: nil, + queue: nil, + using: { _ in + queue.async { + callback() + } + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } + }) + } + } +} + +func strongify(_ context: Context, closure: @escaping (Context) -> Void) -> () -> Void +{ + return { [weak context] in + guard let strongContext = context else { return } + closure(strongContext) + } +} + +func strongify(_ context: Context?, closure: @escaping (Context, Arguments) -> Void) -> (Arguments) -> Void +{ + return { [weak context] arguments in + guard let strongContext = context else { return } + closure(strongContext, arguments) + } +} + +/// Description: 1707828867 +func tieredOpsFee(amount: UInt64) -> UInt64 { + switch amount { + case 0 ..< 250_000_000: + return 350_000 + case 250_000_000 ..< 1_000_000_000: + return 1_500_000 + case _ where amount > 1_000_000_000: + return 3_500_000 + default: + return 3_500_000 + } +} diff --git a/litewallet/Constants/Strings.swift b/litewallet/Constants/Strings.swift new file mode 100644 index 000000000..82a06f58f --- /dev/null +++ b/litewallet/Constants/Strings.swift @@ -0,0 +1,721 @@ +import Foundation + +enum S { + enum CreateStep { + enum MainTitle { + static let intro = Localization(key: "MainTitle.intro", value: "Quick Start", comment: "Litewallet Quick Start") + static let checkboxes = Localization(key: "MainTitle.checkboxes", value: "Stay Connected", comment: "Stay Connected") + static let seedPhrase = Localization(key: "MainTitle.seedPhrase", value: "Protect your Litecoin!", comment: "Protect your Litecoin!") + static let finished = Localization(key: "MainTitle.finished", value: "Confirm and Go", comment: "Confirm and Go") + } + + enum DetailedMessage { + static let intro = Localization(key: "DetailedMessage.intro", value: "Take the next 5 minutes to secure your Litecoin.", comment: "Detailed message for intro") + static let checkboxes = Localization(key: "DetailedMessage.checkboxes", value: "Sign up for the push notifications to learn Litecoin news. Unsubscribe any time in Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "DetailedMessage.seedPhrase", value: "Please write this down", comment: "Warning seed phrase") + static let finished = Localization(key: "DetailedMessage.finished", value: "Settings", comment: "Settings button label") + } + + enum ExtendedMessage { + static let intro = Localization(key: "ExtendedMessage.intro", value: "Please find a private place to write down your PIN and seed phrase.", comment: "extended message") + static let checkboxes = Localization(key: "ExtendedMessage.checkboxes", value: "Enter your email to get updates about Litewallet", comment: "Email sign up website") + static let seedPhrase = Localization(key: "ExtendedMessage.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "ExtendedMessage.finished", value: "Settings", comment: "Settings button label") + } + + enum Bullet1 { + static let intro = Localization(key: "Bullet1.intro", value: "Litewallet is from the Litecoin Foundation and Charlie Lee, creator of Litecoin.", comment: "") + static let checkboxes = Localization(key: "Bullet1.checkboxes", value: "Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "Bullet1.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "Bullet1.finished", value: "Settings", comment: "Settings button label") + } + + enum Bullet2 { + static let intro = Localization(key: "Bullet2.intro", value: "Settings", comment: "Settings button label") + static let checkboxes = Localization(key: "Bullet2.checkboxes", value: "Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "Bullet2.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "Bullet2.finished", value: "Settings", comment: "Settings button label") + } + + enum Bullet3 { + static let intro = Localization(key: "Bullet3.intro", value: "Settings", comment: "Settings button label") + static let checkboxes = Localization(key: "Bullet3.checkboxes", value: "Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "Bullet3.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "Bullet3.finished", value: "Settings", comment: "Settings button label") + } + } + + enum Symbols { + static let photons = "mł" + static let lites = "ł" + static let ltc = "Ł" + static let narrowSpace = "\u{2009}" + static let lock = "\u{1F512}" + static let redX = "\u{274C}" + static func currencyButtonTitle(maxDigits: Int) -> String { + switch maxDigits { + case 2: + return "photons\(S.Symbols.narrowSpace)(m\(S.Symbols.lites))" + case 5: + return "lites\(S.Symbols.narrowSpace)(\(S.Symbols.lites))" + case 8: + return "LTC\(S.Symbols.narrowSpace)(\(S.Symbols.ltc))" + default: + return "lites\(S.Symbols.narrowSpace)(\(S.Symbols.lites))" + } + } + } + + // MARK: - Generic Button labels + + enum Button { + static let ok = Localization(key: "Button.ok", value: "OK", comment: "OK button label") + static let cancel = Localization(key: "Button.cancel", value: "Cancel", comment: "Cancel button label") + static let settings = Localization(key: "Button.settings", value: "Settings", comment: "Settings button label") + static let submit = Localization(key: "Button.submit", value: "Submit", comment: "Settings button label") + static let ignore = Localization(key: "Button.ignore", value: "Ignore", comment: "Ignore button label") + static let yes = Localization(key: "Button.yes", value: "Yes", comment: "Yes button") + static let no = Localization(key: "Button.no", value: "No", comment: "No button") + static let send = Localization(key: "Button.send", value: "send", comment: "send button") + static let receive = Localization(key: "Button.receive", value: "receive", comment: "receive button") + static let menu = Localization(key: "Button.menu", value: "menu", comment: "menu button") + static let buy = Localization(key: "Button.buy", value: "buy", comment: "buy button") + static let resetFields = Localization(key: "Button.resetFields", value: "reset", comment: "resetFields") + } + + enum LitewalletAlert { + static let warning = Localization(key: "Alert.warning", value: "Warning", comment: "Warning alert title") + static let error = Localization(key: "Alert.error", value: "Error", comment: "Error alert title") + static let noInternet = Localization(key: "Alert.noInternet", value: "No internet connection found. Check your connection and try again.", comment: "No internet alert message") + static let corruptionError = Localization(key: "Alert.corruptionError", value: "Database Corruption Error", comment: "Corruption Error alert title") + static let corruptionMessage = Localization(key: "Alert.corruptionMessage", value: "Your local database is corrupted. Go to Settings > Blockchain: Settings > Delete Database to refresh", comment: "Corruption Error alert title") + } + + enum Scanner { + static let flashButtonLabel = Localization(key: "Scanner.flashButtonLabel", value: "Camera Flash", comment: "Scan Litecoin address camera flash toggle") + } + + enum Send { + static let title = Localization(key: "Send.title", value: "Send", comment: "Send modal title") + static let toLabel = Localization(key: "Send.toLabel", value: "To", comment: "Send money to label") + static let enterLTCAddressLabel = Localization(key: "Send.enterLTCAddress", value: "Enter LTC Address", comment: "Enter LTC Address") + static let amountLabel = Localization(key: "Send.amountLabel", value: "Amount", comment: "Send money amount label") + static let descriptionLabel = Localization(key: "Send.descriptionLabel", value: "Memo", comment: "Description for sending money label") + static let sendLabel = Localization(key: "Send.sendLabel", value: "Send", comment: "Send button label") + static let pasteLabel = Localization(key: "Send.pasteLabel", value: "Paste", comment: "Paste button label") + static let scanLabel = Localization(key: "Send.scanLabel", value: "Scan", comment: "Scan button label") + static let invalidAddressTitle = Localization(key: "Send.invalidAddressTitle", value: "Invalid Address", comment: "Invalid address alert title") + static let invalidAddressMessage = Localization(key: "Send.invalidAddressMessage", value: "The destination address is not a valid Litecoin address.", comment: "Invalid address alert message") + static let invalidAddressOnPasteboard = Localization(key: "Send.invalidAddressOnPasteboard", value: "Pasteboard does not contain a valid Litecoin address.", comment: "Invalid address on pasteboard message") + static let emptyPasteboard = Localization(key: "Send.emptyPasteboard", value: "Pasteboard is empty", comment: "Empty pasteboard error message") + static let cameraUnavailableTitle = Localization(key: "Send.cameraUnavailableTitle", value: "Litewallet is not allowed to access the camera", comment: "Camera not allowed alert title") + static let cameraUnavailableMessage = Localization(key: "Send.cameraunavailableMessage", value: "Go to Settings to allow camera access.", comment: "Camera not allowed message") + static let balance = Localization(key: "Send.balance", value: "Balance: %1$@", comment: "Balance: $4.00") + static let networkFee = Localization(key: "Send.networkFee", value: "Network", comment: "Network") + static let serviceFee = Localization(key: "Send.serviceFee", value: "Service", comment: "Service") + static let feeBlank = Localization(key: "Send.feeBlank", value: "Fees:", comment: "Fees: ") + static let bareFee = Localization(key: "Send.fee", value: "Fee: %1$@", comment: "Fee: $0.01") + static let containsAddress = Localization(key: "Send.containsAddress", value: "The destination is your own address. You cannot send to yourself.", comment: "Warning when sending to self.") + enum UsedAddress { + static let title = Localization(key: "Send.UsedAddress.title", value: "Address Already Used", comment: "Adress already used alert title") + static let firstLine = Localization(key: "Send.UsedAddress.firstLine", value: "Litecoin addresses are intended for single use only.", comment: "Adress already used alert message - first part") + static let secondLine = Localization(key: "Send.UsedAddress.secondLIne", value: "Re-use reduces privacy for both you and the recipient and can result in loss if the recipient doesn't directly control the address.", comment: "Adress already used alert message - second part") + } + + static let identityNotCertified = Localization(key: "Send.identityNotCertified", value: "Payee identity isn't certified.", comment: "Payee identity not certified alert title.") + static let createTransactionError = Localization(key: "Send.creatTransactionError", value: "Could not create transaction.", comment: "Could not create transaction alert title") + static let publicTransactionError = Localization(key: "Send.publishTransactionError", value: "Could not publish transaction.", comment: "Could not publish transaction alert title") + static let noAddress = Localization(key: "Send.noAddress", value: "Please enter the recipient's address.", comment: "Empty address alert message") + static let noAmount = Localization(key: "Send.noAmount", value: "Please enter an amount to send.", comment: "Emtpy amount alert message") + static let isRescanning = Localization(key: "Send.isRescanning", value: "Sending is disabled during a full rescan.", comment: "Is rescanning error message") + static let remoteRequestError = Localization(key: "Send.remoteRequestError", value: "Could not load payment request", comment: "Could not load remote request error message") + static let loadingRequest = Localization(key: "Send.loadingRequest", value: "Loading Request", comment: "Loading request activity view message") + static let insufficientFunds = Localization(key: "Send.insufficientFunds", value: "Insufficient Funds", comment: "Insufficient funds error") + static let barItemTitle = Localization(key: "Send.barItemTitle", value: "Send", comment: "Send Bar Item Title") + + enum UnstoppableDomains { + static let placeholder = Localization(key: "Send.UnstoppableDomains.placeholder", value: "Enter a .crypto or .zil domain", comment: "Enter a .crypto,.zil domain") + static let simplePlaceholder = Localization(key: "Send.UnstoppableDomains.simpleplaceholder", value: "Enter domain", comment: "Enter domain") + static let enterA = Localization(key: "Send.UnstoppableDomains.enterA", value: "Enter a", comment: "Enter a") + static let domain = Localization(key: "Send.UnstoppableDomains.domain", value: "domain", comment: "domain") + static let lookup = Localization(key: "Send.UnstoppableDomains.lookup", value: "Lookup", comment: "Lookup") + static let lookupFailureHeader = Localization(key: "Send.UnstoppableDomains.lookupFailureHeader", value: "LookupFailureHeader", comment: "lookupFailureHeader") + static let lookupDomainError = Localization(key: "Send.UnstoppableDomains.lookupDomainError", value: "LookupDomainError", comment: "LookupDomainError") + static let udSystemError = Localization(key: "Send.UnstoppableDomains.udSystemError", value: "UDSystemError", comment: "UDSystemError") + } + } + + enum Receive { + static let title = Localization(key: "Receive.title", value: "Receive", comment: "Receive modal title") + static let emailButton = Localization(key: "Receive.emailButton", value: "Email", comment: "Share via email button label") + static let textButton = Localization(key: "Receive.textButton", value: "Text Message", comment: "Share via text message (SMS)") + static let copied = Localization(key: "Receive.copied", value: "Copied to clipboard.", comment: "Address copied message.") + static let share = Localization(key: "Receive.share", value: "Share", comment: "Share button label") + static let request = Localization(key: "Receive.request", value: "Request an Amount", comment: "Request button label") + static let barItemTitle = Localization(key: "Receive.barItemTitle", value: "Receive", comment: "Receive Bar Item Title") + } + + // MARK: - Litewallet + + enum Litewallet { + static let name = Localization(key: "Litewallet.name", value: "Litewallet", comment: "Litewallet name") + } + + enum Account { + static let loadingMessage = Localization(key: "Account.loadingMessage", value: "Loading Wallet", comment: "Loading Wallet Message") + } + + enum History { + static let barItemTitle = Localization(key: "History.barItemTitle", value: "History", comment: "History Bar Item Title") + static let currentLitecoinValue = Localization(key: "History.currentLitecoinValue", value: "History CurrentLitecoinValue", comment: "History Current Litecoin Value") + } + + enum JailbreakWarnings { + static let title = Localization(key: "JailbreakWarnings.title", value: "WARNING", comment: "Jailbreak warning title") + static let messageWithBalance = Localization(key: "JailbreakWarnings.messageWithBalance", value: "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin! Wipe this wallet immediately and restore on a secure device.", comment: "Jailbreak warning message") + static let messageWithoutBalance = Localization(key: "JailbreakWarnings.messageWithoutBalance", value: "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin. Please only use Litewallet on a non-jailbroken device.", comment: "Jailbreak warning message") + static let ignore = Localization(key: "JailbreakWarnings.ignore", value: "Ignore", comment: "Ignore jailbreak warning button") + static let wipe = Localization(key: "JailbreakWarnings.wipe", value: "Wipe", comment: "Wipe wallet button") + static let close = Localization(key: "JailbreakWarnings.close", value: "Close", comment: "Close app button") + } + + enum ErrorMessages { + static let emailUnavailableTitle = Localization(key: "ErrorMessages.emailUnavailableTitle", value: "Email Unavailable", comment: "Email unavailable alert title") + static let emailUnavailableMessage = Localization(key: "ErrorMessages.emailUnavailableMessage", value: "This device isn't configured to send email with the iOS mail app.", comment: "Email unavailable alert title") + static let messagingUnavailableTitle = Localization(key: "ErrorMessages.messagingUnavailableTitle", value: "Messaging Unavailable", comment: "Messaging unavailable alert title") + static let messagingUnavailableMessage = Localization(key: "ErrorMessages.messagingUnavailableMessage", value: "This device isn't configured to send messages.", comment: "Messaging unavailable alert title") + } + + enum UnlockScreen { + static let myAddress = Localization(key: "UnlockScreen.myAddress", value: "My Address", comment: "My Address button title") + static let scan = Localization(key: "UnlockScreen.scan", value: "Scan", comment: "Scan button title") + static let touchIdText = Localization(key: "UnlockScreen.touchIdText", value: "Unlock with TouchID", comment: "Unlock with TouchID accessibility label") + static let touchIdPrompt = Localization(key: "UnlockScreen.touchIdPrompt", value: "Unlock your Litewallet.", comment: "TouchID/FaceID prompt text") + static let enterPIN = Localization(key: "UnlockScreen.enterPin", value: "Enter PIN", comment: "Unlock Screen sub-header") + static let unlocked = Localization(key: "UnlockScreen.unlocked", value: "Wallet Unlocked", comment: "Wallet unlocked message") + static let disabled = Localization(key: "UnlockScreen.disabled", value: "Disabled until: %1$@", comment: "Disabled until date") + static let resetPin = Localization(key: "UnlockScreen.resetPin", value: "Reset PIN", comment: "Reset PIN with Paper Key button label.") + static let faceIdText = Localization(key: "UnlockScreen.faceIdText", value: "Unlock with FaceID", comment: "Unlock with FaceID accessibility label") + } + + enum Transaction { + static let justNow = Localization(key: "Transaction.justNow", value: "just now", comment: "Timestamp label for event that just happened") + static let invalid = Localization(key: "Transaction.invalid", value: "INVALID", comment: "Invalid transaction") + static let complete = Localization(key: "Transaction.complete", value: "Complete", comment: "Transaction complete label") + static let waiting = Localization(key: "Transaction.waiting", value: "Waiting to be confirmed. Some merchants require confirmation to complete a transaction. Estimated time: 1-2 hours.", comment: "Waiting to be confirmed string") + static let starting = Localization(key: "Transaction.starting", value: "Starting balance: %1$@", comment: "eg. Starting balance: $50.00") + static let fee = Localization(key: "Transaction.fee", value: "(%1$@ total fees)", comment: "(b600 fee)") + static let ending = Localization(key: "Transaction.ending", value: "Ending balance: %1$@", comment: "eg. Ending balance: $50.00") + static let exchangeOnDaySent = Localization(key: "Transaction.exchangeOnDaySent", value: "Exchange rate when sent:", comment: "Exchange rate on date header") + static let exchangeOnDayReceived = Localization(key: "Transaction.exchangeOnDayReceived", value: "Exchange rate when received:", comment: "Exchange rate on date header") + static let receivedStatus = Localization(key: "Transaction.receivedStatus", value: "In progress: %1$@", comment: "Receive status text: 'In progress: 20%'") + static let sendingStatus = Localization(key: "Transaction.sendingStatus", value: "In progress: %1$@", comment: "Send status text: 'In progress: 20%'") + static let available = Localization(key: "Transaction.available", value: "Available to Spend", comment: "Availability status text") + static let txIDLabel = Localization(key: "Transaction.txIDLabel", value: "Transaction txID", comment: "Static TX iD Label") + static let amountDetailLabel = Localization(key: "Transaction.amountDetailLabel", value: "Transaction amount detail", comment: "Static amount Label") + static let startingAmountDetailLabel = Localization(key: "Transaction.startingAmountDetailLabel", value: "Transaction starting amount detail", comment: "Static starting amount Label") + static let endAmountDetailLabel = Localization(key: "Transaction.endAmountDetailLabel", value: "Transaction end amount detail", comment: "Static end amount Label") + static let blockHeightLabel = Localization(key: "Transaction.blockHeightLabel", value: "Transaction blockHeightLabel", comment: "Static blockHeight Label") + static let commentLabel = Localization(key: "Transaction.commentLabel", value: "Transaction comment label", comment: "Static comment Label") + } + + enum TransactionDetails { + static let title = Localization(key: "TransactionDetails.title", value: "Transaction Details", comment: "Transaction Details Title") + static let receiveModaltitle = Localization(key: "TransactionDetails.receivedModalTitle", value: "RECEIVE LTC", comment: "RECEIVE LTCTitle") + static let statusHeader = Localization(key: "TransactionDetails.statusHeader", value: "Status", comment: "Status section header") + static let commentsHeader = Localization(key: "TransactionDetails.commentsHeader", value: "Memo", comment: "Memo section header") + static let amountHeader = Localization(key: "TransactionDetails.amountHeader", value: "Amount", comment: "Amount section header") + static let emptyMessage = Localization(key: "TransactionDetails.emptyMessage", value: "Your transactions will appear here.", comment: "Empty transaction list message.") + static let txHashHeader = Localization(key: "TransactionDetails.txHashHeader", value: "Litecoin Transaction ID", comment: "Transaction ID header") + static let sentAmountDescription = Localization(key: "TransactionDetails.sentAmountDescription", value: "Sent %1@", comment: "Sent $5.00") + static let receivedAmountDescription = Localization(key: "TransactionDetails.receivedAmountDescription", value: "Received %1@", comment: "Received $5.00") + static let movedAmountDescription = Localization(key: "TransactionDetails.movedAmountDescription", value: "Moved %1@", comment: "Moved $5.00") + static let account = Localization(key: "TransactionDetails.account", value: "account", comment: "e.g. I received money from an account.") + static let sent = Localization(key: "TransactionDetails.sent", value: "Sent %1$@", comment: "Sent $5.00 (sent title 1/2)") + static let received = Localization(key: "TransactionDetails.received", value: "Received %1$@", comment: "Received $5.00 (received title 1/2)") + static let moved = Localization(key: "TransactionDetails.moved", value: "Moved %1$@", comment: "Moved $5.00") + static let to = Localization(key: "TransactionDetails.to", value: "to %1$@", comment: "[sent] to
(sent title 2/2)") + static let from = Localization(key: "TransactionDetails.from", value: "at %1$@", comment: "[received] at
(received title 2/2)") + static let blockHeightLabel = Localization(key: "TransactionDetails.blockHeightLabel", value: "Confirmed in Block", comment: "Block height label") + static let notConfirmedBlockHeightLabel = Localization(key: "TransactionDetails.notConfirmedBlockHeightLabel", value: "Not Confirmed", comment: "eg. Confirmed in Block: Not Confirmed") + static let staticTXIDLabel = Localization(key: "TransactionDetails.staticTXLabel", value: "TXID:", comment: "Label for TXID") + static let priceTimeStampLabel = Localization(key: "TransactionDetails.priceTimeStampPrefix", value: "as of", comment: "Prefix for price") + static let copyAllDetails = Localization(key: "TransactionDetails.copyAllDetails", value: "Copy all details", comment: "Copy all details") + static let copiedAll = Localization(key: "TransactionDetails.copiedAll", value: "Copied", comment: "Copied") + } + + // MARK: - Buy Center + + enum BuyCenter { + static let title = Localization(key: "BuyCenter.title", value: "Buy Litecoin", comment: "Buy Center Title") + static let buyModalTitle = Localization(key: "BuyCenter.ModalTitle", value: "Buy Łitecoin", comment: "Buy Modal Title") + enum Cells { + static let moonpayTitle = Localization(key: "BuyCenter.moonpayTitle", value: "Moonpay", comment: "Moonpay Title") + static let moonpayFinancialDetails = Localization(key: "BuyCenter.moonpayFinancialDetails", value: "• Point 1 XXXXX\n• Point 2 XXXXn• XXX Point 3", comment: "Moonpay buy financial details") + static let simplexTitle = Localization(key: "BuyCenter.simplexTitle", value: "Simplex", comment: "Simplex Title") + static let simplexFinancialDetails = Localization(key: "BuyCenter.simplexFinancialDetails", value: "• Get Litecoin in 5 mins!\n• Buy Litecoin via credit card\n• Passport or State ID", comment: "Simplex buy financial details") + static let changellyTitle = Localization(key: "BuyCenter.changellyTitle", value: "Changelly", comment: "Changelly Title") + static let changellyFinancialDetails = Localization(key: "BuyCenter.changellyFinancialDetails", value: "• Change Litecoin for other cryptos\n• No ID Required\n• Buy via credit card\n• Global coverage", comment: "Changelly buy financial details") + static let bitrefillTitle = Localization(key: "BuyCenter.BitrefillTitle", value: "Bitrefill", comment: "Bitrefill Title") + static let bitrefillFinancialDetails = Localization(key: "BuyCenter.bitrefillFinancialDetails", value: "• Buy gift cards\n• Refill prepaid phones\n• Steam, Amazon, Hotels.com\n• Works in 170 countries", comment: "Bitrefill buy financial details") + } + + static let barItemTitle = Localization(key: "BuyCenter.barItemTitle", value: "Buy", comment: "Buy Bar Item Title") + } + + // MARK: - Security Center + + enum SecurityCenter { + static let title = Localization(key: "SecurityCenter.title", value: "Security Center", comment: "Security Center Title") + static let info = Localization(key: "SecurityCenter.info", value: "Enable all security features for maximum protection.", comment: "Security Center Info") + enum Cells { + static let pinTitle = Localization(key: "SecurityCenter.pinTitle", value: "6-Digit PIN", comment: "PIN button title") + static let pinDescription = Localization(key: "SecurityCenter.pinDescription", value: "Protects your Litewallet from unauthorized users.", comment: "PIN button description") + static let touchIdTitle = Localization(key: "SecurityCenter.touchIdTitle", value: "Touch ID", comment: "Touch ID button title") + static let touchIdDescription = Localization(key: "SecurityCenter.touchIdDescription", value: "Conveniently unlock your Litewallet and send money up to a set limit.", comment: "Touch ID/FaceID button description") + static let paperKeyTitle = Localization(key: "SecurityCenter.paperKeyTitle", value: "Paper Key", comment: "Paper Key button title") + static let paperKeyDescription = Localization(key: "SecurityCenter.paperKeyDescription", value: "The only way to access your Litecoin if you lose or upgrade your phone.", comment: "Paper Key button description") + static let faceIdTitle = Localization(key: "SecurityCenter.faceIdTitle", value: "Face ID", comment: "Face ID button title") + } + } + + enum UpdatePin { + static let updateTitle = Localization(key: "UpdatePin.updateTitle", value: "Update PIN", comment: "Update PIN title") + static let createTitle = Localization(key: "UpdatePin.createTitle", value: "Set PIN", comment: "Update PIN title") + static let createTitleConfirm = Localization(key: "UpdatePin.createTitleConfirm", value: "Re-Enter PIN", comment: "Update PIN title") + static let createInstruction = Localization(key: "UpdatePin.createInstruction", value: "Your PIN will be used to unlock your Litewallet and send money.", comment: "PIN creation info.") + static let enterCurrent = Localization(key: "UpdatePin.enterCurrent", value: "Enter your current PIN.", comment: "Enter current PIN instruction") + static let enterNew = Localization(key: "UpdatePin.enterNew", value: "Enter your new PIN.", comment: "Enter new PIN instruction") + static let reEnterNew = Localization(key: "UpdatePin.reEnterNew", value: "Re-Enter your new PIN.", comment: "Re-Enter new PIN instruction") + static let caption = Localization(key: "UpdatePin.caption", value: "Remember this PIN. If you forget it, you won't be able to access your Litecoin.", comment: "Update PIN caption text") + static let setPinErrorTitle = Localization(key: "UpdatePin.setPinErrorTitle", value: "Update PIN Error", comment: "Update PIN failure alert view title") + static let setPinError = Localization(key: "UpdatePin.setPinError", value: "Sorry, could not update PIN.", comment: "Update PIN failure error message.") + } + + enum RecoverWallet { + static let next = Localization(key: "RecoverWallet.next", value: "Next", comment: "Next button label") + static let intro = Localization(key: "RecoverWallet.intro", value: "Recover your Litewallet with your paper key.", comment: "Recover wallet intro") + static let leftArrow = Localization(key: "RecoverWallet.leftArrow", value: "Left Arrow", comment: "Previous button accessibility label") + static let rightArrow = Localization(key: "RecoverWallet.rightArrow", value: "Right Arrow", comment: "Next button accessibility label") + static let done = Localization(key: "RecoverWallet.done", value: "Done", comment: "Done button text") + static let instruction = Localization(key: "RecoverWallet.instruction", value: "Enter Paper Key", comment: "Enter paper key instruction") + static let header = Localization(key: "RecoverWallet.header", value: "Recover Wallet", comment: "Recover wallet header") + static let subheader = Localization(key: "RecoverWallet.subheader", value: "Enter the paper key for the wallet you want to recover.", comment: "Recover wallet sub-header") + + static let headerResetPin = Localization(key: "RecoverWallet.header_reset_pin", value: "Reset PIN", comment: "Reset PIN with paper key: header") + static let subheaderResetPin = Localization(key: "RecoverWallet.subheader_reset_pin", value: "To reset your PIN, enter the words from your paper key into the boxes below.", comment: "Reset PIN with paper key: sub-header") + static let resetPinInfo = Localization(key: "RecoverWallet.reset_pin_more_info", value: "Tap here for more information.", comment: "Reset PIN with paper key: more information button.") + static let invalid = Localization(key: "RecoverWallet.invalid", value: "The paper key you entered is invalid. Please double-check each word and try again.", comment: "Invalid paper key message") + } + + enum ManageWallet { + static let title = Localization(key: "ManageWallet.title", value: "Manage Wallet", comment: "Manage wallet modal title") + static let textFieldLabel = Localization(key: "ManageWallet.textFeildLabel", value: "Wallet Name", comment: "Change Wallet name textfield label") + static let description = Localization(key: "ManageWallet.description", value: "Your wallet name only appears in your account transaction history and cannot be seen by anyone else.", comment: "Manage wallet description text") + static let creationDatePrefix = Localization(key: "ManageWallet.creationDatePrefix", value: "You created your wallet on %1$@", comment: "Wallet creation date prefix") + static let balance = Localization(key: "ManageWallet.balance", value: "Balance", comment: "Balance") + } + + enum AccountHeader { + static let defaultWalletName = Localization(key: "AccountHeader.defaultWalletName", value: "My Litewallet", comment: "Default wallet name") + static let manageButtonName = Localization(key: "AccountHeader.manageButtonName", value: "MANAGE", comment: "Manage wallet button title") + } + + enum VerifyPin { + static let title = Localization(key: "VerifyPin.title", value: "PIN Required", comment: "Verify PIN view title") + static let continueBody = Localization(key: "VerifyPin.continueBody", value: "Please enter your PIN to continue.", comment: "Verify PIN view body") + static let authorize = Localization(key: "VerifyPin.authorize", value: "Please enter your PIN to authorize this transaction.", comment: "Verify PIN for transaction view body") + static let touchIdMessage = Localization(key: "VerifyPin.touchIdMessage", value: "Authorize this transaction", comment: "Authorize transaction with touch id message") + } + + enum TouchIdSettings { + static let title = Localization(key: "TouchIdSettings.title", value: "Touch ID", comment: "Touch ID settings view title") + static let label = Localization(key: "TouchIdSettings.label", value: "Use your fingerprint to unlock your Litewallet and send money up to a set limit.", comment: "Touch Id screen label") + static let switchLabel = Localization(key: "TouchIdSettings.switchLabel", value: "Enable Touch ID for Litewallet", comment: "Touch id switch label.") + static let unavailableAlertTitle = Localization(key: "TouchIdSettings.unavailableAlertTitle", value: "Touch ID Not Set Up", comment: "Touch ID unavailable alert title") + static let unavailableAlertMessage = Localization(key: "TouchIdSettings.unavailableAlertMessage", value: "You have not set up Touch ID on this device. Go to Settings->Touch ID & Passcode to set it up now.", comment: "Touch ID unavailable alert message") + static let spendingLimit = Localization(key: "TouchIdSettings.spendingLimit", value: "Spending limit: %1$@ (%2$@)", comment: "Spending Limit: b100,000 ($100)") + static let limitValue = Localization(key: "TouchIdSettings.limitValue", value: "%1$@ (%2$@)", comment: " ł100,000 ($100)") + static let customizeText = Localization(key: "TouchIdSettings.customizeText", value: "You can customize your Touch ID spending limit from the %1$@.", comment: "You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button]") + static let linkText = Localization(key: "TouchIdSettings.linkText", value: "Touch ID Spending Limit Screen", comment: "Link Text (see TouchIdSettings.customizeText)") + } + + enum FaceIDSettings { + static let title = Localization(key: "FaceIDSettings.title", value: "Face ID", comment: "Face ID settings view title") + static let label = Localization(key: "FaceIDSettings.label", value: "Use your face to unlock your Litewallet and send money up to a set limit.", comment: "Face ID screen label") + static let switchLabel = Localization(key: "FaceIDSettings.switchLabel", value: "Enable Face ID for Litewallet", comment: "Face id switch label.") + static let unavailableAlertTitle = Localization(key: "FaceIDSettings.unavailableAlertTitle", value: "Face ID Not Set Up", comment: "Face ID unavailable alert title") + static let unavailableAlertMessage = Localization(key: "FaceIDSettings.unavailableAlertMessage", value: "You have not set up Face ID on this device. Go to Settings->Face ID & Passcode to set it up now.", comment: "Face ID unavailable alert message") + static let customizeText = Localization(key: "FaceIDSettings.customizeText", value: "You can customize your Face ID spending limit from the %1$@.", comment: "You can customize your Face ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button]") + static let linkText = Localization(key: "FaceIDSettings.linkText", value: "Face ID Spending Limit Screen", comment: "Link Text (see TouchIdSettings.customizeText)") + } + + enum SpendingLimit { + static let titleLabel = Localization(key: "SpendingLimit.title", value: "Current Spending Limit: ", comment: "Current spending limit:") + } + + enum TouchIdSpendingLimit { + static let title = Localization(key: "TouchIdSpendingLimit.title", value: "Touch ID Spending Limit", comment: "Touch Id spending limit screen title") + static let body = Localization(key: "TouchIdSpendingLimit.body", value: "You will be asked to enter your 6-digit PIN to send any transaction over your spending limit, and every 48 hours since the last time you entered your 6-digit PIN.", comment: "Touch ID spending limit screen body") + static let requirePasscode = Localization(key: "TouchIdSpendingLimit", value: "Always require passcode", comment: "Always require passcode option") + } + + enum FaceIdSpendingLimit { + static let title = Localization(key: "FaceIDSpendingLimit.title", value: "Face ID Spending Limit", comment: "Face Id spending limit screen title") + } + + // MARK: - Settings + + enum Settings { + static let title = Localization(key: "Settings.title", value: "Settings", comment: "Settings title") + static let wallet = Localization(key: "Settings.wallet", value: "Wallet", comment: "Wallet Settings section header") + static let manage = Localization(key: "Settings.manage", value: "Manage", comment: "Manage settings section header") + static let support = Localization(key: "Settings.support", value: "Support", comment: "Support settings section header") + static let blockchain = Localization(key: "Settings.blockchain", value: "Blockchain", comment: "Blockchain settings section header") + static let importTile = Localization(key: "Settings.importTitle", value: "Import Wallet", comment: "Import wallet label") + static let notifications = Localization(key: "Settings.notifications", value: "Notifications", comment: "Notifications label") + static let touchIdLimit = Localization(key: "Settings.touchIdLimit", value: "Touch ID Spending Limit", comment: "Touch ID spending limit label") + static let currency = Localization(key: "Settings.currency", value: "Display Currency", comment: "Default currency label") + static let sync = Localization(key: "Settings.sync", value: "Sync Blockchain", comment: "Sync blockchain label") + static let shareData = Localization(key: "Settings.shareData", value: "Share Anonymous Data", comment: "Share anonymous data label") + static let earlyAccess = Localization(key: "Settings.earlyAccess", value: "Join Early Access", comment: "Join Early access label") + static let about = Localization(key: "Settings.about", value: "About", comment: "About label") + static let review = Localization(key: "Settings.review", value: "Leave us a Review", comment: "Leave review button label") + static let enjoying = Localization(key: "Settings.enjoying", value: "Are you enjoying Litewallet?", comment: "Are you enjoying Litewallet alert message body") + static let wipe = Localization(key: "Settings.wipe", value: "Start/Recover Another Wallet", comment: "Start or recover another wallet menu label.") + static let advancedTitle = Localization(key: "Settings.advancedTitle", value: "Advanced Settings", comment: "Advanced Settings title") + static let faceIdLimit = Localization(key: "Settings.faceIdLimit", value: "Face ID Spending Limit", comment: "Face ID spending limit label") + static let languages = Localization(key: "Settings.languages", value: "Languages", comment: "Languages label") + static let litewalletVersion = Localization(key: "Settings.litewallet.version", value: "Litewallet Version:", comment: "Litewallet version") + static let litewalletEnvironment = Localization(key: "Settings.litewallet.environment", value: "Litewallet Environment:", comment: "Litewallet environment") + static let socialLinks = Localization(key: "Settings.socialLinks", value: "Social Links:", comment: "Litewallet Social links") + static let litewalletPartners = Localization(key: "Settings.litewallet.partners", value: "Litewallet Partners:", comment: "Litewallet Partners") + static let currentLocale = Localization(key: "Settings.currentLocale", value: "Current Locale:", comment: "Current Locale") + static let changeLanguageMessage = Localization(key: "Settings.ChangeLanguage.alertMessage", value: nil, comment: nil) + } + + enum About { + static let title = Localization(key: "About.title", value: "About", comment: "About screen title") + static let blog = Localization(key: "About.blog", value: "Website", comment: "About screen website label") + static let twitter = Localization(key: "About.twitter", value: "Twitter", comment: "About screen twitter label") + static let reddit = Localization(key: "About.reddit", value: "Reddit", comment: "About screen reddit label") + static let privacy = Localization(key: "About.privacy", value: "Privacy Policy", comment: "Privay Policy button label") + static let footer = Localization(key: "About.footer", value: "Made by the LiteWallet Team\nof the\nLitecoin Foundation\n%1$@", comment: "About screen footer") + } + + enum Notifications { + static let emailTitle = Localization(key: "Notifications.emailTitle", value: "Don't a miss a thing!", comment: "Email title") + static let pitchMessage = Localization(key: "Notifications.pitchMessage", value: "Sign up to hear about updates & contests from the Litewallet team!\nAccept notifications to get live news, price & market information!", comment: "Pitch to get user to sign up") + static let emailLabel = Localization(key: "Notifications.emailLabel", value: "Email address", comment: "Email address label") + static let emailPlaceholder = Localization(key: "Notifications.emailPlaceholder", value: "Enter here", comment: "Email address placeholder") + static let languagePreference = Localization(key: "Notifications.languagePreference", value: "Preferred language:", comment: "Language preference label") + static let signupCancel = Localization(key: "Notifications.signupCancel", value: "No, thanks", comment: "Signup cancel") + } + + enum DefaultCurrency { + static let rateLabel = Localization(key: "DefaultCurrency.rateLabel", value: "Exchange Rate", comment: "Exchange rate label") + static let bitcoinLabel = Localization(key: "DefaultCurrency.bitcoinLabel", value: "Litecoin Display Unit", comment: "Litecoin denomination picker label") + static let chooseFiatLabel = Localization(key: "DefaultCurrency.chooseFiatLabel", value: "Choose Fiat:", comment: "Label to pick fiat") + } + + enum SyncingView { + static let syncing = Localization(key: "SyncingView.syncing", value: "Syncing", comment: "Syncing view syncing state header text") + static let connecting = Localization(key: "SyncingView.connecting", value: "Connecting", comment: "Syncing view connectiong state header text") + } + + enum SyncingHeader { + static let syncing = Localization(key: "SyncingHeader.syncing", value: "Syncing...", comment: "Syncing view syncing state header text") + static let connecting = Localization(key: "SyncingHeader.connecting", value: "Connecting...", comment: "Syncing view connection state header text") + static let success = Localization(key: "SyncingHeader.success", value: "Success!", comment: "Syncing header success state header text") + static let rescanning = Localization(key: "SyncingHeader.rescan", value: "Rescanning...*", comment: "Rescanning header success state header text") + } + + enum ReScan { + static let header = Localization(key: "ReScan.header", value: "Sync Blockchain", comment: "Sync Blockchain view header") + static let subheader1 = Localization(key: "ReScan.subheader1", value: "Estimated time", comment: "Subheader label") + static let subheader2 = Localization(key: "ReScan.subheader2", value: "When to Sync?", comment: "Subheader label") + static let body1 = Localization(key: "ReScan.body1", value: "20-45 minutes", comment: "extimated time") + static let body2 = Localization(key: "ReScan.body2", value: "If a transaction shows as completed on the Litecoin network but not in your Litewallet.", comment: "Syncing explanation") + static let body3 = Localization(key: "ReScan.body3", value: "You repeatedly get an error saying your transaction was rejected.", comment: "Syncing explanation") + static let buttonTitle = Localization(key: "ReScan.buttonTitle", value: "Start Sync", comment: "Start Sync button label") + static let footer = Localization(key: "ReScan.footer", value: "You will not be able to send money while syncing with the blockchain.", comment: "Sync blockchain view footer") + static let alertTitle = Localization(key: "ReScan.alertTitle", value: "Sync with Blockchain?", comment: "Alert message title") + static let alertMessage = Localization(key: "ReScan.alertMessage", value: "You will not be able to send money while syncing.", comment: "Alert message body") + static let alertAction = Localization(key: "ReScan.alertAction", value: "Sync", comment: "Alert action button label") + } + + enum ShareData { + static let header = Localization(key: "ShareData.header", value: "Share Data?", comment: "Share data header") + static let body = Localization(key: "ShareData.body", value: "Help improve Litewallet by sharing your anonymous data with us. This does not include any financial information. We respect your financial privacy.", comment: "Share data view body") + static let toggleLabel = Localization(key: "ShareData.toggleLabel", value: "Share Anonymous Data?", comment: "Share data switch label.") + } + + enum ConfirmPaperPhrase { + static let word = Localization(key: "ConfirmPaperPhrase.word", value: "Word #%1$@", comment: "Word label eg. Word #1, Word #2") + static let label = Localization(key: "ConfirmPaperPhrase.label", value: "To make sure everything was written down correctly, please enter the following words from your paper key.", comment: "Confirm paper phrase view label.") + static let error = Localization(key: "ConfirmPaperPhrase.error", value: "The words entered do not match your paper key. Please try again.", comment: "Confirm paper phrase error message") + } + + enum StartPaperPhrase { + static let body = Localization(key: "StartPaperPhrase.body", value: "Your paper key is the only way to restore your Litewallet if your mobile is unavailable.\n No one in the Litecoin Foundation team can give this paper key to you!\n\nWe will show you a list of words to write down on a piece of paper and keep safe.\n\nPLEASE MAKE BACKUPS AND DON'T LOSE IT!", comment: "Paper key explanation text.") + static let buttonTitle = Localization(key: "StartPaperPhrase.buttonTitle", value: "Write Down Paper Key", comment: "button label") + static let againButtonTitle = Localization(key: "StartPaperPhrase.againButtonTitle", value: "Write Down Paper Key Again", comment: "button label") + static let date = Localization(key: "StartPaperPhrase.date", value: "You last wrote down your paper key on %1$@", comment: "Argument is date") + } + + enum WritePaperPhrase { + static let instruction = Localization(key: "WritePaperPhrase.instruction", value: "Write down each word in order and store it in a safe place.", comment: "Paper key instructions.") + static let step = Localization(key: "WritePaperPhrase.step", value: "%1$d of %2$d", comment: "1 of 3") + static let next = Localization(key: "WritePaperPhrase.next", value: "Next", comment: "button label") + static let previous = Localization(key: "WritePaperPhrase.previous", value: "Previous", comment: "button label") + } + + enum TransactionDirection { + static let to = Localization(key: "TransactionDirection.to", value: "Sent to this Address", comment: "(this transaction was) Sent to this address:") + static let received = Localization(key: "TransactionDirection.address", value: "Received at this Address", comment: "(this transaction was) Received at this address:") + } + + enum RequestAnAmount { + static let title = Localization(key: "RequestAnAmount.title", value: "Request an Amount", comment: "Request a specific amount of Litecoin") + static let noAmount = Localization(key: "RequestAnAmount.noAmount", value: "Please enter an amount first.", comment: "No amount entered error message.") + } + + // MARK: - Security Alerts + + enum SecurityAlerts { + static let pinSet = Localization(key: "Alerts.pinSet", value: "PIN Set", comment: "Alert Header label (the PIN was set)") + static let paperKeySet = Localization(key: "Alerts.paperKeySet", value: "Paper Key Set", comment: "Alert Header Label (the paper key was set)") + static let sendSuccess = Localization(key: "Alerts.sendSuccess", value: "Send Confirmation", comment: "Send success alert header label (confirmation that the send happened)") + static let resolvedSuccess = Localization(key: "Alerts.resolvedSuccess", value: "Resolved Success", comment: "Resolved Success") + static let resolvedSuccessSubheader = Localization(key: "Alerts.resolvedSuccessSubheader", value: "Resolved", comment: "Resolved Success subheader") + static let sendFailure = Localization(key: "Alerts.sendFailure", value: "Send failed", comment: "Send failure alert header label (the send failed to happen)") + static let paperKeySetSubheader = Localization(key: "Alerts.paperKeySetSubheader", value: "Awesome!", comment: "Alert Subheader label (playfully positive)") + static let sendSuccessSubheader = Localization(key: "Alerts.sendSuccessSubheader", value: "Money Sent!", comment: "Send success alert subheader label (e.g. the money was sent)") + static let copiedAddressesHeader = Localization(key: "Alerts.copiedAddressesHeader", value: "Addresses Copied", comment: "'the addresses were copied'' Alert title") + static let copiedAddressesSubheader = Localization(key: "Alerts.copiedAddressesSubheader", value: "All wallet addresses successfully copied.", comment: "Addresses Copied Alert sub header") + } + + enum MenuButton { + static let security = Localization(key: "MenuButton.security", value: "Security Center", comment: "Menu button title") + static let support = Localization(key: "MenuButton.customer.support", value: "Customer support", comment: "Menu button title") + static let settings = Localization(key: "MenuButton.settings", value: "Settings", comment: "Menu button title") + static let lock = Localization(key: "MenuButton.lock", value: "Lock Wallet", comment: "Menu button title") + static let buy = Localization(key: "MenuButton.buy", value: "Buy Litecoin", comment: "Buy Litecoin title") + } + + enum MenuViewController { + static let modalTitle = Localization(key: "MenuViewController.modalTitle", value: "Menu", comment: "Menu modal title") + } + + enum StartViewController { + static let createButton = Localization(key: "MenuViewController.createButton", value: "Create New Wallet", comment: "button label") + static let recoverButton = Localization(key: "MenuViewController.recoverButton", value: "Recover Wallet", comment: "button label") + static let tagline = Localization(key: "StartViewController.tagline", value: "The most secure and safest way to use Litecoin.", comment: "Start view message") + static let continueButton = Localization(key: "StartViewController.continueButton", value: "Continue", comment: "Continue button label") + } + + enum AccessibilityLabels { + static let close = Localization(key: "AccessibilityLabels.close", value: "Close", comment: "Close modal button accessibility label") + static let faq = Localization(key: "AccessibilityLabels.faq", value: "Support Center", comment: "Support center accessibiliy label") + } + + enum Search { + static let sent = Localization(key: "Search.sent", value: "sent", comment: "Sent filter label") + static let received = Localization(key: "Search.received", value: "received", comment: "Received filter label") + static let pending = Localization(key: "Search.pending", value: "pending", comment: "Pending filter label") + static let complete = Localization(key: "Search.complete", value: "complete", comment: "Complete filter label") + } + + enum Prompts { + static let affirm = Localization(key: "Prompts.PaperKey.affirm", value: "Continue", comment: "Affirm button title.") + static let cancel = Localization(key: "Prompts.PaperKey.cancel", value: "Cancel", comment: "Cancel button.") + static let enable = Localization(key: "Prompts.PaperKey.enable", value: "Enable", comment: "Enable button.") + static let dismiss = Localization(key: "Prompts.dismiss", value: "**Dismiss", comment: "Dismiss button.") + enum TouchId { + static let title = Localization(key: "Prompts.TouchId.title", value: "Enable Touch ID", comment: "Enable touch ID prompt title") + static let body = Localization(key: "Prompts.TouchId.body", value: "Tap here to enable Touch ID", comment: "Enable touch ID prompt body") + } + + enum PaperKey { + static let title = Localization(key: "Prompts.PaperKey.title", value: "Action Required", comment: "An action is required (You must do this action).") + static let body = Localization(key: "Prompts.PaperKey.body", value: "Your Paper Key must be kept in a safe place. It is the only way modify or restore your Litewallet or transfer your Litecoin. Please write it down.", comment: "Warning about paper key.") + } + + enum SetPin { + static let title = Localization(key: "Prompts.SetPin.title", value: "Set PIN", comment: "Set PIN prompt title.") + static let body = Localization(key: "Prompts.SetPin.body", value: "Litewallet requires a 6-digit PIN. Please set and store your PIN in a safe place.", comment: "Upgrade PIN prompt body.") + } + + enum RecommendRescan { + static let title = Localization(key: "Prompts.RecommendRescan.title", value: "Transaction Rejected", comment: "Transaction rejected prompt title") + static let body = Localization(key: "Prompts.RecommendRescan.body", value: "Your wallet may be out of sync. This can often be fixed by rescanning the blockchain.", comment: "Transaction rejected prompt body") + } + + enum NoPasscode { + static let title = Localization(key: "Prompts.NoPasscode.title", value: "Turn device passcode on", comment: "No Passcode set warning title") + static let body = Localization(key: "Prompts.NoPasscode.body", value: "A device passcode is needed to safeguard your wallet.", comment: "No passcode set warning body") + } + + enum ShareData { + static let title = Localization(key: "Prompts.ShareData.title", value: "Share Anonymous Data", comment: "Share data prompt title") + static let body = Localization(key: "Prompts.ShareData.body", value: "Help improve Litewallet by sharing your anonymous data with us", comment: "Share data prompt body") + } + + enum FaceId { + static let title = Localization(key: "Prompts.FaceId.title", value: "Enable Face ID", comment: "Enable face ID prompt title") + static let body = Localization(key: "Prompts.FaceId.body", value: "Tap here to enable Face ID", comment: "Enable face ID prompt body") + } + } + + // MARK: - Payment Protocol + + enum PaymentProtocol { + enum Errors { + static let untrustedCertificate = Localization(key: "PaymentProtocol.Errors.untrustedCertificate", value: "untrusted certificate", comment: "Untrusted certificate payment protocol error message") + static let missingCertificate = Localization(key: "PaymentProtocol.Errors.missingCertificate", value: "missing certificate", comment: "Missing certificate payment protocol error message") + static let unsupportedSignatureType = Localization(key: "PaymentProtocol.Errors.unsupportedSignatureType", value: "unsupported signature type", comment: "Unsupported signature type payment protocol error message") + static let requestExpired = Localization(key: "PaymentProtocol.Errors.requestExpired", value: "request expired", comment: "Request expired payment protocol error message") + static let badPaymentRequest = Localization(key: "PaymentProtocol.Errors.badPaymentRequest", value: "Bad Payment Request", comment: "Bad Payment request alert title") + static let smallOutputErrorTitle = Localization(key: "PaymentProtocol.Errors.smallOutputError", value: "Couldn't make payment", comment: "Payment too small alert title") + static let smallPayment = Localization(key: "PaymentProtocol.Errors.smallPayment", value: "Litecoin payments can't be less than %1$@.", comment: "Amount too small error message") + static let smallTransaction = Localization(key: "PaymentProtocol.Errors.smallTransaction", value: "Litecoin transaction outputs can't be less than $@.", comment: "Output too small error message.") + static let corruptedDocument = Localization(key: "PaymentProtocol.Errors.corruptedDocument", value: "Unsupported or corrupted document", comment: "Error opening payment protocol file message") + } + } + + enum URLHandling { + static let addressListAlertTitle = Localization(key: "URLHandling.addressListAlertTitle", value: "Copy Wallet Addresses", comment: "Authorize to copy wallet address alert title") + static let addressListAlertMessage = Localization(key: "URLHandling.addressaddressListAlertMessage", value: "Copy wallet addresses to clipboard?", comment: "Authorize to copy wallet addresses alert message") + static let addressListVerifyPrompt = Localization(key: "URLHandling.addressList", value: "Authorize to copy wallet address to clipboard", comment: "Authorize to copy wallet address PIN view prompt.") + static let copy = Localization(key: "URLHandling.copy", value: "Copy", comment: "Copy wallet addresses alert button label") + } + + enum ApiClient { + static let notReady = Localization(key: "ApiClient.notReady", value: "Wallet not ready", comment: "Wallet not ready error message") + static let jsonError = Localization(key: "ApiClient.jsonError", value: "JSON Serialization Error", comment: "JSON Serialization error message") + static let tokenError = Localization(key: "ApiClient.tokenError", value: "Unable to retrieve API token", comment: "API Token error message") + } + + enum CameraPlugin { + static let centerInstruction = Localization(key: "CameraPlugin.centerInstruction", value: "Center your ID in the box", comment: "Camera plugin instruction") + } + + enum LocationPlugin { + static let disabled = Localization(key: "LocationPlugin.disabled", value: "Location services are disabled.", comment: "Location services disabled error") + static let notAuthorized = Localization(key: "LocationPlugin.notAuthorized", value: "Litewallet does not have permission to access location services.", comment: "No permissions for location services") + } + + enum Webview { + static let updating = Localization(key: "Webview.updating", value: "Updating...", comment: "Updating webview message") + static let errorMessage = Localization(key: "Webview.errorMessage", value: "There was an error loading the content. Please try again.", comment: "Webview loading error message") + static let dismiss = Localization(key: "Webview.dismiss", value: "Dismiss", comment: "Dismiss button label") + } + + enum TimeSince { + static let seconds = Localization(key: "TimeSince.seconds", value: "%1$@ s", comment: "6 s (6 seconds)") + static let minutes = Localization(key: "TimeSince.minutes", value: "%1$@ m", comment: "6 m (6 minutes)") + static let hours = Localization(key: "TimeSince.hours", value: "%1$@ h", comment: "6 h (6 hours)") + static let days = Localization(key: "TimeSince.days", value: "%1$@ d", comment: "6 d (6 days)") + } + + enum Import { + static let leftCaption = Localization(key: "Import.leftCaption", value: "Wallet to be imported", comment: "Caption for graphics") + static let rightCaption = Localization(key: "Import.rightCaption", value: "Your Litewallet Wallet", comment: "Caption for graphics") + static let importMessage = Localization(key: "Import.message", value: "Importing a wallet transfers all the money from your other wallet into your Litewallet wallet using a single transaction.", comment: "Import wallet intro screen message") + static let importWarning = Localization(key: "Import.warning", value: "Importing a wallet does not include transaction history or other details.", comment: "Import wallet intro warning message") + static let scan = Localization(key: "Import.scan", value: "Scan Private Key", comment: "Scan Private key button label") + static let title = Localization(key: "Import.title", value: "Import Wallet", comment: "Import Wallet screen title") + static let importing = Localization(key: "Import.importing", value: "Importing Wallet", comment: "Importing wallet progress view label") + static let confirm = Localization(key: "Import.confirm", value: "Send %1$@ from this private key into your wallet? The Litecoin network will receive a fee of %2$@.", comment: "Sweep private key confirmation message") + static let checking = Localization(key: "Import.checking", value: "Checking private key balance...", comment: "Checking private key balance progress view text") + static let password = Localization(key: "Import.password", value: "This private key is password protected.", comment: "Enter password alert view title") + static let passwordPlaceholder = Localization(key: "Import.passwordPlaceholder", value: "password", comment: "password textfield placeholder") + static let unlockingActivity = Localization(key: "Import.unlockingActivity", value: "Unlocking Key", comment: "Unlocking Private key activity view message.") + static let importButton = Localization(key: "Import.importButton", value: "Import", comment: "Import button label") + static let success = Localization(key: "Import.success", value: "Success", comment: "Import wallet success alert title") + static let successBody = Localization(key: "Import.SuccessBody", value: "Successfully imported wallet.", comment: "Successfully imported wallet message body") + static let wrongPassword = Localization(key: "Import.wrongPassword", value: "Wrong password, please try again.", comment: "Wrong password alert message") + enum Error { + static let notValid = Localization(key: "Import.Error.notValid", value: "Not a valid private key", comment: "Not a valid private key error message") + static let duplicate = Localization(key: "Import.Error.duplicate", value: "This private key is already in your wallet.", comment: "Duplicate key error message") + static let empty = Localization(key: "Import.Error.empty", value: "This private key is empty.", comment: "empty private key error message") + static let highFees = Localization(key: "Import.Error.highFees", value: "Transaction fees would cost more than the funds available on this private key.", comment: "High fees error message") + static let signing = Localization(key: "Import.Error.signing", value: "Error signing transaction", comment: "Import signing error message") + } + } + + enum WipeWallet { + static let title = Localization(key: "WipeWallet.title", value: "Start or Recover Another Wallet", comment: "Wipe wallet navigation item title.") + static let alertTitle = Localization(key: "WipeWallet.alertTitle", value: "Wipe Wallet?", comment: "Wipe wallet alert title") + static let alertMessage = Localization(key: "WipeWallet.alertMessage", value: "Are you sure you want to delete this wallet?", comment: "Wipe wallet alert message") + static let wipe = Localization(key: "WipeWallet.wipe", value: "Wipe", comment: "Wipe wallet button title") + static let wiping = Localization(key: "WipeWallet.wiping", value: "Wiping...", comment: "Wiping activity message") + static let failedTitle = Localization(key: "WipeWallet.failedTitle", value: "Failed", comment: "Failed wipe wallet alert title") + static let failedMessage = Localization(key: "WipeWallet.failedMessage", value: "Failed to wipe wallet.", comment: "Failed wipe wallet alert message") + static let instruction = Localization(key: "WipeWallet.instruction", value: "To start a new wallet or restore an existing wallet, you must first erase the wallet that is currently installed. To continue, enter the current wallet's Paper Key.", comment: "Enter key to wipe wallet instruction.") + static let startMessage = Localization(key: "WipeWallet.startMessage", value: "Starting or recovering another wallet allows you to access and manage a different Litewallet wallet on this device.", comment: "Start wipe wallet view message") + static let startWarning = Localization(key: "WipeWallet.startWarning", value: "Your current wallet will be removed from this device. If you wish to restore it in the future, you will need to enter your Paper Key.", comment: "Start wipe wallet view warning") + static let emptyWallet = Localization(key: "WipeWallet.emptyWallet", value: "Forget seed or PIN?", comment: "Warning if user lost phrase") + static let resetTitle = Localization(key: "resetTitle", value: " Delete my Litewallet ", comment: "Warning Empty Wipe title") + static let resetButton = Localization(key: "resetButton", value: "Yes, reset wallet", comment: "Reset walet button title") + static let warningTitle = Localization(key: "WipeWallet.warningTitle", value: "PLEASE READ!", comment: "Warning title") + static let warningDescription = Localization(key: "WipeWallet.warningDescription", value: "Your LiteWallet is empty. Resetting will delete the old private key and wipe the app data.\n\nAfter the reset, be prepared to record the new 12 words and keep them in a very secure place.\n\nNo LiteWallet developers can retrieve this seed for you.", comment: "Warning description") + static let warningAlert = Localization(key: "WipeWallet.warningAlert", value: "DO NOT LOSE IT!", comment: "Warning Alert") + static let deleteDatabase = Localization(key: "WipeWallet.deleteDatabase", value: "Delete database", comment: "Delete db") + static let alertDeleteTitle = Localization(key: "WipeWallet.alertDeleteTitle", value: "Delet Database", comment: "Delete database title") + static let deleteMessageTitle = Localization(key: "WipeWallet.deleteMessageTitle", value: "This deletes the database but retains the PIN and phrase. You will be asked to confirm your existing PIN, seed and will re-sync the new db", comment: "Delete database message") + static let deleteSync = Localization(key: "WipeWallet.deleteSync", value: "Delete & Sync", comment: "Delete and sync") + } + + enum FeeSelector { + static let title = Localization(key: "FeeSelector.title", value: "Processing Speed", comment: "Fee Selector title") + static let regularLabel = Localization(key: "FeeSelector.regularLabel", value: "Estimated Delivery: 2.5 - 5+ minutes", comment: "Fee Selector regular fee description") + static let economyLabel = Localization(key: "FeeSelector.economyLabel", value: "Estimated Delivery: ~10 minutes", comment: "Fee Selector economy fee description") + static let luxuryLabel = Localization(key: "FeeSelector.luxuryLabel", value: "Delivery: 2.5 - 5+ minutes", comment: "Fee Selector luxury fee description") + static let economyWarning = Localization(key: "FeeSelector.economyWarning", value: "This option is not recommended for time-sensitive transactions.", comment: "Warning message for economy fee") + static let luxuryMessage = Localization(key: "FeeSelector.luxuryMessage", value: "This option virtually guarantees acceptance of your transaction while you pay a premium.", comment: "Message for luxury fee") + + static let regular = Localization(key: "FeeSelector.regular", value: "Regular", comment: "Regular fee") + static let economy = Localization(key: "FeeSelector.economy", value: "Economy", comment: "Economy fee") + static let luxury = Localization(key: "FeeSelector.luxury", value: "Luxury", comment: "Luxury fee") + } + + enum Confirmation { + static let title = Localization(key: "Confirmation.title", value: "Confirmation", comment: "Confirmation Screen title") + static let send = Localization(key: "Confirmation.send", value: "Send", comment: "Send: (amount)") + static let to = Localization(key: "Confirmation.to", value: "To", comment: "To: (address)") + static let staticAddressLabel = Localization(key: "Confirmation.staticAddressLabel", value: "ADDRESS:", comment: "Address label") + + static let processingTime = Localization(key: "Confirmation.processingTime", value: "Processing time: This transaction will take %1$@ minutes to process.", comment: "eg. Processing time: This transaction will take 10-30 minutes to process.") + static let processingAndDonationTime = Localization(key: "Confirmation.processingAndDonationTime", value: "Processing time: These transactions will take %1$@ minutes to process.", comment: "eg. Processing with Donation time: This transaction will take 10-30 minutes to process.") + static let amountLabel = Localization(key: "Confirmation.amountLabel", value: "Amount to Send:", comment: "Amount to Send: ($1.00)") + static let donateLabel = Localization(key: "Confirmation.donateLabel", value: "Amount to Donate:", comment: "Amount to Donate: ($1.00)") + + static let totalLabel = Localization(key: "Confirmation.totalLabel", value: "Total Cost:", comment: "Total Cost: ($5.00)") + static let amountDetailLabel = Localization(key: "Confirmation.amountDetailLabel", value: "Exchange details:", comment: "$53.09/L + 1.07%") + } + + enum NodeSelector { + static let manualButton = Localization(key: "NodeSelector.manualButton", value: "Switch to Manual Mode", comment: "Switch to manual mode button label") + static let automaticButton = Localization(key: "NodeSelector.automaticButton", value: "Switch to Automatic Mode", comment: "Switch to automatic mode button label") + static let title = Localization(key: "NodeSelector.title", value: "Litecoin Nodes", comment: "Node Selector view title") + static let nodeLabel = Localization(key: "NodeSelector.nodeLabel", value: "Current Primary Node", comment: "Node address label") + static let statusLabel = Localization(key: "NodeSelector.statusLabel", value: "Node Connection Status", comment: "Node status label") + static let connected = Localization(key: "NodeSelector.connected", value: "Connected", comment: "Node is connected label") + static let notConnected = Localization(key: "NodeSelector.notConnected", value: "Not Connected", comment: "Node is not connected label") + static let enterTitle = Localization(key: "NodeSelector.enterTitle", value: "Enter Node", comment: "Enter Node ip address view title") + static let enterBody = Localization(key: "NodeSelector.enterBody", value: "Enter Node IP address and port (optional)", comment: "Enter node ip address view body") + } + + enum Welcome { + static let title = Localization(key: "Welcome.title", value: "Welcome to Litewallet", comment: "Welcome view title") + static let body = Localization(key: "Welcome.body", value: "Litewallet now has a brand new look and some new features.\n\nAll coins are displayed in lites (ł). 1 Litecoin (Ł) = 1000 lites (ł).", comment: "Welcome view body text") + } + + enum Fragments { + static let or = Localization(key: "Fragment.or", value: "or", comment: "Or") + static let confirm = Localization(key: "Fragment.confirm", value: "confirm", comment: "Confirm") + static let to = Localization(key: "Fragment.to", value: "to", comment: "to") + static let sorry = Localization(key: "Fragment.sorry", value: "sorry", comment: "sorry") + } +} diff --git a/litewallet/Controls/MenuButton.swift b/litewallet/Controls/MenuButton.swift new file mode 100644 index 000000000..bf24d6207 --- /dev/null +++ b/litewallet/Controls/MenuButton.swift @@ -0,0 +1,67 @@ +import UIKit + +class MenuButton: UIControl { + // MARK: - Public + + let type: MenuButtonType + + init(type: MenuButtonType) { + self.type = type + super.init(frame: .zero) + + if #available(iOS 11.0, *) { + label.textColor = UIColor(named: "labelTextColor") + self.backgroundColor = UIColor(named: "lfBackgroundColor") + image.tintColor = .whiteTint + } + + setupViews() + } + + // MARK: - Private + + private let label = UILabel(font: .customBody(size: 16.0)) + private let image = UIImageView() + private let border = UIView() + + override var isHighlighted: Bool { + didSet { + if isHighlighted { + backgroundColor = .litecoinSilver + } else { + backgroundColor = .white + } + } + } + + private func setupViews() { + addSubview(label) + addSubview(image) + addSubview(border) + + label.constrain([ + label.constraint(.centerY, toView: self, constant: 0.0), + label.constraint(.leading, toView: self, constant: C.padding[2]), + ]) + image.constrain([ + image.constraint(.centerY, toView: self, constant: 0.0), + image.constraint(.trailing, toView: self, constant: -C.padding[4]), + image.constraint(.width, constant: 16.0), + image.constraint(.height, constant: 16.0), + ]) + border.constrainBottomCorners(sidePadding: 0, bottomPadding: 0) + border.constrain([ + border.constraint(.height, constant: 1.0), + ]) + + label.text = type.title + image.image = type.image + image.contentMode = .scaleAspectFit + border.backgroundColor = .secondaryShadow + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Controls/MenuButtonType.swift b/litewallet/Controls/MenuButtonType.swift new file mode 100644 index 000000000..97e9bf95d --- /dev/null +++ b/litewallet/Controls/MenuButtonType.swift @@ -0,0 +1,34 @@ +import UIKit + +enum MenuButtonType { + case security + case customerSupport + case settings + case lock + + var title: String { + switch self { + case .security: + return S.MenuButton.security.localize() + case .customerSupport: + return S.MenuButton.support.localize() + case .settings: + return S.MenuButton.settings.localize() + case .lock: + return S.MenuButton.lock.localize() + } + } + + var image: UIImage { + switch self { + case .security: + return #imageLiteral(resourceName: "Shield") + case .customerSupport: + return #imageLiteral(resourceName: "FaqFill") + case .settings: + return #imageLiteral(resourceName: "Settings") + case .lock: + return #imageLiteral(resourceName: "Lock") + } + } +} diff --git a/litewallet/Controls/SegmentedButton.swift b/litewallet/Controls/SegmentedButton.swift new file mode 100644 index 000000000..ef99db7dc --- /dev/null +++ b/litewallet/Controls/SegmentedButton.swift @@ -0,0 +1,46 @@ +import UIKit + +enum SegmentedButtonType { + case left + case right +} + +class SegmentedButton: UIControl { + // MARK: - Public + + init(title: String, type: SegmentedButtonType) { + self.title = title + self.type = type + super.init(frame: .zero) + accessibilityLabel = title + setupViews() + } + + // MARK: - Private + + private let title: String + private let type: SegmentedButtonType + private let label = UILabel(font: .customMedium(size: 13.0), color: .white) + + override var isHighlighted: Bool { + didSet { + if isHighlighted { + backgroundColor = UIColor(white: 1.0, alpha: 0.4) + } else { + backgroundColor = .clear + } + } + } + + private func setupViews() { + addSubview(label) + label.constrain(toSuperviewEdges: nil) + label.textAlignment = .center + label.text = title + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Environment.swift b/litewallet/Environment.swift new file mode 100644 index 000000000..069cd7708 --- /dev/null +++ b/litewallet/Environment.swift @@ -0,0 +1,147 @@ +import UIKit + +/// 14 Languages +enum LanguageSelection: Int, CaseIterable, Equatable, Identifiable { + case English = 0 + case ChineseTraditional + case ChineseSimplified + case French + case German + case Indonesian + case Italian + case Japan + case Korean + case Portuguese + case Russian + case Spanish + case Turkish + case Ukrainian + var id: LanguageSelection { self } + + var code: String { + switch self { + case .English: return "en" + case .ChineseTraditional: return "zh" + case .ChineseSimplified: return "zh" + case .French: return "fr" + case .German: return "de" + case .Indonesian: return "id" + case .Italian: return "it" + case .Japan: return "ja" + case .Korean: return "ko" + case .Portuguese: return "pt" + case .Russian: return "ru" + case .Spanish: return "es" + case .Turkish: return "tr" + case .Ukrainian: return "uk" + } + } + + var nativeName: String { + switch self { + case .English: return "English" + case .ChineseTraditional: return "中國人" + case .ChineseSimplified: return "中国人" + case .French: return "Français" + case .German: return "Deutsch" + case .Indonesian: return "Bahasa Indonesia" + case .Italian: return "Italiano" + case .Japan: return "日本語" + case .Korean: return "한국인" + case .Portuguese: return "Português" + case .Russian: return "Русский" + case .Spanish: return "Español" + case .Turkish: return "Türkçe" + case .Ukrainian: return "українська" + } + } + + var voiceFilename: String { + switch self { + case .English: return "English" + case .ChineseTraditional: return "中國人" + case .ChineseSimplified: return "中國人" + case .French: return "Français" + case .German: return "Deutsch" + case .Indonesian: return "BahasaIndonesia" + case .Italian: return "Italiano" + case .Japan: return "日本語" + case .Korean: return "한국인" + case .Portuguese: return "Português" + case .Russian: return "Русский" + case .Spanish: return "Español" + case .Turkish: return "Türkçe" + case .Ukrainian: return "українська" + } + } +} + +struct E { + static let isTestnet: Bool = { + #if Testnet + return true + #else + return false + #endif + }() + + static let isTestFlight: Bool = { + #if Testflight + return true + #else + return false + #endif + }() + + static let isSimulator: Bool = { + #if arch(i386) || arch(x86_64) + return true + #else + return false + #endif + }() + + static let isDebug: Bool = { + #if Debug + return true + #else + return false + #endif + }() + + static let isRelease: Bool = { + #if Release + return true + #else + return false + #endif + }() + + static let isScreenshots: Bool = { + #if Screenshots + return true + #else + return false + #endif + }() + + static var isIPhone4: Bool { + return (UIScreen.main.bounds.size.height == 480.0) + } + + static var isIPhone5: Bool { + return (UIScreen.main.bounds.size.height == 568.0) && (E.is32Bit) + } + + static var isIPhoneX: Bool { + return (UIScreen.main.bounds.size.height == 812.0) + } + + static let is32Bit: Bool = { + MemoryLayout.size == MemoryLayout.size + }() + + static var screenHeight: CGFloat { + return UIScreen.main.bounds.size.height + } +} diff --git a/litewallet/Extensions/ApplicationController+Extension.swift b/litewallet/Extensions/ApplicationController+Extension.swift new file mode 100644 index 000000000..5426f1577 --- /dev/null +++ b/litewallet/Extensions/ApplicationController+Extension.swift @@ -0,0 +1,35 @@ +import Foundation +import StoreKit + +extension ApplicationController { + func setupDefaults() { + if UserDefaults.standard.object(forKey: shouldRequireLoginTimeoutKey) == nil { + UserDefaults.standard.set(60.0 * 3.0, forKey: shouldRequireLoginTimeoutKey) // Default 3 min timeout + } + if UserDefaults.standard.object(forKey: hasSeenAnnounceView) == nil { + UserDefaults.standard.set(false, forKey: hasSeenAnnounceView) // Hasnt seen the Announce View + } + } + + func countLaunches() { + if var launchNumber = UserDefaults.standard.object(forKey: numberOfLitewalletLaunches) as? Int { + launchNumber += 1 + UserDefaults.standard.set(NSNumber(value: launchNumber), forKey: numberOfLitewalletLaunches) + if launchNumber == 5 { + SKStoreReviewController.requestReview() + + SKStoreReviewController.requestReviewInCurrentScene() + + // iOSAppStoreURLFormat = @"itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=%d"; + // [NSURL URLWithString:[NSString stringWithFormat:([[UIDevice currentDevice].systemVersion floatValue] >= 7.0f)? iOS7AppStoreURLFormat: iOSAppStoreURLFormat, YOUR_APP_STORE_ID]]; // Would contain the right link + + LWAnalytics.logEventWithParameters(itemName: ._20200125_DSRR) + } + } else { + UserDefaults.standard.set(NSNumber(value: 1), forKey: numberOfLitewalletLaunches) + } + } + + func willResignActive() + {} +} diff --git a/litewallet/Extensions/Async.swift b/litewallet/Extensions/Async.swift new file mode 100644 index 000000000..22dbf25c6 --- /dev/null +++ b/litewallet/Extensions/Async.swift @@ -0,0 +1,22 @@ +import Foundation + +enum Async { + static func parallel(callbacks: [(@escaping () -> Void) -> Void], completion: @escaping () -> Void) + { + let dispatchGroup = DispatchGroup() + callbacks.forEach { cb in + dispatchGroup.enter() + cb { + dispatchGroup.leave() + } + } + dispatchGroup.notify(queue: .main) { + completion() + } + } +} + +func delay(_ delay: Double, closure: @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) +} diff --git a/litewallet/Extensions/CGContext+Additions.swift b/litewallet/Extensions/CGContext+Additions.swift new file mode 100644 index 000000000..29b720a3a --- /dev/null +++ b/litewallet/Extensions/CGContext+Additions.swift @@ -0,0 +1,11 @@ +import UIKit + +extension CGContext { + func addLineThrough(_ points: [(CGFloat, CGFloat)]) { + guard let first = points.first else { return } + move(to: CGPoint(x: first.0, y: first.1)) + points.dropFirst().forEach { + addLine(to: CGPoint(x: $0.0, y: $0.1)) + } + } +} diff --git a/litewallet/Extensions/CGRect+Additions.swift b/litewallet/Extensions/CGRect+Additions.swift new file mode 100644 index 000000000..91aafee73 --- /dev/null +++ b/litewallet/Extensions/CGRect+Additions.swift @@ -0,0 +1,14 @@ +import UIKit + +extension CGRect { + var center: CGPoint { + return CGPoint(x: midX, y: midY) + } + + func expandVertically(_ deltaY: CGFloat) -> CGRect { + var newFrame = self + newFrame.origin.y = newFrame.origin.y - deltaY + newFrame.size.height = newFrame.size.height + deltaY + return newFrame + } +} diff --git a/litewallet/Extensions/CustomTitleView.swift b/litewallet/Extensions/CustomTitleView.swift new file mode 100644 index 000000000..70674b64c --- /dev/null +++ b/litewallet/Extensions/CustomTitleView.swift @@ -0,0 +1,89 @@ +import UIKit + +protocol CustomTitleView { + var customTitle: String { get } + var titleLabel: UILabel { get } + var navigationItem: UINavigationItem { get } +} + +private struct AssociatedKeys { + static var label = "label" + static var yPosition = "yPosition" +} + +private let restingLabelPosition: CGFloat = 60.0 + +extension CustomTitleView { + var label: UILabel { + var textColor: UIColor + + if #available(iOS 11.0, *) { + textColor = UIColor(named: "labelTextColor")! + } else { + textColor = .darkText + } + + guard let label = objc_getAssociatedObject(self, &AssociatedKeys.label) as? UILabel + else { + let newLabel = UILabel(font: .customBold(size: 17.0), color: textColor) + objc_setAssociatedObject(self, &AssociatedKeys.label, newLabel, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return newLabel + } + return label + } + + var yPosition: NSLayoutConstraint? { + get { + guard let yPosition = objc_getAssociatedObject(self, &AssociatedKeys.yPosition) as? NSLayoutConstraint + else { + return nil + } + return yPosition + } + set { + guard let newValue = newValue else { return } + objc_setAssociatedObject(self, &AssociatedKeys.yPosition, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func addCustomTitle() { + let titleView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 40)) + + if #available(iOS 11.0, *) { + titleView.backgroundColor = UIColor(named: "lfBackgroundColor") + label.textColor = UIColor(named: "labelTextColor") + } else { + titleView.backgroundColor = .clear + label.textColor = .darkText + } + + titleView.clipsToBounds = true + label.text = customTitle + titleView.addSubview(label) + let newYPosition = label.centerYAnchor.constraint(equalTo: titleView.centerYAnchor) + newYPosition.constant = restingLabelPosition + objc_setAssociatedObject(self, &AssociatedKeys.yPosition, newYPosition, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + label.constrain([ + yPosition, + label.centerXAnchor.constraint(equalTo: titleView.centerXAnchor), + ]) + navigationItem.titleView = titleView + } + + func didScrollForCustomTitle(yOffset: CGFloat) { + let progress = min(yOffset / restingLabelPosition, 1.0) + titleLabel.alpha = 1.0 - progress + yPosition?.constant = restingLabelPosition - (restingLabelPosition * progress) + } + + func scrollViewWillEndDraggingForCustomTitle(yOffset: CGFloat) { + let progress = min(yOffset / restingLabelPosition, 1.0) + if progress > 0.2 { + UIView.animate(withDuration: 0.3, animations: { + self.yPosition?.constant = 0.0 + self.titleLabel.alpha = 0.0 + self.titleLabel.superview?.layoutIfNeeded() + }) + } + } +} diff --git a/litewallet/Extensions/Date+Additions.swift b/litewallet/Extensions/Date+Additions.swift new file mode 100644 index 000000000..dba297886 --- /dev/null +++ b/litewallet/Extensions/Date+Additions.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Date { + func hasEqualYear(_ date: Date) -> Bool { + return Calendar.current.compare(self, to: date, toGranularity: .year) == .orderedSame + } + + func hasEqualMonth(_ date: Date) -> Bool { + return Calendar.current.compare(self, to: date, toGranularity: .month) == .orderedSame + } +} diff --git a/litewallet/Extensions/DispatchQueue+Additions.swift b/litewallet/Extensions/DispatchQueue+Additions.swift new file mode 100644 index 000000000..3d1e31efb --- /dev/null +++ b/litewallet/Extensions/DispatchQueue+Additions.swift @@ -0,0 +1,7 @@ +import Foundation + +extension DispatchQueue { + static var walletQueue: DispatchQueue = .init(label: C.walletQueue) + + static let walletConcurrentQueue: DispatchQueue = .init(label: C.walletQueue, attributes: .concurrent) +} diff --git a/litewallet/Extensions/LAContext+Extensions.swift b/litewallet/Extensions/LAContext+Extensions.swift new file mode 100644 index 000000000..7f4b69d11 --- /dev/null +++ b/litewallet/Extensions/LAContext+Extensions.swift @@ -0,0 +1,51 @@ +import Foundation +import LocalAuthentication + +extension LAContext { + static var canUseBiometrics: Bool { + return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil) + } + + static var isBiometricsAvailable: Bool { + var error: NSError? + if LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { + return true + } else { + if error?.code == LAError.biometryNotAvailable.rawValue { + return false + } else { + return true + } + } + } + + static var isPasscodeEnabled: Bool { + return LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) + } + + static func biometricType() -> BiometricType { + let context = LAContext() + if #available(iOS 11, *) { + _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) + switch context.biometryType { + case .none: + return .none + case .touchID: + return .touch + case .faceID: + return .face + case .opticID: + return .optical + } + } else { + return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) ? .touch : .none + } + } + + enum BiometricType { + case none + case touch + case face + case optical + } +} diff --git a/litewallet/Extensions/NumberFormatter+Additions.swift b/litewallet/Extensions/NumberFormatter+Additions.swift new file mode 100644 index 000000000..7875041e4 --- /dev/null +++ b/litewallet/Extensions/NumberFormatter+Additions.swift @@ -0,0 +1,28 @@ +import Foundation + +extension NumberFormatter { + static func formattedString(amount: Satoshis, rate: Rate?, minimumFractionDigits: Int?, maxDigits: Int) -> String + { + let displayAmount = Amount(amount: amount.rawValue, rate: rate ?? Rate.empty, maxDigits: maxDigits) + var formatter: NumberFormatter + var output = "" + if let rate = rate { + formatter = NumberFormatter() + if let minimumFractionDigits = minimumFractionDigits { + formatter.minimumFractionDigits = minimumFractionDigits + } + formatter.locale = rate.locale + formatter.numberStyle = .currency + let value = (Double(amount.rawValue) / Double(C.satoshis)) * rate.rate + output = formatter.string(from: value as NSNumber) ?? "error" + } else { + formatter = displayAmount.ltcFormat + if let minimumFractionDigits = minimumFractionDigits { + formatter.minimumFractionDigits = minimumFractionDigits + } + let bits = Bits(satoshis: amount) + output = formatter.string(from: bits.rawValue as NSNumber) ?? "error" + } + return output + } +} diff --git a/litewallet/Extensions/SafariServices+Extension.swift b/litewallet/Extensions/SafariServices+Extension.swift new file mode 100644 index 000000000..19605b897 --- /dev/null +++ b/litewallet/Extensions/SafariServices+Extension.swift @@ -0,0 +1,95 @@ +import Foundation +import SafariServices +import SwiftUI +import UIKit +import WebKit + +// inspired https://www.swiftyplace.com/blog/loading-a-web-view-in-swiftui-with-wkwebview + +struct WebView: UIViewRepresentable { + let url: URL + @Binding + var scrollToSignup: Bool + + @State + private + var didStartEditing: Bool = false + + func makeUIView(context _: Context) -> WKWebView { + let webview = SignupWebView(frame: CGRectZero, didStartEditing: $didStartEditing) + let request = URLRequest(url: url) + webview.load(request) + return webview + } + + func updateUIView(_ webview: WKWebView, context _: Context) { + webview.endEditing(true) + + if scrollToSignup { + let point = CGPoint(x: 0, y: webview.scrollView.contentSize.height - webview.frame.size.height / 2) + + webview.scrollView.setContentOffset(point, animated: true) + DispatchQueue.main.async { + self.scrollToSignup = false + } + } + } +} + +// https://stackoverflow.com/questions/44684714/show-keyboard-on-button-click-by-calling-wkwebview-input-field +class SignupWebView: WKWebView, WKNavigationDelegate { + @Binding + var didStartEditing: Bool + + init(frame: CGRect, didStartEditing: Binding) { + _didStartEditing = didStartEditing + + let configuration = WKWebViewConfiguration() + super.init(frame: frame, configuration: configuration) + navigationDelegate = self + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + return scrollView.contentSize + } + + func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { + var scriptContent = "var meta = document.createElement('meta');" + scriptContent += "meta.name='viewport';" + scriptContent += "meta.content='width=device-width';" + scriptContent += "document.getElementsByTagName('head')[0].appendChild(meta);" + scriptContent += "document.body.scrollHeight;" + + webView.evaluateJavaScript(scriptContent, completionHandler: { height, error in + + print(height) + print(error) + }) + + webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { (value: Any!, error: Error!) in + if error != nil { + // Error logic + return + } + // webView.invalidateIntrinsicContentSize() + + // let js = "document.getElementById(\"MY_TEXTFIELD_ID\").focus();" + // webView.evaluateJavaScript(js) + + // webview.canBecomeFocused = true + + // document.getElementById('myID').focus(); + + // webview.scrollView.setZoomScale(0.3, animated: true) + + let result = value as? String + + print(value) + }) + } +} diff --git a/litewallet/Extensions/String+Additions.swift b/litewallet/Extensions/String+Additions.swift new file mode 100644 index 000000000..3808b4e38 --- /dev/null +++ b/litewallet/Extensions/String+Additions.swift @@ -0,0 +1,149 @@ +import BRCore +import FirebaseAnalytics +import Foundation +import UIKit + +extension String { + var isValidPrivateKey: Bool { + return BRPrivKeyIsValid(self) != 0 + } + + var isValidBip38Key: Bool { + return BRBIP38KeyIsValid(self) != 0 + } + + var isValidAddress: Bool { + guard lengthOfBytes(using: .utf8) > 0 else { return false } + return BRAddressIsValid(self) != 0 + } + + var sanitized: String { + return applyingTransform(.toUnicodeName, reverse: false) ?? "" + } + + func ltrim(_ chars: Set) -> String { + if let index = index(where: { !chars.contains($0) }) { + return String(self[index ..< endIndex]) + } else { + return "" + } + } + + func rtrim(_ chars: Set) -> String { + if let index = reversed().index(where: { !chars.contains($0) }) { + return String(self[startIndex ... self.index(before: index.base)]) + } else { + return "" + } + } + + func nsRange(from range: Range) -> NSRange { + let location = utf16.distance(from: utf16.startIndex, to: range.lowerBound) + let length = utf16.distance(from: range.lowerBound, to: range.upperBound) + return NSRange(location: location, length: length) + } +} + +private let startTag = "" +private let endTag = "" + +// Convert string with tags to attributed string +extension String { + var tagsRemoved: String { + return replacingOccurrences(of: startTag, with: "").replacingOccurrences(of: endTag, with: "") + } + + var attributedStringForTags: NSAttributedString { + let output = NSMutableAttributedString() + let scanner = Scanner(string: self) + let endCount = tagsRemoved.utf8.count + var i = 0 + while output.string.utf8.count < endCount || i < 50 { + var regular: NSString? + var bold: NSString? + scanner.scanUpTo(startTag, into: ®ular) + scanner.scanUpTo(endTag, into: &bold) + if let regular = regular { + output.append(NSAttributedString(string: (regular as String).tagsRemoved, attributes: UIFont.regularAttributes)) + } + if let bold = bold { + output.append(NSAttributedString(string: (bold as String).tagsRemoved, attributes: UIFont.boldAttributes)) + } + i += 1 + } + return output + } +} + +// MARK: - Hex String conversions + +extension String { + var hexToData: Data? { + let scalars = unicodeScalars + var bytes = [UInt8](repeating: 0, count: (scalars.count + 1) >> 1) + for (index, scalar) in scalars.enumerated() { + guard var nibble = scalar.nibble else { return nil } + if index & 1 == 0 { + nibble <<= 4 + } + bytes[index >> 1] |= nibble + } + return Data(bytes: bytes) + } + + static func localizedString(for key: String, + locale: Locale = .current) -> String + { + let language = locale.languageCode + let path = Bundle.main.path(forResource: language, ofType: "lproj")! + let bundle = Bundle(path: path)! + let localizedString = NSLocalizedString(key, bundle: bundle, comment: "") + + return localizedString + } +} + +extension UnicodeScalar { + var nibble: UInt8? { + if value >= 48, value <= 57 { + return UInt8(value - 48) + } else if value >= 65, value <= 70 { + return UInt8(value - 55) + } else if value >= 97, value <= 102 { + return UInt8(value - 87) + } + return nil + } +} + +extension String { + func capitalizingFirstLetter() -> String { + return prefix(1).uppercased() + lowercased().dropFirst() + } + + mutating func capitalizeFirstLetter() { + self = capitalizingFirstLetter() + } + + func replacingZeroFeeWithTenCents() -> String { + guard count > 3 + else { + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR, properties: ["ERROR": "STRING_ISSUE"]) + return self + } + + let range = index(endIndex, offsetBy: -3) ..< endIndex + return replacingOccurrences(of: ".00", with: ".10", options: .literal, range: range) + } + + func combinedFeeReplacingZeroFeeWithTenCents() -> String { + guard count > 4 + else { + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR, properties: ["ERROR": "STRING_ISSUE"]) + return self + } + + let range = index(endIndex, offsetBy: -4) ..< endIndex + return replacingOccurrences(of: ".00)", with: ".10)", options: .literal, range: range) + } +} diff --git a/litewallet/Extensions/UIBarButtonItem+Additions.swift b/litewallet/Extensions/UIBarButtonItem+Additions.swift new file mode 100644 index 000000000..84cb94262 --- /dev/null +++ b/litewallet/Extensions/UIBarButtonItem+Additions.swift @@ -0,0 +1,9 @@ +import UIKit + +extension UIBarButtonItem { + static var negativePadding: UIBarButtonItem { + let padding = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) + padding.width = -16.0 + return padding + } +} diff --git a/litewallet/Extensions/UIButton+BRWAdditions.swift b/litewallet/Extensions/UIButton+BRWAdditions.swift new file mode 100644 index 000000000..ff67559f0 --- /dev/null +++ b/litewallet/Extensions/UIButton+BRWAdditions.swift @@ -0,0 +1,68 @@ +import UIKit + +extension UIButton { + static func vertical(title: String, image: UIImage) -> UIButton { + let button = UIButton(type: .system) + button.setTitle(title, for: .normal) + button.setImage(image, for: .normal) + button.titleLabel?.font = UIFont.customMedium(size: 11.0) + if let imageSize = button.imageView?.image?.size, + let font = button.titleLabel?.font + { + let spacing: CGFloat = C.padding[1] / 2.0 + let titleSize = NSString(string: title).size(withAttributes: [NSAttributedString.Key.font: font]) + + // These edge insets place the image vertically above the title label + button.titleEdgeInsets = UIEdgeInsets(top: 0.0, left: -imageSize.width, bottom: -(imageSize.height + spacing), right: 0.0) + button.imageEdgeInsets = UIEdgeInsets(top: -(titleSize.height + spacing), left: 0.0, bottom: 0.0, right: -titleSize.width) + } + return button + } + + static var close: UIButton { + let accessibilityLabel = E.isScreenshots ? "Close" : S.AccessibilityLabels.close.localize() + return UIButton.icon(image: #imageLiteral(resourceName: "Close"), accessibilityLabel: accessibilityLabel) + } + + static func buildFaqButton(store: Store, articleId: String) -> UIButton { + let button = UIButton.icon(image: #imageLiteral(resourceName: "Faq"), accessibilityLabel: S.AccessibilityLabels.faq.localize()) + button.tap = { + store.trigger(name: .presentFaq(articleId)) + } + return button + } + + static func icon(image: UIImage, accessibilityLabel: String) -> UIButton { + let button = UIButton(type: .system) + button.frame = CGRect(x: 0, y: 0, width: 44, height: 44) + button.setImage(image, for: .normal) + + if image == #imageLiteral(resourceName: "Close") { + button.imageEdgeInsets = UIEdgeInsets(top: 14.0, left: 14.0, bottom: 14.0, right: 14.0) + } else { + button.imageEdgeInsets = UIEdgeInsets(top: 12.0, left: 12.0, bottom: 12.0, right: 12.0) + } + + button.tintColor = .darkText + button.accessibilityLabel = accessibilityLabel + return button + } + + func tempDisable() { + isEnabled = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in + self?.isEnabled = true + } + } + + static func stylizeLitewalletBlueButton(title: String, frame: CGRect) -> UIButton { + let button = UIButton() + button.frame = frame + button.setTitle(title, for: .normal) + button.layer.cornerRadius = 4.0 + button.titleLabel?.textColor = .white + button.titleLabel?.font = UIFont.barlowMedium(size: 18) + button.clipsToBounds = true + return button + } +} diff --git a/litewallet/Extensions/UIColor+Extension.swift b/litewallet/Extensions/UIColor+Extension.swift new file mode 100644 index 000000000..0668402cd --- /dev/null +++ b/litewallet/Extensions/UIColor+Extension.swift @@ -0,0 +1,176 @@ +import SwiftUI +import UIKit + +extension UIColor { + // TODO: New Color Scheme + // #A6A9AA, Silver C, UIColor(red: 166.0/255.0, green: 169.0/255.0, blue: 170.0/255.0, alpha: 1.0) + // #4D4D4E, Cool Gray 11 C + // #FFFFFF, White + // #345D9D, Blue, 7684 C, UIColor(red: 52.0/255.0, green: 52.0/255.0, blue: 157.0/255.0, alpha: 1.0) + + static var litecoinWhite: UIColor { + return .white + } + + static var litecoinGray: UIColor { // F1F1F1 + return #colorLiteral(red: 0.9450980392, green: 0.9450980392, blue: 0.9450980392, alpha: 1) + } + + static var litewalletLightGray: UIColor { // F8F8F8 + return #colorLiteral(red: 0.9725490196, green: 0.9725490196, blue: 0.9725490196, alpha: 1) + } + + static var litecoinSilver: UIColor { // A6A9AA + return #colorLiteral(red: 0.6509803922, green: 0.662745098, blue: 0.6666666667, alpha: 1) + } + + static var litecoinDarkSilver: UIColor { // 4D4D4E + return #colorLiteral(red: 0.3019607843, green: 0.3019607843, blue: 0.3058823529, alpha: 1) + } + + static var liteWalletBlue: UIColor { // 345D9D + return #colorLiteral(red: 0.2039215686, green: 0.3647058824, blue: 0.6156862745, alpha: 1) + } + + static var liteWalletDarkBlue: UIColor { // 0C3475 + return #colorLiteral(red: 0.04705882353, green: 0.2039215686, blue: 0.4588235294, alpha: 1) + } + + static var litewalletOrange: UIColor { // FE5F55 + return #colorLiteral(red: 0.9960784314, green: 0.3725490196, blue: 0.3333333333, alpha: 1) + } + + static var litecoinGreen: UIColor { // 179E27 + return #colorLiteral(red: 0.09019607843, green: 0.6196078431, blue: 0.1529411765, alpha: 1) + } + + static var litewalletLime: UIColor { // D9E76C + return #colorLiteral(red: 0.8509803922, green: 0.9058823529, blue: 0.4235294118, alpha: 1) + } + + // MARK: Buttons + + static var primaryButton: UIColor { + return #colorLiteral(red: 0.2980392157, green: 0.5960784314, blue: 0.9882352941, alpha: 1) // UIColor(red: 76.0/255.0, green: 152.0/255.0, blue: 252.0/255.0, alpha: 1.0) + } + + static var primaryText: UIColor { + return .white + } + + static var secondaryButton: UIColor { + return #colorLiteral(red: 0.9607843137, green: 0.968627451, blue: 0.9803921569, alpha: 1) // UIColor(red: 245.0/255.0, green: 247.0/255.0, blue: 250.0/255.0, alpha: 1.0) + } + + static var secondaryBorder: UIColor { + return #colorLiteral(red: 0.8352941176, green: 0.8549019608, blue: 0.8784313725, alpha: 1) // UIColor(red: 213.0/255.0, green: 218.0/255.0, blue: 224.0/255.0, alpha: 1.0) + } + + static var darkText: UIColor { + return #colorLiteral(red: 0.137254902, green: 0.1450980392, blue: 0.1490196078, alpha: 1) // UIColor(red: 35.0/255.0, green: 37.0/255.0, blue: 38.0/255.0, alpha: 1.0) + } + + static var darkLine: UIColor { + return #colorLiteral(red: 0.1411764706, green: 0.137254902, blue: 0.1490196078, alpha: 1) // UIColor(red: 36.0/255.0, green: 35.0/255.0, blue: 38.0/255.0, alpha: 1.0) + } + + static var secondaryShadow: UIColor { + return UIColor(red: 213.0 / 255.0, green: 218.0 / 255.0, blue: 224.0 / 255.0, alpha: 1.0) + } + + static var offWhite: UIColor { + return UIColor(white: 247.0 / 255.0, alpha: 1.0) + } + + static var borderGray: UIColor { + return UIColor(white: 221.0 / 255.0, alpha: 1.0) + } + + static var separatorGray: UIColor { + return UIColor(white: 221.0 / 255.0, alpha: 1.0) + } + + static var grayText: UIColor { + return #colorLiteral(red: 0.5333333333, green: 0.5333333333, blue: 0.5333333333, alpha: 1) // UIColor(white: 136.0/255.0, alpha: 1.0) + } + + static var grayTextTint: UIColor { + return UIColor(red: 163.0 / 255.0, green: 168.0 / 255.0, blue: 173.0 / 255.0, alpha: 1.0) + } + + static var secondaryGrayText: UIColor { + return UIColor(red: 101.0 / 255.0, green: 105.0 / 255.0, blue: 110.0 / 255.0, alpha: 1.0) + } + + static var grayBackgroundTint: UIColor { + return UIColor(red: 250.0 / 255.0, green: 251.0 / 255.0, blue: 252.0 / 255.0, alpha: 1.0) + } + + static var cameraGuidePositive: UIColor { + return UIColor(red: 72.0 / 255.0, green: 240.0 / 255.0, blue: 184.0 / 255.0, alpha: 1.0) + } + + static var purple: UIColor { + return UIColor(red: 209.0 / 255.0, green: 125.0 / 255.0, blue: 245.0 / 255.0, alpha: 1.0) + } + + static var darkPurple: UIColor { + return UIColor(red: 127.0 / 255.0, green: 83.0 / 255.0, blue: 230.0 / 255.0, alpha: 1.0) + } + + static var pink: UIColor { + return UIColor(red: 252.0 / 255.0, green: 83.0 / 255.0, blue: 148.0 / 255.0, alpha: 1.0) + } + + static var blue: UIColor { + return UIColor(red: 76.0 / 255.0, green: 152.0 / 255.0, blue: 252.0 / 255.0, alpha: 1.0) + } + + // MARK: Gradient + + static var gradientStart: UIColor { + return UIColor(red: 131.0 / 255.0, green: 175.0 / 255.0, blue: 224.0 / 255.0, alpha: 1.0) + } + + static var gradientEnd: UIColor { + return UIColor(red: 118.0 / 255.0, green: 126.0 / 255.0, blue: 227.0 / 255.0, alpha: 1.0) + } + + static var whiteTint: UIColor { + return UIColor(red: 245.0 / 255.0, green: 247.0 / 255.0, blue: 250.0 / 255.0, alpha: 1.0) + } + + static var transparentWhite: UIColor { + return UIColor(white: 1.0, alpha: 0.3) + } + + static var transparentBlack: UIColor { + return UIColor(white: 0.0, alpha: 0.3) + } + + static var blueGradientStart: UIColor { + return UIColor(red: 99.0 / 255.0, green: 188.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0) + } + + static var blueGradientEnd: UIColor { + return UIColor(red: 56.0 / 255.0, green: 141.0 / 255.0, blue: 252.0 / 255.0, alpha: 1.0) + } +} + +extension Color { + static var litewalletBlue: Color { // 345D9D + return Color(#colorLiteral(red: 0.2039215686, green: 0.3647058824, blue: 0.6156862745, alpha: 1)) + } + + static var litewalletDarkBlue: Color { // 0C3475 + return Color(#colorLiteral(red: 0.04705882353, green: 0.2039215686, blue: 0.4588235294, alpha: 1)) + } + + static var litewalletOrange: Color { // FE5F55 + return Color(#colorLiteral(red: 0.9960784314, green: 0.3725490196, blue: 0.3333333333, alpha: 1)) + } + + static var litewalletGreen: Color { // 179E27 + return Color(#colorLiteral(red: 0.09019607843, green: 0.6196078431, blue: 0.1529411765, alpha: 1)) + } +} diff --git a/litewallet/Extensions/UIControl+Callback.swift b/litewallet/Extensions/UIControl+Callback.swift new file mode 100644 index 000000000..a20960367 --- /dev/null +++ b/litewallet/Extensions/UIControl+Callback.swift @@ -0,0 +1,69 @@ +import UIKit + +private class CallbackWrapper: NSObject, NSCopying { + init(_ callback: @escaping () -> Void) { + self.callback = callback + } + + let callback: () -> Void + + func copy(with _: NSZone? = nil) -> Any { + return CallbackWrapper(callback) + } +} + +private struct AssociatedKeys { + static var didTapCallback = "didTapCallback" + static var valueChangedCallback = "valueChangedCallback" + static var valueEditingChangedCallback = "valueEditingChangedCallback" +} + +extension UIControl { + var tap: (() -> Void)? { + get { + guard let callbackWrapper = objc_getAssociatedObject(self, &AssociatedKeys.didTapCallback) as? CallbackWrapper else { return nil } + return callbackWrapper.callback + } + set { + guard let newValue = newValue else { return } + addTarget(self, action: #selector(didTap), for: .touchUpInside) + objc_setAssociatedObject(self, &AssociatedKeys.didTapCallback, CallbackWrapper(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + @objc private func didTap() { + tap?() + } + + var valueChanged: (() -> Void)? { + get { + guard let callbackWrapper = objc_getAssociatedObject(self, &AssociatedKeys.valueChangedCallback) as? CallbackWrapper else { return nil } + return callbackWrapper.callback + } + set { + guard let newValue = newValue else { return } + addTarget(self, action: #selector(valueDidChange), for: .valueChanged) + objc_setAssociatedObject(self, &AssociatedKeys.valueChangedCallback, CallbackWrapper(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + var editingChanged: (() -> Void)? { + get { + guard let callbackWrapper = objc_getAssociatedObject(self, &AssociatedKeys.valueEditingChangedCallback) as? CallbackWrapper else { return nil } + return callbackWrapper.callback + } + set { + guard let newValue = newValue else { return } + addTarget(self, action: #selector(editingChange), for: .editingChanged) + objc_setAssociatedObject(self, &AssociatedKeys.valueEditingChangedCallback, CallbackWrapper(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + @objc private func valueDidChange() { + valueChanged?() + } + + @objc private func editingChange() { + editingChanged?() + } +} diff --git a/litewallet/Extensions/UIFont+BRWAdditions.swift b/litewallet/Extensions/UIFont+BRWAdditions.swift new file mode 100644 index 000000000..255c16e55 --- /dev/null +++ b/litewallet/Extensions/UIFont+BRWAdditions.swift @@ -0,0 +1,84 @@ +import SwiftUI +import UIKit + +extension UIFont { + static var header: UIFont { + return UIFont(name: "BarlowSemiCondensed-Bold", size: 17.0) ?? UIFont.preferredFont(forTextStyle: .headline) + } + + static func customBold(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Bold", size: size) ?? UIFont.preferredFont(forTextStyle: .headline) + } + + static func customBody(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Regular", size: size) ?? UIFont.preferredFont(forTextStyle: .subheadline) + } + + static func customMedium(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Medium", size: size) ?? UIFont.preferredFont(forTextStyle: .body) + } + + static func barlowBold(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Bold", size: size) ?? UIFont.preferredFont(forTextStyle: .body) + } + + static func barlowSemiBold(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-SemiBold", size: size) ?? UIFont.preferredFont(forTextStyle: .body) + } + + static func barlowItalic(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Italic", size: size) ?? UIFont.preferredFont(forTextStyle: .body) + } + + static func barlowMedium(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Medium", size: size) ?? UIFont.preferredFont(forTextStyle: .body) + } + + static func barlowRegular(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Regular", size: size) ?? UIFont.preferredFont(forTextStyle: .body) + } + + static func barlowLight(size: CGFloat) -> UIFont { + return UIFont(name: "BarlowSemiCondensed-Light", size: size) ?? UIFont.preferredFont(forTextStyle: .body) + } + + static var regularAttributes: [NSAttributedString.Key: Any] { + return [ + .font: UIFont.customBody(size: 14.0), + .foregroundColor: UIColor.darkText, + ] + } + + static var boldAttributes: [NSAttributedString.Key: Any] { + return [ + .font: UIFont.customBold(size: 14.0), + .foregroundColor: UIColor.darkText, + ] + } +} + +extension Font { + static func barlowSemiBold(size: CGFloat) -> Font { + return Font.custom("BarlowSemiCondensed-SemiBold", size: size) + } + + static func barlowBold(size: CGFloat) -> Font { + return Font.custom("BarlowSemiCondensed-Bold", size: size) + } + + static func barlowItalic(size: CGFloat) -> Font { + return Font.custom("BarlowSemiCondensed-Italic", size: size) + } + + static func barlowMedium(size: CGFloat) -> Font { + return Font.custom("BarlowSemiCondensed-Medium", size: size) + } + + static func barlowRegular(size: CGFloat) -> Font { + return Font.custom("BarlowSemiCondensed-Regular", size: size) + } + + static func barlowLight(size: CGFloat) -> Font { + return Font.custom("BarlowSemiCondensed-Light", size: size) + } +} diff --git a/litewallet/Extensions/UIImage+Utils.swift b/litewallet/Extensions/UIImage+Utils.swift new file mode 100644 index 000000000..2474a7550 --- /dev/null +++ b/litewallet/Extensions/UIImage+Utils.swift @@ -0,0 +1,60 @@ +import CoreGraphics +import UIKit + +private let inputImageKey = "inputImage" + +extension UIImage { + static func qrCode(data: Data, color: CIColor) -> UIImage? { + let qrFilter = CIFilter(name: "CIQRCodeGenerator") + let maskFilter = CIFilter(name: "CIMaskToAlpha") + let invertFilter = CIFilter(name: "CIColorInvert") + let colorFilter = CIFilter(name: "CIFalseColor") + var filter = colorFilter + + qrFilter?.setValue(data, forKey: "inputMessage") + qrFilter?.setValue("L", forKey: "inputCorrectionLevel") + + if Double(color.alpha) > .ulpOfOne { + invertFilter?.setValue(qrFilter?.outputImage, forKey: inputImageKey) + maskFilter?.setValue(invertFilter?.outputImage, forKey: inputImageKey) + invertFilter?.setValue(maskFilter?.outputImage, forKey: inputImageKey) + colorFilter?.setValue(invertFilter?.outputImage, forKey: inputImageKey) + colorFilter?.setValue(color, forKey: "inputColor0") + } else { + maskFilter?.setValue(qrFilter?.outputImage, forKey: inputImageKey) + filter = maskFilter + } + + // force software rendering for security (GPU rendering causes image artifacts on iOS 7 and is generally crashy) + let context = CIContext(options: [.useSoftwareRenderer: true]) + objc_sync_enter(context) + defer { objc_sync_exit(context) } + guard let outputImage = filter?.outputImage else { assertionFailure("No qr output image"); return nil } + guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { assertionFailure("Could not create image."); return nil } + return UIImage(cgImage: cgImage) + } + + func resize(_ size: CGSize) -> UIImage? { + UIGraphicsBeginImageContext(size) + defer { UIGraphicsEndImageContext() } + guard let context = UIGraphicsGetCurrentContext() else { assertionFailure("Could not create image context"); return nil } + guard let cgImage = cgImage else { assertionFailure("No cgImage property"); return nil } + + context.interpolationQuality = .none + context.rotate(by: π) // flip + context.scaleBy(x: -1.0, y: 1.0) // mirror + context.draw(cgImage, in: context.boundingBoxOfClipPath) + return UIGraphicsGetImageFromCurrentImageContext() + } + + static func imageForColor(_ color: UIColor) -> UIImage { + let rect = CGRect(x: 0, y: 0, width: 1.0, height: 1.0) + UIGraphicsBeginImageContext(rect.size) + let context = UIGraphicsGetCurrentContext() + context?.setFillColor(color.cgColor) + context?.fill(rect) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image ?? UIImage() + } +} diff --git a/litewallet/Extensions/UILabel+BRWAdditions.swift b/litewallet/Extensions/UILabel+BRWAdditions.swift new file mode 100644 index 000000000..1dfd36f18 --- /dev/null +++ b/litewallet/Extensions/UILabel+BRWAdditions.swift @@ -0,0 +1,42 @@ +import UIKit + +extension UILabel { + static func wrapping(font: UIFont, color: UIColor) -> UILabel { + let label = UILabel() + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.font = font + label.textColor = color + return label + } + + static func wrapping(font: UIFont) -> UILabel { + let label = UILabel() + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.font = font + return label + } + + convenience init(font: UIFont) { + self.init() + self.font = font + } + + convenience init(font: UIFont, color: UIColor) { + self.init() + self.font = font + textColor = color + } + + func pushNewText(_ newText: String) { + let animation = CATransition() + animation.timingFunction = CAMediaTimingFunction(name: + CAMediaTimingFunctionName.easeInEaseOut) + animation.type = CATransitionType.push + animation.subtype = CATransitionSubtype.fromTop + animation.duration = C.animationDuration + layer.add(animation, forKey: "kCATransitionPush") + text = newText + } +} diff --git a/litewallet/Extensions/UINavigationController+Extension.swift b/litewallet/Extensions/UINavigationController+Extension.swift new file mode 100644 index 000000000..1c9c05d6d --- /dev/null +++ b/litewallet/Extensions/UINavigationController+Extension.swift @@ -0,0 +1,57 @@ +import SwiftUI +import UIKit + +extension UINavigationController { + override open func viewDidLoad() { + super.viewDidLoad() + } + + func setDefaultStyle() { + setClearNavbar() + setBlackBackArrow() + } + + func setWhiteStyle() { + navigationBar.tintColor = .white + navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.white, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + setTintableBackArrow() + } + + func setClearNavbar() { + navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationBar.shadowImage = UIImage() + navigationBar.isTranslucent = true + } + + func setNormalNavbar() { + navigationBar.setBackgroundImage(nil, for: .default) + navigationBar.shadowImage = nil + } + + func setBlackBackArrow() { + let image = #imageLiteral(resourceName: "Back") + let renderedImage = image.withRenderingMode(.alwaysOriginal) + navigationBar.backIndicatorImage = renderedImage + navigationBar.backIndicatorTransitionMaskImage = renderedImage + } + + func setTintableBackArrow() { + navigationBar.backIndicatorImage = #imageLiteral(resourceName: "Back") + navigationBar.backIndicatorTransitionMaskImage = #imageLiteral(resourceName: "Back") + } +} + +extension UINavigationBarAppearance { + func setColor(title: UIColor? = nil, background: UIColor? = nil) { + configureWithTransparentBackground() + if let titleColor = title { + titleTextAttributes = [.foregroundColor: titleColor] + } + backgroundColor = background + UINavigationBar.appearance().scrollEdgeAppearance = self + UINavigationBar.appearance().standardAppearance = self + } +} diff --git a/litewallet/Extensions/UIScreen+Additions.swift b/litewallet/Extensions/UIScreen+Additions.swift new file mode 100644 index 000000000..f47428c7e --- /dev/null +++ b/litewallet/Extensions/UIScreen+Additions.swift @@ -0,0 +1,7 @@ +import UIKit + +extension UIScreen { + var safeWidth: CGFloat { + return min(bounds.width, bounds.height) + } +} diff --git a/litewallet/Extensions/UISlider+Gradient.swift b/litewallet/Extensions/UISlider+Gradient.swift new file mode 100644 index 000000000..b60980b89 --- /dev/null +++ b/litewallet/Extensions/UISlider+Gradient.swift @@ -0,0 +1,24 @@ +import UIKit + +extension UISlider { + func addGradientTrack() { + superview?.layoutIfNeeded() + setMaximumTrackImage(imageForColors(colors: [UIColor.grayTextTint.cgColor, UIColor.grayTextTint.cgColor], offset: 4.0), for: .normal) + setMinimumTrackImage(imageForColors(colors: [UIColor.gradientStart.cgColor, UIColor.gradientEnd.cgColor]), for: .normal) + } + + private func imageForColors(colors: [CGColor], offset: CGFloat = 0.0) -> UIImage? { + let layer = CAGradientLayer() + layer.cornerRadius = bounds.height / 2.0 + layer.frame = CGRect(x: bounds.minX, y: bounds.minY, width: bounds.width - offset, height: bounds.height) + layer.colors = colors + layer.endPoint = CGPoint(x: 1.0, y: 1.0) + layer.startPoint = CGPoint(x: 0.0, y: 1.0) + + UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0.0) + layer.render(in: UIGraphicsGetCurrentContext()!) + let layerImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return layerImage?.resizableImage(withCapInsets: .zero) + } +} diff --git a/litewallet/Extensions/UITableView+Additions.swift b/litewallet/Extensions/UITableView+Additions.swift new file mode 100644 index 000000000..9567cba4f --- /dev/null +++ b/litewallet/Extensions/UITableView+Additions.swift @@ -0,0 +1,9 @@ +import UIKit + +extension UIScrollView { + func verticallyOffsetContent(_ deltaY: CGFloat) { + contentOffset = CGPoint(x: contentOffset.x, y: contentOffset.y - deltaY) + contentInset = UIEdgeInsets(top: contentInset.top + deltaY, left: contentInset.left, bottom: contentInset.bottom, right: contentInset.right) + scrollIndicatorInsets = contentInset + } +} diff --git a/litewallet/Extensions/UIView+AnimationAdditions.swift b/litewallet/Extensions/UIView+AnimationAdditions.swift new file mode 100644 index 000000000..f716102c7 --- /dev/null +++ b/litewallet/Extensions/UIView+AnimationAdditions.swift @@ -0,0 +1,21 @@ +import UIKit + +extension UIView { + static func spring(_ duration: TimeInterval, delay: TimeInterval, animations: @escaping () -> Void, completion: @escaping (Bool) -> Void) + { + if #available(iOS 10.0, *) { + UIViewPropertyAnimator.springAnimation(duration, delay: delay, animations: animations, completion: { _ in completion(true) }) + } else { + UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: animations, completion: completion) + } + } + + static func spring(_ duration: TimeInterval, animations: @escaping () -> Void, completion: @escaping (Bool) -> Void) + { + if #available(iOS 10.0, *) { + UIViewPropertyAnimator.springAnimation(duration, animations: animations, completion: { _ in completion(true) }) + } else { + UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: animations, completion: completion) + } + } +} diff --git a/litewallet/Extensions/UIView+BRWAdditions.swift b/litewallet/Extensions/UIView+BRWAdditions.swift new file mode 100644 index 000000000..d67869d60 --- /dev/null +++ b/litewallet/Extensions/UIView+BRWAdditions.swift @@ -0,0 +1,205 @@ +import UIKit + +enum Dimension { + case width + case height + + var layoutAttribute: NSLayoutConstraint.Attribute { + switch self { + case .width: + return .width + case .height: + return .height + } + } +} + +extension UIView { + var firstResponder: UIView? { + guard !isFirstResponder else { return self } + for subview in subviews { + if subview.isFirstResponder { + return subview + } + } + return nil + } + + func constrain(toSuperviewEdges: UIEdgeInsets?) { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1.0, constant: toSuperviewEdges?.left ?? 0.0), + NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: toSuperviewEdges?.top ?? 0.0), + NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1.0, constant: toSuperviewEdges?.right ?? 0.0), + NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: toSuperviewEdges?.bottom ?? 0.0), + ]) + } + + func constrain(_ constraints: [NSLayoutConstraint?]) { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(constraints.compactMap { $0 }) + } + + func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView, constant: CGFloat?) -> NSLayoutConstraint? + { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant ?? 0.0) + } + + func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView) -> NSLayoutConstraint? { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: 0.0) + } + + func constraint(_ dimension: Dimension, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: dimension.layoutAttribute, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: constant) + } + + func constraint(toBottom: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: toBottom, attribute: .bottom, multiplier: 1.0, constant: constant) + } + + func pinToBottom(to: UIView, height: CGFloat) { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.width, toView: to), + constraint(toBottom: to, constant: 0.0), + constraint(.height, constant: height), + ]) + } + + func constraint(toTop: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: toTop, attribute: .top, multiplier: 1.0, constant: constant) + } + + func constraint(toTrailing: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: toTrailing, attribute: .trailing, multiplier: 1.0, constant: constant) + } + + func constraint(toLeading: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: toLeading, attribute: .leading, multiplier: 1.0, constant: constant) + } + + func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat, topLayoutGuide: UILayoutSupport) + { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: topPadding), + constraint(.trailing, toView: view, constant: -sidePadding), + ]) + } + + func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat) { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + constraint(.top, toView: view, constant: topPadding), + constraint(.trailing, toView: view, constant: -sidePadding), + ]) + } + + func constrainTopCorners(height: CGFloat) { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view), + constraint(.top, toView: view), + constraint(.trailing, toView: view), + constraint(.height, constant: height), + ]) + } + + func constrainBottomCorners(sidePadding: CGFloat, bottomPadding: CGFloat) { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + constraint(.bottom, toView: view, constant: -bottomPadding), + constraint(.trailing, toView: view, constant: -sidePadding), + ]) + } + + func constrainBottomCorners(height: CGFloat) { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.leading, toView: view), + constraint(.bottom, toView: view), + constraint(.trailing, toView: view), + constraint(.height, constant: height), + ]) + } + + func constrainLeadingCorners() { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.top, toView: view), + constraint(.leading, toView: view), + constraint(.bottom, toView: view), + ]) + } + + func constrainTrailingCorners() { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.top, toView: view), + constraint(.trailing, toView: view), + constraint(.bottom, toView: view), + ]) + } + + func constrainToCenter() { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.centerX, toView: view), + constraint(.centerY, toView: view), + ]) + } + + func pinTo(viewAbove: UIView, padding: CGFloat = 0.0, height: CGFloat? = nil) { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + constraint(.width, toView: viewAbove), + constraint(toBottom: viewAbove, constant: padding), + centerXAnchor.constraint(equalTo: viewAbove.centerXAnchor), + height != nil ? constraint(.height, constant: height!) : nil, + ]) + } + + func pin(toSize: CGSize) { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + widthAnchor.constraint(equalToConstant: toSize.width), + heightAnchor.constraint(equalToConstant: toSize.height), + ]) + } + + // Post iOS 8 + func pinTopLeft(padding: CGFloat) { + guard let view = superview else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding), + topAnchor.constraint(equalTo: view.topAnchor, constant: padding), + ]) + } + + func pinTopLeft(toView: UIView, topPadding: CGFloat) { + guard superview != nil else { assertionFailure("Superview cannot be nil when adding contraints"); return } + constrain([ + leadingAnchor.constraint(equalTo: toView.leadingAnchor), + topAnchor.constraint(equalTo: toView.bottomAnchor, constant: topPadding), + ]) + } +} diff --git a/litewallet/Extensions/UIView+FrameChangeBlocking.swift b/litewallet/Extensions/UIView+FrameChangeBlocking.swift new file mode 100644 index 000000000..9d753dadc --- /dev/null +++ b/litewallet/Extensions/UIView+FrameChangeBlocking.swift @@ -0,0 +1,44 @@ +import UIKit + +extension UIView { + private struct AssociatedKeys { + static var frameBlockedKey = "FrameBlockedKey" + } + + var isFrameChangeBlocked: Bool { + get { + guard let object = objc_getAssociatedObject(self, &AssociatedKeys.frameBlockedKey) as? Bool else { return false } + return object + } + + set { + objc_setAssociatedObject(self, &AssociatedKeys.frameBlockedKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + static func swizzleSetFrame() { + guard self == UIView.self else { return } + + // This is now a way to do the equivalent of dispatch_once in swift 3 + let _: () = { + let originalSelector = #selector(setter: UIView.frame) + let swizzledSelector = #selector(UIView.requestSetFrame(_:)) + + let originalMethod = class_getInstanceMethod(self, originalSelector) + let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) + + let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!)) + if didAddMethod { + class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!)) + } else { + method_exchangeImplementations(originalMethod!, swizzledMethod!) + } + + }() + } + + @objc func requestSetFrame(_ frame: CGRect) { + guard !isFrameChangeBlocked else { return } + requestSetFrame(frame) + } +} diff --git a/litewallet/Extensions/UIView+InitAdditions.swift b/litewallet/Extensions/UIView+InitAdditions.swift new file mode 100644 index 000000000..9136065f0 --- /dev/null +++ b/litewallet/Extensions/UIView+InitAdditions.swift @@ -0,0 +1,17 @@ +import QuartzCore +import UIKit + +extension UIView { + @objc convenience init(color: UIColor) { + self.init(frame: .zero) + backgroundColor = color + } + + var imageRepresentation: UIImage { + UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0.0) + layer.render(in: UIGraphicsGetCurrentContext()!) + let tempImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return tempImage! + } +} diff --git a/litewallet/Extensions/UIViewController+Alerts.swift b/litewallet/Extensions/UIViewController+Alerts.swift new file mode 100644 index 000000000..03eaaad36 --- /dev/null +++ b/litewallet/Extensions/UIViewController+Alerts.swift @@ -0,0 +1,15 @@ +import UIKit + +extension UIViewController { + func showErrorMessage(_ message: String) { + let alert = UIAlertController(title: S.LitewalletAlert.error.localize(), message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + present(alert, animated: true, completion: nil) + } + + func showAlert(title: String, message: String, buttonLabel _: String) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + present(alertController, animated: true, completion: nil) + } +} diff --git a/litewallet/Extensions/UIViewController+BRWAdditions.swift b/litewallet/Extensions/UIViewController+BRWAdditions.swift new file mode 100644 index 000000000..75b913ef0 --- /dev/null +++ b/litewallet/Extensions/UIViewController+BRWAdditions.swift @@ -0,0 +1,31 @@ +import UIKit + +extension UIViewController { + func addChildViewController(_ viewController: UIViewController, layout: () -> Void) { + addChild(viewController) + view.addSubview(viewController.view) + layout() + viewController.didMove(toParent: self) + } + + func remove() { + willMove(toParent: nil) + view.removeFromSuperview() + removeFromParent() + } + + func addCloseNavigationItem(tintColor: UIColor? = nil) { + let close = UIButton.close + close.tap = { [weak self] in + self?.dismiss(animated: true, completion: nil) + } + if let color = tintColor { + close.tintColor = UIColor.black + } + navigationItem.leftBarButtonItems = [UIBarButtonItem.negativePadding, UIBarButtonItem(customView: close)] + } + + func hideCloseNavigationItem() { + navigationItem.leftBarButtonItems = [UIBarButtonItem.negativePadding, UIBarButtonItem(customView: UIView())] + } +} diff --git a/litewallet/Extensions/UIViewControllerContextTransitioning+BRAdditions.swift b/litewallet/Extensions/UIViewControllerContextTransitioning+BRAdditions.swift new file mode 100644 index 000000000..adde78ddd --- /dev/null +++ b/litewallet/Extensions/UIViewControllerContextTransitioning+BRAdditions.swift @@ -0,0 +1,9 @@ +import UIKit + +extension UIViewControllerContextTransitioning { + var views: (UIView, UIView)? { + guard let fromView = view(forKey: .from) else { assertionFailure("Empty from view"); return nil } + guard let toView = view(forKey: .to) else { assertionFailure("Empty to view"); return nil } + return (fromView, toView) + } +} diff --git a/litewallet/Extensions/UIViewPropertyAnimator+BRWAdditions.swift b/litewallet/Extensions/UIViewPropertyAnimator+BRWAdditions.swift new file mode 100644 index 000000000..320650b69 --- /dev/null +++ b/litewallet/Extensions/UIViewPropertyAnimator+BRWAdditions.swift @@ -0,0 +1,18 @@ +import UIKit + +@available(iOS 10.0, *) +extension UIViewPropertyAnimator { + static func springAnimation(_ duration: TimeInterval, delay: TimeInterval, animations: @escaping () -> Void, completion: @escaping (UIViewAnimatingPosition) -> Void) + { + let springParameters = UISpringTimingParameters(dampingRatio: 0.7) + let animator = UIViewPropertyAnimator(duration: duration, timingParameters: springParameters) + animator.addAnimations(animations) + animator.addCompletion(completion) + animator.startAnimation(afterDelay: delay) + } + + static func springAnimation(_ duration: TimeInterval, animations: @escaping () -> Void, completion: @escaping (UIViewAnimatingPosition) -> Void) + { + springAnimation(duration, delay: 0.0, animations: animations, completion: completion) + } +} diff --git a/litewallet/Extensions/UserDefaults+Additions.swift b/litewallet/Extensions/UserDefaults+Additions.swift new file mode 100644 index 000000000..bb595aa6e --- /dev/null +++ b/litewallet/Extensions/UserDefaults+Additions.swift @@ -0,0 +1,188 @@ +import Foundation + +private let defaults = UserDefaults.standard +private let isBiometricsEnabledKey = "isbiometricsenabled" +private let defaultCurrencyCodeKey = "defaultcurrency" +private let hasAquiredShareDataPermissionKey = "has_acquired_permission" +private let legacyWalletNeedsBackupKey = "WALLET_NEEDS_BACKUP" +private let writePaperPhraseDateKey = "writepaperphrasedatekey" +private let hasPromptedBiometricsKey = "haspromptedtouched" +private let isLtcSwappedKey = "isLtcSwappedKey" +private let maxDigitsKey = "SETTINGS_MAX_DIGITS" +private let pushTokenKey = "pushTokenKey" +private let currentRateKey = "currentRateKey" +private let customNodeIPKey = "customNodeIPKey" +private let customNodePortKey = "customNodePortKey" +private let hasPromptedShareDataKey = "hasPromptedShareDataKey" +private let didSeeTransactionCorruption = "DidSeeTransactionCorruption" +private let userIsInUSAKey = "userIsInUSAKey" +private let selectedLanguageKey = "selectedLanguage" + +extension UserDefaults { + static var selectedLanguage: String { + get { + guard defaults.object(forKey: selectedLanguageKey) != nil else { + return "en" + } + return defaults.string(forKey: selectedLanguageKey) ?? "en" + } + set { defaults.set(newValue, forKey: selectedLanguageKey) } + } + + static var isBiometricsEnabled: Bool { + get { + guard defaults.object(forKey: isBiometricsEnabledKey) != nil + else { + return false + } + return defaults.bool(forKey: isBiometricsEnabledKey) + } + set { defaults.set(newValue, forKey: isBiometricsEnabledKey) } + } + + static var didSeeCorruption: Bool { + get { return defaults.bool(forKey: didSeeTransactionCorruption) } + set { defaults.set(newValue, forKey: didSeeTransactionCorruption) } + } + + static var defaultCurrencyCode: String { + get { + var currencyCode = "USD" + if defaults.object(forKey: defaultCurrencyCodeKey) == nil { + currencyCode = Locale.current.currencyCode ?? "USD" + } else { + currencyCode = defaults.string(forKey: defaultCurrencyCodeKey)! + } + let acceptedCurrencyCodes = ["USD", "EUR", "JPY", "BGN", "CZK", "DKK", "GBP", "HUF", "PLN", "RON", "SEK", "CHF", "NOK", "HRK", "RUB", "TRY", "AUD", "BRL", "CAD", "CNY", "HKD", "IDR", "ILS", "INR", "KRW", "MXN", "MYR", "NZD", "PHP", "SDG", "THB", "ZAR"] + + if !(acceptedCurrencyCodes.contains(currencyCode)) { + return "USD" + } + + return currencyCode + } + set { defaults.set(newValue, forKey: defaultCurrencyCodeKey) } + } + + static var hasAquiredShareDataPermission: Bool { + get { return defaults.bool(forKey: hasAquiredShareDataPermissionKey) } + set { defaults.set(newValue, forKey: hasAquiredShareDataPermissionKey) } + } + + static var isLtcSwapped: Bool { + get { return defaults.bool(forKey: isLtcSwappedKey) + } + set { defaults.set(newValue, forKey: isLtcSwappedKey) } + } + + static var userIsInUSA: Bool { + get { return defaults.bool(forKey: userIsInUSAKey) + } + set { defaults.set(newValue, forKey: userIsInUSAKey) } + } + + // + // 2 - photons + // 5 - lites + // 8 - LTC + // + static var maxDigits: Int { + get { + guard defaults.object(forKey: maxDigitsKey) != nil + else { + return 8 /// Default to LTC + } + return defaults.integer(forKey: maxDigitsKey) + } + set { defaults.set(newValue, forKey: maxDigitsKey) } + } + + static var pushToken: Data? { + get { + guard defaults.object(forKey: pushTokenKey) != nil + else { + return nil + } + return defaults.data(forKey: pushTokenKey) + } + set { defaults.set(newValue, forKey: pushTokenKey) } + } + + static var currentRate: Rate? { + guard let data = defaults.object(forKey: currentRateKey) as? [String: Any] + else { + return nil + } + return Rate(data: data) + } + + static var currentRateData: [String: Any]? { + get { + guard let data = defaults.object(forKey: currentRateKey) as? [String: Any] + else { + return nil + } + return data + } + set { defaults.set(newValue, forKey: currentRateKey) } + } + + static var customNodeIP: Int? { + get { + guard defaults.object(forKey: customNodeIPKey) != nil else { return nil } + return defaults.integer(forKey: customNodeIPKey) + } + set { defaults.set(newValue, forKey: customNodeIPKey) } + } + + static var customNodePort: Int? { + get { + guard defaults.object(forKey: customNodePortKey) != nil else { return nil } + return defaults.integer(forKey: customNodePortKey) + } + set { defaults.set(newValue, forKey: customNodePortKey) } + } + + static var hasPromptedShareData: Bool { + get { return defaults.bool(forKey: hasPromptedBiometricsKey) } + set { defaults.set(newValue, forKey: hasPromptedBiometricsKey) } + } +} + +// MARK: - Wallet Requires Backup + +extension UserDefaults { + static var legacyWalletNeedsBackup: Bool? { + guard defaults.object(forKey: legacyWalletNeedsBackupKey) != nil + else { + return nil + } + return defaults.bool(forKey: legacyWalletNeedsBackupKey) + } + + static func removeLegacyWalletNeedsBackupKey() { + defaults.removeObject(forKey: legacyWalletNeedsBackupKey) + } + + static var writePaperPhraseDate: Date? { + get { return defaults.object(forKey: writePaperPhraseDateKey) as! Date? } + set { defaults.set(newValue, forKey: writePaperPhraseDateKey) } + } + + static var walletRequiresBackup: Bool { + if UserDefaults.writePaperPhraseDate != nil { + return false + } else { + return true + } + } +} + +// MARK: - Prompts + +extension UserDefaults { + static var hasPromptedBiometrics: Bool { + get { return defaults.bool(forKey: hasPromptedBiometricsKey) } + set { defaults.set(newValue, forKey: hasPromptedBiometricsKey) } + } +} diff --git a/litewallet/FeeManager.swift b/litewallet/FeeManager.swift new file mode 100644 index 000000000..1ddb579a2 --- /dev/null +++ b/litewallet/FeeManager.swift @@ -0,0 +1,83 @@ +import FirebaseAnalytics +import Foundation + +// this is the default that matches the mobile-api if the server is unavailable +private let defaultEconomyFeePerKB: UInt64 = 8000 // Updated Dec 2, 2024 +private let defaultRegularFeePerKB: UInt64 = 25000 +private let defaultLuxuryFeePerKB: UInt64 = 66746 +private let defaultTimestamp: UInt64 = 1_583_015_199_122 + +struct Fees: Equatable { + let luxury: UInt64 + let regular: UInt64 + let economy: UInt64 + let timestamp: UInt64 + + static var usingDefaultValues: Fees { + return Fees(luxury: defaultLuxuryFeePerKB, + regular: defaultRegularFeePerKB, + economy: defaultEconomyFeePerKB, + timestamp: defaultTimestamp) + } +} + +enum FeeType { + case regular + case economy + case luxury +} + +class FeeUpdater: Trackable { + // MARK: - Private + + private let walletManager: WalletManager + private let store: Store + private lazy var minFeePerKB: UInt64 = Fees.usingDefaultValues.economy + + private let maxFeePerKB = Fees.usingDefaultValues.luxury + private var timer: Timer? + private let feeUpdateInterval: TimeInterval = 15 // meet Nyquist for api server interval (30) + + // MARK: - Public + + init(walletManager: WalletManager, store: Store) { + self.walletManager = walletManager + self.store = store + } + + func refresh(completion: @escaping () -> Void) { + walletManager.apiClient?.feePerKb { newFees, error in + guard error == nil + else { + let properties: [String: String] = ["ERROR_MESSAGE": String(describing: error), + "ERROR_TYPE": "FEE_PER_KB"] + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR, properties: properties) + completion() + return + } + + if newFees == Fees.usingDefaultValues { + LWAnalytics.logEventWithParameters(itemName: ._20200301_DUDFPK) + self.saveEvent("wallet.didUseDefaultFeePerKB") + } + + self.store.perform(action: UpdateFees.set(newFees)) + completion() + } + + if timer == nil { + timer = Timer.scheduledTimer(timeInterval: feeUpdateInterval, + target: self, + selector: #selector(intervalRefresh), + userInfo: nil, repeats: true) + } + } + + func refresh() { + refresh(completion: {}) + } + + @objc func intervalRefresh() { + refresh(completion: {}) + } +} diff --git a/litewallet/FlowControllers/MessageUIPresenter.swift b/litewallet/FlowControllers/MessageUIPresenter.swift new file mode 100644 index 000000000..89cf3adbf --- /dev/null +++ b/litewallet/FlowControllers/MessageUIPresenter.swift @@ -0,0 +1,124 @@ +import MessageUI +import UIKit + +class MessageUIPresenter: NSObject, Trackable { + weak var presenter: UIViewController? + + func presentMailCompose(litecoinAddress: String, image: UIImage) { + presentMailCompose(string: "litecoin: \(litecoinAddress)", image: image) + } + + func presentMailCompose(bitcoinURL: String, image: UIImage) { + presentMailCompose(string: bitcoinURL, image: image) + } + + private func presentMailCompose(string: String, image: UIImage) { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setMessageBody(string, isHTML: false) + if let data = image.pngData() { + emailView.addAttachmentData(data, mimeType: "image/png", fileName: "litecoinqr.png") + } + emailView.mailComposeDelegate = self + present(emailView) + } + + func presentFeedbackCompose() { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setToRecipients([C.feedbackEmail]) + emailView.mailComposeDelegate = self + present(emailView) + } + + func presentSupportCompose() { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setSubject("Litewallet Support") + emailView.setToRecipients([C.supportEmail]) + emailView.setMessageBody(C.troubleshootingQuestions, isHTML: true) + emailView.mailComposeDelegate = self + present(emailView) + } + + func presentMailCompose(emailAddress: String) { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setToRecipients([emailAddress]) + emailView.mailComposeDelegate = self + saveEvent("receive.presentMailCompose") + present(emailView) + } + + func presentMessageCompose(address: String, image: UIImage) { + presentMessage(string: "litecoin: \(address)", image: image) + } + + func presentMessageCompose(bitcoinURL: String, image: UIImage) { + presentMessage(string: bitcoinURL, image: image) + } + + private func presentMessage(string: String, image: UIImage) { + guard MFMessageComposeViewController.canSendText() else { showMessageUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let textView = MFMessageComposeViewController() + textView.body = string + if let data = image.pngData() { + textView.addAttachmentData(data, typeIdentifier: "public.image", filename: "litecoinqr.png") + } + textView.messageComposeDelegate = self + saveEvent("receive.presentMessage") + present(textView) + } + + fileprivate var originalTitleTextAttributes: [NSAttributedString.Key: Any]? + + private func present(_ viewController: UIViewController) { + presenter?.view.isFrameChangeBlocked = true + presenter?.present(viewController, animated: true, completion: {}) + } + + fileprivate func dismiss(_ viewController: UIViewController) { + UINavigationBar.appearance().titleTextAttributes = originalTitleTextAttributes + viewController.dismiss(animated: true, completion: { + self.presenter?.view.isFrameChangeBlocked = false + }) + } + + private func showEmailUnavailableAlert() { + saveEvent("receive.emailUnavailable") + let alert = UIAlertController(title: S.ErrorMessages.emailUnavailableTitle.localize(), message: S.ErrorMessages.emailUnavailableMessage.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + presenter?.present(alert, animated: true, completion: nil) + } + + private func showMessageUnavailableAlert() { + saveEvent("receive.messagingUnavailable") + let alert = UIAlertController(title: S.ErrorMessages.messagingUnavailableTitle.localize(), message: S.ErrorMessages.messagingUnavailableMessage.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + presenter?.present(alert, animated: true, completion: nil) + } +} + +extension MessageUIPresenter: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith _: MFMailComposeResult, error _: Error?) + { + dismiss(controller) + } +} + +extension MessageUIPresenter: MFMessageComposeViewControllerDelegate { + func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith _: MessageComposeResult) + { + dismiss(controller) + } +} diff --git a/litewallet/FlowControllers/StartFlowPresenter.swift b/litewallet/FlowControllers/StartFlowPresenter.swift new file mode 100644 index 000000000..a51199265 --- /dev/null +++ b/litewallet/FlowControllers/StartFlowPresenter.swift @@ -0,0 +1,217 @@ +import UIKit + +class StartFlowPresenter: Subscriber { + // MARK: - Public + + // MARK: - Private + + private let store: Store + private let rootViewController: UIViewController + private var navigationController: ModalNavigationController? + private let navigationControllerDelegate: StartNavigationDelegate + private let walletManager: WalletManager + private var loginViewController: UIViewController? + private let loginTransitionDelegate = LoginTransitionDelegate() + + init(store: Store, walletManager: WalletManager, rootViewController: UIViewController) { + self.store = store + self.walletManager = walletManager + self.rootViewController = rootViewController + navigationControllerDelegate = StartNavigationDelegate(store: store) + addSubscriptions() + } + + private func addSubscriptions() { + store.subscribe(self, + selector: { $0.isStartFlowVisible != $1.isStartFlowVisible }, + callback: { self.handleStartFlowChange(state: $0) }) + store.lazySubscribe(self, + selector: { $0.isLoginRequired != $1.isLoginRequired }, + callback: { self.handleLoginRequiredChange(state: $0) }) + store.subscribe(self, name: .lock, + callback: { _ in self.presentLoginFlow(isPresentedForLock: true) }) + } + + private func handleStartFlowChange(state: ReduxState) { + if state.isStartFlowVisible { + guardProtected(queue: DispatchQueue.main) { [weak self] in + self?.presentStartFlow() + } + } else { + dismissStartFlow() + } + } + + private func handleLoginRequiredChange(state: ReduxState) { + if state.isLoginRequired { + presentLoginFlow(isPresentedForLock: false) + } else { + dismissLoginFlow() + } + } + + // MARK: - SwiftUI Start Flow + + private func presentStartFlow() { + /// DOC: This is a legacy path for iPad users since SwiftUI doesnt gracefully handle presentations like iPhone + if UIDevice.current.userInterfaceIdiom == .pad { + let startViewController = StartViewController(store: store, + didTapCreate: { [weak self] in + self?.pushPinCreationViewControllerForNewWallet() + }, + didTapRecover: { [weak self] in + guard let myself = self else { return } + let recoverIntro = RecoverWalletIntroViewController(didTapNext: myself.pushRecoverWalletView) + myself.navigationController?.setClearNavbar() + myself.navigationController?.modalPresentationStyle = .fullScreen + myself.navigationController?.setNavigationBarHidden(false, animated: false) + myself.navigationController?.pushViewController(recoverIntro, animated: true) + }) + + navigationController = ModalNavigationController(rootViewController: startViewController) + navigationController?.delegate = navigationControllerDelegate + navigationController?.modalPresentationStyle = .fullScreen + } else { + let startHostingController = StartHostingController(store: store, + walletManager: walletManager) + + startHostingController.viewModel.userWantsToCreate { + self.pushPinCreationViewControllerForNewWallet() + } + + startHostingController.viewModel.userWantsToRecover { + let recoverIntro = RecoverWalletIntroViewController(didTapNext: self.pushRecoverWalletView) + self.navigationController?.setClearNavbar() + self.navigationController?.modalPresentationStyle = .fullScreen + self.navigationController?.setNavigationBarHidden(false, animated: false) + self.navigationController?.pushViewController(recoverIntro, animated: true) + } + + navigationController = ModalNavigationController(rootViewController: startHostingController) + navigationController?.delegate = navigationControllerDelegate + navigationController?.modalPresentationStyle = .fullScreen + } + + if let startFlow = navigationController { + startFlow.setNavigationBarHidden(true, animated: false) + rootViewController.present(startFlow, animated: false, completion: nil) + } + } + + private var pushRecoverWalletView: () -> Void { + return { [weak self] in + guard let myself = self else { return } + let recoverWalletViewController = EnterPhraseViewController(store: myself.store, walletManager: myself.walletManager, reason: .setSeed(myself.pushPinCreationViewForRecoveredWallet)) + myself.navigationController?.pushViewController(recoverWalletViewController, animated: true) + } + } + + private func pushPinCreationViewControllerForNewWallet() { + let pinCreationViewController = UpdatePinViewController(store: store, walletManager: walletManager, type: .creationNoPhrase, showsBackButton: true, phrase: nil) + pinCreationViewController.setPinSuccess = { [weak self] pin in + autoreleasepool { + guard self?.walletManager.setRandomSeedPhrase() != nil else { self?.handleWalletCreationError(); return } + self?.store.perform(action: WalletChange.setWalletCreationDate(Date())) + DispatchQueue.walletQueue.async { + self?.walletManager.peerManager?.connect() + DispatchQueue.main.async { + self?.pushStartPaperPhraseCreationViewController(pin: pin) + self?.store.trigger(name: .didCreateOrRecoverWallet) + } + } + } + } + + navigationController?.setNavigationBarHidden(false, animated: false) + navigationController?.setTintableBackArrow() + navigationController?.setClearNavbar() + navigationController?.pushViewController(pinCreationViewController, animated: true) + } + + private var pushPinCreationViewForRecoveredWallet: (String) -> Void { + return { [weak self] phrase in + guard let myself = self else { return } + let pinCreationView = UpdatePinViewController(store: myself.store, walletManager: myself.walletManager, type: .creationWithPhrase, showsBackButton: false, phrase: phrase) + pinCreationView.setPinSuccess = { [weak self] _ in + DispatchQueue.walletQueue.async { + self?.walletManager.peerManager?.connect() + DispatchQueue.main.async { + self?.store.trigger(name: .didCreateOrRecoverWallet) + } + } + } + myself.navigationController?.pushViewController(pinCreationView, animated: true) + } + } + + private func pushStartPaperPhraseCreationViewController(pin: String) { + let paperPhraseViewController = StartPaperPhraseViewController(store: store, callback: { [weak self] in + self?.pushWritePaperPhraseViewController(pin: pin) + }) + paperPhraseViewController.title = S.SecurityCenter.Cells.paperKeyTitle.localize() + paperPhraseViewController.navigationItem.setHidesBackButton(true, animated: false) + paperPhraseViewController.hideCloseNavigationItem() // Forces user to confirm paper-key + + navigationController?.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.white, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController?.pushViewController(paperPhraseViewController, animated: true) + } + + private func pushWritePaperPhraseViewController(pin: String) { + let writeViewController = WritePaperPhraseViewController(store: store, walletManager: walletManager, pin: pin, callback: { [weak self] in + self?.pushConfirmPaperPhraseViewController(pin: pin) + }) + writeViewController.title = S.SecurityCenter.Cells.paperKeyTitle.localize() + writeViewController.hideCloseNavigationItem() + navigationController?.pushViewController(writeViewController, animated: true) + } + + private func pushConfirmPaperPhraseViewController(pin: String) { + let confirmVC = UIStoryboard(name: "Phrase", bundle: nil).instantiateViewController(withIdentifier: "ConfirmPaperPhraseViewController") as? ConfirmPaperPhraseViewController + confirmVC?.store = store + confirmVC?.walletManager = walletManager + confirmVC?.pin = pin + confirmVC?.didCompleteConfirmation = { [weak self] in + guard let myself = self else { return } + myself.store.perform(action: SimpleReduxAlert.Show(.paperKeySet(callback: { + self?.store.perform(action: HideStartFlow()) + }))) + } + navigationController?.navigationBar.tintColor = .white + if let confirmVC = confirmVC { + navigationController?.pushViewController(confirmVC, animated: true) + } + } + + private func presentLoginFlow(isPresentedForLock: Bool) { + let loginView = LoginViewController(store: store, isPresentedForLock: isPresentedForLock, walletManager: walletManager) + if isPresentedForLock { + loginView.shouldSelfDismiss = true + } + loginView.transitioningDelegate = loginTransitionDelegate + loginView.modalPresentationStyle = .overFullScreen + loginView.modalPresentationCapturesStatusBarAppearance = true + loginViewController = loginView + rootViewController.present(loginView, animated: false, completion: nil) + } + + private func handleWalletCreationError() { + let alert = UIAlertController(title: S.LitewalletAlert.error.localize(), message: "Could not create wallet", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + navigationController?.present(alert, animated: true, completion: nil) + } + + private func dismissStartFlow() { + navigationController?.dismiss(animated: true) { [weak self] in + self?.navigationController = nil + } + } + + private func dismissLoginFlow() { + loginViewController?.dismiss(animated: true, completion: { [weak self] in + self?.loginViewController = nil + }) + } +} diff --git a/litewallet/FlowControllers/StartNavigationDelegate.swift b/litewallet/FlowControllers/StartNavigationDelegate.swift new file mode 100644 index 000000000..c6d54b5d4 --- /dev/null +++ b/litewallet/FlowControllers/StartNavigationDelegate.swift @@ -0,0 +1,53 @@ +import UIKit + +class StartNavigationDelegate: NSObject, UINavigationControllerDelegate { + let store: Store + + init(store: Store) { + self.store = store + } + + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated _: Bool) + { + if viewController is RecoverWalletIntroViewController { + navigationController.navigationBar.tintColor = .white + navigationController.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.white, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController.setClearNavbar() + navigationController.navigationBar.barTintColor = .clear + } + + if viewController is EnterPhraseViewController { + navigationController.navigationBar.tintColor = .darkText + navigationController.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController.setClearNavbar() + navigationController.navigationBar.isTranslucent = false + navigationController.navigationBar.barTintColor = .whiteTint + } + + if viewController is UpdatePinViewController { + navigationController.navigationBar.tintColor = .darkText + navigationController.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController.setClearNavbar() + } + + if viewController is UpdatePinViewController { + if let gr = navigationController.interactivePopGestureRecognizer { + navigationController.view.removeGestureRecognizer(gr) + } + } + + if viewController is StartWipeWalletViewController { + navigationController.setClearNavbar() + navigationController.setWhiteStyle() + } + } +} diff --git a/litewallet/FlowControllers/URLController.swift b/litewallet/FlowControllers/URLController.swift new file mode 100644 index 000000000..97ef92660 --- /dev/null +++ b/litewallet/FlowControllers/URLController.swift @@ -0,0 +1,75 @@ +import UIKit + +// DEV: This whole class should be removed. +// Need more testing. +class URLController: Trackable { + init(store: Store, walletManager: WalletManager) { + self.store = store + self.walletManager = walletManager + } + + private let store: Store + private let walletManager: WalletManager + private var xSource, xSuccess, xError, uri: String? + + func handleUrl(_ url: URL) -> Bool { + saveEvent("send.handleURL", attributes: [ + "scheme": url.scheme ?? C.null, + "host": url.host ?? C.null, + "path": url.path, + ]) + + switch url.scheme ?? "" { + case "loaf": + if let query = url.query { + for component in query.components(separatedBy: "&") { + let pair = component.components(separatedBy: "+") + if pair.count < 2 { continue } + let key = pair[0] + var value = String(component[component.index(key.endIndex, offsetBy: 2)...]) + value = (value.replacingOccurrences(of: "+", with: " ") as NSString).removingPercentEncoding! + switch key { + case "x-source": + xSource = value + case "x-success": + xSuccess = value + case "x-error": + xError = value + case "uri": + uri = value + default: + print("Key not supported: \(key)") + } + } + } + + if url.host == "scanqr" || url.path == "/scanqr" { + store.trigger(name: .scanQr) + } else if url.host == "addresslist" || url.path == "/addresslist" { + store.trigger(name: .copyWalletAddresses(xSuccess, xError)) + } else if url.path == "/address" { + if let success = xSuccess { + copyAddress(callback: success) + } + } + return true + + default: + return false + } + } + + private func copyAddress(callback: String) { + if let url = URL(string: callback), let wallet = walletManager.wallet { + let queryLength = url.query?.utf8.count ?? 0 + let callback = callback.appendingFormat("%@address=%@", queryLength > 0 ? "&" : "?", wallet.receiveAddress) + if let callbackURL = URL(string: callback) { + UIApplication.shared.open(callbackURL, options: [:], completionHandler: nil) + } + } + } + + private func present(alert: UIAlertController) { + store.trigger(name: .showAlert(alert)) + } +} diff --git a/litewallet/Functions.swift b/litewallet/Functions.swift new file mode 100644 index 000000000..7805bf290 --- /dev/null +++ b/litewallet/Functions.swift @@ -0,0 +1,62 @@ +import UIKit + +func guardProtected(queue: DispatchQueue, callback: @escaping () -> Void) { + DispatchQueue.main.async { + if UIApplication.shared.isProtectedDataAvailable { + callback() + } else { + var observer: Any? + observer = NotificationCenter + .default + .addObserver(forName: UIApplication.protectedDataDidBecomeAvailableNotification, + object: nil, + queue: nil, + using: { _ in + queue.async { + callback() + } + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } + }) + } + } +} + +func strongify(_ context: Context, closure: @escaping (Context) -> Void) -> () -> Void +{ + return { [weak context] in + guard let strongContext = context else { return } + closure(strongContext) + } +} + +func strongify(_ context: Context?, closure: @escaping (Context, Arguments) -> Void) -> (Arguments) -> Void +{ + return { [weak context] arguments in + guard let strongContext = context else { return } + closure(strongContext, arguments) + } +} + +/// Description: 1701029422 +func tieredOpsFee(amount: UInt64) -> UInt64 { + switch amount { + case 0 ..< 1_398_000: + return 69900 + case 1_398_000 ..< 6_991_000: + return 111_910 + case 6_991_000 ..< 27_965_000: + return 279_700 + case 27_965_000 ..< 139_820_000: + return 699_540 + case 139_820_000 ..< 279_653_600: + return 1_049_300 + case 279_653_600 ..< 699_220_000: + return 1_398_800 + case 699_220_000 ..< 1_398_440_000: + return 2_797_600 + default: + return 2_797_600 + } +} diff --git a/litewallet/GoogleService-Info.plist b/litewallet/GoogleService-Info.plist new file mode 100644 index 000000000..0f5222c0c --- /dev/null +++ b/litewallet/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyC58u1MhxC27M-eWBHO60czLz08f4rrAMw + GCM_SENDER_ID + 969229325957 + PLIST_VERSION + 1 + BUNDLE_ID + com.litewallet.newborn + PROJECT_ID + litewallet-newborn-alpha + STORAGE_BUCKET + litewallet-newborn-alpha.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:969229325957:ios:b96117f47e5a662f41bbec + + \ No newline at end of file diff --git a/litewallet/KVStoreCoordinator.swift b/litewallet/KVStoreCoordinator.swift new file mode 100644 index 000000000..a1d9f6e86 --- /dev/null +++ b/litewallet/KVStoreCoordinator.swift @@ -0,0 +1,45 @@ +import Foundation + +class KVStoreCoordinator: Subscriber { + init(store: Store, kvStore: BRReplicatedKVStore) { + self.store = store + self.kvStore = kvStore + } + + func retreiveStoredWalletInfo() { + guard !hasRetreivedInitialWalletInfo else { return } + if let walletInfo = WalletInfo(kvStore: kvStore) { + store.perform(action: WalletChange.setWalletName(walletInfo.name)) + store.perform(action: WalletChange.setWalletCreationDate(walletInfo.creationDate)) + } else { + print("no wallet info found") + } + hasRetreivedInitialWalletInfo = true + } + + func listenForWalletChanges() { + store.subscribe(self, + selector: { $0.walletState.creationDate != $1.walletState.creationDate }, + callback: { + if let existingInfo = WalletInfo(kvStore: self.kvStore) { + self.store.perform(action: WalletChange.setWalletCreationDate(existingInfo.creationDate)) + } else { + let newInfo = WalletInfo(name: $0.walletState.name) + newInfo.creationDate = $0.walletState.creationDate + self.set(newInfo) + } + }) + } + + private func set(_ info: BRKVStoreObject) { + do { + _ = try kvStore.set(info) + } catch { + print("error setting wallet info: \(error)") + } + } + + private let store: Store + private let kvStore: BRReplicatedKVStore + private var hasRetreivedInitialWalletInfo = false +} diff --git a/litewallet/LockScreenHeaderViewModel.swift b/litewallet/LockScreenHeaderViewModel.swift new file mode 100644 index 000000000..ee19d72da --- /dev/null +++ b/litewallet/LockScreenHeaderViewModel.swift @@ -0,0 +1,54 @@ +import AVFoundation +import Foundation +import SwiftUI +import UIKit + +class LockScreenViewModel: ObservableObject, Subscriber { + // MARK: - Combine Variables + + @Published + var currentValueInFiat: String = "" + + @Published + var currencyCode: String = "" + + // MARK: - Public Variables + + var store: Store? + + init(store: Store) { + self.store = store + addSubscriptions() + fetchCurrentPrice() + } + + private func fetchCurrentPrice() { + guard let currentRate = store?.state.currentRate + else { + print("Error: Rate not fetched ") + return + } + + // Price Label + let fiatRate = Double(round(100 * currentRate.rate / 100)) + let formattedFiatString = String(format: "%.02f", fiatRate) + currencyCode = currentRate.code + let currencySymbol = Currency.getSymbolForCurrencyCode(code: currencyCode) ?? "" + currentValueInFiat = String(currencySymbol + formattedFiatString) + } + + // MARK: - Add Subscriptions + + private func addSubscriptions() { + guard let store = store + else { + NSLog("ERROR: Store not initialized") + return + } + + store.subscribe(self, selector: { $0.currentRate != $1.currentRate }, + callback: { _ in + self.fetchCurrentPrice() + }) + } +} diff --git a/litewallet/LoginView.swift b/litewallet/LoginView.swift new file mode 100644 index 000000000..817b88505 --- /dev/null +++ b/litewallet/LoginView.swift @@ -0,0 +1,30 @@ +// +// LoginView.swift +// litewallet +// +// Created by Kerry Washington on 12/25/23. +// Copyright © 2023 Litecoin Foundation. All rights reserved. +// +import SwiftUI + +struct LoginView: View { + @ObservedObject + var viewModel: LockScreenViewModel + + init(viewModel: LockScreenViewModel) { + self.viewModel = viewModel + + /// lockScreenHeaderView + } + + var body: some View { + GeometryReader { _ in + ZStack { + Color.litewalletBlue.edgesIgnoringSafeArea(.all) + VStack { + Spacer() + } + } + } + } +} diff --git a/litewallet/LoginViewController.swift b/litewallet/LoginViewController.swift new file mode 100644 index 000000000..4f2445f07 --- /dev/null +++ b/litewallet/LoginViewController.swift @@ -0,0 +1,493 @@ +import Firebase +import LocalAuthentication +import SwiftUI +import UIKit + +private let squareButtonSize: CGFloat = 32.0 +private let headerHeight: CGFloat = 110 + +protocol LoginViewControllerDelegate { + func didUnlockLogin() +} + +class LoginViewController: UIViewController, Subscriber, Trackable { + // MARK: - Public + + var walletManager: WalletManager? { + didSet { + guard walletManager != nil else { return } + pinView = PinView(style: .login, length: store.state.pinLength) + } + } + + var shouldSelfDismiss = false + + init(store: Store, isPresentedForLock: Bool, walletManager: WalletManager? = nil) { + self.store = store + self.walletManager = walletManager + self.isPresentedForLock = isPresentedForLock + disabledView = WalletDisabledView(store: store) + if walletManager != nil { + pinView = PinView(style: .login, length: store.state.pinLength) + } + + let viewModel = LockScreenViewModel(store: self.store) + headerView = UIHostingController(rootView: LockScreenHeaderView(viewModel: viewModel)) + + super.init(nibName: nil, bundle: nil) + } + + deinit { + store.unsubscribe(self) + } + + // MARK: - Private + + private let store: Store + + private let backgroundView: UIView = { + let view = UIView() + view.backgroundColor = .liteWalletDarkBlue + return view + }() + + private let headerView: UIHostingController + private let pinPadViewController = PinPadViewController(style: .clear, keyboardType: .pinPad, maxDigits: 0) + private let pinViewContainer = UIView() + private var pinView: PinView? + private let isPresentedForLock: Bool + private let disabledView: WalletDisabledView + private let activityView = UIActivityIndicatorView(style: .large) + private let wipeBannerButton = UIButton() + + var delegate: LoginViewControllerDelegate? + + private var logo: UIImageView = { + let image = UIImageView(image: UIImage(named: "new-logotype-white")) + image.contentMode = .scaleAspectFit + image.alpha = 0.8 + return image + }() + + private let biometricsButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setImage(LAContext.biometricType() == .face ? #imageLiteral(resourceName: "FaceId") : #imageLiteral(resourceName: "TouchId"), for: .normal) + button.layer.borderColor = UIColor.white.cgColor + button.layer.borderWidth = 1.0 + button.layer.cornerRadius = squareButtonSize / 2.0 + button.layer.masksToBounds = true + button.accessibilityLabel = LAContext.biometricType() == .face ? S.UnlockScreen.faceIdText.localize() : S.UnlockScreen.touchIdText.localize() + return button + }() + + private let showLTCAddressButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setImage(#imageLiteral(resourceName: "genericqricon"), for: .normal) + button.layer.masksToBounds = true + return button + }() + + private let enterPINLabel = UILabel(font: .barlowSemiBold(size: 18), color: .white) + private var pinPadBottom: NSLayoutConstraint? + private var topControlTop: NSLayoutConstraint? + private var unlockTimer: Timer? + private var pinPadBackground = UIView() + private var hasAttemptedToShowBiometrics = false + private let lockedOverlay = UIVisualEffectView() + private var isResetting = false + private let versionLabel = UILabel(font: .barlowRegular(size: 12), color: .white) + private var isWalletEmpty = false + + override func viewDidLoad() { + checkWalletBalance() + addSubviews() + addConstraints() + addBiometricsButton() + + addPinPadCallback() + if pinView != nil { + addPinView() + } + addWipeWalletView() + disabledView.didTapReset = { [weak self] in + guard let store = self?.store else { return } + guard let walletManager = self?.walletManager else { return } + self?.isResetting = true + let nc = UINavigationController() + let recover = EnterPhraseViewController(store: store, walletManager: walletManager, reason: .validateForResettingPin + { phrase in + let updatePin = UpdatePinViewController(store: store, walletManager: walletManager, type: .creationWithPhrase, showsBackButton: false, phrase: phrase) + nc.pushViewController(updatePin, animated: true) + updatePin.resetFromDisabledWillSucceed = { + self?.disabledView.isHidden = true + } + updatePin.resetFromDisabledSuccess = { + self?.authenticationSucceded() + LWAnalytics.logEventWithParameters(itemName: ._20200217_DUWP) + } + }) + recover.addCloseNavigationItem() + nc.viewControllers = [recover] + nc.navigationBar.tintColor = .darkText + nc.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + nc.setClearNavbar() + nc.navigationBar.isTranslucent = false + nc.navigationBar.barTintColor = .whiteTint + nc.viewControllers = [recover] + self?.present(nc, animated: true, completion: nil) + } + store.subscribe(self, name: .loginFromSend, callback: { _ in + self.authenticationSucceded() + }) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + guard UIApplication.shared.applicationState != .background else { return } + if shouldUseBiometrics, !hasAttemptedToShowBiometrics, !isPresentedForLock { + hasAttemptedToShowBiometrics = true + biometricsTapped() + } + + addShowAddressButton() + + if !isResetting { + lockIfNeeded() + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unlockTimer?.invalidate() + } + + private func addPinView() { + guard let pinView = pinView else { return } + pinViewContainer.addSubview(pinView) + + logo.constrain([ + logo.topAnchor.constraint(equalTo: view.centerYAnchor, constant: -100), + logo.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logo.constraint(.height, constant: 45), + logo.constraint(.width, constant: 201), + ]) + enterPINLabel.constrain([ + enterPINLabel.topAnchor.constraint(equalTo: pinView.topAnchor, constant: -40), + enterPINLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + ]) + + pinView.constrain([ + pinView.centerYAnchor.constraint(equalTo: pinPadViewController.view.topAnchor, constant: -40), + pinView.centerXAnchor.constraint(equalTo: pinViewContainer.centerXAnchor), + pinView.widthAnchor.constraint(equalToConstant: pinView.width), + pinView.heightAnchor.constraint(equalToConstant: pinView.itemSize), + ]) + } + + private func addSubviews() { + view.addSubview(backgroundView) + view.addSubview(headerView.view) + view.addSubview(pinViewContainer) + view.addSubview(logo) + view.addSubview(versionLabel) + view.addSubview(enterPINLabel) + + pinPadBackground.backgroundColor = .clear + if walletManager != nil { + view.addSubview(pinPadBackground) + } else { + view.addSubview(activityView) + } + } + + private func addConstraints() { + backgroundView.constrain(toSuperviewEdges: nil) + headerView.view.constrain([ + headerView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + headerView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + headerView.view.topAnchor.constraint(equalTo: backgroundView.topAnchor), + headerView.view.heightAnchor.constraint(equalToConstant: headerHeight), + ]) + + if walletManager != nil { + addChildViewController(pinPadViewController, layout: { + pinPadBottom = pinPadViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -120) + pinPadViewController.view.constrain([ + pinPadViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + pinPadViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + pinPadBottom, + pinPadViewController.view.heightAnchor.constraint(equalToConstant: pinPadViewController.height), + ]) + }) + } + pinViewContainer.constrain(toSuperviewEdges: nil) + + versionLabel.constrain([ + versionLabel.constraint(.bottom, toView: view, constant: -15), + versionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor), + versionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor), + versionLabel.heightAnchor.constraint(equalToConstant: 24.0), + ]) + + if walletManager != nil { + pinPadBackground.constrain([ + pinPadBackground.leadingAnchor.constraint(equalTo: pinPadViewController.view.leadingAnchor), + pinPadBackground.trailingAnchor.constraint(equalTo: pinPadViewController.view.trailingAnchor), + pinPadBackground.topAnchor.constraint(equalTo: pinPadViewController.view.topAnchor), + pinPadBackground.bottomAnchor.constraint(equalTo: pinPadViewController.view.bottomAnchor), + ]) + } else { + activityView.constrain([ + activityView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + activityView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20.0), + ]) + activityView.startAnimating() + } + + enterPINLabel.text = S.UnlockScreen.enterPIN.localize() + versionLabel.text = AppVersion.string + versionLabel.textAlignment = .center + } + + private func deviceTopConstraintConstant() -> CGFloat { + let screenHeight = E.screenHeight + var constant = 0.0 + if screenHeight <= 640 { + constant = 35 + } else if screenHeight > 640, screenHeight < 800 { + constant = 45 + } else { + constant = 55 + } + return C.padding[1] + CGFloat(constant) + } + + private func addWipeWalletView() { + view.addSubview(wipeBannerButton) + wipeBannerButton.translatesAutoresizingMaskIntoConstraints = true + wipeBannerButton.backgroundColor = .clear + wipeBannerButton.adjustsImageWhenHighlighted = true + + wipeBannerButton.constrain([ + wipeBannerButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -35), + wipeBannerButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), + wipeBannerButton.trailingAnchor.constraint(equalTo: view.trailingAnchor), + wipeBannerButton.heightAnchor.constraint(equalToConstant: 60), + ]) + + wipeBannerButton.setTitle(S.WipeWallet.emptyWallet.localize(), for: .normal) + wipeBannerButton.setTitleColor(UIColor.white.withAlphaComponent(0.7), for: .normal) + wipeBannerButton.titleLabel?.font = .barlowSemiBold(size: 17) + wipeBannerButton.addTarget(self, action: #selector(wipeTapped), for: .touchUpInside) + } + + private func addBiometricsButton() { + guard shouldUseBiometrics else { return } + view.addSubview(biometricsButton) + biometricsButton.addTarget(self, action: #selector(biometricsTapped), for: .touchUpInside) + biometricsButton.constrain([ + biometricsButton.widthAnchor.constraint(equalToConstant: squareButtonSize), + biometricsButton.heightAnchor.constraint(equalToConstant: squareButtonSize), + biometricsButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + biometricsButton.topAnchor.constraint(equalTo: view.topAnchor, constant: headerHeight + C.padding[2]), + ]) + } + + private func addShowAddressButton() { + view.addSubview(showLTCAddressButton) + showLTCAddressButton.addTarget(self, action: #selector(showLTCAddress), for: .touchUpInside) + showLTCAddressButton.constrain([ + showLTCAddressButton.widthAnchor.constraint(equalToConstant: squareButtonSize), + showLTCAddressButton.heightAnchor.constraint(equalToConstant: squareButtonSize), + showLTCAddressButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + showLTCAddressButton.topAnchor.constraint(equalTo: view.topAnchor, constant: headerHeight + C.padding[2]), + ]) + } + + private func addPinPadCallback() { + pinPadViewController.ouputDidUpdate = { [weak self] pin in + guard let myself = self else { return } + guard let pinView = self?.pinView else { return } + let attemptLength = pin.utf8.count + pinView.fill(attemptLength) + self?.pinPadViewController.isAppendingDisabled = attemptLength < myself.store.state.pinLength ? false : true + if attemptLength == myself.store.state.pinLength { + self?.authenticate(pin: pin) + } + } + } + + private func checkWalletBalance() { + if let wallet = walletManager?.wallet { + if wallet.balance == 0 { + isWalletEmpty = true + } else { + isWalletEmpty = false + } + } + } + + private func authenticate(pin: String) { + guard let walletManager = walletManager else { return } + guard !E.isScreenshots else { return authenticationSucceded() } + guard walletManager.authenticate(pin: pin) else { return authenticationFailed() } + authenticationSucceded() + LWAnalytics.logEventWithParameters(itemName: ._20200217_DUWP) + } + + private func authenticationSucceded() { + saveEvent("login.success") + let label = UILabel(font: enterPINLabel.font) + label.textColor = .white + label.text = S.UnlockScreen.unlocked.localize() + let lock = UIImageView(image: #imageLiteral(resourceName: "unlock")) + lock.transform = .init(scaleX: 0.6, y: 0.6) + + if let _pinView = pinView { + enterPINLabel.removeFromSuperview() + _pinView.removeFromSuperview() + } + + view.addSubview(label) + view.addSubview(lock) + + label.constrain([ + label.bottomAnchor.constraint(equalTo: view.centerYAnchor, constant: -C.padding[1]), + label.centerXAnchor.constraint(equalTo: view.centerXAnchor), + ]) + lock.constrain([ + lock.topAnchor.constraint(equalTo: label.bottomAnchor, constant: C.padding[1]), + lock.centerXAnchor.constraint(equalTo: label.centerXAnchor), + ]) + view.layoutIfNeeded() + + logo.alpha = 0.0 + wipeBannerButton.alpha = 1.0 + + UIView.spring(0.6, delay: 0.4, animations: { + self.pinPadBottom?.constant = self.pinPadViewController.height + self.topControlTop?.constant = -100.0 + + lock.alpha = 0.0 + label.alpha = 0.0 + self.wipeBannerButton.alpha = 0.0 + self.enterPINLabel.alpha = 0.0 + self.pinView?.alpha = 0.0 + + self.view.layoutIfNeeded() + }) { _ in + self.delegate?.didUnlockLogin() + if self.shouldSelfDismiss { + self.dismiss(animated: true, completion: nil) + } + self.store.perform(action: LoginSuccess()) + self.store.trigger(name: .showStatusBar) + } + } + + private func authenticationFailed() { + saveEvent("login.failed") + guard let pinView = pinView else { return } + pinPadViewController.view.isUserInteractionEnabled = false + pinView.shake { [weak self] in + self?.pinPadViewController.view.isUserInteractionEnabled = true + } + pinPadViewController.clear() + DispatchQueue.main.asyncAfter(deadline: .now() + pinView.shakeDuration) { [weak self] in + pinView.fill(0) + self?.lockIfNeeded() + } + } + + private var shouldUseBiometrics: Bool { + guard let walletManager = walletManager else { return false } + return LAContext.canUseBiometrics && !walletManager.pinLoginRequired && store.state.isBiometricsEnabled + } + + @objc func biometricsTapped() { + guard !isWalletDisabled else { return } + walletManager?.authenticate(biometricsPrompt: S.UnlockScreen.touchIdPrompt.localize(), completion: { result in + if result == .success { + self.authenticationSucceded() + LWAnalytics.logEventWithParameters(itemName: ._20200217_DUWB) + } + }) + } + + @objc func showLTCAddress() { + guard !isWalletDisabled else { return } + store.perform(action: RootModalActions.Present(modal: .loginAddress)) + } + + @objc func wipeTapped() { + store.perform(action: RootModalActions.Present(modal: .wipeEmptyWallet)) + } + + private func lockIfNeeded() { + if let disabledUntil = walletManager?.walletDisabledUntil { + let now = Date().timeIntervalSince1970 + if disabledUntil > now { + saveEvent("login.locked") + let disabledUntilDate = Date(timeIntervalSince1970: disabledUntil) + let unlockInterval = disabledUntil - now + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate(unlockInterval > C.secondsInDay ? "h:mm:ss a MMM d, yyy" : "h:mm:ss a") + + disabledView.setTimeLabel(string: String(format: S.UnlockScreen.disabled.localize(), df.string(from: disabledUntilDate))) + + pinPadViewController.view.isUserInteractionEnabled = false + unlockTimer?.invalidate() + unlockTimer = Timer.scheduledTimer(timeInterval: unlockInterval, target: self, selector: #selector(LoginViewController.unlock), userInfo: nil, repeats: false) + + if disabledView.superview == nil { + view.addSubview(disabledView) + setNeedsStatusBarAppearanceUpdate() + disabledView.constrain(toSuperviewEdges: nil) + disabledView.show() + } + } else { + pinPadViewController.view.isUserInteractionEnabled = true + disabledView.hide { [weak self] in + self?.disabledView.removeFromSuperview() + self?.setNeedsStatusBarAppearanceUpdate() + } + } + } + } + + private var isWalletDisabled: Bool { + guard let walletManager = walletManager else { return false } + let now = Date().timeIntervalSince1970 + return walletManager.walletDisabledUntil > now + } + + @objc private func unlock() { + saveEvent("login.unlocked") + delegate?.didUnlockLogin() + enterPINLabel.pushNewText(S.UnlockScreen.enterPIN.localize()) + pinPadViewController.view.isUserInteractionEnabled = true + unlockTimer = nil + disabledView.hide { [weak self] in + self?.disabledView.removeFromSuperview() + self?.setNeedsStatusBarAppearanceUpdate() + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + if disabledView.superview == nil { + return .lightContent + } else { + return .default + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/LoginViewModel.swift b/litewallet/LoginViewModel.swift new file mode 100644 index 000000000..de2f4e8d8 --- /dev/null +++ b/litewallet/LoginViewModel.swift @@ -0,0 +1,18 @@ +import AVFoundation +import Foundation +import SwiftUI +import UIKit + +class LoginViewModel: ObservableObject { + // MARK: - Combine Variables + + var store: Store + var walletManager: WalletManager? + var isPresentedForLock: Bool + + init(store: Store, isPresentedForLock: Bool, walletManager: WalletManager?) { + self.store = store + self.walletManager = walletManager + self.isPresentedForLock = isPresentedForLock + } +} diff --git a/litewallet/MessageUIPresenter.swift b/litewallet/MessageUIPresenter.swift new file mode 100644 index 000000000..89cf3adbf --- /dev/null +++ b/litewallet/MessageUIPresenter.swift @@ -0,0 +1,124 @@ +import MessageUI +import UIKit + +class MessageUIPresenter: NSObject, Trackable { + weak var presenter: UIViewController? + + func presentMailCompose(litecoinAddress: String, image: UIImage) { + presentMailCompose(string: "litecoin: \(litecoinAddress)", image: image) + } + + func presentMailCompose(bitcoinURL: String, image: UIImage) { + presentMailCompose(string: bitcoinURL, image: image) + } + + private func presentMailCompose(string: String, image: UIImage) { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setMessageBody(string, isHTML: false) + if let data = image.pngData() { + emailView.addAttachmentData(data, mimeType: "image/png", fileName: "litecoinqr.png") + } + emailView.mailComposeDelegate = self + present(emailView) + } + + func presentFeedbackCompose() { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setToRecipients([C.feedbackEmail]) + emailView.mailComposeDelegate = self + present(emailView) + } + + func presentSupportCompose() { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setSubject("Litewallet Support") + emailView.setToRecipients([C.supportEmail]) + emailView.setMessageBody(C.troubleshootingQuestions, isHTML: true) + emailView.mailComposeDelegate = self + present(emailView) + } + + func presentMailCompose(emailAddress: String) { + guard MFMailComposeViewController.canSendMail() else { showEmailUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let emailView = MFMailComposeViewController() + emailView.setToRecipients([emailAddress]) + emailView.mailComposeDelegate = self + saveEvent("receive.presentMailCompose") + present(emailView) + } + + func presentMessageCompose(address: String, image: UIImage) { + presentMessage(string: "litecoin: \(address)", image: image) + } + + func presentMessageCompose(bitcoinURL: String, image: UIImage) { + presentMessage(string: bitcoinURL, image: image) + } + + private func presentMessage(string: String, image: UIImage) { + guard MFMessageComposeViewController.canSendText() else { showMessageUnavailableAlert(); return } + originalTitleTextAttributes = UINavigationBar.appearance().titleTextAttributes + UINavigationBar.appearance().titleTextAttributes = nil + let textView = MFMessageComposeViewController() + textView.body = string + if let data = image.pngData() { + textView.addAttachmentData(data, typeIdentifier: "public.image", filename: "litecoinqr.png") + } + textView.messageComposeDelegate = self + saveEvent("receive.presentMessage") + present(textView) + } + + fileprivate var originalTitleTextAttributes: [NSAttributedString.Key: Any]? + + private func present(_ viewController: UIViewController) { + presenter?.view.isFrameChangeBlocked = true + presenter?.present(viewController, animated: true, completion: {}) + } + + fileprivate func dismiss(_ viewController: UIViewController) { + UINavigationBar.appearance().titleTextAttributes = originalTitleTextAttributes + viewController.dismiss(animated: true, completion: { + self.presenter?.view.isFrameChangeBlocked = false + }) + } + + private func showEmailUnavailableAlert() { + saveEvent("receive.emailUnavailable") + let alert = UIAlertController(title: S.ErrorMessages.emailUnavailableTitle.localize(), message: S.ErrorMessages.emailUnavailableMessage.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + presenter?.present(alert, animated: true, completion: nil) + } + + private func showMessageUnavailableAlert() { + saveEvent("receive.messagingUnavailable") + let alert = UIAlertController(title: S.ErrorMessages.messagingUnavailableTitle.localize(), message: S.ErrorMessages.messagingUnavailableMessage.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + presenter?.present(alert, animated: true, completion: nil) + } +} + +extension MessageUIPresenter: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith _: MFMailComposeResult, error _: Error?) + { + dismiss(controller) + } +} + +extension MessageUIPresenter: MFMessageComposeViewControllerDelegate { + func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith _: MessageComposeResult) + { + dismiss(controller) + } +} diff --git a/litewallet/ModalPresenter.swift b/litewallet/ModalPresenter.swift new file mode 100644 index 000000000..b54f6d4f1 --- /dev/null +++ b/litewallet/ModalPresenter.swift @@ -0,0 +1,866 @@ +import LocalAuthentication +import SafariServices +import SwiftUI +import UIKit + +class ModalPresenter: Subscriber, Trackable { + // MARK: - Public + + var walletManager: WalletManager? + init(store: Store, walletManager: WalletManager, window: UIWindow, apiClient: BRAPIClient) { + self.store = store + self.window = window + self.walletManager = walletManager + modalTransitionDelegate = ModalTransitionDelegate(type: .regular, store: store) + wipeNavigationDelegate = StartNavigationDelegate(store: store) + noAuthApiClient = apiClient + addSubscriptions() + } + + // MARK: - Private + + private let store: Store + private let window: UIWindow + private let alertHeight: CGFloat = 260.0 + private let modalTransitionDelegate: ModalTransitionDelegate + private let messagePresenter = MessageUIPresenter() + private let securityCenterNavigationDelegate = SecurityCenterNavigationDelegate() + private let verifyPinTransitionDelegate = TransitioningDelegate() + private let noAuthApiClient: BRAPIClient + + private var currentRequest: PaymentRequest? + private var reachability = ReachabilityMonitor() + private var notReachableAlert: InAppAlert? + private let wipeNavigationDelegate: StartNavigationDelegate + + private func addSubscriptions() { + store.subscribe(self, + selector: { $0.rootModal != $1.rootModal }, + callback: { self.presentModal($0.rootModal) }) + store.subscribe(self, + selector: { $0.alert != $1.alert && $1.alert != nil }, + callback: { self.handleAlertChange($0.alert) }) + + // Subscribe to prompt actions + store.subscribe(self, name: .promptUpgradePin, callback: { _ in + self.presentUpgradePin() + }) + store.subscribe(self, name: .promptPaperKey, callback: { _ in + self.presentWritePaperKey() + }) + store.subscribe(self, name: .promptBiometrics, callback: { _ in + self.presentBiometricsSetting() + }) + store.subscribe(self, name: .promptShareData, callback: { _ in + self.promptShareData() + }) + store.subscribe(self, name: .recommendRescan, callback: { _ in + self.presentRescan() + }) + + store.subscribe(self, name: .scanQr, callback: { _ in + self.handleScanQrURL() + }) + store.subscribe(self, name: .copyWalletAddresses(nil, nil), callback: { + guard let trigger = $0 else { return } + if case let .copyWalletAddresses(success, error) = trigger { + self.handleCopyAddresses(success: success, error: error) + } + }) + reachability.didChange = { isReachable in + if isReachable { + self.hideNotReachable() + } else { + self.showNotReachable() + } + } + store.subscribe(self, name: .lightWeightAlert(""), callback: { + guard let trigger = $0 else { return } + if case let .lightWeightAlert(message) = trigger { + self.showLightWeightAlert(message: message) + } + }) + store.subscribe(self, name: .showAlert(nil), callback: { + guard let trigger = $0 else { return } + if case let .showAlert(alert) = trigger { + if let alert = alert { + self.topViewController?.present(alert, animated: true, completion: nil) + } + } + }) + } + + // MARK: - Prompts + + private func presentRescan() { + let vc = ReScanViewController(store: store) + let nc = UINavigationController(rootViewController: vc) + nc.setClearNavbar() + vc.addCloseNavigationItem() + topViewController?.present(nc, animated: true, completion: nil) + } + + func presentBiometricsSetting() { + guard let walletManager = walletManager else { return } + let biometricsSettings = BiometricsSettingsViewController(walletManager: walletManager, store: store) + biometricsSettings.addCloseNavigationItem(tintColor: .white) + let nc = ModalNavigationController(rootViewController: biometricsSettings) + biometricsSettings.presentSpendingLimit = strongify(self) { myself in + myself.pushBiometricsSpendingLimit(onNc: nc) + } + nc.setDefaultStyle() + nc.isNavigationBarHidden = true + nc.delegate = securityCenterNavigationDelegate + topViewController?.present(nc, animated: true, completion: nil) + } + + private func promptShareData() { + let shareData = ShareDataViewController(store: store) + let nc = ModalNavigationController(rootViewController: shareData) + nc.setDefaultStyle() + nc.isNavigationBarHidden = true + nc.delegate = securityCenterNavigationDelegate + shareData.addCloseNavigationItem() + topViewController?.present(nc, animated: true, completion: nil) + } + + func presentWritePaperKey() { + guard let vc = topViewController else { return } + presentWritePaperKey(fromViewController: vc) + } + + func presentUpgradePin() { + guard let walletManager = walletManager else { return } + let updatePin = UpdatePinViewController(store: store, walletManager: walletManager, type: .update) + let nc = ModalNavigationController(rootViewController: updatePin) + nc.setDefaultStyle() + nc.isNavigationBarHidden = true + nc.delegate = securityCenterNavigationDelegate + updatePin.addCloseNavigationItem() + topViewController?.present(nc, animated: true, completion: nil) + } + + private func presentModal(_ type: RootModal, configuration: ((UIViewController) -> Void)? = nil) { + guard type != .loginScan else { return presentLoginScan() } + guard let vc = rootModalViewController(type) + else { + store.perform(action: RootModalActions.Present(modal: .none)) + return + } + vc.transitioningDelegate = modalTransitionDelegate + vc.modalPresentationStyle = .overFullScreen + vc.modalPresentationCapturesStatusBarAppearance = true + configuration?(vc) + topViewController?.present(vc, animated: true, completion: { + self.store.perform(action: RootModalActions.Present(modal: .none)) + self.store.trigger(name: .hideStatusBar) + }) + } + + private func handleAlertChange(_ type: AlertType?) { + guard let type = type else { return } + presentAlert(type, completion: { + self.store.perform(action: SimpleReduxAlert.Hide()) + }) + } + + private func presentAlert(_ type: AlertType, completion: @escaping () -> Void) { + let alertView = AlertView(type: type) + guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first + else { + saveEvent("ERROR: Window not found in the UIApplication window stack") + return + } + + let size = window.bounds.size + window.addSubview(alertView) + + let topConstraint = alertView.constraint(.top, toView: window, constant: size.height) + alertView.constrain([ + alertView.constraint(.width, constant: size.width), + alertView.constraint(.height, constant: alertHeight + 25.0), + alertView.constraint(.leading, toView: window, constant: nil), + topConstraint, + ]) + window.layoutIfNeeded() + + UIView.spring(0.6, animations: { + topConstraint?.constant = size.height - self.alertHeight + window.layoutIfNeeded() + }, completion: { _ in + alertView.animate() + UIView.spring(0.6, delay: 3.0, animations: { + topConstraint?.constant = size.height + window.layoutIfNeeded() + }, completion: { _ in + // TODO: - Make these callbacks generic + if case let .paperKeySet(callback) = type { + callback() + } + if case let .pinSet(callback) = type { + callback() + } + if case let .sweepSuccess(callback) = type { + callback() + } + completion() + alertView.removeFromSuperview() + }) + }) + } + + private func presentFailureAlert(_: AlertFailureType, + errorMessage: String, + completion: @escaping () -> Void) + { + let hostingViewController = UIHostingController(rootView: AlertFailureView(alertFailureType: .failedResolution, + errorMessage: errorMessage)) + + guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first, + let failureAlertView = hostingViewController.view else { return } + let size = window.bounds.size + window.addSubview(failureAlertView) + + let topConstraint = failureAlertView.constraint(.top, toView: window, constant: size.height) + failureAlertView.constrain([ + failureAlertView.constraint(.width, constant: size.width), + failureAlertView.constraint(.height, constant: alertHeight + 50.0), + failureAlertView.constraint(.leading, toView: window, constant: nil), + topConstraint, + ]) + window.layoutIfNeeded() + + UIView.spring(0.6, animations: { + topConstraint?.constant = size.height - self.alertHeight + window.layoutIfNeeded() + }, completion: { _ in + UIView.spring(0.6, delay: 5.0, animations: { + topConstraint?.constant = size.height + window.layoutIfNeeded() + }, completion: { _ in + // TODO: - Make these callbacks generic + completion() + failureAlertView.removeFromSuperview() + }) + }) + } + + private func rootModalViewController(_ type: RootModal) -> UIViewController? { + switch type { + case .none: + return nil + case .send: + return makeSendView() + case .receive: + return receiveView(isRequestAmountVisible: true) + case .menu: + return menuViewController() + case .loginScan: + return nil // The scan view needs a custom presentation + case .loginAddress: + return receiveView(isRequestAmountVisible: false) + case .manageWallet: + return ModalViewController(childViewController: ManageWalletViewController(store: store), store: store) + case .wipeEmptyWallet: + return wipeEmptyView() + case .requestAmount: + guard let wallet = walletManager?.wallet else { return nil } + let requestVc = RequestAmountViewController(wallet: wallet, store: store) + requestVc.presentEmail = { [weak self] bitcoinURL, image in + self?.messagePresenter.presenter = self?.topViewController + self?.messagePresenter.presentMailCompose(bitcoinURL: bitcoinURL, image: image) + } + requestVc.presentText = { [weak self] bitcoinURL, image in + self?.messagePresenter.presenter = self?.topViewController + self?.messagePresenter.presentMessageCompose(bitcoinURL: bitcoinURL, image: image) + } + return ModalViewController(childViewController: requestVc, store: store) + } + } + + private func wipeEmptyView() -> UIViewController? { + guard let walletManager = walletManager else { return nil } + + let wipeEmptyvc = WipeEmptyWalletViewController(walletManager: walletManager, store: store, didTapYesDelete: ({ [weak self] in + guard let myself = self else { return } + myself.wipeWallet() + })) + return ModalViewController(childViewController: wipeEmptyvc, store: store) + } + + private func makeSendView() -> UIViewController? { + guard !store.state.walletState.isRescanning + else { + let alert = UIAlertController(title: S.LitewalletAlert.error.localize(), message: S.Send.isRescanning.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .cancel, handler: nil)) + topViewController?.present(alert, animated: true, completion: nil) + return nil + } + guard let walletManager = walletManager else { return nil } + guard let kvStore = walletManager.apiClient?.kv else { return nil } + + let sendVC = SendViewController(store: store, sender: Sender(walletManager: walletManager, kvStore: kvStore, store: store), walletManager: walletManager, initialRequest: currentRequest) + currentRequest = nil + + if store.state.isLoginRequired { + sendVC.isPresentedFromLock = true + } + + let root = ModalViewController(childViewController: sendVC, store: store) + sendVC.presentScan = presentScan(parent: root) + sendVC.presentVerifyPin = { [weak self, weak root] bodyText, callback in + guard let myself = self else { return } + guard let myroot = root else { return } + + let vc = VerifyPinViewController(bodyText: bodyText, pinLength: myself.store.state.pinLength, callback: callback) + vc.transitioningDelegate = myself.verifyPinTransitionDelegate + vc.modalPresentationStyle = .overFullScreen + vc.modalPresentationCapturesStatusBarAppearance = true + myroot.view.isFrameChangeBlocked = true + myroot.present(vc, animated: true, completion: nil) + } + sendVC.onPublishSuccess = { [weak self] in + self?.presentAlert(.sendSuccess, completion: {}) + } + + sendVC.onResolvedSuccess = { [weak self] in + self?.presentAlert(.resolvedSuccess, completion: {}) + } + + sendVC.onResolutionFailure = { [weak self] failureMessage in + self?.presentFailureAlert(.failedResolution, errorMessage: failureMessage, completion: {}) + } + return root + } + + private func receiveView(isRequestAmountVisible: Bool) -> UIViewController? { + guard let wallet = walletManager?.wallet else { return nil } + let receiveVC = ReceiveViewController(wallet: wallet, store: store, isRequestAmountVisible: isRequestAmountVisible) + let root = ModalViewController(childViewController: receiveVC, store: store) + receiveVC.presentEmail = { [weak self, weak root] address, image in + guard let root = root else { return } + self?.messagePresenter.presenter = root + self?.messagePresenter.presentMailCompose(litecoinAddress: address, image: image) + } + receiveVC.presentText = { [weak self, weak root] address, image in + guard let root = root else { return } + self?.messagePresenter.presenter = root + self?.messagePresenter.presentMessageCompose(address: address, image: image) + } + return root + } + + private func menuViewController() -> UIViewController? { + let menu = MenuViewController() + let root = ModalViewController(childViewController: menu, store: store) + menu.didTapSecurity = { [weak self, weak menu] in + self?.modalTransitionDelegate.reset() + menu?.dismiss(animated: true) { + self?.presentSecurityCenter() + } + } + + menu.didTapSupport = { [weak self, weak menu] in + menu?.dismiss(animated: true, completion: { + let urlString = FoundationSupport.dashboard + + guard let url = URL(string: urlString) else { return } + + LWAnalytics.logEventWithParameters(itemName: ._20201118_DTS) + + let vc = SFSafariViewController(url: url) + self?.topViewController?.present(vc, animated: true, completion: nil) + }) + } + menu.didTapLock = { [weak self, weak menu] in + menu?.dismiss(animated: true) { + self?.store.trigger(name: .lock) + } + } + menu.didTapSettings = { [weak self, weak menu] in + menu?.dismiss(animated: true) { + self?.presentSettings() + } + } + return root + } + + private func presentLoginScan() { + guard let top = topViewController else { return } + let present = presentScan(parent: top) + store.perform(action: RootModalActions.Present(modal: .none)) + present { paymentRequest in + guard let request = paymentRequest else { return } + self.currentRequest = request + self.presentModal(.send) + } + } + + private func presentSettings() { + guard let top = topViewController else { return } + guard let walletManager = walletManager else { return } + let settingsNav = UINavigationController() + let sections = ["About", "Wallet", "Manage", "Support", "Blockchain"] + let rows = [ + "About": [Setting(title: S.Settings.litewalletVersion.localize(), accessoryText: { + AppVersion.string + }, callback: {}), + Setting(title: S.Settings.litewalletEnvironment.localize(), accessoryText: { + var envName = "Release" + #if Debug || Testflight + envName = "Debug" + #endif + return envName + }, callback: {}), + Setting(title: S.Settings.litewalletPartners.localize(), callback: { + let partnerView = UIHostingController(rootView: PartnersView(viewModel: PartnerViewModel())) + settingsNav.pushViewController(partnerView, animated: true) + }), + Setting(title: S.Settings.socialLinks.localize(), callback: { + settingsNav.pushViewController(AboutViewController(), animated: true) + }), + + ], + "Wallet": [Setting(title: S.Settings.importTile.localize(), callback: { [weak self] in + guard let myself = self else { return } + guard let walletManager = myself.walletManager else { return } + let importNav = ModalNavigationController() + importNav.setClearNavbar() + importNav.setWhiteStyle() + let start = StartImportViewController(walletManager: walletManager, store: myself.store) + start.addCloseNavigationItem(tintColor: .white) + start.navigationItem.title = S.Import.title.localize() + importNav.viewControllers = [start] + settingsNav.dismiss(animated: true, completion: { + myself.topViewController?.present(importNav, animated: true, completion: nil) + }) + }), + Setting(title: S.Settings.wipe.localize(), callback: { [weak self] in + guard let myself = self else { return } + guard let walletManager = myself.walletManager else { return } + let nc = ModalNavigationController() + nc.setClearNavbar() + nc.setWhiteStyle() + nc.delegate = myself.wipeNavigationDelegate + let start = StartWipeWalletViewController { + let recover = EnterPhraseViewController(store: myself.store, walletManager: walletManager, reason: .validateForWipingWallet + { + myself.wipeWallet() + }) + nc.pushViewController(recover, animated: true) + } + start.addCloseNavigationItem(tintColor: .white) + start.navigationItem.title = S.WipeWallet.title.localize() + nc.viewControllers = [start] + settingsNav.dismiss(animated: true, completion: { + myself.topViewController?.present(nc, animated: true, completion: nil) + }) + }), + ], + "Manage": [ + Setting(title: S.Settings.languages.localize(), callback: strongify(self) { _ in + settingsNav.pushViewController(LanguageSelectionViewController(), animated: true) + }), + Setting(title: LAContext.biometricType() == .face ? S.Settings.faceIdLimit.localize() : S.Settings.touchIdLimit.localize(), accessoryText: { [weak self] in + guard let myself = self else { return "" } + guard let rate = myself.store.state.currentRate else { return "" } + let amount = Amount(amount: walletManager.spendingLimit, rate: rate, maxDigits: myself.store.state.maxDigits) + return amount.localCurrency + }, callback: { + self.pushBiometricsSpendingLimit(onNc: settingsNav) + }), + Setting(title: S.Settings.currency.localize(), accessoryText: { + let code = self.store.state.defaultCurrencyCode + let components: [String: String] = [NSLocale.Key.currencyCode.rawValue: code] + let identifier = Locale.identifier(fromComponents: components) + return Locale(identifier: identifier).currencyCode ?? "" + }, callback: { + guard let wm = self.walletManager else { print("NO WALLET MANAGER!"); return } + settingsNav.pushViewController(DefaultCurrencyViewController(walletManager: wm, store: self.store), animated: true) + }), + Setting(title: S.Settings.currentLocale.localize(), accessoryText: { + // Get the current locale + let currentLocale = Locale.current + + if let regionCode = currentLocale.regionCode, + let displayName = currentLocale.localizedString(forRegionCode: regionCode) + { + return displayName + } else { + return "" + } + + }, callback: { + let localeView = UIHostingController(rootView: LocaleChangeView(viewModel: LocaleChangeViewModel())) + settingsNav.pushViewController(localeView, animated: true) + }), + Setting(title: S.Settings.sync.localize(), callback: { + settingsNav.pushViewController(ReScanViewController(store: self.store), animated: true) + }), + Setting(title: S.UpdatePin.updateTitle.localize(), callback: strongify(self) { myself in + let updatePin = UpdatePinViewController(store: myself.store, walletManager: walletManager, type: .update) + settingsNav.pushViewController(updatePin, animated: true) + }), + ], + "Support": [ + Setting(title: S.Settings.shareData.localize(), callback: { + settingsNav.pushViewController(ShareDataViewController(store: self.store), animated: true) + }), + ], + "Blockchain": [ + Setting(title: S.Settings.advancedTitle.localize(), callback: { [weak self] in + guard let myself = self else { return } + guard let walletManager = myself.walletManager else { return } + let sections = ["Network"] + var networkRows = [Setting]() + networkRows = [Setting(title: "Litecoin Nodes", callback: { + let nodeSelector = NodeSelectorViewController(walletManager: walletManager) + settingsNav.pushViewController(nodeSelector, animated: true) + })] + + // TODO: Develop this feature for issues with the TXID + // if UserDefaults.didSeeCorruption { + // networkRows.append( + // Setting(title: S.WipeWallet.deleteDatabase, callback: { + // self?.deleteDatabase() + // }) + // ) + // } + + let advancedSettings = ["Network": networkRows] + let advancedSettingsVC = SettingsViewController(sections: sections, rows: advancedSettings, optionalTitle: S.Settings.advancedTitle.localize()) + settingsNav.pushViewController(advancedSettingsVC, animated: true) + }), + ], + ] + + let settings = SettingsViewController(sections: sections, rows: rows) + settings.addCloseNavigationItem() + settingsNav.viewControllers = [settings] + top.present(settingsNav, animated: true, completion: nil) + } + + private func presentScan(parent: UIViewController) -> PresentScan { + return { [weak parent] scanCompletion in + guard ScanViewController.isCameraAllowed + else { + self.saveEvent("scan.cameraDenied") + if let parent = parent { + ScanViewController.presentCameraUnavailableAlert(fromRoot: parent) + } + return + } + let vc = ScanViewController(completion: { paymentRequest in + scanCompletion(paymentRequest) + parent?.view.isFrameChangeBlocked = false + }, isValidURI: { address in + address.isValidAddress + }) + parent?.view.isFrameChangeBlocked = true + parent?.present(vc, animated: true, completion: {}) + } + } + + private func presentSecurityCenter() { + guard let walletManager = walletManager else { return } + let securityCenter = SecurityCenterViewController(store: store, walletManager: walletManager) + let nc = ModalNavigationController(rootViewController: securityCenter) + nc.setDefaultStyle() + nc.isNavigationBarHidden = true + nc.delegate = securityCenterNavigationDelegate + securityCenter.didTapPin = { [weak self] in + guard let myself = self else { return } + let updatePin = UpdatePinViewController(store: myself.store, walletManager: walletManager, type: .update) + nc.pushViewController(updatePin, animated: true) + } + securityCenter.didTapBiometrics = strongify(self) { myself in + let biometricsSettings = BiometricsSettingsViewController(walletManager: walletManager, store: myself.store) + biometricsSettings.presentSpendingLimit = { + myself.pushBiometricsSpendingLimit(onNc: nc) + } + nc.pushViewController(biometricsSettings, animated: true) + } + securityCenter.didTapPaperKey = { [weak self] in + self?.presentWritePaperKey(fromViewController: nc) + } + + window.rootViewController?.present(nc, animated: true, completion: nil) + } + + private func pushBiometricsSpendingLimit(onNc: UINavigationController) { + guard let walletManager = walletManager else { return } + + let verify = VerifyPinViewController(bodyText: S.VerifyPin.continueBody.localize(), pinLength: store.state.pinLength, callback: { [weak self] pin, vc in + guard let myself = self else { return false } + if walletManager.authenticate(pin: pin) { + vc.dismiss(animated: true, completion: { + let spendingLimit = BiometricsSpendingLimitViewController(walletManager: walletManager, store: myself.store) + onNc.pushViewController(spendingLimit, animated: true) + }) + return true + } else { + return false + } + }) + verify.transitioningDelegate = verifyPinTransitionDelegate + verify.modalPresentationStyle = .overFullScreen + verify.modalPresentationCapturesStatusBarAppearance = true + onNc.present(verify, animated: true, completion: nil) + } + + private func presentWritePaperKey(fromViewController vc: UIViewController) { + guard let walletManager = walletManager else { return } + let paperPhraseNavigationController = UINavigationController() + paperPhraseNavigationController.setClearNavbar() + paperPhraseNavigationController.setWhiteStyle() + paperPhraseNavigationController.modalPresentationStyle = .overFullScreen + let start = StartPaperPhraseViewController(store: store, callback: { [weak self] in + guard let myself = self else { return } + let verify = VerifyPinViewController(bodyText: S.VerifyPin.continueBody.localize(), pinLength: myself.store.state.pinLength, callback: { pin, vc in + if walletManager.authenticate(pin: pin) { + var write: WritePaperPhraseViewController? + write = WritePaperPhraseViewController(store: myself.store, walletManager: walletManager, pin: pin, callback: { [weak self] in + guard let myself = self else { return } + let confirmVC = UIStoryboard(name: "Phrase", bundle: nil).instantiateViewController(withIdentifier: "ConfirmPaperPhraseViewController") as? ConfirmPaperPhraseViewController + confirmVC?.store = myself.store + confirmVC?.walletManager = myself.walletManager + confirmVC?.pin = pin + confirmVC?.didCompleteConfirmation = { + confirmVC?.dismiss(animated: true, completion: { + myself.store.perform(action: SimpleReduxAlert.Show(.paperKeySet(callback: { + myself.store.perform(action: HideStartFlow()) + + }))) + }) + } + // write?.navigationItem.title = S.SecurityCenter.Cells.paperKeyTitle + if let confirm = confirmVC { + paperPhraseNavigationController.pushViewController(confirm, animated: true) + } + }) + write?.hideCloseNavigationItem() + /// write?.navigationItem.title = S.SecurityCenter.Cells.paperKeyTitle + + vc.dismiss(animated: true, completion: { + guard let write = write else { return } + paperPhraseNavigationController.pushViewController(write, animated: true) + }) + return true + } else { + return false + } + }) + verify.transitioningDelegate = self?.verifyPinTransitionDelegate + verify.modalPresentationStyle = .overFullScreen + verify.modalPresentationCapturesStatusBarAppearance = true + paperPhraseNavigationController.present(verify, animated: true, completion: nil) + }) + start.navigationItem.title = S.SecurityCenter.Cells.paperKeyTitle.localize() + + if UserDefaults.writePaperPhraseDate != nil { + start.addCloseNavigationItem(tintColor: .lightGray) + } else { + start.hideCloseNavigationItem() + } + + paperPhraseNavigationController.viewControllers = [start] + vc.present(paperPhraseNavigationController, animated: true, completion: nil) + } + + private func wipeWallet() { + let group = DispatchGroup() + let alert = UIAlertController(title: S.WipeWallet.alertTitle.localize(), message: S.WipeWallet.alertMessage.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: S.WipeWallet.wipe.localize(), style: .default, handler: { _ in + self.topViewController?.dismiss(animated: true, completion: { + let activity = BRActivityViewController(message: S.WipeWallet.wiping.localize()) + self.topViewController?.present(activity, animated: true, completion: nil) + + group.enter() + DispatchQueue.walletQueue.async { + self.walletManager?.peerManager?.disconnect() + group.leave() + } + + group.enter() + DispatchQueue.walletQueue.asyncAfter(deadline: .now() + 2.0) { + print("::: Pausing to show 'Wiping' Dialog") + group.leave() + } + + group.notify(queue: .main) { + if let canForceWipeWallet = (self.walletManager?.wipeWallet(pin: "forceWipe")), + canForceWipeWallet + { + self.store.trigger(name: .reinitWalletManager { + activity.dismiss(animated: true, completion: { + print("::: Reiniting the WalletManager") + }) + }) + } else { + let failure = UIAlertController(title: S.WipeWallet.failedTitle.localize(), message: S.WipeWallet.failedMessage.localize(), preferredStyle: .alert) + failure.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + self.topViewController?.present(failure, animated: true, completion: nil) + } + } + }) + })) + topViewController?.present(alert, animated: true, completion: nil) + } + + private func handleScanQrURL() { + guard !store.state.isLoginRequired else { presentLoginScan(); return } + + if topViewController is MainViewController || topViewController is LoginViewController { + presentLoginScan() + } else { + LWAnalytics.logEventWithParameters(itemName: ._20210427_HCIEEH) + if let presented = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first?.rootViewController?.presentedViewController + { + presented.dismiss(animated: true, completion: { + self.presentLoginScan() + }) + } + } + } + + private func handleCopyAddresses(success: String?, error _: String?) { + guard let walletManager = walletManager else { return } + let alert = UIAlertController(title: S.URLHandling.addressListAlertTitle.localize(), message: S.URLHandling.addressListAlertMessage.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: S.URLHandling.copy.localize(), style: .default, handler: { [weak self] _ in + guard let myself = self else { return } + let verify = VerifyPinViewController(bodyText: S.URLHandling.addressListVerifyPrompt.localize(), pinLength: myself.store.state.pinLength, callback: { [weak self] pin, view in + if walletManager.authenticate(pin: pin) { + self?.copyAllAddressesToClipboard() + view.dismiss(animated: true, completion: { + self?.store.perform(action: SimpleReduxAlert.Show(.addressesCopied)) + if let success = success, let url = URL(string: success) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + }) + return true + } else { + return false + } + }) + verify.transitioningDelegate = self?.verifyPinTransitionDelegate + verify.modalPresentationStyle = .overFullScreen + verify.modalPresentationCapturesStatusBarAppearance = true + self?.topViewController?.present(verify, animated: true, completion: nil) + })) + topViewController?.present(alert, animated: true, completion: nil) + } + + private func copyAllAddressesToClipboard() { + guard let wallet = walletManager?.wallet else { return } + let addresses = wallet.allAddresses.filter { wallet.addressIsUsed($0) } + UIPasteboard.general.string = addresses.joined(separator: "\n") + } + + private var topViewController: UIViewController? { + var viewController = window.rootViewController + while viewController?.presentedViewController != nil { + viewController = viewController?.presentedViewController + } + return viewController + } + + private func showNotReachable() { + guard notReachableAlert == nil else { return } + let alert = InAppAlert(message: S.LitewalletAlert.noInternet.localize(), image: #imageLiteral(resourceName: "BrokenCloud")) + notReachableAlert = alert + guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first + else { + saveEvent("ERROR: Window not found in the UIApplication window stack") + return + } + let size = window.bounds.size + window.addSubview(alert) + let bottomConstraint = alert.bottomAnchor.constraint(equalTo: window.topAnchor, constant: 0.0) + alert.constrain([ + alert.constraint(.width, constant: size.width), + alert.constraint(.height, constant: InAppAlert.height), + alert.constraint(.leading, toView: window, constant: nil), + bottomConstraint, + ]) + window.layoutIfNeeded() + alert.bottomConstraint = bottomConstraint + alert.hide = { + self.hideNotReachable() + } + UIView.spring(C.animationDuration, animations: { + alert.bottomConstraint?.constant = InAppAlert.height + window.layoutIfNeeded() + }, completion: { _ in }) + } + + private func hideNotReachable() { + UIView.animate(withDuration: C.animationDuration, animations: { + self.notReachableAlert?.bottomConstraint?.constant = 0.0 + self.notReachableAlert?.superview?.layoutIfNeeded() + }, completion: { _ in + self.notReachableAlert?.removeFromSuperview() + self.notReachableAlert = nil + }) + } + + private func showLightWeightAlert(message: String) { + let alert = LightWeightAlert(message: message) + + guard let view = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first + else { + saveEvent("ERROR: Window not found in the UIApplication window stack") + return + } + + view.addSubview(alert) + alert.constrain([ + alert.centerXAnchor.constraint(equalTo: view.centerXAnchor), + alert.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + alert.background.effect = nil + UIView.animate(withDuration: 0.6, animations: { + alert.background.effect = alert.effect + }, completion: { _ in + UIView.animate(withDuration: 0.6, delay: 1.0, options: [], animations: { + alert.background.effect = nil + }, completion: { _ in + alert.removeFromSuperview() + }) + }) + } +} + +class SecurityCenterNavigationDelegate: NSObject, UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated _: Bool) + { + guard let coordinator = navigationController.topViewController?.transitionCoordinator else { return } + + if coordinator.isInteractive { + coordinator.notifyWhenInteractionChanges { context in + // We only want to style the view controller if the + // pop animation wasn't cancelled + if !context.isCancelled { + self.setStyle(navigationController: navigationController, viewController: viewController) + } + } + } else { + setStyle(navigationController: navigationController, viewController: viewController) + } + } + + func setStyle(navigationController: UINavigationController, viewController: UIViewController) { + if viewController is SecurityCenterViewController { + navigationController.isNavigationBarHidden = true + } else { + navigationController.isNavigationBarHidden = false + } + + if viewController is BiometricsSettingsViewController { + navigationController.setWhiteStyle() + } else { + navigationController.setDefaultStyle() + } + } +} diff --git a/litewallet/Models/KeyboardNotificationInfo.swift b/litewallet/Models/KeyboardNotificationInfo.swift new file mode 100644 index 000000000..73c007c09 --- /dev/null +++ b/litewallet/Models/KeyboardNotificationInfo.swift @@ -0,0 +1,35 @@ +import UIKit + +struct KeyboardNotificationInfo { + let endFrame: CGRect + + let startFrame: CGRect + + var deltaY: CGFloat { + return endFrame.minY - startFrame.minY + } + + var animationOptions: UIView.AnimationOptions { + return UIView.AnimationOptions(rawValue: animationCurve << 16) + } + + let animationDuration: Double + + init?(_ userInfo: [AnyHashable: Any]?) { + guard let userInfo = userInfo else { return nil } + guard let endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, + let startFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue, + let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber, + let animationCurve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber + else { + return nil + } + + self.endFrame = endFrame.cgRectValue + self.startFrame = startFrame.cgRectValue + self.animationDuration = animationDuration.doubleValue + self.animationCurve = animationCurve.uintValue + } + + private let animationCurve: UInt +} diff --git a/litewallet/Models/Rate.swift b/litewallet/Models/Rate.swift new file mode 100644 index 000000000..c8c2cc843 --- /dev/null +++ b/litewallet/Models/Rate.swift @@ -0,0 +1,71 @@ +import Foundation +import UIKit + +struct Rate { + let code: String + let name: String + let rate: Double + let lastTimestamp: Date + + var currencySymbol: String { + if let symbol = Rate.symbolMap[code] { + return symbol + } else { + let components: [String: String] = [NSLocale.Key.currencyCode.rawValue: code] + let identifier = Locale.identifier(fromComponents: components) + return Locale(identifier: identifier).currencySymbol ?? code + } + } + + static var symbolMap: [String: String] = { + var map = [String: String]() + Locale.availableIdentifiers.forEach { identifier in + let locale = Locale(identifier: identifier) + guard let code = locale.currencyCode else { return } + guard let symbol = locale.currencySymbol else { return } + + if let collision = map[code] { + if collision.utf8.count > symbol.utf8.count { + map[code] = symbol + } + } else { + map[code] = symbol + } + } + return map + }() + + var locale: Locale { + let components: [String: String] = [NSLocale.Key.currencyCode.rawValue: code] + let identifier = Locale.identifier(fromComponents: components) + return Locale(identifier: identifier) + } + + static var empty: Rate { + return Rate(code: "", name: "", rate: 0.0, lastTimestamp: Date()) + } +} + +extension Rate { + init?(data: Any) { + guard let dictionary = data as? [String: Any] else { return nil } + guard let code = dictionary["code"] as? String else { return nil } + guard let name = dictionary["name"] as? String else { return nil } + guard let rate = dictionary["n"] as? Double else { return nil } + self.init(code: code, name: name, rate: rate, lastTimestamp: Date()) + } + + var dictionary: [String: Any] { + return [ + "code": code, + "name": name, + "n": rate, + ] + } +} + +extension Rate: Equatable {} + +func == (lhs: Rate, rhs: Rate) -> Bool { + return lhs.code == rhs.code && lhs.name == rhs.name && lhs.rate == rhs.rate +} diff --git a/litewallet/Models/Setting.swift b/litewallet/Models/Setting.swift new file mode 100644 index 000000000..8708ab5e0 --- /dev/null +++ b/litewallet/Models/Setting.swift @@ -0,0 +1,15 @@ +import Foundation + +struct Setting { + let title: String + let accessoryText: (() -> String)? + let callback: () -> Void +} + +extension Setting { + init(title: String, callback: @escaping () -> Void) { + self.title = title + accessoryText = nil + self.callback = callback + } +} diff --git a/litewallet/Models/SimpleUTXO.swift b/litewallet/Models/SimpleUTXO.swift new file mode 100644 index 000000000..a409a103f --- /dev/null +++ b/litewallet/Models/SimpleUTXO.swift @@ -0,0 +1,23 @@ +import BRCore +import Foundation + +struct SimpleUTXO { + let hash: UInt256 + let index: UInt32 + let script: [UInt8] + let satoshis: UInt64 + + init?(json: [String: Any]) { + guard let txid = json["txid"] as? String, + let vout = json["vout"] as? Int, + let scriptPubKey = json["scriptPubKey"] as? String, + let satoshis = json["satoshis"] as? UInt64 else { return nil } + guard let hashData = txid.hexToData, + let scriptData = scriptPubKey.hexToData else { return nil } + + hash = hashData.reverse.uInt256 + index = UInt32(vout) + script = [UInt8](scriptData) + self.satoshis = satoshis + } +} diff --git a/litewallet/Models/Types.swift b/litewallet/Models/Types.swift new file mode 100644 index 000000000..c774c4a77 --- /dev/null +++ b/litewallet/Models/Types.swift @@ -0,0 +1,94 @@ +import Foundation +import UIKit + +// MARK: - Satoshis + +struct Satoshis { + let rawValue: UInt64 +} + +extension Satoshis { + init(_ rawValue: UInt64) { + self.rawValue = rawValue + } + + init(bits: Bits) { + rawValue = UInt64((bits.rawValue * 100.0).rounded(.toNearestOrEven)) + } + + init(bitcoin: Bitcoin) { + rawValue = UInt64((bitcoin.rawValue * Double(C.satoshis)).rounded(.toNearestOrEven)) + } + + init(value: Double, rate: Rate) { + rawValue = UInt64((value / rate.rate * Double(C.satoshis)).rounded(.toNearestOrEven)) + } + + init?(btcString: String) { + var decimal: Decimal = 0.0 + var amount: Decimal = 0.0 + guard Scanner(string: btcString).scanDecimal(&decimal) else { return nil } + NSDecimalMultiplyByPowerOf10(&amount, &decimal, 8, .up) + rawValue = NSDecimalNumber(decimal: amount).uint64Value + } +} + +extension Satoshis: Equatable {} + +func == (lhs: Satoshis, rhs: Satoshis) -> Bool { + return lhs.rawValue == rhs.rawValue +} + +func == (lhs: Satoshis?, rhs: UInt64) -> Bool { + return lhs?.rawValue == rhs +} + +func + (lhs: Satoshis, rhs: UInt64) -> Satoshis { + return Satoshis(lhs.rawValue + rhs) +} + +func + (lhs: Satoshis, rhs: Satoshis) -> Satoshis { + return Satoshis(lhs.rawValue + rhs.rawValue) +} + +func += (lhs: inout Satoshis, rhs: UInt64) { + lhs = lhs + rhs +} + +func > (lhs: Satoshis, rhs: UInt64) -> Bool { + return lhs.rawValue > rhs +} + +func < (lhs: Satoshis, rhs: UInt64) -> Bool { + return lhs.rawValue < rhs +} + +// MARK: - Bits + +struct Bits { + let rawValue: Double +} + +extension Bits { + init(satoshis: Satoshis) { + rawValue = Double(satoshis.rawValue) / 100.0 + } + + init?(string: String) { + guard let value = Double(string) else { return nil } + rawValue = value + } +} + +// MARK: - Bitcoin + +struct Bitcoin { + let rawValue: Double +} + +extension Bitcoin { + init?(string: String) { + guard let value = Double(string) else { return nil } + rawValue = value + } +} diff --git a/litewallet/PINFieldView.swift b/litewallet/PINFieldView.swift new file mode 100644 index 000000000..769ff65ad --- /dev/null +++ b/litewallet/PINFieldView.swift @@ -0,0 +1,105 @@ +import Foundation +import SwiftUI +import UIKit + +struct PINFieldView: UIViewRepresentable { + // MARK: - Combine Variables + + @Binding + var pinText: String + + @Binding + var pinIsFilled: Bool + + @Binding + var viewRect: CGRect + + // MARK: - Public Variables + + public + var isFirstResponder: Bool = false + + public + var placeholder: String = "------" + + // MARK: - Private Variables + + private + let viewKerning: CGFloat = 10.0 + + private + let maxPinDigits: Int = 6 + + init(pinText: Binding, + pinIsFilled: Binding, + viewRect: Binding) + { + _pinText = pinText + _pinIsFilled = pinIsFilled + _viewRect = viewRect + } + + func makeUIView(context: UIViewRepresentableContext) -> UITextField { + let textField = UITextField() + textField.delegate = context.coordinator + textField.font = .barlowSemiBold(size: 24.0) + textField.textAlignment = .center + textField.backgroundColor = .clear + textField.textColor = UIColor(Color.liteWalletDarkBlue) + textField.defaultTextAttributes.updateValue(viewKerning, forKey: NSAttributedString.Key.kern) + textField.keyboardType = .decimalPad + textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [ + NSAttributedString.Key.kern: 15.0, + NSAttributedString.Key.foregroundColor: UIColor(Color.green), + NSAttributedString.Key.font: UIFont.barlowBold(size: 17.0), + ]) + viewRect = textField.bounds + return textField + } + + func updateUIView(_: UITextField, context _: UIViewRepresentableContext) {} + + func makeCoordinator() -> PINFieldView.Coordinator { + Coordinator(parent: self) + } + + class Coordinator: NSObject, UITextFieldDelegate { + var parent: PINFieldView + + init(parent: PINFieldView) { + self.parent = parent + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let maxLength = parent.maxPinDigits + let currentString: NSString = (textField.text ?? "") as NSString + let newString: NSString = + currentString.replacingCharacters(in: range, with: string) as NSString + + if newString.length == parent.maxPinDigits { + parent.pinText = String(newString) + parent.pinIsFilled = true + } else { + parent.pinIsFilled = false + } + + return newString.length <= maxLength + } + + func textFieldDidEndEditing(_ textField: UITextField) { + textField.resignFirstResponder() + } + } +} + +struct PINFieldView_Previews: PreviewProvider { + static var previews: some View { + Group { + HStack { + PINFieldView(pinText: .constant(""), + pinIsFilled: .constant(true), + viewRect: .constant(CGRect())) + } + } + } +} diff --git a/litewallet/POSTBouncer.html b/litewallet/POSTBouncer.html new file mode 100644 index 000000000..a710f739c --- /dev/null +++ b/litewallet/POSTBouncer.html @@ -0,0 +1,31 @@ + + + + + +

Loading...

+ + diff --git a/litewallet/PaymentProtocol.swift b/litewallet/PaymentProtocol.swift new file mode 100644 index 000000000..2f21ce87e --- /dev/null +++ b/litewallet/PaymentProtocol.swift @@ -0,0 +1,334 @@ +import BRCore +import Foundation + +class PaymentProtocolDetails { + internal let cPtr: UnsafeMutablePointer + internal var isManaged: Bool + + internal init(_ cPtr: UnsafeMutablePointer) { + self.cPtr = cPtr + isManaged = false + } + + init?(network: String = "main", outputs: [BRTxOutput], time: UInt64, expires: UInt64, memo: String? = nil, + paymentURL: String? = nil, merchantData: [UInt8]? = nil) + { + guard let cPtr = BRPaymentProtocolDetailsNew(network, outputs, outputs.count, time, expires, memo, paymentURL, + merchantData, merchantData?.count ?? 0) else { return nil } + self.cPtr = cPtr + isManaged = true + } + + init?(bytes: [UInt8]) { + guard let cPtr = BRPaymentProtocolDetailsParse(bytes, bytes.count) else { return nil } + self.cPtr = cPtr + isManaged = true + } + + var bytes: [UInt8] { + var bytes = [UInt8](repeating: 0, count: BRPaymentProtocolDetailsSerialize(cPtr, nil, 0)) + BRPaymentProtocolDetailsSerialize(cPtr, &bytes, bytes.count) + return bytes + } + + var network: String { // "main" or "test", default is "main" + return String(cString: cPtr.pointee.network) + } + + var outputs: [BRTxOutput] { // where to send payments, outputs[n].amount defaults to 0 + return [BRTxOutput](UnsafeBufferPointer(start: cPtr.pointee.outputs, count: cPtr.pointee.outCount)) + } + + var time: UInt64 { // request creation time, seconds since unix epoch, optional + return cPtr.pointee.time + } + + var expires: UInt64 { // when this request should be considered invalid, optional + return cPtr.pointee.expires + } + + var memo: String? { // human-readable description of request for the customer, optional + guard cPtr.pointee.memo != nil else { return nil } + return String(cString: cPtr.pointee.memo) + } + + var paymentURL: String? { // url to send payment and get payment ack, optional + guard cPtr.pointee.paymentURL != nil else { return nil } + return String(cString: cPtr.pointee.paymentURL) + } + + var merchantData: [UInt8]? { // arbitrary data to include in the payment message, optional + guard cPtr.pointee.merchantData != nil else { return nil } + return [UInt8](UnsafeBufferPointer(start: cPtr.pointee.merchantData, count: cPtr.pointee.merchDataLen)) + } + + deinit { + if isManaged { BRPaymentProtocolDetailsFree(cPtr) } + } +} + +class PaymentProtocolRequest { + internal let cPtr: UnsafeMutablePointer + internal var isManaged: Bool + private var cName: String? + private var errMsg: String? + private var didValidate: Bool = false + + internal init(_ cPtr: UnsafeMutablePointer) { + self.cPtr = cPtr + isManaged = false + } + + init?(version: UInt32 = 1, pkiType: String = "none", pkiData: [UInt8]? = nil, details: PaymentProtocolDetails, + signature: [UInt8]? = nil) + { + guard details.isManaged else { return nil } // request must be able take over memory management of details + guard let cPtr = BRPaymentProtocolRequestNew(version, pkiType, pkiData, pkiData?.count ?? 0, details.cPtr, + signature, signature?.count ?? 0) else { return nil } + details.isManaged = false + self.cPtr = cPtr + isManaged = true + } + + init?(data: Data) { + let bytes = [UInt8](data) + guard let cPtr = BRPaymentProtocolRequestParse(bytes, bytes.count) else { return nil } + self.cPtr = cPtr + isManaged = true + } + + var bytes: [UInt8] { + var bytes = [UInt8](repeating: 0, count: BRPaymentProtocolRequestSerialize(cPtr, nil, 0)) + BRPaymentProtocolRequestSerialize(cPtr, &bytes, bytes.count) + return bytes + } + + var version: UInt32 { // default is 1 + return cPtr.pointee.version + } + + var pkiType: String { // none / x509+sha256 / x509+sha1, default is "none" + return String(cString: cPtr.pointee.pkiType) + } + + var pkiData: [UInt8]? { // depends on pkiType, optional + guard cPtr.pointee.pkiData != nil else { return nil } + return [UInt8](UnsafeBufferPointer(start: cPtr.pointee.pkiData, count: cPtr.pointee.pkiDataLen)) + } + + var details: PaymentProtocolDetails { // required + return PaymentProtocolDetails(cPtr.pointee.details) + } + + var signature: [UInt8]? { // pki-dependent signature, optional + guard cPtr.pointee.signature != nil else { return nil } + return [UInt8](UnsafeBufferPointer(start: cPtr.pointee.signature, count: cPtr.pointee.sigLen)) + } + + var certs: [[UInt8]] { // array of DER encoded certificates + var certs = [[UInt8]]() + var idx = 0 + + while BRPaymentProtocolRequestCert(cPtr, nil, 0, idx) > 0 { + certs.append([UInt8](repeating: 0, count: BRPaymentProtocolRequestCert(cPtr, nil, 0, idx))) + BRPaymentProtocolRequestCert(cPtr, UnsafeMutablePointer(mutating: certs[idx]), certs[idx].count, idx) + idx = idx + 1 + } + + return certs + } + + var digest: [UInt8] { // hash of the request needed to sign or verify the request + let digest = [UInt8](repeating: 0, count: BRPaymentProtocolRequestDigest(cPtr, nil, 0)) + BRPaymentProtocolRequestDigest(cPtr, UnsafeMutablePointer(mutating: digest), digest.count) + return digest + } + + func isValid() -> Bool { + defer { didValidate = true } + + if pkiType != "none" { + var certs = [SecCertificate]() + let policies = [SecPolicy](repeating: SecPolicyCreateBasicX509(), count: 1) + var trust: SecTrust? + var trustResult = SecTrustResultType.invalid + + for c in self.certs { + if let cert = SecCertificateCreateWithData(nil, Data(bytes: c) as CFData) { certs.append(cert) } + } + + if !certs.isEmpty { + cName = SecCertificateCopySubjectSummary(certs[0]) as String? + } + + SecTrustCreateWithCertificates(certs as CFTypeRef, policies as CFTypeRef, &trust) + if let trust = trust { SecTrustEvaluate(trust, &trustResult) } // verify certificate chain + + // .unspecified indicates a positive result that wasn't decided by the user + guard trustResult == .unspecified || trustResult == .proceed + else { + errMsg = certs.count > 0 ? S.PaymentProtocol.Errors.untrustedCertificate.localize() : S.PaymentProtocol.Errors.missingCertificate.localize() + + if let trust = trust, let properties = SecTrustCopyProperties(trust) { + for prop in properties as! [[AnyHashable: Any]] { + if prop["type"] as? String != kSecPropertyTypeError as String { continue } + errMsg = errMsg! + " - " + (prop["value"] as! String) + break + } + } + + return false + } + + var status = errSecUnimplemented + var pubKey: SecKey? + if let trust = trust { pubKey = SecTrustCopyPublicKey(trust) } + + if let pubKey = pubKey, let signature = signature { + if pkiType == "x509+sha256" { + status = SecKeyRawVerify(pubKey, .PKCS1SHA256, digest, digest.count, signature, signature.count) + } else if pkiType == "x509+sha1" { + status = SecKeyRawVerify(pubKey, .PKCS1SHA1, digest, digest.count, signature, signature.count) + } + } + + guard status == errSecSuccess + else { + if status == errSecUnimplemented { + errMsg = S.PaymentProtocol.Errors.unsupportedSignatureType.localize() + print(errMsg!) + } else { + errMsg = NSError(domain: NSOSStatusErrorDomain, code: Int(status)).localizedDescription + print("SecKeyRawVerify error: " + errMsg!) + } + + return false + } + } else if !certs.isEmpty { // non-standard extention to include an un-certified request name + cName = String(data: Data(certs[0]), encoding: .utf8) + } + + guard details.expires == 0 || NSDate.timeIntervalSinceReferenceDate <= Double(details.expires) + else { + errMsg = S.PaymentProtocol.Errors.requestExpired.localize() + return false + } + + return true + } + + var commonName: String? { + if !didValidate { _ = isValid() } + return cName + } + + var errorMessage: String? { + if !didValidate { _ = isValid() } + return errMsg + } + + deinit { + if isManaged { BRPaymentProtocolRequestFree(cPtr) } + } +} + +class PaymentProtocolPayment { + internal let cPtr: UnsafeMutablePointer + internal var isManaged: Bool + + internal init(_ cPtr: UnsafeMutablePointer) { + self.cPtr = cPtr + isManaged = false + } + + init?(merchantData: [UInt8]? = nil, transactions: [BRTxRef?], refundTo: [(address: String, amount: UInt64)], + memo: String? = nil) + { + var txRefs = transactions + guard let cPtr = BRPaymentProtocolPaymentNew(merchantData, merchantData?.count ?? 0, &txRefs, txRefs.count, + refundTo.map { $0.amount }, + refundTo.map { BRAddress(string: $0.address) ?? BRAddress() }, + refundTo.count, memo) else { return nil } + self.cPtr = cPtr + isManaged = true + } + + init?(bytes: [UInt8]) { + guard let cPtr = BRPaymentProtocolPaymentParse(bytes, bytes.count) else { return nil } + self.cPtr = cPtr + isManaged = true + } + + var bytes: [UInt8] { + var bytes = [UInt8](repeating: 0, count: BRPaymentProtocolPaymentSerialize(cPtr, nil, 0)) + BRPaymentProtocolPaymentSerialize(cPtr, &bytes, bytes.count) + return bytes + } + + var merchantData: [UInt8]? { // from request->details->merchantData, optional + guard cPtr.pointee.merchantData != nil else { return nil } + return [UInt8](UnsafeBufferPointer(start: cPtr.pointee.merchantData, count: cPtr.pointee.merchDataLen)) + } + + var transactions: [BRTxRef?] { // array of signed BRTxRef to satisfy outputs from details + return [BRTxRef?](UnsafeBufferPointer(start: cPtr.pointee.transactions, count: cPtr.pointee.txCount)) + } + + var refundTo: [BRTxOutput] + { // where to send refunds, if a refund is necessary, refundTo[n].amount defaults to 0 + return [BRTxOutput](UnsafeBufferPointer(start: cPtr.pointee.refundTo, count: cPtr.pointee.refundToCount)) + } + + var memo: String? { // human-readable message for the merchant, optional + guard cPtr.pointee.memo != nil else { return nil } + return String(cString: cPtr.pointee.memo) + } + + deinit { + if isManaged { BRPaymentProtocolPaymentFree(cPtr) } + } +} + +class PaymentProtocolACK { + internal let cPtr: UnsafeMutablePointer + internal var isManaged: Bool + + internal init(_ cPtr: UnsafeMutablePointer) { + self.cPtr = cPtr + isManaged = false + } + + init?(payment: PaymentProtocolPayment, memo: String? = nil) { + guard payment.isManaged else { return nil } // ack must be able to take over memory management of payment + guard let cPtr = BRPaymentProtocolACKNew(payment.cPtr, memo) else { return nil } + payment.isManaged = false + self.cPtr = cPtr + isManaged = true + } + + init?(data: Data) { + let bytes = [UInt8](data) + guard let cPtr = BRPaymentProtocolACKParse(bytes, bytes.count) else { return nil } + self.cPtr = cPtr + isManaged = true + } + + var bytes: [UInt8] { + var bytes = [UInt8](repeating: 0, count: BRPaymentProtocolACKSerialize(cPtr, nil, 0)) + BRPaymentProtocolACKSerialize(cPtr, &bytes, bytes.count) + return bytes + } + + var payment: PaymentProtocolPayment { // payment message that triggered this ack, required + return PaymentProtocolPayment(cPtr.pointee.payment) + } + + var memo: String? { // human-readable message for customer, optional + guard cPtr.pointee.memo != nil else { return nil } + return String(cString: cPtr.pointee.memo) + } + + deinit { + if isManaged { BRPaymentProtocolACKFree(cPtr) } + } +} diff --git a/litewallet/PaymentRequest.swift b/litewallet/PaymentRequest.swift new file mode 100644 index 000000000..5e56db0ef --- /dev/null +++ b/litewallet/PaymentRequest.swift @@ -0,0 +1,119 @@ +import BRCore +import Foundation + +enum PaymentRequestType { + case local + case remote +} + +struct PaymentRequest { + init?(string: String) { + if var url = NSURL(string: string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).replacingOccurrences(of: " ", with: "%20")) + { + if let scheme = url.scheme, let resourceSpecifier = url.resourceSpecifier, url.host == nil { + url = NSURL(string: "\(scheme)://\(resourceSpecifier)")! + + if url.scheme == "litecoin", let host = url.host { + toAddress = host + guard let components = url.query?.components(separatedBy: "&") else { type = .local; return } + for component in components { + let pair = component.components(separatedBy: "=") + if pair.count < 2 { continue } + let key = pair[0] + var value = String(component[component.index(key.endIndex, offsetBy: 1)...]) + value = (value.replacingOccurrences(of: "+", with: " ") as NSString).removingPercentEncoding! + + switch key { + case "amount": + amount = Satoshis(btcString: value) + case "label": + label = value + case "message": + message = value + case "r": + r = URL(string: value) + default: + print("Key not found: \(key)") + } + } + type = r == nil ? .local : .remote + return + } + } else if url.scheme == "http" || url.scheme == "https" { + type = .remote + remoteRequest = url + return + } + } + + if string.isValidAddress { + toAddress = string + type = .local + return + } + + return nil + } + + init?(data: Data) { + paymentProtocolRequest = PaymentProtocolRequest(data: data) + type = .local + } + + func fetchRemoteRequest(completion: @escaping (PaymentRequest?) -> Void) { + let request: NSMutableURLRequest + if let url = r { + request = NSMutableURLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 5.0) + } else { + request = NSMutableURLRequest(url: remoteRequest! as URL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 5.0) // TODO: - fix ! + } + + request.setValue("application/litecoin-paymentrequest", forHTTPHeaderField: "Accept") + + URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in + guard error == nil else { return completion(nil) } + guard let data = data else { return completion(nil) } + guard let response = response else { return completion(nil) } + + if response.mimeType?.lowercased() == "application/litecoin-paymentrequest" { + completion(PaymentRequest(data: data)) + } else if response.mimeType?.lowercased() == "text/uri-list" { + guard let dataStringArray = String(data: data, encoding: .utf8)?.components(separatedBy: "\n") + else { + NSLog("ERROR: Data string must not be empty") + return + } + + for line in dataStringArray { + if line.hasPrefix("#") { continue } + completion(PaymentRequest(string: line)) + break + } + completion(nil) + } else { + completion(nil) + } + }.resume() + } + + static func requestString(withAddress: String, forAmount: UInt64) -> String { + let btcAmount = convertToBTC(fromSatoshis: forAmount) + return "litecoin:\(withAddress)?amount=\(btcAmount)" + } + + var toAddress: String? + let type: PaymentRequestType + var amount: Satoshis? + var label: String? + var message: String? + var remoteRequest: NSURL? + var paymentProtocolRequest: PaymentProtocolRequest? + var r: URL? +} + +private func convertToBTC(fromSatoshis: UInt64) -> String { + var decimal = Decimal(fromSatoshis) + var amount: Decimal = 0.0 + NSDecimalMultiplyByPowerOf10(&amount, &decimal, -8, .up) + return NSDecimalNumber(decimal: amount).stringValue +} diff --git a/litewallet/PinDigitView.swift b/litewallet/PinDigitView.swift new file mode 100644 index 000000000..5b68de505 --- /dev/null +++ b/litewallet/PinDigitView.swift @@ -0,0 +1,97 @@ +import Foundation +import SwiftUI + +struct PinDigit: Identifiable, Hashable { + let id = UUID() + let digit: String +} + +struct PinDigitView: View { + let pinDigit: PinDigit + + var body: some View { + GeometryReader { _ in + + ZStack { + VStack { + ZStack { + RoundedRectangle(cornerRadius: bigButtonCornerRadius) + .frame(height: 45, alignment: .center) + .foregroundColor(.red) + .shadow(radius: 3, x: 3.0, y: 3.0) + + Text(pinDigit.digit) + .frame(height: 45, alignment: .center) + .font(.barlowSemiBold(size: 18.0)) + .foregroundColor(.black) + } + } + } + } + } +} + +#Preview { + PinDigitView(pinDigit: PinDigit(digit: "0")) +} + +/// Inspired by https://stackoverflow.com/questions/72926965/creating-an-ios-passcode-view-with-swiftui-how-to-hide-a-textview + +import SwiftUI +struct PasscodeView: View { + @EnvironmentObject + var viewModel: StartViewModel + @Environment(\.dismiss) var dismiss + + private let maxDigits: Int = 6 + private let userPasscode = "000000" + + @State var enteredPasscode: String = "" + @FocusState var keyboardFocused: Bool + + public var body: some View { + ZStack { + HStack { + ForEach(0 ..< maxDigits, id: \.self) { + let entered = ($0 + 1) > enteredPasscode.count + + ZStack { + Image(systemName: "circle") + } + } + } + HStack { + TextField("", text: $enteredPasscode) + .opacity(1.0) + .keyboardType(.decimalPad) + .focused($keyboardFocused) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + keyboardFocused = true + } + } + } + } + .padding() + .onChange(of: enteredPasscode) { newValue in + if newValue.count == viewModel.walletManager.pinLength { + keyboardFocused = false + } + } + // .onChange(of: enteredPasscode) { _ in + // guard enteredPasscode.count == maxDigits else { return } +// + // passcodeValidation() + // } + } + + // func passcodeValidation() { + // if enteredPasscode == userPasscode { + // viewModel.isUnlocked = true + // dismiss() + // } else { + // enteredPasscode = "" + // showAlert = true + // } + // } +} diff --git a/litewallet/Platform/BRAPIClient+Assets.swift b/litewallet/Platform/BRAPIClient+Assets.swift new file mode 100644 index 000000000..c6b047727 --- /dev/null +++ b/litewallet/Platform/BRAPIClient+Assets.swift @@ -0,0 +1,296 @@ +import Foundation + +// open class AssetArchive { +// let name: String +// private let fileManager: FileManager +// private let archiveUrl: URL +// private let archivePath: String +// private let extractedPath: String +// let extractedUrl: URL +// private let apiClient: BRAPIClient +// +// private var archiveExists: Bool { +// return fileManager.fileExists(atPath: archivePath) +// } +// +// private var extractedDirExists: Bool { +// return fileManager.fileExists(atPath: extractedPath) +// } +// +// private var version: String? { +// guard let archiveContents = try? Data(contentsOf: archiveUrl) +// else { +// return nil +// } +// return archiveContents.sha256.hexString +// } +// +// init?(name: String, apiClient: BRAPIClient) { +// self.name = name +// self.apiClient = apiClient +// fileManager = FileManager.default +// let bundleDirUrl = apiClient.bundleDirUrl +// archiveUrl = bundleDirUrl.appendingPathComponent("\(name).tar") +// extractedUrl = bundleDirUrl.appendingPathComponent("\(name)-extracted", isDirectory: true) +// archivePath = archiveUrl.path +// extractedPath = extractedUrl.path +// } +// +// func update(completionHandler: @escaping (_ error: Error?) -> Void) { +// do { +// try ensureExtractedPath() +// // If directory creation failed due to file existing +// } catch let error as NSError where error.code == 512 && error.domain == NSCocoaErrorDomain { +// do { +// try fileManager.removeItem(at: apiClient.bundleDirUrl) +// try fileManager.createDirectory(at: extractedUrl, withIntermediateDirectories: true, attributes: nil) +// } catch let e { +// return completionHandler(e) +// } +// } catch let e { +// return completionHandler(e) +// } +// if !archiveExists { +// // see if the archive was shipped with the app +// copyBundledArchive() +// } +// if !archiveExists { +// // we do not have the archive, download a fresh copy +// return downloadCompleteArchive(completionHandler: completionHandler) +// } +// apiClient.getAssetVersions(name) { versions, err in +// DispatchQueue.global(qos: .utility).async { +// if let err = err { +// print("[AssetArchive] could not get asset versions. error: \(err)") +// return completionHandler(err) +// } +// guard let versions = versions, let version = self.version +// else { +// return completionHandler(BRAPIClientError.unknownError) +// } +// if versions.index(of: version) == versions.count - 1 { +// // have the most recent version +// print("[AssetArchive] already at most recent version of bundle \(self.name)") +// do { +// try self.extractArchive() +// return completionHandler(nil) +// } catch let e { +// print("[AssetArchive] error extracting bundle: \(e)") +// return completionHandler(BRAPIClientError.unknownError) +// } +// } else { +// // need to update the version +// self.downloadAndPatchArchive(fromVersion: version, completionHandler: completionHandler) +// } +// } +// } +// } +// +// private func downloadCompleteArchive(completionHandler: @escaping (_ error: Error?) -> Void) { +// apiClient.downloadAssetArchive(name) { data, err in +// DispatchQueue.global(qos: .utility).async { +// if let err = err { +// print("[AssetArchive] error downloading complete archive \(self.name) error=\(err)") +// return completionHandler(err) +// } +// guard let data = data +// else { +// return completionHandler(BRAPIClientError.unknownError) +// } +// do { +// try data.write(to: self.archiveUrl, options: .atomic) +// try self.extractArchive() +// return completionHandler(nil) +// } catch let e { +// print("[AssetArchive] error extracting complete archive \(self.name) error=\(e)") +// return completionHandler(e) +// } +// } +// } +// } +// +// private func downloadAndPatchArchive(fromVersion: String, completionHandler: @escaping (_ error: Error?) -> Void) +// { +// apiClient.downloadAssetDiff(name, fromVersion: fromVersion) { data, err in +// DispatchQueue.global(qos: .utility).async { +// if let err = err { +// print("[AssetArchive] error downloading asset path \(self.name) \(fromVersion) error=\(err)") +// return completionHandler(err) +// } +// guard let data = data +// else { +// return completionHandler(BRAPIClientError.unknownError) +// } +// let fm = self.fileManager +// let diffPath = self.apiClient.bundleDirUrl.appendingPathComponent("\(self.name).diff").path +// let oldBundlePath = self.apiClient.bundleDirUrl.appendingPathComponent("\(self.name).old").path +// do { +// if fm.fileExists(atPath: diffPath) { +// try fm.removeItem(atPath: diffPath) +// } +// if fm.fileExists(atPath: oldBundlePath) { +// try fm.removeItem(atPath: oldBundlePath) +// } +// try data.write(to: URL(fileURLWithPath: diffPath), options: .atomic) +// try fm.moveItem(atPath: self.archivePath, toPath: oldBundlePath) +// _ = try BRBSPatch.patch( +// oldBundlePath, newFilePath: self.archivePath, patchFilePath: diffPath +// ) +// try fm.removeItem(atPath: diffPath) +// try fm.removeItem(atPath: oldBundlePath) +// try self.extractArchive() +// return completionHandler(nil) +// } catch let e { +// // something failed, clean up whatever we can, next attempt +// // will download fresh +// _ = try? fm.removeItem(atPath: diffPath) +// _ = try? fm.removeItem(atPath: oldBundlePath) +// _ = try? fm.removeItem(atPath: self.archivePath) +// print("[AssetArchive] error applying diff \(self.name) error=\(e)") +// } +// } +// } +// } +// +// private func ensureExtractedPath() throws { +// if !extractedDirExists { +// try fileManager.createDirectory( +// atPath: extractedPath, withIntermediateDirectories: true, attributes: nil +// ) +// } +// } +// +// private func extractArchive() throws { +// try BRTar.createFilesAndDirectoriesAtPath(extractedPath, withTarPath: archivePath) +// } +// +// private func copyBundledArchive() { +// if let bundledArchiveUrl = Bundle.main.url(forResource: name, withExtension: "tar") { +// do { +// try fileManager.copyItem(at: bundledArchiveUrl, to: archiveUrl) +// print("[AssetArchive] used bundled archive for \(name)") +// } catch let e { +// print("[AssetArchive] unable to copy bundled archive `\(name)` \(bundledArchiveUrl) -> \(archiveUrl): \(e)") +// } +// } +// } +// } +// +//// Platform bundle management +// extension BRAPIClient { +// // updates asset bundles with names included in the AssetBundles.plist file +// // if we are in a staging/debug/test environment the bundle names will have "-staging" appended to them +// open func updateBundles(completionHandler: @escaping (_ results: [(String, Error?)]) -> Void) { +// // ensure we can create the bundle directory +// do { +// try ensureBundlePaths() +// } catch let e { +// // if not return the creation error for every bundle name +// return completionHandler([("INVALID", e)]) +// } +// guard let path = Bundle.main.path(forResource: "AssetBundles", ofType: "plist"), +// var names = NSArray(contentsOfFile: path) as? [String] +// else { +// log("updateBundles unable to load bundle names") +// return completionHandler([("INVALID", BRAPIClientError.unknownError)]) +// } +// +// if E.isDebug || E.isTestFlight { +// names = names.map { n in n + "-staging" } +// } +// +// let grp = DispatchGroup() +// let queue = DispatchQueue.global(qos: .utility) +// var results: [(String, Error?)] = names.map { v in (v, nil) } +// queue.async { +// var i = 0 +// for name in names { +// if let archive = AssetArchive(name: name, apiClient: self) { +// let resIdx = i +// grp.enter() +// archive.update(completionHandler: { err in +// objc_sync_enter(results) +// results[resIdx] = (name, err) +// objc_sync_exit(results) +// grp.leave() +// }) +// } +// i += 1 +// } +// grp.wait() +// completionHandler(results) +// } +// } +// +// fileprivate var bundleDirUrl: URL { +// let fm = FileManager.default +// let docsUrl = fm.urls(for: .documentDirectory, in: .userDomainMask).first! +// let bundleDirUrl = docsUrl.appendingPathComponent("bundles", isDirectory: true) +// return bundleDirUrl +// } +// +// private func ensureBundlePaths() throws { +// let fm = FileManager.default +// var attrs = try? fm.attributesOfItem(atPath: bundleDirUrl.path) +// if attrs == nil { +// try fm.createDirectory(atPath: bundleDirUrl.path, withIntermediateDirectories: true, attributes: nil) +// attrs = try fm.attributesOfItem(atPath: bundleDirUrl.path) +// } +// } +// +// open func getAssetVersions(_ name: String, completionHandler: @escaping ([String]?, Error?) -> Void) +// { +// let req = URLRequest(url: url("/assets/bundles/\(name)/versions")) +// dataTaskWithRequest(req) { data, _, err in +// if let err = err { +// completionHandler(nil, err) +// return +// } +// if let data = data, +// let parsed = try? JSONSerialization.jsonObject(with: data, options: []), +// let top = parsed as? NSDictionary, +// let versions = top["versions"] as? [String] +// { +// completionHandler(versions, nil) +// } else { +// completionHandler(nil, BRAPIClientError.malformedDataError) +// } +// }.resume() +// } +// +// open func downloadAssetArchive(_ name: String, completionHandler: @escaping (Data?, Error?) -> Void) +// { +// let req = URLRequest(url: url("/assets/bundles/\(name)/download")) +// dataTaskWithRequest(req) { data, response, err in +// if err != nil { +// return completionHandler(nil, err) +// } +// if response?.statusCode != 200 { +// return completionHandler(nil, BRAPIClientError.unknownError) +// } +// if let data = data { +// return completionHandler(data, nil) +// } else { +// return completionHandler(nil, BRAPIClientError.malformedDataError) +// } +// }.resume() +// } +// +// open func downloadAssetDiff(_ name: String, fromVersion: String, completionHandler: @escaping (Data?, Error?) -> Void) +// { +// let req = URLRequest(url: url("/assets/bundles/\(name)/diff/\(fromVersion)")) +// dataTaskWithRequest(req, handler: { data, resp, err in +// if err != nil { +// return completionHandler(nil, err) +// } +// if resp?.statusCode != 200 { +// return completionHandler(nil, BRAPIClientError.unknownError) +// } +// if let data = data { +// return completionHandler(data, nil) +// } else { +// return completionHandler(nil, BRAPIClientError.malformedDataError) +// } +// }).resume() +// } +// } diff --git a/litewallet/Platform/BRAPIClient.swift b/litewallet/Platform/BRAPIClient.swift new file mode 100644 index 000000000..0e49f977f --- /dev/null +++ b/litewallet/Platform/BRAPIClient.swift @@ -0,0 +1,380 @@ +import BRCore +import Foundation + +let BRAPIClientErrorDomain = "BRApiClientErrorDomain" + +// these flags map to api feature flag name values +// eg "buy-bitcoin-with-cash" is a persistent name in the /me/features list +@objc public enum BRFeatureFlags: Int, CustomStringConvertible { + case buyLitecoin + + public var description: String { + switch self { + case .buyLitecoin: return "buy-litecoin" + } + } +} + +public typealias URLSessionTaskHandler = (Data?, HTTPURLResponse?, NSError?) -> Void +public typealias URLSessionChallengeHandler = (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + +// an object which implements BRAPIAdaptor can execute API Requests on the current wallet's behalf +public protocol BRAPIAdaptor { + // execute an API request against the current wallet + func dataTaskWithRequest( + _ request: URLRequest, authenticated: Bool, retryCount: Int, + handler: @escaping URLSessionTaskHandler + ) -> URLSessionDataTask + + func url(_ path: String, args: [String: String]?) -> URL +} + +open class BRAPIClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate, BRAPIAdaptor { + private var authenticator: WalletAuthenticator + + // whether or not to emit log messages from this instance of the client + private var logEnabled = true + + // proto is the transport protocol to use for talking to the API (either http or https) + var proto = "https" + + // host is the server(s) on which the API is hosted + var host = "api-prod.lite-wallet.org" + + // isFetchingAuth is set to true when a request is currently trying to renew authentication (the token) + // it is useful because fetching auth is not idempotent and not reentrant, so at most one auth attempt + // can take place at any one time + private var isFetchingAuth = false + + // used when requests are waiting for authentication to be fetched + private var authFetchGroup = DispatchGroup() + + // the NSURLSession on which all NSURLSessionTasks are executed + private lazy var session: URLSession = .init(configuration: .default, delegate: self, delegateQueue: self.queue) + + // the queue on which the NSURLSession operates + private var queue = OperationQueue() + + // convenience getter for the API endpoint + private var baseUrl: String { + return "\(proto)://\(host)" + } + + init(authenticator: WalletAuthenticator) { + self.authenticator = authenticator + } + + // prints whatever you give it if logEnabled is true + func log(_ s: String) { + if !logEnabled { + return + } + print("[BRAPIClient] \(s)") + } + + var deviceId: String { + return UserDefaults.standard.deviceID + } + + var authKey: BRKey? { + if authenticator.noWallet { return nil } + guard let keyStr = authenticator.apiAuthKey else { return nil } + var key = BRKey() + key.compressed = 1 + if BRKeySetPrivKey(&key, keyStr) == 0 { + // DEV: Comment out to get tBTC + /// #if DEBUG + /// fatalError("Unable to decode private key") + /// #endif + } + return key + } + + // MARK: Networking functions + + // Constructs a full NSURL for a given path and url parameters + public func url(_ path: String, args: [String: String]? = nil) -> URL { + func joinPath(_ k: String...) -> URL { + return URL(string: ([baseUrl] + k).joined(separator: ""))! + } + + if let args = args { + return joinPath(path + "?" + args.map { + "\($0.0.urlEscapedString)=\($0.1.urlEscapedString)" + }.joined(separator: "&")) + } else { + return joinPath(path) + } + } + + private func signRequest(_ request: URLRequest) -> URLRequest { + var mutableRequest = request + let dateHeader = mutableRequest.allHTTPHeaderFields?.get(lowercasedKey: "date") + if dateHeader == nil { + // add Date header if necessary + mutableRequest.setValue(Date().RFC1123String(), forHTTPHeaderField: "Date") + } + if let tokenData = authenticator.userAccount, + let token = tokenData["token"] as? String, + let authKey = authKey, + let signingData = mutableRequest.signingString.data(using: .utf8) + { + let sig = signingData.sha256_2.compactSign(key: authKey) + let hval = "Litewallet \(token):\(sig.base58)" + mutableRequest.setValue(hval, forHTTPHeaderField: "Authorization") + } + return mutableRequest + } + + private func decorateRequest(_ request: URLRequest) -> URLRequest { + var actualRequest = request + actualRequest.setValue("\(E.isTestnet ? 1 : 0)", forHTTPHeaderField: "X-Litecoin-Testnet") + actualRequest.setValue("\((E.isTestFlight || E.isDebug) ? 1 : 0)", forHTTPHeaderField: "X-Testflight") + actualRequest.setValue(Locale.current.identifier, forHTTPHeaderField: "Accept-Language") + return actualRequest + } + + public func dataTaskWithRequest(_ request: URLRequest, authenticated: Bool = false, + retryCount: Int = 0, handler: @escaping URLSessionTaskHandler) -> URLSessionDataTask + { + let start = Date() + var logLine = "" + if let meth = request.httpMethod, let u = request.url { + logLine = "\(meth) \(u) auth=\(authenticated) retry=\(retryCount)" + } + + // copy the request and authenticate it. retain the original request for retries + var actualRequest = decorateRequest(request) + if authenticated { + actualRequest = signRequest(actualRequest) + } + return session.dataTask(with: actualRequest, completionHandler: { data, resp, err in + DispatchQueue.main.async { + let end = Date() + let dur = Int(end.timeIntervalSince(start) * 1000) + if let httpResp = resp as? HTTPURLResponse { + var errStr = "" + if httpResp.statusCode >= 400 { + if let data = data, let s = String(data: data, encoding: .utf8) { + errStr = s + } + } + + self.log("\(logLine) -> status=\(httpResp.statusCode) duration=\(dur)ms errStr=\(errStr)") + + if authenticated, httpResp.isBreadChallenge { + self.log("\(logLine) got authentication challenge from API - will attempt to get token") + self.getToken { err in + if err != nil, retryCount < 1 { // retry once + self.log("\(logLine) error retrieving token: \(String(describing: err)) - will retry") + DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 1)) { + self.dataTaskWithRequest( + request, authenticated: authenticated, + retryCount: retryCount + 1, handler: handler + ).resume() + } + } else if err != nil, retryCount > 0 { // fail if we already retried + self.log("\(logLine) error retrieving token: \(String(describing: err)) - will no longer retry") + handler(nil, nil, err) + } else if retryCount < 1 { // no error, so attempt the request again + self.log("\(logLine) retrieved token, so retrying the original request") + self.dataTaskWithRequest( + request, authenticated: authenticated, + retryCount: retryCount + 1, handler: handler + ).resume() + } else { + self.log("\(logLine) retried token multiple times, will not retry again") + handler(data, httpResp, err) + } + } + } else { + handler(data, httpResp, err as NSError?) + } + } else { + self.log("\(logLine) encountered connection error \(String(describing: err))") + handler(data, nil, err as NSError?) + } + } + }) + } + + // retrieve a token and save it in the keychain data for this account + private func getToken(_ handler: @escaping (NSError?) -> Void) { + if isFetchingAuth { + log("already fetching auth, waiting...") + authFetchGroup.notify(queue: DispatchQueue.main) { + handler(nil) + } + return + } + guard let authKey = authKey + else { + return handler(NSError(domain: BRAPIClientErrorDomain, code: 500, userInfo: [ + NSLocalizedDescriptionKey: S.ApiClient.notReady, + ])) + } + let authPubKey = authKey.publicKey + isFetchingAuth = true + log("auth: entering group") + authFetchGroup.enter() + var req = URLRequest(url: url("/token")) + req.httpMethod = "POST" + req.setValue("application/json", forHTTPHeaderField: "Content-Type") + req.setValue("application/json", forHTTPHeaderField: "Accept") + let reqJson = [ + "pubKey": authPubKey.base58, + "deviceID": deviceId, + ] + do { + let dat = try JSONSerialization.data(withJSONObject: reqJson, options: []) + req.httpBody = dat + } catch let e { + log("JSON Serialization error \(e)") + isFetchingAuth = false + authFetchGroup.leave() + return handler(NSError(domain: BRAPIClientErrorDomain, code: 500, userInfo: [ + NSLocalizedDescriptionKey: S.ApiClient.jsonError, + ])) + } + session.dataTask(with: req, completionHandler: { data, resp, err in + DispatchQueue.main.async { + if let httpResp = resp as? HTTPURLResponse { + // unsuccessful response from the server + if httpResp.statusCode != 200 { + if let data = data, let s = String(data: data, encoding: .utf8) { + self.log("Token error: \(s)") + } + self.isFetchingAuth = false + self.authFetchGroup.leave() + return handler(NSError(domain: BRAPIClientErrorDomain, code: httpResp.statusCode, userInfo: [ + NSLocalizedDescriptionKey: S.ApiClient.tokenError, + ])) + } + } + if let data = data { + do { + let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) + self.log("POST /token json response: \(json)") + if let topObj = json as? [String: Any], + let tok = topObj["token"] as? String, + let uid = topObj["userID"] as? String + { + // success! store it in the keychain + var kcData = self.authenticator.userAccount ?? [AnyHashable: Any]() + kcData["token"] = tok + kcData["userID"] = uid + self.authenticator.userAccount = kcData + } + } catch let e { + self.log("JSON Deserialization error \(e)") + } + } + self.isFetchingAuth = false + self.authFetchGroup.leave() + handler(err as NSError?) + } + }).resume() + } + + // MARK: URLSession Delegate + + public func urlSession(_: URLSession, task _: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if challenge.protectionSpace.host == host, challenge.protectionSpace.serverTrust != nil { + log("URLSession challenge accepted!") + completionHandler(.useCredential, + URLCredential(trust: challenge.protectionSpace.serverTrust!)) + } else { + log("URLSession challenge rejected") + completionHandler(.rejectProtectionSpace, nil) + } + } + } + + public func urlSession(_: URLSession, task: URLSessionTask, willPerformHTTPRedirection _: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) + { + var actualRequest = request + if let currentReq = task.currentRequest, var curHost = currentReq.url?.host, let curScheme = currentReq.url?.scheme + { + if let curPort = currentReq.url?.port, curPort != 443, curPort != 80 { + curHost = "\(curHost):\(curPort)" + } + if curHost == host, curScheme == proto { + // follow the redirect if we're interacting with our API + actualRequest = decorateRequest(request) + log("redirecting \(String(describing: currentReq.url)) to \(String(describing: request.url))") + if let curAuth = currentReq.allHTTPHeaderFields?["Authorization"], curAuth.hasPrefix("Litewallet") + { + // add authentication because the previous request was authenticated + log("adding authentication to redirected request") + actualRequest = signRequest(actualRequest) + } + return completionHandler(actualRequest) + } + } + completionHandler(nil) + } +} + +extension Dictionary where Key == String, Value == String { + func get(lowercasedKey k: String) -> String? { + let lcKey = k.lowercased() + if let v = self[lcKey] { + return v + } + for (lk, v) in self { + if lk.lowercased() == lcKey { + return v + } + } + return nil + } +} + +private extension URLRequest { + var signingString: String { + var parts = [ + httpMethod ?? "", + "", + allHTTPHeaderFields?.get(lowercasedKey: "content-type") ?? "", + allHTTPHeaderFields?.get(lowercasedKey: "date") ?? "", + url?.resourceString ?? "", + ] + if let meth = httpMethod { + switch meth { + case "POST", "PUT", "PATCH": + if let d = httpBody, !d.isEmpty { + parts[1] = d.sha256.base58 + } + default: break + } + } + return parts.joined(separator: "\n") + } +} + +private extension HTTPURLResponse { + var isBreadChallenge: Bool { + if let headers = allHeaderFields as? [String: String], + let challenge = headers.get(lowercasedKey: "www-authenticate") + { + if challenge.lowercased().hasPrefix("Litewallet") { + return true + } + } + return false + } +} + +private extension URL { + var resourceString: String { + var urlStr = "\(path)" + if let query = query { + if query.lengthOfBytes(using: String.Encoding.utf8) > 0 { + urlStr = "\(urlStr)?\(query)" + } + } + return urlStr + } +} diff --git a/litewallet/Platform/BRActivityView.swift b/litewallet/Platform/BRActivityView.swift new file mode 100644 index 000000000..c9daaacdc --- /dev/null +++ b/litewallet/Platform/BRActivityView.swift @@ -0,0 +1,77 @@ +import UIKit + +class BRActivityViewController: UIViewController { + let activityView = BRActivityView() + + init(message: String) { + super.init(nibName: nil, bundle: nil) + modalTransitionStyle = .crossDissolve + + if #available(iOS 8.0, *) { + modalPresentationStyle = .overFullScreen + } + + activityView.messageLabel.text = message + view = activityView + } + + @available(*, unavailable) + public required init(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@objc open class BRActivityView: UIView { + let activityIndicatorView = UIActivityIndicatorView(style: .large) + let boundingBoxView = UIView(frame: CGRect.zero) + let messageLabel = UILabel(frame: CGRect.zero) + + init() { + super.init(frame: CGRect.zero) + + backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + boundingBoxView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + boundingBoxView.layer.cornerRadius = 12.0 + + activityIndicatorView.startAnimating() + + messageLabel.font = UIFont.boldSystemFont(ofSize: UIFont.labelFontSize) + messageLabel.textColor = UIColor.white + messageLabel.textAlignment = .center + messageLabel.shadowColor = UIColor.black + messageLabel.shadowOffset = CGSize(width: 0.0, height: 1.0) + messageLabel.numberOfLines = 0 + + addSubview(boundingBoxView) + addSubview(activityIndicatorView) + addSubview(messageLabel) + } + + @available(*, unavailable) + public required init(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func layoutSubviews() { + super.layoutSubviews() + + boundingBoxView.frame.size.width = 160.0 + boundingBoxView.frame.size.height = 160.0 + boundingBoxView.frame.origin.x = ceil((bounds.width / 2.0) - (boundingBoxView.frame.width / 2.0)) + boundingBoxView.frame.origin.y = ceil((bounds.height / 2.0) - (boundingBoxView.frame.height / 2.0)) + + activityIndicatorView.frame.origin.x = ceil((bounds.width / 2.0) - (activityIndicatorView.frame.width / 2.0)) + activityIndicatorView.frame.origin.y = ceil((bounds.height / 2.0) - (activityIndicatorView.frame.height / 2.0)) + + let messageLabelSize = messageLabel.sizeThatFits(CGSize(width: 160.0 - 20.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + messageLabel.frame.size.width = messageLabelSize.width + messageLabel.frame.size.height = messageLabelSize.height + messageLabel.frame.origin.x = ceil((bounds.width / 2.0) - (messageLabel.frame.width / 2.0)) + messageLabel.frame.origin.y = ceil( + activityIndicatorView.frame.origin.y + + activityIndicatorView.frame.size.height + + ((boundingBoxView.frame.height - activityIndicatorView.frame.height) / 4.0) + - (messageLabel.frame.height / 2.0)) + } +} diff --git a/litewallet/Platform/BRCoding.swift b/litewallet/Platform/BRCoding.swift new file mode 100644 index 000000000..2d44f0b6f --- /dev/null +++ b/litewallet/Platform/BRCoding.swift @@ -0,0 +1,143 @@ +import Foundation + +// BRCoder/BRCoding works a lot like NSCoder/NSCoding but simpler +// instead of using optionals everywhere we just use zero values, and take advantage +// of the swift type system somewhat to make the whole api a little cleaner + +protocol BREncodable { + // return anything that is JSON-able + func encode() -> AnyObject + // zeroValue is a zero-value initializer + static func zeroValue() -> Self + // decode can be passed any value which is json-able + static func decode(_ value: AnyObject) -> Self +} + +// An object which can encode and decode values +open class BRCoder { + var data: [String: AnyObject] + + init(data: [String: AnyObject]) { + self.data = data + } + + func encode(_ obj: BREncodable, key: String) { + data[key] = obj.encode() + } + + func decode(_ key: String) -> T { + guard let d = data[key] + else { + return T.zeroValue() + } + return T.decode(d) + } +} + +// An object which may be encoded/decoded using the archiving/unarchiving classes below +protocol BRCoding { + init?(coder decoder: BRCoder) + func encode(_ coder: BRCoder) +} + +// A basic analogue of NSKeyedArchiver, except it uses JSON and uses +open class BRKeyedArchiver { + static func archivedDataWithRootObject(_ obj: BRCoding, compressed: Bool = true) -> Data { + let coder = BRCoder(data: [String: AnyObject]()) + obj.encode(coder) + do { + let j = try JSONSerialization.data(withJSONObject: coder.data, options: []) + guard let bz = (compressed ? nil : j) + else { + print("compression error") + return Data() + } + return bz + } catch let e { + print("BRKeyedArchiver unable to archive object: \(e)") + return "{}".data(using: String.Encoding.utf8)! + } + } +} + +// A basic analogue of NSKeyedUnarchiver +open class BRKeyedUnarchiver { + static func unarchiveObjectWithData(_ data: Data, compressed: Bool = true) -> T? { + do { + guard let bz = (compressed ? nil : data), + let j = try JSONSerialization.jsonObject(with: bz, options: []) as? [String: AnyObject] + else { + print("BRKeyedUnarchiver invalid json object, or invalid bz data") + return nil + } + let coder = BRCoder(data: j) + return T(coder: coder) + } catch let e { + print("BRKeyedUnarchiver unable to deserialize JSON: \(e)") + return nil + } + } +} + +// converters + +extension Date: BREncodable { + func encode() -> AnyObject { + return timeIntervalSince1970 as AnyObject + } + + public static func zeroValue() -> Date { + return dateFromTimeIntervalSince1970(0) + } + + public static func decode(_ value: AnyObject) -> Date { + let d = (value as? Double) ?? Double() + return dateFromTimeIntervalSince1970(d) + } + + static func dateFromTimeIntervalSince1970(_ d: Double) -> T { + return Date(timeIntervalSince1970: d) as! T + } +} + +extension Int: BREncodable { + func encode() -> AnyObject { + return self as AnyObject + } + + static func zeroValue() -> Int { + return 0 + } + + static func decode(_ s: AnyObject) -> Int { + return (s as? Int) ?? zeroValue() + } +} + +extension Double: BREncodable { + func encode() -> AnyObject { + return self as AnyObject + } + + static func zeroValue() -> Double { + return 0.0 + } + + static func decode(_ s: AnyObject) -> Double { + return (s as? Double) ?? zeroValue() + } +} + +extension String: BREncodable { + func encode() -> AnyObject { + return self as AnyObject + } + + static func zeroValue() -> String { + return "" + } + + static func decode(_ s: AnyObject) -> String { + return (s as? String) ?? zeroValue() + } +} diff --git a/litewallet/Platform/BRReplicatedKVStore.swift b/litewallet/Platform/BRReplicatedKVStore.swift new file mode 100644 index 000000000..bad0eca03 --- /dev/null +++ b/litewallet/Platform/BRReplicatedKVStore.swift @@ -0,0 +1,806 @@ +import BRCore +import Foundation +import SQLite3 + +public enum BRReplicatedKVStoreError: Error { + case sqLiteError + case replicationError + case alreadyReplicating + case conflict + case notFound + case invalidKey + case unknown + case malformedData +} + +public enum BRRemoteKVStoreError: Error { + case notFound + case conflict + case tombstone + case unknown +} + +/// An interface to a remote key value store which utilizes optimistic-locking for concurrency control +public protocol BRRemoteKVStoreAdaptor { + /// Fetch the version of the key from the remote store + /// returns a tuple of (remoteVersion, remoteDate, remoteErr?) + func ver(key: String, completionFunc: @escaping (UInt64, Date, BRRemoteKVStoreError?) -> Void) + + /// Save a new version of the key to the remote server + /// takes the value and current remote version (zero if creating) + /// returns a tuple of (remoteVersion, remoteDate, remoteErr?) + func put(_ key: String, value: [UInt8], version: UInt64, + completionFunc: @escaping (UInt64, Date, BRRemoteKVStoreError?) -> Void) + + /// Marks a key as deleted on the remote server + /// takes the current remote version (which same as put() must match the current servers time) + /// returns a tuple of (remoteVersion, remoteDate, remoteErr?) + func del(_ key: String, version: UInt64, completionFunc: @escaping (UInt64, Date, BRRemoteKVStoreError?) -> Void) + + /// Get a key from the server + /// takes the current remote version (which may optionally be zero to fetch the newest version) + /// returns a tuple of (remoteVersion, remoteDate, remoteBytes, remoteErr?) + func get(_ key: String, version: UInt64, + completionFunc: @escaping (UInt64, Date, [UInt8], BRRemoteKVStoreError?) -> Void) + + /// Get a list of all keys on the remote server + /// returns a list of tuples of (remoteKey, remoteVersion, remoteDate, remoteErr?) + func keys(_ completionFunc: @escaping ([(String, UInt64, Date, BRRemoteKVStoreError?)], BRRemoteKVStoreError?) -> Void) +} + +private func dispatch_sync_throws(_ queue: DispatchQueue, f: () throws -> Void) throws { + var e: Error? + queue.sync { + do { + try f() + } catch let caught { + e = caught + } + } + if let e = e { + throw e + } +} + +/// A key value store which can replicate its data to remote servers that utilizes optimistic locking for local +/// concurrency control +open class BRReplicatedKVStore: NSObject { + fileprivate var db: OpaquePointer? // sqlite3* + fileprivate(set) var key: BRKey + fileprivate(set) var remote: BRRemoteKVStoreAdaptor + fileprivate(set) var syncRunning = false + fileprivate var dbQueue: DispatchQueue + fileprivate let keyRegex = try! NSRegularExpression(pattern: "^[^_][\\w-]{1,255}$", options: []) + + /// Whether or not we immediately sync a key when set() or del() is called + /// by default it is off because only one sync can run at a time, and if you set or del a lot of keys + /// most operations will err out + open var syncImmediately = false + + /// Whether or not the data replicated to the serve is encrypted. Default value should always be yes, + /// this property should only be used for testing with non-sensitive data + open var encryptedReplication = true + + /// Whether the data is encrypted at rest on disk + open var encrypted = true + + static var dbPath: URL { + let fm = FileManager.default + let docsUrl = fm.urls(for: .documentDirectory, in: .userDomainMask).first! + let bundleDirUrl = docsUrl.appendingPathComponent("kvstore.sqlite3") + return bundleDirUrl + } + + init(encryptionKey: BRKey, remoteAdaptor: BRRemoteKVStoreAdaptor) throws { + key = encryptionKey + remote = remoteAdaptor + dbQueue = DispatchQueue(label: "com.litecoin.loafwallet.kvDBQueue", attributes: []) + super.init() + try openDatabase() + try migrateDatabase() + } + + /// Removes the entire database all at once. One must call openDatabase() and migrateDatabase() + /// if one wishes to use this instance again after calling this + open func rmdb() throws { + try dispatch_sync_throws(dbQueue) { + try self.checkErr(sqlite3_close(self.db), s: "rmdb - close") + try FileManager.default.removeItem(at: BRReplicatedKVStore.dbPath) + self.db = nil + } + } + + /// Creates the internal database connection. Called automatically in init() + func openDatabase() throws { + try dispatch_sync_throws(dbQueue) { + if self.db != nil { + print("Database already open") + throw BRReplicatedKVStoreError.sqLiteError + } + try self.checkErr(sqlite3_open_v2( + BRReplicatedKVStore.dbPath.absoluteString, &self.db, + SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil + ), s: "opening db") + self.log("opened DB at \(BRReplicatedKVStore.dbPath.absoluteString)") + } + } + + deinit { + sqlite3_close(db) + } + + /// Creates the local database structure. Called automatically in init() + func migrateDatabase() throws { + try dispatch_sync_throws(dbQueue) { + let commands = [ + "CREATE TABLE IF NOT EXISTS dbversion (ver INT NOT NULL PRIMARY KEY ON CONFLICT REPLACE);", + "INSERT INTO dbversion (ver) VALUES (1);", + "CREATE TABLE IF NOT EXISTS kvstore (" + + " version BIGINT NOT NULL, " + + " remote_version BIGINT NOT NULL DEFAULT 0, " + + " key TEXT NOT NULL, " + + " value BLOB NOT NULL, " + + " thetime BIGINT NOT NULL, " + // server unix timestamp in MS + " deleted BOOL NOT NULL, " + + " PRIMARY KEY (key, version) " + + ");", + ] + for cmd in commands { + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try self.checkErr(sqlite3_prepare_v2(self.db, cmd, -1, &stmt, nil), s: "migrate prepare") + try self.checkErr(sqlite3_step(stmt), s: "migrate stmt exec") + } + } + } + + // get a key from the local database, optionally specifying a version + func get(_ key: String, version: UInt64 = 0) throws -> (UInt64, Date, Bool, [UInt8]) { + try checkKey(key) + var ret = [UInt8]() + var curVer: UInt64 = 0 + var deleted = false + var time = Date(timeIntervalSince1970: Double()) + try txn { + if version == 0 { + (curVer, _) = try self._localVersion(key) + } else { + // check for the existence of such a version + var vStmt: OpaquePointer? + defer { + sqlite3_finalize(vStmt) + } + try self.checkErr(sqlite3_prepare_v2( + self.db, "SELECT version FROM kvstore WHERE key = ? AND version = ? ORDER BY version DESC LIMIT 1", + -1, &vStmt, nil + ), s: "get - get version - prepare") + sqlite3_bind_text(vStmt, 1, NSString(string: key).utf8String, -1, nil) + sqlite3_bind_int64(vStmt, 2, Int64(version)) + try self.checkErr(sqlite3_step(vStmt), s: "get - get version - exec", r: SQLITE_ROW) + curVer = UInt64(sqlite3_column_int64(vStmt, 0)) + } + if curVer == 0 { + throw BRReplicatedKVStoreError.notFound + } + + self.log("GET key: \(key) ver: \(curVer)") + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try self.checkErr(sqlite3_prepare_v2( + self.db, "SELECT value, length(value), thetime, deleted FROM kvstore WHERE key=? AND version=? LIMIT 1", + -1, &stmt, nil + ), s: "get - prepare stmt") + sqlite3_bind_text(stmt, 1, NSString(string: key).utf8String, -1, nil) + sqlite3_bind_int64(stmt, 2, Int64(curVer)) + try self.checkErr(sqlite3_step(stmt), s: "get - step stmt", r: SQLITE_ROW) + let blob = sqlite3_column_blob(stmt, 0) + let blobLength = sqlite3_column_int(stmt, 1) + time = Date.withMsTimestamp(UInt64(sqlite3_column_int64(stmt, 2))) + deleted = sqlite3_column_int(stmt, 3) > 0 + ret = Array(UnsafeBufferPointer(start: blob?.assumingMemoryBound(to: UInt8.self), count: Int(blobLength))) + } + return (curVer, time, deleted, encrypted ? try decrypt(ret) : ret) + } + + /// Set the value of a key locally in the database. If syncImmediately is true (the default) then immediately + /// after successfully saving locally, replicate to server. The `localVer` key must be the same as is currently + /// stored in the database. To create a new key, pass `0` as `localVer` + func set(_ key: String, value: [UInt8], localVer: UInt64) throws -> (UInt64, Date) { + try checkKey(key) + let (newVer, time) = try _set(key, value: value, localVer: localVer) + if syncImmediately { + try syncKey(key) { _ in + self.log("SET key synced: \(key)") + } + } + return (newVer, time) + } + + fileprivate func _set(_ key: String, value: [UInt8], localVer: UInt64) throws -> (UInt64, Date) { + var newVer: UInt64 = 0 + let time = Date() + try txn { + let (curVer, _) = try self._localVersion(key) + if curVer != localVer { + self.log("set key \(key) conflict: version \(localVer) != current version \(curVer)") + throw BRReplicatedKVStoreError.conflict + } + self.log("SET key: \(key) ver: \(curVer)") + newVer = curVer + 1 + let encryptedData = self.encrypted ? try self.encrypt(value) : value + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try self.checkErr(sqlite3_prepare_v2(self.db, + "INSERT INTO kvstore (version, key, value, thetime, deleted) " + + "VALUES (?, ?, ?, ?, ?)", -1, &stmt, nil), s: "set - prepare stmt") + sqlite3_bind_int64(stmt, 1, Int64(newVer)) + sqlite3_bind_text(stmt, 2, NSString(string: key).utf8String, -1, nil) + sqlite3_bind_blob(stmt, 3, encryptedData, Int32(encryptedData.count), nil) + sqlite3_bind_int64(stmt, 4, Int64(time.msTimestamp())) + sqlite3_bind_int(stmt, 5, 0) + try self.checkErr(sqlite3_step(stmt), s: "set - step stmt") + } + return (newVer, time) + } + + /// Mark a key as removed locally. If syncImmediately is true (the defualt) then immediately mark the key + /// as removed on the server as well. `localVer` must match the most recent version in the local database. + func del(_ key: String, localVer: UInt64) throws -> (UInt64, Date) { + try checkKey(key) + let (newVer, time) = try _del(key, localVer: localVer) + if syncImmediately { + try syncKey(key) { _ in + self.log("DEL key synced: \(key)") + } + } + return (newVer, time) + } + + func _del(_ key: String, localVer: UInt64) throws -> (UInt64, Date) { + if localVer == 0 { + throw BRReplicatedKVStoreError.notFound + } + var newVer: UInt64 = 0 + let time = Date() + try txn { + let (curVer, _) = try self._localVersion(key) + if curVer != localVer { + self.log("del key \(key) conflict: version \(localVer) != current version \(curVer)") + throw BRReplicatedKVStoreError.conflict + } + self.log("DEL key: \(key) ver: \(curVer)") + newVer = curVer + 1 + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try self.checkErr(sqlite3_prepare_v2( + self.db, "INSERT INTO kvstore (version, key, value, thetime, deleted) " + + "SELECT ?, key, value, ?, ? " + + "FROM kvstore WHERE key=? AND version=?", + -1, &stmt, nil + ), s: "del - prepare stmt") + sqlite3_bind_int64(stmt, 1, Int64(newVer)) + sqlite3_bind_int64(stmt, 2, Int64(time.msTimestamp())) + sqlite3_bind_int(stmt, 3, 1) + sqlite3_bind_text(stmt, 4, NSString(string: key).utf8String, -1, nil) + sqlite3_bind_int64(stmt, 5, Int64(curVer)) + try self.checkErr(sqlite3_step(stmt), s: "del - exec stmt") + } + return (newVer, time) + } + + /// Gets the local version of the provided key, or 0 if it doesn't exist + func localVersion(_ key: String) throws -> (UInt64, Date) { + try checkKey(key) + var retVer: UInt64 = 0 + var retTime = Date(timeIntervalSince1970: Double()) + try txn { + (retVer, retTime) = try self._localVersion(key) + } + return (retVer, retTime) + } + + func _localVersion(_ key: String) throws -> (UInt64, Date) { + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try checkErr(sqlite3_prepare_v2( + db, "SELECT version, thetime FROM kvstore WHERE key = ? ORDER BY version DESC LIMIT 1", -1, + &stmt, nil + ), s: "get version - prepare") + sqlite3_bind_text(stmt, 1, NSString(string: key).utf8String, -1, nil) + try checkErr(sqlite3_step(stmt), s: "get version - exec", r: SQLITE_ROW) + return ( + UInt64(sqlite3_column_int64(stmt, 0)), + Date.withMsTimestamp(UInt64(sqlite3_column_int64(stmt, 1))) + ) + } + + /// Get the remote version for the key for the most recent local version of the key, if stored. + // If local key doesn't exist, return 0 + // func remoteVersion(key: String) throws -> UInt64 { + + //// return 0 + // } + func remoteVersion(_ key: String) throws -> Int + { // this would be UInt64.. but it makes the compiler crash + try checkKey(key) + var ret: UInt64 = 0 + try txn { + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try self.checkErr(sqlite3_prepare_v2( + self.db, "SELECT remote_version FROM kvstore WHERE key = ? ORDER BY version DESC LIMIT 1", -1, &stmt, nil + ), s: "get remote version - prepare") + sqlite3_bind_text(stmt, 1, NSString(string: key).utf8String, -1, nil) + try self.checkErr(sqlite3_step(stmt), s: "get remote version - exec", r: SQLITE_ROW) + ret = UInt64(sqlite3_column_int64(stmt, 0)) + } + return Int(ret) + } + + /// Record the remote version for the object in a new version of the local key + func setRemoteVersion(key: String, localVer: UInt64, remoteVer: UInt64) throws -> (UInt64, Date) { + try checkKey(key) + if localVer < 1 { + throw BRReplicatedKVStoreError.conflict // setRemoteVersion can't be used for creates + } + var newVer: UInt64 = 0 + let time = Date() + try txn { + let (curVer, _) = try self._localVersion(key) + if curVer != localVer { + self.log("set remote version key \(key) conflict: version \(localVer) != current version \(curVer)") + throw BRReplicatedKVStoreError.conflict + } + self.log("SET REMOTE VERSION: \(key) ver: \(localVer) remoteVer=\(remoteVer)") + newVer = curVer + 1 + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try self.checkErr(sqlite3_prepare_v2( + self.db, "INSERT INTO kvstore (version, key, value, thetime, deleted, remote_version) " + + "SELECT ?, key, value, ?, deleted, ? " + + "FROM kvstore WHERE key=? AND version=?", + -1, &stmt, nil + ), s: "update remote version - prepare stmt") + sqlite3_bind_int64(stmt, 1, Int64(newVer)) + sqlite3_bind_int64(stmt, 2, Int64(time.msTimestamp())) + sqlite3_bind_int64(stmt, 3, Int64(remoteVer)) + sqlite3_bind_text(stmt, 4, NSString(string: key).utf8String, -1, nil) + sqlite3_bind_int64(stmt, 5, Int64(curVer)) + try self.checkErr(sqlite3_step(stmt), s: "update remote - exec stmt") + } + return (newVer, time) + } + + /// Get a list of (key, localVer, localTime, remoteVer, deleted) + func localKeys() throws -> [(String, UInt64, Date, UInt64, Bool)] { + var ret = [(String, UInt64, Date, UInt64, Bool)]() + try txn { + var stmt: OpaquePointer? + defer { + sqlite3_finalize(stmt) + } + try self.checkErr(sqlite3_prepare_v2(self.db, + "SELECT kvs.key, kvs.version, kvs.thetime, kvs.remote_version, kvs.deleted " + + "FROM kvstore kvs " + + "INNER JOIN ( " + + " SELECT MAX(version) AS latest_version, key " + + " FROM kvstore " + + " GROUP BY key " + + ") vermax " + + "ON kvs.version = vermax.latest_version " + + "AND kvs.key = vermax.key", -1, &stmt, nil), + s: "local keys - prepare stmt") + while sqlite3_step(stmt) == SQLITE_ROW { + let key = sqlite3_column_text(stmt, 0) + let ver = sqlite3_column_int64(stmt, 1) + let date = sqlite3_column_int64(stmt, 2) + let rver = sqlite3_column_int64(stmt, 3) + let del = sqlite3_column_int(stmt, 4) + if let key = key { + ret.append(( + String(cString: key), + UInt64(ver), + Date.withMsTimestamp(UInt64(date)), + UInt64(rver), + del > 0 + )) + } + } + } + return ret + } + + /// Sync all keys to and from the remote kv store adaptor + func syncAllKeys(_ completionHandler: @escaping (BRReplicatedKVStoreError?) -> Void) { + // update all keys locally and on the remote server, replacing missing keys + // + // 1. get a list of all keys from the server + // 2. for keys that we don't have, add em + // 3. for keys that we do have, sync em + // 4. for keys that they don't have that we do, upload em + if syncRunning { + DispatchQueue.main.async { + completionHandler(.alreadyReplicating) + } + return + } + syncRunning = true + let startTime = Date() + remote.keys { keyData, err in + if let err = err { + self.log("Error fetching remote key data: \(err)") + self.syncRunning = false + return completionHandler(.replicationError) + } + var localKeyData: [(String, UInt64, Date, UInt64, Bool)] + do { + localKeyData = try self.localKeys() + } catch let e { + self.syncRunning = false + self.log("Error getting local key data: \(e)") + return completionHandler(.replicationError) + } + let allRemoteKeys = Set(keyData.map { e in e.0 }) + var allKeyData = keyData + for k in localKeyData { + if !allRemoteKeys.contains(k.0) { + // server is missing a key that we have + allKeyData.append((k.0, 0, Date(timeIntervalSince1970: Double()), nil)) + } + } + + self.log("Syncing \(allKeyData.count) keys") + var failures = 0 + let q = DispatchQueue(label: "com.litecoin.loafwallet.kvSyncQueue", attributes: DispatchQueue.Attributes.concurrent) + let grp = DispatchGroup() + let seph = DispatchSemaphore(value: 10) + + grp.enter() + q.async { + q.async { + for k in allKeyData { + grp.enter() + _ = seph.wait(timeout: DispatchTime.distantFuture) + q.async(group: grp) { + do { + try self._syncKey(k.0, remoteVer: k.1, remoteTime: k.2, remoteErr: k.3, + completionHandler: { err in + if err != nil { + failures += 1 + } + seph.signal() + grp.leave() + }) + } catch { + failures += 1 + seph.signal() + grp.leave() + } + } + } + grp.leave() + } + _ = grp.wait(timeout: DispatchTime.distantFuture) + DispatchQueue.main.async { + self.syncRunning = false + self.log("Finished syncing in \(Date().timeIntervalSince(startTime))") + completionHandler(failures > 0 ? .replicationError : nil) + } + } + } + } + + /// Sync an individual key. Normally this is only called internally and you should call syncAllKeys + func syncKey(_ key: String, remoteVersion: UInt64? = nil, remoteTime: Date? = nil, + remoteErr: BRRemoteKVStoreError? = nil, completionHandler: @escaping (BRReplicatedKVStoreError?) -> Void) throws + { + try checkKey(key) + if syncRunning { + throw BRReplicatedKVStoreError.alreadyReplicating + } + syncRunning = true + let myCompletionHandler: (_ e: BRReplicatedKVStoreError?) -> Void = { e in + completionHandler(e) + self.syncRunning = false + } + if let remoteVersion = remoteVersion, let remoteTime = remoteTime { + try _syncKey(key, remoteVer: remoteVersion, remoteTime: remoteTime, + remoteErr: remoteErr, completionHandler: myCompletionHandler) + } else { + remote.ver(key: key) { remoteVer, remoteTime, err in + _ = try? self._syncKey(key, remoteVer: remoteVer, remoteTime: remoteTime, + remoteErr: err, completionHandler: myCompletionHandler) + } + } + } + + // the syncKey kernel - this is provided so syncAllKeys can provide get a bunch of key versions at once + // and fan out the _syncKey operations + fileprivate func _syncKey(_ key: String, remoteVer: UInt64, remoteTime: Date, remoteErr: BRRemoteKVStoreError?, + completionHandler: @escaping (BRReplicatedKVStoreError?) -> Void) throws + { + // this is a basic last-write-wins strategy. data loss is possible but in general + // we will attempt to sync before making any local modifications to the data + // and concurrency will be so low that we don't really need a fancier solution than this. + // the strategy is: + // + // 1. get the remote version. this is our "lock" + // 2. along with the remote version will come the last-modified date of the remote object + // 3. if their last-modified date is newer than ours, overwrite ours + // 4. if their last-modified date is older than ours, overwrite theirs + + if !syncRunning { + throw BRReplicatedKVStoreError.unknown // how did we get here + } + + // one optimization is we keep the remote version on the most recent local version, if they match, + // there is nothing to do + let recordedRemoteVersion = try UInt64(remoteVersion(key)) + if remoteErr != .some(.notFound), remoteVer > 0, recordedRemoteVersion == remoteVer { + log("Remote version of key \(key) is the same as the one we have") + return completionHandler(nil) // this key is already up to date + } + + var localVer: UInt64 + var localTime: Date + var localDeleted: Bool + var localValue: [UInt8] + do { + (localVer, localTime, localDeleted, localValue) = try get(key) + localValue = encryptedReplication ? try encrypt(localValue) : localValue + } catch BRReplicatedKVStoreError.notFound { + // missing key locally + (localVer, localTime, localDeleted, localValue) = (0, Date(timeIntervalSince1970: Double()), false, []) + } + let (lt, rt) = (localTime.msTimestamp(), remoteTime.msTimestamp()) + + switch remoteErr { + case nil, .some(.tombstone), .some(.notFound): + if localDeleted && remoteErr == .some(.tombstone) { // was removed on both server and locally + log("Local key \(key) was deleted, and so was the remote key") + do { + _ = try setRemoteVersion(key: key, localVer: localVer, remoteVer: remoteVer) + } catch let e where e is BRReplicatedKVStoreError { + return completionHandler(e as! BRReplicatedKVStoreError) + } catch { + return completionHandler(.replicationError) + } + return completionHandler(nil) + } + + if lt > rt || lt == rt { // local is newer (or a tiebreaker) + if localDeleted { + log("Local key \(key) was deleted, removing remotely...") + remote.del(key, version: remoteVer, completionFunc: { newRemoteVer, _, delErr in + if delErr == .some(.notFound) { + self.log("Local key \(key) was already missing on the server. Ignoring") + return completionHandler(nil) + } + if let delErr = delErr { + self.log("Error deleting remote version for key \(key), error: \(delErr)") + return completionHandler(.replicationError) + } + do { + _ = try self.setRemoteVersion(key: key, localVer: localVer, remoteVer: newRemoteVer) + } catch let e where e is BRReplicatedKVStoreError { + return completionHandler(e as! BRReplicatedKVStoreError) + } catch { + return completionHandler(.replicationError) + } + self.log("Local key \(key) removed on server") + completionHandler(nil) + }) + } else { + log("Local key \(key) is newer remoteVer=\(remoteVer), updating remotely...") + // if the remote version is zero it means it doesnt yet exist on the server. set the remote version + // to "1" to create the key on the server + let useRemoteVer = remoteVer == 0 || remoteVer < recordedRemoteVersion ? 1 : remoteVer + remote.put(key, value: localValue, version: useRemoteVer, + completionFunc: { newRemoteVer, _, putErr in + if let putErr = putErr { + self.log("Error updating remote version for key \(key), newRemoteVer=\(newRemoteVer) error: \(putErr)") + return completionHandler(.replicationError) + } + do { + _ = try self.setRemoteVersion(key: key, localVer: localVer, remoteVer: newRemoteVer) + } catch let e where e is BRReplicatedKVStoreError { + return completionHandler(e as! BRReplicatedKVStoreError) + } catch { + return completionHandler(.replicationError) + } + self.log("Local key \(key) updated on server") + completionHandler(nil) + }) + } + } else { + // local is out-of-date + if remoteErr == .some(.tombstone) { + // remote is deleted + log("Remote key \(key) deleted, removing locally") + do { + let (newLocalVer, _) = try _del(key, localVer: localVer) + _ = try setRemoteVersion(key: key, localVer: newLocalVer, remoteVer: remoteVer) + } catch BRReplicatedKVStoreError.notFound { + // well a deleted key isn't found, so why do we care + } catch let e where e is BRReplicatedKVStoreError { + return completionHandler(e as! BRReplicatedKVStoreError) + } catch { + return completionHandler(.replicationError) + } + log("Remote key \(key) was removed locally") + completionHandler(nil) + } else { + log("Remote key \(key) is newer, fetching...") + // get the remote version + remote.get(key, version: remoteVer, completionFunc: { newRemoteVer, _, remoteData, getErr in + if let getErr = getErr { + self.log("Error fetching the remote value for key \(getErr), error: \(getErr)") + return completionHandler(.replicationError) + } + do { + let decryptedValue = self.encryptedReplication ? try self.decrypt(remoteData) : remoteData + let (newLocalVer, _) = try self._set(key, value: decryptedValue, localVer: localVer) + _ = try self.setRemoteVersion(key: key, localVer: newLocalVer, remoteVer: newRemoteVer) + } catch BRReplicatedKVStoreError.malformedData { + _ = try? self.del(key, localVer: localVer) + return completionHandler(BRReplicatedKVStoreError.malformedData) + } catch let e where e is BRReplicatedKVStoreError { + return completionHandler(e as! BRReplicatedKVStoreError) + } catch { + return completionHandler(.replicationError) + } + self.log("Updated local key \(key)") + completionHandler(nil) + }) + } + } + default: + log("Error fetching remote version for key \(key), error: \(String(describing: remoteErr))") + completionHandler(.replicationError) + } + } + + // execute a function inside a transaction, if that function throws then rollback, otherwise commit + // calling txn() from within a txn function will deadlock + fileprivate func txn(_ fn: () throws -> Void) throws { + try dispatch_sync_throws(dbQueue) { + var beginStmt: OpaquePointer? + var finishStmt: OpaquePointer? + defer { + sqlite3_finalize(beginStmt) + sqlite3_finalize(finishStmt) + } + try self.checkErr(sqlite3_prepare_v2(self.db, "BEGIN", -1, &beginStmt, nil), s: "txn - prepare begin") + try self.checkErr(sqlite3_step(beginStmt), s: "txn - exec begin begin") + do { + try fn() + } catch let e { + try self.checkErr(sqlite3_prepare_v2( + self.db, "ROLLBACK", -1, &finishStmt, nil + ), s: "txn - prepare rollback") + try self.checkErr(sqlite3_step(finishStmt), s: "txn - execute rollback") + throw e + } + try self.checkErr(sqlite3_prepare_v2(self.db, "COMMIT", -1, &finishStmt, nil), s: "txn - prepare commit") + try self.checkErr(sqlite3_step(finishStmt), s: "txn - execute commit") + } + } + + // ensure the sqlite3 error code is an acceptable one (or that its the one you provide as `r` + // this MUST be called from within the dbQueue + fileprivate func checkErr(_ e: Int32, s: String, r: Int32 = SQLITE_NULL) throws { + if r == SQLITE_NULL, e != SQLITE_OK, e != SQLITE_DONE, e != SQLITE_ROW, + e != SQLITE_NULL, e != r + { + let es = NSString(cString: sqlite3_errstr(e), encoding: String.Encoding.utf8.rawValue) + let em = NSString(cString: sqlite3_errmsg(db), encoding: String.Encoding.utf8.rawValue) + log("\(s): errcode=\(e) errstr=\(String(describing: es)) errmsg=\(String(describing: em))") + throw BRReplicatedKVStoreError.sqLiteError + } + } + + // validates the key. keys can not start with a _ + fileprivate func checkKey(_ key: String) throws { + let m = keyRegex.matches(in: key, options: [], range: NSMakeRange(0, key.count)) + if m.count != 1 { + throw BRReplicatedKVStoreError.invalidKey + } + } + + // encrypt some data using self.key + fileprivate func encrypt(_ data: [UInt8]) throws -> [UInt8] { + return [UInt8](Data(data).chacha20Poly1305AEADEncrypt(key: key)) + } + + // decrypt some data using self.key + fileprivate func decrypt(_ data: [UInt8]) throws -> [UInt8] { + return try [UInt8](Data(data).chacha20Poly1305AEADDecrypt(key: key)) + } + + // generate a nonce using microseconds-since-epoch + fileprivate func genNonce() -> [UInt8] { + var tv = timeval() + gettimeofday(&tv, nil) + var t = UInt64(tv.tv_usec) * 1_000_000 + UInt64(tv.tv_usec) + let p = [UInt8](repeating: 0, count: 4) + return Data(bytes: &t, count: MemoryLayout.size).withUnsafeBytes + { (dat: UnsafePointer) -> [UInt8] in + let buf = UnsafeBufferPointer(start: dat, count: MemoryLayout.size) + return p + Array(buf) + } + } + + fileprivate func log(_ s: String) { + print("[KVStore] \(s)") + } +} + +// MARK: - Objective-C compatability layer + +@objc open class BRKVStoreObject: NSObject { + open var version: UInt64 + open var lastModified: Date + open var deleted: Bool + open var key: String + + fileprivate var _data: Data? + + var data: Data { + get { + return getData() ?? _data ?? Data() // allow subclasses to override the data that is retrieved + } + set(v) { + _data = v + dataWasSet(v) + } + } + + init(key: String, version: UInt64, lastModified: Date, deleted: Bool, data: Data) { + self.version = version + self.key = key + self.lastModified = lastModified + self.deleted = deleted + super.init() + self.data = data + } + + func getData() -> Data? { return nil } + + func dataWasSet(_: Data) {} +} + +public extension BRReplicatedKVStore { + func get(_ key: String) throws -> BRKVStoreObject { + let (v, d, r, b) = try get(key) + return BRKVStoreObject(key: key, version: v, lastModified: d, deleted: r, + data: Data(bytes: UnsafePointer(b), count: b.count)) + } + + func set(_ object: BRKVStoreObject) throws -> BRKVStoreObject { + let dat = object.data + var bytes = [UInt8](repeating: 0, count: dat.count) + (dat as NSData).getBytes(&bytes, length: dat.count) + (object.version, object.lastModified) = try set(object.key, value: bytes, localVer: object.version) + return object + } + + func del(_ object: BRKVStoreObject) throws -> BRKVStoreObject { + (object.version, object.lastModified) = try del(object.key, localVer: object.version) + object.deleted = true + return object + } +} diff --git a/litewallet/Platform/BRSocketHelpers.c b/litewallet/Platform/BRSocketHelpers.c new file mode 100644 index 000000000..6f45926ec --- /dev/null +++ b/litewallet/Platform/BRSocketHelpers.c @@ -0,0 +1,105 @@ +// +// BRSocketHelpers.c +// BreadWallet +// +// Created by Samuel Sutch on 2/17/16. +// Copyright (c) 2016 breadwallet LLC +// +// Permission 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE 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. + +#include "BRSocketHelpers.h" +#include +#include +#include +#include +#include + +int bw_nbioify(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) return flags; + flags = flags &~ O_NONBLOCK; + return fcntl(fd, F_SETFL, flags); +} + +struct bw_select_result bw_select(struct bw_select_request request) { + fd_set read_fds, write_fds, err_fds; + FD_ZERO(&read_fds); + FD_ZERO(&write_fds); + FD_ZERO(&err_fds); + int max_fd = 0; + // copy requested file descriptors from request to fd_sets + for (int i = 0; i < request.read_fd_len; i++) { + int fd = request.read_fds[i]; + if (fd > max_fd) max_fd = fd; + // printf("bw_select: read fd=%i open=%i\n", fd, fcntl(fd, F_GETFD)); + FD_SET(fd, &read_fds); + } + for (int i = 0; i < request.write_fd_len; i++) { + int fd = request.write_fds[i]; + if (fd > max_fd) max_fd = fd; + // printf("bw_select: write fd=%i open=%i\n", fd, fcntl(fd, F_GETFD)); + FD_SET(fd, &write_fds); + } + + struct bw_select_result result = { 0, 0, 0, 0, NULL, NULL, NULL }; + // printf("bw_select max_fd=%i\n", max_fd); + + // initiate a select + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 10000; // 10ms + int activity = select(max_fd + 1, &read_fds, &write_fds, &err_fds, &tv); + if (activity < 0 && errno != EINTR) { + result.error = errno; + perror("select"); + return result; + } + // indicate to the caller which file descriptors are ready for reading + for (int i = 0; i < request.read_fd_len; i++) { + int fd = request.read_fds[i]; + // printf("bw_select: i=%i read_ready_fd=%i\n", i, fd); + if (FD_ISSET(fd, &read_fds)) { + result.read_fd_len += 1; + result.read_fds = (int *)realloc(result.read_fds, result.read_fd_len * sizeof(int)); + result.read_fds[result.read_fd_len - 1] = fd; + } + // ... which ones are erroring + if (FD_ISSET(fd, &err_fds)) { + result.error_fd_len += 1; + result.error_fds = (int *)realloc(result.error_fds, result.error_fd_len * sizeof(int)); + result.error_fds[result.error_fd_len - 1] = fd; + } + } + // ... and which ones are ready for writing + for (int i = 0; i < request.write_fd_len; i++) { + int fd = request.write_fds[i]; + // printf("bw_select: write_ready_fd=%i\n", fd); + if (FD_ISSET(fd, &write_fds)) { + result.write_fd_len += 1; + result.write_fds = (int *)realloc(result.write_fds, result.write_fd_len * sizeof(int)); + result.write_fds[result.write_fd_len - 1] = fd; + } + if (FD_ISSET(fd, &err_fds)) { + result.error_fd_len += 1; + result.error_fds = (int *)realloc(result.error_fds, result.error_fd_len * sizeof(int)); + result.error_fds[result.error_fd_len - 1] = fd; + } + } + return result; +} diff --git a/litewallet/Platform/BRSocketHelpers.h b/litewallet/Platform/BRSocketHelpers.h new file mode 100644 index 000000000..862df234a --- /dev/null +++ b/litewallet/Platform/BRSocketHelpers.h @@ -0,0 +1,52 @@ +// +// BRSocketHelpers.h +// BreadWallet +// +// Created by Samuel Sutch on 2/17/16. +// Copyright (c) 2016 breadwallet LLC +// +// Permission 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE 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. + +#ifndef BRSocketHelpers_h +#define BRSocketHelpers_h + +#include + +int bw_nbioify(int fd); + +struct bw_select_request { + int write_fd_len; + int read_fd_len; + int *write_fds; + int *read_fds; +}; + +struct bw_select_result { + int error; // if > 0 there is an error + int write_fd_len; + int read_fd_len; + int error_fd_len; + int *write_fds; + int *read_fds; + int *error_fds; +}; + +struct bw_select_result bw_select(struct bw_select_request); + +#endif /* BRSocketHelpers_h */ diff --git a/litewallet/Platform/BRTar.swift b/litewallet/Platform/BRTar.swift new file mode 100644 index 000000000..027a6be5f --- /dev/null +++ b/litewallet/Platform/BRTar.swift @@ -0,0 +1,178 @@ +import Foundation + +enum BRTarError: Error { + case unknown + case fileDoesntExist +} + +enum BRTarType { + case file + case directory + case nullBlock + case headerBlock + case unsupported + case invalid + + init(fromData: Data) { + if fromData.count < 1 { + BRTar.log("invalid data") + self = .invalid + return + } + let byte = (fromData as NSData).bytes.bindMemory(to: CChar.self, capacity: fromData.count)[0] + switch byte { + case CChar(48): // "0" + self = .file + case CChar(53): // "5" + self = .directory + case CChar(0): + self = .nullBlock + case CChar(120): // "x" + self = .headerBlock + case CChar(49), CChar(50), CChar(51), CChar(52), CChar(53), CChar(54), CChar(55), CChar(103): + // "1, 2, 3, 4, 5, 6, 7, g" + self = .unsupported + default: + BRTar.log("invalid block type: \(byte)") + self = .invalid + } + } +} + +class BRTar { + static let tarBlockSize: UInt64 = 512 + static let tarTypePosition: UInt64 = 156 + static let tarNamePosition: UInt64 = 0 + static let tarNameSize: UInt64 = 100 + static let tarSizePosition: UInt64 = 124 + static let tarSizeSize: UInt64 = 12 + static let tarMaxBlockLoadInMemory: UInt64 = 100 + static let tarLogEnabled: Bool = false + + static func createFilesAndDirectoriesAtPath(_ path: String, withTarPath tarPath: String) throws { + let fm = FileManager.default + if !fm.fileExists(atPath: tarPath) { + log("tar file \(tarPath) does not exist") + throw BRTarError.fileDoesntExist + } + let attrs = try fm.attributesOfItem(atPath: tarPath) + guard let tarFh = FileHandle(forReadingAtPath: tarPath) + else { + log("could not open tar file for reading") + throw BRTarError.unknown + } + var loc: UInt64 = 0 + guard let sizee = attrs[FileAttributeKey.size] as? Int + else { + log("could not read tar file size") + throw BRTarError.unknown + } + let size = UInt64(sizee) + + while loc < size { + var blockCount: UInt64 = 1 + let tarType = readTypeAtLocation(loc, fromHandle: tarFh) + switch tarType { + case .file: + // read name + let name = try readNameAtLocation(loc, fromHandle: tarFh) + log("got file name from tar \(name)") + let newFilePath = (path as NSString).appendingPathComponent(name) + log("will write to \(newFilePath)") + var size = readSizeAtLocation(loc, fromHandle: tarFh) + log("its size is \(size)") + + if fm.fileExists(atPath: newFilePath) { + try fm.removeItem(atPath: newFilePath) + } + if size == 0 { + // empty file + try "".write(toFile: newFilePath, atomically: true, encoding: String.Encoding.utf8) + break + } + blockCount += (size - 1) / tarBlockSize + 1 + // write file + fm.createFile(atPath: newFilePath, contents: nil, attributes: nil) + guard let destFh = FileHandle(forWritingAtPath: newFilePath) + else { + log("unable to open destination file for writing") + throw BRTarError.unknown + } + tarFh.seek(toFileOffset: loc + tarBlockSize) + let maxSize = tarMaxBlockLoadInMemory * tarBlockSize + while size > maxSize { + autoreleasepool { () in + destFh.write(tarFh.readData(ofLength: Int(maxSize))) + size -= maxSize + } + } + destFh.write(tarFh.readData(ofLength: Int(size))) + destFh.closeFile() + log("success writing file") + case .directory: + let name = try readNameAtLocation(loc, fromHandle: tarFh) + log("got new directory name \(name)") + let dirPath = (path as NSString).appendingPathComponent(name) + log("will create directory at \(dirPath)") + + if fm.fileExists(atPath: dirPath) { + try fm.removeItem(atPath: dirPath) // will automatically recursively remove directories if exists + } + + try fm.createDirectory(atPath: dirPath, withIntermediateDirectories: true, attributes: nil) + log("success creating directory") + case .nullBlock: + break + case .headerBlock: + blockCount += 1 + case .unsupported: + let size = readSizeAtLocation(loc, fromHandle: tarFh) + blockCount += size / tarBlockSize + case .invalid: + log("Invalid block encountered") + throw BRTarError.unknown + } + loc += blockCount * tarBlockSize + log("new location \(loc)") + } + } + + fileprivate static func readTypeAtLocation(_ location: UInt64, fromHandle handle: FileHandle) -> BRTarType + { + log("reading type at location \(location)") + handle.seek(toFileOffset: location + tarTypePosition) + let typeDat = handle.readData(ofLength: 1) + let ret = BRTarType(fromData: typeDat) + log("type: \(ret)") + return ret + } + + fileprivate static func readNameAtLocation(_ location: UInt64, fromHandle handle: FileHandle) throws -> String + { + handle.seek(toFileOffset: location + tarNamePosition) + let dat = handle.readData(ofLength: Int(tarNameSize)) + guard let ret = String(bytes: dat, encoding: String.Encoding.ascii) + else { + log("unable to read name") + throw BRTarError.unknown + } + return ret + } + + fileprivate static func readSizeAtLocation(_ location: UInt64, fromHandle handle: FileHandle) -> UInt64 + { + handle.seek(toFileOffset: location + tarSizePosition) + let sizeDat = handle.readData(ofLength: Int(tarSizeSize)) + let octal = NSString(data: sizeDat, encoding: String.Encoding.ascii.rawValue)! + log("size octal: \(octal)") + let dec = strtoll(octal.utf8String, nil, 8) + log("size decimal: \(dec)") + return UInt64(dec) + } + + fileprivate static func log(_ string: String) { + if tarLogEnabled { + print("[BRTar] \(string)") + } + } +} diff --git a/litewallet/Platform/BRWebViewController.swift b/litewallet/Platform/BRWebViewController.swift new file mode 100644 index 000000000..f92dd38ed --- /dev/null +++ b/litewallet/Platform/BRWebViewController.swift @@ -0,0 +1,213 @@ +import Foundation +import UIKit +import WebKit + +@available(iOS 8.0, *) +@objc open class BRWebViewController: UIViewController, WKNavigationDelegate, BRWebSocketClient, WKScriptMessageHandler +{ + var wkProcessPool: WKProcessPool + var webView: WKWebView? + var server = BRHTTPServer() + var mountPoint: String + var walletManager: WalletManager + let store: Store + let noAuthApiClient: BRAPIClient? + let partner: String? + let activityIndicator: UIActivityIndicatorView + var didLoad = false + var didAppear = false // we are also a socket server which sends didview/didload events to the listening client(s) + var sockets = [String: BRWebSocket]() + + // this is the data that occasionally gets sent to the above connected sockets + var webViewInfo: [String: Any] { + return [ + "visible": didAppear, + "loaded": didLoad, + ] + } + + var indexUrl: URL { + return URL(string: "http://127.0.0.1:\(server.port)\(mountPoint)")! + } + + init(partner: String?, mountPoint: String = "/", walletManager: WalletManager, store: Store, noAuthApiClient: BRAPIClient? = nil) + { + wkProcessPool = WKProcessPool() + self.mountPoint = mountPoint + self.walletManager = walletManager + self.store = store + self.noAuthApiClient = noAuthApiClient + self.partner = partner ?? "" + activityIndicator = UIActivityIndicatorView() + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override open func loadView() { + didLoad = false + + let contentController = WKUserContentController() + contentController.add(self, name: "callback") + + let config = WKWebViewConfiguration() + config.processPool = wkProcessPool + config.allowsInlineMediaPlayback = false + config.allowsAirPlayForMediaPlayback = false + config.allowsPictureInPictureMediaPlayback = false + config.userContentController = contentController + + let request = URLRequest(url: indexUrl) + + view = UIView(frame: CGRect.zero) + view.backgroundColor = UIColor(red: 0.98, green: 0.98, blue: 0.98, alpha: 1.0) + + webView = WKWebView(frame: CGRect.zero, configuration: config) + webView?.navigationDelegate = self + webView?.backgroundColor = UIColor(red: 0.98, green: 0.98, blue: 0.98, alpha: 1.0) + _ = webView?.load(request) + webView?.autoresizingMask = [UIViewAutoresizing.flexibleHeight, UIViewAutoresizing.flexibleWidth] + webView?.scrollView.contentInsetAdjustmentBehavior = .never + + view.addSubview(webView!) + + let center = NotificationCenter.default + center.addObserver(forName: .UIApplicationDidBecomeActive, object: nil, queue: .main) + { [weak self] _ in + self?.didAppear = true + if let info = self?.webViewInfo { + self?.sendToAllSockets(data: info) + } + } + center.addObserver(forName: .UIApplicationWillResignActive, object: nil, queue: .main) + { [weak self] _ in + self?.didAppear = false + if let info = self?.webViewInfo { + self?.sendToAllSockets(data: info) + } + } + + activityIndicator.activityIndicatorViewStyle = .white + activityIndicator.color = .darkGray + activityIndicator.startAnimating() + activityIndicator.isHidden = false + activityIndicator.bounds = CGRect(x: 0, y: 0, width: 15, height: 15) + activityIndicator.autoresizingMask = [UIViewAutoresizing.flexibleHeight, UIViewAutoresizing.flexibleWidth] + view.addSubview(activityIndicator) + } + + override open func viewDidAppear(_: Bool) { + didAppear = true + sendToAllSockets(data: webViewInfo) + } + + override open func viewDidDisappear(_: Bool) { + didAppear = false + sendToAllSockets(data: webViewInfo) + } + + // signal to the presenter that the webview content successfully loaded + fileprivate func webviewDidLoad() { + didLoad = true + sendToAllSockets(data: webViewInfo) + } + + fileprivate func closeNow() { + store.trigger(name: .showStatusBar) + dismiss(animated: true, completion: nil) + } + + open func preload() { + _ = view // force webview loading + } + + open func refresh() { + let request = URLRequest(url: indexUrl) + _ = webView?.load(request) + } + + // MARK: - navigation delegate + + open func webView(_: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) + { + if let url = navigationAction.request.url?.absoluteString { + let mutableurl = url + if mutableurl.contains("/close") { + DispatchQueue.main.async { + self.closeNow() + } + } + } + return decisionHandler(.allow) + } + + public func webView(_: WKWebView, didFinish _: WKNavigation!) { + activityIndicator.stopAnimating() + activityIndicator.isHidden = true + } + + public func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) + { + guard let response = message.body as? String else { return } + + let URLString = URL(string: "https://checkout.simplexcc.com/payments/new") + + var req = URLRequest(url: URLString!) + req.httpBody = Data(response.utf8) + req.httpMethod = "POST" + + DispatchQueue.main.async { + let browser = BRBrowserViewController() + browser.load(req) + self.present(browser, animated: true, completion: nil) + } + } + + // MARK: - socket delegate + + func sendTo(socket: BRWebSocket, data: [String: Any]) { + do { + let j = try JSONSerialization.data(withJSONObject: data, options: []) + if let s = String(data: j, encoding: .utf8) { + socket.request.queue.async { + socket.send(s) + } + } + } catch let e { + print("LOCATION SOCKET FAILED ENCODE JSON: \(e)") + } + } + + func sendToAllSockets(data: [String: Any]) { + for (_, s) in sockets { + sendTo(socket: s, data: data) + } + } + + public func socketDidConnect(_ socket: BRWebSocket) { + print("WEBVIEW SOCKET CONNECT \(socket.id)") + sockets[socket.id] = socket + sendTo(socket: socket, data: webViewInfo) + } + + public func socketDidDisconnect(_ socket: BRWebSocket) { + print("WEBVIEW SOCKET DISCONNECT \(socket.id)") + sockets.removeValue(forKey: socket.id) + } + + public func socket(_: BRWebSocket, didReceiveText text: String) { + print("WEBVIEW SOCKET RECV TEXT \(text)") + } + + public func socket(_: BRWebSocket, didReceiveData data: Data) { + print("WEBVIEW SOCKET RECV TEXT \(data.hexString)") + } +} diff --git a/litewallet/Platform/Extensions.swift b/litewallet/Platform/Extensions.swift new file mode 100644 index 000000000..b79051f57 --- /dev/null +++ b/litewallet/Platform/Extensions.swift @@ -0,0 +1,566 @@ +import BRCore +import Foundation +// import libbz2 +import UIKit + +public extension String { + static func buildQueryString(_ options: [String: [String]]?, includeQ: Bool = false) -> String { + var s = "" + if let options = options, !options.isEmpty { + s = includeQ ? "?" : "" + var i = 0 + for (k, vals) in options { + for v in vals { + if i != 0 { + s += "&" + } + i += 1 + s += "\(k.urlEscapedString)=\(v.urlEscapedString)" + } + } + } + return s + } + + static var urlQuoteCharacterSet: CharacterSet { + if let cset = (NSMutableCharacterSet.urlQueryAllowed as NSCharacterSet).mutableCopy() as? NSMutableCharacterSet + { + cset.removeCharacters(in: "?=&") + return cset as CharacterSet + } + return NSMutableCharacterSet.urlQueryAllowed as CharacterSet + } + + func md5() -> String { + guard let data = data(using: .utf8) + else { + assertionFailure("couldnt encode string as utf8 data") + return "" + } + + var result = Data(count: 128 / 8) + let resultCount = result.count + return result.withUnsafeMutableBytes + { (resultBytes: UnsafeMutablePointer) -> String in + data.withUnsafeBytes { dataBytes in + BRMD5(resultBytes, dataBytes, data.count) + } + var hash = String() + for i in 0 ..< resultCount { + hash = hash.appendingFormat("%02x", resultBytes[i]) + } + return hash + } + } + + func base58DecodedData() -> Data { + let len = BRBase58Decode(nil, 0, self) + var data = Data(count: len) + _ = data.withUnsafeMutableBytes { BRBase58Decode($0, len, self) } + return data + } + + var urlEscapedString: String { + return addingPercentEncoding(withAllowedCharacters: String.urlQuoteCharacterSet) ?? "" + } + + func parseQueryString() -> [String: [String]] { + var ret = [String: [String]]() + var strippedString = self + if String(self[...size) + } else if Int32(i) <= UINT16_MAX { + var header = UInt8(VAR_INT16_HEADER) + var payload = CFSwapInt16HostToLittle(UInt16(i)) + append(&header, length: MemoryLayout.size) + append(&payload, length: MemoryLayout.size) + } else if UInt32(i) <= UINT32_MAX { + var header = UInt8(VAR_INT32_HEADER) + var payload = CFSwapInt32HostToLittle(UInt32(i)) + append(&header, length: MemoryLayout.size) + append(&payload, length: MemoryLayout.size) + } else { + var header = UInt8(VAR_INT64_HEADER) + var payload = CFSwapInt64HostToLittle(i) + append(&header, length: MemoryLayout.size) + append(&payload, length: MemoryLayout.size) + } + } +} + +var BZCompressionBufferSize: UInt32 = 1024 +var BZDefaultBlockSize: Int32 = 7 +var BZDefaultWorkFactor: Int32 = 100 + +private struct AssociatedKeys { + static var hexString = "hexString" +} + +public extension Data { + var hexString: String { + if let string = getCachedHexString() { + return string + } else { + let string = reduce("") { $0 + String(format: "%02x", $1) } + setHexString(string: string) + return string + } + } + + private func setHexString(string: String) { + objc_setAssociatedObject(self, &AssociatedKeys.hexString, string, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private func getCachedHexString() -> String? { + return objc_getAssociatedObject(self, &AssociatedKeys.hexString) as? String + } + +// var bzCompressedData: Data? { +// guard !isEmpty +// else { +// return self +// } +// +// var compressed = Data() +// var stream = bz_stream() +// var mself = self +// var success = true +// mself.withUnsafeMutableBytes { (selfBuff: UnsafeMutablePointer) in +// let outBuff = UnsafeMutablePointer.allocate(capacity: Int(BZCompressionBufferSize)) +// defer { outBuff.deallocate() } +// +// stream.next_in = selfBuff +// stream.avail_in = UInt32(self.count) +// stream.next_out = outBuff +// stream.avail_out = BZCompressionBufferSize +// +// var bzret = BZ2_bzCompressInit(&stream, BZDefaultBlockSize, 0, BZDefaultWorkFactor) +// guard bzret == BZ_OK +// else { +// print("failed compression init") +// success = false +// return +// } +// repeat { +// bzret = BZ2_bzCompress(&stream, stream.avail_in > 0 ? BZ_RUN : BZ_FINISH) +// guard bzret >= BZ_OK +// else { +// print("failed compress") +// success = false +// return +// } +// let bpp = UnsafeBufferPointer(start: outBuff, count: Int(BZCompressionBufferSize) - Int(stream.avail_out)) +// compressed.append(bpp) +// stream.next_out = outBuff +// stream.avail_out = BZCompressionBufferSize +// } while bzret != BZ_STREAM_END +// } +// BZ2_bzCompressEnd(&stream) +// guard success else { return nil } +// return compressed +// } +// +// init?(bzCompressedData data: Data) { +// guard !data.isEmpty +// else { +// return nil +// } +// var stream = bz_stream() +// var decompressed = Data() +// var myDat = data +// var success = true +// myDat.withUnsafeMutableBytes { (datBuff: UnsafeMutablePointer) in +// let outBuff = UnsafeMutablePointer.allocate(capacity: Int(BZCompressionBufferSize)) +// defer { outBuff.deallocate() } +// +// stream.next_in = datBuff +// stream.avail_in = UInt32(data.count) +// stream.next_out = outBuff +// stream.avail_out = BZCompressionBufferSize +// +// var bzret = BZ2_bzDecompressInit(&stream, 0, 0) +// guard bzret == BZ_OK +// else { +// print("failed decompress init") +// success = false +// return +// } +// repeat { +// bzret = BZ2_bzDecompress(&stream) +// guard bzret >= BZ_OK +// else { +// print("failed decompress") +// success = false +// return +// } +// let bpp = UnsafeBufferPointer(start: outBuff, count: Int(BZCompressionBufferSize) - Int(stream.avail_out)) +// decompressed.append(bpp) +// stream.next_out = outBuff +// stream.avail_out = BZCompressionBufferSize +// } while bzret != BZ_STREAM_END +// } +// BZ2_bzDecompressEnd(&stream) +// guard success else { return nil } +// self.init(decompressed) +// } + + var base58: String { + return withUnsafeBytes { (selfBytes: UnsafePointer) -> String in + let len = BRBase58Encode(nil, 0, selfBytes, self.count) + var data = Data(count: len) + return data.withUnsafeMutableBytes { (b: UnsafeMutablePointer) in + BRBase58Encode(b, len, selfBytes, self.count) + return String(cString: b) + } + } + } + + var sha1: Data { + var data = Data(count: 20) + data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in + self.withUnsafeBytes { (selfBytes: UnsafePointer) in + BRSHA1(bytes, selfBytes, self.count) + } + } + return data + } + + var sha256: Data { + var data = Data(count: 32) + data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in + self.withUnsafeBytes { (selfBytes: UnsafePointer) in + BRSHA256(bytes, selfBytes, self.count) + } + } + return data + } + + var sha256_2: Data { + return sha256.sha256 + } + + var uInt256: UInt256 { + return withUnsafeBytes { (ptr: UnsafePointer) -> UInt256 in + ptr.pointee + } + } + + func uInt8(atOffset offset: UInt) -> UInt8 { + let offt = Int(offset) + let size = MemoryLayout.size + if count < offt + size { return 0 } + return subdata(in: offt ..< (offt + size)).withUnsafeBytes + { (ptr: UnsafePointer) -> UInt8 in + ptr.pointee + } + } + + func uInt32(atOffset offset: UInt) -> UInt32 { + let offt = Int(offset) + let size = MemoryLayout.size + if count < offt + size { return 0 } + return subdata(in: offt ..< (offt + size)).withUnsafeBytes + { (ptr: UnsafePointer) -> UInt32 in + CFSwapInt32LittleToHost(ptr.pointee) + } + } + + func uInt64(atOffset offset: UInt) -> UInt64 { + let offt = Int(offset) + let size = MemoryLayout.size + if count < offt + size { return 0 } + return subdata(in: offt ..< (offt + size)).withUnsafeBytes + { (ptr: UnsafePointer) -> UInt64 in + CFSwapInt64LittleToHost(ptr.pointee) + } + } + + func compactSign(key: BRKey) -> Data { + return withUnsafeBytes { (_: UnsafePointer) -> Data in + var data = Data(count: 65) + var k = key + _ = data.withUnsafeMutableBytes { BRKeyCompactSign(&k, $0, 65, self.uInt256) } + return data + } + } + + private func genNonce() -> [UInt8] { + var tv = timeval() + gettimeofday(&tv, nil) + var t = UInt64(tv.tv_usec) * 1_000_000 + UInt64(tv.tv_usec) + let p = [UInt8](repeating: 0, count: 4) + return Data(bytes: &t, count: MemoryLayout.size).withUnsafeBytes + { (dat: UnsafePointer) -> [UInt8] in + let buf = UnsafeBufferPointer(start: dat, count: MemoryLayout.size) + return p + Array(buf) + } + } + + func chacha20Poly1305AEADEncrypt(key: BRKey) -> Data { + let data = [UInt8](self) + let inData = UnsafePointer(data) + let nonce = genNonce() + var null = CChar(0) + var sk = key.secret + return withUnsafePointer(to: &sk) { + let outSize = BRChacha20Poly1305AEADEncrypt(nil, 0, $0, nonce, inData, data.count, &null, 0) + var outData = [UInt8](repeating: 0, count: outSize) + BRChacha20Poly1305AEADEncrypt(&outData, outSize, $0, nonce, inData, data.count, &null, 0) + return Data(nonce + outData) + } + } + + func chacha20Poly1305AEADDecrypt(key: BRKey) throws -> Data { + let data = [UInt8](self) + guard data.count > 12 else { throw BRReplicatedKVStoreError.malformedData } + let nonce = Array(data[data.startIndex ... data.startIndex.advanced(by: 12)]) + let inData = Array(data[data.startIndex.advanced(by: 12) ... (data.endIndex - 1)]) + var null = CChar(0) + var sk = key.secret + return withUnsafePointer(to: &sk) { + let outSize = BRChacha20Poly1305AEADDecrypt(nil, 0, $0, nonce, inData, inData.count, &null, 0) + var outData = [UInt8](repeating: 0, count: outSize) + BRChacha20Poly1305AEADDecrypt(&outData, outSize, $0, nonce, inData, inData.count, &null, 0) + return Data(outData) + } + } + + var masterPubKey: BRMasterPubKey? { + guard count >= (4 + 32 + 33) else { return nil } + var mpk = BRMasterPubKey() + mpk.fingerPrint = subdata(in: 0 ..< 4).withUnsafeBytes { $0.pointee } + mpk.chainCode = subdata(in: 4 ..< (4 + 32)).withUnsafeBytes { $0.pointee } + mpk.pubKey = subdata(in: (4 + 32) ..< (4 + 32 + 33)).withUnsafeBytes { $0.pointee } + return mpk + } + + init(masterPubKey mpk: BRMasterPubKey) { + var data = [mpk.fingerPrint].withUnsafeBufferPointer { Data(buffer: $0) } + [mpk.chainCode].withUnsafeBufferPointer { data.append($0) } + [mpk.pubKey].withUnsafeBufferPointer { data.append($0) } + self.init(data) + } + + var urlEncodedObject: [String: [String]]? { + guard let str = String(data: self, encoding: .utf8) + else { + return nil + } + return str.parseQueryString() + } +} + +public extension Date { + static func withMsTimestamp(_ ms: UInt64) -> Date { + return Date(timeIntervalSince1970: Double(ms) / 1000.0) + } + + func msTimestamp() -> UInt64 { + return UInt64((timeIntervalSince1970 < 0 ? 0 : timeIntervalSince1970) * 1000.0) + } + + // this is lifted from: https://github.com/Fykec/NSDate-RFC1123/blob/master/NSDate%2BRFC1123.swift + // Copyright © 2015 Foster Yin. All rights reserved. + fileprivate static func cachedThreadLocalObjectWithKey(_ key: String, create: () -> T) -> T + { + let threadDictionary = Thread.current.threadDictionary + if let cachedObject = threadDictionary[key] as! T? { + return cachedObject + } else { + let newObject = create() + threadDictionary[key] = newObject + return newObject + } + } + + fileprivate static func RFC1123DateFormatter() -> DateFormatter { + return cachedThreadLocalObjectWithKey("RFC1123DateFormatter") { + let locale = Locale(identifier: "en_US") + let timeZone = TimeZone(identifier: "GMT") + let dateFormatter = DateFormatter() + dateFormatter.locale = locale // need locale for some iOS 9 verision, will not select correct default locale + dateFormatter.timeZone = timeZone + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z" + return dateFormatter + } + } + + fileprivate static func RFC850DateFormatter() -> DateFormatter { + return cachedThreadLocalObjectWithKey("RFC850DateFormatter") { + let locale = Locale(identifier: "en_US") + let timeZone = TimeZone(identifier: "GMT") + let dateFormatter = DateFormatter() + dateFormatter.locale = locale // need locale for some iOS 9 verision, will not select correct default locale + dateFormatter.timeZone = timeZone + dateFormatter.dateFormat = "EEEE, dd-MMM-yy HH:mm:ss z" + return dateFormatter + } + } + + fileprivate static func asctimeDateFormatter() -> DateFormatter { + return cachedThreadLocalObjectWithKey("asctimeDateFormatter") { + let locale = Locale(identifier: "en_US") + let timeZone = TimeZone(identifier: "GMT") + let dateFormatter = DateFormatter() + dateFormatter.locale = locale // need locale for some iOS 9 verision, will not select correct default locale + dateFormatter.timeZone = timeZone + dateFormatter.dateFormat = "EEE MMM d HH:mm:ss yyyy" + return dateFormatter + } + } + + static func fromRFC1123(_ dateString: String) -> Date? { + var date: Date? + // RFC1123 + date = Date.RFC1123DateFormatter().date(from: dateString) + if date != nil { + return date + } + + // RFC850 + date = Date.RFC850DateFormatter().date(from: dateString) + if date != nil { + return date + } + + // asctime-date + date = Date.asctimeDateFormatter().date(from: dateString) + if date != nil { + return date + } + return nil + } + + func RFC1123String() -> String? { + return Date.RFC1123DateFormatter().string(from: self) + } +} + +public extension BRKey { + var publicKey: Data { + var k = self + let len = BRKeyPubKey(&k, nil, 0) + var data = Data(count: len) + BRKeyPubKey(&k, data.withUnsafeMutableBytes { (d: UnsafeMutablePointer) -> UnsafeMutablePointer in d }, len) + return data + } +} + +extension UIImage { + /// Represents a scaling mode + enum ScalingMode { + case aspectFill + case aspectFit + + /// Calculates the aspect ratio between two sizes + /// + /// - parameters: + /// - size: the first size used to calculate the ratio + /// - otherSize: the second size used to calculate the ratio + /// + /// - return: the aspect ratio between the two sizes + func aspectRatio(between size: CGSize, and otherSize: CGSize) -> CGFloat { + let aspectWidth = size.width / otherSize.width + let aspectHeight = size.height / otherSize.height + + switch self { + case .aspectFill: + return max(aspectWidth, aspectHeight) + case .aspectFit: + return min(aspectWidth, aspectHeight) + } + } + } + + /// Scales an image to fit within a bounds with a size governed by the passed size. Also keeps the aspect ratio. + /// - parameters: + /// - newSize: the size of the bounds the image must fit within. + /// - scalingMode: the desired scaling mode + /// + /// - returns: a new scaled image. + func scaled(to newSize: CGSize, scalingMode: UIImage.ScalingMode = .aspectFill) -> UIImage { + let aspectRatio = scalingMode.aspectRatio(between: newSize, and: size) + + /* Build the rectangle representing the area to be drawn */ + var scaledImageRect = CGRect.zero + + scaledImageRect.size.width = size.width * aspectRatio + scaledImageRect.size.height = size.height * aspectRatio + scaledImageRect.origin.x = (newSize.width - size.width * aspectRatio) / 2.0 + scaledImageRect.origin.y = (newSize.height - size.height * aspectRatio) / 2.0 + + /* Draw and retrieve the scaled image */ + UIGraphicsBeginImageContext(newSize) + + draw(in: scaledImageRect) + let scaledImage = UIGraphicsGetImageFromCurrentImageContext() + + UIGraphicsEndImageContext() + + return scaledImage! + } +} + +extension Dictionary where Key: ExpressibleByStringLiteral, Value: Any { + var flattened: [Key: String] { + var ret = [Key: String]() + for (k, v) in self { + if let v = v as? [String] { + if !v.isEmpty { + ret[k] = v[0] + } + } + } + return ret + } + + var jsonString: String { + guard let json = try? JSONSerialization.data(withJSONObject: self, options: []) + else { + return "null" + } + guard let jstring = String(data: json, encoding: .utf8) + else { + return "null" + } + return jstring + } +} diff --git a/litewallet/Platform/TxMetaData.swift b/litewallet/Platform/TxMetaData.swift new file mode 100644 index 000000000..013289982 --- /dev/null +++ b/litewallet/Platform/TxMetaData.swift @@ -0,0 +1,134 @@ +import BRCore +import Foundation + +// MARK: - Txn Metadata + +// Txn metadata stores additional information about a given transaction +open class TxMetaData: BRKVStoreObject, BRCoding { + var classVersion: Int = 2 + + var blockHeight: Int = 0 + var exchangeRate: Double = 0 + var exchangeRateCurrency: String = "" + var feeRate: Double = 0 + var size: Int = 0 + var created: Date = .zeroValue() + var deviceId: String = "" + var comment = "" + + public required init?(coder decoder: BRCoder) { + classVersion = decoder.decode("classVersion") + if classVersion == Int.zeroValue() { + print("[BRTxMetadataObject] Unable to unarchive _TXMetadata: no version") + return nil + } + blockHeight = decoder.decode("bh") + exchangeRate = decoder.decode("er") + exchangeRateCurrency = decoder.decode("erc") + feeRate = decoder.decode("fr") + size = decoder.decode("s") + deviceId = decoder.decode("dId") + created = decoder.decode("c") + comment = decoder.decode("comment") + super.init(key: "", version: 0, lastModified: Date(), deleted: true, data: Data()) + } + + func encode(_ coder: BRCoder) { + coder.encode(classVersion, key: "classVersion") + coder.encode(blockHeight, key: "bh") + coder.encode(exchangeRate, key: "er") + coder.encode(exchangeRateCurrency, key: "erc") + coder.encode(feeRate, key: "fr") + coder.encode(size, key: "s") + coder.encode(created, key: "c") + coder.encode(deviceId, key: "dId") + coder.encode(comment, key: "comment") + } + + // Find metadata object based on the txHash + public init?(txHash: UInt256, store: BRReplicatedKVStore) { + var ver: UInt64 + var date: Date + var del: Bool + var bytes: [UInt8] + + print("[BRTxMetadataObject] find \(txHash.txKey)") + do { + (ver, date, del, bytes) = try store.get(txHash.txKey) + let bytesDat = Data(bytes: &bytes, count: bytes.count) + super.init(key: txHash.txKey, version: ver, lastModified: date, deleted: del, data: bytesDat) + return + } catch let e { + print("[BRTxMetadataObject] Unable to initialize BRTxMetadataObject: \(String(describing: e))") + } + + return nil + } + + // Find metadata object based on the txKey + public init?(txKey: String, store: BRReplicatedKVStore) { + var ver: UInt64 + var date: Date + var del: Bool + var bytes: [UInt8] + + print("[BRTxMetadataObject] find \(txKey)") + do { + (ver, date, del, bytes) = try store.get(txKey) + let bytesDat = Data(bytes: &bytes, count: bytes.count) + super.init(key: txKey, version: ver, lastModified: date, deleted: del, data: bytesDat) + return + } catch let e { + print("[BRTxMetadataObject] Unable to initialize BRTxMetadataObject: \(String(describing: e))") + } + + return nil + } + + /// Create new transaction metadata + public init(transaction: BRTransaction, exchangeRate: Double, exchangeRateCurrency: String, feeRate: Double, + deviceId: String, comment: String? = nil) + { + print("[BRTxMetadataObject] new \(transaction.txHash.txKey)") + super.init(key: transaction.txHash.txKey, version: 0, lastModified: Date(), deleted: false, data: Data()) + blockHeight = Int(transaction.blockHeight) + created = Date() + var txn = transaction + size = BRTransactionSize(&txn) + self.exchangeRate = exchangeRate + self.exchangeRateCurrency = exchangeRateCurrency + self.feeRate = feeRate + self.deviceId = deviceId + self.comment = comment ?? "" + } + + override func getData() -> Data? { + return BRKeyedArchiver.archivedDataWithRootObject(self) + } + + override func dataWasSet(_ value: Data) { + guard let s: TxMetaData = BRKeyedUnarchiver.unarchiveObjectWithData(value) + else { + print("[BRTxMetadataObject] unable to deserialise tx metadata") + return + } + blockHeight = s.blockHeight + exchangeRate = s.exchangeRate + exchangeRateCurrency = s.exchangeRateCurrency + feeRate = s.feeRate + size = s.size + created = s.created + deviceId = s.deviceId + comment = s.comment + } +} + +extension UInt256 { + var txKey: String { + var u = self + return withUnsafePointer(to: &u) { p in + let bd = Data(bytes: p, count: MemoryLayout.stride).sha256 + return "txn2-\(bd.hexString)" + } + } +} diff --git a/litewallet/Platform/WalletInfo.swift b/litewallet/Platform/WalletInfo.swift new file mode 100644 index 000000000..05d655ce4 --- /dev/null +++ b/litewallet/Platform/WalletInfo.swift @@ -0,0 +1,54 @@ +import Foundation + +let walletInfoKey = "wallet-info" + +class WalletInfo: BRKVStoreObject, BRCoding { + var classVersion = 2 + var name = "" + var creationDate = Date.zeroValue() + + // Create new + init(name: String) { + super.init(key: walletInfoKey, version: 0, lastModified: Date(), deleted: false, data: Data()) + self.name = name + } + + // Find existing + init?(kvStore: BRReplicatedKVStore) { + var ver: UInt64 + var date: Date + var del: Bool + var bytes: [UInt8] + do { + (ver, date, del, bytes) = try kvStore.get(walletInfoKey) + } catch let e { + print("Unable to initialize WalletInfo: \(e)") + return nil + } + let bytesData = Data(bytes: &bytes, count: bytes.count) + super.init(key: walletInfoKey, version: ver, lastModified: date, deleted: del, data: bytesData) + } + + override func getData() -> Data? { + return BRKeyedArchiver.archivedDataWithRootObject(self) + } + + override func dataWasSet(_ value: Data) { + guard let s: WalletInfo = BRKeyedUnarchiver.unarchiveObjectWithData(value) else { return } + name = s.name + creationDate = s.creationDate + } + + public required init?(coder decoder: BRCoder) { + classVersion = decoder.decode("classVersion") + name = decoder.decode("name") + creationDate = decoder.decode("creationDate") + super.init(key: "", version: 0, lastModified: Date(), deleted: true, data: Data()) + } + + func encode(_ coder: BRCoder) { + coder.encode(classVersion, key: "classVersion") + coder.encode(name, key: "name") + coder.encode(creationDate, key: "creationDate") + } +} diff --git a/litewallet/Platform/module.modulemap b/litewallet/Platform/module.modulemap new file mode 100644 index 000000000..836fb658f --- /dev/null +++ b/litewallet/Platform/module.modulemap @@ -0,0 +1,4 @@ +module BRSocketHelpers [system] [extern_c] { + header "BRSocketHelpers.h" + export * +} diff --git a/litewallet/ReachabilityMonitor.swift b/litewallet/ReachabilityMonitor.swift new file mode 100644 index 000000000..7e0ede5cd --- /dev/null +++ b/litewallet/ReachabilityMonitor.swift @@ -0,0 +1,50 @@ +import Foundation +import SystemConfiguration + +private func callback(reachability _: SCNetworkReachability, flags _: SCNetworkReachabilityFlags, info: UnsafeMutableRawPointer?) +{ + guard let info = info else { return } + let reachability = Unmanaged.fromOpaque(info).takeUnretainedValue() + reachability.notify() +} + +class ReachabilityMonitor: Trackable { + init() { + networkReachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, "google.com") + start() + } + + var didChange: ((Bool) -> Void)? + + private var networkReachability: SCNetworkReachability? + private let reachabilitySerialQueue = DispatchQueue(label: "com.litecoin.reachabilityQueue") + + func notify() { + DispatchQueue.main.async { + self.didChange?(self.isReachable) + self.saveEvent(self.isReachable ? "reachability.isReachable" : "reachability.isNotReachable") + } + } + + var isReachable: Bool { + return flags.contains(.reachable) + } + + private func start() { + var context = SCNetworkReachabilityContext() + context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + guard let reachability = networkReachability else { return } + SCNetworkReachabilitySetCallback(reachability, callback, &context) + SCNetworkReachabilitySetDispatchQueue(reachability, reachabilitySerialQueue) + } + + private var flags: SCNetworkReachabilityFlags { + var flags = SCNetworkReachabilityFlags(rawValue: 0) + if let reachability = networkReachability, withUnsafeMutablePointer(to: &flags, { SCNetworkReachabilityGetFlags(reachability, UnsafeMutablePointer($0)) }) == true + { + return flags + } else { + return [] + } + } +} diff --git a/litewallet/RetryTimer.swift b/litewallet/RetryTimer.swift new file mode 100644 index 000000000..f92008d6c --- /dev/null +++ b/litewallet/RetryTimer.swift @@ -0,0 +1,25 @@ +import Foundation + +class RetryTimer { + var callback: (() -> Void)? + private var timer: Timer? + private var fibA: TimeInterval = 0.0 + private var fibB: TimeInterval = 1.0 + + func start() { + timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(retry), userInfo: nil, repeats: false) + } + + func stop() { + timer?.invalidate() + } + + @objc private func retry() { + callback?() + timer?.invalidate() + let newInterval = fibA + fibB + fibA = fibB + fibB = newInterval + timer = Timer.scheduledTimer(timeInterval: newInterval, target: self, selector: #selector(retry), userInfo: nil, repeats: false) + } +} diff --git a/litewallet/SafariServices+Extension.swift b/litewallet/SafariServices+Extension.swift new file mode 100644 index 000000000..06edd45e3 --- /dev/null +++ b/litewallet/SafariServices+Extension.swift @@ -0,0 +1,98 @@ +import Foundation +import SafariServices +import SwiftUI +import UIKit +import WebKit + +// inspired https://www.swiftyplace.com/blog/loading-a-web-view-in-swiftui-with-wkwebview + +struct WebView: UIViewRepresentable { + let url: URL + @Binding + var scrollToSignup: Bool + + @State + private + var didStartEditing: Bool = false + + func makeUIView(context _: Context) -> WKWebView { + let webview = SignupWebView(frame: CGRectZero, didStartEditing: $didStartEditing) + let request = URLRequest(url: url) + webview.load(request) + return webview + } + + func updateUIView(_ webview: WKWebView, context _: Context) { + print("::: webview \(webview.frame.size)") + + webview.endEditing(true) + + if scrollToSignup { + let point = CGPoint(x: 0, y: webview.scrollView.contentSize.height - webview.frame.size.height / 2) + print("::: point \(point)") + + webview.scrollView.setContentOffset(point, animated: true) + DispatchQueue.main.async { + self.scrollToSignup = false + } + } + } +} + +// https://stackoverflow.com/questions/44684714/show-keyboard-on-button-click-by-calling-wkwebview-input-field +class SignupWebView: WKWebView, WKNavigationDelegate { + @Binding + var didStartEditing: Bool + + init(frame: CGRect, didStartEditing: Binding) { + _didStartEditing = didStartEditing + + let configuration = WKWebViewConfiguration() + super.init(frame: frame, configuration: configuration) + navigationDelegate = self + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + return scrollView.contentSize + } + + func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { + var scriptContent = "var meta = document.createElement('meta');" + scriptContent += "meta.name='viewport';" + scriptContent += "meta.content='width=device-width';" + scriptContent += "document.getElementsByTagName('head')[0].appendChild(meta);" + scriptContent += "document.body.scrollHeight;" + + webView.evaluateJavaScript(scriptContent, completionHandler: { height, error in + + print(height) + print(error) + }) + + webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { (value: Any!, error: Error!) in + if error != nil { + // Error logic + return + } + // webView.invalidateIntrinsicContentSize() + + // let js = "document.getElementById(\"MY_TEXTFIELD_ID\").focus();" + // webView.evaluateJavaScript(js) + + // webview.canBecomeFocused = true + + // document.getElementById('myID').focus(); + + // webview.scrollView.setZoomScale(0.3, animated: true) + + let result = value as? String + + print(value) + }) + } +} diff --git a/litewallet/ScanViewController.swift b/litewallet/ScanViewController.swift new file mode 100644 index 000000000..fecc4ab44 --- /dev/null +++ b/litewallet/ScanViewController.swift @@ -0,0 +1 @@ +import Foundation diff --git a/litewallet/Sender.swift b/litewallet/Sender.swift new file mode 100644 index 000000000..62b0d11ca --- /dev/null +++ b/litewallet/Sender.swift @@ -0,0 +1,224 @@ +import BRCore +import Foundation +import UIKit + +enum SendResult { + case success + case creationError(String) + case publishFailure(BRPeerManagerError) +} + +class Sender { + // MARK: - Private Variables + + private let walletManager: WalletManager + + private let kvStore: BRReplicatedKVStore + + private let store: Store + + // MARK: - Public Variables + + var transaction: BRTxRef? + + var rate: Rate? + + var comment: String? + + var feePerKb: UInt64? + + var fee: UInt64 { + guard let tx = transaction else { return 0 } + return walletManager.wallet?.feeForTx(tx) ?? 0 + } + + var canUseBiometrics: Bool { + guard let tx = transaction else { return false } + return walletManager.canUseBiometrics(forTx: tx) + } + + init(walletManager: WalletManager, kvStore: BRReplicatedKVStore, store: Store) { + self.walletManager = walletManager + self.kvStore = kvStore + self.store = store + } + + func createTransaction(amount: UInt64, to: String) -> Bool { + transaction = walletManager.wallet?.createTransaction(forAmount: amount, toAddress: to) + return transaction != nil + } + + func createTransactionWithOpsOutputs(amount: UInt64, + to: String) -> Bool + { + transaction = walletManager.wallet?.createOpsTransaction(forAmount: amount, + toAddress: to, + opsFee: tieredOpsFee(amount: amount), + opsAddress: Partner.partnerKeyPath(name: .litewalletOps)) + + return transaction != nil + } + + func feeForTx(amount: UInt64) -> UInt64 { + return walletManager.wallet?.feeForTx(amount: amount) ?? 0 + } + + /// Send + /// - Parameters: + /// - biometricsMessage: Response from decoding the biometrics + /// - rate: LTC - Fiat rate + /// - comment: Users note to themselves + /// - feePerKb: comment rate of fee per kb + /// - verifyPinFunction: verification + /// - completion: completion + func send(biometricsMessage: String, + rate: Rate?, + comment: String?, + feePerKb: UInt64, + verifyPinFunction: + @escaping (@escaping (String) -> Bool) -> Void, + completion: @escaping (SendResult) -> Void) + { + guard let tx = transaction + else { + return completion(.creationError(S.Send.createTransactionError.localize())) + } + + self.rate = rate + self.comment = comment + self.feePerKb = feePerKb + + if UserDefaults.isBiometricsEnabled && + walletManager.canUseBiometrics(forTx: tx) + { + DispatchQueue.walletQueue.async { [weak self] in + guard let myself = self else { return } + myself + .walletManager + .signTransaction(tx, + biometricsPrompt: + biometricsMessage, + completion: { result in + if result == .success { + myself.publish(completion: completion) + } else { + if result == .failure || result == .fallback { + myself.verifyPin(tx: tx, + withFunction: verifyPinFunction, + completion: completion) + } + } + }) + } + } else { + verifyPin(tx: tx, withFunction: verifyPinFunction, completion: completion) + } + } + + /// Verify Pin + /// - Parameters: + /// - tx: TX package + /// - withFunction: completion mid-range + /// - completion: completion + + // DEV: Important Note + // This func needs to be REFACTORED as it violates OOP and intertangles TX and Pin authentication + // This means it should be 2 functions. + // VerifyPIN and VerifyTX + private func verifyPin(tx: BRTxRef, + withFunction: (@escaping (String) -> Bool) -> Void, + completion: @escaping (SendResult) -> Void) + { + withFunction { pin in + var success = false + let group = DispatchGroup() + group.enter() + DispatchQueue.walletQueue.async { + if self.walletManager.signTransaction(tx, pin: pin) { + self.publish(completion: completion) + success = true + } + group.leave() + } + let result = group.wait(timeout: .now() + 30.0) + if result == .timedOut { + let properties: [String: String] = + ["ERROR_TX": "\(tx.txHash)", + "ERROR_BLOCKHEIGHT": "\(tx.blockHeight)"] + + LWAnalytics.logEventWithParameters(itemName: + ._20200112_ERR, + properties: properties) + + let alert = UIAlertController(title: S.LitewalletAlert.corruptionError.localize(), + message: S.LitewalletAlert.corruptionMessage.localize(), + preferredStyle: .alert) + + UserDefaults.didSeeCorruption = true + alert.addAction(UIAlertAction(title: "OK", + style: .default, + handler: nil)) + return false + } + return success + } + } + + /// Publish TX + /// - Parameter completion: completion + private func publish(completion: @escaping (SendResult) -> Void) { + guard let tx = transaction else { assertionFailure("publish failure"); return } + DispatchQueue.walletQueue.async { [weak self] in + guard let myself = self else { assertionFailure("myself didn't exist"); return } + myself.walletManager.peerManager?.publishTx(tx, completion: { _, error in + DispatchQueue.main.async { + if let error = error { + completion(.publishFailure(error)) + } else { + myself.setMetaData() + completion(.success) + } + } + }) + } + } + + /// Set transaction metadata + private func setMetaData() { + // Fires an event if the rate is not set + guard let rate = rate + else { + LWAnalytics.logEventWithParameters(itemName: ._20200111_RNI) + return + } + + // Fires an event if the transaction is not set + guard let tx = transaction + else { + LWAnalytics.logEventWithParameters(itemName: ._20200111_TNI) + return + } + + // Fires an event if the feePerKb is not set + guard let feePerKb = feePerKb + else { + LWAnalytics.logEventWithParameters(itemName: ._20200111_FNI) + return + } + + let metaData = TxMetaData(transaction: tx.pointee, + exchangeRate: rate.rate, + exchangeRateCurrency: rate.code, + feeRate: Double(feePerKb), + deviceId: UserDefaults.standard.deviceID, + comment: comment) + do { + _ = try kvStore.set(metaData) + } catch { + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR, + properties: ["error": + String(describing: error)]) + } + store.trigger(name: .txMemoUpdated(tx.pointee.txHash.description)) + } +} diff --git a/litewallet/SignupWebView.swift b/litewallet/SignupWebView.swift new file mode 100644 index 000000000..a15ef5666 --- /dev/null +++ b/litewallet/SignupWebView.swift @@ -0,0 +1,83 @@ +import Combine +import Foundation +import SafariServices +import SwiftUI +import UIKit +import WebKit + +struct SignupWebViewRepresentable: UIViewRepresentable { + @Binding + var userAction: Bool + + let urlString: String + + private var webView: WKWebView? + + init(userAction: Binding, urlString: String) { + webView = WKWebView() + self.urlString = urlString + _userAction = userAction + } + + func makeUIView(context: Context) -> WKWebView { + let source = "var meta = document.createElement('meta');" + + "meta.name = 'viewport';" + + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" + + "var head = document.getElementsByTagName('head')[0];" + + "head.appendChild(meta);" + + let script = WKUserScript(source: source, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true) + + let userContentController = WKUserContentController() + userContentController.addUserScript(script) + + let configuration = WKWebViewConfiguration() + configuration.userContentController = userContentController + + let _wkwebview = WKWebView(frame: .zero, configuration: configuration) + _wkwebview.navigationDelegate = context.coordinator + _wkwebview.uiDelegate = context.coordinator + _wkwebview.allowsBackForwardNavigationGestures = false + _wkwebview.scrollView.isScrollEnabled = false + _wkwebview.backgroundColor = UIColor.liteWalletDarkBlue + _wkwebview.load(URLRequest(url: URL(string: urlString)!)) + + return _wkwebview + } + + func updateUIView(_ webView: WKWebView, context _: Context) { + webView.evaluateJavaScript("document.getElementById('submit-email').value") { response, _ in + + if let resultString = response as? String, + resultString.contains("Please") + { + userAction = true + + let signupDict: [String: String] = ["date_accepted": Date().ISO8601Format()] + LWAnalytics.logEventWithParameters(itemName: ._20240101_US, properties: signupDict) + } + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + class Coordinator: NSObject, + WKNavigationDelegate, + WKUIDelegate, + WKScriptMessageHandler + { + func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { + print("::: message \(message)") + } + + let parent: SignupWebViewRepresentable + + init(_ parent: SignupWebViewRepresentable) { + self.parent = parent + } + } +} diff --git a/litewallet/SignupWebViewModel.swift b/litewallet/SignupWebViewModel.swift new file mode 100644 index 000000000..d4de24f21 --- /dev/null +++ b/litewallet/SignupWebViewModel.swift @@ -0,0 +1,14 @@ +// +// SignupWebViewModel.swift +// litewallet +// +// Created by Kerry Washington on 1/9/24. +// Copyright © 2024 Litecoin Foundation. All rights reserved. +// +import Combine +import Foundation + +class SignupWebViewModel: ObservableObject { + var showLoader = PassthroughSubject() + var valuePublisher = PassthroughSubject() +} diff --git a/litewallet/SimpleRedux.swift b/litewallet/SimpleRedux.swift new file mode 100644 index 000000000..1482ceda4 --- /dev/null +++ b/litewallet/SimpleRedux.swift @@ -0,0 +1,186 @@ +import UIKit + +typealias Reducer = (ReduxState) -> ReduxState +typealias Selector = (_ oldState: ReduxState, _ newState: ReduxState) -> Bool + +protocol Action { + var reduce: Reducer { get } +} + +// We need reference semantics for Subscribers, so they are restricted to classes +protocol Subscriber: AnyObject {} + +extension Subscriber { + var hashValue: Int { + return ObjectIdentifier(self).hashValue + } +} + +typealias StateUpdatedCallback = (ReduxState) -> Void + +struct Subscription { + let selector: (_ oldState: ReduxState, _ newState: ReduxState) -> Bool + let callback: (ReduxState) -> Void +} + +struct Trigger { + let name: TriggerName + let callback: (TriggerName?) -> Void +} + +enum TriggerName { + case presentFaq(String) + case registerForPushNotificationToken + case retrySync + case rescan + case lock + case promptBiometrics + case promptPaperKey + case promptUpgradePin + case loginFromSend + case blockModalDismissal + case unblockModalDismissal + case recommendRescan + case scanQr + case copyWalletAddresses(String?, String?) + case hideStatusBar + case showStatusBar + case lightWeightAlert(String) + case didCreateOrRecoverWallet + case showAlert(UIAlertController?) + case reinitWalletManager((() -> Void)?) + case didUpgradePin + case txMemoUpdated(String) + case promptShareData + case didEnableShareData + case didWritePaperKey +} // NB : remember to add to triggers to == fuction below + +extension TriggerName: Equatable {} + +func == (lhs: TriggerName, rhs: TriggerName) -> Bool { + switch (lhs, rhs) { + case (.presentFaq(_), .presentFaq(_)): + return true + case (.registerForPushNotificationToken, .registerForPushNotificationToken): + return true + case (.retrySync, .retrySync): + return true + case (.rescan, .rescan): + return true + case (.lock, .lock): + return true + case (.promptBiometrics, .promptBiometrics): + return true + case (.promptPaperKey, .promptPaperKey): + return true + case (.promptUpgradePin, .promptUpgradePin): + return true + case (.loginFromSend, .loginFromSend): + return true + case (.blockModalDismissal, .blockModalDismissal): + return true + case (.unblockModalDismissal, .unblockModalDismissal): + return true + case (.recommendRescan, .recommendRescan): + return true + case (.scanQr, .scanQr): + return true + case (.copyWalletAddresses(_, _), .copyWalletAddresses(_, _)): + return true + case (.showStatusBar, .showStatusBar): + return true + case (.hideStatusBar, .hideStatusBar): + return true + case (.lightWeightAlert(_), .lightWeightAlert(_)): + return true + case (.didCreateOrRecoverWallet, .didCreateOrRecoverWallet): + return true + case (.showAlert(_), .showAlert(_)): + return true + case (.reinitWalletManager(_), .reinitWalletManager(_)): + return true + case (.didUpgradePin, .didUpgradePin): + return true + case (.txMemoUpdated(_), .txMemoUpdated(_)): + return true + case (.promptShareData, .promptShareData): + return true + case (.didEnableShareData, .didEnableShareData): + return true + case (.didWritePaperKey, .didWritePaperKey): + return true + default: + return false + } +} + +class Store { + // MARK: - Public + + func perform(action: Action) { + state = action.reduce(state) + } + + func trigger(name: TriggerName) { + triggers + .flatMap { $0.value } + .filter { $0.name == name } + .forEach { $0.callback(name) } + } + + // Subscription callback is immediately called with current State value on subscription + // and then any time the selected value changes + func subscribe(_ subscriber: Subscriber, selector: @escaping Selector, callback: @escaping (ReduxState) -> Void) + { + lazySubscribe(subscriber, selector: selector, callback: callback) + callback(state) + } + + // Same as subscribe(), but doesn't call the callback with current state upon subscription + func lazySubscribe(_ subscriber: Subscriber, selector: @escaping Selector, callback: @escaping (ReduxState) -> Void) + { + let key = subscriber.hashValue + let subscription = Subscription(selector: selector, callback: callback) + if subscriptions[key] != nil { + subscriptions[key]?.append(subscription) + } else { + subscriptions[key] = [subscription] + } + } + + func subscribe(_ subscriber: Subscriber, name: TriggerName, callback: @escaping (TriggerName?) -> Void) + { + let key = subscriber.hashValue + let trigger = Trigger(name: name, callback: callback) + if triggers[key] != nil { + triggers[key]?.append(trigger) + } else { + triggers[key] = [trigger] + } + } + + func unsubscribe(_ subscriber: Subscriber) { + subscriptions.removeValue(forKey: subscriber.hashValue) + triggers.removeValue(forKey: subscriber.hashValue) + } + + // MARK: - Private + + private(set) var state = ReduxState.initial { + didSet { + subscriptions + .flatMap { $0.value } // Retreive all subscriptions (subscriptions is a dictionary) + .filter { $0.selector(oldValue, state) } + .forEach { $0.callback(state) } + } + } + + func removeAllSubscriptions() { + subscriptions.removeAll() + triggers.removeAll() + } + + private var subscriptions: [Int: [Subscription]] = [:] + private var triggers: [Int: [Trigger]] = [:] +} diff --git a/litewallet/SimpleRedux/Actions.swift b/litewallet/SimpleRedux/Actions.swift new file mode 100644 index 000000000..82315a16f --- /dev/null +++ b/litewallet/SimpleRedux/Actions.swift @@ -0,0 +1,741 @@ +import UIKit + +// MARK: - Startup Modals + +struct ShowStartFlow: Action { + let reduce: Reducer = { + $0.clone(isStartFlowVisible: true) + } +} + +struct HideStartFlow: Action { + let reduce: Reducer = { state in + ReduxState(isStartFlowVisible: false, + isLoginRequired: state.isLoginRequired, + rootModal: .none, + walletState: state.walletState, + isLtcSwapped: state.isLtcSwapped, + currentRate: state.currentRate, + rates: state.rates, + alert: state.alert, + isBiometricsEnabled: state.isBiometricsEnabled, + defaultCurrencyCode: state.defaultCurrencyCode, + recommendRescan: state.recommendRescan, + isLoadingTransactions: state.isLoadingTransactions, + maxDigits: state.maxDigits, + isPushNotificationsEnabled: state.isPushNotificationsEnabled, + isPromptingBiometrics: state.isPromptingBiometrics, + pinLength: state.pinLength, + fees: state.fees) + } +} + +struct Reset: Action { + let reduce: Reducer = { _ in + ReduxState.initial.clone(isLoginRequired: false) + } +} + +struct RequireLogin: Action { + let reduce: Reducer = { + $0.clone(isLoginRequired: true) + } +} + +struct LoginSuccess: Action { + let reduce: Reducer = { + $0.clone(isLoginRequired: false) + } +} + +// MARK: - Root Modals + +struct RootModalActions { + struct Present: Action { + let reduce: Reducer + init(modal: RootModal) { + reduce = { $0.rootModal(modal) } + } + } +} + +// MARK: - Wallet State + +enum WalletChange { + struct setProgress: Action { + let reduce: Reducer + init(progress: Double, timestamp: UInt32) { + reduce = { $0.clone(walletSyncProgress: progress, timestamp: timestamp) } + } + } + + struct setSyncingState: Action { + let reduce: Reducer + init(_ syncState: SyncState) { + reduce = { $0.clone(syncState: syncState) } + } + } + + struct setBalance: Action { + let reduce: Reducer + init(_ balance: UInt64) { + reduce = { $0.clone(balance: balance) } + } + } + + struct setTransactions: Action { + let reduce: Reducer + init(_ transactions: [Transaction]) { + reduce = { $0.clone(transactions: transactions) } + } + } + + struct setWalletName: Action { + let reduce: Reducer + init(_ name: String) { + reduce = { $0.clone(walletName: name) } + } + } + + struct setWalletCreationDate: Action { + let reduce: Reducer + init(_ date: Date) { + reduce = { $0.clone(walletCreationDate: date) } + } + } + + struct setIsRescanning: Action { + let reduce: Reducer + init(_ isRescanning: Bool) { + reduce = { $0.clone(isRescanning: isRescanning) } + } + } +} + +// MARK: - Currency + +enum CurrencyChange { + struct toggle: Action { + let reduce: Reducer = { + UserDefaults.isLtcSwapped = !$0.isLtcSwapped + return $0.clone(isLtcSwapped: !$0.isLtcSwapped) + } + } +} + +// MARK: - Exchange Rates + +enum ExchangeRates { + struct setRates: Action { + let reduce: Reducer + init(currentRate: Rate, rates: [Rate]) { + UserDefaults.currentRateData = currentRate.dictionary + reduce = { $0.clone(currentRate: currentRate, rates: rates) } + } + } + + struct setRate: Action { + let reduce: Reducer + init(_ currentRate: Rate) { + reduce = { $0.clone(currentRate: currentRate) } + } + } +} + +// MARK: - Alerts + +enum SimpleReduxAlert { + struct Show: Action { + let reduce: Reducer + init(_ type: AlertType) { + reduce = { $0.clone(alert: type) } + } + } + + struct Hide: Action { + let reduce: Reducer = { $0.clone(alert: nil) } + } +} + +enum Biometrics { + struct setIsEnabled: Action, Trackable { + let reduce: Reducer + init(_ isBiometricsEnabled: Bool) { + UserDefaults.isBiometricsEnabled = isBiometricsEnabled + reduce = { $0.clone(isBiometricsEnabled: isBiometricsEnabled) } + saveEvent("event.enableBiometrics", attributes: ["isEnabled": "\(isBiometricsEnabled)"]) + } + } +} + +enum DefaultCurrency { + struct setDefault: Action, Trackable { + let reduce: Reducer + init(_ defaultCurrencyCode: String) { + UserDefaults.defaultCurrencyCode = defaultCurrencyCode + reduce = { $0.clone(defaultCurrencyCode: defaultCurrencyCode) } + saveEvent("event.setDefaultCurrency", attributes: ["code": defaultCurrencyCode]) + } + } +} + +enum RecommendRescan { + struct set: Action, Trackable { + let reduce: Reducer + init(_ recommendRescan: Bool) { + reduce = { $0.clone(recommendRescan: recommendRescan) } + saveEvent("event.recommendRescan") + } + } +} + +enum LoadTransactions { + struct set: Action { + let reduce: Reducer + init(_ isLoadingTransactions: Bool) { + reduce = { $0.clone(isLoadingTransactions: isLoadingTransactions) } + } + } +} + +enum MaxDigits { + struct set: Action, Trackable { + let reduce: Reducer + init(_ maxDigits: Int) { + UserDefaults.maxDigits = maxDigits + reduce = { $0.clone(maxDigits: maxDigits) } + saveEvent("maxDigits.set", attributes: ["maxDigits": "\(maxDigits)"]) + } + } +} + +enum biometricsActions { + struct setIsPrompting: Action { + let reduce: Reducer + init(_ isPrompting: Bool) { + reduce = { $0.clone(isPromptingBiometrics: isPrompting) } + } + } +} + +enum PinLength { + struct set: Action { + let reduce: Reducer + init(_ pinLength: Int) { + reduce = { $0.clone(pinLength: pinLength) } + } + } +} + +enum UpdateFees { + struct set: Action { + let reduce: Reducer + init(_ fees: Fees) { + reduce = { $0.clone(fees: fees) } + } + } +} + +// MARK: - State Creation Helpers + +extension ReduxState { + func clone(isStartFlowVisible: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func rootModal(_ type: RootModal) -> ReduxState { + return ReduxState(isStartFlowVisible: false, + isLoginRequired: isLoginRequired, + rootModal: type, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(pasteboard _: String?) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(walletSyncProgress: Double, timestamp: UInt32) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletSyncProgress, syncState: walletState.syncState, balance: walletState.balance, transactions: walletState.transactions, lastBlockTimestamp: timestamp, name: walletState.name, creationDate: walletState.creationDate, isRescanning: walletState.isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(syncState: SyncState) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletState.syncProgress, syncState: syncState, balance: walletState.balance, transactions: walletState.transactions, lastBlockTimestamp: walletState.lastBlockTimestamp, name: walletState.name, creationDate: walletState.creationDate, isRescanning: walletState.isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(balance: UInt64) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletState.syncProgress, syncState: walletState.syncState, balance: balance, transactions: walletState.transactions, lastBlockTimestamp: walletState.lastBlockTimestamp, name: walletState.name, creationDate: walletState.creationDate, isRescanning: walletState.isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(transactions: [Transaction]) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletState.syncProgress, syncState: walletState.syncState, balance: walletState.balance, transactions: transactions, lastBlockTimestamp: walletState.lastBlockTimestamp, name: walletState.name, creationDate: walletState.creationDate, isRescanning: walletState.isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(walletName: String) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletState.syncProgress, syncState: walletState.syncState, balance: walletState.balance, transactions: walletState.transactions, lastBlockTimestamp: walletState.lastBlockTimestamp, name: walletName, creationDate: walletState.creationDate, isRescanning: walletState.isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(walletSyncingErrorMessage _: String?) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletState.syncProgress, syncState: walletState.syncState, balance: walletState.balance, transactions: walletState.transactions, lastBlockTimestamp: walletState.lastBlockTimestamp, name: walletState.name, creationDate: walletState.creationDate, isRescanning: walletState.isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(walletCreationDate: Date) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletState.syncProgress, syncState: walletState.syncState, balance: walletState.balance, transactions: walletState.transactions, lastBlockTimestamp: walletState.lastBlockTimestamp, name: walletState.name, creationDate: walletCreationDate, isRescanning: walletState.isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(isRescanning: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: WalletState(isConnected: walletState.isConnected, syncProgress: walletState.syncProgress, syncState: walletState.syncState, balance: walletState.balance, transactions: walletState.transactions, lastBlockTimestamp: walletState.lastBlockTimestamp, name: walletState.name, creationDate: walletState.creationDate, isRescanning: isRescanning), + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(isLtcSwapped: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(isLoginRequired: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(currentRate: Rate, rates: [Rate]) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(currentRate: Rate) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(alert: AlertType?) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(isBiometricsEnabled: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(defaultCurrencyCode: String) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(recommendRescan: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(isLoadingTransactions: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(maxDigits: Int) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(isPushNotificationsEnabled: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(isPromptingBiometrics: Bool) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(pinLength: Int) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } + + func clone(fees: Fees) -> ReduxState { + return ReduxState(isStartFlowVisible: isStartFlowVisible, + isLoginRequired: isLoginRequired, + rootModal: rootModal, + walletState: walletState, + isLtcSwapped: isLtcSwapped, + currentRate: currentRate, + rates: rates, + alert: alert, + isBiometricsEnabled: isBiometricsEnabled, + defaultCurrencyCode: defaultCurrencyCode, + recommendRescan: recommendRescan, + isLoadingTransactions: isLoadingTransactions, + maxDigits: maxDigits, + isPushNotificationsEnabled: isPushNotificationsEnabled, + isPromptingBiometrics: isPromptingBiometrics, + pinLength: pinLength, + fees: fees) + } +} diff --git a/litewallet/SimpleRedux/ReduxState.swift b/litewallet/SimpleRedux/ReduxState.swift new file mode 100644 index 000000000..3111fad19 --- /dev/null +++ b/litewallet/SimpleRedux/ReduxState.swift @@ -0,0 +1,82 @@ +import UIKit + +struct ReduxState { + let isStartFlowVisible: Bool + let isLoginRequired: Bool + let rootModal: RootModal + let walletState: WalletState + let isLtcSwapped: Bool + let currentRate: Rate? + let rates: [Rate] + let alert: AlertType? + let isBiometricsEnabled: Bool + let defaultCurrencyCode: String + let recommendRescan: Bool + let isLoadingTransactions: Bool + let maxDigits: Int + let isPushNotificationsEnabled: Bool + let isPromptingBiometrics: Bool + let pinLength: Int + let fees: Fees +} + +extension ReduxState { + static var initial: ReduxState { + return ReduxState(isStartFlowVisible: false, + isLoginRequired: true, + rootModal: .none, + walletState: WalletState.initial, + isLtcSwapped: UserDefaults.isLtcSwapped, + currentRate: UserDefaults.currentRate, + rates: [], + alert: nil, + isBiometricsEnabled: UserDefaults.isBiometricsEnabled, + defaultCurrencyCode: UserDefaults.defaultCurrencyCode, + recommendRescan: false, + isLoadingTransactions: false, + maxDigits: UserDefaults.maxDigits, + isPushNotificationsEnabled: UserDefaults.pushToken != nil, + isPromptingBiometrics: false, + pinLength: 6, + fees: Fees.usingDefaultValues) + } +} + +enum RootModal { + case none + case send + case receive + case menu + case loginAddress + case loginScan + case manageWallet + case requestAmount + case wipeEmptyWallet +} + +enum SyncState { + case syncing + case connecting + case success +} + +struct WalletState { + let isConnected: Bool + let syncProgress: Double + let syncState: SyncState + let balance: UInt64? + let transactions: [Transaction] + let lastBlockTimestamp: UInt32 + let name: String + let creationDate: Date + let isRescanning: Bool + static var initial: WalletState { + return WalletState(isConnected: false, syncProgress: 0.0, syncState: .success, balance: nil, transactions: [], lastBlockTimestamp: 0, name: S.AccountHeader.defaultWalletName.localize(), creationDate: Date.zeroValue(), isRescanning: false) + } +} + +extension WalletState: Equatable {} + +func == (lhs: WalletState, rhs: WalletState) -> Bool { + return lhs.isConnected == rhs.isConnected && lhs.syncProgress == rhs.syncProgress && lhs.syncState == rhs.syncState && lhs.balance == rhs.balance && lhs.transactions == rhs.transactions && lhs.name == rhs.name && lhs.creationDate == rhs.creationDate && lhs.isRescanning == rhs.isRescanning +} diff --git a/litewallet/StartFlowPresenter.swift b/litewallet/StartFlowPresenter.swift new file mode 100644 index 000000000..a51199265 --- /dev/null +++ b/litewallet/StartFlowPresenter.swift @@ -0,0 +1,217 @@ +import UIKit + +class StartFlowPresenter: Subscriber { + // MARK: - Public + + // MARK: - Private + + private let store: Store + private let rootViewController: UIViewController + private var navigationController: ModalNavigationController? + private let navigationControllerDelegate: StartNavigationDelegate + private let walletManager: WalletManager + private var loginViewController: UIViewController? + private let loginTransitionDelegate = LoginTransitionDelegate() + + init(store: Store, walletManager: WalletManager, rootViewController: UIViewController) { + self.store = store + self.walletManager = walletManager + self.rootViewController = rootViewController + navigationControllerDelegate = StartNavigationDelegate(store: store) + addSubscriptions() + } + + private func addSubscriptions() { + store.subscribe(self, + selector: { $0.isStartFlowVisible != $1.isStartFlowVisible }, + callback: { self.handleStartFlowChange(state: $0) }) + store.lazySubscribe(self, + selector: { $0.isLoginRequired != $1.isLoginRequired }, + callback: { self.handleLoginRequiredChange(state: $0) }) + store.subscribe(self, name: .lock, + callback: { _ in self.presentLoginFlow(isPresentedForLock: true) }) + } + + private func handleStartFlowChange(state: ReduxState) { + if state.isStartFlowVisible { + guardProtected(queue: DispatchQueue.main) { [weak self] in + self?.presentStartFlow() + } + } else { + dismissStartFlow() + } + } + + private func handleLoginRequiredChange(state: ReduxState) { + if state.isLoginRequired { + presentLoginFlow(isPresentedForLock: false) + } else { + dismissLoginFlow() + } + } + + // MARK: - SwiftUI Start Flow + + private func presentStartFlow() { + /// DOC: This is a legacy path for iPad users since SwiftUI doesnt gracefully handle presentations like iPhone + if UIDevice.current.userInterfaceIdiom == .pad { + let startViewController = StartViewController(store: store, + didTapCreate: { [weak self] in + self?.pushPinCreationViewControllerForNewWallet() + }, + didTapRecover: { [weak self] in + guard let myself = self else { return } + let recoverIntro = RecoverWalletIntroViewController(didTapNext: myself.pushRecoverWalletView) + myself.navigationController?.setClearNavbar() + myself.navigationController?.modalPresentationStyle = .fullScreen + myself.navigationController?.setNavigationBarHidden(false, animated: false) + myself.navigationController?.pushViewController(recoverIntro, animated: true) + }) + + navigationController = ModalNavigationController(rootViewController: startViewController) + navigationController?.delegate = navigationControllerDelegate + navigationController?.modalPresentationStyle = .fullScreen + } else { + let startHostingController = StartHostingController(store: store, + walletManager: walletManager) + + startHostingController.viewModel.userWantsToCreate { + self.pushPinCreationViewControllerForNewWallet() + } + + startHostingController.viewModel.userWantsToRecover { + let recoverIntro = RecoverWalletIntroViewController(didTapNext: self.pushRecoverWalletView) + self.navigationController?.setClearNavbar() + self.navigationController?.modalPresentationStyle = .fullScreen + self.navigationController?.setNavigationBarHidden(false, animated: false) + self.navigationController?.pushViewController(recoverIntro, animated: true) + } + + navigationController = ModalNavigationController(rootViewController: startHostingController) + navigationController?.delegate = navigationControllerDelegate + navigationController?.modalPresentationStyle = .fullScreen + } + + if let startFlow = navigationController { + startFlow.setNavigationBarHidden(true, animated: false) + rootViewController.present(startFlow, animated: false, completion: nil) + } + } + + private var pushRecoverWalletView: () -> Void { + return { [weak self] in + guard let myself = self else { return } + let recoverWalletViewController = EnterPhraseViewController(store: myself.store, walletManager: myself.walletManager, reason: .setSeed(myself.pushPinCreationViewForRecoveredWallet)) + myself.navigationController?.pushViewController(recoverWalletViewController, animated: true) + } + } + + private func pushPinCreationViewControllerForNewWallet() { + let pinCreationViewController = UpdatePinViewController(store: store, walletManager: walletManager, type: .creationNoPhrase, showsBackButton: true, phrase: nil) + pinCreationViewController.setPinSuccess = { [weak self] pin in + autoreleasepool { + guard self?.walletManager.setRandomSeedPhrase() != nil else { self?.handleWalletCreationError(); return } + self?.store.perform(action: WalletChange.setWalletCreationDate(Date())) + DispatchQueue.walletQueue.async { + self?.walletManager.peerManager?.connect() + DispatchQueue.main.async { + self?.pushStartPaperPhraseCreationViewController(pin: pin) + self?.store.trigger(name: .didCreateOrRecoverWallet) + } + } + } + } + + navigationController?.setNavigationBarHidden(false, animated: false) + navigationController?.setTintableBackArrow() + navigationController?.setClearNavbar() + navigationController?.pushViewController(pinCreationViewController, animated: true) + } + + private var pushPinCreationViewForRecoveredWallet: (String) -> Void { + return { [weak self] phrase in + guard let myself = self else { return } + let pinCreationView = UpdatePinViewController(store: myself.store, walletManager: myself.walletManager, type: .creationWithPhrase, showsBackButton: false, phrase: phrase) + pinCreationView.setPinSuccess = { [weak self] _ in + DispatchQueue.walletQueue.async { + self?.walletManager.peerManager?.connect() + DispatchQueue.main.async { + self?.store.trigger(name: .didCreateOrRecoverWallet) + } + } + } + myself.navigationController?.pushViewController(pinCreationView, animated: true) + } + } + + private func pushStartPaperPhraseCreationViewController(pin: String) { + let paperPhraseViewController = StartPaperPhraseViewController(store: store, callback: { [weak self] in + self?.pushWritePaperPhraseViewController(pin: pin) + }) + paperPhraseViewController.title = S.SecurityCenter.Cells.paperKeyTitle.localize() + paperPhraseViewController.navigationItem.setHidesBackButton(true, animated: false) + paperPhraseViewController.hideCloseNavigationItem() // Forces user to confirm paper-key + + navigationController?.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.white, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController?.pushViewController(paperPhraseViewController, animated: true) + } + + private func pushWritePaperPhraseViewController(pin: String) { + let writeViewController = WritePaperPhraseViewController(store: store, walletManager: walletManager, pin: pin, callback: { [weak self] in + self?.pushConfirmPaperPhraseViewController(pin: pin) + }) + writeViewController.title = S.SecurityCenter.Cells.paperKeyTitle.localize() + writeViewController.hideCloseNavigationItem() + navigationController?.pushViewController(writeViewController, animated: true) + } + + private func pushConfirmPaperPhraseViewController(pin: String) { + let confirmVC = UIStoryboard(name: "Phrase", bundle: nil).instantiateViewController(withIdentifier: "ConfirmPaperPhraseViewController") as? ConfirmPaperPhraseViewController + confirmVC?.store = store + confirmVC?.walletManager = walletManager + confirmVC?.pin = pin + confirmVC?.didCompleteConfirmation = { [weak self] in + guard let myself = self else { return } + myself.store.perform(action: SimpleReduxAlert.Show(.paperKeySet(callback: { + self?.store.perform(action: HideStartFlow()) + }))) + } + navigationController?.navigationBar.tintColor = .white + if let confirmVC = confirmVC { + navigationController?.pushViewController(confirmVC, animated: true) + } + } + + private func presentLoginFlow(isPresentedForLock: Bool) { + let loginView = LoginViewController(store: store, isPresentedForLock: isPresentedForLock, walletManager: walletManager) + if isPresentedForLock { + loginView.shouldSelfDismiss = true + } + loginView.transitioningDelegate = loginTransitionDelegate + loginView.modalPresentationStyle = .overFullScreen + loginView.modalPresentationCapturesStatusBarAppearance = true + loginViewController = loginView + rootViewController.present(loginView, animated: false, completion: nil) + } + + private func handleWalletCreationError() { + let alert = UIAlertController(title: S.LitewalletAlert.error.localize(), message: "Could not create wallet", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: nil)) + navigationController?.present(alert, animated: true, completion: nil) + } + + private func dismissStartFlow() { + navigationController?.dismiss(animated: true) { [weak self] in + self?.navigationController = nil + } + } + + private func dismissLoginFlow() { + loginViewController?.dismiss(animated: true, completion: { [weak self] in + self?.loginViewController = nil + }) + } +} diff --git a/litewallet/StartNavigationDelegate.swift b/litewallet/StartNavigationDelegate.swift new file mode 100644 index 000000000..c6d54b5d4 --- /dev/null +++ b/litewallet/StartNavigationDelegate.swift @@ -0,0 +1,53 @@ +import UIKit + +class StartNavigationDelegate: NSObject, UINavigationControllerDelegate { + let store: Store + + init(store: Store) { + self.store = store + } + + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated _: Bool) + { + if viewController is RecoverWalletIntroViewController { + navigationController.navigationBar.tintColor = .white + navigationController.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.white, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController.setClearNavbar() + navigationController.navigationBar.barTintColor = .clear + } + + if viewController is EnterPhraseViewController { + navigationController.navigationBar.tintColor = .darkText + navigationController.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController.setClearNavbar() + navigationController.navigationBar.isTranslucent = false + navigationController.navigationBar.barTintColor = .whiteTint + } + + if viewController is UpdatePinViewController { + navigationController.navigationBar.tintColor = .darkText + navigationController.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + navigationController.setClearNavbar() + } + + if viewController is UpdatePinViewController { + if let gr = navigationController.interactivePopGestureRecognizer { + navigationController.view.removeGestureRecognizer(gr) + } + } + + if viewController is StartWipeWalletViewController { + navigationController.setClearNavbar() + navigationController.setWhiteStyle() + } + } +} diff --git a/litewallet/StartView.swift b/litewallet/StartView.swift new file mode 100644 index 000000000..3525f5cb8 --- /dev/null +++ b/litewallet/StartView.swift @@ -0,0 +1,215 @@ +import SwiftUI +import UIKit + +struct StartView: View { + let buttonFont: Font = .barlowSemiBold(size: 20.0) + let buttonLightFont: Font = .barlowLight(size: 20.0) + let tinyFont: Font = .barlowRegular(size: 12.0) + + let squareButtonSize: CGFloat = 55.0 + let squareImageSize: CGFloat = 25.0 + + @ObservedObject + var startViewModel: StartViewModel + + @State + private var selectedLang: Bool = false + + @State + private var delayedSelect: Bool = false + + @State + private var currentTagline = "" + + @State + private var animationAmount = 0.0 + + @State + private var pickedLanguage: LanguageSelection = .English + + @State + private var didContinue: Bool = false + + init(viewModel: StartViewModel) { + startViewModel = viewModel + } + + var body: some View { + GeometryReader { geometry in + + let width = geometry.size.width + let height = geometry.size.height + + NavigationView { + ZStack { + Color.litewalletDarkBlue.edgesIgnoringSafeArea(.all) + VStack { + Spacer() + Image("new-logotype-white") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: width * 0.6) + .padding(.top, height * 0.05) + .padding(.bottom, height * 0.05) + + Text(currentTagline) + .font(buttonLightFont) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, + vertical: true) + .foregroundColor(.white) + .frame(width: width * 0.7, + height: height * 0.05, + alignment: .center) + .padding(.top, height * 0.02) + .padding(.bottom, height * 0.08) + .onAppear { + currentTagline = startViewModel.staticTagline + } + HStack { + ZStack { + Picker("", selection: $pickedLanguage) { + ForEach(startViewModel.languages, id: \.self) { + Text($0.nativeName) + .font(selectedLang ? buttonFont : buttonLightFont) + .foregroundColor(.white) + } + } + .pickerStyle(.wheel) + .frame(width: width * 0.5) + .onChange(of: $pickedLanguage.wrappedValue) { _ in + startViewModel.currentLanguage = pickedLanguage + selectedLang = true + currentTagline = startViewModel.taglines[startViewModel.currentLanguage.rawValue] + startViewModel.speakLanguage() + delay(1.5) { + delayedSelect = true + } + } + + Image(systemName: "checkmark.message.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: squareImageSize, + height: squareImageSize, + alignment: .center) + .foregroundColor(selectedLang ? + .litewalletGreen : .litecoinGray.opacity(0.4)) + .shadow(radius: 6, x: 3.0, y: 3.0) + .padding(.all, 4.0) + .frame(width: width * 0.3, + height: squareButtonSize, + alignment: .center) + .offset(CGSize(width: width * 0.18, + height: -height * 0.03)) + .scaleEffect(CGSize(width: animationAmount, height: animationAmount)) + .animation( + .easeInOut(duration: 1.8) + .repeatCount(5), + value: animationAmount + ) + .onAppear { + animationAmount = 1.0 + } + } + } + .frame(width: width * 0.9, + height: height * 0.1, + alignment: .center) + .alert(startViewModel + .alertMessage[startViewModel.currentLanguage.rawValue], + isPresented: $delayedSelect) { + HStack { + Button(startViewModel + .yesLabel[startViewModel.currentLanguage.rawValue], role: .cancel) { + // Changes and Dismisses + startViewModel.setLanguage(code: startViewModel.currentLanguage.code) + selectedLang = false + } + Button(startViewModel + .cancelLabel[startViewModel.currentLanguage.rawValue], role: .destructive) { + // Dismisses + selectedLang = false + } + } + } + Spacer() + NavigationLink(destination: + + AnnounceUpdatesView(navigateStart: .create, + language: startViewModel.currentLanguage, + didTapContinue: $didContinue) + .environmentObject(startViewModel) + .navigationBarBackButtonHidden(false) + ) { + ZStack { + RoundedRectangle(cornerRadius: bigButtonCornerRadius) + .frame(width: width * 0.9, height: 45, alignment: .center) + .foregroundColor(.white) + .shadow(radius: 3, x: 3.0, y: 3.0) + + Text(S.StartViewController.createButton.localize()) + .frame(width: width * 0.9, height: 45, alignment: .center) + .font(buttonFont) + .foregroundColor(.litewalletBlue) + .overlay( + RoundedRectangle(cornerRadius: bigButtonCornerRadius) + .stroke(.white, lineWidth: 2.0) + ) + } + } + .padding([.top, .bottom], 10.0) + + NavigationLink(destination: + + AnnounceUpdatesView(navigateStart: .recover, + language: startViewModel.currentLanguage, + didTapContinue: $didContinue) + .environmentObject(startViewModel) + .navigationBarBackButtonHidden(false) + ) { + ZStack { + RoundedRectangle(cornerRadius: bigButtonCornerRadius) + .frame(width: width * 0.9, height: 45, alignment: .center) + .foregroundColor(Color(UIColor.liteWalletBlue) + ).shadow(radius: 5, x: 3.0, y: 3.0) + + Text(S.StartViewController.recoverButton.localize()) + .frame(width: width * 0.9, height: 45, alignment: .center) + .font(buttonLightFont) + .foregroundColor(Color(UIColor.litecoinWhite)) + .overlay( + RoundedRectangle(cornerRadius: bigButtonCornerRadius) + .stroke(.white) + ) + } + } + .padding([.top, .bottom], 10.0) + + Text(AppVersion.string) + .frame(width: 100, alignment: .center) + .font(tinyFont) + .foregroundColor(.white) + .padding(.all, 5.0) + } + .padding(.all, swiftUICellPadding) + } + } + } + .alert(S.LitewalletAlert.error.localize(), + isPresented: $startViewModel.walletCreationDidFail, + actions: { + HStack { + Button(S.Button.ok.localize(), role: .cancel) { + startViewModel.walletCreationDidFail = false + } + } + }) + } +} + +// #Preview { +// StartView(viewModel: StartViewModel(store: Store(), +// walletManager: WalletManager(store: Store()))) +// .environment(\.locale, .init(identifier: "en")) +// } diff --git a/litewallet/StartViewModel.swift b/litewallet/StartViewModel.swift index 849ea475d..70a8b7646 100644 --- a/litewallet/StartViewModel.swift +++ b/litewallet/StartViewModel.swift @@ -134,7 +134,7 @@ class StartViewModel: ObservableObject { // MARK: - Lengthy elements /// Set these to the bottom to make the others more readable - + /// These are semi-hardcoded because the state is in flux let taglines: [String] = [ "The most secure and easiest way to use Litecoin.", "使用莱特币最安全、最简单的方式。", @@ -168,6 +168,41 @@ class StartViewModel: ObservableObject { "Dili Türkçe olarak değiştirmek istediğinizden emin misiniz?", "Ви впевнені, що хочете змінити мову на українську?", ] + + let yesLabel: [String] = [ + "Yes", + "是的", + "是的", + "Oui", + "Ja", + "Ya", + "SÌ", + "はい", + "예", + "Sim", + "Да", + "Sí", + "Evet", + "Так", + ] + + let cancelLabel: [String] = [ + "Cancel", + "取消", + "取消", + "Annuler", + "Stornieren", + "Membatalkan", + "Annulla", + "キャンセル", + "취소", + "Cancelar", + "Отмена", + "Cancelar", + "İptal etmek", + "Скасувати", + ] + func stringToCurrentLanguage(languageString: String) -> LanguageSelection { switch languageString { case "English": diff --git a/litewallet/Strings.swift b/litewallet/Strings.swift new file mode 100644 index 000000000..36ce677a1 --- /dev/null +++ b/litewallet/Strings.swift @@ -0,0 +1,724 @@ +import Foundation + +enum S { + enum CreateStep { + enum MainTitle { + static let intro = Localization(key: "MainTitle.intro", value: "Quick Start", comment: "Litewallet Quick Start") + static let checkboxes = Localization(key: "MainTitle.checkboxes", value: "Stay Connected", comment: "Stay Connected") + static let seedPhrase = Localization(key: "MainTitle.seedPhrase", value: "Protect your Litecoin!", comment: "Protect your Litecoin!") + static let finished = Localization(key: "MainTitle.finished", value: "Confirm and Go", comment: "Confirm and Go") + } + + enum DetailedMessage { + static let intro = Localization(key: "DetailedMessage.intro", value: "Take the next 5 minutes to secure your Litecoin.", comment: "Detailed message for intro") + static let checkboxes = Localization(key: "DetailedMessage.checkboxes", value: "Sign up for the push notifications to learn Litecoin news. Unsubscribe any time in Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "DetailedMessage.seedPhrase", value: "Please write this down", comment: "Warning seed phrase") + static let finished = Localization(key: "DetailedMessage.finished", value: "Settings", comment: "Settings button label") + } + + enum ExtendedMessage { + static let intro = Localization(key: "ExtendedMessage.intro", value: "Please find a private place to write down your PIN and seed phrase.", comment: "extended message") + static let checkboxes = Localization(key: "ExtendedMessage.checkboxes", value: "Enter your email to get updates about Litewallet", comment: "Email sign up website") + static let seedPhrase = Localization(key: "ExtendedMessage.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "ExtendedMessage.finished", value: "Settings", comment: "Settings button label") + } + + enum Bullet1 { + static let intro = Localization(key: "Bullet1.intro", value: "Litewallet is from the Litecoin Foundation and Charlie Lee, creator of Litecoin.", comment: "") + static let checkboxes = Localization(key: "Bullet1.checkboxes", value: "Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "Bullet1.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "Bullet1.finished", value: "Settings", comment: "Settings button label") + } + + enum Bullet2 { + static let intro = Localization(key: "Bullet2.intro", value: "Settings", comment: "Settings button label") + static let checkboxes = Localization(key: "Bullet2.checkboxes", value: "Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "Bullet2.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "Bullet2.finished", value: "Settings", comment: "Settings button label") + } + + enum Bullet3 { + static let intro = Localization(key: "Bullet3.intro", value: "Settings", comment: "Settings button label") + static let checkboxes = Localization(key: "Bullet3.checkboxes", value: "Settings", comment: "Settings button label") + static let seedPhrase = Localization(key: "Bullet3.seedPhrase", value: "Settings", comment: "Settings button label") + static let finished = Localization(key: "Bullet3.finished", value: "Settings", comment: "Settings button label") + } + } + + enum Symbols { + static let photons = "mł" + static let lites = "ł" + static let ltc = "Ł" + static let narrowSpace = "\u{2009}" + static let lock = "\u{1F512}" + static let redX = "\u{274C}" + static func currencyButtonTitle(maxDigits: Int) -> String { + switch maxDigits { + case 2: + return "photons\(S.Symbols.narrowSpace)(m\(S.Symbols.lites))" + case 5: + return "lites\(S.Symbols.narrowSpace)(\(S.Symbols.lites))" + case 8: + return "LTC\(S.Symbols.narrowSpace)(\(S.Symbols.ltc))" + default: + return "lites\(S.Symbols.narrowSpace)(\(S.Symbols.lites))" + } + } + } + + enum Conjuction { + static let asOf = Localization(key: "Conjunction.asOf", value: "as of", comment: "as of a time or date") + } + + // MARK: - Generic Button labels + + enum Button { + static let ok = Localization(key: "Button.ok", value: "OK", comment: "OK button label") + static let cancel = Localization(key: "Button.cancel", value: "Cancel", comment: "Cancel button label") + static let settings = Localization(key: "Button.settings", value: "Settings", comment: "Settings button label") + static let submit = Localization(key: "Button.submit", value: "Submit", comment: "Settings button label") + static let ignore = Localization(key: "Button.ignore", value: "Ignore", comment: "Ignore button label") + static let yes = Localization(key: "Button.yes", value: "Yes", comment: "Yes button") + static let no = Localization(key: "Button.no", value: "No", comment: "No button") + static let send = Localization(key: "Button.send", value: "send", comment: "send button") + static let receive = Localization(key: "Button.receive", value: "receive", comment: "receive button") + static let menu = Localization(key: "Button.menu", value: "menu", comment: "menu button") + static let buy = Localization(key: "Button.buy", value: "buy", comment: "buy button") + static let resetFields = Localization(key: "Button.resetFields", value: "reset", comment: "resetFields") + } + + enum LitewalletAlert { + static let warning = Localization(key: "Alert.warning", value: "Warning", comment: "Warning alert title") + static let error = Localization(key: "Alert.error", value: "Error", comment: "Error alert title") + static let noInternet = Localization(key: "Alert.noInternet", value: "No internet connection found. Check your connection and try again.", comment: "No internet alert message") + static let corruptionError = Localization(key: "Alert.corruptionError", value: "Database Corruption Error", comment: "Corruption Error alert title") + static let corruptionMessage = Localization(key: "Alert.corruptionMessage", value: "Your local database is corrupted. Go to Settings > Blockchain: Settings > Delete Database to refresh", comment: "Corruption Error alert title") + } + + enum Scanner { + static let flashButtonLabel = Localization(key: "Scanner.flashButtonLabel", value: "Camera Flash", comment: "Scan Litecoin address camera flash toggle") + } + + enum Send { + static let title = Localization(key: "Send.title", value: "Send", comment: "Send modal title") + static let toLabel = Localization(key: "Send.toLabel", value: "To", comment: "Send money to label") + static let enterLTCAddressLabel = Localization(key: "Send.enterLTCAddress", value: "Enter LTC Address", comment: "Enter LTC Address") + static let amountLabel = Localization(key: "Send.amountLabel", value: "Amount", comment: "Send money amount label") + static let descriptionLabel = Localization(key: "Send.descriptionLabel", value: "Memo", comment: "Description for sending money label") + static let sendLabel = Localization(key: "Send.sendLabel", value: "Send", comment: "Send button label") + static let pasteLabel = Localization(key: "Send.pasteLabel", value: "Paste", comment: "Paste button label") + static let scanLabel = Localization(key: "Send.scanLabel", value: "Scan", comment: "Scan button label") + static let invalidAddressTitle = Localization(key: "Send.invalidAddressTitle", value: "Invalid Address", comment: "Invalid address alert title") + static let invalidAddressMessage = Localization(key: "Send.invalidAddressMessage", value: "The destination address is not a valid Litecoin address.", comment: "Invalid address alert message") + static let invalidAddressOnPasteboard = Localization(key: "Send.invalidAddressOnPasteboard", value: "Pasteboard does not contain a valid Litecoin address.", comment: "Invalid address on pasteboard message") + static let emptyPasteboard = Localization(key: "Send.emptyPasteboard", value: "Pasteboard is empty", comment: "Empty pasteboard error message") + static let cameraUnavailableTitle = Localization(key: "Send.cameraUnavailableTitle", value: "Litewallet is not allowed to access the camera", comment: "Camera not allowed alert title") + static let cameraUnavailableMessage = Localization(key: "Send.cameraunavailableMessage", value: "Go to Settings to allow camera access.", comment: "Camera not allowed message") + static let balance = Localization(key: "Send.balance", value: "Balance: %1$@", comment: "Balance: $4.00") + static let fee = Localization(key: "Send.fees", value: "Fees: %1$@", comment: "Fees: $0.10") + static let feeBlank = Localization(key: "Send.feeBlank", value: "Fees:", comment: "Fees: ") + static let bareFee = Localization(key: "Send.fee", value: "Fee: %1$@", comment: "Fee: $0.01") + static let containsAddress = Localization(key: "Send.containsAddress", value: "The destination is your own address. You cannot send to yourself.", comment: "Warning when sending to self.") + enum UsedAddress { + static let title = Localization(key: "Send.UsedAddress.title", value: "Address Already Used", comment: "Adress already used alert title") + static let firstLine = Localization(key: "Send.UsedAddress.firstLine", value: "Litecoin addresses are intended for single use only.", comment: "Adress already used alert message - first part") + static let secondLine = Localization(key: "Send.UsedAddress.secondLIne", value: "Re-use reduces privacy for both you and the recipient and can result in loss if the recipient doesn't directly control the address.", comment: "Adress already used alert message - second part") + } + + static let identityNotCertified = Localization(key: "Send.identityNotCertified", value: "Payee identity isn't certified.", comment: "Payee identity not certified alert title.") + static let createTransactionError = Localization(key: "Send.creatTransactionError", value: "Could not create transaction.", comment: "Could not create transaction alert title") + static let publicTransactionError = Localization(key: "Send.publishTransactionError", value: "Could not publish transaction.", comment: "Could not publish transaction alert title") + static let noAddress = Localization(key: "Send.noAddress", value: "Please enter the recipient's address.", comment: "Empty address alert message") + static let noAmount = Localization(key: "Send.noAmount", value: "Please enter an amount to send.", comment: "Emtpy amount alert message") + static let isRescanning = Localization(key: "Send.isRescanning", value: "Sending is disabled during a full rescan.", comment: "Is rescanning error message") + static let remoteRequestError = Localization(key: "Send.remoteRequestError", value: "Could not load payment request", comment: "Could not load remote request error message") + static let loadingRequest = Localization(key: "Send.loadingRequest", value: "Loading Request", comment: "Loading request activity view message") + static let insufficientFunds = Localization(key: "Send.insufficientFunds", value: "Insufficient Funds", comment: "Insufficient funds error") + static let barItemTitle = Localization(key: "Send.barItemTitle", value: "Send", comment: "Send Bar Item Title") + + enum UnstoppableDomains { + static let placeholder = Localization(key: "Send.UnstoppableDomains.placeholder", value: "Enter a .crypto or .zil domain", comment: "Enter a .crypto,.zil domain") + static let simplePlaceholder = Localization(key: "Send.UnstoppableDomains.simpleplaceholder", value: "Enter domain", comment: "Enter domain") + static let enterA = Localization(key: "Send.UnstoppableDomains.enterA", value: "Enter a", comment: "Enter a") + static let domain = Localization(key: "Send.UnstoppableDomains.domain", value: "domain", comment: "domain") + static let lookup = Localization(key: "Send.UnstoppableDomains.lookup", value: "Lookup", comment: "Lookup") + static let lookupFailureHeader = Localization(key: "Send.UnstoppableDomains.lookupFailureHeader", value: "LookupFailureHeader", comment: "lookupFailureHeader") + static let lookupDomainError = Localization(key: "Send.UnstoppableDomains.lookupDomainError", value: "LookupDomainError", comment: "LookupDomainError") + static let udSystemError = Localization(key: "Send.UnstoppableDomains.udSystemError", value: "UDSystemError", comment: "UDSystemError") + } + } + + enum Receive { + static let title = Localization(key: "Receive.title", value: "Receive", comment: "Receive modal title") + static let emailButton = Localization(key: "Receive.emailButton", value: "Email", comment: "Share via email button label") + static let textButton = Localization(key: "Receive.textButton", value: "Text Message", comment: "Share via text message (SMS)") + static let copied = Localization(key: "Receive.copied", value: "Copied to clipboard.", comment: "Address copied message.") + static let share = Localization(key: "Receive.share", value: "Share", comment: "Share button label") + static let request = Localization(key: "Receive.request", value: "Request an Amount", comment: "Request button label") + static let barItemTitle = Localization(key: "Receive.barItemTitle", value: "Receive", comment: "Receive Bar Item Title") + } + + // MARK: - Litewallet + + enum Litewallet { + static let name = Localization(key: "Litewallet.name", value: "Litewallet", comment: "Litewallet name") + } + + enum Account { + static let loadingMessage = Localization(key: "Account.loadingMessage", value: "Loading Wallet", comment: "Loading Wallet Message") + } + + enum History { + static let barItemTitle = Localization(key: "History.barItemTitle", value: "History", comment: "History Bar Item Title") + static let currentLitecoinValue = Localization(key: "History.currentLitecoinValue", value: "History CurrentLitecoinValue", comment: "History Current Litecoin Value") + } + + enum JailbreakWarnings { + static let title = Localization(key: "JailbreakWarnings.title", value: "WARNING", comment: "Jailbreak warning title") + static let messageWithBalance = Localization(key: "JailbreakWarnings.messageWithBalance", value: "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin! Wipe this wallet immediately and restore on a secure device.", comment: "Jailbreak warning message") + static let messageWithoutBalance = Localization(key: "JailbreakWarnings.messageWithoutBalance", value: "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin. Please only use Litewallet on a non-jailbroken device.", comment: "Jailbreak warning message") + static let ignore = Localization(key: "JailbreakWarnings.ignore", value: "Ignore", comment: "Ignore jailbreak warning button") + static let wipe = Localization(key: "JailbreakWarnings.wipe", value: "Wipe", comment: "Wipe wallet button") + static let close = Localization(key: "JailbreakWarnings.close", value: "Close", comment: "Close app button") + } + + enum ErrorMessages { + static let emailUnavailableTitle = Localization(key: "ErrorMessages.emailUnavailableTitle", value: "Email Unavailable", comment: "Email unavailable alert title") + static let emailUnavailableMessage = Localization(key: "ErrorMessages.emailUnavailableMessage", value: "This device isn't configured to send email with the iOS mail app.", comment: "Email unavailable alert title") + static let messagingUnavailableTitle = Localization(key: "ErrorMessages.messagingUnavailableTitle", value: "Messaging Unavailable", comment: "Messaging unavailable alert title") + static let messagingUnavailableMessage = Localization(key: "ErrorMessages.messagingUnavailableMessage", value: "This device isn't configured to send messages.", comment: "Messaging unavailable alert title") + } + + enum UnlockScreen { + static let myAddress = Localization(key: "UnlockScreen.myAddress", value: "My Address", comment: "My Address button title") + static let scan = Localization(key: "UnlockScreen.scan", value: "Scan", comment: "Scan button title") + static let touchIdText = Localization(key: "UnlockScreen.touchIdText", value: "Unlock with TouchID", comment: "Unlock with TouchID accessibility label") + static let touchIdPrompt = Localization(key: "UnlockScreen.touchIdPrompt", value: "Unlock your Litewallet.", comment: "TouchID/FaceID prompt text") + static let enterPIN = Localization(key: "UnlockScreen.enterPin", value: "Enter PIN", comment: "Unlock Screen sub-header") + static let unlocked = Localization(key: "UnlockScreen.unlocked", value: "Wallet Unlocked", comment: "Wallet unlocked message") + static let disabled = Localization(key: "UnlockScreen.disabled", value: "Disabled until: %1$@", comment: "Disabled until date") + static let resetPin = Localization(key: "UnlockScreen.resetPin", value: "Reset PIN", comment: "Reset PIN with Paper Key button label.") + static let faceIdText = Localization(key: "UnlockScreen.faceIdText", value: "Unlock with FaceID", comment: "Unlock with FaceID accessibility label") + } + + enum Transaction { + static let justNow = Localization(key: "Transaction.justNow", value: "just now", comment: "Timestamp label for event that just happened") + static let invalid = Localization(key: "Transaction.invalid", value: "INVALID", comment: "Invalid transaction") + static let complete = Localization(key: "Transaction.complete", value: "Complete", comment: "Transaction complete label") + static let waiting = Localization(key: "Transaction.waiting", value: "Waiting to be confirmed. Some merchants require confirmation to complete a transaction. Estimated time: 1-2 hours.", comment: "Waiting to be confirmed string") + static let starting = Localization(key: "Transaction.starting", value: "Starting balance: %1$@", comment: "eg. Starting balance: $50.00") + static let fee = Localization(key: "Transaction.fee", value: "(%1$@ fee)", comment: "(b600 fee)") + static let ending = Localization(key: "Transaction.ending", value: "Ending balance: %1$@", comment: "eg. Ending balance: $50.00") + static let exchangeOnDaySent = Localization(key: "Transaction.exchangeOnDaySent", value: "Exchange rate when sent:", comment: "Exchange rate on date header") + static let exchangeOnDayReceived = Localization(key: "Transaction.exchangeOnDayReceived", value: "Exchange rate when received:", comment: "Exchange rate on date header") + static let receivedStatus = Localization(key: "Transaction.receivedStatus", value: "In progress: %1$@", comment: "Receive status text: 'In progress: 20%'") + static let sendingStatus = Localization(key: "Transaction.sendingStatus", value: "In progress: %1$@", comment: "Send status text: 'In progress: 20%'") + static let available = Localization(key: "Transaction.available", value: "Available to Spend", comment: "Availability status text") + static let txIDLabel = Localization(key: "Transaction.txIDLabel", value: "Transaction txID", comment: "Static TX iD Label") + static let amountDetailLabel = Localization(key: "Transaction.amountDetailLabel", value: "Transaction amount detail", comment: "Static amount Label") + static let startingAmountDetailLabel = Localization(key: "Transaction.startingAmountDetailLabel", value: "Transaction starting amount detail", comment: "Static starting amount Label") + static let endAmountDetailLabel = Localization(key: "Transaction.endAmountDetailLabel", value: "Transaction end amount detail", comment: "Static end amount Label") + static let blockHeightLabel = Localization(key: "Transaction.blockHeightLabel", value: "Transaction blockHeightLabel", comment: "Static blockHeight Label") + static let commentLabel = Localization(key: "Transaction.commentLabel", value: "Transaction comment label", comment: "Static comment Label") + } + + enum TransactionDetails { + static let title = Localization(key: "TransactionDetails.title", value: "Transaction Details", comment: "Transaction Details Title") + static let receiveModaltitle = Localization(key: "TransactionDetails.receivedModalTitle", value: "RECEIVE LTC", comment: "RECEIVE LTCTitle") + static let statusHeader = Localization(key: "TransactionDetails.statusHeader", value: "Status", comment: "Status section header") + static let commentsHeader = Localization(key: "TransactionDetails.commentsHeader", value: "Memo", comment: "Memo section header") + static let amountHeader = Localization(key: "TransactionDetails.amountHeader", value: "Amount", comment: "Amount section header") + static let emptyMessage = Localization(key: "TransactionDetails.emptyMessage", value: "Your transactions will appear here.", comment: "Empty transaction list message.") + static let txHashHeader = Localization(key: "TransactionDetails.txHashHeader", value: "Litecoin Transaction ID", comment: "Transaction ID header") + static let sentAmountDescription = Localization(key: "TransactionDetails.sentAmountDescription", value: "Sent %1@", comment: "Sent $5.00") + static let receivedAmountDescription = Localization(key: "TransactionDetails.receivedAmountDescription", value: "Received %1@", comment: "Received $5.00") + static let movedAmountDescription = Localization(key: "TransactionDetails.movedAmountDescription", value: "Moved %1@", comment: "Moved $5.00") + static let account = Localization(key: "TransactionDetails.account", value: "account", comment: "e.g. I received money from an account.") + static let sent = Localization(key: "TransactionDetails.sent", value: "Sent %1$@", comment: "Sent $5.00 (sent title 1/2)") + static let received = Localization(key: "TransactionDetails.received", value: "Received %1$@", comment: "Received $5.00 (received title 1/2)") + static let moved = Localization(key: "TransactionDetails.moved", value: "Moved %1$@", comment: "Moved $5.00") + static let to = Localization(key: "TransactionDetails.to", value: "to %1$@", comment: "[sent] to
(sent title 2/2)") + static let from = Localization(key: "TransactionDetails.from", value: "at %1$@", comment: "[received] at
(received title 2/2)") + static let blockHeightLabel = Localization(key: "TransactionDetails.blockHeightLabel", value: "Confirmed in Block", comment: "Block height label") + static let notConfirmedBlockHeightLabel = Localization(key: "TransactionDetails.notConfirmedBlockHeightLabel", value: "Not Confirmed", comment: "eg. Confirmed in Block: Not Confirmed") + static let staticTXIDLabel = Localization(key: "TransactionDetails.staticTXLabel", value: "TXID:", comment: "Label for TXID") + static let priceTimeStampLabel = Localization(key: "TransactionDetails.priceTimeStampPrefix", value: "as of", comment: "Prefix for price") + static let copyAllDetails = Localization(key: "TransactionDetails.copyAllDetails", value: "Copy all details", comment: "Copy all details") + static let copiedAll = Localization(key: "TransactionDetails.copiedAll", value: "Copied", comment: "Copied") + } + + // MARK: - Buy Center + + enum BuyCenter { + static let title = Localization(key: "BuyCenter.title", value: "Buy Litecoin", comment: "Buy Center Title") + static let buyModalTitle = Localization(key: "BuyCenter.ModalTitle", value: "Buy Łitecoin", comment: "Buy Modal Title") + enum Cells { + static let moonpayTitle = Localization(key: "BuyCenter.moonpayTitle", value: "Moonpay", comment: "Moonpay Title") + static let moonpayFinancialDetails = Localization(key: "BuyCenter.moonpayFinancialDetails", value: "• Point 1 XXXXX\n• Point 2 XXXXn• XXX Point 3", comment: "Moonpay buy financial details") + static let simplexTitle = Localization(key: "BuyCenter.simplexTitle", value: "Simplex", comment: "Simplex Title") + static let simplexFinancialDetails = Localization(key: "BuyCenter.simplexFinancialDetails", value: "• Get Litecoin in 5 mins!\n• Buy Litecoin via credit card\n• Passport or State ID", comment: "Simplex buy financial details") + static let changellyTitle = Localization(key: "BuyCenter.changellyTitle", value: "Changelly", comment: "Changelly Title") + static let changellyFinancialDetails = Localization(key: "BuyCenter.changellyFinancialDetails", value: "• Change Litecoin for other cryptos\n• No ID Required\n• Buy via credit card\n• Global coverage", comment: "Changelly buy financial details") + static let bitrefillTitle = Localization(key: "BuyCenter.BitrefillTitle", value: "Bitrefill", comment: "Bitrefill Title") + static let bitrefillFinancialDetails = Localization(key: "BuyCenter.bitrefillFinancialDetails", value: "• Buy gift cards\n• Refill prepaid phones\n• Steam, Amazon, Hotels.com\n• Works in 170 countries", comment: "Bitrefill buy financial details") + } + + static let barItemTitle = Localization(key: "BuyCenter.barItemTitle", value: "Buy", comment: "Buy Bar Item Title") + } + + // MARK: - Security Center + + enum SecurityCenter { + static let title = Localization(key: "SecurityCenter.title", value: "Security Center", comment: "Security Center Title") + static let info = Localization(key: "SecurityCenter.info", value: "Enable all security features for maximum protection.", comment: "Security Center Info") + enum Cells { + static let pinTitle = Localization(key: "SecurityCenter.pinTitle", value: "6-Digit PIN", comment: "PIN button title") + static let pinDescription = Localization(key: "SecurityCenter.pinDescription", value: "Protects your Litewallet from unauthorized users.", comment: "PIN button description") + static let touchIdTitle = Localization(key: "SecurityCenter.touchIdTitle", value: "Touch ID", comment: "Touch ID button title") + static let touchIdDescription = Localization(key: "SecurityCenter.touchIdDescription", value: "Conveniently unlock your Litewallet and send money up to a set limit.", comment: "Touch ID/FaceID button description") + static let paperKeyTitle = Localization(key: "SecurityCenter.paperKeyTitle", value: "Paper Key", comment: "Paper Key button title") + static let paperKeyDescription = Localization(key: "SecurityCenter.paperKeyDescription", value: "The only way to access your Litecoin if you lose or upgrade your phone.", comment: "Paper Key button description") + static let faceIdTitle = Localization(key: "SecurityCenter.faceIdTitle", value: "Face ID", comment: "Face ID button title") + } + } + + enum UpdatePin { + static let updateTitle = Localization(key: "UpdatePin.updateTitle", value: "Update PIN", comment: "Update PIN title") + static let createTitle = Localization(key: "UpdatePin.createTitle", value: "Set PIN", comment: "Update PIN title") + static let createTitleConfirm = Localization(key: "UpdatePin.createTitleConfirm", value: "Re-Enter PIN", comment: "Update PIN title") + static let createInstruction = Localization(key: "UpdatePin.createInstruction", value: "Your PIN will be used to unlock your Litewallet and send money.", comment: "PIN creation info.") + static let enterCurrent = Localization(key: "UpdatePin.enterCurrent", value: "Enter your current PIN.", comment: "Enter current PIN instruction") + static let enterNew = Localization(key: "UpdatePin.enterNew", value: "Enter your new PIN.", comment: "Enter new PIN instruction") + static let reEnterNew = Localization(key: "UpdatePin.reEnterNew", value: "Re-Enter your new PIN.", comment: "Re-Enter new PIN instruction") + static let caption = Localization(key: "UpdatePin.caption", value: "Remember this PIN. If you forget it, you won't be able to access your Litecoin.", comment: "Update PIN caption text") + static let setPinErrorTitle = Localization(key: "UpdatePin.setPinErrorTitle", value: "Update PIN Error", comment: "Update PIN failure alert view title") + static let setPinError = Localization(key: "UpdatePin.setPinError", value: "Sorry, could not update PIN.", comment: "Update PIN failure error message.") + } + + enum RecoverWallet { + static let next = Localization(key: "RecoverWallet.next", value: "Next", comment: "Next button label") + static let intro = Localization(key: "RecoverWallet.intro", value: "Recover your Litewallet with your paper key.", comment: "Recover wallet intro") + static let leftArrow = Localization(key: "RecoverWallet.leftArrow", value: "Left Arrow", comment: "Previous button accessibility label") + static let rightArrow = Localization(key: "RecoverWallet.rightArrow", value: "Right Arrow", comment: "Next button accessibility label") + static let done = Localization(key: "RecoverWallet.done", value: "Done", comment: "Done button text") + static let instruction = Localization(key: "RecoverWallet.instruction", value: "Enter Paper Key", comment: "Enter paper key instruction") + static let header = Localization(key: "RecoverWallet.header", value: "Recover Wallet", comment: "Recover wallet header") + static let subheader = Localization(key: "RecoverWallet.subheader", value: "Enter the paper key for the wallet you want to recover.", comment: "Recover wallet sub-header") + + static let headerResetPin = Localization(key: "RecoverWallet.header_reset_pin", value: "Reset PIN", comment: "Reset PIN with paper key: header") + static let subheaderResetPin = Localization(key: "RecoverWallet.subheader_reset_pin", value: "To reset your PIN, enter the words from your paper key into the boxes below.", comment: "Reset PIN with paper key: sub-header") + static let resetPinInfo = Localization(key: "RecoverWallet.reset_pin_more_info", value: "Tap here for more information.", comment: "Reset PIN with paper key: more information button.") + static let invalid = Localization(key: "RecoverWallet.invalid", value: "The paper key you entered is invalid. Please double-check each word and try again.", comment: "Invalid paper key message") + } + + enum ManageWallet { + static let title = Localization(key: "ManageWallet.title", value: "Manage Wallet", comment: "Manage wallet modal title") + static let textFieldLabel = Localization(key: "ManageWallet.textFeildLabel", value: "Wallet Name", comment: "Change Wallet name textfield label") + static let description = Localization(key: "ManageWallet.description", value: "Your wallet name only appears in your account transaction history and cannot be seen by anyone else.", comment: "Manage wallet description text") + static let creationDatePrefix = Localization(key: "ManageWallet.creationDatePrefix", value: "You created your wallet on %1$@", comment: "Wallet creation date prefix") + static let balance = Localization(key: "ManageWallet.balance", value: "Balance", comment: "Balance") + } + + enum AccountHeader { + static let defaultWalletName = Localization(key: "AccountHeader.defaultWalletName", value: "My Litewallet", comment: "Default wallet name") + static let manageButtonName = Localization(key: "AccountHeader.manageButtonName", value: "MANAGE", comment: "Manage wallet button title") + } + + enum VerifyPin { + static let title = Localization(key: "VerifyPin.title", value: "PIN Required", comment: "Verify PIN view title") + static let continueBody = Localization(key: "VerifyPin.continueBody", value: "Please enter your PIN to continue.", comment: "Verify PIN view body") + static let authorize = Localization(key: "VerifyPin.authorize", value: "Please enter your PIN to authorize this transaction.", comment: "Verify PIN for transaction view body") + static let touchIdMessage = Localization(key: "VerifyPin.touchIdMessage", value: "Authorize this transaction", comment: "Authorize transaction with touch id message") + } + + enum TouchIdSettings { + static let title = Localization(key: "TouchIdSettings.title", value: "Touch ID", comment: "Touch ID settings view title") + static let label = Localization(key: "TouchIdSettings.label", value: "Use your fingerprint to unlock your Litewallet and send money up to a set limit.", comment: "Touch Id screen label") + static let switchLabel = Localization(key: "TouchIdSettings.switchLabel", value: "Enable Touch ID for Litewallet", comment: "Touch id switch label.") + static let unavailableAlertTitle = Localization(key: "TouchIdSettings.unavailableAlertTitle", value: "Touch ID Not Set Up", comment: "Touch ID unavailable alert title") + static let unavailableAlertMessage = Localization(key: "TouchIdSettings.unavailableAlertMessage", value: "You have not set up Touch ID on this device. Go to Settings->Touch ID & Passcode to set it up now.", comment: "Touch ID unavailable alert message") + static let spendingLimit = Localization(key: "TouchIdSettings.spendingLimit", value: "Spending limit: %1$@ (%2$@)", comment: "Spending Limit: b100,000 ($100)") + static let limitValue = Localization(key: "TouchIdSettings.limitValue", value: "%1$@ (%2$@)", comment: " ł100,000 ($100)") + static let customizeText = Localization(key: "TouchIdSettings.customizeText", value: "You can customize your Touch ID spending limit from the %1$@.", comment: "You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button]") + static let linkText = Localization(key: "TouchIdSettings.linkText", value: "Touch ID Spending Limit Screen", comment: "Link Text (see TouchIdSettings.customizeText)") + } + + enum FaceIDSettings { + static let title = Localization(key: "FaceIDSettings.title", value: "Face ID", comment: "Face ID settings view title") + static let label = Localization(key: "FaceIDSettings.label", value: "Use your face to unlock your Litewallet and send money up to a set limit.", comment: "Face ID screen label") + static let switchLabel = Localization(key: "FaceIDSettings.switchLabel", value: "Enable Face ID for Litewallet", comment: "Face id switch label.") + static let unavailableAlertTitle = Localization(key: "FaceIDSettings.unavailableAlertTitle", value: "Face ID Not Set Up", comment: "Face ID unavailable alert title") + static let unavailableAlertMessage = Localization(key: "FaceIDSettings.unavailableAlertMessage", value: "You have not set up Face ID on this device. Go to Settings->Face ID & Passcode to set it up now.", comment: "Face ID unavailable alert message") + static let customizeText = Localization(key: "FaceIDSettings.customizeText", value: "You can customize your Face ID spending limit from the %1$@.", comment: "You can customize your Face ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button]") + static let linkText = Localization(key: "FaceIDSettings.linkText", value: "Face ID Spending Limit Screen", comment: "Link Text (see TouchIdSettings.customizeText)") + } + + enum SpendingLimit { + static let titleLabel = Localization(key: "SpendingLimit.title", value: "Current Spending Limit: ", comment: "Current spending limit:") + } + + enum TouchIdSpendingLimit { + static let title = Localization(key: "TouchIdSpendingLimit.title", value: "Touch ID Spending Limit", comment: "Touch Id spending limit screen title") + static let body = Localization(key: "TouchIdSpendingLimit.body", value: "You will be asked to enter your 6-digit PIN to send any transaction over your spending limit, and every 48 hours since the last time you entered your 6-digit PIN.", comment: "Touch ID spending limit screen body") + static let requirePasscode = Localization(key: "TouchIdSpendingLimit", value: "Always require passcode", comment: "Always require passcode option") + } + + enum FaceIdSpendingLimit { + static let title = Localization(key: "FaceIDSpendingLimit.title", value: "Face ID Spending Limit", comment: "Face Id spending limit screen title") + } + + // MARK: - Settings + + enum Settings { + static let title = Localization(key: "Settings.title", value: "Settings", comment: "Settings title") + static let wallet = Localization(key: "Settings.wallet", value: "Wallet", comment: "Wallet Settings section header") + static let manage = Localization(key: "Settings.manage", value: "Manage", comment: "Manage settings section header") + static let support = Localization(key: "Settings.support", value: "Support", comment: "Support settings section header") + static let blockchain = Localization(key: "Settings.blockchain", value: "Blockchain", comment: "Blockchain settings section header") + static let importTile = Localization(key: "Settings.importTitle", value: "Import Wallet", comment: "Import wallet label") + static let notifications = Localization(key: "Settings.notifications", value: "Notifications", comment: "Notifications label") + static let touchIdLimit = Localization(key: "Settings.touchIdLimit", value: "Touch ID Spending Limit", comment: "Touch ID spending limit label") + static let currency = Localization(key: "Settings.currency", value: "Display Currency", comment: "Default currency label") + static let sync = Localization(key: "Settings.sync", value: "Sync Blockchain", comment: "Sync blockchain label") + static let shareData = Localization(key: "Settings.shareData", value: "Share Anonymous Data", comment: "Share anonymous data label") + static let earlyAccess = Localization(key: "Settings.earlyAccess", value: "Join Early Access", comment: "Join Early access label") + static let about = Localization(key: "Settings.about", value: "About", comment: "About label") + static let review = Localization(key: "Settings.review", value: "Leave us a Review", comment: "Leave review button label") + static let enjoying = Localization(key: "Settings.enjoying", value: "Are you enjoying Litewallet?", comment: "Are you enjoying Litewallet alert message body") + static let wipe = Localization(key: "Settings.wipe", value: "Start/Recover Another Wallet", comment: "Start or recover another wallet menu label.") + static let advancedTitle = Localization(key: "Settings.advancedTitle", value: "Advanced Settings", comment: "Advanced Settings title") + static let faceIdLimit = Localization(key: "Settings.faceIdLimit", value: "Face ID Spending Limit", comment: "Face ID spending limit label") + static let languages = Localization(key: "Settings.languages", value: "Languages", comment: "Languages label") + static let litewalletVersion = Localization(key: "Settings.litewallet.version", value: "Litewallet Version:", comment: "Litewallet version") + static let litewalletEnvironment = Localization(key: "Settings.litewallet.environment", value: "Litewallet Environment:", comment: "Litewallet environment") + static let socialLinks = Localization(key: "Settings.socialLinks", value: "Social Links:", comment: "Litewallet Social links") + static let litewalletPartners = Localization(key: "Settings.litewallet.partners", value: "Litewallet Partners:", comment: "Litewallet Partners") + static let currentLocale = Localization(key: "Settings.currentLocale", value: "Current Locale:", comment: "Current Locale") + static let changeLanguageMessage = Localization(key: "Settings.ChangeLanguage.alertMessage", value: nil, comment: nil) + } + + enum About { + static let title = Localization(key: "About.title", value: "About", comment: "About screen title") + static let blog = Localization(key: "About.blog", value: "Website", comment: "About screen website label") + static let twitter = Localization(key: "About.twitter", value: "Twitter", comment: "About screen twitter label") + static let reddit = Localization(key: "About.reddit", value: "Reddit", comment: "About screen reddit label") + static let privacy = Localization(key: "About.privacy", value: "Privacy Policy", comment: "Privay Policy button label") + static let footer = Localization(key: "About.footer", value: "Made by the LiteWallet Team\nof the\nLitecoin Foundation\n%1$@", comment: "About screen footer") + } + + enum Notifications { + static let emailTitle = Localization(key: "Notifications.emailTitle", value: "Don't a miss a thing!", comment: "Email title") + static let pitchMessage = Localization(key: "Notifications.pitchMessage", value: "Sign up to hear about updates & contests from the Litewallet team!\nAccept notifications to get live news, price & market information!", comment: "Pitch to get user to sign up") + static let emailLabel = Localization(key: "Notifications.emailLabel", value: "Email address", comment: "Email address label") + static let emailPlaceholder = Localization(key: "Notifications.emailPlaceholder", value: "Enter here", comment: "Email address placeholder") + static let languagePreference = Localization(key: "Notifications.languagePreference", value: "Preferred language:", comment: "Language preference label") + static let signupCancel = Localization(key: "Notifications.signupCancel", value: "No, thanks", comment: "Signup cancel") + } + + enum DefaultCurrency { + static let rateLabel = Localization(key: "DefaultCurrency.rateLabel", value: "Exchange Rate", comment: "Exchange rate label") + static let bitcoinLabel = Localization(key: "DefaultCurrency.bitcoinLabel", value: "Litecoin Display Unit", comment: "Litecoin denomination picker label") + static let chooseFiatLabel = Localization(key: "DefaultCurrency.chooseFiatLabel", value: "Choose Fiat:", comment: "Label to pick fiat") + } + + enum SyncingView { + static let syncing = Localization(key: "SyncingView.syncing", value: "Syncing", comment: "Syncing view syncing state header text") + static let connecting = Localization(key: "SyncingView.connecting", value: "Connecting", comment: "Syncing view connectiong state header text") + } + + enum SyncingHeader { + static let syncing = Localization(key: "SyncingHeader.syncing", value: "Syncing...", comment: "Syncing view syncing state header text") + static let connecting = Localization(key: "SyncingHeader.connecting", value: "Connecting...", comment: "Syncing view connection state header text") + static let success = Localization(key: "SyncingHeader.success", value: "Success!", comment: "Syncing header success state header text") + static let rescanning = Localization(key: "SyncingHeader.rescan", value: "Rescanning...*", comment: "Rescanning header success state header text") + } + + enum ReScan { + static let header = Localization(key: "ReScan.header", value: "Sync Blockchain", comment: "Sync Blockchain view header") + static let subheader1 = Localization(key: "ReScan.subheader1", value: "Estimated time", comment: "Subheader label") + static let subheader2 = Localization(key: "ReScan.subheader2", value: "When to Sync?", comment: "Subheader label") + static let body1 = Localization(key: "ReScan.body1", value: "20-45 minutes", comment: "extimated time") + static let body2 = Localization(key: "ReScan.body2", value: "If a transaction shows as completed on the Litecoin network but not in your Litewallet.", comment: "Syncing explanation") + static let body3 = Localization(key: "ReScan.body3", value: "You repeatedly get an error saying your transaction was rejected.", comment: "Syncing explanation") + static let buttonTitle = Localization(key: "ReScan.buttonTitle", value: "Start Sync", comment: "Start Sync button label") + static let footer = Localization(key: "ReScan.footer", value: "You will not be able to send money while syncing with the blockchain.", comment: "Sync blockchain view footer") + static let alertTitle = Localization(key: "ReScan.alertTitle", value: "Sync with Blockchain?", comment: "Alert message title") + static let alertMessage = Localization(key: "ReScan.alertMessage", value: "You will not be able to send money while syncing.", comment: "Alert message body") + static let alertAction = Localization(key: "ReScan.alertAction", value: "Sync", comment: "Alert action button label") + } + + enum ShareData { + static let header = Localization(key: "ShareData.header", value: "Share Data?", comment: "Share data header") + static let body = Localization(key: "ShareData.body", value: "Help improve Litewallet by sharing your anonymous data with us. This does not include any financial information. We respect your financial privacy.", comment: "Share data view body") + static let toggleLabel = Localization(key: "ShareData.toggleLabel", value: "Share Anonymous Data?", comment: "Share data switch label.") + } + + enum ConfirmPaperPhrase { + static let word = Localization(key: "ConfirmPaperPhrase.word", value: "Word #%1$@", comment: "Word label eg. Word #1, Word #2") + static let label = Localization(key: "ConfirmPaperPhrase.label", value: "To make sure everything was written down correctly, please enter the following words from your paper key.", comment: "Confirm paper phrase view label.") + static let error = Localization(key: "ConfirmPaperPhrase.error", value: "The words entered do not match your paper key. Please try again.", comment: "Confirm paper phrase error message") + } + + enum StartPaperPhrase { + static let body = Localization(key: "StartPaperPhrase.body", value: "Your paper key is the only way to restore your Litewallet if your mobile is unavailable.\n No one in the Litecoin Foundation team can give this paper key to you!\n\nWe will show you a list of words to write down on a piece of paper and keep safe.\n\nPLEASE MAKE BACKUPS AND DON'T LOSE IT!", comment: "Paper key explanation text.") + static let buttonTitle = Localization(key: "StartPaperPhrase.buttonTitle", value: "Write Down Paper Key", comment: "button label") + static let againButtonTitle = Localization(key: "StartPaperPhrase.againButtonTitle", value: "Write Down Paper Key Again", comment: "button label") + static let date = Localization(key: "StartPaperPhrase.date", value: "You last wrote down your paper key on %1$@", comment: "Argument is date") + } + + enum WritePaperPhrase { + static let instruction = Localization(key: "WritePaperPhrase.instruction", value: "Write down each word in order and store it in a safe place.", comment: "Paper key instructions.") + static let step = Localization(key: "WritePaperPhrase.step", value: "%1$d of %2$d", comment: "1 of 3") + static let next = Localization(key: "WritePaperPhrase.next", value: "Next", comment: "button label") + static let previous = Localization(key: "WritePaperPhrase.previous", value: "Previous", comment: "button label") + } + + enum TransactionDirection { + static let to = Localization(key: "TransactionDirection.to", value: "Sent to this Address", comment: "(this transaction was) Sent to this address:") + static let received = Localization(key: "TransactionDirection.address", value: "Received at this Address", comment: "(this transaction was) Received at this address:") + } + + enum RequestAnAmount { + static let title = Localization(key: "RequestAnAmount.title", value: "Request an Amount", comment: "Request a specific amount of Litecoin") + static let noAmount = Localization(key: "RequestAnAmount.noAmount", value: "Please enter an amount first.", comment: "No amount entered error message.") + } + + // MARK: - Security Alerts + + enum SecurityAlerts { + static let pinSet = Localization(key: "Alerts.pinSet", value: "PIN Set", comment: "Alert Header label (the PIN was set)") + static let paperKeySet = Localization(key: "Alerts.paperKeySet", value: "Paper Key Set", comment: "Alert Header Label (the paper key was set)") + static let sendSuccess = Localization(key: "Alerts.sendSuccess", value: "Send Confirmation", comment: "Send success alert header label (confirmation that the send happened)") + static let resolvedSuccess = Localization(key: "Alerts.resolvedSuccess", value: "Resolved Success", comment: "Resolved Success") + static let resolvedSuccessSubheader = Localization(key: "Alerts.resolvedSuccessSubheader", value: "Resolved", comment: "Resolved Success subheader") + static let sendFailure = Localization(key: "Alerts.sendFailure", value: "Send failed", comment: "Send failure alert header label (the send failed to happen)") + static let paperKeySetSubheader = Localization(key: "Alerts.paperKeySetSubheader", value: "Awesome!", comment: "Alert Subheader label (playfully positive)") + static let sendSuccessSubheader = Localization(key: "Alerts.sendSuccessSubheader", value: "Money Sent!", comment: "Send success alert subheader label (e.g. the money was sent)") + static let copiedAddressesHeader = Localization(key: "Alerts.copiedAddressesHeader", value: "Addresses Copied", comment: "'the addresses were copied'' Alert title") + static let copiedAddressesSubheader = Localization(key: "Alerts.copiedAddressesSubheader", value: "All wallet addresses successfully copied.", comment: "Addresses Copied Alert sub header") + } + + enum MenuButton { + static let security = Localization(key: "MenuButton.security", value: "Security Center", comment: "Menu button title") + static let support = Localization(key: "MenuButton.customer.support", value: "Customer support", comment: "Menu button title") + static let settings = Localization(key: "MenuButton.settings", value: "Settings", comment: "Menu button title") + static let lock = Localization(key: "MenuButton.lock", value: "Lock Wallet", comment: "Menu button title") + static let buy = Localization(key: "MenuButton.buy", value: "Buy Litecoin", comment: "Buy Litecoin title") + } + + enum MenuViewController { + static let modalTitle = Localization(key: "MenuViewController.modalTitle", value: "Menu", comment: "Menu modal title") + } + + enum StartViewController { + static let createButton = Localization(key: "MenuViewController.createButton", value: "Create New Wallet", comment: "button label") + static let recoverButton = Localization(key: "MenuViewController.recoverButton", value: "Recover Wallet", comment: "button label") + static let tagline = Localization(key: "StartViewController.tagline", value: "The most secure and safest way to use Litecoin.", comment: "Start view message") + static let continueButton = Localization(key: "StartViewController.continueButton", value: "Continue", comment: "Continue button label") + } + + enum AccessibilityLabels { + static let close = Localization(key: "AccessibilityLabels.close", value: "Close", comment: "Close modal button accessibility label") + static let faq = Localization(key: "AccessibilityLabels.faq", value: "Support Center", comment: "Support center accessibiliy label") + } + + enum Search { + static let sent = Localization(key: "Search.sent", value: "sent", comment: "Sent filter label") + static let received = Localization(key: "Search.received", value: "received", comment: "Received filter label") + static let pending = Localization(key: "Search.pending", value: "pending", comment: "Pending filter label") + static let complete = Localization(key: "Search.complete", value: "complete", comment: "Complete filter label") + } + + enum Prompts { + static let affirm = Localization(key: "Prompts.PaperKey.affirm", value: "Continue", comment: "Affirm button title.") + static let cancel = Localization(key: "Prompts.PaperKey.cancel", value: "Cancel", comment: "Cancel button.") + static let enable = Localization(key: "Prompts.PaperKey.enable", value: "Enable", comment: "Enable button.") + static let dismiss = Localization(key: "Prompts.dismiss", value: "**Dismiss", comment: "Dismiss button.") + enum TouchId { + static let title = Localization(key: "Prompts.TouchId.title", value: "Enable Touch ID", comment: "Enable touch ID prompt title") + static let body = Localization(key: "Prompts.TouchId.body", value: "Tap here to enable Touch ID", comment: "Enable touch ID prompt body") + } + + enum PaperKey { + static let title = Localization(key: "Prompts.PaperKey.title", value: "Action Required", comment: "An action is required (You must do this action).") + static let body = Localization(key: "Prompts.PaperKey.body", value: "Your Paper Key must be kept in a safe place. It is the only way modify or restore your Litewallet or transfer your Litecoin. Please write it down.", comment: "Warning about paper key.") + } + + enum SetPin { + static let title = Localization(key: "Prompts.SetPin.title", value: "Set PIN", comment: "Set PIN prompt title.") + static let body = Localization(key: "Prompts.SetPin.body", value: "Litewallet requires a 6-digit PIN. Please set and store your PIN in a safe place.", comment: "Upgrade PIN prompt body.") + } + + enum RecommendRescan { + static let title = Localization(key: "Prompts.RecommendRescan.title", value: "Transaction Rejected", comment: "Transaction rejected prompt title") + static let body = Localization(key: "Prompts.RecommendRescan.body", value: "Your wallet may be out of sync. This can often be fixed by rescanning the blockchain.", comment: "Transaction rejected prompt body") + } + + enum NoPasscode { + static let title = Localization(key: "Prompts.NoPasscode.title", value: "Turn device passcode on", comment: "No Passcode set warning title") + static let body = Localization(key: "Prompts.NoPasscode.body", value: "A device passcode is needed to safeguard your wallet.", comment: "No passcode set warning body") + } + + enum ShareData { + static let title = Localization(key: "Prompts.ShareData.title", value: "Share Anonymous Data", comment: "Share data prompt title") + static let body = Localization(key: "Prompts.ShareData.body", value: "Help improve Litewallet by sharing your anonymous data with us", comment: "Share data prompt body") + } + + enum FaceId { + static let title = Localization(key: "Prompts.FaceId.title", value: "Enable Face ID", comment: "Enable face ID prompt title") + static let body = Localization(key: "Prompts.FaceId.body", value: "Tap here to enable Face ID", comment: "Enable face ID prompt body") + } + } + + // MARK: - Payment Protocol + + enum PaymentProtocol { + enum Errors { + static let untrustedCertificate = Localization(key: "PaymentProtocol.Errors.untrustedCertificate", value: "untrusted certificate", comment: "Untrusted certificate payment protocol error message") + static let missingCertificate = Localization(key: "PaymentProtocol.Errors.missingCertificate", value: "missing certificate", comment: "Missing certificate payment protocol error message") + static let unsupportedSignatureType = Localization(key: "PaymentProtocol.Errors.unsupportedSignatureType", value: "unsupported signature type", comment: "Unsupported signature type payment protocol error message") + static let requestExpired = Localization(key: "PaymentProtocol.Errors.requestExpired", value: "request expired", comment: "Request expired payment protocol error message") + static let badPaymentRequest = Localization(key: "PaymentProtocol.Errors.badPaymentRequest", value: "Bad Payment Request", comment: "Bad Payment request alert title") + static let smallOutputErrorTitle = Localization(key: "PaymentProtocol.Errors.smallOutputError", value: "Couldn't make payment", comment: "Payment too small alert title") + static let smallPayment = Localization(key: "PaymentProtocol.Errors.smallPayment", value: "Litecoin payments can't be less than %1$@.", comment: "Amount too small error message") + static let smallTransaction = Localization(key: "PaymentProtocol.Errors.smallTransaction", value: "Litecoin transaction outputs can't be less than $@.", comment: "Output too small error message.") + static let corruptedDocument = Localization(key: "PaymentProtocol.Errors.corruptedDocument", value: "Unsupported or corrupted document", comment: "Error opening payment protocol file message") + } + } + + enum URLHandling { + static let addressListAlertTitle = Localization(key: "URLHandling.addressListAlertTitle", value: "Copy Wallet Addresses", comment: "Authorize to copy wallet address alert title") + static let addressListAlertMessage = Localization(key: "URLHandling.addressaddressListAlertMessage", value: "Copy wallet addresses to clipboard?", comment: "Authorize to copy wallet addresses alert message") + static let addressListVerifyPrompt = Localization(key: "URLHandling.addressList", value: "Authorize to copy wallet address to clipboard", comment: "Authorize to copy wallet address PIN view prompt.") + static let copy = Localization(key: "URLHandling.copy", value: "Copy", comment: "Copy wallet addresses alert button label") + } + + enum ApiClient { + static let notReady = Localization(key: "ApiClient.notReady", value: "Wallet not ready", comment: "Wallet not ready error message") + static let jsonError = Localization(key: "ApiClient.jsonError", value: "JSON Serialization Error", comment: "JSON Serialization error message") + static let tokenError = Localization(key: "ApiClient.tokenError", value: "Unable to retrieve API token", comment: "API Token error message") + } + + enum CameraPlugin { + static let centerInstruction = Localization(key: "CameraPlugin.centerInstruction", value: "Center your ID in the box", comment: "Camera plugin instruction") + } + + enum LocationPlugin { + static let disabled = Localization(key: "LocationPlugin.disabled", value: "Location services are disabled.", comment: "Location services disabled error") + static let notAuthorized = Localization(key: "LocationPlugin.notAuthorized", value: "Litewallet does not have permission to access location services.", comment: "No permissions for location services") + } + + enum Webview { + static let updating = Localization(key: "Webview.updating", value: "Updating...", comment: "Updating webview message") + static let errorMessage = Localization(key: "Webview.errorMessage", value: "There was an error loading the content. Please try again.", comment: "Webview loading error message") + static let dismiss = Localization(key: "Webview.dismiss", value: "Dismiss", comment: "Dismiss button label") + } + + enum TimeSince { + static let seconds = Localization(key: "TimeSince.seconds", value: "%1$@ s", comment: "6 s (6 seconds)") + static let minutes = Localization(key: "TimeSince.minutes", value: "%1$@ m", comment: "6 m (6 minutes)") + static let hours = Localization(key: "TimeSince.hours", value: "%1$@ h", comment: "6 h (6 hours)") + static let days = Localization(key: "TimeSince.days", value: "%1$@ d", comment: "6 d (6 days)") + } + + enum Import { + static let leftCaption = Localization(key: "Import.leftCaption", value: "Wallet to be imported", comment: "Caption for graphics") + static let rightCaption = Localization(key: "Import.rightCaption", value: "Your Litewallet Wallet", comment: "Caption for graphics") + static let importMessage = Localization(key: "Import.message", value: "Importing a wallet transfers all the money from your other wallet into your Litewallet wallet using a single transaction.", comment: "Import wallet intro screen message") + static let importWarning = Localization(key: "Import.warning", value: "Importing a wallet does not include transaction history or other details.", comment: "Import wallet intro warning message") + static let scan = Localization(key: "Import.scan", value: "Scan Private Key", comment: "Scan Private key button label") + static let title = Localization(key: "Import.title", value: "Import Wallet", comment: "Import Wallet screen title") + static let importing = Localization(key: "Import.importing", value: "Importing Wallet", comment: "Importing wallet progress view label") + static let confirm = Localization(key: "Import.confirm", value: "Send %1$@ from this private key into your wallet? The Litecoin network will receive a fee of %2$@.", comment: "Sweep private key confirmation message") + static let checking = Localization(key: "Import.checking", value: "Checking private key balance...", comment: "Checking private key balance progress view text") + static let password = Localization(key: "Import.password", value: "This private key is password protected.", comment: "Enter password alert view title") + static let passwordPlaceholder = Localization(key: "Import.passwordPlaceholder", value: "password", comment: "password textfield placeholder") + static let unlockingActivity = Localization(key: "Import.unlockingActivity", value: "Unlocking Key", comment: "Unlocking Private key activity view message.") + static let importButton = Localization(key: "Import.importButton", value: "Import", comment: "Import button label") + static let success = Localization(key: "Import.success", value: "Success", comment: "Import wallet success alert title") + static let successBody = Localization(key: "Import.SuccessBody", value: "Successfully imported wallet.", comment: "Successfully imported wallet message body") + static let wrongPassword = Localization(key: "Import.wrongPassword", value: "Wrong password, please try again.", comment: "Wrong password alert message") + enum Error { + static let notValid = Localization(key: "Import.Error.notValid", value: "Not a valid private key", comment: "Not a valid private key error message") + static let duplicate = Localization(key: "Import.Error.duplicate", value: "This private key is already in your wallet.", comment: "Duplicate key error message") + static let empty = Localization(key: "Import.Error.empty", value: "This private key is empty.", comment: "empty private key error message") + static let highFees = Localization(key: "Import.Error.highFees", value: "Transaction fees would cost more than the funds available on this private key.", comment: "High fees error message") + static let signing = Localization(key: "Import.Error.signing", value: "Error signing transaction", comment: "Import signing error message") + } + } + + enum WipeWallet { + static let title = Localization(key: "WipeWallet.title", value: "Start or Recover Another Wallet", comment: "Wipe wallet navigation item title.") + static let alertTitle = Localization(key: "WipeWallet.alertTitle", value: "Wipe Wallet?", comment: "Wipe wallet alert title") + static let alertMessage = Localization(key: "WipeWallet.alertMessage", value: "Are you sure you want to delete this wallet?", comment: "Wipe wallet alert message") + static let wipe = Localization(key: "WipeWallet.wipe", value: "Wipe", comment: "Wipe wallet button title") + static let wiping = Localization(key: "WipeWallet.wiping", value: "Wiping...", comment: "Wiping activity message") + static let failedTitle = Localization(key: "WipeWallet.failedTitle", value: "Failed", comment: "Failed wipe wallet alert title") + static let failedMessage = Localization(key: "WipeWallet.failedMessage", value: "Failed to wipe wallet.", comment: "Failed wipe wallet alert message") + static let instruction = Localization(key: "WipeWallet.instruction", value: "To start a new wallet or restore an existing wallet, you must first erase the wallet that is currently installed. To continue, enter the current wallet's Paper Key.", comment: "Enter key to wipe wallet instruction.") + static let startMessage = Localization(key: "WipeWallet.startMessage", value: "Starting or recovering another wallet allows you to access and manage a different Litewallet wallet on this device.", comment: "Start wipe wallet view message") + static let startWarning = Localization(key: "WipeWallet.startWarning", value: "Your current wallet will be removed from this device. If you wish to restore it in the future, you will need to enter your Paper Key.", comment: "Start wipe wallet view warning") + static let emptyWallet = Localization(key: "WipeWallet.emptyWallet", value: "Forget seed or PIN?", comment: "Warning if user lost phrase") + static let resetTitle = Localization(key: "resetTitle", value: " Delete my Litewallet ", comment: "Warning Empty Wipe title") + static let resetButton = Localization(key: "resetButton", value: "Yes, reset wallet", comment: "Reset walet button title") + static let warningTitle = Localization(key: "WipeWallet.warningTitle", value: "PLEASE READ!", comment: "Warning title") + static let warningDescription = Localization(key: "WipeWallet.warningDescription", value: "Your LiteWallet is empty. Resetting will delete the old private key and wipe the app data.\n\nAfter the reset, be prepared to record the new 12 words and keep them in a very secure place.\n\nNo LiteWallet developers can retrieve this seed for you.", comment: "Warning description") + static let warningAlert = Localization(key: "WipeWallet.warningAlert", value: "DO NOT LOSE IT!", comment: "Warning Alert") + static let deleteDatabase = Localization(key: "WipeWallet.deleteDatabase", value: "Delete database", comment: "Delete db") + static let alertDeleteTitle = Localization(key: "WipeWallet.alertDeleteTitle", value: "Delet Database", comment: "Delete database title") + static let deleteMessageTitle = Localization(key: "WipeWallet.deleteMessageTitle", value: "This deletes the database but retains the PIN and phrase. You will be asked to confirm your existing PIN, seed and will re-sync the new db", comment: "Delete database message") + static let deleteSync = Localization(key: "WipeWallet.deleteSync", value: "Delete & Sync", comment: "Delete and sync") + } + + enum FeeSelector { + static let title = Localization(key: "FeeSelector.title", value: "Processing Speed", comment: "Fee Selector title") + static let regularLabel = Localization(key: "FeeSelector.regularLabel", value: "Estimated Delivery: 2.5 - 5+ minutes", comment: "Fee Selector regular fee description") + static let economyLabel = Localization(key: "FeeSelector.economyLabel", value: "Estimated Delivery: ~10 minutes", comment: "Fee Selector economy fee description") + static let luxuryLabel = Localization(key: "FeeSelector.luxuryLabel", value: "Delivery: 2.5 - 5+ minutes", comment: "Fee Selector luxury fee description") + static let economyWarning = Localization(key: "FeeSelector.economyWarning", value: "This option is not recommended for time-sensitive transactions.", comment: "Warning message for economy fee") + static let luxuryMessage = Localization(key: "FeeSelector.luxuryMessage", value: "This option virtually guarantees acceptance of your transaction while you pay a premium.", comment: "Message for luxury fee") + + static let regular = Localization(key: "FeeSelector.regular", value: "Regular", comment: "Regular fee") + static let economy = Localization(key: "FeeSelector.economy", value: "Economy", comment: "Economy fee") + static let luxury = Localization(key: "FeeSelector.luxury", value: "Luxury", comment: "Luxury fee") + } + + enum Confirmation { + static let title = Localization(key: "Confirmation.title", value: "Confirmation", comment: "Confirmation Screen title") + static let send = Localization(key: "Confirmation.send", value: "Send", comment: "Send: (amount)") + static let to = Localization(key: "Confirmation.to", value: "To", comment: "To: (address)") + static let staticAddressLabel = Localization(key: "Confirmation.staticAddressLabel", value: "ADDRESS:", comment: "Address label") + + static let processingTime = Localization(key: "Confirmation.processingTime", value: "Processing time: This transaction will take %1$@ minutes to process.", comment: "eg. Processing time: This transaction will take 10-30 minutes to process.") + static let processingAndDonationTime = Localization(key: "Confirmation.processingAndDonationTime", value: "Processing time: These transactions will take %1$@ minutes to process.", comment: "eg. Processing with Donation time: This transaction will take 10-30 minutes to process.") + static let amountLabel = Localization(key: "Confirmation.amountLabel", value: "Amount to Send:", comment: "Amount to Send: ($1.00)") + static let donateLabel = Localization(key: "Confirmation.donateLabel", value: "Amount to Donate:", comment: "Amount to Donate: ($1.00)") + + static let totalLabel = Localization(key: "Confirmation.totalLabel", value: "Total Cost:", comment: "Total Cost: ($5.00)") + static let amountDetailLabel = Localization(key: "Confirmation.amountDetailLabel", value: "Exchange details:", comment: "$53.09/L + 1.07%") + } + + enum NodeSelector { + static let manualButton = Localization(key: "NodeSelector.manualButton", value: "Switch to Manual Mode", comment: "Switch to manual mode button label") + static let automaticButton = Localization(key: "NodeSelector.automaticButton", value: "Switch to Automatic Mode", comment: "Switch to automatic mode button label") + static let title = Localization(key: "NodeSelector.title", value: "Litecoin Nodes", comment: "Node Selector view title") + static let nodeLabel = Localization(key: "NodeSelector.nodeLabel", value: "Current Primary Node", comment: "Node address label") + static let statusLabel = Localization(key: "NodeSelector.statusLabel", value: "Node Connection Status", comment: "Node status label") + static let connected = Localization(key: "NodeSelector.connected", value: "Connected", comment: "Node is connected label") + static let notConnected = Localization(key: "NodeSelector.notConnected", value: "Not Connected", comment: "Node is not connected label") + static let enterTitle = Localization(key: "NodeSelector.enterTitle", value: "Enter Node", comment: "Enter Node ip address view title") + static let enterBody = Localization(key: "NodeSelector.enterBody", value: "Enter Node IP address and port (optional)", comment: "Enter node ip address view body") + } + + enum Welcome { + static let title = Localization(key: "Welcome.title", value: "Welcome to Litewallet", comment: "Welcome view title") + static let body = Localization(key: "Welcome.body", value: "Litewallet now has a brand new look and some new features.\n\nAll coins are displayed in lites (ł). 1 Litecoin (Ł) = 1000 lites (ł).", comment: "Welcome view body text") + } + + enum Fragments { + static let or = Localization(key: "Fragment.or", value: "or", comment: "Or") + static let confirm = Localization(key: "Fragment.confirm", value: "confirm", comment: "Confirm") + static let to = Localization(key: "Fragment.to", value: "to", comment: "to") + static let sorry = Localization(key: "Fragment.sorry", value: "sorry", comment: "sorry") + } +} diff --git a/litewallet/Strings/Base.lproj/Localizable.strings b/litewallet/Strings/Base.lproj/Localizable.strings new file mode 100644 index 000000000..d07f2e339 --- /dev/null +++ b/litewallet/Strings/Base.lproj/Localizable.strings @@ -0,0 +1,1478 @@ +/* About screen website label */ +"About.blog" = "Website"; + +/* About screen footer */ +"About.footer" = "Made by the LiteWallet Team\nof the\nLitecoin Foundation\n%1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Privacy Policy"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "About"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Close"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Support Center"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Loading Wallet"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "My Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "MANAGE"; + +/* Error alert title */ +"Alert.error" = "Error"; + +/* No internet alert message */ +"Alert.noInternet" = "No internet connection found. Check your connection and try again."; + +/* Warning alert title */ +"Alert.warning" = "Warning"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Addresses Copied"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "All wallet addresses successfully copied."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Paper Key Set"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Awesome!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN Set"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Send failed"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Send Confirmation"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Money Sent!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON Serialization Error"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Wallet not ready"; + +/* API Token error message */ +"ApiClient.tokenError" = "Unable to retrieve API token"; + +/* buy button */ +"Button.buy" = "buy"; + +/* Cancel button label */ +"Button.cancel" = "Cancel"; + +/* Ignore button label */ +"Button.ignore" = "Ignore"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "No"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "receive"; + +/* send button */ +"Button.send" = "send"; + +/* Settings button label */ +"Button.settings" = "Settings"; + +/* Settings button label */ +"Button.submit" = "Submit"; + +/* Yes button */ +"Button.yes" = "Yes"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Buy gift cards\n• Refill prepaid phones\n• Steam, Amazon, Hotels.com\n• Works in 170 countries"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Change Litecoin for other cryptos\n• No ID Required\n• Buy via credit card\n• Global coverage"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Buy Łitecoin"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Get Litecoin in 5 mins!\n• Buy Litecoin via credit card\n• Passport or State ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Buy Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Center your ID in the box"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Amount to Send:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Amount to Donate:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Processing time: These transactions will take %1$@ minutes to process."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Processing time: This transaction will take %1$@ minutes to process."; + +/* Send: (amount) */ +"Confirmation.send" = "Send"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Confirmation"; + +/* To: (address) */ +"Confirmation.to" = "To"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Total Cost:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "The words entered do not match your paper key. Please try again."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "To make sure everything was written down correctly, please enter the following words from your paper key."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Word #%1$@"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin Display Unit"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Exchange Rate"; + +/* Donate Memo */ +"Donate.memo" = "Donation to the Litecoin Foundation"; + +/* Donate to the Litecoin Foundation */ +"Donate.title" = "Donate to the Litecoin Foundation (%@)"; + +/* Donate Confirmation */ +"Donate.title.confirmation" = "Confirm Donation"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "This device isn't configured to send email with the iOS mail app."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "Email Unavailable"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "This device isn't configured to send messages."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Messaging Unavailable"; + +/* You can customize your Face ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "You can customize your Face ID spending limit from the %1$@."; + +/* Face ID screen label */ +"FaceIDSettings.label" = "Use your face to unlock your Litewallet and send money up to a set limit."; + +/* Link Text (see TouchIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "Face ID Spending Limit Screen"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "Enable Face ID for Litewallet"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "Face ID"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "You have not set up Face ID on this device. Go to Settings->Face ID & Passcode to set it up now."; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "Face ID Not Set Up"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "Face ID Spending Limit"; + +/* Economy fee */ +"FeeSelector.economy" = "Economy"; + +/* Fee Selector economy fee description */ +"FeeSelector.economyLabel" = "Estimated Delivery: 10+ minutes"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "This option is not recommended for time-sensitive transactions."; + +/* Regular fee */ +"FeeSelector.regular" = "Regular"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Estimated Delivery: 2.5 - 5+ minutes"; + +/* Fee Selector title */ +"FeeSelector.title" = "Processing Speed"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Checking private key balance..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Send %1$@ from this private key into your wallet? The Litecoin network will receive a fee of %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "This private key is already in your wallet."; + +/* empty private key error message */ +"Import.Error.empty" = "This private key is empty."; + +/* High fees error message */ +"Import.Error.highFees" = "Transaction fees would cost more than the funds available on this private key."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Not a valid private key"; + +/* Import signing error message */ +"Import.Error.signing" = "Error signing transaction"; + +/* Import button label */ +"Import.importButton" = "Import"; + +/* Importing wallet progress view label */ +"Import.importing" = "Importing Wallet"; + +/* Caption for graphics */ +"Import.leftCaption" = "Wallet to be imported"; + +/* Import wallet intro screen message */ +"Import.message" = "Importing a wallet transfers all the money from your other wallet into your Litewallet wallet using a single transaction."; + +/* Enter password alert view title */ +"Import.password" = "This private key is password protected."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "password"; + +/* Caption for graphics */ +"Import.rightCaption" = "Your Litewallet Wallet"; + +/* Scan Private key button label */ +"Import.scan" = "Scan Private Key"; + +/* Import wallet success alert title */ +"Import.success" = "Success"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Successfully imported wallet."; + +/* Import Wallet screen title */ +"Import.title" = "Import Wallet"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Unlocking Key"; + +/* Import wallet intro warning message */ +"Import.warning" = "Importing a wallet does not include transaction history or other details."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Wrong password, please try again."; + +/* Close app button */ +"JailbreakWarnings.close" = "Close"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignore"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin! Wipe this wallet immediately and restore on a secure device."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin. Please only use Litewallet on a non-jailbroken device."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "WARNING"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Wipe"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Location services are disabled."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet does not have permission to access location services."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Malformed URI"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "You created your wallet on %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Your wallet name only appears in your account transaction history and cannot be seen by anyone else."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Wallet Name"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Manage Wallet"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Buy Litecoin"; + +/* Menu button title */ +"MenuButton.lock" = "Lock Wallet"; + +/* Menu button title */ +"MenuButton.security" = "Security Center"; + +/* Menu button title */ +"MenuButton.settings" = "Settings"; + +/* Menu button title */ +"MenuButton.support" = "Support"; + +/* button label */ +"MenuViewController.createButton" = "Create New Wallet"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Recover Wallet"; + +/* No wallet. */ +"No wallet" = "No wallet"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Switch to Automatic Mode"; + +/* Node is connected label */ +"NodeSelector.connected" = "Connected"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Enter Node IP address and port (optional)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Enter Node"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Switch to Manual Mode"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Current Primary Node"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Not Connected"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Node Connection Status"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin Nodes"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Bad Payment Request"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Unsupported or corrupted document"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "missing certificate"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "request expired"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Couldn't make payment"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Litecoin payments can't be less than %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Litecoin transaction outputs can't be less than $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "unsupported signature type"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "untrusted certificate"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "Tap here to enable Face ID"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "Enable Face ID"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "A device passcode is needed to safeguard your wallet."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Turn device passcode on"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Your Paper Key must be kept in a safe place. It is the only way modify or restore your Litewallet or transfer your Litecoin. Please write it down."; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Action Required"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Your wallet may be out of sync. This can often be fixed by rescanning the blockchain."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transaction Rejected"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Help improve Litewallet by sharing your anonymous data with us"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Share Anonymous Data"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Tap here to enable Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Enable Touch ID"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet requires a 6-digit PIN. Please set and store your PIN in a safe place."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Set PIN"; + +/* Address copied message. */ +"Receive.copied" = "Copied to clipboard."; + +/* Share via email button label */ +"Receive.emailButton" = "Email"; + +/* Request button label */ +"Receive.request" = "Request an Amount"; + +/* Share button label */ +"Receive.share" = "Share"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Text Message"; + +/* Receive modal title */ +"Receive.title" = "Receive"; + +/* Done button text */ +"RecoverWallet.done" = "Done"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Recover Wallet"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Reset PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Enter Paper Key"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Recover your Litewallet with your paper key."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "The paper key you entered is invalid. Please double-check each word and try again."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Left Arrow"; + +/* Next button label */ +"RecoverWallet.next" = "Next"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Tap here for more information."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Right Arrow"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Enter the paper key for the wallet you want to recover."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "To reset your PIN, enter the words from your paper key into the boxes below."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Please enter an amount first."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Request an Amount"; + +/* Alert action button label */ +"ReScan.alertAction" = "Sync"; + +/* Alert message body */ +"ReScan.alertMessage" = "You will not be able to send money while syncing."; + +/* Alert message title */ +"ReScan.alertTitle" = "Sync with Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minutes"; + +/* Syncing explanation */ +"ReScan.body2" = "If a transaction shows as completed on the Litecoin network but not in your Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "You repeatedly get an error saying your transaction was rejected."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Start Sync"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "You will not be able to send money while syncing with the blockchain."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Sync Blockchain"; + +/* Subheader label */ +"ReScan.subheader1" = "Estimated time"; + +/* Subheader label */ +"ReScan.subheader2" = "When to Sync?"; + +/* Reset walet button title */ +"resetButton" = "Yes, reset wallet"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Delete my Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Camera Flash"; + +/* Complete filter label */ +"Search.complete" = "complete"; + +/* Pending filter label */ +"Search.pending" = "pending"; + +/* Received filter label */ +"Search.received" = "received"; + +/* Sent filter label */ +"Search.sent" = "sent"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "Face ID"; + +/* Security Center Info */ +"SecurityCenter.info" = "Enable all security features for maximum protection."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "The only way to access your Litecoin if you lose or upgrade your phone."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Paper Key"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Protects your Litewallet from unauthorized users."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-Digit PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "Security Center"; + +/* Touch ID/FaceID button description */ +"SecurityCenter.touchIdDescription" = "Conveniently unlock your Litewallet and send money up to a set limit."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Amount"; + +/* Balance: $4.00 */ +"Send.balance" = "Balance: %1$@"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Go to Settings to allow camera access."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet is not allowed to access the camera"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "The destination is your own address. You cannot send to yourself."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Could not create transaction."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Memo"; + +/* Empty pasteboard error message */ +"Send.emptyPasteboard" = "Pasteboard is empty"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Payee identity isn't certified."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Insufficient Funds"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "The destination address is not a valid Litecoin address."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Pasteboard does not contain a valid Litecoin address."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Invalid Address"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Sending is disabled during a full rescan."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Loading Request"; + +/* Empty address alert message */ +"Send.noAddress" = "Please enter the recipient's address."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Please enter an amount to send."; + +/* Paste button label */ +"Send.pasteLabel" = "Paste"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Could not publish transaction."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Could not load payment request"; + +/* Scan button label */ +"Send.scanLabel" = "Scan"; + +/* Send button label */ +"Send.sendLabel" = "Send"; + +/* Send modal title */ +"Send.title" = "Send"; + +/* Send money to label */ +"Send.toLabel" = "To"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin addresses are intended for single use only."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Re-use reduces privacy for both you and the recipient and can result in loss if the recipient doesn't directly control the address."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Address Already Used"; + +/* About label */ +"Settings.about" = "About"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Advanced Settings"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Blockchain"; + +/* Default currency label */ +"Settings.currency" = "Display Currency"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Join Early Access"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Are you enjoying Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "Face ID Spending Limit"; + +/* Import wallet label */ +"Settings.importTitle" = "Import Wallet"; + +/* Manage settings section header */ +"Settings.manage" = "Manage"; + +/* Notifications label */ +"Settings.notifications" = "Notifications"; + +/* Leave review button label */ +"Settings.review" = "Leave us a Review"; + +/* Share anonymous data label */ +"Settings.shareData" = "Share Anonymous Data"; + +/* Support settings section header */ +"Settings.support" = "Support"; + +/* Sync blockchain label */ +"Settings.sync" = "Sync Blockchain"; + +/* Settings title */ +"Settings.title" = "Settings"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch ID Spending Limit"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Wallet"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Start/Recover Another Wallet"; + +/* Share data view body */ +"ShareData.body" = "Help improve Litewallet by sharing your anonymous data with us. This does not include any financial information. We respect your financial privacy."; + +/* Share data header */ +"ShareData.header" = "Share Data?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Share Anonymous Data?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Current Spending Limit: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Write Down Paper Key Again"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Your paper key is the only way to restore your Litewallet if your phone is lost, stolen, broken, or upgraded.\n\nWe will show you a list of words to write down on a piece of paper and keep safe."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Write Down Paper Key"; + +/* Argument is date */ +"StartPaperPhrase.date" = "You last wrote down your paper key on %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "The most secure and safest way to use Litecoin."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Connecting"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Syncing"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "You can customize your Touch ID spending limit from the %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Use your fingerprint to unlock your Litewallet and send money up to a set limit."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Touch ID Spending Limit Screen"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Spending limit: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Enable Touch ID for Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "You have not set up Touch ID on this device. Go to Settings->Touch ID & Passcode to set it up now."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID Not Set Up"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Always require passcode"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "You will be asked to enter your 6-digit PIN to send any transaction over your spending limit, and every 48 hours since the last time you entered your 6-digit PIN."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch ID Spending Limit"; + +/* Availability status text */ +"Transaction.available" = "Available to Spend"; + +/* Transaction complete label */ +"Transaction.complete" = "Complete"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Ending balance: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Exchange rate when received:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Exchange rate when sent:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ fee)"; + +/* Invalid transaction */ +"Transaction.invalid" = "INVALID"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "just now"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "In progress: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "In progress: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Starting balance: %1$@"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Waiting to be confirmed. Some merchants require confirmation to complete a transaction. Estimated time: 1-2 hours."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "account"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Amount"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Confirmed in Block"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Memo"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Your transactions will appear here."; + +/* [received] at
(received title 2/2) */ +"TransactionDetails.from" = "at %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.moved" = "Moved %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Moved %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Not Confirmed"; + +/* Received $5.00 (received title 1/2) */ +"TransactionDetails.received" = "Received %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Received %1@"; + +/* Sent $5.00 (sent title 1/2) */ +"TransactionDetails.sent" = "Sent %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Sent %1@"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Status"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Transaction Details"; + +/* [sent] to
(sent title 2/2) */ +"TransactionDetails.to" = "to %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin Transaction ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Received at this Address"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Sent to this Address"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Disabled until: %1$@"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Unlock with FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "My Address"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Reset PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Scan"; + +/* TouchID/FaceID prompt text */ +"UnlockScreen.touchIdPrompt" = "Unlock your Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Unlock with TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Wallet Unlocked"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Remember this PIN. If you forget it, you won't be able to access your Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Your PIN will be used to unlock your Litewallet and send money."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Set PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Re-Enter PIN"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Enter your current PIN."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Enter your new PIN."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Re-Enter your new PIN."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Sorry, could not update PIN."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Update PIN Error"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Update PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Copy wallet addresses to clipboard?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Authorize to copy wallet address to clipboard"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Copy Wallet Addresses"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Copy"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Please enter your PIN to authorize this transaction."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Please enter your PIN to continue."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN Required"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Authorize this transaction"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Open the Litewallet iPhone app to set up your wallet."; + +/* Dismiss button label */ +"Webview.dismiss" = "Dismiss"; + +/* Webview loading error message */ +"Webview.errorMessage" = "There was an error loading the content. Please try again."; + +/* Updating webview message */ +"Webview.updating" = "Updating..."; + +/* Welcome view body text */ +"Welcome.body" = "Litewallet now has a brand new look and some new features.\n\nAll coins are displayed in lites (ł). 1 Litecoin (Ł) = 1000 lites (ł)."; + +/* Welcome view title */ +"Welcome.title" = "Welcome to Litewallet!"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Are you sure you want to delete this wallet?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Wipe Wallet?"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Forget your seed phrase or PIN?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Failed to wipe wallet."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Failed"; + +/* Enter key to wipe wallet instruction. */ +"WipeWallet.instruction" = "To start a new wallet or restore an existing wallet, you must first erase the wallet that is currently installed. To continue, enter the current wallet's Paper Key."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Starting or recovering another wallet allows you to access and manage a different Litewallet wallet on this device."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Your current wallet will be removed from this device. If you wish to restore it in the future, you will need to enter your Paper Key."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Start or Recover Another Wallet"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "This action will wipe your Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Deleting your wallet means the private key and wipe the app data will be gone. You may lose your Litecoin forever! \n\n\nNo one on the Litewallet team can retrieve this seed for you. We are not responsible if you fail to heed this warning."; + +/* Warning title */ +"WipeWallet.warningTitle" = "PLEASE READ!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Wipe"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Wiping..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Write down each word in order and store it in a safe place."; + +/* button label */ +"WritePaperPhrase.next" = "Next"; + +/* button label */ +"WritePaperPhrase.previous" = "Previous"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d of %2$d"; + +/* No comment provided by engineer. */ +"Copy" = "Copy"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "BUY"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Exchange details:"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "FEE:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADDRESS:"; + +/* History Bar Item Title */ +"History.barItemTitle" = "HISTORY"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "RECEIVE"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "SEND"; + +/* Less button title */ +"TransactionDetails.less" = "Less..."; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "as of"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "RECEIVE ADDRESS"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID:"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Enter PIN"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Amount Detail:"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Block:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo:"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Transaction end amount detail"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Transaction starting amount detail"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Connecting..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Success!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Syncing..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Rescanning..."; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Continue"; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Cancel"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Enable"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DISMISS"; + +/* Languages label */ +"Settings.languages" = "Languages"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Choose:"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Environment:"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet version:"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Delete Database"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Delete Database"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "This deletes the database but retains the PIN and phrase. Confirm your existing PIN, seed and wait to compelete syncing to the new db"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Delete & Sync"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Local Corruption Error"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Your local database is corrupted. Go to Settings > Blockchain: Settings > Delete Database to refresh"; + +/* Donate articles to the */ +"Donate.toThe" = "Donate to the"; + +/* Donate */ +"Donate.word" = "Donate"; + +/* Luxury fee */ +"FeeSelector.luxury" = "Luxury"; + +/* Fee Selector economy fee description */ +"FeeSelector.luxuryLabel" = "Estimated Delivery: 2.5 - 5+ minutes"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "This option virtually guarantees acceptance of your transaction though you are paying a premium."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Support the Litecoin Foundation"; + +/* Menu button title */ +"MenuButton.customer.support" = "Customer support"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Enter a .crypto, .wallet, .zil, .nft, .blockchain,\n.bitcoin, .coin, .888, .dao, or .x domain."; + +/* Or */ +"Fragment.or" = "or"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Enter a Litecoin address"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet partners"; + +/* Card Bar Item Title */ +"LitecoinCard.barItemTitle" = "Card"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Lookup"; + +/* Forgot password? */ +"LitecoinCard.forgotPassword" = "Forgot password?"; + +/* Login */ +"LitecoinCard.login" = "Login"; + +/* Register */ +"LitecoinCard.registerCard" = "Register"; + +/* Litecoin card */ +"LitecoinCard.registerCardPhrase" = "Register for Litecoin Card"; + +/* Reset Litecoin card password */ +"LitecoinCard.resetPassword" = "Reset password"; + +/* Litecoin card visit */ +"LitecoinCard.visit.toReset" = "Visit https://litecoin.dashboard.getblockcard.com/password/forgot\nto reset your password."; + +/* address */ +"LitecoinCard.Registration.address" = "Address"; + +/* city */ +"LitecoinCard.Registration.city" = "City"; + +/* confirm password */ +"LitecoinCard.Registration.confirmPassword" = "Confirm password"; + +/* country */ +"LitecoinCard.Registration.country" = "Country"; + +/* First name */ +"LitecoinCard.Registration.firstName" = "First name"; + +/* kycIDNumber */ +"LitecoinCard.Registration.kycIDNumber" = "ID Number"; + +/* SSN */ +"LitecoinCard.Registration.kycSSN" = "SSN"; + +/* Last name */ +"LitecoinCard.Registration.lastName" = "Last name"; + +/* mobile number */ +"LitecoinCard.Registration.mobileNumber" = "Mobile number"; + +/* password */ +"LitecoinCard.Registration.password" = "Password"; + +/* state province */ +"LitecoinCard.Registration.stateProvince" = "State / Province"; + +/* registraition username / email */ +"LitecoinCard.Registration.usernameEmail" = "Email (Username)"; + +/* zip post Code */ +"LitecoinCard.Registration.zipPostCode" = "Zip / Postcode"; + +/* identification */ +"LitecoinCard.Registration.identification" = "Identification"; + +/* Register for Litecoin Card */ +"LitecoinCard.registerCard.phrase" = "Register for Litecoin Card"; + +/* kycIDType */ +"LitecoinCard.Registration.kycIDType" = "ID Type"; + +/* resetFields */ +"Button.resetFields" = "Reset fields"; + +/* Registering user... */ +"LitecoinCard.registering.user" = "Registering user ..."; + +/* Card balance */ +"LitecoinCard.cardBalance" = "Card balance"; + +/* Logout */ +"LitecoinCard.logout" = "Logout"; + +/* Registration failure */ +"LitecoinCard.registrationFailure" = "There was a problem with your registration. \nPlease check your data and try again."; + +/* Registration success */ +"LitecoinCard.registrationSuccess" = "You have registered!\nPlease check and confirm your email. Then come back to login."; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Domain Resolution"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Address was resolved!"; + +/* must not be empty */ +"LitecoinCard.Registration.ValidationError.empty" = "Must not be empty"; + +/* Invalid email address */ +"LitecoinCard.Registration.ValidationError.invalidEmail" = "Invalid e-mail Address"; + +/* Mobile number 10 digits required */ +"LitecoinCard.Registration.ValidationError.numberDigitsRequired" = "Mobile number must have at least 10 digits"; + +/* Mobile number required */ +"LitecoinCard.Registration.ValidationError.numberRequired" = "Mobile number is required"; + +/* 6 Password characters required */ +"LitecoinCard.Registration.ValidationError.passwordCharacters" = "Password must have at least 6 characters"; + +/* Captial and numeric password characters required */ +"LitecoinCard.Registration.ValidationError.passwordComposition" = "Password must be more than 6 characters, with at least one character and one numeric character"; + +/* Password required */ +"LitecoinCard.Registration.ValidationError.passwordRequired" = "Password is Required"; + +/* Required field */ +"LitecoinCard.Registration.ValidationError.requiredField" = "Required field"; + +/* Failed Login */ +"LitecoinCard.failed.login" = "Login failed"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Copied"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Copy all details"; + +/* Features and limitations */ +"LitecoinCard.Disclaimer.bullets" = "- Registration & Login\n- Available card balance\n- Reset password\n- No transfer to Litewallet\n- US Only"; + +/* Description of the status */ +"LitecoinCard.Disclaimer.description" = "Litecoin Card currently has limited functionality in Litewallet."; + +/* Referral to the website */ +"LitecoinCard.Disclaimer.referral" = "Visit litecoin.getblockcard.com for full access"; + +/* Beta Testing Litecoin Card */ +"LitecoinCard.Disclaimer.title" = "Litecoin Card Beta"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Lookup failed"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Sorry, domain was not found. [Error: %2$d]"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "System lookup problem. [Error: %2$d]"; + +/* Balance */ +"ManageWallet.balance" = "Balance"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Buy LTC with many fiat pairs\n• Pay with multiple methods\n• Global payment provider"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Reset password detail */ +"LitecoinCard.resetPasswordDetail" = "Enter the email address associated with your Litecoin Card account & look for an email with reset instructions."; + +/* Current Locale */ +"Settings.currentLocale" = "Current Locale:"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Current LTC value in"; + +/* Confirm */ +"Fragment.confirm" = "confirm"; + +/* Enter code */ +"LitecoinCard.enterCode" = "Enter Code"; + +/* Enter code details */ +"LitecoinCard.enterCode.detail" = "Enter the code that was recently sent to your Litecoin Card account email."; + +/* Message when 2FA is on */ +"LitecoinCard.twoFAOn" = "2FA Enabled"; + +/* Message when 2FA is off */ +"LitecoinCard.twoFAOff" = "2FA Not Enabled"; + +/* Litewallet balance label */ +"LitecoinCard.Transfer.litewalletBalance" = "Litewallet balance"; + +/* Transfer title */ +"LitecoinCard.Transfer.title" = "Transfer"; + +/* Description of action */ +"LitecoinCard.Transfer.description" = "Choose the transferring wallet:"; + +/* Start transfer label */ +"LitecoinCard.Transfer.startTransfer" = "Start transfer"; + +/* Transfer to card label */ +"LitecoinCard.Transfer.amountToCard" = "Transfer to Card"; + +/* Transfer to Litewallet label */ +"LitecoinCard.Transfer.amountToLitewallet" = "Transfer to Litewallet"; + +/* Set transfer amount label */ +"LitecoinCard.Transfer.setAmount" = "Slide to set transfer amount"; + +/* Destination address label */ +"LitecoinCard.Transfer.destinationAddress" = "Address"; + +/* to */ +"Fragment.to" = "to"; + +/* Card Bar Item Title */ +"LitecoinCard.name" = "Litecoin Card"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Message when 2FA is off and user is viewing the Card Balance */ +"LitecoinCard.cardBalanceOnlyDescription" = "Logout & Enable 2FA to allow Transfers"; + +/* sorry */ +"Fragment.sorry" = "sorry"; + +/* 2FA Error message */ +"LitecoinCard.twoFAErrorMessage" = "There was an error. Please toggle 2FA to *Enabled*, enter the emailed code, and try again."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Enter domain"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Enter a"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "domain"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Are you sure you want to change the language to %l?"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Fee: %1$@"; + +/* Fees: $0.01*/ +"Send.fee" = "Fees: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Fees:"; + +/* Network */ +"Send.networkFee" = "Network"; + +/* Service */ +"Send.serviceFee" = "Service"; + +/* Signup cancel */ +"Notifications.signupCancel" = "No, thanks"; + +/* "Email title" */ +"Notifications.emailTitle" = "Don't a miss a thing!"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Sign up to hear about updates & contests."; + +/* "Email address label" */ +"Notifications.emailLabel" = "Email address"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Enter here"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Preferred language:"; diff --git a/litewallet/Strings/da.lproj/Localizable.strings b/litewallet/Strings/da.lproj/Localizable.strings new file mode 100755 index 000000000..2c191a0d9 --- /dev/null +++ b/litewallet/Strings/da.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "Blog"; + +/* About screen footer */ +"About.footer" = "Lavet af det globale Litewallet hold. Version %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Privatlivspolitik"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Om"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Luk"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Supportcenter"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Indlæser pung"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Mit Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "ADMINISTRER"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Lokal korruptionsfejl"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Lokal korruptionsfejl"; + +/* Error alert title */ +"Alert.error" = "Fejl"; + +/* No internet alert message */ +"Alert.noInternet" = "Ingen internetforbindelse fundet. Tjek din forbindelse og prøv igen."; + +/* Warning alert title */ +"Alert.warning" = "Advarsel"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Adresser kopieret"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Alle pungens adresser er kopieret."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Paper key indstillet"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Fedt!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN indstillet"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Domæneopløsning"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Din adresse blev løst!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Afsendelse mislykkedes"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Send bekræftelse"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Pengene blev sendt!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON serialiseringsfejl"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Pung ikke klar"; + +/* API Token error message */ +"ApiClient.tokenError" = "Kunne ikke hente API-nøgle"; + +/* buy button */ +"Button.buy" = "købe"; + +/* Cancel button label */ +"Button.cancel" = "Annuller"; + +/* Ignore button label */ +"Button.ignore" = "Ignorer"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "Nej"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "modtag"; + +/* resetFields */ +"Button.resetFields" = "Nulstil felter"; + +/* send button */ +"Button.send" = "send"; + +/* Settings button label */ +"Button.settings" = "Indstillinger"; + +/* Settings button label */ +"Button.submit" = "Send"; + +/* Yes button */ +"Button.yes" = "Ja"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "KØBE"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Køb gavekort \n • Genopfyld forudbetalte telefoner \n • Steam, Amazon, Hotels.com \n • Fungerer i 170 lande"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Skift Litecoin til andre kryptos \n • Ingen ID krævet \n • Køb med kreditkort \n • Global dækning"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Købe Litecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Køb LTC med mange fiat-par\n• Betal med flere metoder\n• Global betalingsudbyder"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Få Litecoin inden for 5 minutter! \n • Køb Litecoin via kreditkort \n • Pas eller stats-ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Købe Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Centrer dit ID i boksen"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Udvekslingsoplysninger:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Beløb der skal sendes:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Beløb til donation:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Behandlingstid: Disse transaktioner tager %1$@ minutter at behandle."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Behandlingstid: denne transaktion vil tage %1$@ minutter at behandle."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Send"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "BETALING:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADRESSE:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Bekræftelse"; + +/* To: (address) */ +"Confirmation.to" = "Til"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Samlede omkostninger:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "De indtastede ord stemmer ikke overens med din papirnøgle. Prøv igen."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "For at sørge for, at alt blev skrevet korrekt, bedes du indtaste følgende ord fra din paper key."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Ord #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Kopi"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin visningsenhed"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Vælge:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Valutakurs"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Denne enhed er ikke konfigureret til at sende e-mail med iOS mailappen."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "E-mail utilgængelig"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Denne enhed er ikke konfigureret til at sende beskeder."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Beskeder utilgængelig"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Økonomi"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Forventet leveringstid: 10+ minutter"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Denne mulighed er ikke anbefalet for tidsfølsomme transaktioner."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Luksus"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Anslået levering: 2,5 - 5 minutter"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Denne mulighed garanterer næsten accept af din transaktion, selvom du betaler en præmie."; + +/* Regular fee */ +"FeeSelector.regular" = "Almindelig"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Anslået levering: 2,5 - 5+ minutter"; + +/* Fee Selector title */ +"FeeSelector.title" = "Behandlingshastighed"; + +/* Confirm */ +"Fragment.confirm" = "Bekræfte"; + +/* Or */ +"Fragment.or" = "eller"; + +/* sorry */ +"Fragment.sorry" = "Undskyld"; + +/* to */ +"Fragment.to" = "til"; + +/* History Bar Item Title */ +"History.barItemTitle" = "HISTORIE"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Aktuel LTC-værdi i"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Tjekker beløb på privat nøgle..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Send %1$@ fra denne private nøgle til din pung? Litecoin netværket vil modtage et gebyr på %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Denne private nøgle findes allerede i din pung."; + +/* empty private key error message */ +"Import.Error.empty" = "Denne private nøgle er tom."; + +/* High fees error message */ +"Import.Error.highFees" = "Transaktionsgebyrer vil koste mere end der er midler til på denne private nøgle."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Ikke en gyldig privat nøgle"; + +/* Import signing error message */ +"Import.Error.signing" = "Fejl ved underskrivelse af transaktion"; + +/* Import button label */ +"Import.importButton" = "Importer"; + +/* Importing wallet progress view label */ +"Import.importing" = "Importerer pung"; + +/* Caption for graphics */ +"Import.leftCaption" = "Pung bliver importeret"; + +/* Import wallet intro screen message */ +"Import.message" = "Import af en pung overfører alle pengene fra din anden pung til din Litewallet pung, via en enkelt overførsel."; + +/* Enter password alert view title */ +"Import.password" = "Denne private nøgle er beskyttet med et kodeord."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "adgangskode"; + +/* Caption for graphics */ +"Import.rightCaption" = "Din Litewallet pung"; + +/* Scan Private key button label */ +"Import.scan" = "Scan privat nøgle"; + +/* Import wallet success alert title */ +"Import.success" = "Succes"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Importeret til din pung."; + +/* Import Wallet screen title */ +"Import.title" = "Importer pung"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Låser op for nøgle"; + +/* Import wallet intro warning message */ +"Import.warning" = "Import af en pung inkluderer ikke transaktionshistorik eller andre oplysninger."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Forkert adgangskode, prøv venligst igen."; + +/* Close app button */ +"JailbreakWarnings.close" = "Luk"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignorer"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "ENHEDENS SIKKERHED ER KOMPROMITTERET\n Enhver 'jailbreak' app kan få adgang til Litewallets nøgledata og stjæle dine Litecoin! Ryd straks denne pung og gendan på en sikker enhed."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "ENHEDENS SIKKERHED ER KOMPROMITTERET\n Enhver 'jailbreak' app kan få adgang til Litewallets nøgledata og stjæle dine Litecoin. Brug kun Litewallet på en ikke jailbreaket enhed."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "ADVARSEL"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Ryd"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Placeringstjenester er deaktiveret."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet har ikke adgang til placeringstjenester."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Fejlformet URI"; + +/* Balance */ +"ManageWallet.balance" = "Balance"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Du lavede din pung den %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Din pungs navn vises kun i din kontos transaktionshistorik, og kan ikke ses af nogen andre."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Pungens navn"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Administrer pung"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Køb Litecoin"; + +/* Menu button title */ +"MenuButton.customer.support" = "Kunde support"; + +/* Menu button title */ +"MenuButton.lock" = "Lås pung"; + +/* Menu button title */ +"MenuButton.security" = "Sikkerhedscenter"; + +/* Menu button title */ +"MenuButton.settings" = "Indstillinger"; + +/* Menu button title */ +"MenuButton.support" = "Support"; + +/* button label */ +"MenuViewController.createButton" = "Opret ny pung"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Gendan pung"; + +/* No comment provided by engineer. */ +"No wallet" = "Ingen tegnebog"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Skift til automatisk tilstand"; + +/* Node is connected label */ +"NodeSelector.connected" = "Forbundet"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Indtast node-IP-adresse og port (valgfrit)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Indtast node"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Skift til manuel tilstand"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Nuværende primære node"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Ikke forbundet"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Node forbindelsesstatus"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin-nodes"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Dårlig betalingsanmodning"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Ikke understøttet eller beskadiget dokument"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "manglende certifikat"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "anmodning udløbet"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Kunne ikke foretage betaling"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Litecoin betalinger kan ikke være mindre end %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Litecoin transaktionsoutput kan ikke være mindre end $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "ikke understøttet signaturtype"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "certifikat der ikke er tillid til"; + +/* Dismiss button. */ +"Prompts.dismiss" = "AFSKEDIGE"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "En adgangskode til enheden er nødvendig for at beskytte din wallet. Gå til indstillinger og slå adgangskode til."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Slå enhedens adgangskode til"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Blive ved"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Din Papirnøgle skal gemmes i tilfælde af du nogensinde mister eller skifter telefon. Tryk her for at fortsætte."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Afbestille"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Aktiver"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Handling påkrævet"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Din pung er muligvis ikke synkroniseret. Dette kan ofte løses ved at genskanne blokkæden."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transaktion afvist"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet er blevet opgraderet til at bruge en 6-cifret PIN. Tryk her for at opgradere."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Opgader PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Hjælp med at forbedre Litewallet ved at dele dine anonyme data med os"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Del anonyme data"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Tryk her for at aktivere Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Aktiver Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "Modtage"; + +/* Address copied message. */ +"Receive.copied" = "Kopieret til udklipsholder."; + +/* Share via email button label */ +"Receive.emailButton" = "E-mail"; + +/* Request button label */ +"Receive.request" = "Anmod om et beløb"; + +/* Share button label */ +"Receive.share" = "Del"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "SMS"; + +/* Receive modal title */ +"Receive.title" = "Modtag"; + +/* Done button text */ +"RecoverWallet.done" = "Færdig"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Gendan pung"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Nulstil PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Indtast paper key"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Gendan dit Litewallet med din paper key."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "Den paper key du indtastede er ugyldig. Dobbelttjek hvert ord og prøv igen."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Venstre pil"; + +/* Next button label */ +"RecoverWallet.next" = "Næste"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Tryk her for flere oplysninger."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Højre pil"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Indtast paper key til pungen du vil gendanne."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "For at nulstille din PIN, skal du indtaste ordene fra din paper key i boksene nedenfor."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Indtast venligst et beløb først."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Anmod om et beløb"; + +/* Alert action button label */ +"ReScan.alertAction" = "Synkroniser"; + +/* Alert message body */ +"ReScan.alertMessage" = "Du vil ikke kunne sende penge mens du synkroniserer."; + +/* Alert message title */ +"ReScan.alertTitle" = "Synkroniser med blokkæde?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minutter"; + +/* Syncing explanation */ +"ReScan.body2" = "Hvis en transaktion vises som gennemført i Litecoin netværket, men ikke i dit Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Du modtager gentagne gange en fejlmeddelelse om at din transaktion er blevet afvist."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Start synkronisering"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Du vil ikke kunne bruge penge, når du synkroniserer med blokkæden."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Synkroniser blokkæde"; + +/* Subheader label */ +"ReScan.subheader1" = "Anslået tid"; + +/* Subheader label */ +"ReScan.subheader2" = "Hvornår skal der synkroniseres?"; + +/* Reset walet button title */ +"resetButton" = "Ja, nulstil tegnebogen"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Slet min Litewallet"; + +/* Scan bitcoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Kamerablitz"; + +/* Complete filter label */ +"Search.complete" = "færdig"; + +/* Pending filter label */ +"Search.pending" = "afventer"; + +/* Received filter label */ +"Search.received" = "modtaget"; + +/* Sent filter label */ +"Search.sent" = "sendt"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Aktiver alle sikkerhedsfunktioner for maksimal beskyttelse."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "Den eneste måde at få adgang til dine Litecoin på, hvis du mister eller opgraderer din telefon."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Paper key"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Beskytter dit Litewallet mod uberettigede brugere."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-cifret PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "Sikkerhedscenter"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Lås dit Break op og send penge op til en vis grænse."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Beløb"; + +/* Balance: $4.00 */ +"Send.balance" = "Balance: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "SENDE"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Gå til Indstillinger og giv adgang til kamera."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet har ikke adgang til kameraet"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "Destinationen er din egen adresse. Du kan ikke sende til dig selv."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Kunne ikke oprette transaktion."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Memo"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Tegnebord er tomt"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Indtast en Litecoin-adresse"; + +/* Network Fee: $0.01 */ +"Send.fee" = "Netværksafgift: %1$@"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Betalingsmodtagerens identitet er ikke bekræftet."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "ikke nok penge"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Modtageradressen er ikke en gyldig Litecoin-adresse."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Tegnebord indeholder ikke en gyldig Litecoin adresse."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Ugyldig adresse"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Afsendelse er deaktiveret under en ny scanning."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Behandler Anmodning"; + +/* Empty address alert message */ +"Send.noAddress" = "Indtast modtagerens adresse."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Indtast et beløb, der skal sendes."; + +/* Paste button label */ +"Send.pasteLabel" = "Indsæt"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Kunne ikke offentliggøre transaktion."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Kunne ikke behandle betalingsanmodning"; + +/* Scan button label */ +"Send.scanLabel" = "Scan"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Send"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Send penge"; + +/* Send money to label */ +"Send.toLabel" = "Til"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Kig op"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Beklager, domænet blev ikke fundet. [Fejl: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Opslag mislykkedes"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Indtast et .crypto- eller .zil-domæne"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Problem med systemopslag. [Fejl: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin adresser er kun beregnet til en enkelt anvendelse."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Genbrug reducerer privatliv for både dig og modtageren og kan resultere i tab, hvis modtageren ikke har direkte kontrol over adressen."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Adresse allerede brugt"; + +/* About label */ +"Settings.about" = "Om"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Avancerede indstillinger"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "blockchain"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Vis valuta"; + +/* Current Locale */ +"Settings.currentLocale" = "Nuværende landestandard:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Tilmeld dig Early Access"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Nyder du Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Importer pung"; + +/* Languages label */ +"Settings.languages" = "Sprog"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Miljø:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet-partnere"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet version:"; + +/* Manage settings section header */ +"Settings.manage" = "Administrer"; + +/* Notifications label */ +"Settings.notifications" = "Notifikationer"; + +/* Leave review button label */ +"Settings.review" = "Giv os en anmeldelse"; + +/* Share anonymous data label */ +"Settings.shareData" = "Del anonyme data"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Support settings section header */ +"Settings.support" = "Support"; + +/* Sync blockchain label */ +"Settings.sync" = "Synkroniser blokkæde"; + +/* Settings title */ +"Settings.title" = "Indstillinger"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch ID brugsgrænse"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Wallet"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Start/gendan en anden wallet"; + +/* Share data view body */ +"ShareData.body" = "Hjælp med at forbedre Litewallet ved at dele dine anonyme data med os. Dette inkluderer ikke finansielle oplysninger. Vi respekterer dit finansielle privatliv."; + +/* Share data header */ +"ShareData.header" = "Del data?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Del anonyme data?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Nuværende forbrugsgrænse: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Skriv paper key ned igen"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Din paper key er den eneste måde at gendanne dit Litewallet på, hvis din telefon mistes, stjæles, går i stykker eller opgraderes.\n\nVi viser dig en liste over word du skal skrive ned på et papir, og holde sikkert."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Skriv paper key ned"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Du skrev sidst din paper key ned den %1$@"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Støt Litecoin Foundation"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Tilslutning..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Rescanning ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Succes!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Synkronisering ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Tilslutter"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Synkroniserer"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Du kan tilpasse forbrugsgrænsen for dit Touch ID fra %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Brug dit fingeraftryk til at låse op for dit Litewallet og sende penge op til en vis grænse."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Touch ID forbrugsgrænseside"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Brugsgrænse: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Aktiver Touch ID til Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Du har ikke indstillet Touch ID på denne enhed. Gå til Indstillinger -> Touch ID og Adgangskode og indstil det nu."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID ikke indstillet"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Kræv altid adgangskode"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Du bliver bedt om at indtaste din 6-cifrede PIN for at sende en transaktion over din brugsgrænse, og hver 48 timer siden du sidst indtastede din 6-cifrede PIN."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch ID brugsgrænse"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Mængde detaljer:"; + +/* Availability status text */ +"Transaction.available" = "Tilgængelig at bruge"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Blok:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "BEMÆRK:"; + +/* Transaction complete label */ +"Transaction.complete" = "Færdig"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Detaljer om transaktionssluttebeløb"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Slutbalance: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Valutakurs ved modtagelsestidspunkt:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Valutakurs ved afsendelsestidspunkt:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ gebyr)"; + +/* Invalid transaction */ +"Transaction.invalid" = "UGYLDIG"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "netop nu"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "I gang: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "I gang: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Startbalance: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Detaljer for transaktionsstartbeløb"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Venter på bekræftelse. Nogen forhandlere kræver bekræftelse for at gennemføre en transaktion. Anslået tid: 1-2 timer."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "konto"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Beløb"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Bekræftet i blok"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Memo"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Kopieret"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Kopier alle detaljer"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Dine transaktioner vil vises her."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "på %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Mindre"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "Flyttede %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Flyttet %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Ikke bekræftet"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "fra"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "Modtaget %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Modtaget %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "Modtag adresse"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "Sendt %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Sendt %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Status"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Transaktionsoplysninger"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "til %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin transaktions-ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Modtaget på denne adresse"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Sendt til denne adresse"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Deaktiveret indtil: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Indtast PIN-kode"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Lås op med FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Min adresse"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Nulstil PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Scan"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Lås op for dit Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Lås op med Touch ID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Pung låst op"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Husk denne PIN. Hvis du glemmer den, vil du ikke kunne få adgang til dine Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Din PIN bliver brugt til at låse op for dit Litewallet og sende penge."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Indstil PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Genindtast PIN"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Indtast din nuværende PIN."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Indtast din nye PIN."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Genindtast din nye PIN."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Beklager, kunne ikke opdatere PIN."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Fejl ved opdatering af PIN"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Opdater PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Kopier pungadresse til udklipsholderen?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Tillad at kopiere pungadresse til udklipsholder"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Kopier pungadresse"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Kopier"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Indtast venligst din PIN for at godkende denne transaktion."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Indtast din PIN for at fortsætte."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN påkrævet"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Godkend denne transaktion"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Åben Litewallet iPhone appen for at konfigurere din pung."; + +/* Dismiss button label */ +"Webview.dismiss" = "Afvis"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Der var en fejl ved indlæsning af indholdet. Prøv igen."; + +/* Updating webview message */ +"Webview.updating" = "Opdaterer..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Velkommen til Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Velkommen til Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Slet database"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Er du sikker på at du vil slette denne wallet?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Slet wallet?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Slet database"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Dette sletter databasen, men bevarer PIN-koden og sætningen. Bekræft din eksisterende PIN-kode, udsæd, og vent på at synkronisere med den nye db"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Slet & synkroniser"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Glem din frø sætning eller pinkode?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Fejl ved sletning af wallet."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Fejl"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Indtast denne wallets gendannelsessætning for at slette den og starte eller gendanne en anden. Din nuværende saldo forbliver med denne sætning."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Påbegyndelse eller gendannelse af en anden tegnebog giver mulighed for at få adgang samt administrere en Litewallet tegnebog på denne enhed."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Du vil ikke længere være i stand til at få adgang til din nuværende Litewallet tegnebog fra denne enhed. Saldoen vil forblive på frasen."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Start eller gendan en anden wallet."; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Denne handling vil slette din Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Sletning af din tegnebog betyder, at den private nøgle og sletning af appdata vil være væk. Du kan miste Litecoin for altid!\n\n\nIngen på Litewallet-teamet kan hente dette frø for dig. Vi er ikke ansvarlige, hvis du undlader at følge denne advarsel."; + +/* Warning title */ +"WipeWallet.warningTitle" = "LÆS VENLIGST!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Slet"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Sletter..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Skriv hvert ord ned i rækkefølge og opbevar den et sikkert sted."; + +/* button label */ +"WritePaperPhrase.next" = "Næste"; + +/* button label */ +"WritePaperPhrase.previous" = "Forrige"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d af %2$d"; + +/* "Email address label" */ +"Notifications.emailLabel" = ""; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = ""; + +/* "Email title" */ +"Notifications.emailTitle" = ""; + +/* "Language preference label" */ +"Notifications.languagePreference" = ""; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = ""; + +/* Signup cancel */ +"Notifications.signupCancel" = ""; + +/* Fee: $0.01 */ +"Send.bareFee" = ""; + +/* Fees Blank: */ +"Send.feeBlank" = ""; + +/* Network */ +"Send.networkFee" = ""; + +/* Service */ +"Send.serviceFee" = ""; + +/* domain */ +"Send.UnstoppableDomains.domain" = ""; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = ""; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = ""; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = ""; + +/* Start view tagline */ +"StartViewController.tagline" = ""; diff --git a/litewallet/Strings/de.lproj/Localizable.strings b/litewallet/Strings/de.lproj/Localizable.strings new file mode 100755 index 000000000..ac9b82526 --- /dev/null +++ b/litewallet/Strings/de.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "Blog"; + +/* About screen footer */ +"About.footer" = "Erstellt vom globalen Litewallet-Team. Version %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Datenschutzerklärung"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Info"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Schließen"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Support-Center"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Wallet wird geladen"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Mein Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "VERWALTEN"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Lokaler Korruptionsfehler"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Ihre lokale Datenbank ist beschädigt. Gehen Sie zu Einstellungen> Blockchain: Einstellungen> Datenbank löschen, um sie zu aktualisieren"; + +/* Error alert title */ +"Alert.error" = "Fehler"; + +/* No internet alert message */ +"Alert.noInternet" = "Keine Internetverbindung gefunden. Bitte überprüfe deine Verbindung und versuche es erneut."; + +/* Warning alert title */ +"Alert.warning" = "Achtung"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Adressen kopiert"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Es wurden alle Wallet-Adressen erfolgreich kopiert."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Offlineschlüssel festgelegt"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Großartig!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN festgelegt"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Domänenauflösung"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Die Adresse wurde aufgelöst!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Versand fehlgeschlagen"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Bestätigung gesendet"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Geld versandt!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON-Serialisierungsfehler"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Wallet nicht bereit"; + +/* API Token error message */ +"ApiClient.tokenError" = "API-Token konnte nicht abgerufen werden"; + +/* buy button */ +"Button.buy" = "kaufen"; + +/* Cancel button label */ +"Button.cancel" = "Abbrechen"; + +/* Ignore button label */ +"Button.ignore" = "Ignorieren"; + +/* menu button */ +"Button.menu" = "Menü"; + +/* No button */ +"Button.no" = "Nein"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "erhalten"; + +/* resetFields */ +"Button.resetFields" = "Felder zurücksetzen"; + +/* send button */ +"Button.send" = "senden"; + +/* Settings button label */ +"Button.settings" = "Einstellungen"; + +/* Settings button label */ +"Button.submit" = "Abschicken"; + +/* Yes button */ +"Button.yes" = "Ja"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "Kaufen"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "Vergessen Sie Ihre Startphrase oder PIN?"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "- Geschenkgutscheine kaufen\n - Prepaid-Telefone nachfüllen\n - Steam, Amazon, Hotels.com\n - Arbeiten in 170 Ländern"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Kaufen Sie Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Kaufen Sie LTC mit vielen Fiat-Paaren.\n• Bezahlen Sie mit mehreren Methoden.\n• Globaler Zahlungsanbieter"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Holen Sie sich Litecoin in 5 Minuten!\n• Kaufen Sie Litecoin per Kreditkarte\n• Reisepass oder Staatsausweis."; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Kaufen Sie Łitecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Zentriere deinen Ausweis im Rechteck"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Austauschdetails:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Betrag, welcher versendet werden soll:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Zu spendender Betrag:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Verarbeitungszeit: Die Bearbeitung dieser Transaktionen dauert %1$@ Minuten."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Verarbeitungszeit: Die Verarbeitung dieser Transaktion wird %1$@ Minuten dauern."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Senden"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "GEBÜHR:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADRESSE"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Bestätigung"; + +/* To: (address) */ +"Confirmation.to" = "An"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Gesamtkosten:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Die eingegebenen Wörter stimmen nicht mit Ihrem Papierschlüssel überein. Bitte versuchen Sie es erneut."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Um sicherzustellen, dass alles korrekt notiert worden ist, gib bitte die folgenden Wörter deines Offlineschlüssels ein."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Wort #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Kopieren"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin-Anzeigeeinheit"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Wählen:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Wechselkurs"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Dieses Gerät ist nicht dafür eingerichtet, E-Mails mit der iOS-Mail-App zu versenden."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "E-Mail nicht verfügbar"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Dieses Gerät ist nicht dafür eingerichtet, Nachrichten zu versenden."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Nachrichtensystem nicht verfügbar"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "Du kannst dein Face ID Ausgabenlimit über die %1$@ anpassen."; + +/* Face Id screen label */ +"FaceIDSettings.label" = "Benutze dein Gesicht, um deine Litewallet zu entsperren und Geld bis zu einem festgelegten Limit zu senden."; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "Face ID Ausgabenlimit-Bildschirm"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "Face ID für Litewallet aktivieren"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "Face ID"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "Sie haben auf diesem Gerät keine Face ID eingerichtet. Gehen Sie zu Settings->Face ID & Passcode, um es jetzt einzurichten."; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "Face ID nicht eingerichtet"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "Face ID Ausgabenlimit"; + +/* Economy fee */ +"FeeSelector.economy" = "Sparsam"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Voraussichtliche Transaktionsdauer: 10+ Minuten"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Diese Option wird nicht für zeitkritische Transaktionen empfohlen."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Luxus"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Voraussichtliche Lieferzeit: 2,5 - 5 Minuten"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Diese Option garantiert praktisch die Annahme Ihrer Transaktion, obwohl Sie eine Prämie zahlen."; + +/* Regular fee */ +"FeeSelector.regular" = "Regulär"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Voraussichtliche Lieferzeit: 2,5 - 5+ Minuten"; + +/* Fee Selector title */ +"FeeSelector.title" = "Verarbeitungsgeschwindigkeit"; + +/* Confirm */ +"Fragment.confirm" = "Bestätigen Sie"; + +/* Or */ +"Fragment.or" = "oder"; + +/* sorry */ +"Fragment.sorry" = "Verzeihung"; + +/* to */ +"Fragment.to" = "zu"; + +/* History Bar Item Title */ +"History.barItemTitle" = "GESCHICHTE"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Aktueller LTC-Wert in"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Das Privatschlüsselguthaben wird überprüft …"; + +/* Sweep private key confirmation message */ +"Import.confirm" = "%1$@ von diesem privaten Schlüssel in Ihr Wallet senden? Das Litecoin-Netzwerk erhält eine Gebühr von %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Dieser private Schlüssel befindet sich bereits in deinem Wallet."; + +/* empty private key error message */ +"Import.Error.empty" = "Dieser private Schlüssel ist leer."; + +/* High fees error message */ +"Import.Error.highFees" = "Die Transaktionsgebühren würden das Guthaben überschreiten, die auf diesem Wallet vorhandenen sind."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Kein gültiger privater Schlüssel"; + +/* Import signing error message */ +"Import.Error.signing" = "Fehler beim Signieren der Transaktion"; + +/* Import button label */ +"Import.importButton" = "Importieren"; + +/* Importing wallet progress view label */ +"Import.importing" = "Wallet wird importiert"; + +/* Caption for graphics */ +"Import.leftCaption" = "Zu importierendes Wallet"; + +/* Import wallet intro screen message */ +"Import.message" = "Der Import eines Wallets überträgt das gesamte Guthaben deines anderen Wallets mit einer einzigen Transaktion in dein Litewallet."; + +/* Enter password alert view title */ +"Import.password" = "Dieser private Schlüssel ist passwortgeschützt."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "Passwort"; + +/* Caption for graphics */ +"Import.rightCaption" = "Dein Litewallet"; + +/* Scan Private key button label */ +"Import.scan" = "Privaten Schlüssel scannen"; + +/* Import wallet success alert title */ +"Import.success" = "Der Vorgang war erfolgreich"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Wallet erfolgreich importiert"; + +/* Import Wallet screen title */ +"Import.title" = "Wallet importieren"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Schlüssel wird freigeschaltet"; + +/* Import wallet intro warning message */ +"Import.warning" = "Der Import eines Wallets umfasst nicht den Transaktionsverlauf oder andere Zusatzinformationen."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Falsches Passwort, bitte versuchen Sie es erneut."; + +/* Close app button */ +"JailbreakWarnings.close" = "Schließen"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignorieren"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "DAS GERÄT IST NICHT LÄNGER SICHER\nJede beliebige „Jailbreak“-App kann auf Litewallets Keychain-Daten zugreifen und deine Litecoins stehlen! Lösche dieses Wallet unverzüglich und stelle es auf einem sicheren Gerät wieder her."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "DAS GERÄT IST NICHT LÄNGER SICHER\nJede beliebige „Jailbreak“-App kann auf Litewallets Keychain-Daten zugreifen und deine Litecoins stehlen. Bitte setze Litewallet ausschließlich auf Geräten ohne Jailbreak ein."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "ACHTUNG"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Löschen"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Standortdienste sind deaktiviert"; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet fehlt die Berechtigung, um auf die Standortdienste zuzugreifen."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Fehlgestalteter URI"; + +/* Balance */ +"ManageWallet.balance" = "Kontostand"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Du hast dein Wallet am %1$@ erstellt"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Der Name deines Wallets erscheint ausschließlich im Transaktionsverlauf deines Kontos und kann von niemandem sonst eingesehen werden."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Name des Wallets"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Wallet verwalten"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Litecoin kaufen"; + +/* Menu button title */ +"MenuButton.customer.support" = "Kundendienst"; + +/* Menu button title */ +"MenuButton.lock" = "Wallet sperren"; + +/* Menu button title */ +"MenuButton.security" = "Sicherheits-Center"; + +/* Menu button title */ +"MenuButton.settings" = "Einstellungen"; + +/* Menu button title */ +"MenuButton.support" = "Support"; + +/* button label */ +"MenuViewController.createButton" = "Neues Wallet erstellen"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menü"; + +/* button label */ +"MenuViewController.recoverButton" = "Wallet wiederherstellen"; + +/* No comment provided by engineer. */ +"No wallet" = "Keine Brieftasche"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Zum automatischen Modus wechseln"; + +/* Node is connected label */ +"NodeSelector.connected" = "Verbunden"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Geben Sie die Node-IP-Adresse und den Port ein (optional)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Node eingeben"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Zum manuellen Modus wechseln"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Aktueller primärer Node"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Nicht verbunden"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Node-Verbindungsstatus"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin-Nodes"; + +/* "Email address label" */ +"Notifications.emailLabel" = "E-Mail-Adresse"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Hier eintreten"; + +/* "Email title" */ +"Notifications.emailTitle" = "Miss nichts verpassen!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Bevorzugte Sprache:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Melden Sie sich an, um über Updates und Wettbewerbe zu erfahren."; + +/* Signup cancel */ +"Notifications.signupCancel" = "Nein danke."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Ungültige Zahlungsanforderung"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Nicht unterstütztes oder fehlerhaftes Dokument"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "Fehlendes Zertifikat"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "Anfrage abgelaufen"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Die Zahlung konnte nicht durchgeführt werden"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Litecoin-Zahlungen können nicht geringer als %1$@ sein."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Litecoin-Transaktionsausgaben können nicht geringer als $@ sein."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "Nicht unterstützter Signaturtyp"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "Nicht vertrauenswürdiges Zertifikat"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DISMISS"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Ein Gerät-Passcode wird benötigt, um Ihr Wallet zu schützen. Gehen Sie zu den Einstellungen und aktivieren Sie den Passcode."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Aktivieren Sie den Gerät-Passcode"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Fortsetzen"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Ihr Paper-Key muss für den Fall, dass Sie Ihr Telefon verlieren oder wechseln, gespeichert werden. Tippen Sie hier, um fortzufahren."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Stornieren"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Aktivieren"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Maßnahme erforderlich"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Möglicherweise ist dein Wallet nicht synchronisiert. Dies kann oft durch ein erneutes Einscannen der Blockchain behoben werden."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transaktion abgelehnt"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet nutzt mittlerweile eine 6-stellige PIN. Hier antippen, um upzugraden."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "PIN upgraden"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Tragen Sie zur Verbesserung von Litewallet bei, indem Sie Ihre anonymen Daten mit uns teilen"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Anonyme Daten teilen"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Hier antippen, um Touch ID zu aktivieren"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Touch ID aktivieren"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "ERHALTEN"; + +/* Address copied message. */ +"Receive.copied" = "In die Zwischenablage kopiert."; + +/* Share via email button label */ +"Receive.emailButton" = "E-Mail"; + +/* Request button label */ +"Receive.request" = "Einen Betrag anfordern"; + +/* Share button label */ +"Receive.share" = "Teilen"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Textnachricht"; + +/* Receive modal title */ +"Receive.title" = "Erhalten"; + +/* Done button text */ +"RecoverWallet.done" = "Fertig"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Wallet wiederherstellen"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "PIN zurücksetzen"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Offlineschlüssel eingeben"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Stelle dein Litewallet mithilfe deines Offlineschlüssels wieder her."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "Der von dir eingegebene Offlineschlüssel ist ungültig. Bitte überprüfe nochmal jedes Wort und versuche es erneut."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Linker Pfeil"; + +/* Next button label */ +"RecoverWallet.next" = "Weiter"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Für weitere Informationen hier antippen."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Rechter Pfeil"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Gib den Offlineschlüssel für das Wallet ein, das du wiederherstellen willst."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Gib die Wörter des Offlineschlüssels in die Kästen unten ein, um deine PIN zurückzusetzen."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Bitte geben Sie zuerst einen Betrag ein."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Einen Betrag anfordern"; + +/* Alert action button label */ +"ReScan.alertAction" = "Synchronisieren"; + +/* Alert message body */ +"ReScan.alertMessage" = "Während der Synchronisation kannst du kein Geld versenden."; + +/* Alert message title */ +"ReScan.alertTitle" = "Mit der Blockchain synchronisieren?"; + +/* extimated time */ +"ReScan.body1" = "20–45 Minuten"; + +/* Syncing explanation */ +"ReScan.body2" = "Wenn eine Transaktion im Litecoin-Netzwerk, aber nicht in deinem Litewallet als abgeschlossen angezeigt wird."; + +/* Syncing explanation */ +"ReScan.body3" = "Sie erhalten wiederholt eine Fehlermeldung, dass Ihre Transaktion abgewiesen wurde."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Synchronisation starten"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Während der Synchronisation mit der Blockchain kannst du kein Geld versenden."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Blockchain-Synchronisation"; + +/* Subheader label */ +"ReScan.subheader1" = "Voraussichtliche Dauer"; + +/* Subheader label */ +"ReScan.subheader2" = "Wann soll synchronisiert werden?"; + +/* Reset walet button title */ +"resetButton" = "Ja, Wallet zurücksetzen"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Mein Litewallet löschen"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Kamerablitz"; + +/* Complete filter label */ +"Search.complete" = "abgeschlossen"; + +/* Pending filter label */ +"Search.pending" = "offen"; + +/* Received filter label */ +"Search.received" = "erhalten"; + +/* Sent filter label */ +"Search.sent" = "gesendet"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Aktiviere alle Sicherheitsfunktionen für maximalen Schutz."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "Die einzige Möglichkeit, wie du auf deine Litecoins zugreifen kannst, falls du dein Smartphone verlierst oder wechselst."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Offlineschlüssel"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Schütze dein Litewallet vor nicht autorisierten Benutzern."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-stellige PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "Sicherheits-Center"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Eine praktische Möglichkeit, dein Litewallet zu entsperren und Geld bis zu einer festlegbaren Höchstgrenze zu versenden."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Betrag"; + +/* Balance: $4.00 */ +"Send.balance" = "Guthaben: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Gebühr: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "SENDEN"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Rufe die Einstellungen auf, um den Zugriff auf die Kamera zu erlauben."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet darf nicht auf die Kamera zugreifen"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "Das Ziel ist deine eigene Adresse. Du kannst nichts an dich selbst senden."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Transaktion konnte nicht erstellen werden."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Vermerk"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Pasteboard ist leer."; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Geben Sie eine Litecoin-Adresse ein"; + +/* Fees: $0.01*/ +"Send.fee" = "Gebühren: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Gebühren:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "die Identität des Zahlungsempfängers ist nicht bestätigt."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Unzureichende Mittel"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Die Zieladresse ist keine gültige Litecoin-Adresse."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Pasteboard enthält keine gültige Litecoin-Adresse."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Ungültige Adresse"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Das Senden ist während eines kompletten Neuscans deaktiviert."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Anfrage wird geladen"; + +/* Network */ +"Send.networkFee" = "Netzwerk"; + +/* Empty address alert message */ +"Send.noAddress" = "Bitte gib die Adresse des Empfängers ein."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Bitte gib den zu sendenden Betrag ein."; + +/* Paste button label */ +"Send.pasteLabel" = "Einfügen"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Transaktion konnte nicht veröffentlicht werden."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Zahlungsanfrage konnte nicht geladen werden"; + +/* Scan button label */ +"Send.scanLabel" = "Scannen"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Senden"; + +/* Service */ +"Send.serviceFee" = "Service"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Senden"; + +/* Send money to label */ +"Send.toLabel" = "An"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "Domain"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Geben Sie ein"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Nachsehen"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Entschuldigung, Domain wurde nicht gefunden. [Error: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Lookup failed"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Geben Sie eine .crypto-, .wallet-, .zil-, .nft-, .blockchain-, .bitcoin-, .coin-, .888-, .dao- oder .x-Domäne ein."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Domäne eingeben"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Problem mit der Systemsuche. [Fehler: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin-Adressen sind lediglich zur einmaligen Benutzung vorgesehen."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Durch eine mehrfache Verwendung können sowohl du als auch der Empfänger leichter identifiziert werden und falls der Empfänger die Adresse nicht direkt kontrolliert, kann das Verluste bedeuten."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Adresse bereits in Verwendung"; + +/* About label */ +"Settings.about" = "Über"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Erweiterte Einstellungen"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Blockchain"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Möchten Sie die Sprache wirklich auf %l ändern?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Anzeigewährung"; + +/* Current Locale */ +"Settings.currentLocale" = "Aktuelles Gebietsschema:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Zum Early Access anmelden"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Gefällt dir Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "Gesichts-ID Ausgabenlimit"; + +/* Import wallet label */ +"Settings.importTitle" = "Wallet importieren"; + +/* Languages label */ +"Settings.languages" = "Sprachen"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Umgebung:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet-Partner"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet-Version:"; + +/* Manage settings section header */ +"Settings.manage" = "Verwalten"; + +/* Notifications label */ +"Settings.notifications" = "Benachrichtigungen"; + +/* Leave review button label */ +"Settings.review" = "Schreibe eine Rezension für unsere App"; + +/* Share anonymous data label */ +"Settings.shareData" = "Übermittle anonyme Nutzungsdaten"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Support settings section header */ +"Settings.support" = "Unterstützung"; + +/* Sync blockchain label */ +"Settings.sync" = "Blockchain-Synchronisation"; + +/* Settings title */ +"Settings.title" = "Einstellungen"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch-ID-Ausgabenlimit"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Wallet"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Ein anderes Wallet starten/wiederherstellen"; + +/* Share data view body */ +"ShareData.body" = "Hilf dabei, Litewallet zu verbessern, indem du uns deine Nutzungsdaten anonym übermittelst. Hiervon sind sämtliche Finanzinformationen ausgeschlossen. Wir respektieren deine finanzielle Privatsphäre."; + +/* Share data header */ +"ShareData.header" = "Nutzungsdaten übermitteln?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Anonyme Nutzungsdaten übermitteln?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Aktuelles Ausgabenlimit:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Offlineschlüssel erneut notieren"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Dein Offlineschlüssel ist die einzige Möglichkeit, dein Litewallet wiederherzustellen, falls du dein Smartphone verlierst, austauscht, es gestohlen wird oder kaputtgeht.\n\nHierfür zeigen wir dir eine Liste von Wörtern an, damit du sie dir auf ein Stück Papier notieren kannst, das es sicher aufzubewahren gilt."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Offlineschlüssel notieren"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Du hast dir deinen Offlineschlüssel zuletzt am %1$@ notiert"; + +/* Start view tagline */ +"StartViewController.tagline" = "Die sicherste Option zur Nutzung von Litecoin."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Unterstützung der Litecoin Foundation"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Verbindung wird hergestellt ..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Erneut scannen ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Erfolg!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Synchronisierung läuft ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Verbindet"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Synchronisiert"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Sie können über %1$@ ein Ausgabenmaximum für Touch ID festlegen."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Nutze deinen Fingerabdruck, um dein Litewallet zu entsperren und Geld bis zu einer festlegbaren Höchstgrenze zu versenden."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Bildschirm für Touch-ID-Ausgabenmaximum"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Ausgabelimit: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Touch ID für Litewallet aktivieren"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Du hast Touch ID auf diesem Gerät noch nicht eingerichtet. Öffne Einstellungen -> Touch ID & Passcode, um es jetzt einzurichten."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID nicht eingerichtet"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Immer PIN anfordern"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Du musst deine 6-stellige PIN eingeben, um Transaktionen abzuschicken, die über deinem Ausgabenlimit liegen, sowie alle 48 Stunden seit du deine 6-stellige PIN das letzte Mal eingegeben hast."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch-ID-Ausgabenlimit"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Betrag Detail:"; + +/* Availability status text */ +"Transaction.available" = "Verfügbar für Ausgaben"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Block:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo:"; + +/* Transaction complete label */ +"Transaction.complete" = "Abgeschlossen"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Transaktionsendbetrag Detail"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Endguthaben: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Wechselkurs beim Erhalt:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Wechselkurs beim Senden:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ Gebühr)"; + +/* Invalid transaction */ +"Transaction.invalid" = "UNGÜLTIG"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "gerade eben"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "In Bearbeitung: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "In Bearbeitung: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Startguthaben: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Einzelheiten zum Transaktionsstartbetrag"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tk ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Bestätigung ausstehend. Einige Händler fordern eine Bestätigung, um Transaktionen abzuschließen. Voraussichtliche Dauer: 1–2 Stunden."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "Konto"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Betrag"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Bestätigt im Block"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Vermerk"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Kopiert"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Kopieren Sie alle Details"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Hier werden deine Transaktionen angezeigt."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "an %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Weniger"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "%1$@ bewegt"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Verschoben %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Nicht bestätigt"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "ab"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "%1$@ erhalten"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Erhalten %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "ADRESSE ERHALTEN"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "%1$@ gesendet"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Gesendet %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tk ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Status"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Transaktionsdaten"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "an %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin-Transaktions-ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "An dieser Adresse empfangen"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "An diese Adresse versandt"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Deaktiviert bis: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Pin eingeben"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Entsperren mit FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Meine Adresse"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "PIN zurücksetzen"; + +/* Scan button title */ +"UnlockScreen.scan" = "Scannen"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Entsperre dein Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Mit Touch ID entsperren"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Wallet entsperrt"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Merke dir diese PIN. Falls du sie vergisst, kannst du nicht mehr auf deine Litecoins zugreifen."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Deine PIN wird verwendet, um dein Litewallet zu entsperren und Geld zu versenden."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "PIN festlegen"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "PIN erneut eingeben"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Gib deine derzeitige PIN ein."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Gib deine neue PIN ein."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Gib deine neue PIN erneut ein."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Leider konnte deine PIN nicht aktualisiert werden."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Fehler bei der PIN-Aktualisierung"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "PIN aktualisieren"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Wallet-Adressen in die Zwischenablage kopieren?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Berechtigung für das Kopieren der Wallet-Adresse in die Zwischenablage erteilen"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Wallet-Adressen kopieren"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Kopieren"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Bitte geben Sie Ihren PIN ein, um diese Transaktion zu genehmigen."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Bitte gib deine PIN ein, um fortzufahren"; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN erforderlich"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Diese Transaktion genehmigen"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Öffne die Litewallet-iPhone-App, um dein Wallet einzurichten."; + +/* Dismiss button label */ +"Webview.dismiss" = "Verwerfen"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Beim Laden des Inhalts ist ein Fehler aufgetreten. Bitte versuche es erneut."; + +/* Updating webview message */ +"Webview.updating" = "Aktualisierung läuft …"; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Willkommen bei Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Willkommen bei Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Datenbank löschen"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Sind Sie sicher, dass Sie dieses Wallet löschen möchten?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Wallet löschen?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Datenbank löschen"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Dadurch wird die Datenbank gelöscht, die PIN und der Ausdruck bleiben jedoch erhalten. Bestätigen Sie Ihre vorhandene PIN und warten Sie, bis die Synchronisierung mit der neuen Datenbank abgeschlossen ist"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Löschen und synchronisieren"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Vergessen Sie Ihre Startphrase oder PIN?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Wallet konnte nicht gelöscht werden."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Fehlgeschlagen"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Geben Sie die Wiederherstellungsphrase des Wallets ein, um es zu löschen und die Wiederherstellung eines anderen Wallets zu beginnen. Ihr aktueller Kontostand bleibt mit dieser Phrase verbunden."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Das Anlegen oder Wiederherstellen einer weiteren Wallet ermöglicht es Ihnen, auf diesem Gerät auf eine andere Litewallet zuzugreifen und diese zu verwalten."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Sie werden von diesem Gerät aus keinen Zugriff auf Ihr aktuelles Litewallet mehr haben. Der Saldo verbleibt auf dem Satz."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Ein anderes Wallet beginnen oder wiederherstellen"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Diese Aktion löscht Ihr Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Das Löschen Ihrer Brieftasche bedeutet, dass der private Schlüssel und die App-Daten gelöscht werden. Sie können Litecoin für immer verlieren!\n\n\nNiemand im Litewallet-Team kann diesen Seed für Sie abrufen. Wir sind nicht verantwortlich, wenn Sie diese Warnung nicht beachten."; + +/* Warning title */ +"WipeWallet.warningTitle" = "BITTE LESEN!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Löschen"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Wird gelöscht ..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Schreib dir jedes Wort der Reihenfolge nach auf und bewahre die Notiz an einem sicheren Ort auf."; + +/* button label */ +"WritePaperPhrase.next" = "Weiter"; + +/* button label */ +"WritePaperPhrase.previous" = "Zurück"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d von %2$d"; diff --git a/litewallet/Strings/en.lproj/Localizable.strings b/litewallet/Strings/en.lproj/Localizable.strings new file mode 100644 index 000000000..a16a32788 --- /dev/null +++ b/litewallet/Strings/en.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen website label */ +"About.blog" = "Website"; + +/* About screen footer */ +"About.footer" = "Made by the LiteWallet Team\nof the\nLitecoin Foundation\n%1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Privacy Policy"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "About"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Close"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Support Center"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Loading Wallet"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "My Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "MANAGE"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Local Corruption Error"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Your local database is corrupted. Go to Settings > Blockchain: Settings > Delete Database to refresh"; + +/* Error alert title */ +"Alert.error" = "Error"; + +/* No internet alert message */ +"Alert.noInternet" = "No internet connection found. Check your connection and try again."; + +/* Warning alert title */ +"Alert.warning" = "Warning"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Addresses Copied"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "All wallet addresses successfully copied."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Paper Key Set"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Awesome!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN Set"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Domain Resolution"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Address was resolved!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Send failed"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Send Confirmation"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Money Sent!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON Serialization Error"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Wallet not ready"; + +/* API Token error message */ +"ApiClient.tokenError" = "Unable to retrieve API token"; + +/* buy button */ +"Button.buy" = "buy"; + +/* Cancel button label */ +"Button.cancel" = "Cancel"; + +/* Ignore button label */ +"Button.ignore" = "Ignore"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "No"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "receive"; + +/* resetFields */ +"Button.resetFields" = "Reset fields"; + +/* send button */ +"Button.send" = "send"; + +/* Settings button label */ +"Button.settings" = "Settings"; + +/* Settings button label */ +"Button.submit" = "Submit"; + +/* Yes button */ +"Button.yes" = "Yes"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "BUY"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Buy gift cards\n• Refill prepaid phones\n• Steam, Amazon, Hotels.com\n• Works in 170 countries"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Change Litecoin for other cryptos\n• No ID Required\n• Buy via credit card\n• Global coverage"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Buy Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Buy LTC with many fiat pairs\n• Pay with multiple methods\n• Global payment provider"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Get Litecoin in 5 mins!\n• Buy Litecoin via credit card\n• Passport or State ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Buy Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Center your ID in the box"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Exchange details:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Amount to Send:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Amount to Donate:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Processing time: These transactions will take %1$@ minutes to process."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Processing time: This transaction will take %1$@ minutes to process."; + +/* Send: (amount) */ +"Confirmation.send" = "Send"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "FEE:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADDRESS:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Confirmation"; + +/* To: (address) */ +"Confirmation.to" = "To"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Total Cost:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "The words entered do not match your paper key. Please try again."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "To make sure everything was written down correctly, please enter the following words from your paper key."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Word #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Copy"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin Display Unit"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Choose:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Exchange Rate"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "This device isn't configured to send email with the iOS mail app."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "Email Unavailable"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "This device isn't configured to send messages."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Messaging Unavailable"; + +/* You can customize your Face ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "You can customize your Face ID spending limit from the %1$@."; + +/* Face ID screen label */ +"FaceIDSettings.label" = "Use your face to unlock your Litewallet and send money up to a set limit."; + +/* Link Text (see TouchIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "Face ID Spending Limit Screen"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "Enable Face ID for Litewallet"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "Face ID"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "You have not set up Face ID on this device. Go to Settings->Face ID & Passcode to set it up now."; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "Face ID Not Set Up"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "Face ID Spending Limit"; + +/* Economy fee */ +"FeeSelector.economy" = "Economy"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Estimated Delivery: 10+ minutes"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "This option is not recommended for time-sensitive transactions."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Luxury"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Estimated Delivery: 2.5 - 5 minutes"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "This option virtually guarantees acceptance of your transaction though you are paying a premium."; + +/* Regular fee */ +"FeeSelector.regular" = "Regular"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Estimated Delivery: 2.5 - 5+ minutes"; + +/* Fee Selector title */ +"FeeSelector.title" = "Processing Speed"; + +/* Confirm */ +"Fragment.confirm" = "confirm"; + +/* Or */ +"Fragment.or" = "or"; + +/* sorry */ +"Fragment.sorry" = "sorry"; + +/* to */ +"Fragment.to" = "to"; + +/* History Bar Item Title */ +"History.barItemTitle" = "HISTORY"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Current LTC value in"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Checking private key balance..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Send %1$@ from this private key into your wallet? The Litecoin network will receive a fee of %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "This private key is already in your wallet."; + +/* empty private key error message */ +"Import.Error.empty" = "This private key is empty."; + +/* High fees error message */ +"Import.Error.highFees" = "Transaction fees would cost more than the funds available on this private key."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Not a valid private key"; + +/* Import signing error message */ +"Import.Error.signing" = "Error signing transaction"; + +/* Import button label */ +"Import.importButton" = "Import"; + +/* Importing wallet progress view label */ +"Import.importing" = "Importing Wallet"; + +/* Caption for graphics */ +"Import.leftCaption" = "Wallet to be imported"; + +/* Import wallet intro screen message */ +"Import.message" = "Importing a wallet transfers all the money from your other wallet into your Litewallet wallet using a single transaction."; + +/* Enter password alert view title */ +"Import.password" = "This private key is password protected."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "password"; + +/* Caption for graphics */ +"Import.rightCaption" = "Your Litewallet Wallet"; + +/* Scan Private key button label */ +"Import.scan" = "Scan Private Key"; + +/* Import wallet success alert title */ +"Import.success" = "Success"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Successfully imported wallet."; + +/* Import Wallet screen title */ +"Import.title" = "Import Wallet"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Unlocking Key"; + +/* Import wallet intro warning message */ +"Import.warning" = "Importing a wallet does not include transaction history or other details."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Wrong password, please try again."; + +/* Close app button */ +"JailbreakWarnings.close" = "Close"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignore"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin! Wipe this wallet immediately and restore on a secure device."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "DEVICE SECURITY COMPROMISED\n Any 'jailbreak' app can access Litewallet's keychain data and steal your Litecoin. Please only use Litewallet on a non-jailbroken device."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "WARNING"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Wipe"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Location services are disabled."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet does not have permission to access location services."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Malformed URI"; + +/* Balance */ +"ManageWallet.balance" = "Balance"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "You created your wallet on %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Your wallet name only appears in your account transaction history and cannot be seen by anyone else."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Wallet Name"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Manage Wallet"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Buy Litecoin"; + +/* Menu button title */ +"MenuButton.customer.support" = "Customer support"; + +/* Menu button title */ +"MenuButton.lock" = "Lock Wallet"; + +/* Menu button title */ +"MenuButton.security" = "Security Center"; + +/* Menu button title */ +"MenuButton.settings" = "Settings"; + +/* Menu button title */ +"MenuButton.support" = "Support"; + +/* button label */ +"MenuViewController.createButton" = "Create New Wallet"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Recover Wallet"; + +/* No comment provided by engineer. */ +"No wallet" = "No wallet"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Switch to Automatic Mode"; + +/* Node is connected label */ +"NodeSelector.connected" = "Connected"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Enter Node IP address and port (optional)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Enter Node"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Switch to Manual Mode"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Current Primary Node"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Not Connected"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Node Connection Status"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin Nodes"; + +/* "Email address label" */ +"Notifications.emailLabel" = "Email address"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Enter here"; + +/* "Email title" */ +"Notifications.emailTitle" = "Don't a miss a thing!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Preferred language:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Sign up to hear about updates & contests."; + +/* Signup cancel */ +"Notifications.signupCancel" = "No, thanks"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Bad Payment Request"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Unsupported or corrupted document"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "missing certificate"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "request expired"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Couldn't make payment"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Litecoin payments can't be less than %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Litecoin transaction outputs can't be less than $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "unsupported signature type"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "untrusted certificate"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DISMISS"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "Tap here to enable Face ID"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "Enable Face ID"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "A device passcode is needed to safeguard your wallet."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Turn device passcode on"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Continue"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Your Paper Key must be kept in a safe place. It is the only way modify or restore your Litewallet or transfer your Litecoin. Please write it down."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Cancel"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Enable"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Action Required"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Your wallet may be out of sync. This can often be fixed by rescanning the blockchain."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transaction Rejected"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet requires a 6-digit PIN. Please set and store your PIN in a safe place."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Set PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Help improve Litewallet by sharing your anonymous data with us"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Share Anonymous Data"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Tap here to enable Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Enable Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "RECEIVE"; + +/* Address copied message. */ +"Receive.copied" = "Copied to clipboard."; + +/* Share via email button label */ +"Receive.emailButton" = "Email"; + +/* Request button label */ +"Receive.request" = "Request an Amount"; + +/* Share button label */ +"Receive.share" = "Share"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Text Message"; + +/* Receive modal title */ +"Receive.title" = "Receive"; + +/* Done button text */ +"RecoverWallet.done" = "Done"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Recover Wallet"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Reset PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Enter Paper Key"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Recover your Litewallet with your paper key."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "The paper key you entered is invalid. Please double-check each word and try again."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Left Arrow"; + +/* Next button label */ +"RecoverWallet.next" = "Next"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Tap here for more information."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Right Arrow"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Enter the paper key for the wallet you want to recover."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "To reset your PIN, enter the words from your paper key into the boxes below."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Please enter an amount first."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Request an Amount"; + +/* Alert action button label */ +"ReScan.alertAction" = "Sync"; + +/* Alert message body */ +"ReScan.alertMessage" = "You will not be able to send money while syncing."; + +/* Alert message title */ +"ReScan.alertTitle" = "Sync with Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minutes"; + +/* Syncing explanation */ +"ReScan.body2" = "If a transaction shows as completed on the Litecoin network but not in your Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "You repeatedly get an error saying your transaction was rejected."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Start Sync"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "You will not be able to send money while syncing with the blockchain."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Sync Blockchain"; + +/* Subheader label */ +"ReScan.subheader1" = "Estimated time"; + +/* Subheader label */ +"ReScan.subheader2" = "When to Sync?"; + +/* Reset walet button title */ +"resetButton" = "Yes, reset wallet"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Delete my Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Camera Flash"; + +/* Complete filter label */ +"Search.complete" = "complete"; + +/* Pending filter label */ +"Search.pending" = "pending"; + +/* Received filter label */ +"Search.received" = "received"; + +/* Sent filter label */ +"Search.sent" = "sent"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "Face ID"; + +/* Security Center Info */ +"SecurityCenter.info" = "Enable all security features for maximum protection."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "The only way to access your Litecoin if you lose or upgrade your phone."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Paper Key"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Protects your Litewallet from unauthorized users."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-Digit PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "Security Center"; + +/* Touch ID/FaceID button description */ +"SecurityCenter.touchIdDescription" = "Conveniently unlock your Litewallet and send money up to a set limit."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Amount"; + +/* Balance: $4.00 */ +"Send.balance" = "Balance: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Fee: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "SEND"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Go to Settings to allow camera access."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet is not allowed to access the camera"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "The destination is your own address. You cannot send to yourself."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Could not create transaction."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Memo"; + +/* Empty pasteboard error message */ +"Send.emptyPasteboard" = "Pasteboard is empty"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Enter a Litecoin address"; + +/* Fees: $0.01*/ +"Send.fee" = "Fees: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Fees:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Payee identity isn't certified."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Insufficient Funds"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "The destination address is not a valid Litecoin address."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Pasteboard does not contain a valid Litecoin address."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Invalid Address"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Sending is disabled during a full rescan."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Loading Request"; + +/* Network */ +"Send.networkFee" = "Network"; + +/* Empty address alert message */ +"Send.noAddress" = "Please enter the recipient's address."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Please enter an amount to send."; + +/* Paste button label */ +"Send.pasteLabel" = "Paste"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Could not publish transaction."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Could not load payment request"; + +/* Scan button label */ +"Send.scanLabel" = "Scan"; + +/* Send button label */ +"Send.sendLabel" = "Send"; + +/* Service */ +"Send.serviceFee" = "Service"; + +/* Send modal title */ +"Send.title" = "Send"; + +/* Send money to label */ +"Send.toLabel" = "To"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "domain"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Enter a"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Lookup"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Sorry, domain was not found. [Error:%2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Lookup failed"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Enter a .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao, or .x domain."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Enter domain"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "System lookup problem. [Error: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin addresses are intended for single use only."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Re-use reduces privacy for both you and the recipient and can result in loss if the recipient doesn't directly control the address."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Address Already Used"; + +/* About label */ +"Settings.about" = "About"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Advanced Settings"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Blockchain"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Are you sure you want to change the language to %l?"; + +/* Default currency label */ +"Settings.currency" = "Display Currency"; + +/* Current Locale */ +"Settings.currentLocale" = "Current Locale:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Join Early Access"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Are you enjoying Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "Face ID Spending Limit"; + +/* Import wallet label */ +"Settings.importTitle" = "Import Wallet"; + +/* Languages label */ +"Settings.languages" = "Languages"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Environment:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet partners"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet version:"; + +/* Manage settings section header */ +"Settings.manage" = "Manage"; + +/* Notifications label */ +"Settings.notifications" = "Notifications"; + +/* Leave review button label */ +"Settings.review" = "Leave us a Review"; + +/* Share anonymous data label */ +"Settings.shareData" = "Share Anonymous Data"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Support settings section header */ +"Settings.support" = "Support"; + +/* Sync blockchain label */ +"Settings.sync" = "Sync Blockchain"; + +/* Settings title */ +"Settings.title" = "Settings"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch ID Spending Limit"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Wallet"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Start/Recover Another Wallet"; + +/* Share data view body */ +"ShareData.body" = "Help improve Litewallet by sharing your anonymous data with us. This does not include any financial information. We respect your financial privacy."; + +/* Share data header */ +"ShareData.header" = "Share Data?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Share Anonymous Data?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Current Spending Limit: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Write Down Paper Key Again"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Your paper key is the only way to restore your Litewallet if your phone is lost, stolen, broken, or upgraded.\n\nWe will show you a list of words to write down on a piece of paper and keep safe."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Write Down Paper Key"; + +/* Argument is date */ +"StartPaperPhrase.date" = "You last wrote down your paper key on %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "The most secure and easiest way to use Litecoin."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Support the Litecoin Foundation"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Connecting..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Rescanning..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Success!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Syncing..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Connecting"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Syncing"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "You can customize your Touch ID spending limit from the %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Use your fingerprint to unlock your Litewallet and send money up to a set limit."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Touch ID Spending Limit Screen"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Spending limit: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Enable Touch ID for Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "You have not set up Touch ID on this device. Go to Settings->Touch ID & Passcode to set it up now."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID Not Set Up"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Always require passcode"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "You will be asked to enter your 6-digit PIN to send any transaction over your spending limit, and every 48 hours since the last time you entered your 6-digit PIN."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch ID Spending Limit"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Amount Detail:"; + +/* Availability status text */ +"Transaction.available" = "Available to Spend"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Block:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo:"; + +/* Transaction complete label */ +"Transaction.complete" = "Complete"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Transaction end amount detail"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Ending balance: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Exchange rate when received:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Exchange rate when sent:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ fee)"; + +/* Invalid transaction */ +"Transaction.invalid" = "INVALID"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "just now"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "In progress: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "In progress: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Starting balance: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Transaction starting amount detail"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Waiting to be confirmed. Some merchants require confirmation to complete a transaction. Estimated time: 1-2 hours."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "account"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Amount"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Confirmed in Block"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Memo"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Copied"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Copy all details"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Your transactions will appear here."; + +/* [received] at
(received title 2/2) */ +"TransactionDetails.from" = "at %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Less..."; + +/* Moved $5.00 */ +"TransactionDetails.moved" = "Moved %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Moved %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Not Confirmed"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "as of"; + +/* Received $5.00 (received title 1/2) */ +"TransactionDetails.received" = "Received %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Received %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "RECEIVE ADDRESS"; + +/* Sent $5.00 (sent title 1/2) */ +"TransactionDetails.sent" = "Sent %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Sent %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Status"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Transaction Details"; + +/* [sent] to
(sent title 2/2) */ +"TransactionDetails.to" = "to %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin Transaction ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Received at this Address"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Sent to this Address"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Disabled until: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Enter PIN"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Unlock with FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "My Address"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Reset PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Scan"; + +/* TouchID/FaceID prompt text */ +"UnlockScreen.touchIdPrompt" = "Unlock your Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Unlock with TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Wallet Unlocked"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Remember this PIN. If you forget it, you won't be able to access your Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Your PIN will be used to unlock your Litewallet and send money."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Set PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Re-Enter PIN"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Enter your current PIN."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Enter your new PIN."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Re-Enter your new PIN."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Sorry, could not update PIN."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Update PIN Error"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Update PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Copy wallet addresses to clipboard?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Authorize to copy wallet address to clipboard"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Copy Wallet Addresses"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Copy"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Please enter your PIN to authorize this transaction."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Please enter your PIN to continue."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN Required"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Authorize this transaction"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Open the Litewallet iPhone app to set up your wallet."; + +/* Dismiss button label */ +"Webview.dismiss" = "Dismiss"; + +/* Webview loading error message */ +"Webview.errorMessage" = "There was an error loading the content. Please try again."; + +/* Updating webview message */ +"Webview.updating" = "Updating..."; + +/* Welcome view body text */ +"Welcome.body" = "Litewallet now has a brand new look and some new features.\n\nAll coins are displayed in lites (ł). 1 Litecoin (Ł) = 1000 lites (ł)."; + +/* Welcome view title */ +"Welcome.title" = "Welcome to Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Delete Database"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Are you sure you want to delete this wallet?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Wipe Wallet?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Delete Database"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "This deletes the database but retains the PIN and phrase. Confirm your existing PIN, seed and wait to compelete syncing to the new db"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Delete & Sync"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Forget your seed phrase or PIN?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Failed to wipe wallet."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Failed"; + +/* Enter key to wipe wallet instruction. */ +"WipeWallet.instruction" = "To start a new wallet or restore an existing wallet, you must first erase the wallet that is currently installed. To continue, enter the current wallet's Paper Key."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Starting or recovering another wallet allows you to access and manage a different Litewallet wallet on this device."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Your current wallet will be removed from this device. If you wish to restore it in the future, you will need to enter your Paper Key."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Start or Recover Another Wallet"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "This action will wipe your Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Deleting your wallet means the private key and wipe the app data will be gone. You may lose your Litecoin forever! \n\n\nNo one on the Litewallet team can retrieve this seed for you. We are not responsible if you fail to heed this warning."; + +/* Warning title */ +"WipeWallet.warningTitle" = "PLEASE READ!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Wipe"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Wiping..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Write down each word in order and store it in a safe place."; + +/* button label */ +"WritePaperPhrase.next" = "Next"; + +/* button label */ +"WritePaperPhrase.previous" = "Previous"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d of %2$d"; diff --git a/litewallet/Strings/es.lproj/Localizable.strings b/litewallet/Strings/es.lproj/Localizable.strings new file mode 100755 index 000000000..09210630b --- /dev/null +++ b/litewallet/Strings/es.lproj/Localizable.strings @@ -0,0 +1,1293 @@ +/* About screen blog label */ +"About.blog" = "Blog"; + +/* About screen footer */ +"About.footer" = "Hecho por el equipo mundial de Litewallet. Versión %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Política de privacidad"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Acerca de nosotros"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Cerrar"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Centro de ayuda"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Cargando cartera"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Mi Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "GESTIONAR"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Eliminar base de datos"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Su base de datos local está dañada. Vaya a Configuración> Blockchain: Configuración> Eliminar base de datos para actualizar"; + +/* Error alert title */ +"Alert.error" = "Error"; + +/* No internet alert message */ +"Alert.noInternet" = "No hay conexión a Internet. Compruébala y vuelve a intentarlo más tarde."; + +/* Warning alert title */ +"Alert.warning" = "Aviso"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Se han copiado las direcciones"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Todas las direcciones de la cartera se han copiado con éxito."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Conjunto de claves en papel"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "¡Genial!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "Conjunto de PIN"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Resolución de dominio"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "¡Tu dirección fue resuelta!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Error en el envío"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Confirmación de envío"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "¡El dinero se ha enviado!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "Error de serialización de JSON"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "La cartera no está lista"; + +/* API Token error message */ +"ApiClient.tokenError" = "No se puede recuperar el token de API"; + +/* buy button */ +"Button.buy" = "COMPRAR"; + +/* Cancel button label */ +"Button.cancel" = "Cancelar"; + +/* Ignore button label */ +"Button.ignore" = "Ignorar"; + +/* menu button */ +"Button.menu" = "menú"; + +/* No button */ +"Button.no" = "No"; + +/* OK button label */ +"Button.ok" = "Vale"; + +/* receive button */ +"Button.receive" = "recibir"; + +/* resetFields */ +"Button.resetFields" = "Reestablecer campos"; + +/* send button */ +"Button.send" = "Enviar"; + +/* Settings button label */ +"Button.settings" = "Ajustes"; + +/* Settings button label */ +"Button.submit" = "Enviar"; + +/* Yes button */ +"Button.yes" = "Sí"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "COMPRAR"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Compre tarjetas de regalo \n • Recargue teléfonos prepagos \n • Steam, Amazon, Hotels.com \n • Funciona en 170 países"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = " +• Cambiar Litecoin por otras criptos \n • No se requiere identificación \n • Comprar con tarjeta de crédito \n • Cobertura global"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Comprar Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Compre LTC con muchos pares fiat\n• Pague con múltiples métodos\n• Proveedor de pago global"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• ¡Obtenga Litecoin en 5 minutos! \n • Compre Litecoin con tarjeta de crédito \n • Pasaporte o identificación del estado"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Comprar Łitecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Centra tu ID en el recuadro"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "a partir de"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Cantidad a enviar:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Cantidad a donar:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Tiempo de procesamiento: estas transacciones tardarán %1$@ minutos en procesarse."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Tiempo de procesamiento: Esta transacción tardará %1$@ minutos en procesarse."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Enviar"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "CUOTA:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "DIRECCIÓN:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Confirmación"; + +/* To: (address) */ +"Confirmation.to" = "Para"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Costo total:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Las palabras ingresadas no coinciden con su clave de papel. Inténtalo de nuevo."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Para asegurarte de que todo se ha guardado de forma correcta, introduce las palabras a continuación de la clave en papel."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Palabra #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Copiar"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Unidad de visualización Litecoin"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Escoger:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Tasa de cambio"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Este dispositivo no está configurado para enviar correo electrónico mediante una aplicación de iOS"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "Correo electrónico no disponible"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Este dispositivo no está configurado para enviar mensajes."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Mensajería no disponible"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Economía"; + +/* Fee Selector economy fee description */ +"FeeSelector.economyLabel" = "Entrega estimada: más de 10 minutos"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Esta opción no se recomienda para las transacciones urgentes."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Lujo"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Entrega estimada: 2.5 - 5 minutos"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Esta opción prácticamente garantiza la aceptación de su transacción aunque esté pagando una prima."; + +/* Regular fee */ +"FeeSelector.regular" = "Regular"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Entrega estimada: 2.5 - 5+ minutos"; + +/* Fee Selector title */ +"FeeSelector.title" = "Velocidad de procesamiento"; + +/* Confirm */ +"Fragment.confirm" = "Confirmar"; + +/* Or */ +"Fragment.or" = "o"; + +/* sorry */ +"Fragment.sorry" = "Lo siento"; + +/* to */ +"Fragment.to" = "para"; + +/* History Bar Item Title */ +"History.barItemTitle" = "HISTORIA"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Valor actual de LTC en"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Averiguando el saldo de la clave privada..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "¿Quieres enviar %1$@ desde esta clave privada a tu monedero? La red Litecoin recibirá una comisión de %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Esta clave privada ya está en tu cartera."; + +/* empty private key error message */ +"Import.Error.empty" = "Esta clave privada está vacía."; + +/* High fees error message */ +"Import.Error.highFees" = "Las comisiones de transacción costarán más que los fondos disponibles en esta clave privada."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Clave privada no válida"; + +/* Import signing error message */ +"Import.Error.signing" = "Error al firmar la transacción"; + +/* Import button label */ +"Import.importButton" = "Importar"; + +/* Importing wallet progress view label */ +"Import.importing" = "Importando cartera"; + +/* Caption for graphics */ +"Import.leftCaption" = "Monedero por importar"; + +/* Import wallet intro screen message */ +"Import.message" = "Importar una cartera transfiere todo el dinero de tu otra carpeta a tu cartera de Litewallet mediante una sola transacción."; + +/* Enter password alert view title */ +"Import.password" = "Esta clave privada está protegida con una contraseña."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "contraseña"; + +/* Caption for graphics */ +"Import.rightCaption" = "Tu cartera Litewallet"; + +/* Scan Private key button label */ +"Import.scan" = "Escanear clave privada"; + +/* Import wallet success alert title */ +"Import.success" = "Éxito"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Cartera importada con éxito."; + +/* Import Wallet screen title */ +"Import.title" = "Importar cartera"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Desbloqueando la clave"; + +/* Import wallet intro warning message */ +"Import.warning" = "Al importar una cartera no se incluye el historial de transacciones ni otros detalles."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Contraseña incorrecta, inténtalo de nuevo."; + +/* Close app button */ +"JailbreakWarnings.close" = "Cerrar"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignorar"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "SEGURIDAD DEL DISPOSITIVO COMPROMETIDA\nCualquier aplicación \"jailbreak\" puede acceder a los datos del llavero de Litewallet y robar tus Litecoins. Borra esta cartera de inmediato y restáurala en un dispositivo seguro."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "SEGURIDAD DEL DISPOSITIVO COMPROMETIDA\nCualquier aplicación \"jailbreak\" puede acceder a los datos del llavero de Litewallet y robar tus Litecoins. Usa Litewallet únicamente en un dispositivo sin jailbreak."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "AVISO"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Borrar"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Los servicios de ubicación están desactivados."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet no tiene permiso para acceder a los servicios de ubicación."; + +/* No comment provided by engineer. */ +"Malformed URI" = "URI malformado"; + +/* Balance */ +"ManageWallet.balance" = "Saldo"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Has creado una cartera en %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "El nombre de tu cartera solo aparece en el historial de transacciones de tu cuenta y nadie más lo puede ver."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Nombre de la cartera"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Gestionar cartera"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Comprar Litecoins"; + +/* Menu button title */ +"MenuButton.customer.support" = "Atención al cliente"; + +/* Menu button title */ +"MenuButton.lock" = "Bloquear cartera"; + +/* Menu button title */ +"MenuButton.security" = "Centro de seguridad"; + +/* Menu button title */ +"MenuButton.settings" = "Ajustes"; + +/* Menu button title */ +"MenuButton.support" = "Ayuda técnica"; + +/* button label */ +"MenuViewController.createButton" = "Crear nueva cartera"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menú"; + +/* button label */ +"MenuViewController.recoverButton" = "Recuperar cartera"; + +/* No comment provided by engineer. */ +"No wallet" = "Sin billetera"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Cambiar al modo automático"; + +/* Node is connected label */ +"NodeSelector.connected" = "Conectado"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Introduce el puerto y la dirección IP del nodo (opcional)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Introducir nodo"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Cambiar al modo manual"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Nodo principal actual"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "No conectado"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Estado de la conexión del nodo"; + +/* Node Selector view title */ +"NodeSelector.title" = "Nodos Litecoin"; + +/* "Email address label" */ +"Notifications.emailLabel" = "Dirección de correo electrónico"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Entre aquí"; + +/* "Email title" */ +"Notifications.emailTitle" = "¡No te pierdas nada!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Idioma preferido:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Regístrese para escuchar sobre actualizaciones y concursos."; + +/* Signup cancel */ +"Notifications.signupCancel" = "No, gracias."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Solicitud de pago incorrecta"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Documento dañado o no compatible"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "certificado ausente"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "solicitud expirada"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "No se ha podido realizar el pago"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Los pagos en Litecoins no pueden ser inferiores a %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "La transacción de Litecoins no puede ser inferior a $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "tipo de firma no compatible"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "el certificado no es de confianza"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DESCARTAR"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Se necesita una contraseña de dispositivo para proteger tu cartera. Ve a los ajustes y activa la contraseña."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Activar el código de acceso del dispositivo"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Seguir"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Debes tener tu clave de papel guardada por si pierdes o cambias de teléfono. Pulsa aquí para continuar."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Cancelar"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Habilitar"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Acción obligatoria"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Tu cartera puede no estar sincronizada. Esto a menudo puede arreglarse volviendo a escanear la cadena de bloques."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transacción rechazada"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet se ha actualizado y ahora utiliza un PIN de 6 dígitos. Toca aquí para actualizar."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Actualizar PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Ayuda a mejorar Litewallet compartiendo tus datos anónimos con nosotros"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Compartir datos anónimos"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Toca aquí para activar Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Activar Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "RECIBIR"; + +/* Address copied message. */ +"Receive.copied" = "Copiado en el portapapeles."; + +/* Share via email button label */ +"Receive.emailButton" = "Correo electrónico"; + +/* Request button label */ +"Receive.request" = "Solicitar una cantidad"; + +/* Share button label */ +"Receive.share" = "Compartir"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Mensaje de texto"; + +/* Receive modal title */ +"Receive.title" = "Recibir"; + +/* Done button text */ +"RecoverWallet.done" = "Listo"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Recuperar cartera"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Restablecer PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Introducir clave en papel"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Recupera tu Litewallet con la clave en papel."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "La clave en papel que acabas de introducir no es válida. Vuelve a comprobar cada palabra e inténtalo de nuevo."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Flecha izquierda"; + +/* Next button label */ +"RecoverWallet.next" = "Siguiente"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Pulsa aquí para obtener más información."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Flecha derecha"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Introduce la clave en papel para la cartera que deseas recuperar."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Para restablecer el PIN, introduce las palabras de tu clave en papel en los cuadros a continuación."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Ingresa primero una cantidad."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Solicitar una cantidad"; + +/* Alert action button label */ +"ReScan.alertAction" = "Sincronizar"; + +/* Alert message body */ +"ReScan.alertMessage" = "No podrás enviar dinero durante la sincronización."; + +/* Alert message title */ +"ReScan.alertTitle" = "¿Quieres sincronizar con la cadena de bloques?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minutos"; + +/* Syncing explanation */ +"ReScan.body2" = "Si una transacción se muestra como completada en la red de Litecoin pero no en Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Obtienes un error repetidamente que dice que tu transacción ha sido rechazada."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Iniciar sincronización"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "No podrás enviar dinero mientras se realiza la sincronización con la cadena de bloques."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Sincronizar cadena de bloques"; + +/* Subheader label */ +"ReScan.subheader1" = "Hora prevista"; + +/* Subheader label */ +"ReScan.subheader2" = "¿Cuándo sincronizar?"; + +/* Reset walet button title */ +"resetButton" = "REINICIAR"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Eliminar mi Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Flash de la cámara"; + +/* Complete filter label */ +"Search.complete" = "completar"; + +/* Pending filter label */ +"Search.pending" = "pendiente"; + +/* Received filter label */ +"Search.received" = "recibido"; + +/* Sent filter label */ +"Search.sent" = "enviado"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Activa todas las funciones de seguridad para una protección máxima."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "La única manera de acceder a tu Litecoin si pierdes o actualizas tu teléfono."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Clave en papel"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Protege tu Litewallet de usuarios no autorizados."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "PIN de 6 dígitos"; + +/* Security Center Title */ +"SecurityCenter.title" = "Centro de seguridad"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Desbloquea tu Litewallet de forma cómoda y envía dinero hasta un límite establecido."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Cantidad"; + +/* Balance: $4.00 */ +"Send.balance" = "Saldo: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Tarifa: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "ENVIAR"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Ve a Ajustes para activar el acceso a la cámara."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet no tiene permisos para acceder a la cámara"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "El destino es tu propia dirección. No puedes hacerte un envío a ti mismo."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "No se pudo crear la transacción."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Memo"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Pasteboard está vacío"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Introduzca una dirección de Litecoin"; + +/* Fees: $0.01*/ +"Send.fee" = "Honorarios: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Honorarios:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "La identidad del beneficiario no está certificada."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Fondos insuficientes"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "La dirección de destino no es una dirección Litecoin válida."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Pasteboard no contiene una dirección Litecoin válida."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Dirección inválida"; + +/* Is rescanning error message */ +"Send.isRescanning" = "El envío se inhabilita durante un reescaneado completo."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Cargando solicitud"; + +/* Network */ +"Send.networkFee" = "Red"; + +/* Empty address alert message */ +"Send.noAddress" = "Introduce la dirección del destinatario."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Introduce el importe a enviar."; + +/* Paste button label */ +"Send.pasteLabel" = "Pegar"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "No se pudo publicar la transacción."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "No se pudo cargar la solicitud de pago"; + +/* Scan button label */ +"Send.scanLabel" = "Escanear"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Enviar"; + +/* Service */ +"Send.serviceFee" = "Servicio"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Enviar"; + +/* Send money to label */ +"Send.toLabel" = "A"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "dominio"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Entrar a"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Buscar"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Lo sentimos, no se encontró el dominio. [Error: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Búsqueda fallida"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Ingrese un dominio .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao o .x."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Introducir dominio"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Problema de búsqueda del sistema. [Error: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Las direcciones Litecoin están diseñadas para un solo uso."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "La reutilización reduce la privacidad tanto para ti como para el destinatario y puede resultar en pérdida si el destinatario no controla la dirección directamente."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Ya se ha utilizado la dirección"; + +/* About label */ +"Settings.about" = "Acerca de nosotros"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Ajustes avanzados"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Cadena articulada"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "¿Está seguro de que desea cambiar el idioma a %l?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Mostrar moneda"; + +/* Current Locale */ +"Settings.currentLocale" = "Local actual:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Únete al acceso anticipado"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "¿Te gusta Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Importar cartera"; + +/* Languages label */ +"Settings.languages" = "Idiomas"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Ambiente:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Socios de Litewallet"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Versión Litewallet:"; + +/* Manage settings section header */ +"Settings.manage" = "Gestionar"; + +/* Notifications label */ +"Settings.notifications" = "Notificaciones"; + +/* Leave review button label */ +"Settings.review" = "Déjanos una valoración"; + +/* Share anonymous data label */ +"Settings.shareData" = "Compartir datos anónimos"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Support settings section header */ +"Settings.support" = "Apoyo"; + +/* Sync blockchain label */ +"Settings.sync" = "Sincronizar cadena de bloques"; + +/* Settings title */ +"Settings.title" = "Ajustes"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Límite de gasto de Touch ID"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Cartera"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Iniciar/recuperar otra cartera"; + +/* Share data view body */ +"ShareData.body" = "Ayuda a mejorar Litewallet compartiendo tus datos anónimos con nosotros. Esto no incluye ninguna información financiera. Respetamos tu privacidad financiera."; + +/* Share data header */ +"ShareData.header" = "¿Compartir datos?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "¿Compartir datos anónimos?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Límite de gasto actual: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Apunta nuevamente la clave en papel"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Tu clave de papel es la única manera de restaurar tu Litewallet si pierdes tu teléfono, o te lo roban, se rompe o se actualiza.\n\nTe mostraremos una lista de palabras para apuntar en un trozo de papel y guardarlo en un sitio seguro."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Apuntar la clave en papel"; + +/* Argument is date */ +"StartPaperPhrase.date" = "La última vez que anotaste tu clave en papel fue el %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "La forma más segura y sencilla de utilizar Litecoin"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Apoyar la Fundación Litecoin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Conectando ..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Reescaneando ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "¡Éxito!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Sincronizando ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Conectando"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Sincronizando"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Puedes personalizar el límite de gasto de tu Touch ID desde %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Utiliza tu huella digital para desbloquear Litewallet y enviar dinero hasta un límite establecido."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Pantalla de límite de gasto del Touch ID"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Límite de gasto: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Activa Touch ID para Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "No has configurado Touch ID en este dispositivo. Ve a Configuración->Touch ID & Passcode para configurarlo ahora."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID no está configurado"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Pedir siempre el código de acceso"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Se te pedirá que introduzcas tu PIN de 6 dígitos para enviar cualquier transacción que supere tu límite de gasto y cada 48 horas desde la última vez que hayas introducido tu PIN de 6 dígitos."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Límite de gasto de Touch ID"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Detalle de cantidad:"; + +/* Availability status text */ +"Transaction.available" = "Disponible para gastar"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Bloquear:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memorándum:"; + +/* Transaction complete label */ +"Transaction.complete" = "Completada"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Detalle del monto final de la transacción"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Saldo final: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Tipo de cambio cuando se recibió:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Tipo de cambio cuando se envió:"; + +/* (b600 fee) */ +"Transaction.fee" = "(cuota de %1$@ )"; + +/* Invalid transaction */ +"Transaction.invalid" = "INVÁLIDO"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "ahora mismo"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "En curso: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "En curso: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Saldo inicial: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Detalle del monto inicial de la transacción"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Esperando confirmación. Algunos comerciantes requieren confirmación para completar una transacción. Tiempo estimado: 1-2 horas."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "cuenta"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Cantidad"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Confirmado en bloque"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Memo"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Copiado"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Copiar todos los detalles"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Tus transacciones aparecerán aquí."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "en %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Menos"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "He movido %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Movidos %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "No confirmado"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "a partir de"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "Recibido %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Recibido %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "RECIBIR DIRECCIÓN"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "Enviado %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Enviado %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Estado"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Detalles de la transacción"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "a %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Identificador de transacción de Litecoin"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Recibido en esta dirección"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Enviado a esta dirección"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Desactivado hasta: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Ingrese su PIN"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Desbloquear con FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Mi dirección"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Restablecer PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Escanear"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Desbloquea tu Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Desbloquear con TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Cartera desbloqueada"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Recuerda este PIN. Si lo olvidas, no podrás acceder a tu Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Tu PIN se usará para desbloquear Litewallet y enviar dinero."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Establecer PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Vuelve a introducir el PIN"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Introduce tu PIN actual."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Introduce tu nuevo PIN."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Vuelve a introducir tu nuevo PIN."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "No se pudo actualizar el PIN."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Error al actualizar el PIN"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Actualizar PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "¿Quieres copiar las direcciones de la cartera al portapapeles?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Autoriza para copiar la dirección de la cartera al portapapeles"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Copiar direcciones de la cartera"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Copiar"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Introduce tu PIN para autorizar esta transacción."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Introduce tu PIN para continuar"; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN requerido"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Autorizar esta transacción"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Abre la aplicación Litewallet en iPhone para configurar tu cartera."; + +/* Dismiss button label */ +"Webview.dismiss" = "Descartar"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Se produjo un error al cargar el contenido. Vuelve a intentarlo."; + +/* Updating webview message */ +"Webview.updating" = "Actualizando..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "¡Bienvenido a Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "¡Bienvenido a Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Error de corrupción local"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "¿Seguro que quieres eliminar esta cartera?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "¿Borrar cartera?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Eliminar base de datos"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Esto elimina la base de datos pero conserva el PIN y la frase. Confirme su PIN actual, inicialice y espere para completar la sincronización con la nueva base de datos"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Eliminar y sincronizar"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "¿Olvidaste la semilla o el PIN?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "No se ha podido borrar la cartera."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "No se ha realizado"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Introduce la frase de recuperación de esta cartera para borrarla y comenzar o recuperar otra. Tu saldo actual sigue en esta frase."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Iniciar o recuperar otra cartera te permite acceder y administrar una cartera de Litewallet diferente en este dispositivo."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Ya no podrás acceder a tu cartera Litewallet actual desde este dispositivo. El saldo se mantendrá en la frase."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Comenzar o recuperar otra cartera"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "¡Esta acción borrará tu Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Eliminar su billetera significa que la clave privada y borrar los datos de la aplicación desaparecerán. ¡Puedes perder Litecoin para siempre!\n\n\nNadie en el equipo de Litewallet puede recuperar esta semilla por usted. No somos responsables si no presta atención a esta advertencia."; + +/* Warning title */ +"WipeWallet.warningTitle" = "¡POR FAVOR LEE!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Borrar"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Borrando..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Apunta cada palabra en orden y guárdala en un lugar seguro."; + +/* button label */ +"WritePaperPhrase.next" = "Siguiente"; + +/* button label */ +"WritePaperPhrase.previous" = "Anterior"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d de %2$d"; diff --git a/litewallet/Strings/fr.lproj/Localizable.strings b/litewallet/Strings/fr.lproj/Localizable.strings new file mode 100755 index 000000000..566c3e0f1 --- /dev/null +++ b/litewallet/Strings/fr.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "Blogue"; + +/* About screen footer */ +"About.footer" = "Fait par l'équipe mondiale de Litewallet. Version %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Politique de confidentialité"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "À propos de"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Fermer"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Centre d'assistance"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Chargement du portefeuille"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Mon Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "GÉRER"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Erreur de corruption locale"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Votre base de données locale est corrompue. Accédez à Paramètres> Blockchain: Paramètres> Supprimer la base de données pour actualiser"; + +/* Error alert title */ +"Alert.error" = "Erreur"; + +/* No internet alert message */ +"Alert.noInternet" = "Aucune connexion Internet n'a été trouvée. Vérifiez votre connexion et réessayez."; + +/* Warning alert title */ +"Alert.warning" = "Avertissement"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Adresses copiées"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Toutes les adresses du portefeuille ont été copiées avec succès."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Ensemble de clés de papier"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Impressionnant !"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "Ensemble PIN"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Résolution de domaine"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "L'adresse a été résolue!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Échec de l'envoi"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Envoyer confirmation"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Argent envoyé !"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "Erreur de sérialisation de JSON"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Le portefeuille n'est pas prêt"; + +/* API Token error message */ +"ApiClient.tokenError" = "Impossible de récupérer le jeton de l'API"; + +/* buy button */ +"Button.buy" = "ACHETER"; + +/* Cancel button label */ +"Button.cancel" = "Annuler"; + +/* Ignore button label */ +"Button.ignore" = "Ignorer"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "Non"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "recevoir"; + +/* resetFields */ +"Button.resetFields" = "Réinitialisation des champs"; + +/* send button */ +"Button.send" = "envoyer"; + +/* Settings button label */ +"Button.settings" = "Paramètres"; + +/* Settings button label */ +"Button.submit" = "Envoyer"; + +/* Yes button */ +"Button.yes" = "Oui"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "ACHETER"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Acheter des cartes-cadeaux \n • Recharger des téléphones prépayés \n • Steam, Amazon, Hotels.com \n • Fonctionne dans 170 pays"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Changer Litecoin pour d'autres cryptos \n • Aucun ID requis \n • Acheter par carte de crédit \n • Couverture mondiale"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Acheter Litecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Achetez des LTC avec de nombreuses paires de fiat\n• Payez avec plusieurs méthodes\n• Fournisseur de paiement mondial"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Obtenez du Litecoin en 5 minutes! \n • Achetez du Litecoin par carte de crédit \n • Passeport ou identifiant d'État"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Acheter Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Centrez votre identifiant dans le champ"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Détails de l'échange:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Montant à envoyer :"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Montant à donner:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Temps de traitement: le traitement de ces transactions prendra %1$@ minutes."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Temps de traitement : Le traitement de cette transaction prendra %1$@ minutes à traiter."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Envoyer"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "FRAIS:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADRESSE"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Confirmation"; + +/* To: (address) */ +"Confirmation.to" = "À"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Coût total :"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Les mots saisis ne correspondent pas à votre clé papier. Veuillez réessayer."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Pour vous assurer que tout a été écrit correctement, entrez les mots suivants à partir de votre clé de papier."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Mot no.%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Copie"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin Display Unit"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Choisir:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Taux de change"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Cet appareil n'est pas configuré pour envoyer un e-mail avec l'application e-mail pour iOS."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "E-mail non disponible"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Cet appareil n'est pas configuré pour envoyer des messages."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Messagerie non disponible"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Économie"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Livraison estimée : 10 minutes ou +"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Cette option n'est pas recommandée pour les transactions dont les délais sont cruciaux."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Luxe"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Livraison estimée: 2.5 - 5 minutes"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Cette option garantit pratiquement l'acceptation de votre transaction même si vous payez une prime."; + +/* Regular fee */ +"FeeSelector.regular" = "Régulier"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Livraison estimée: 2.5 - 5+ minutes"; + +/* Fee Selector title */ +"FeeSelector.title" = "Vitesse de traitement"; + +/* Confirm */ +"Fragment.confirm" = "Confirmer"; + +/* Or */ +"Fragment.or" = "ou"; + +/* sorry */ +"Fragment.sorry" = "Pardon"; + +/* to */ +"Fragment.to" = "à"; + +/* History Bar Item Title */ +"History.barItemTitle" = "HISTOIRE"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Valeur LTC actuelle en"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Vérification du solde des clés privées..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Envoyer %1$@ de cette clé privée dans votre portefeuille ? Le réseau Litecoin recevra des frais de %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Cette clé privée est déjà dans votre portefeuille."; + +/* empty private key error message */ +"Import.Error.empty" = "Cette clé privée est vide."; + +/* High fees error message */ +"Import.Error.highFees" = "Les frais de transactions excederaient le sold disponible sur cette clé privée"; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Pas une clé privée valide"; + +/* Import signing error message */ +"Import.Error.signing" = "Erreur lors de la signature de la transaction"; + +/* Import button label */ +"Import.importButton" = "Importer"; + +/* Importing wallet progress view label */ +"Import.importing" = "Importation du portefeuille"; + +/* Caption for graphics */ +"Import.leftCaption" = "Portefeuille à importer"; + +/* Import wallet intro screen message */ +"Import.message" = "L'importation d'un portefeuille transfère tout l'argent de votre autre portefeuille vers votre portefeuille Litewallet avec une seule transaction."; + +/* Enter password alert view title */ +"Import.password" = "Cette clé privée est protégée par mot de passe."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "mot de passe"; + +/* Caption for graphics */ +"Import.rightCaption" = "Votre portefeuille de Litewallet"; + +/* Scan Private key button label */ +"Import.scan" = "Scanner la clé privée"; + +/* Import wallet success alert title */ +"Import.success" = "Succès"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Portefeuille importé avec succès."; + +/* Import Wallet screen title */ +"Import.title" = "Importer le portefeuille"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Clé de déverrouillage"; + +/* Import wallet intro warning message */ +"Import.warning" = "L'importation d'un portefeuille n'inclut pas l'historique des transactions ou d'autres détails."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Mauvais mot de passe, veuillez essayer de nouveau."; + +/* Close app button */ +"JailbreakWarnings.close" = "Fermer"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignorer"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "SÉCURITÉ DU DISPOSITIF COMPROMISE\n  Toute application « évasive » peut accéder aux données du porte-clé de Litewallet et voler vos Litecoins ! Détruisez ce portefeuille immédiatement et restaurez-le sur un appareil sécurisé."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "SÉCURITÉ DU DISPOSITIF COMPROMISE\n  Toute application « évasive » peut accéder aux données du porte-clé de Litewallet et voler vos Litecoins. N'utilisez Litewallet que sur un appareil non évasif."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "AVERTISSEMENT"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Supprimer"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Les services de localisation sont désactivés."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet n'a pas l'autorisation d'accéder aux services de localisation."; + +/* No comment provided by engineer. */ +"Malformed URI" = "URI mal formé"; + +/* Balance */ +"ManageWallet.balance" = "Solde"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Vous avez créé votre portefeuille sur %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Le nom de votre portefeuille n'apparaît que dans l'historique des transactions de votre compte et ne peut être vu par personne d'autre."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Nom du portefeuille"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Gérer le portefeuille"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Acheter des Litecoins"; + +/* Menu button title */ +"MenuButton.customer.support" = "Service client"; + +/* Menu button title */ +"MenuButton.lock" = "Verrouiller le portefeuille"; + +/* Menu button title */ +"MenuButton.security" = "Centre de sécurité"; + +/* Menu button title */ +"MenuButton.settings" = "Paramètres"; + +/* Menu button title */ +"MenuButton.support" = "Assistance"; + +/* button label */ +"MenuViewController.createButton" = "Créer un nouveau portefeuille"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Récupérer le portefeuille"; + +/* No comment provided by engineer. */ +"No wallet" = "Pas de portefeuille"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Passer au mode automatique"; + +/* Node is connected label */ +"NodeSelector.connected" = "Connecté"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Entrer l'adresse IP et le port des nœuds (optionnel)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Soumettre le nœud"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Passer au mode manuel"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Nœud principal actuel"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Non connecté"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Statut de connexion du nœud"; + +/* Node Selector view title */ +"NodeSelector.title" = "Nœud litecoin"; + +/* "Email address label" */ +"Notifications.emailLabel" = "Adresse e-mail"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Entrer ici"; + +/* "Email title" */ +"Notifications.emailTitle" = "Ne manquez rien!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Langue préférée:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Inscrivez-vous pour entendre parler des mises à jour et des concours."; + +/* Signup cancel */ +"Notifications.signupCancel" = "Non merci."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Mauvaise demande de paiement"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Document non pris en charge ou endommagé"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "certificat manquant"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "demande expirée"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Impossible de faire un paiement"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Les paiements par Litecoin ne peuvent être inférieurs à %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Les résultats des transactions de Litecoin ne peuvent pas être inférieurs à $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "type de signature non pris en charge"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "certificat non approuvé"; + +/* Dismiss button. */ +"Prompts.dismiss" = "REJETER"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Un code d'accès pour l'appareil est requis pour sécuriser votre portefeuille. Allez aux paramètres et activez le code d'accès."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Activez le code d'accès de l'appareil"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Continuer"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Votre paperkey doit être sauvegardé au cas où vous perdiez ou changiez votre téléphone. Cliquez ici pour continuer."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Annuler"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Activer"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Une action est requise"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Votre portefeuille peut être désynchronisé. Cela peut souvent être corrigé en scannant la chaîne de blocs à nouveau."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transaction rejetée"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet a été mis à niveau pour utiliser un code PIN à six chiffres. Appuyez ici pour mettre à niveau."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Mettre à jour le code PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Aidez à améliorer Litewallet en partageant vos données anonymes avec nous"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Partager les données anonymes"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Tapez ici pour activer Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Activer Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "RECEVOIR"; + +/* Address copied message. */ +"Receive.copied" = "Copié dans le presse-papier."; + +/* Share via email button label */ +"Receive.emailButton" = "E-mail"; + +/* Request button label */ +"Receive.request" = "Demander un montant"; + +/* Share button label */ +"Receive.share" = "Partager"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Message texte"; + +/* Receive modal title */ +"Receive.title" = "Recevoir"; + +/* Done button text */ +"RecoverWallet.done" = "Terminé"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Récupérer le portefeuille"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Réinitialiser PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Entrez la clé de papier"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Récupérez votre Litewallet avec votre clé de papier."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "La clé de papier saisie n'est pas valide. Revérifiez chaque mot et réessayez."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Flèche gauche"; + +/* Next button label */ +"RecoverWallet.next" = "Suivant"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Cliquez ici pour plus d'informations."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Flèche droite"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Entrez la clé de papier pour le portefeuille que vous souhaitez récupérer."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Pour réinitialiser votre NIP, entrez les mots de votre clé papier dans les cases ci-dessous."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Veuillez entrer un montant d'abord."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Demander un montant"; + +/* Alert action button label */ +"ReScan.alertAction" = "Synchroniser"; + +/* Alert message body */ +"ReScan.alertMessage" = "Vous ne pourrez pas envoyer d'argent lors de la synchronisation."; + +/* Alert message title */ +"ReScan.alertTitle" = "Synchroniser avec la chaîne de bloc ?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minutes"; + +/* Syncing explanation */ +"ReScan.body2" = "Si une transaction apparaît comme achevée sur le réseau de Litecoin mais pas dans votre Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Vous recevez de manière répétée un message d'erreur vous informant du rejet de votre transaction."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Démarrer la synchronisation"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Vous ne pourrez pas envoyer d'argent en synchronisant avec la chaîne de blocs."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Synchroniser chaîne de blocs"; + +/* Subheader label */ +"ReScan.subheader1" = "Temps estimé"; + +/* Subheader label */ +"ReScan.subheader2" = "Quand synchroniser ?"; + +/* Reset walet button title */ +"resetButton" = "Oui, réinitialiser le portefeuille"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Supprimer mon Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Flash d'appareil photo"; + +/* Complete filter label */ +"Search.complete" = "achevé"; + +/* Pending filter label */ +"Search.pending" = "en attente"; + +/* Received filter label */ +"Search.received" = "reçu"; + +/* Sent filter label */ +"Search.sent" = "envoyé"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Activez toutes les fonctionnalités de sécurité pour une protection maximale."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "La seule façon d'accéder à votre Litecoin si vous perdez ou mettez à niveau votre téléphone."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Clé de papier"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Protège votre Litewallet contre les utilisateurs non autorisés."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "PIN à six chiffres"; + +/* Security Center Title */ +"SecurityCenter.title" = "Centre de sécurité"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Débloquez convenablement votre Litewallet et envoyez de l'argent jusqu'à une limite définie."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Montant"; + +/* Balance: $4.00 */ +"Send.balance" = "Solde : %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Frais: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "ENVOYER"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Accédez à Paramètres pour autoriser l'accès à la caméra."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet n'est pas autorisé à accéder à la caméra"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "La destination est votre propre adresse. Vous ne pouvez pas envoyer à vous même."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Impossible de créer une transaction."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Mémo"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Le tableau d'affichage est vide."; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Entrez une adresse Litecoin"; + +/* Fees: $0.01*/ +"Send.fee" = "Frais: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Frais:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "L'identité du bénéficiaire n'est pas certifiée."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Fonds insuffisants"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "L'adresse de destination n'est pas une adresse Litecoin valide."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Le tableau d'affichage ne contient aucune adresse Litecoin valide."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Adresse non valide"; + +/* Is rescanning error message */ +"Send.isRescanning" = "L'envoi est désactivé pendant un renumérisation complète."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Chargement de la demande"; + +/* Network */ +"Send.networkFee" = "Réseau"; + +/* Empty address alert message */ +"Send.noAddress" = "Entrez l'adresse du destinataire."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Entrez un montant à envoyer."; + +/* Paste button label */ +"Send.pasteLabel" = "Coller"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Impossible de publier la transaction."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Impossible de charger la demande de paiement"; + +/* Scan button label */ +"Send.scanLabel" = "Scanner"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Envoyer"; + +/* Service */ +"Send.serviceFee" = "Service"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Envoyer"; + +/* Send money to label */ +"Send.toLabel" = "Pour"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "domaine"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Entrez un"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Chercher"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Désolé, le domaine est introuvable. [Erreur: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "La recherche a échoué"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Saisissez un domaine .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao ou .x."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Entrez le domaine"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Problème de recherche système. [Erreur: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Les adresses Litecoin sont destinées à une utilisation unique."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "La réutilisation réduit la confidentialité pour vous et le destinataire et peut entraîner une perte si le destinataire ne contrôle pas directement l'adresse."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Adresse déjà utilisée"; + +/* About label */ +"Settings.about" = "À propos de"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Paramètres avancés"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Bloc de la chaîne"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Voulez-vous vraiment changer la langue en %l?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Afficher la devise"; + +/* Current Locale */ +"Settings.currentLocale" = "Paramètres régionaux actuels:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Rejoignez l'accès anticipé"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Vous aimez Litewallet ?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Importer le portefeuille"; + +/* Languages label */ +"Settings.languages" = "Les langues"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Environnement:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Partenaires Litewallet"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Version Litewallet:"; + +/* Manage settings section header */ +"Settings.manage" = "Gérer"; + +/* Notifications label */ +"Settings.notifications" = "Notifications"; + +/* Leave review button label */ +"Settings.review" = "Laissez-nous un commentaire"; + +/* Share anonymous data label */ +"Settings.shareData" = "Partager des données anonymes"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Support settings section header */ +"Settings.support" = "Soutien"; + +/* Sync blockchain label */ +"Settings.sync" = "Synchroniser chaîne de blocs"; + +/* Settings title */ +"Settings.title" = "Paramètres"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Limite de dépenses de Touch ID"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Portefeuille"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Démarrer/récupérer un autre portefeuille"; + +/* Share data view body */ +"ShareData.body" = "Aidez à améliorer Litewallet en partageant vos données anonymes avec nous. Cela n'inclut aucune information financière. Nous respectons votre confidentialité financière."; + +/* Share data header */ +"ShareData.header" = "Partager les données ?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Partager des données anonymes ?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Limite de dépenses actuelle: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Enregistrez la clé de papier à nouveau"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Votre clé de papier est la seule façon de restaurer votre Litewallet si votre téléphone est perdu, volé, cassé ou mis à niveau.\n\nNous vous montrerons une liste de mots à écrire sur un morceau de papier et à garder en sécurité."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Écrire la clé de papier"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Vous avez finalement écrit votre clé de papier sur %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "La manière la plus sûre et la plus simple d'utiliser Litecoin."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Soutenez la Fondation Litecoin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "De liaison..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Nouvelle numérisation ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Succès!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Synchronisation ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Connexion…"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Synchronisation"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Vous pouvez personnaliser votre plafond de dépenses Touch ID à partir de %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Utilisez votre empreinte digitale pour débloquer votre Litewallet et envoyer de l'argent jusqu'à une limite définie."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Écran de plafond de dépenses Touch ID"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Limite de dépenses : %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Activer Touch ID pour Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Vous n'avez pas configuré Touch ID sur cet appareil. Accédez à Paramètres -> Touch ID et mot de passe pour le configurer maintenant."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID non configuré"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Toujours demander un code d'accès"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Vous devrez entrer votre code PIN à six chiffres pour envoyer une transaction supérieure à votre limite de dépenses et toutes les 48 heures depuis la dernière fois que vous avez entré votre code PIN à six chiffres."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Limite de dépenses de Touch ID"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Détail du montant de la transaction"; + +/* Availability status text */ +"Transaction.available" = "Disponible pour dépenser"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Blouquer:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo:"; + +/* Transaction complete label */ +"Transaction.complete" = "Terminer"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Détail du montant final de la transaction"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Solde final : %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Taux de change au moment de la réception :"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Taux de change au moment de l'envoi :"; + +/* (b600 fee) */ +"Transaction.fee" = "(frais de %1$@)"; + +/* Invalid transaction */ +"Transaction.invalid" = "NON VALIDE"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "juste maintenant"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "En cours : %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "En cours : %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Solde initial : %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Détail du montant de départ de la transaction"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "En attente d'être confirmé. Certains vendeurs exigent une confirmation pour mener à bien une transaction. Temps estimé : 1-2 heures."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "compte"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Montant"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Confirmé en bloc"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Mémo"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Copié"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Copiez tous les détails"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Vos transactions apparaîtront ici."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "à %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Moins"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "Transféré %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Déplacé %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Non confirmé"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "au"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "A reçu %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Reçu %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "RECEVOIR L'ADRESSE"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "A envoyé %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Envoyé %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "État"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Détails de la transaction"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "jusqu'à %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Identifiant de transaction Litecoin"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Reçu à cette adresse"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Envoyé à cette adresse"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Désactivé jusqu'à : %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Entrez le code PIN"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Déverrouillez avec FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Mon adresse"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Réinitialiser PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Scanner"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Déverrouillez votre Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Déverrouiller avec TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Portefeuille déverrouillé"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Souvenez-vous de ce code PIN. Si vous l'oubliez, vous ne pourrez pas accéder à votre Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Votre NIP sera utilisé pour débloquer votre Litewallet et envoyer de l'argent."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Définir PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Entrer le PIN à nouveau"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Entrez votre code PIN actuel."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Entrez votre code PIN nouveau."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Entrez votre nouveau PIN à nouveau."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Désolés, nous n'avons pas pu mettre à jour le code PIN."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Erreur de mise à jour du code PIN"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Mettre à jour le code PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Copier les adresses du portefeuille dans le presse-papiers ?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Autoriser à copier l'adresse du portefeuille dans le presse-papiers"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Copier les adresses du portefeuille"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Copier"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Veuillez entrer votre NIP pour autoriser cette transaction."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Entrez votre code PIN pour continuer."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN requis"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Autoriser cette transaction"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Ouvrez l'application Litewallet pour iPhone pour configurer votre portefeuille."; + +/* Dismiss button label */ +"Webview.dismiss" = "Rejeter"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Une erreur s'est produite lors du chargement du contenu. Veuillez réessayer."; + +/* Updating webview message */ +"Webview.updating" = "Mise à jour..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Bienvenue chez Litewallet !"; + +/* Top title of welcome screen */ +"Welcome.title" = "Bienvenue chez Litewallet !"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Supprimer la base de données"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Souhaitez-vous vraiment supprimer ce portefeuille ?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Effacer le portefeuille ?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Supprimer la base de données"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Cela supprime la base de données mais conserve le code PIN et la phrase. Confirmez votre code PIN existant, amorcez et attendez la fin de la synchronisation avec la nouvelle base de données"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Supprimer et synchroniser"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Oubliez les graines ou le NIP?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Échec de la suppression du portefeuille."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Échec"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Soumettez la phrase de récupération de ce portefeuille pour l'effacer et commencer à en récupérer un autre. Votre solde actuel demeure sur cette phrase."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "L'ouverture ou la récupération d'un autre portefeuille vous permet d'accéder et de gérer différents portefeuilles Litewallet depuis cet appareil."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Vous ne pourrez plus accéder à votre portefeuille Litewallet depuis cet appareil. Le solde restera inchangé."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Commencer un nouveau portefeuille ou récupérer un autre portefeuille"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Cette action effacera votre Litewallet !"; + +/* Warning description */ +"WipeWallet.warningDescription" = "La suppression de votre portefeuille signifie que la clé privée et l'effacement des données de l'application auront disparu. Vous risquez de perdre du Litecoin pour toujours !\n\n\nPersonne dans l'équipe Litewallet ne peut récupérer cette graine pour vous. Nous ne sommes pas responsables si vous ne tenez pas compte de cet avertissement."; + +/* Warning title */ +"WipeWallet.warningTitle" = "LISEZ S'IL VOUS PLAÎT!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Effacer"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Effacement..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Écrivez chaque mot en ordre et rangez-le dans un lieu sûr."; + +/* button label */ +"WritePaperPhrase.next" = "Suivant"; + +/* button label */ +"WritePaperPhrase.previous" = "Précédent"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d de %2$d"; diff --git a/litewallet/Strings/id.lproj/Localizable.strings b/litewallet/Strings/id.lproj/Localizable.strings new file mode 100644 index 000000000..44088091d --- /dev/null +++ b/litewallet/Strings/id.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen website label */ +"About.blog" = "Situs Web"; + +/* About screen footer */ +"About.footer" = "Dibuat Oleh LiteWallet Tim \n dari \n Yayasan Litecoin \n %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Aturan Pribadi"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Mengenai"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Tutup"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Dukungan Pusat"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Muatan Dompet"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Litewallet Saya"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "MENGATUR"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Hapus Database"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Ini menghapus basis data tetapi tetap menggunakan PIN dan frasa. Konfirmasikan PIN Anda yang ada, seed dan tunggu untuk menyinkronkan ke db baru"; + +/* Error alert title */ +"Alert.error" = "Kesalahan"; + +/* No internet alert message */ +"Alert.noInternet" = "Tidak Menemukan Hubungan Internet. Periksa koneksi Anda dan coba lagi."; + +/* Warning alert title */ +"Alert.warning" = "Peringatan"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Alamat Tersalin"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Semua alamat dompet berhasil tersalin."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Set Kunci Kertas"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Luar biasa!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "Set PIN"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Resolusi Domain"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Alamatnya sudah teratasi!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Pengiriman gagal"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Kirim Konfirmasi"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Uang Terkirim!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON Kesalahan Serialisasi"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Dompet belum siap"; + +/* API Token error message */ +"ApiClient.tokenError" = "Tidak mampu mendapatkan kembali bukti API"; + +/* buy button */ +"Button.buy" = "membeli"; + +/* Cancel button label */ +"Button.cancel" = "Membatalkan"; + +/* Ignore button label */ +"Button.ignore" = "Mengabaikan"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "Tidak"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "menerima"; + +/* resetFields */ +"Button.resetFields" = "Setel ulang bidang"; + +/* send button */ +"Button.send" = "kirim"; + +/* Settings button label */ +"Button.settings" = "Pengaturan"; + +/* Settings button label */ +"Button.submit" = "Mengajukan"; + +/* Yes button */ +"Button.yes" = "Ya"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "MEMBELI"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Membeli kartu hadiah \n • Isi ulang telepon prabayar \n • Steam, Amazon, Hotels.com \n • Berfungsi di 170 negara"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Ubah Litecoin untuk cryptos lainnya \n • Tidak diperlukan ID \n • Membeli dengan kartu kredit \n • Pencakupan global"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Membeli Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Beli LTC dengan banyak pasangan fiat\n• Bayar dengan berbagai metode\n• Penyedia pembayaran global"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Gunakan ID atau Paspor \n • Membeli Litecoin dengan kartu kredit \n • Membeli dengan USD atau EUR"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Membeli Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Pusatkan ID Anda di dalam Kotak"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Detail pertukaran:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Jumlah yang dikirimkan:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Jumlah yang disumbangkan:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Waktu pemrosesan: Transaksi-transaksi ini akan memakan waktu %1$@ menit untuk diproses."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Waktu memproses: Transaksi ini akan memakan %1$@ menit untuk diproses."; + +/* Send: (amount) */ +"Confirmation.send" = "Kirim"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "BIAYA:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ALAMAT:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Mengkonfirmasi"; + +/* To: (address) */ +"Confirmation.to" = "Untuk"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Total Biaya:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Kata-kata yang dimasukan tidak cocok dengan kunci kertas Anda. Silahkan coba lagi."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Untuk memastikan semuanya ditulis dengan benar, masukkan kata-kata berikut dari kunci kertas Anda."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Kata #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Salinan"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Unit Tampilan Litecoin"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Memilih:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Kurs"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Perangkat ini tidak dikonfigurasikan untuk mengirim email dengan aplikasi surat iOS."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "Email Tidak Tersedia"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Perangkat ini tidak dikonfigurasikan untuk mengirim pesan."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Olahpesan Tidak Tersedia"; + +/* You can customize your Face ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "Anda dapat menyesuaikan batas pengeluaran ID Wajah dari %1$@."; + +/* Face ID screen label */ +"FaceIDSettings.label" = "Gunakan wajah Anda untuk membuka kunci Litewallet Anda dan mengirim uang hingga batas yang ditentukan."; + +/* Link Text (see TouchIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "Layar Batas Pengeluaran ID Wajah"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "Aktifkan ID Wajah untuk Litewallet"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "ID Wajah"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "Anda belum mengatur ID Wajah pada perangkat ini. Pergi ke Pengaturan-> ID Wajah & Kode Sandi untuk mengaturnya sekarang."; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "ID Wajah Tidak Diatur"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "Batas Pengeluaran ID Wajah"; + +/* Economy fee */ +"FeeSelector.economy" = "Ekonomi"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Perkiraan Pengiriman: 10+ menit"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Pilihan ini tidak disarankan untuk transaksi yang sensitif terhadap waktu."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Kemewahan"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Estimasi Pengiriman: 2,5 - 5 menit"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Opsi ini secara virtual menjamin penerimaan transaksi Anda meskipun Anda membayar premi."; + +/* Regular fee */ +"FeeSelector.regular" = "Tetap"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Estimasi Pengiriman: 2,5 - 5+ menit"; + +/* Fee Selector title */ +"FeeSelector.title" = "Kecepatan pemrosesan"; + +/* Confirm */ +"Fragment.confirm" = "Mengonfirmasi"; + +/* Or */ +"Fragment.or" = "atau"; + +/* sorry */ +"Fragment.sorry" = "Maaf"; + +/* to */ +"Fragment.to" = "ke"; + +/* History Bar Item Title */ +"History.barItemTitle" = "SEJARAH"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Nilai LTC saat ini dalam"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Memeriksa saldo kunci pribadi..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Kirim %1$@ dari kunci pribadi ini ke dompet Anda? Jaringan Litecoin akan menerima biaya %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Kunci pribadi ini sudah ada di dompet Anda."; + +/* empty private key error message */ +"Import.Error.empty" = "Kunci pribadi ini kosong."; + +/* High fees error message */ +"Import.Error.highFees" = "Biaya transaksi akan lebih mahal daripada dana yang tersedia pada kunci pribadi ini."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Bukan kunci pribadi yang benar"; + +/* Import signing error message */ +"Import.Error.signing" = "Kesalahan menandatangani transaksi"; + +/* Import button label */ +"Import.importButton" = "Impor"; + +/* Importing wallet progress view label */ +"Import.importing" = "Mengimpor Dompet"; + +/* Caption for graphics */ +"Import.leftCaption" = "Dompet yang diimpor"; + +/* Import wallet intro screen message */ +"Import.message" = "Mengimpor dompet mentransfer semua uang dari dompet Anda yang lain ke dompet Litewallet Anda menggunakan satu transaksi."; + +/* Enter password alert view title */ +"Import.password" = "Kunci pribadi ini dilindungi kata sandi."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "kata sandi"; + +/* Caption for graphics */ +"Import.rightCaption" = "Dompet Litewallet Anda"; + +/* Scan Private key button label */ +"Import.scan" = "Pemindaian Kunci Pribadi"; + +/* Import wallet success alert title */ +"Import.success" = "Keberhasilan"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Dompet yang berhasil diimpor."; + +/* Import Wallet screen title */ +"Import.title" = "Impor Dompet"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Membuka Kunci"; + +/* Import wallet intro warning message */ +"Import.warning" = "Mengimpor dompet tidak termasuk riwayat transaksi atau detail lainnya."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Kata sandi salah, silakan coba lagi."; + +/* Close app button */ +"JailbreakWarnings.close" = "Tutup"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Mengabaikan"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "KEAMANAN PERANGKAT YANG KOMPROMISASI\n Setiap aplikasi 'jailbreak' dapat mengakses data gantungan kunci Litewallet dan mencuri Litecoin Anda! Segera bersihkan dompet ini dan pulihkan pada perangkat yang aman."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "PERANGKAT KEAMANAN DI KOMPROMISASI\n Setiap aplikasi 'jailbreak' dapat mengakses data gantungan kunci Litewallet dan mencuri Litecoin Anda. Harap hanya menggunakan Litewallet pada perangkat yang tidak dipenjara."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "Peringatan"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Hapus"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Layanan lokasi dinonaktifkan."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet tidak memiliki izin untuk mengakses layanan lokasi."; + +/* Http error code */ +"Malformed URI" = "URI cacat"; + +/* Balance */ +"ManageWallet.balance" = "Saldo"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Anda membuat dompet Anda di %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Nama dompet Anda hanya muncul dalam riwayat transaksi akun Anda dan tidak dapat dilihat oleh orang lain."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Nama Dompet"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Mengelola Dompet"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Membeli Litecoin"; + +/* Menu button title */ +"MenuButton.customer.support" = "Dukungan pelanggan"; + +/* Menu button title */ +"MenuButton.lock" = "Dompet Kunci"; + +/* Menu button title */ +"MenuButton.security" = "Pusat Keamanan"; + +/* Menu button title */ +"MenuButton.settings" = "Pengaturan"; + +/* Menu button title */ +"MenuButton.support" = "Dukungan"; + +/* button label */ +"MenuViewController.createButton" = "Buat Dompet Baru"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Pulihkan Dompet"; + +/* No comment provided by engineer. */ +"No wallet" = "Tidak Ada Dompet"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Beralih ke Mode Otomatis"; + +/* Node is connected label */ +"NodeSelector.connected" = "Terhubung"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Masukkan alamat IP Node dan pelabuhan(opsional)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Masukkan Node"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Beralih ke Mode Manual"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Primer Node Saat Ini"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Tidak Terhubung"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Status Koneksi Node"; + +/* Node Selector view title */ +"NodeSelector.title" = "Node Litecoin"; + +/* "Email address label" */ +"Notifications.emailLabel" = "Alamat email"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Masukkan di sini"; + +/* "Email title" */ +"Notifications.emailTitle" = "Jangan lewatkan apa -apa!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Bahasa yang disukai:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Daftar untuk mendengar tentang pembaruan & kontes."; + +/* Signup cancel */ +"Notifications.signupCancel" = "Tidak, terima kasih."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Permintaan Pembayaran salah"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Dokumen tidak didukung atau rusak"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "sertifikat hilang"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "permintaan kadaluwarsa"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Tidak dapat melakukan pembayaran"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Pembayaran Litecoin tidak boleh kurang dari %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Hasil transaksi Litecoin tidak boleh kurang dari $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "jenis tanda tangan yang tidak didukung"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "sertifikat tidak dipercaya"; + +/* Dismiss button. */ +"Prompts.dismiss" = "Memberhentikan"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "Ketuk di sini untuk mengaktifkan ID Wajah"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "Aktifkan ID Wajah"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Kode sandi perangkat diperlukan untuk melindungi dompet Anda. Buka pengaturan dan nyalakan kode sandi."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Aktifkan kode sandi perangkat"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Terus"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Kunci Kertas Anda harus disimpan jika Anda kehilangan atau mengganti telepon Anda. Ketuk di sini untuk melanjutkan."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Membatalkan"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Memungkinkan"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Diperlukan Tindakan"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Dompet Anda mungkin tidak sinkron. Ini sering dapat diperbaiki dengan memindai ulang blockchain."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transaksi Ditolak"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet telah ditingkatkan untuk menggunakan PIN 6-digit. Ketuk di sini untuk meningkatkan."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Tingkatkan PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Bantu tingkatkan Litewallet dengan membagikan data anonim Anda kepada kami"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Bagikan Data Anonim"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Ketuk di sini untuk mengaktifkan Sentuh ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Aktifkan Sentuh ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "MENERIMA"; + +/* Address copied message. */ +"Receive.copied" = "Disalin ke papan klip."; + +/* Share via email button label */ +"Receive.emailButton" = "Email"; + +/* Request button label */ +"Receive.request" = "Permintaan Jumlah"; + +/* Share button label */ +"Receive.share" = "Bagikan"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Pesan teks"; + +/* Receive modal title */ +"Receive.title" = "Menerima"; + +/* Done button text */ +"RecoverWallet.done" = "Selesai"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Pulihkan Dompet"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Atur ulang PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Masukkan Kunci Kertas"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Pulihkan Litewallet Anda dengan kunci kertas Anda."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "Kunci kertas yang Anda masukkan tidak benar. Periksa ulang setiap kata dan coba lagi."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Panah Kiri"; + +/* Next button label */ +"RecoverWallet.next" = "Lanjut"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Ketuk di sini untuk informasi lebih lanjut."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Panah Kanan"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Masukkan kunci kertas untuk dompet yang ingin Anda pulihkan."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Untuk mengatur ulang PIN Anda, masukkan kata-kata dari kunci kertas Anda ke dalam kotak di bawah ini."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Silakan masukkan jumlah terlebih dahulu."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Permintaan Jumlah"; + +/* Alert action button label */ +"ReScan.alertAction" = "Sinkron"; + +/* Alert message body */ +"ReScan.alertMessage" = "Anda tidak akan dapat mengirim uang saat menyinkronkan."; + +/* Alert message title */ +"ReScan.alertTitle" = "Sinkron dengan Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 menit"; + +/* Syncing explanation */ +"ReScan.body2" = "Jika suatu transaksi terlihat selesai di jaringan Litecoin tetapi tidak di Litewallet Anda."; + +/* Syncing explanation */ +"ReScan.body3" = "Anda berulang kali mendapatkan pesan kesalahan yang mengatakan transaksi Anda ditolak."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Mulai Sinkron"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Anda tidak akan dapat mengirim uang saat menyinkronkan dengan blockchain."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Sinkron Blockchain"; + +/* Subheader label */ +"ReScan.subheader1" = "Perkiraan Waktu"; + +/* Subheader label */ +"ReScan.subheader2" = "Kapan Melakukan Sinkron?"; + +/* Reset walet button title */ +"resetButton" = "Ya, atur ulang dompet"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Hapus dompet Lite saya"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Flash Kamera"; + +/* Complete filter label */ +"Search.complete" = "lengkap"; + +/* Pending filter label */ +"Search.pending" = "tertunda"; + +/* Received filter label */ +"Search.received" = "diterima"; + +/* Sent filter label */ +"Search.sent" = "dikirim"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "ID Wajah"; + +/* Security Center Info */ +"SecurityCenter.info" = "Aktifkan semua fitur keamanan untuk perlindungan maksimal."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "Satu-satunya cara untuk mengakses Litecoin Anda jika Anda kehilangan atau meningkatkan telepon Anda."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Kunci Kertas"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Melindungi Litewallet Anda dari pengguna yang tidak sah."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "PIN 6-Digit"; + +/* Security Center Title */ +"SecurityCenter.title" = "Pusat Keamanan"; + +/* Touch ID/FaceID button description */ +"SecurityCenter.touchIdDescription" = "Buka kunci Litewallet Anda dengan mudah dan kirim uang hingga batas yang ditentukan."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Sentuh ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Jumlah"; + +/* Balance: $4.00 */ +"Send.balance" = "Saldo: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Biaya: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "KIRIM"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Buka Pengaturan untuk mengizinkan akses kamera."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet tidak diizinkan untuk mengakses kamera"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "Tujuannya adalah alamat Anda sendiri. Anda tidak dapat mengirim ke diri sendiri."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Tidak dapat membuat transaksi."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Memo"; + +/* Empty pasteboard error message */ +"Send.emptyPasteboard" = "Papan tulis kosong"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Masukkan alamat Litecoin"; + +/* Fees: $0.01*/ +"Send.fee" = "Biaya: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Biaya:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Identitas penerima pembayaran tidak disertifikasi."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Dana tidak mencukupi"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Alamat tujuan bukan alamat Litecoin yang benar."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Papan tulis tidak mengandung alamat Litecoin yang benar."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Alamat tidak benar"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Pengiriman dinonaktifkan selama pemindaian ulang penuh."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Memuat Permintaan"; + +/* Network */ +"Send.networkFee" = "Jaringan"; + +/* Empty address alert message */ +"Send.noAddress" = "Silakan masukkan alamat penerima."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Silakan masukkan jumlah."; + +/* Paste button label */ +"Send.pasteLabel" = "Tempel"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Tidak dapat mempublikasikan transaksi."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Tidak dapat memuat permintaan pembayaran"; + +/* Scan button label */ +"Send.scanLabel" = "Pemindaian"; + +/* Send button label */ +"Send.sendLabel" = "Kirim"; + +/* Service */ +"Send.serviceFee" = "Melayani"; + +/* Send modal title */ +"Send.title" = "Kirim"; + +/* Send money to label */ +"Send.toLabel" = "Untuk"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "domain"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Masukkan a"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Lihatlah"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Masalah sistem, domain tidak ditemukan. [Kesalahan: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Pencarian gagal"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Masukkan domain .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao, atau .x."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Masukkan domain"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Masalah pencarian sistem. [Kesalahan: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Alamat Litecoin hanya ditujukan untuk penggunaan tunggal."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Penggunaan kembali mengurangi privasi untuk Anda dan penerima dan dapat mengakibatkan kerugian jika penerima tidak secara langsung mengontrol alamat."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Alamat Sudah Digunakan"; + +/* About label */ +"Settings.about" = "Tentang"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Pengaturan lanjutan"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Blockchain"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Apakah Anda yakin ingin mengubah bahasa ke %l?"; + +/* Default currency label */ +"Settings.currency" = "Mata uang tampilan"; + +/* Current Locale */ +"Settings.currentLocale" = "Lokal Saat Ini:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Bergabunglah dengan Akses Dini"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Apakah Anda menikmati Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "Batas Pengeluaran Wajah ID"; + +/* Import wallet label */ +"Settings.importTitle" = "Impor Dompet"; + +/* Languages label */ +"Settings.languages" = "Bahasa"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Lingkungan Hidup:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Mitra Litewallet"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Versi litewallet:"; + +/* Manage settings section header */ +"Settings.manage" = "Mengelola"; + +/* Notifications label */ +"Settings.notifications" = "Notifikasi"; + +/* Leave review button label */ +"Settings.review" = "Tinggalkan kami Ulasan"; + +/* Share anonymous data label */ +"Settings.shareData" = "Bagikan Data Anonim"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Sosial"; + +/* Support settings section header */ +"Settings.support" = "Dukung"; + +/* Sync blockchain label */ +"Settings.sync" = "Blockchain Sinkron"; + +/* Settings title */ +"Settings.title" = "Pengaturan"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Sentuh Batas Pengeluaran ID"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Dompet"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Mulai/Pulihkan Dompet Lainnya"; + +/* Share data view body */ +"ShareData.body" = "Bantu meningkatkan Litewallet dengan membagikan data anonim Anda kepada kami. Ini tidak termasuk informasi keuangan apa pun. Kami menghargai privasi finansial Anda."; + +/* Share data header */ +"ShareData.header" = "Bagikan Data?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Bagikan Data Anonim?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Batas Pengeluaran Saat Ini: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Tuliskan Kunci Kertas Lagi"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Kunci kertas Anda adalah satu-satunya cara untuk mengembalikan Litewallet Anda jika ponsel Anda hilang, dicuri, rusak, atau ditingkatkan. \n\n Kami akan menunjukkan kepada Anda daftar kata-kata untuk dituliskan di selembar kertas dan tetap aman."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Tuliskan Kunci Kertas"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Terakhir Anda menuliskan kunci kertas Anda di %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "Cara paling aman dan termudah untuk menggunakan Litecoin."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Dukung Yayasan Litecoin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Menghubungkan ..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Memindai ulang ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Keberhasilan!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Menyinkronkan ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Menghubungkan"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Sinkronisasi"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Anda dapat menyesuaikan batas pengeluaran ID Sentuh Anda dari %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Gunakan sidik jari Anda untuk membuka kunci Litewallet Anda dan mengirim uang hingga batas yang ditentukan."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Layar Sentuh ID Batas Pengeluaran"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Batas pengeluaran: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Aktifkan Sentuh ID untuk Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Sentuh ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Anda belum mengatur Sentuh ID pada perangkat ini. Pergi ke Pengaturan-> Sentuh ID & Kode Sandi untuk mengaturnya sekarang."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Sentuh ID Tidak Diatur"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Selalu memerlukan kode sandi"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Anda akan diminta memasukkan 6 digit PIN Anda untuk mengirim transaksi apa pun melebihi batas pengeluaran Anda, dan setiap 48 jam sejak terakhir kali Anda memasukkan 6 digit PIN Anda."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Sentuh Batas Pengeluaran ID"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Detail Jumlah:"; + +/* Availability status text */ +"Transaction.available" = "Tersedia untuk Dibelanjakan"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Blok:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo:"; + +/* Transaction complete label */ +"Transaction.complete" = "Lengkap"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Detail jumlah akhir transaksi"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Saldo akhir: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Nilai tukar saat diterima:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Nilai tukar saat dikirim:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ biaya)"; + +/* Invalid transaction */ +"Transaction.invalid" = "Tidak valid"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "baru saja"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "Berlangsung: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "Berlangsung: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Saldo Awal: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Detail jumlah mulai transaksi"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "ID Tx:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Menunggu untuk dikonfirmasi. Beberapa pedagang memerlukan konfirmasi untuk menyelesaikan transaksi. Perkiraan waktu: 1-2 jam."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "akun"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Jumlah"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Dikonfirmasi dalam Blokir"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Memo"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Disalin"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Salin semua detail"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Transaksi Anda akan muncul di sini."; + +/* [received] at
(received title 2/2) */ +"TransactionDetails.from" = "di %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Kurang..."; + +/* Moved $5.00 */ +"TransactionDetails.moved" = "Pindahkan %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Pindahkan %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Tidak dikonfirmasi"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "pada"; + +/* Received $5.00 (received title 1/2) */ +"TransactionDetails.received" = "Terima %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Terima %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "TransactionDetails.receivedModalTitle"; + +/* Sent $5.00 (sent title 1/2) */ +"TransactionDetails.sent" = "Kirim %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Kirim %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Status"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Detil transaksi"; + +/* [sent] to
(sent title 2/2) */ +"TransactionDetails.to" = "untuk %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "ID Transaksi Litecoin"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Diterima di Alamat ini"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Dikirim ke Alamat ini"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Dinonaktifkan hingga: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Masukkan PIN"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Buka kunci dengan FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Alamat saya"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Setel ulang PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Memindai"; + +/* TouchID/FaceID prompt text */ +"UnlockScreen.touchIdPrompt" = "Buka kunci Litewallet Anda."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Buka kunci dengan TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Dompet Tidak Terkunci"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Ingat PIN ini. Jika Anda lupa, Anda tidak akan dapat mengakses Litecoin Anda."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "PIN Anda akan digunakan untuk membuka kunci Litewallet Anda dan mengirim uang."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Tetapkan PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Masukkan kembali PIN"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Masukkan PIN Anda sekarang ini."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Masukkan PIN baru Anda."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Masukkan kembali PIN baru Anda."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Maaf, tidak dapat memperbarui PIN."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Perbarui Kesalahan PIN"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Perbarui PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Salin alamat dompet ke papan klip?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Otorisasi untuk menyalin alamat dompet ke papan klip"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Salin Alamat Dompet"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Salin"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "silakan masukkan PIN Anda untuk mengesahkan transaksi ini."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Silakan masukkan PIN Anda untuk melanjutkan."; + +/* Verify PIN view title */ +"VerifyPin.title" = "Diperlukan PIN"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Otorisasi transaksi ini"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Buka aplikasi iPhone Litewallet untuk mengatur dompet Anda."; + +/* Dismiss button label */ +"Webview.dismiss" = "Memberhentikan"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Terjadi kesalahan saat memuat konten. Silakan coba lagi."; + +/* Updating webview message */ +"Webview.updating" = "Memperbarui..."; + +/* Welcome view body text */ +"Welcome.body" = "Litewallet sekarang memiliki tampilan baru dan beberapa fitur baru.\n\nJika Anda butuh bantuan, cari (?) Di kanan atas sebagian besar layar. Semua koin ditampilkan dalam lites(ł). 1 Litecoin (Ł) = 1000 lites (ł)."; + +/* Welcome view title */ +"Welcome.title" = "Selamat datang di Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Kesalahan Korupsi Lokal"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Yakin ingin menghapus dompet ini?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Hapus Dompet?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Hapus basis data"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Basis data lokal Anda rusak. Buka Pengaturan> Blockchain: Pengaturan> Hapus Basis Data untuk menyegarkan"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Hapus & Sinkronkan"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Lupakan frasa atau PIN awal Anda?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Gagal menghapus dompet."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Gagal"; + +/* Enter key to wipe wallet instruction. */ +"WipeWallet.instruction" = "Untuk memulai dompet baru atau mengembalikan dompet yang ada, Anda harus terlebih dahulu menghapus dompet yang saat ini dipasang. Untuk melanjutkan, masukkan Kunci Kertas dompet saat ini."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Memulai atau memulihkan dompet lain memungkinkan Anda mengakses dan mengelola dompet Litewallet berbeda di perangkat ini."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Dompet Anda saat ini akan dihapus dari perangkat ini. Jika Anda ingin mengembalikannya di masa mendatang, Anda harus memasukkan Kunci Kertas Anda."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Mulai atau Pulihkan Dompet Lainnya"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Tindakan ini akan menghapus Litewallet Anda!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Menghapus dompet Anda berarti kunci pribadi dan menghapus data aplikasi akan hilang. Anda mungkin kehilangan Litecoin selamanya!\n\n\nTak seorang pun di tim Litewallet dapat mengambil benih ini untuk Anda. Kami tidak bertanggung jawab jika Anda tidak mengindahkan peringatan ini."; + +/* Warning title */ +"WipeWallet.warningTitle" = "HARAP BACA!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Hapus"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Menghapus..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Tuliskan setiap kata secara berurutan dan simpan di tempat yang aman."; + +/* button label */ +"WritePaperPhrase.next" = "Lanjut"; + +/* button label */ +"WritePaperPhrase.previous" = "Sebelumnya"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d dari %2$d"; diff --git a/litewallet/Strings/it.lproj/Localizable.strings b/litewallet/Strings/it.lproj/Localizable.strings new file mode 100755 index 000000000..699d1997a --- /dev/null +++ b/litewallet/Strings/it.lproj/Localizable.strings @@ -0,0 +1,1298 @@ +/* About screen blog label */ +"About.blog" = "Blog"; + +/* About screen footer */ +"About.footer" = "Realizzato dal team globale Litewallet. Versione %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Informativa sulla Privacy"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Chi siamo"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Chiudi"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Centro Assistenza"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Caricamento Portafoglio"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Il Mio Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "GESTISCI"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Errore di corruzione locale"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Elimina database"; + +/* Error alert title */ +"Alert.error" = "Errore"; + +/* No internet alert message */ +"Alert.noInternet" = "Non è stata trovata alcuna connessione internet. Controlla la connessione e riprova."; + +/* Warning alert title */ +"Alert.warning" = "Attenzione"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Indirizzi Copiati"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Tutti gli indirizzi del portafoglio sono stati copiati con successo."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Chiave Cartacea Impostata"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Fantastico!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN Impostato"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Risoluzione del dominio"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Il tuo indirizzo è stato risolto!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Invio fallito"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Conferma dell'Invio"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Denaro Inviato!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "Errore di Serializzazione JSON"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Il portafoglio non è pronto"; + +/* API Token error message */ +"ApiClient.tokenError" = "Impossibile recuperare il token API"; + +/* buy button */ +"Button.buy" = "acquistare"; + +/* Cancel button label */ +"Button.cancel" = "Annulla"; + +/* Ignore button label */ +"Button.ignore" = "Ignora"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "No"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "ricevi"; + +/* resetFields */ +"Button.resetFields" = "Resettare i campi"; + +/* send button */ +"Button.send" = "invia"; + +/* Settings button label */ +"Button.settings" = "Impostazioni"; + +/* Settings button label */ +"Button.submit" = "Invia"; + +/* Yes button */ +"Button.yes" = "Sì"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "ACQUISTARE"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Acquista buoni regalo \n • Ricarica telefoni prepagati \n • Steam, Amazon, Hotels.com \n • Funziona in 170 paesi"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Modifica Litecoin per altri cryptos \n • Nessun ID richiesto \n • Acquista tramite carta di credito \n • Copertura globale"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Acquista Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Acquista LTC con molte coppie fiat\n• Paga con più metodi\n• Fornitore globale di pagamenti"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Ottieni Litecoin in 5 minuti! \n • Acquista Litecoin tramite carta di credito \n • Passaporto o carta d'identità"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Acquista Łitecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Centra il tuo ID nel riquadro"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Scambia dettagli:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Somma da inviare:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Importo da donare:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Tempo di elaborazione: l'elaborazione di queste transazioni richiederà %1$@ minuti."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Tempo di lavorazione: questa transazione richiederà %1$@ minuti per essere lavorata."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Invia"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "FEE:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "INDIRIZZO:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Conferma"; + +/* To: (address) */ +"Confirmation.to" = "A"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Costo totale:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Le parole inserite non corrispondono alla chiave cartacea. Per favore riprova."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Per assicurarti che tutto sia stato scritto correttamente, per favore inserisci le seguenti parole della tua chiave cartacea."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Parola n°%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "copia"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Unità di visualizzazione Litecoin"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Scegliere:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Tasso di Cambio"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Questo dispositivo non è configurato per inviare e-mail con l'app Mail di iOS."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "E-mail Non Disponibile"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Questo dispositivo non è configurato per inviare messaggi."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Messaggistica Non Disponibile"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Economy"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Tempo stimato di consegna: più di 10 minuti"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Questa opzione non è raccomandata per le transazioni dalle tempistiche delicate."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Lusso"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Consegna prevista: 2,5 - 5 minuti"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Questa opzione garantisce virtualmente l'accettazione della tua transazione anche se stai pagando un premio."; + +/* Regular fee */ +"FeeSelector.regular" = "Regolare"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Consegna prevista: 2,5 - 5+ minuti"; + +/* Fee Selector title */ +"FeeSelector.title" = "Velocità di lavorazione"; + +/* Confirm */ +"Fragment.confirm" = "Confermare"; + +/* Or */ +"Fragment.or" = "o"; + +/* sorry */ +"Fragment.sorry" = "Spiacente"; + +/* to */ +"Fragment.to" = "a"; + +/* History Bar Item Title */ +"History.barItemTitle" = "STORIA"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Valore LTC corrente in"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Verifica del saldo della chiave privata..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Inviare %1$@ da questa chiave privata al tuo portafoglio? La rete Litecoin riceverà una commissione di %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Questa chiave privata è già nel tuo portafoglio."; + +/* empty private key error message */ +"Import.Error.empty" = "Questa chiave privata è vuota."; + +/* High fees error message */ +"Import.Error.highFees" = "I costi per la transazione sono superiori ai fondi disponibili su questa chiave privata."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Non è una chiave privata valida"; + +/* Import signing error message */ +"Import.Error.signing" = "Errore nella firma della transazione"; + +/* Import button label */ +"Import.importButton" = "Importa"; + +/* Importing wallet progress view label */ +"Import.importing" = "Importazione Portafoglio"; + +/* Caption for graphics */ +"Import.leftCaption" = "Portafoglio da importare"; + +/* Import wallet intro screen message */ +"Import.message" = "L'importazione di un portafoglio trasferisce tutto il denaro dal tuo altro portafoglio al portafoglio Litewallet utilizzando una singola transazione."; + +/* Enter password alert view title */ +"Import.password" = "Questa chiave privata è protetta da password."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "password"; + +/* Caption for graphics */ +"Import.rightCaption" = "Il Tuo Portafoglio Litewallet"; + +/* Scan Private key button label */ +"Import.scan" = "Acquisisci Chiave Privata"; + +/* Import wallet success alert title */ +"Import.success" = "Completato"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Portafoglio importato con successo."; + +/* Import Wallet screen title */ +"Import.title" = "Importa Portafoglio"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Sbloccaggio Chiave"; + +/* Import wallet intro warning message */ +"Import.warning" = "L'importazione di un portafoglio non include la cronologia delle transazioni o altri dettagli."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Password sbagliata, per favore riprova."; + +/* Close app button */ +"JailbreakWarnings.close" = "Chiudi"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignora"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "SICUREZZA DEL DISPOSITIVO COMPROMESSA\nQualsiasi applicazione \"jailbreak\" può accedere ai dati di keychain di Litewallet e rubarti i Litecoin! Cancella immediatamente questo portafoglio ed effettua il ripristino su un dispositivo sicuro."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "SICUREZZA DEL DISPOSITIVO COMPROMESSA\nQualsiasi applicazione \"jailbreak\" può accedere ai dati di keychain di Litewallet e rubarti i Litecoin. Utilizza Litewallet solamente su un dispositivo senza jailbreak."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "ATTENZIONE"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Cancella"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "I servizi di localizzazione sono disabilitati."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet non dispone dell'autorizzazione per accedere ai servizi di localizzazione."; + +/* No comment provided by engineer. */ +"Malformed URI" = "URI non valido"; + +/* Balance */ +"ManageWallet.balance" = "Saldo"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Hai creato il tuo portafoglio il %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Il nome del tuo portafoglio appare solo nella cronologia delle transazioni del tuo account e non può essere visto da nessun altro."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Nome Portafoglio"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Gestisci Portafoglio"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Acquista Litecoin"; + +/* Menu button title */ +"MenuButton.customer.support" = "Servizio Clienti"; + +/* Menu button title */ +"MenuButton.lock" = "Blocca Portafoglio"; + +/* Menu button title */ +"MenuButton.security" = "Centro Sicurezza"; + +/* Menu button title */ +"MenuButton.settings" = "Impostazioni"; + +/* Menu button title */ +"MenuButton.support" = "Assistenza"; + +/* button label */ +"MenuViewController.createButton" = "Crea Nuovo Portafoglio"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Recupera Portafoglio"; + +/* No comment provided by engineer. */ +"No wallet" = "Nessun portafoglio"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Passa alla modalità automatica"; + +/* Node is connected label */ +"NodeSelector.connected" = "Connesso"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Inserisci l'indirizzo IP del nodo e la porta (opzionale)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Inserisci il nodo"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Passa alla modalità manuale"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Nodo primario attuale"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Non connesso"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Stato della connessione del nodo"; + +/* Node Selector view title */ +"NodeSelector.title" = "Nodi Litecoin"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Richiesta Cattivo Pagamento"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Documento non supportato o danneggiato"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "certificato mancante"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "richiesta scaduta"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Impossibile effettuare il pagamento"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "I pagamenti in Litecoin non possono essere inferiori a %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Gli output di transazione Litecoin non possono essere inferiori a $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "tipo di firma non supportato"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "certificato non attendibile"; + +/* Dismiss button. */ +"Prompts.dismiss" = "RESPINGERE"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "È necessaria una password per l'apparecchio per proteggere il tuo portafogli. Vai alle impostazioni e attiva la password."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Attiva la password dell'apparecchio"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Continua"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "È necessario salvare la tua Paper Key nel caso in cui dovessi perdere o cambiare telefono. Premi qui per continuare."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Annulla"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Abilitare"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Azione necessaria"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Il tuo portafoglio potrebbe non essere sincronizzato. Il problema si può spesso risolvere scansionando nuovamente la blockchain."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transazione Rifiutata"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet è stato aggiornato all'utilizzo di un PIN a 6 cifre. Tocca qui per aggiornare."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Aggiorna il PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Aiutaci a migliorare Litewallet condividendo i tuoi dati con noi anonimamente"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Condividi dati anonimamente"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Tocca qui per attivare Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Attiva Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "RICEVERE"; + +/* Address copied message. */ +"Receive.copied" = "Copiato negli appunti."; + +/* Share via email button label */ +"Receive.emailButton" = "E-mail"; + +/* Request button label */ +"Receive.request" = "Richiedi un Importo"; + +/* Share button label */ +"Receive.share" = "Condividi"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "SMS"; + +/* Receive modal title */ +"Receive.title" = "Ricevi"; + +/* Done button text */ +"RecoverWallet.done" = "Completato"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Recupera Portafoglio"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Reimposta PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Inserisci Chiave Cartacea"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Recupera il tuo Litewallet con la tua chiave cartacea."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "La chiave cartacea inserita non è valida. Si prega di controllare ogni parola due volte e riprovare."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Freccia Sinistra"; + +/* Next button label */ +"RecoverWallet.next" = "Successivo"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Tocca qui per ulteriori informazioni."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Freccia Destra"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Inserisci la chiave cartacea per il portafoglio che vuoi recuperare."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Per reimpostare il tuo PIN, immetti le parole della tua chiave cartacea all'interno dei riquadri sottostanti."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Per favore inserisci prima una somma."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Richiedi un Importo"; + +/* Alert action button label */ +"ReScan.alertAction" = "Sincronizza"; + +/* Alert message body */ +"ReScan.alertMessage" = "Non potrai inviare denaro durante la sincronizzazione."; + +/* Alert message title */ +"ReScan.alertTitle" = "Sincronizza con la Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minuti"; + +/* Syncing explanation */ +"ReScan.body2" = "Se una transazione figura come completata sulla rete Litecoin, ma non sul tuo Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Hai ricevuto ripetutamente un errore che dice che la transazione è stata rifiutata."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Avvia Sincronizzazione"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Non potrai inviare denaro durante la sincronizzazione con la blockchain."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Sincronizza Blockchain"; + +/* Subheader label */ +"ReScan.subheader1" = "Tempo stimato"; + +/* Subheader label */ +"ReScan.subheader2" = "Quando Sincronizzare?"; + +/* Reset walet button title */ +"resetButton" = "Sì, ripristina portafoglio"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Elimina il mio Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Flash della Fotocamera"; + +/* Complete filter label */ +"Search.complete" = "completo"; + +/* Pending filter label */ +"Search.pending" = "in attesa"; + +/* Received filter label */ +"Search.received" = "ricevuto"; + +/* Sent filter label */ +"Search.sent" = "inviato"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Attiva tutte le funzionalità di sicurezza per la massima protezione."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "L'unico modo per accedere al tuo Litecoin se perdi o aggiorni il telefono."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Chiave Cartacea"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Protegge il tuo Litewallet dagli utenti non autorizzati."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "PIN a 6 cifre"; + +/* Security Center Title */ +"SecurityCenter.title" = "Centro Sicurezza"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Sblocca comodamente il tuo Litewallet e invia denaro fino a un limite impostato."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Importo"; + +/* Balance: $4.00 */ +"Send.balance" = "Saldo: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Tassa: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "SPEDIRE"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Vai su Impostazioni per consentire l'accesso alla fotocamera."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet non è autorizzato ad accedere alla fotocamera"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "La destinazione corrisponde al tuo indirizzo. Non puoi inviare a te stesso."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Impossibile creare la transazione."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Promemoria"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Il cartone è vuoto"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Inserire un indirizzo Litecoin"; + +/* Fees: $0.01*/ +"Send.fee" = "Commissioni: %1$@"; + +/* Fees: $0.01*/ +"Send.fee" = "Commissioni: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Commissioni:"; + +/* Fees Blank: */ +"Send.feeBlank" = "Commissioni:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Identità del beneficiario non certificata."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "fondi insufficienti"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "L'indirizzo di destinazione non è un indirizzo Litecoin valido."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Il cartone non contiene un indirizzo Litecoin valido."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Indirizzo Non Valido"; + +/* Is rescanning error message */ +"Send.isRescanning" = "L'invio è disabilitato durante la ripetizione dello scan."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Caricamento della richiesta"; + +/* Empty address alert message */ +"Send.noAddress" = "Inserisci l'indirizzo del destinatario."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Inserisci un importo da inviare."; + +/* Paste button label */ +"Send.pasteLabel" = "Incolla"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Impossibile pubblicare la transazione."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Non è stato possibile caricare la richiesta"; + +/* Scan button label */ +"Send.scanLabel" = "Acquisisci"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Invia"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Invia"; + +/* Send money to label */ +"Send.toLabel" = "A"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "dominio"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Entrare in un"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Consultare"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Spiacenti, il dominio non è stato trovato. [Errore: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Ricerca non riuscita"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Inserisci un dominio .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao o .x."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Inserisci dominio"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Problema di ricerca del sistema. [Errore: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Gli indirizzi Litecoin dovrebbero essere utilizzati una sola volta."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Il riutilizzo riduce la privacy sia tua che del destinatario e può causare perdite se il destinatario non controlla direttamente l'indirizzo."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Indirizzo Già Utilizzato"; + +/* About label */ +"Settings.about" = "Chi siamo"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Impostazioni avanzate"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "blockchain"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Sei sicuro di voler cambiare la lingua in %l?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Visualizza valuta"; + +/* Current Locale */ +"Settings.currentLocale" = "Locale corrente:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Iscriviti all'Accesso Immediato"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Ti piace Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Importa Portafoglio"; + +/* Languages label */ +"Settings.languages" = "Le lingue"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Ambiente:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Partner di Litewallet"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Versione Litewallet:"; + +/* Manage settings section header */ +"Settings.manage" = "Gestisci"; + +/* Notifications label */ +"Settings.notifications" = "Notifiche"; + +/* Leave review button label */ +"Settings.review" = "Scrivi una Recensione"; + +/* Share anonymous data label */ +"Settings.shareData" = "Condividi Dati Anonimi"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Sociale"; + +/* Support settings section header */ +"Settings.support" = "supporto"; + +/* Sync blockchain label */ +"Settings.sync" = "Sincronizza Blockchain"; + +/* Settings title */ +"Settings.title" = "Impostazioni"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Limite di Spesa Touch ID"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Portafogli"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Inizia/Recupera un altro portafogli"; + +/* Share data view body */ +"ShareData.body" = "Aiuta a migliorare Litewallet condividendo con noi i tuoi dati anonimi. Ciò non include alcuna informazione finanziaria. Rispettiamo la tua privacy finanziaria."; + +/* Share data header */ +"ShareData.header" = "Condividi Dati?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Condividi Dati Anonimi?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Limite di spesa corrente:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Annota la Chiave Cartacea di nuovo"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "La tua chiave cartacea è l'unico modo per ripristinare il tuo Litewallet, nel caso il tuo telefono sia perso, rubato, rotto o aggiornato.\n\nTi mostreremo un elenco di parole da annotare su un pezzo di carta e conservare al sicuro."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Annota la Chiave Cartacea"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Hai annotato la tua chiave cartacea l'ultima volta il %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "Il modo più sicuro e semplice per utilizzare Litecoin."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Sostenere la Fondazione Litecoin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Connessione in corso ..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Nuova analisi ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Successo!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Sincronizzazione ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Connessione in corso"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Sincronizzazione in corso"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ giorni"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ ore"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ minuti"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ secondi"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Puoi personalizzare il tuo limite di spesa tramite Touch ID %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Utilizza l'impronta digitale per sbloccare il tuo Litewallet e inviare denaro fino a un limite impostato."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Schermata limite di spesa con Touch ID"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Limite di spesa: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Abilita Touch ID per Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Non hai impostato Touch ID su questo dispositivo. Vai su Impostazioni -> Touch ID e Codice per impostarlo ora."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID Non Impostato"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Richiede sempre una password"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Ti verrà chiesto di inserire il tuo PIN a 6 cifre per inviare qualsiasi transazione superiore al tuo limite di spesa e ogni 48 ore dall'ultima volta che hai inserito il tuo PIN a 6 cifre."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Limite di Spesa Touch ID"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Scambia dettagli:"; + +/* Availability status text */ +"Transaction.available" = "Disponibile da Spendere"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Bloccare:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo:"; + +/* Transaction complete label */ +"Transaction.complete" = "Completo"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Dettagli sull'importo finale della transazione"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Saldo finale: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Tasso di cambio alla ricezione:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Tasso di cambio all'invio:"; + +/* (b600 fee) */ +"Transaction.fee" = "(commissione %1$@)"; + +/* Invalid transaction */ +"Transaction.invalid" = "NON VALIDO"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "proprio ora"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "In corso: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "In corso: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Saldo iniziale: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Dettagli sull'importo iniziale della transazione"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "In attesa di conferma. Alcuni commercianti richiedono la conferma per completare una transazione. Tempo stimato: 1-2 ore."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "account"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Importo"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Confermato in blocco"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Promemoria"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Copiato"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Copia tutti i dettagli"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Le tue transazioni appariranno qui."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "a %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Di meno"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "Trasferiti %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Spostato %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Non confermato"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "come di"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "Ricevuti %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Ricevuto %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "RICEVERE L'INDIRIZZO"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "Inviati %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Inviato %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Stato"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Dettagli Transazione"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "a %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "ID transazione Litecoin"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Ricevuta a questo indirizzo"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Inviata a questo indirizzo"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Disabilitato fino al: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Inserisci il PIN"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Sblocca con FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Il Mio Indirizzo"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Reimposta PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Acquisisci"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Sblocca il tuo Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Sblocca con Touch ID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Portafoglio Sbloccato"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Ricorda questo PIN. Se lo dimentichi, non sarai in grado di accedere al tuo Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Il PIN verrà utilizzato per sbloccare il tuo Litewallet e inviare denaro."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Imposta PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Inserisci nuovamente il PIN"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Inserisci il tuo PIN attuale."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Inserisci il tuo nuovo PIN."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Inserisci ancora il tuo nuovo PIN."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Purtroppo non è possibile aggiornare il PIN."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Errore Aggiornamento PIN"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Aggiorna PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Copiare gli indirizzi del portafoglio negli appunti?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Autorizza a copiare l'indirizzo del portafoglio negli appunti"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Copia Indirizzi Portafoglio"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Copia"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Per favore inserisci il tuo PIN per autorizzare questa transazione."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Inserisci il tuo PIN per continuare."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN richiesto"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Autorizza questa transazione"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Apri l'app Litewallet per iPhone per impostare tuo il portafoglio."; + +/* Dismiss button label */ +"Webview.dismiss" = "Ignora"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Si è verificato un errore nel caricamento del contenuto. Riprova."; + +/* Updating webview message */ +"Webview.updating" = "In aggiornamento…"; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Ti diamo il benvenuto su Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Ti diamo il benvenuto su Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Ciò elimina il database ma conserva il PIN e la frase. Conferma il PIN esistente, esegui il seeding e attendi per completare la sincronizzazione con il nuovo db"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Sicuro di voler eliminare questo portafogli?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Ripulisci portafogli?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Elimina database"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Il database locale è danneggiato. Vai su Impostazioni> Blockchain: Impostazioni> Elimina database per aggiornare"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Elimina e sincronizza"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Hai dimenticato la frase seme o il PIN?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Ripulitura portafogli fallita."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Fallito"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Inserisci la frase di recupero di questo portafogli per ripulirlo o recuperane un'altra. Il tuo saldo rimane in questa frase."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Iniziare o recuperare un altro portafoglio ti consente di avere accesso e gestire un portafoglio Litewallet diverso su questo dispositivo."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Non sarai più in grado di accedere al tuo portafoglio Litewallet attuale da questo dispositivo. Il saldo resterà solo un modo di dire."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Inizia o recupera un altro portafogli."; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Questa azione cancellerà il tuo Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "L'eliminazione del portafoglio significa che la chiave privata e la cancellazione dei dati dell'app saranno spariti. Potresti perdere Litecoin per sempre!\n\n\nNessuno nel team di Litewallet può recuperare questo seme per te. Non siamo responsabili se non ascolti questo avviso."; + +/* Warning title */ +"WipeWallet.warningTitle" = "SI PREGA DI LEGGERE!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Ripulisci"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Ripulitura..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Scrivi ciascuna parola in ordine e conservala in un luogo sicuro."; + +/* button label */ +"WritePaperPhrase.next" = "Successivo"; + +/* button label */ +"WritePaperPhrase.previous" = "Precedente"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d di %2$d"; + +/* "Email address label" */ +"Notifications.emailLabel" = ""; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = ""; + +/* "Email title" */ +"Notifications.emailTitle" = ""; + +/* "Language preference label" */ +"Notifications.languagePreference" = ""; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = ""; + +/* Signup cancel */ +"Notifications.signupCancel" = ""; + +/* Network */ +"Send.networkFee" = ""; + +/* Service */ +"Send.serviceFee" = ""; diff --git a/litewallet/Strings/ja.lproj/Localizable.strings b/litewallet/Strings/ja.lproj/Localizable.strings new file mode 100755 index 000000000..cbc3c0363 --- /dev/null +++ b/litewallet/Strings/ja.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "ブログ"; + +/* About screen footer */ +"About.footer" = "グローバルブレッドチームによって開発されました。バージョン %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "プライバシーポリシー"; + +/* About screen reddit label */ +"About.reddit" = "Reddit(レディット)"; + +/* About screen title */ +"About.title" = "概要"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "閉じる"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "サポートセンター"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "ウォレット読み込み中"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "マイブレッド"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "管理"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "ローカル破損エラー"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "ローカルデータベースが破損しています。 [設定]> [ブロックチェーン:設定]> [データベースの削除]に移動して更新します"; + +/* Error alert title */ +"Alert.error" = "エラー"; + +/* No internet alert message */ +"Alert.noInternet" = "インターネット接続が見つかりません。接続を確認してから、もう一度お試しください。"; + +/* Warning alert title */ +"Alert.warning" = "警告"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "アドレスがコピーされました"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "すべてのウォレットアドレスが正常にコピーされました。"; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "紙の鍵、セットされました。"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "素晴らしい!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PINコードのセット"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "ドメイン解決"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "あなたの住所は解決されました!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "送金に失敗しました"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "送金のご確認"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "送金しました!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSONシリアル化エラー"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "ウォレットの準備ができていません"; + +/* API Token error message */ +"ApiClient.tokenError" = "APIトークンを取得できません"; + +/* buy button */ +"Button.buy" = "購入"; + +/* Cancel button label */ +"Button.cancel" = "キャンセル"; + +/* Ignore button label */ +"Button.ignore" = "無視する"; + +/* menu button */ +"Button.menu" = "メニュー"; + +/* No button */ +"Button.no" = "いいえ"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "受取"; + +/* resetFields */ +"Button.resetFields" = "フィールドをリセット"; + +/* send button */ +"Button.send" = "送金する"; + +/* Settings button label */ +"Button.settings" = "設定"; + +/* Settings button label */ +"Button.submit" = "提出"; + +/* Yes button */ +"Button.yes" = "はい"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "購入"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "•ギフトカードを購入\n•プリペイド電話を補充\n•Steam、Amazon、Hotels.com \n•170か国で利用可能"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "ビットリフィル"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "•他の暗号のLitecoinを変更 \n•IDは不要\n•クレジットカードで購入 \n•グローバルな補償範囲"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Litecoin を購入する"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "•多くの法定紙幣ペアでLTCを購入する\n•複数の方法で支払う\n•グローバル決済プロバイダー"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "•5分でLitecoinを入手!\n •クレジットカードでLitecoinを購入 \n•パスポートまたは州ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "シンプレックス"; + +/* Buy Center Title */ +"BuyCenter.title" = "Litecoin を購入する"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "ご自身のIDをこのボックス内で中央に配置してください"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "交換の詳細:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "送金する数量:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "寄付金額:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "処理時間:これらのトランザクションの処理には %1$@ 分かかります。"; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "処理時間:このトランザクションは完了までに%1$@分かかる見込みです。"; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "送金する"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "費用:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "住所:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "承認"; + +/* To: (address) */ +"Confirmation.to" = "宛て先"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "費用合計:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "入力した単語は、紙のキーと一致しません。もう一度お試しください"; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "すべてが正しく書き留められていることを確認するため、紙の鍵に記入した下記のフレーズを入力してください。"; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "フレーズ #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "コピー"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "リテコインの表示単位"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "選ぶ:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "為替レート"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "この端末は、iOSのメールアプリを使用してメールを送信する設定がされていません。"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "メールが利用できません"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "この端末はメール送信の設定がされていません。"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "メッセージの送受信ができません"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "エコノミー"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "想定される送金時間:60分以上"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "このオプションは速く送金を済ませたい方にはお勧めしません。"; + +/* Luxury fee */ +"FeeSelector.luxury" = "ぜいたく"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "配達予定日:2.5 - 5 分"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "このオプションは、プレミアムを支払っていますが、トランザクションの受け入れを事実上保証します。"; + +/* Regular fee */ +"FeeSelector.regular" = "レギュラー"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "配達予定日:2.5 - 5+ 分"; + +/* Fee Selector title */ +"FeeSelector.title" = "処理速度"; + +/* Confirm */ +"Fragment.confirm" = "確認"; + +/* Or */ +"Fragment.or" = "或いは"; + +/* sorry */ +"Fragment.sorry" = "ごめん"; + +/* to */ +"Fragment.to" = "に"; + +/* History Bar Item Title */ +"History.barItemTitle" = "歴史"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "現在のLTC値"; + +/* Checking private key balance progress view text */ +"Import.checking" = "秘密鍵の残高確認中..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "この秘密鍵からウォレットに%1$@を送信しますか? リテコインネットワークは手数料%2$@を頂戴します。"; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "この秘密鍵はすでにあなたのウォレットに存在しています。"; + +/* empty private key error message */ +"Import.Error.empty" = "この秘密鍵は空白です。"; + +/* High fees error message */ +"Import.Error.highFees" = "トランザクション手数料がこのプライベートキーのアカウントにある残高を上回っています。"; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "有効な秘密鍵ではありません"; + +/* Import signing error message */ +"Import.Error.signing" = "トランザクションへのサイン中にエラーが発生しました"; + +/* Import button label */ +"Import.importButton" = "インポート"; + +/* Importing wallet progress view label */ +"Import.importing" = "ウォレットをインポート中"; + +/* Caption for graphics */ +"Import.leftCaption" = "インポート\nされる\nウォレット"; + +/* Import wallet intro screen message */ +"Import.message" = "ウォレットをインポートすると、1回のトランザクションで、ご自身の他のウォレットからすべてのお金をブレッドのウォレットへ転送できます。"; + +/* Enter password alert view title */ +"Import.password" = "この秘密鍵はパスワードで保護されています。"; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "パスワード"; + +/* Caption for graphics */ +"Import.rightCaption" = "あなたの\nブレッドの\nウォレット"; + +/* Scan Private key button label */ +"Import.scan" = "秘密鍵をスキャン"; + +/* Import wallet success alert title */ +"Import.success" = "成功"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "ウォレットが正常にインポートされました。"; + +/* Import Wallet screen title */ +"Import.title" = "ウォレットをインポート"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "鍵のロックを解除中です"; + +/* Import wallet intro warning message */ +"Import.warning" = "ウォレットのインポートには、取引履歴やその他の詳細は含まれません。"; + +/* Wrong password alert message */ +"Import.wrongPassword" = "パスワードが違います。もう一度お試しください。"; + +/* Close app button */ +"JailbreakWarnings.close" = "閉じる"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "無視する"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "端末がセキュリティの危険にさらされている可能性があります\n 「脱獄」アプリはブレッドのキーチェーンデータにアクセスし、あなたのリテコインを盗む恐れがあります! 直ちにこのウォレットを完全削除して、安全な端末で復元して下さい。"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "端末がセキュリティの危険にさらされている可能性があります\n「脱獄」アプリはブレッドのキーチェーンデータにアクセスし、あなたのリテコインを盗む恐れがあります! ブレッドは必ず脱獄端末以外で利用してください。"; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "警告"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "完全削除"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "位置情報サービスが無効化されています。"; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "ブレッドは位置情報サービスへのアクセスが許可されていません。"; + +/* No comment provided by engineer. */ +"Malformed URI" = "不正なURI"; + +/* Balance */ +"ManageWallet.balance" = "残高"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "あなたは%1$@でご自身のウォレットを作成しました"; + +/* Manage wallet description text */ +"ManageWallet.description" = "あなたのウォレットの名前はご自身のアカウントの取引履歴上でのみ表示され、あなた以外誰も閲覧することができません。"; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "ウォレットの名前"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "ウォレットを管理"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "リテコインを購入"; + +/* Menu button title */ +"MenuButton.customer.support" = "顧客サポート"; + +/* Menu button title */ +"MenuButton.lock" = "ウォレットにロックを設定する"; + +/* Menu button title */ +"MenuButton.security" = "セキュリティセンター"; + +/* Menu button title */ +"MenuButton.settings" = "設定"; + +/* Menu button title */ +"MenuButton.support" = "サポート"; + +/* button label */ +"MenuViewController.createButton" = "新規ウォレットを作成"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "メニュー"; + +/* button label */ +"MenuViewController.recoverButton" = "ウォレットを復元"; + +/* No comment provided by engineer. */ +"No wallet" = "財布なし"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "自動モードに切り替える"; + +/* Node is connected label */ +"NodeSelector.connected" = "接続中"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "ノードのIPアドレスとポートを入力する(任意)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "ノードを入力する"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "マニュアルモードに切り替える"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "現在のプライマリーノード"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "未接続"; + +/* Node status label */ +"NodeSelector.statusLabel" = "ノードの接続状況"; + +/* Node Selector view title */ +"NodeSelector.title" = "リテコインノード"; + +/* "Email address label" */ +"Notifications.emailLabel" = "電子メールアドレス"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "ここに入力"; + +/* "Email title" */ +"Notifications.emailTitle" = "見逃さないでください!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "優先言語:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "更新とコンテストについて聞いてサインアップしてください。"; + +/* Signup cancel */ +"Notifications.signupCancel" = "結構です。"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "不正な支払い要求"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "ドキュメントがサポートされていないか、またはドキュメントが破損しています"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "証明書が不足しています"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "期限切れのリクエスト"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "支払いが行えませんでした"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "%1$@未満のリテコインの支払いは行えません。"; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "リテコインのトランザクションの出力は$@以上である必要があります。"; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "署名の種類がサポートされていません"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "信頼できない証明書"; + +/* Dismiss button. */ +"Prompts.dismiss" = "退出させる"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "ウォレットを保護するためには端末のパスコードが必要です。設定画面にてパスコードをオンにしてください。"; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "端末のパスコードをオンにする"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "継続する"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "携帯電話の紛失時や変更時に備えて、お客様のペーパーキーは必ず保管しておいてください。続けるにはここをタップしてください。"; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "キャンセル"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "有効にする"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "アクションが要求されました"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "あなたのウォレットは同期されていない可能性があります。ブロックチェーンを再スキャンすることで修復できる可能性があります。"; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "トランザクションが拒否されました"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "ブレッドは6桁のPINコード使用にアップグレードされました。こちらをタップして更新してください。"; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "PINコードを更新する"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "匿名のデータを共有してLitewalletの機能向上に役立てる"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "匿名のデータを共有する"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Touch IDを有効にするにはこちらをタップしてください"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Touch IDを有効化する"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "受信する"; + +/* Address copied message. */ +"Receive.copied" = "クリップボードにコピーされました。"; + +/* Share via email button label */ +"Receive.emailButton" = "メール"; + +/* Request button label */ +"Receive.request" = "金額のリクエスト"; + +/* Share button label */ +"Receive.share" = "シェアする"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "テキストメッセージ"; + +/* Receive modal title */ +"Receive.title" = "受け取る"; + +/* Done button text */ +"RecoverWallet.done" = "終了"; + +/* Recover wallet header */ +"RecoverWallet.header" = "ウォレットを復元"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "PINコードをリセット"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "紙の鍵を入力してください"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "紙の鍵を使用し、あなたのブレッドを復元する。"; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "あなたが入力した紙の鍵は無効です。各フレーズを再確認の上、もう一度お試しください。"; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "左矢印"; + +/* Next button label */ +"RecoverWallet.next" = "次へ"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "詳しくはこちらをタップしてください。"; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "右矢印"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "復元を希望するウォレットのペーパーキーを入力してください。"; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "PINコードをリセットするには、紙の鍵の中からこのフレーズを下記ボックスの中に入力してください。"; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "初めに数量を入力してください。"; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "金額のリクエスト"; + +/* Alert action button label */ +"ReScan.alertAction" = "同期"; + +/* Alert message body */ +"ReScan.alertMessage" = "同期中は送金することができません。"; + +/* Alert message title */ +"ReScan.alertTitle" = "ブロックチェーンで同期しますか?"; + +/* extimated time */ +"ReScan.body1" = "20〜45分"; + +/* Syncing explanation */ +"ReScan.body2" = "リテコインネットワークでトランザクションが完了したと表示されているが、Litewallet上では完了と表示されない場合。"; + +/* Syncing explanation */ +"ReScan.body3" = "送金しているが、拒否され続けている場合。"; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "同期を開始する"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "ブロックチェーンで同期中は送金することはできません。"; + +/* Sync Blockchain view header */ +"ReScan.header" = "ブロックチェーン同期"; + +/* Subheader label */ +"ReScan.subheader1" = "予定時刻"; + +/* Subheader label */ +"ReScan.subheader2" = "いつ同期しますか?"; + +/* Reset walet button title */ +"resetButton" = "はい、ウォレットをリセットします"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Litewalletを削除します"; + +/* Scan bitcoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "カメラのフラッシュ"; + +/* Complete filter label */ +"Search.complete" = "完了"; + +/* Pending filter label */ +"Search.pending" = "保留中"; + +/* Received filter label */ +"Search.received" = "受取済"; + +/* Sent filter label */ +"Search.sent" = "送信済"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "セキュリティを最大限に高めるため、すべてのセキュリティ設定を有効にしてください。"; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "あなたが携帯電話を紛失したり、アップグレードした場合にご自身のリテコインへアクセスできる唯一の方法です。"; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "ペーパーキー"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "アクセス権限のないユーザーからあなたのブレッドを守ります。"; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6桁のPINコード"; + +/* Security Center Title */ +"SecurityCenter.title" = "セキュリティセンター"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Litewalletロック解除の他、設定した限度額までの送金も指紋認証で出来ます。"; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "金額"; + +/* Balance: $4.00 */ +"Send.balance" = "残高: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "手数料: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "送信"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "設定画面へ移動し、カメラへのアクセス許可をしてください。"; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "ブレッドはカメラへのアクセスが許可されていません"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "送金先がご自身のアドレスになっています。自分宛てには送金できません。"; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "トランザクションの作成ができませんでした。"; + +/* Description for sending money label */ +"Send.descriptionLabel" = "メモ"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "ペーストボードが空です"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Litecoinのアドレスを入力してください。"; + +/* Fees: $0.01*/ +"Send.fee" = "料金: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "料金:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "受取人の本人確認が行われていません。"; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "資金不足"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "送金先のアドレスが無効なリテコインアドレスです。"; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "ペーストボードに有効なリテコインアドレスが入っていません。"; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "無効なアドレスです"; + +/* Is rescanning error message */ +"Send.isRescanning" = "フルリスキャンを行っている間は送金できません。"; + +/* Loading request activity view message */ +"Send.loadingRequest" = "ロード要求"; + +/* Network */ +"Send.networkFee" = "通信網"; + +/* Empty address alert message */ +"Send.noAddress" = "受取人のアドレスを入力してください。"; + +/* Emtpy amount alert message */ +"Send.noAmount" = "送金金額を入力してください。"; + +/* Paste button label */ +"Send.pasteLabel" = "貼り付け"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "取引を実行できませんでした。"; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "支払い要求がロードできませんでした"; + +/* Scan button label */ +"Send.scanLabel" = "スキャン"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "送金する"; + +/* Service */ +"Send.serviceFee" = "通信網"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "送金"; + +/* Send money to label */ +"Send.toLabel" = "宛先"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "ドメイン"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "入力します"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "調べる"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "申し訳ありませんが、ドメインが見つかりませんでした。 [エラー: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "ルックアップに失敗しました"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = ".crypto、.wallet、.zil、.nft、.blockchain、.bitcoin、.coin、.888、.dao、または.xドメインを入力します。"; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "ドメインを入力"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "システム ルックアップの問題。 [エラー: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "リテコインアドレスは、個人利用のみを想定しています。"; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "再利用は、最初の利用者と受取人の両方のプライバシーの侵害につながるおそれがあり、受取人がアドレスを直接管理しない場合、損害につながるおそれもあります。"; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "アドレスはすでに使用されています"; + +/* About label */ +"Settings.about" = "概要"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "詳細設定"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "ブロックチェーン"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "本当に言語を変更しますか?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "表示される通貨"; + +/* Current Locale */ +"Settings.currentLocale" = "現在のロケール:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "早期アクセスに参加"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "ブレッドをお楽しみいただいていますか?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "ウォレットをインポート"; + +/* Languages label */ +"Settings.languages" = "言語"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "環境:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewalletパートナー"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewalletバージョン:"; + +/* Manage settings section header */ +"Settings.manage" = "管理"; + +/* Notifications label */ +"Settings.notifications" = "通知"; + +/* Leave review button label */ +"Settings.review" = "レビューを書く"; + +/* Share anonymous data label */ +"Settings.shareData" = "匿名データをシェアする"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "ソーシャル"; + +/* Support settings section header */ +"Settings.support" = "サポート"; + +/* Sync blockchain label */ +"Settings.sync" = "ブロックチェーン同期"; + +/* Settings title */ +"Settings.title" = "設定"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch IDの利用限度額"; + +/* Wallet Settings section header */ +"Settings.wallet" = "ウォレット"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "別のウォレットを作成、または復元する"; + +/* Share data view body */ +"ShareData.body" = "匿名データをシェアして、ブレッドの改善にご協力ください。これにはいかなる財務情報も含まれません。私たちはあなたの財務プライバシーを尊重しています。"; + +/* Share data header */ +"ShareData.header" = "データをシェしますか?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "匿名データをシェアしますか?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "現在の支出制限:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "紙の鍵をもう一度書き留めてください"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "紙の鍵は、あなたが携帯電話を紛失したり、盗まれたり、壊れたり、アップグレードした場合に、ご自身のブレッドを復元できる唯一の方法です。\n\nフレーズの一覧を表示しますので、紙に書き留めて安全に保管してください。"; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "紙の鍵を書き留めてください"; + +/* Argument is date */ +"StartPaperPhrase.date" = "あなたが最後に紙の鍵を書き留めたのは %1$@です"; + +/* Start view tagline */ +"StartViewController.tagline" = "最も安全にリテコインを使う手段。"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Litecoin基金を支援する"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "接続しています..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "再スキャンしています..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "成功"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "同期しています..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "接続中"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "同期中"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@日"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@時間"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@分"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@秒"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "%1$@からTouch ID利用限度額を変更できます。"; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "設定された限度額以下の送金を行う場合には、指紋を使ってご自身のブレッドのロックを解除してください。"; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Touch ID利用限度額画面"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "利用限度: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "ブレッドでTouch IDの有効化"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "あなたはこの端末でTouch IDの利用設定をしていません。設定->Touch IDとパスコードに移動し、今すぐ設定してください。"; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch IDが設定されていません"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "常にパスコードを要求する"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "限度額を超える金額の送金を行う場合や、最後に6桁のPINコードを入力してから48時間毎に6桁のPINコードの入力が求められます。"; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch IDの利用限度額"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "取引金額の詳細"; + +/* Availability status text */ +"Transaction.available" = "利用可能"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "ブロック:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "メモ:"; + +/* Transaction complete label */ +"Transaction.complete" = "完了"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "取引終了額の詳細"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "期末残高: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "受け取り時の為替レート:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "送金時の為替レート:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ 手数料)"; + +/* Invalid transaction */ +"Transaction.invalid" = "無効"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "たった今"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "進行中: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "進行中: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "期首残高: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "取引開始額の詳細"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "認証を待っています。トランザクションの完了に認証を必要とする販売者もいます。推定時間: 1-2時間。"; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "アカウント"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "金額"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "ブロックにて承認済み"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "メモ"; + +/* Copied */ +"TransactionDetails.copiedAll" = "コピー"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "すべての詳細をコピーする"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "あなたの取引はこちらに表示されます。"; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "%1$@で"; + +/* Less button title */ +"TransactionDetails.less" = "もっと少なく"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "%1$@を移動させました"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "%1@を移動させました"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "未承認"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "の時点で"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "%1$@を受け取りました"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "%1@を受け取りました"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "受信アドレス"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "%1$@を送金しました"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "%1@を送金しました"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "ステータス"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "取引詳細"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "%1$@に"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "リテコイン トランザクションID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "このアドレスで受け取りました"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "このアドレスに送金しました"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "以下まで無効: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "PINを入力してください"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "FaceID でロック解除"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "マイアドレス"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "PINコードをリセット"; + +/* Scan button title */ +"UnlockScreen.scan" = "スキャン"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "ブレッドのロックを解除してください。"; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Touch IDでロックを解除する"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "ウォレットのロックが解除されました"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "このPINコードは覚えておいてください。忘れた場合、ご自身のリテコインへアクセスできなくなります。"; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "あなたのPINコードはご自身のブレッドのロックを解除したり、送金する時に利用します。"; + +/* Update PIN title */ +"UpdatePin.createTitle" = "PINコードの設定"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "PINコードを再入力してください"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "現在のPINコードを入力してください。"; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "新しいPINコードを入力してください。"; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "新しいPINコードを再入力してください。"; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "申し訳ありませんが、PINコードが更新できませんでした。"; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "PINコード更新エラー"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "PINコードを更新する"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "ウォレットアドレスをクリップボードにコピーしますか?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "ウォレットアドレスのクリップボードへのコピーを認証"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "ウォレットアドレスをコピーします"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "コピー"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "このトランザクションを認証するためにはPINを入力してください。"; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "続けるにはPINコードを入力してください。"; + +/* Verify PIN view title */ +"VerifyPin.title" = "PINコードが必要です"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "このトランザクションを認証する"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "LitewalletのiPhoneアプリを開き、ウォレットをセットアップする"; + +/* Dismiss button label */ +"Webview.dismiss" = "解除"; + +/* Webview loading error message */ +"Webview.errorMessage" = "このコンテンツの読み込み中にエラーが発生しました。もう一度お試しください。"; + +/* Updating webview message */ +"Webview.updating" = "更新中..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Litewalletへようこそ!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Litewalletへようこそ!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "データベースを削除"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "本当にこのウォレットを削除しますか?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "ウォレットを消去しますか?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "データベースを削除"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "これにより、データベースは削除されますが、PINとフレーズは保持されます。既存のPINを確認してシードし、新しいデータベースへの同期が完了するまで待ちます"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "削除と同期"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "シードフレーズまたはPINを忘れましたか?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "ウォレットを消去できませんでした。"; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "失敗しました"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "ウォレットを消去し、新しいウォレットを作成したり、復元したりしたい場合は復旧フレーズを入力してください。この段階では残高は残っています。"; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "別のウォレットを開設するか復元することにより、このデバイス上で異なるLitewalletウォレットへのアクセスと管理ができるようになります。"; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "このデバイスから現在使用中のLitewalletウォレットへのアクセスはできなくなります。残高はフレーズに残されます。"; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "別のウォレットを作成、または復元する"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "このアクションにより、Litewalletがワイプされます。"; + +/* Warning description */ +"WipeWallet.warningDescription" = "ウォレットを削除すると、秘密鍵とアプリデータのワイプが失われます。 あなたはライトコインを永遠に失うかもしれません! \ n \ n \ nLitewalletチームの誰もこのシードを取得できません。 この警告に注意を怠った場合、当社は責任を負いません"; + +/* Warning title */ +"WipeWallet.warningTitle" = "読んでください!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "消去"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "消去中..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "フレーズは順番通りに書き留めて、安全な場所に保管しておいてください。"; + +/* button label */ +"WritePaperPhrase.next" = "次へ"; + +/* button label */ +"WritePaperPhrase.previous" = "前へ"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%2$dの%1$d"; diff --git a/litewallet/Strings/ko.lproj/Localizable.strings b/litewallet/Strings/ko.lproj/Localizable.strings new file mode 100755 index 000000000..26a7b5830 --- /dev/null +++ b/litewallet/Strings/ko.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "블로그"; + +/* About screen footer */ +"About.footer" = "글로벌 Litewallet 팀 제작. 버전 %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "개인정보 보호정책"; + +/* About screen reddit label */ +"About.reddit" = "레딧"; + +/* About screen title */ +"About.title" = "관련 사항"; + +/* About screen twitter label */ +"About.twitter" = "트위터"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "닫기"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "고객지원 센터"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "지갑 로딩"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "나의 Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "관리하기"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "로컬 손상 오류"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "데이터베이스는 삭제되지만 PIN과 문구는 유지됩니다. 기존 PIN을 확인하고 시드하고 새 DB와의 동기화를 기다립니다."; + +/* Error alert title */ +"Alert.error" = "오류"; + +/* No internet alert message */ +"Alert.noInternet" = "인터넷이 연결되지 않았습니다. 연결을 확인하시고 다시 시도해 주세요."; + +/* Warning alert title */ +"Alert.warning" = "경고"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "주소들 복사됨"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "모든 지갑 주소들이 성공적으로 복사됨."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "종이 열쇠 준비"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "굉장해요!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN 준비"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "도메인 확인"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "주소가 확인되었습니다!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "보내기 실패함"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "보내기 확인"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "돈을 보냈어요!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON 직렬화 오류"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "지갑 준비 안됨"; + +/* API Token error message */ +"ApiClient.tokenError" = "API 토큰을 가져올 수 없습니다"; + +/* buy button */ +"Button.buy" = "구입"; + +/* Cancel button label */ +"Button.cancel" = "취소"; + +/* Ignore button label */ +"Button.ignore" = "무시"; + +/* menu button */ +"Button.menu" = "메뉴"; + +/* No button */ +"Button.no" = "아니오"; + +/* OK button label */ +"Button.ok" = "오케이"; + +/* receive button */ +"Button.receive" = "받기"; + +/* resetFields */ +"Button.resetFields" = "리셋 필드"; + +/* send button */ +"Button.send" = "보내기"; + +/* Settings button label */ +"Button.settings" = "설정"; + +/* Settings button label */ +"Button.submit" = "전송"; + +/* Yes button */ +"Button.yes" = "예"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "구입"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• 기프트 카드 구매 \n • 선불 전화 리필 \n • Steam, Amazon, Hotels.com \ n • 170 개국에서 작동"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "비트 리필"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "다른 암호화를 위해 Litecoin 변경 \n • ID 불필요 \n • 신용 카드로 구매 \ n • 글로벌 범위"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "체인지"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Łitecoin 구매"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• 많은 법정 화폐 쌍으로 LTC 구매\n• 여러 방법으로 지불\n• 글로벌 지불 공급자"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "5 분 안에 Litecoin을 받으십시오! \n • 신용 카드를 통해 Litecoin을 구매하십시오 \n • 여권 또는 주 ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "심플 렉스"; + +/* Buy Center Title */ +"BuyCenter.title" = "Litecoin 구매"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "박스안에 귀하의 ID를 중앙에 두세요"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "교환 내용"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "보낼 금액:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "기부 금액"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "처리 시간 :이 거래는 % 1 $ @ 분이 소요됩니다."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "처리 시간: 이 트랜잭션을 처리하는 데 %1$@분이 걸립니다."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "보내기"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "회비"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "주소"; + +/* Confirmation Screen title */ +"Confirmation.title" = "확정"; + +/* To: (address) */ +"Confirmation.to" = "수취인"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "총 금액:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "입력 한 단어가 종이 키와 일치하지 않습니다. 다시 시도하십시오"; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "모든 것이 정확하게 적혀 있는지 확인하기 위해, 귀하의 종이 열쇠로부터 다음의 단어들을 입력해 주세요."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "단어 #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "부"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin Display Unit"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "고르다:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "환율"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "이 장치는 iOS 메일 앱으로 이메일을 전송하도록 설정되지 않았습니다."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "이메일 사용불가"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "이 장치는 메시지를 보내도록 설정되지 않았습니다."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "메시지 사용불가"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "경제"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "예상 배송 시간: 10분 이상"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "이 옵션은 시간에 민감한 거래에는 추천하지 않습니다."; + +/* Luxury fee */ +"FeeSelector.luxury" = "사치"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "배송 예정 시간 : 2.5 - 5 분"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "이 옵션은 귀하가 보험료를 지불하더라도 사실상 거래 수락을 보장합니다."; + +/* Regular fee */ +"FeeSelector.regular" = "일반"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "배송 예정 시간 : 2.5 - 5+ 분"; + +/* Fee Selector title */ +"FeeSelector.title" = "처리 속도"; + +/* Confirm */ +"Fragment.confirm" = "확인하다"; + +/* Or */ +"Fragment.or" = "또는"; + +/* sorry */ +"Fragment.sorry" = "죄송합니다"; + +/* to */ +"Fragment.to" = "NS"; + +/* History Bar Item Title */ +"History.barItemTitle" = "역사"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "현재 LTC 값"; + +/* Checking private key balance progress view text */ +"Import.checking" = "개인 열쇠 잔고 확인 중..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "이 개인 열쇠로부터 귀하의 지갑으로 %1$@ 를 보낼까요? Litecoin 네트워크가 %2$@의 수수료를 받게 됩니다."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "이 개인 열쇠가 이미 귀하의 지갑에 있습니다."; + +/* empty private key error message */ +"Import.Error.empty" = "이 개인 열쇠는 비어 있습니다."; + +/* High fees error message */ +"Import.Error.highFees" = "이 개인 열쇠에서 사용할 수 있는 자금보다 거래 수수료가 더 많이 듭니다."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "유효하지 않은 개인 열쇠입니다"; + +/* Import signing error message */ +"Import.Error.signing" = "서명 거래 오류"; + +/* Import button label */ +"Import.importButton" = "불러오기"; + +/* Importing wallet progress view label */ +"Import.importing" = "지갑 불러오는 중"; + +/* Caption for graphics */ +"Import.leftCaption" = "불러오기 할 지갑"; + +/* Import wallet intro screen message */ +"Import.message" = "지갑 불러오기는 단일 거래를 사용하여 귀하의 다른 지갑으로부터 귀하의 Litewallet 지갑으로 모든 돈을 송금합니다."; + +/* Enter password alert view title */ +"Import.password" = "이 개인 열쇠는 비밀번호로 보호됩니다."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "비밀번호"; + +/* Caption for graphics */ +"Import.rightCaption" = "귀하의 Litewallet 지갑"; + +/* Scan Private key button label */ +"Import.scan" = "개인 열쇠 스캔"; + +/* Import wallet success alert title */ +"Import.success" = "성공"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "성공적으로 불러온 지갑."; + +/* Import Wallet screen title */ +"Import.title" = "지갑 내보내기"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "열쇠 풀기"; + +/* Import wallet intro warning message */ +"Import.warning" = "지갑 불러오기는 거래 기록 혹은 다른 세부사항을 포함하지 않습니다."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "잘못된 비밀번호. 다시 시도해 주세요."; + +/* Close app button */ +"JailbreakWarnings.close" = "닫기"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "무시"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "장치 보안 취약\n모든 'jailbreak' 앱이 Litewallet의 키체인 데이터에 접속하여 귀하의 Litecoin을 훔칠 수 있습니다! 이 지갑을 바로 지우시고 안전한 장치에서 복구하세요."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "장치 보안 취약\n모든 'jailbreak' 앱이 Litewallet의 키체인 데이터에 접속하여 귀하의 Litecoin을 훔칠 수 있습니다! Litewallet를 비-jailbroken 장치에서만 사용해 주세요."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "경고"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "초기화"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "위치 서비스가 꺼졌습니다."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet가 위치 서비스 접속 권한이 없습니다."; + +/* No comment provided by engineer. */ +"Malformed URI" = "잘못된 URI"; + +/* Balance */ +"ManageWallet.balance" = "밸런스"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "귀하께서는 %1$@ 에 지갑을 만들었습니다"; + +/* Manage wallet description text */ +"ManageWallet.description" = "귀하의 지갑 이름은 귀하 계정의 거래 기록에서만 나타나며 다른 사람은 볼 수 없습니다."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "지갑 이름"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "지갑 관리"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Litecoin 구매"; + +/* Menu button title */ +"MenuButton.customer.support" = "고객 지원"; + +/* Menu button title */ +"MenuButton.lock" = "지갑 잠그기"; + +/* Menu button title */ +"MenuButton.security" = "보안 센터"; + +/* Menu button title */ +"MenuButton.settings" = "설정"; + +/* Menu button title */ +"MenuButton.support" = "고객지원"; + +/* button label */ +"MenuViewController.createButton" = "새 지갑 만들기"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "메뉴"; + +/* button label */ +"MenuViewController.recoverButton" = "지갑 복구하기"; + +/* No comment provided by engineer. */ +"No wallet" = "지갑 없음"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "자동 모드로 전환"; + +/* Node is connected label */ +"NodeSelector.connected" = "연결됨"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "노드 IP 주소와 포트를 입력하십시오(선택)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "노드를 입력하십시오"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "수동 모드로 전환"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "현재 주요 모드"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "연결되지 않음"; + +/* Node status label */ +"NodeSelector.statusLabel" = "노드 연결 상태"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin 노드"; + +/* "Email address label" */ +"Notifications.emailLabel" = "이메일 주소"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "여기에 입력하십시오"; + +/* "Email title" */ +"Notifications.emailTitle" = "놓치지 마세요!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "선호하는 언어:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "업데이트 및 컨테스트에 대해 들어 보려면 가입하십시오."; + +/* Signup cancel */ +"Notifications.signupCancel" = "고맙지 만 사양 할게."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "틀린 결제 요청"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "지원되지 않거나 망가진 문서"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "빠진 증명서"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "요청 만료됨"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "결제를 할 수 없습니다"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Litecoin 결제는 %1$@ 보다 작을 수 없습니다."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Litecoin 거래 출력들은 $@보다 작을 수 없습니다."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "지원되지 않는 서명 형태"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "신용할 수 없는 증명서"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DISMISS"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "지갑을 지키려면 장치 비밀번호가 필요합니다. 설정으로 가셔서 비밀번호를 켜십시오."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "기기 비밀번호 설정"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "계속하다"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "귀하의 종이 열쇠는 귀하의 핸드폰일 잃어버리거나 바꿀 경우에 대비해 저장되어야만 합니다. 계속하시려면 여기를 탭 하세요."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "취소"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "사용"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "조치 필요함"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "귀하의 지갑이 동기화 되어 있지 않을 수 있습니다. 블록체인을 재스캔하면 대부분 고쳐집니다."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "거래 거절됨"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "6자리 PIN을 사용하도록 브레드가 업그레이드되었습니다. 업그레이드하려면 여기를 탭하세요."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "PIN 업그레이드"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Litewallet가 발전할 수 있도록 익명 데이터를 공유해주십시오"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "익명 데이터 공유"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Touch ID를 켜시려면 여기를 탭하세요"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Touch ID 켜"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "받다"; + +/* Address copied message. */ +"Receive.copied" = "클립보드에 복사되었습니다."; + +/* Share via email button label */ +"Receive.emailButton" = "이메일"; + +/* Request button label */ +"Receive.request" = "금액 요청"; + +/* Share button label */ +"Receive.share" = "공유"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "문자 메시지"; + +/* Receive modal title */ +"Receive.title" = "수신"; + +/* Done button text */ +"RecoverWallet.done" = "완료"; + +/* Recover wallet header */ +"RecoverWallet.header" = "지갑 복구하기"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "PIN 재설정"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "페이퍼 키 입력"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "페이퍼 키를 사용해 브레드를 복구해 주세요."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "올바르지 않은 페이퍼 키를 입력하셨습니다. 각 단어를 재확인한 후 다시 시도해 주세요."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "왼쪽 화살표"; + +/* Next button label */ +"RecoverWallet.next" = "다음"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "자세한 내용을 보려면 여기를 탭하세요."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "오른쪽 화살표"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "복구하려는 지갑의 페이퍼 키를 입력해 주세요."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "PIN을 재설정하려면, 페이퍼 키의 단어들을 아래 상자에 입력해 주세요."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "먼저 금액을 입력해 주세요."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "금액 요청"; + +/* Alert action button label */ +"ReScan.alertAction" = "동기화"; + +/* Alert message body */ +"ReScan.alertMessage" = "동기화 중에는 송금할 수 없습니다."; + +/* Alert message title */ +"ReScan.alertTitle" = "블록체인과 동기화하겠습니까?"; + +/* extimated time */ +"ReScan.body1" = "20-45분"; + +/* Syncing explanation */ +"ReScan.body2" = "거래가 Litecoin 네트워크에는 완료된 것으로 표시되지만, 브레드에는 표시되지 않는 경우."; + +/* Syncing explanation */ +"ReScan.body3" = "거래가 거절되었음을 알리는 오류 메시지가 반복적으로 나타납니다."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "동기화 시작"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "블록체인과 동기화하는 동안에는 송금할 수 없습니다."; + +/* Sync Blockchain view header */ +"ReScan.header" = "블록체인 동기화"; + +/* Subheader label */ +"ReScan.subheader1" = "예상 시간"; + +/* Subheader label */ +"ReScan.subheader2" = "동기화는 언제 해야 합니까?"; + +/* Reset walet button title */ +"resetButton" = "예, 지갑 재설정"; + +/* Warning Empty Wipe title */ +"resetTitle" = "내 라이트월렛 삭제"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "카메라 플래시"; + +/* Complete filter label */ +"Search.complete" = "완료"; + +/* Pending filter label */ +"Search.pending" = "대기 중"; + +/* Received filter label */ +"Search.received" = "수신"; + +/* Sent filter label */ +"Search.sent" = "송금"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "최대한의 보호를 위해 모든 보안 기능을 사용하시기 바랍니다."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "전화를 분실하거나 업그레이드하는 경우, Litecoin에 액세스할 수 있는 유일한 방법입니다."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "페이퍼 키"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "허가받지 않은 사용자로부터 브레드를 보호하세요."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6자리 PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "보안 센터"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "편리하게 브레드의 잠금을 해제하고 최대한도까지 송금해 보세요."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "터치 ID"; + +/* Send money amount label */ +"Send.amountLabel" = "금액"; + +/* Balance: $4.00 */ +"Send.balance" = "잔액: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "요금: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "보내다"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "설정에서 카메라 액세스를 허가해 주세요."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "브레드는 카메라 액세스를 허용하지 않습니다."; + +/* Warning when sending to self. */ +"Send.containsAddress" = "여러분의 주소를 입력해 주세요. 본인에게 전송할 수 없습니다."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "전송할 수 없습니다."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "메모"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "클립보드가 비어 있습니다"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "라이트 코인 주소를 입력하세요"; + +/* Fees: $0.01*/ +"Send.fee" = "수수료: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "수수료:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "인증되지 않은 수취인입니다."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "부족한 자금"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "해당 수취인 주소는 Litecoin에서 유효하지 않은 주소입니다."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "클립보드가 유효한 Litecoin 주소를 가지고 있지 않습니다"; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "유효하지 않은 주소입니다"; + +/* Is rescanning error message */ +"Send.isRescanning" = "전체 재검색 중에는 송금이 비활성화됩니다."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "요청 불러오는 중"; + +/* Network */ +"Send.networkFee" = "회로망"; + +/* Empty address alert message */ +"Send.noAddress" = "수신인의 주소를 입력하세요."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "송금액을 입력하세요"; + +/* Paste button label */ +"Send.pasteLabel" = "붙여넣기"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "전송할 수 없습니다."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "결제 요청을 불러올 수 없었습니다"; + +/* Scan button label */ +"Send.scanLabel" = "스캔"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "보내기"; + +/* Service */ +"Send.serviceFee" = "서비스"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "보내기"; + +/* Send money to label */ +"Send.toLabel" = "수취인"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "도메인"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "입력"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "조회"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "죄송합니다. 도메인을 찾을 수 없습니다. [오류 : %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "조회 실패"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = ".crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao 또는 .x 도메인을 입력하세요."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "도메인 입력"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "시스템 조회 문제. [오류: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin 주소는 일회용입니다."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "주소를 재사용할 경우 귀하와 받는 사람 모두의 개인 정보 보호가 약해지며, 받는 사람이 직접 주소를 관리하지 않을 경우 분실을 초래할 수 있습니다."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "이미 사용중인 주소입니다"; + +/* About label */ +"Settings.about" = "관련 사항"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "고급 설정"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "블록 체인"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "언어를 변경하시겠습니까?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "통화 보기"; + +/* Current Locale */ +"Settings.currentLocale" = "현재 로케일 :"; + +/* Join Early access label */ +"Settings.earlyAccess" = "초기 버전을 사용해 보세요"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "브레드를 사용하고 있으신가요?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "지갑 내보내기"; + +/* Languages label */ +"Settings.languages" = "언어"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "환경:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet 파트너"; + +/* Litewallet version */ +"Settings.litewallet.version" = "라이트 월렛 버전 :"; + +/* Manage settings section header */ +"Settings.manage" = "관리"; + +/* Notifications label */ +"Settings.notifications" = "알림"; + +/* Leave review button label */ +"Settings.review" = "후기를 남겨주세요"; + +/* Share anonymous data label */ +"Settings.shareData" = "익명 데이터 공유"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "사회적인"; + +/* Support settings section header */ +"Settings.support" = "지원하다"; + +/* Sync blockchain label */ +"Settings.sync" = "블록체인 동기화"; + +/* Settings title */ +"Settings.title" = "설정"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch ID 지출 한도"; + +/* Wallet Settings section header */ +"Settings.wallet" = "지갑"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "다른 월렛을 시작/복원"; + +/* Share data view body */ +"ShareData.body" = "브레드 기능 향상을 위해 여러분의 익명 데이터를 공유해 주세요. 금융 정보는 포함되지 않습니다. 여러분의 금융 관련 개인정보는 안전하게 보호됩니다."; + +/* Share data header */ +"ShareData.header" = "데이터를 공유하시겠습니까?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "익명 데이터를 공유하시겠습니까?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "현재 지출 한도"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "페이퍼 키를 다시 쓰기"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "페이퍼 키는 핸드폰을 분실하거나, 도난당하거나, 망가지거나, 업그레이드한 경우 브레드를 다시 살릴 수 있는 유일한 방법입니다.\n\n저희가 제시하는 단어 목록을 잘 적어서 안전하게 보관해 주세요."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "페이퍼 키 작성하기"; + +/* Argument is date */ +"StartPaperPhrase.date" = "%1$@에 마지막으로 페이퍼 키를 작성하셨습니다."; + +/* Start view tagline */ +"StartViewController.tagline" = "라이트코인을 사용하는 가장 안전하고 쉬운 방법입니다."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "라이트 코인 재단 지원"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "연결 중 ..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "다시 스캔하는 중 ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "성공!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "동기화 중 ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "연결 중"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "동기화 중"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ 일"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ 시"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ 분"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ 초"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "%1$@에서 ???????????."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "지문을 사용하여 브레드의 보안을 해제하고 설정된 금액으로 송금할 수 있습니다."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "% 1 $ @ (% 2 $ @)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "터치 ID 지불 한도 화면"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "지불 한도: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "브레드에서"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "터치 ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "이 기기에서 Touch ID가 설정되어 있지 않습니다. 지금 설정 -> Touch ID 및 암호로 이동하여 설정해 주세요."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID가 설정되지 않음"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "항상 비밀번호 요구"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "지출 한도를 넘는 트랜잭션을 보내실 경우, 그리고 마지막으로 6자리 PIN 코드를 입력하시고 48시간이 경과한 경우에는 6자리 PIN 코드를 입력하셔야 합니다."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch ID 지출 한도"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "금액 세부 정보"; + +/* Availability status text */ +"Transaction.available" = "지출 가능"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "블록:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "메모:"; + +/* Transaction complete label */ +"Transaction.complete" = "완료"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "거래 종료 금액 세부 정보"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "기말 잔액: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "받을 때의 환율:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "보낼 때의 환율:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ 수수료)"; + +/* Invalid transaction */ +"Transaction.invalid" = "무효"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "지금 바로"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "진행 중: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "진행 중: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "기초 잔액: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "거래 시작 금액 세부 정보"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID :"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "확인 대기 중. 일부 판매자는 트랜잭션을 완료하기 위해 확인을 요구합니다. 예상 소요 시간: 1~2시간."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "계정"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "금액"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "차단 확인"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "메모"; + +/* Copied */ +"TransactionDetails.copiedAll" = "복사 됨"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "모든 세부 사항 복사"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "귀하의 트랜잭션이 여기에 표시됩니다."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "%1$@에서"; + +/* Less button title */ +"TransactionDetails.less" = "적게"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "%1$@ 이동함"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "%1@ 이동됨"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "확인되지 않음"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "현재"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "%1$@ 받음"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "%1@ 수신됨"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "수신 주소"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "%1$@ 보냄"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "%1@ 전송됨"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID :"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "상태"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "트랜잭션 세부 정보"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "%1$@에게"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin Transaction ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "이 주소에서 받음"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "이 주소로 보냄"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "다음 기간까지 비활성화됨: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "PIN을 입력하십시오"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "FaceID로 잠금 해제"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "내 주소"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "PIN 재설정"; + +/* Scan button title */ +"UnlockScreen.scan" = "스캔"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Litewallet의 잠금을 해제하세요."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Touch ID로 잠금 해제"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "월렛의 잠금이 해제됨"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "이 PIN 코드를 기억하세요. PIN 코드를 잊어버리면 자신의 Litecoin을 사용할 수 없게 됩니다."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "귀하의 PIN 코드는 Litewallet의 잠금을 해제하고 돈을 송금하는 데 사용됩니다."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "PIN 설정"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "PIN 코드를 다시 입력"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "현재의 PIN 코드를 입력하세요."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "새 PIN 코드를 입력하세요."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "새 PIN 코드를 다시 입력하세요."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "죄송합니다, PIN 코드를 업데이트하지 못했습니다."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "PIN 코드 업데이트 오류"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "PIN 코드 업데이트"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "월렛 주소를 클립 보드에 복사할까요?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "월렛 주소를 클립 보드에 복사할 수 있는 권한을 부여"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "월렛 주소 복사하기"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "복사"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "PIN을 입력하여 이 거래를 승인하십시오."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "계속하려면 PIN 코드를 입력해 주세요."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN 코드 입력 필수"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "이 거래 승인"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "귀하의 월렛을 설정하려면 Litewallet 앱을 열어 주세요."; + +/* Dismiss button label */ +"Webview.dismiss" = "무시"; + +/* Webview loading error message */ +"Webview.errorMessage" = "콘텐츠를 로드하는 중에 오류가 발생했습니다. 다시 시도해 주세요."; + +/* Updating webview message */ +"Webview.updating" = "업데이트 중..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Litewallet에 환영합니다!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Litewallet에 환영합니다!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "데이터베이스 삭제"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "정말 이 월렛을 삭제할까요?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "월렛을 초기화할까요?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "데이터베이스 삭제"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "로컬 데이터베이스가 손상되었습니다. 설정> 블록 체인 : 설정> 데이터베이스 삭제로 이동하여 새로 고침"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "삭제 및 동기화"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "시드 문구 나 PIN을 잊어 버리셨습니까?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "월렛 초기화가 실패함."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "실패함"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "월렛을 초기화하고 다른 월렛을 시작하거나 복원하려면 이 월렛의 초기화 문구를 입력해 주세요. 현재 잔액은 이 문구에 그대로 보관됩니다."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "다른 지갑을 만들거나 복구하면 이 장치에서 다른 Litewallet 지갑을 이용하고 관리할 수 있습니다."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "이장치에서 현재 Litewallet 지갑을 더는 이용할 수 없게 됩니다. 잔고는 남아 있게 됩니다."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "다른 월렛을 시작 또는 복원"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "이 작업은 Litewallet을 지울 것입니다!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "지갑을 삭제하면 개인 키와 앱 데이터 지우기가 사라집니다. Litecoin을 영원히 잃을 수 있습니다!\n\n\nLitewallet 팀의 누구도 이 시드를 검색할 수 없습니다. 이 경고에 주의를 기울이지 않으면 우리는 책임을 지지 않습니다."; + +/* Warning title */ +"WipeWallet.warningTitle" = "읽어주세요!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "초기화"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "초기화 중..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "각 단어를 순서대로 적어 놓고, 안전한 곳에 보관하세요."; + +/* button label */ +"WritePaperPhrase.next" = "다음"; + +/* button label */ +"WritePaperPhrase.previous" = "이전"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%2$d의 %1$d"; diff --git a/litewallet/Strings/nl.lproj/Localizable.strings b/litewallet/Strings/nl.lproj/Localizable.strings new file mode 100755 index 000000000..b8d2e9ca8 --- /dev/null +++ b/litewallet/Strings/nl.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "Blog"; + +/* About screen footer */ +"About.footer" = "Gemaakt door het globale Litewallet team. Versie %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Privacyverklaring"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Over"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Sluiten"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Ondersteuningscentrum"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Portemonnee Laden"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Mijn Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "BEHEREN"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Lokale corruptiefout"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Uw lokale database is beschadigd. Ga naar Instellingen> Blockchain: Instellingen> Verwijder database om te vernieuwen"; + +/* Error alert title */ +"Alert.error" = "Error"; + +/* No internet alert message */ +"Alert.noInternet" = "Er is geen internetconnectie gevonden. Controleer je connectie en probeer opnieuw."; + +/* Warning alert title */ +"Alert.warning" = "Waarschuwing"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Adressen Gekopieerd"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Alle portemonnee adressen zijn succesvol gekopieerd."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Papieren Sleutel Set."; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Geweldig!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN Set."; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Domeinresolutie"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Uw adres is opgelost!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Zending mislukt"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Zending Bevestiging"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Geld Verstuurd!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON Serialisatie Error"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Portemonnee niet gereed"; + +/* API Token error message */ +"ApiClient.tokenError" = "Niet in staat op API teken te vinden"; + +/* buy button */ +"Button.buy" = "kopen"; + +/* Cancel button label */ +"Button.cancel" = "Annuleren"; + +/* Ignore button label */ +"Button.ignore" = "Negeren"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "Nee"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "ontvangen"; + +/* resetFields */ +"Button.resetFields" = "Reset velden"; + +/* send button */ +"Button.send" = "versturen"; + +/* Settings button label */ +"Button.settings" = "Instellingen"; + +/* Settings button label */ +"Button.submit" = "Indienen"; + +/* Yes button */ +"Button.yes" = "Ja"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "KOPEN"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Cadeaubonnen kopen \n • Prepaid-telefoons bijvullen \n • Steam, Amazon, Hotels.com \n • Werkt in 170 landen"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Litecoin wijzigen voor andere crypto's \n • Geen ID vereist \n • Kopen via creditcard \n • Wereldwijde dekking"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Koop Litecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Koop LTC met veel fiat-paren\n• Betaal met meerdere methoden\n• Wereldwijde betalingsprovider"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Krijg Litecoin in 5 minuten! \n • Koop Litecoin via creditcard \n • Paspoort of staat ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Koop Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Centreer je ID in het vak"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Uitwisselingsgegevens:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Bedrag om te Versturen:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Te doneren bedrag:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Verwerkingstijd: het verwerken van deze transacties duurt %1$@ minuten."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Verwerkingstijd: Deze transactie zal %1$@ minuten kosten om te verwerken."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Versturen"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "VERGOEDING:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADRES:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Bevestiging"; + +/* To: (address) */ +"Confirmation.to" = "Naar"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Totale Prijs:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "De ingevoerde woorden komen niet overeen met uw papieren sleutel. Probeer het opnieuw."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Voer de volgende woorden in van je papieren sleutel om te verzekeren dat alles correct is opgeschreven."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Woord #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Kopiëren"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin-eenheid"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Kiezen:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Wisselkoers"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Dit apparaat is niet geconfigureerd om e-mails te sturen via de iOS Mail app."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "E-mail Niet Beschikbaar"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Dit apparaat is niet geconfigureerd om berichten te sturen."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Bericht Sturen Onbeschikbaar"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Enonomy"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Geschatte Bezorgingstijd: 10+ minuten"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Deze optie wordt niet aangeraden voor tijdgevoelige transacties."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Luxe"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Verwachte levertijd: 2,5 - 5 minuten"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Deze optie garandeert vrijwel acceptatie van uw transactie, hoewel u een premie betaalt."; + +/* Regular fee */ +"FeeSelector.regular" = "Normaal"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Verwachte levertijd: 2,5 - 5+ minuten"; + +/* Fee Selector title */ +"FeeSelector.title" = "Verwerkingssnelheid"; + +/* Confirm */ +"Fragment.confirm" = "Bevestigen"; + +/* Or */ +"Fragment.or" = "of"; + +/* sorry */ +"Fragment.sorry" = "Sorry"; + +/* to */ +"Fragment.to" = "tot"; + +/* History Bar Item Title */ +"History.barItemTitle" = "GESCHIEDENIS"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Huidige LTC-waarde in"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Persoonlijke sleutel balans checken..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "%1$@ van deze persoonlijke sleutel naar je portemonnee sturen? Het Litecoin netwerk zal een heffing ontvangen van %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Deze persoonlijke sleutel zit al in je portemonnee."; + +/* empty private key error message */ +"Import.Error.empty" = "Deze persoonlijke sleutel is leeg."; + +/* High fees error message */ +"Import.Error.highFees" = "Transactiekosten zouden hoger zijn dan de fondsen die op deze private sleutel aanwezig zijn."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Geen geldige persoonlijke sleutel"; + +/* Import signing error message */ +"Import.Error.signing" = "Error transactie ondertekenen"; + +/* Import button label */ +"Import.importButton" = "Importeren"; + +/* Importing wallet progress view label */ +"Import.importing" = "Portemonnee Importeren"; + +/* Caption for graphics */ +"Import.leftCaption" = "Portemonnee die geïmporteerd kan worden"; + +/* Import wallet intro screen message */ +"Import.message" = "Door een portemonnee te importeren plaats je het geld van je andere portemonnee naar je Litewallet portemonnee via een enkele transactie."; + +/* Enter password alert view title */ +"Import.password" = "Deze persoonlijke sleutel is beveiligd met een wachtwoord."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "wachtwoord"; + +/* Caption for graphics */ +"Import.rightCaption" = "Jouw Litewallet Portemonnee"; + +/* Scan Private key button label */ +"Import.scan" = "Scan Persoonlijke Sleutel"; + +/* Import wallet success alert title */ +"Import.success" = "Succes"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Succesvol portemonnee geïmporteerd."; + +/* Import Wallet screen title */ +"Import.title" = "Importeer Portemonnee"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Sleutel Ontgrendelen"; + +/* Import wallet intro warning message */ +"Import.warning" = "Door een portemonnee te importeren zet je niet de transactie geschiedenis of andere details over."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Verkeerd wachtwoord, probeer alstublieft opnieuw."; + +/* Close app button */ +"JailbreakWarnings.close" = "Sluiten"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Negeren"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "APPARAAT BEVEILIGING BESCHADIGD\nElke 'jailbreak' app heeft toegang tot de sleutelhanger data van Litewallet en kan hiermee je Litecoins stelen! Wis deze portemonnee direct en herstel het op een veilig apparaat."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "APPARAAT BEVEILIGING BESCHADIGD\nElke 'jailbreak' app heeft toegang tot de sleutelhanger data van Litewallet en kan hiermee je Litecoins stelen. Gebruik Litewallet a.u.b. alleen op een niet-gejailbreakt apparaat."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "WAARSCHUWING"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Wissen"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Locatievoorzieningen zijn uitgeschakeld."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet heeft geen toestemming om locatievoorzieningen te gebruiken."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Misvormde URI"; + +/* Balance */ +"ManageWallet.balance" = "Saldo"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Je hebt je portemonnee gecreëerd op %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Jouw portemonnee naam zal alleen verschijnen in je account transactie geschiedenis en kan door niemand anders worden gezien."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Portemonnee Naam"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Portemonnee Beheren"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Litecoins Kopen"; + +/* Menu button title */ +"MenuButton.customer.support" = "Klantenservice"; + +/* Menu button title */ +"MenuButton.lock" = "Portemonnee Vergrendelen"; + +/* Menu button title */ +"MenuButton.security" = "Veiligheidscentrum"; + +/* Menu button title */ +"MenuButton.settings" = "Instellingen"; + +/* Menu button title */ +"MenuButton.support" = "Ondersteuning"; + +/* button label */ +"MenuViewController.createButton" = "Nieuwe Portemonnee Creëren"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Portemonnee Herstellen"; + +/* No comment provided by engineer. */ +"No wallet" = "Geen portemonnee"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Omschakelen naar automatische modus"; + +/* Node is connected label */ +"NodeSelector.connected" = "Verbonden"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Voer het IP-adres en de poort (optioneel) van de node in"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Node invoeren"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Omschakelen naar handmatige modus"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Huidige primaire node"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Niet Verbonden"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Connectiestatus van de node"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin-nodes"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Slecht Betalingsverzoek"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Niet-ondersteund of beschadigd document"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "missend certificaat"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "verzoek verlopen"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Kon de betaling niet uitvoeren"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Litecoin betalingen kunnen niet minder zijn dan %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Litecoin transactie outputs kunnen niet minder zijn dan $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "niet-ondersteund handtekening type"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "niet-vertrouwd certificaat"; + +/* Dismiss button. */ +"Prompts.dismiss" = "VERWERPEN"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Er is een apparaat-pascode nodig om je portomonnee te beschermen. Ga naar de instellingen en zet je pascode aan."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Zet pascode voor apparaat aan"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Doorgaan met"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Uw Papiersleutel moet opgeslagen worden voor het geval dat u uw telefoon ooit verliest of vervangt. Klik hier om door te gaan."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "annuleren"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Inschakelen"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Actie Vereist"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Je portemonnee is misschien niet gesynchroniseerd. Dit kan vaak worden verholpen door de blokketen opnieuw te scannen."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transactie Geweigerd"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet is geüpgrade naar een 6-cijferige PIN. Tik hier om te upgraden."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "PIN Upgraden"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Help ons bij het verbeteren van Litewallet door je anonieme data met ons te delen"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Deel Anonieme Data"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Tik hier om Touch ID in te schakelen"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Touch ID Inschakelen"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "TE ONTVANGEN"; + +/* Address copied message. */ +"Receive.copied" = "Gekopieerd naar plakbord."; + +/* Share via email button label */ +"Receive.emailButton" = "E-mail"; + +/* Request button label */ +"Receive.request" = "Een bedrag aanvragen"; + +/* Share button label */ +"Receive.share" = "Delen"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Bericht"; + +/* Receive modal title */ +"Receive.title" = "Ontvangen"; + +/* Done button text */ +"RecoverWallet.done" = "Voltooid"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Portemonnee Herstellen"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "PIN Resetten"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Papieren Sleutel Invoeren"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Herstel je Litewallet met je papieren sleutel."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "De papieren sleutel die je hebt ingevoerd is ongeldig. Check ieder woord a.u.b. en probeer het opnieuw."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Linker Pijl"; + +/* Next button label */ +"RecoverWallet.next" = "Volgende"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Tik hier voor meer informatie."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Rechter Pijl"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Voer de papieren sleutel in voor de portemonnee die je wilt herstellen."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Om je PIN te resetten moet je de woorden van je papieren sleutel in onderstaande velden invoeren."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Vul alstublieft eerst een bedrag in."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Een bedrag aanvragen"; + +/* Alert action button label */ +"ReScan.alertAction" = "Synchroniseren"; + +/* Alert message body */ +"ReScan.alertMessage" = "Je zult niet in staat zijn om geld te versturen wanneer je aan het synchroniseren bent."; + +/* Alert message title */ +"ReScan.alertTitle" = "Synchroniseren met Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minuten"; + +/* Syncing explanation */ +"ReScan.body2" = "Als een transactie als voltooid staat op het Litecoin netwerk maar niet in je Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Je kreeg herhaaldelijk een foutmelding die zei dat je transactie werd afgewezen."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Start Synchronisatie"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Je zult niet in staat zijn om geld te versturen wanneer je aan het synchroniseren bent met de blokketen."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Synchroniseer Blockchain"; + +/* Subheader label */ +"ReScan.subheader1" = "Geschatte tijd"; + +/* Subheader label */ +"ReScan.subheader2" = "Wanneer Synchroniseren?"; + +/* Reset walet button title */ +"resetButton" = "Ja, portemonnee opnieuw instellen"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Mijn Litewallet verwijderen"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Camera Flits"; + +/* Complete filter label */ +"Search.complete" = "complete"; + +/* Pending filter label */ +"Search.pending" = "in afwachting van"; + +/* Received filter label */ +"Search.received" = "ontvangen"; + +/* Sent filter label */ +"Search.sent" = "verstuurd"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Activeer alle veiligheidsmaatregelen voor maximale bescherming."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "De enige manier om toegang te hebben tot je Litecoins is als je je telefoon verliest of upgrade."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Papieren Sleutel"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Beschermt je Litewallet van ongeautoriseerde gebruikers."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-cijferige PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "Veiligheidscentrum"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Ontgrendel je Litewallet handig en stuur geld tot een bepaalde ingestelde limiet."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Bedrag"; + +/* Balance: $4.00 */ +"Send.balance" = "Balans: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "STUREN"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Ga naar Instellingen om camera gebruik toe te staan."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet heeft geen toestemming voor het gebruik van de camera"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "De bestemming is je eigen adres. Je kunt het niet naar jezelf sturen."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Kon de transactie niet creëren."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Memo"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Plakbord is leeg"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Voer een Litecoin-adres in"; + +/* Network Fee: $0.01 */ +"Send.fee" = "Netwerkkosten: %1$@"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Begunstigde is niet gecertifcieerd."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "onvoldoende middelen"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Het bestemmingsadres is geen geldig Litecoin-adres."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Plakbord bevat geen geldig Litecoin adres."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Ongeldig Adres"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Zenden is uitgeschakeld tijdens volledig opnieuw scannen"; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Verzoek aan het laden"; + +/* Empty address alert message */ +"Send.noAddress" = "Voer a.u.b. het adres van de begunstigde in."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Voer a.u.b. een bedrag in om te versturen."; + +/* Paste button label */ +"Send.pasteLabel" = "Plakken"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Kon de transactie niet publiceren."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Kon de betalingsaanvraag niet laden"; + +/* Scan button label */ +"Send.scanLabel" = "Scannen"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Versturen"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Versturen"; + +/* Send money to label */ +"Send.toLabel" = "Naar"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Opzoeken"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Sorry, het domein is niet gevonden. [Fout: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Opzoeken mislukt"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Voer een .crypto- of .zil-domein in"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Systeem opzoeken probleem. [Fout: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin adressen zijn alleen bedoeld voor enkel gebruik."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Hergebruikt vermindert privacy voor zowel jou en de ontvanger en kan resulteren in verlies als de ontvanger niet direct het adres beheert."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Adres Al Gebruikt"; + +/* About label */ +"Settings.about" = "Over"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Geavanceerde instellingen"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "blockchain"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Toon valuta"; + +/* Current Locale */ +"Settings.currentLocale" = "Huidige landinstelling:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Doe Mee aan Early Access"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Vind je Litewallet leuk?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Importeer Portemonnee"; + +/* Languages label */ +"Settings.languages" = "talen"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Milieu:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet-partners"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet-versie:"; + +/* Manage settings section header */ +"Settings.manage" = "Beheer"; + +/* Notifications label */ +"Settings.notifications" = "Notificaties"; + +/* Leave review button label */ +"Settings.review" = "Laat een review achter"; + +/* Share anonymous data label */ +"Settings.shareData" = "Deel Anonieme Data"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Sociaal"; + +/* Support settings section header */ +"Settings.support" = "Ondersteuning"; + +/* Sync blockchain label */ +"Settings.sync" = "Synchroniseer Blockchain"; + +/* Settings title */ +"Settings.title" = "Instellingen"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch ID Bestedingslimiet"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Portemonnee"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Begin of herstel een andere portomonnee"; + +/* Share data view body */ +"ShareData.body" = "Help om Litewallet te verbeteren door je anonieme data met ons te delen. Dit bevat geen financiële informatie. We respecteren je financiële privacy."; + +/* Share data header */ +"ShareData.header" = "Data Delen?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Anonieme Data Delen?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Huidige bestedingslimiet: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Schrijf Papieren Sleutel Opnieuw Op"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Je papieren sleutel is de enige manier om je Litewallet te herstellen als je telefoon verloren, gestolen, kapot of geüpgrade is.\n\nWe zullen je een lijst met woorden laten zie die je op een papiertje moet schrijven en veilig moet bewaren."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Schrijf Papieren Sleutel Op"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Je hebt voor het laatst je papieren sleutel opgeschreven op %1$@"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Steun de Stichting Litecoin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Verbinden..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Opnieuw scannen ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Succes!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Synchroniseren ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Aan het verbinden"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Aan het synchroniseren"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Je kunt je Touch-ID uitgavenlimiet aanpassen van de %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Gebruik je vingerafdruk om je Litewallet te ontgrendelen en geld tot een ingestelde limiet te versturen."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Touch-ID uitgavenlimietscherm"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Bestedingslimiet: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Schakel Touch ID in voor Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Je hebt je Touch ID niet ingesteld op dit apparaat. Ga naar Instellingen -> Touch ID & Wachtwoord om het nu in te stellen."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID Niet Ingesteld"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Vraag altijd naar wachtwoord"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Je zal gevraagd worden om je 6-cijferige PIN in te voeren om elke transactie boven je bestedingslimiet te versturen, en elke 48 uur sinds de laatste keer dat je je 6-cijferige PIN hebt ingevoerd."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch ID Bestedingslimiet"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Uitwisselingsgegevens: "; + +/* Availability status text */ +"Transaction.available" = "Beschikbaar om te besteden"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Blok:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo: "; + +/* Transaction complete label */ +"Transaction.complete" = "Compleet"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Detail eindbedrag transactie"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Eindbalans: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Wisselkoers bij ontvangst:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Wisselkoers bij verzending:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ fee)"; + +/* Invalid transaction */ +"Transaction.invalid" = "ONGELDIG"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "zojuist"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "In uitvoering: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "In uitvoering: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Startbalans: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Transactie startbedrag detail"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Wachtend op bevestiging. Sommige handelaren willen bevestiging om een transactie af te roden. Geschatte tijd: 1-2 uur."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "account"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Bedrag"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Bevestigd in blok"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Memo"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Gekopieerd"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Kopieer alle details"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Je transacties zullen hier verschijnen."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "op %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Minder"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "%1$@ verplaatst"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "%1@ verplaatst"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Niet bevestigd"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "vanaf"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "Ontvangen %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Ontvangen %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "ONTVANG ADRES"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "Verstuurd %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Verstuurd %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Status"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Transactie Details"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "aan %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin Transactie-ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Ontvangen op dit adres"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Verstuurd naar dit adres"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Uitgeschakeld tot: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Voer Pin-code in"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Ontgrendel met FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Mijn Adres"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "PIN Resetten"; + +/* Scan button title */ +"UnlockScreen.scan" = "Scannen"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Ontgrendel je Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Ontgrendel met Touch ID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Portemonnee Ontgrendeld"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Onthoud deze PIN. Als je deze vergeet, zul je niet in staat zijn om bij je Litecoins te kunnen."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Je PIN zal worden gebruikt om je Litewallet te ontgrendelen en geld te versturen."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "PIN Instellen"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Voer PIN opnieuw in"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Voer je huidige PIN in."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Voer je nieuwe PIN in."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Voer je nieuwe PIN opnieuw in."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Sorry, we konden je PIN niet updaten."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Update PIN Error"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Update PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Portemonnee adressen naar plakbord kopiëren?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Autoriseer om portemonnee adres naar plakbord te kopiëren"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Kopieer Portemonnee Adressen"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Kopiëren"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Voer alstublieft uw PIN in om deze transactie te autoriseren."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Voer a.u.b. je PIN in om verder te gaan."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN Vereist"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Autoriseer deze transactie"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Open de Litewallet iPhone app om je portemonnee in te stellen."; + +/* Dismiss button label */ +"Webview.dismiss" = "Verwerpen"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Er was een error bij het laden van de inhoud. Probeer a.u.b. opnieuw."; + +/* Updating webview message */ +"Webview.updating" = "Updaten..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Welkom bij Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Welkom bij Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Database verwijderen"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Weet je zeker dat je deze portomonnee wilt verwijderen?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Portomonnee wissen?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Verwijder database"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Hiermee wordt de database verwijderd, maar worden de pincode en zinsdeel behouden. Bevestig uw bestaande pincode, start en wacht om te synchroniseren met de nieuwe DB"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Verwijderen en synchroniseren"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Vergeet uw startzin of pincode?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Wissen van portomonnee niet gelukt"; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Mislukt"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Voer de herstelzin van deze portomonnee in om hem te wissen of een anderen te herstellen. Je huidige saldo zal op deze zin blijven."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Een andere portemonnee beginnen of herstellen zal u toestaan om nog een Litewallet portemonnee op dit apparaat te gebruiken."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "U zal niet langer uw huidige Litewallet portemonnee kunnen bereiken vanaf dit apparaat. Uw saldo zal hetzelfde blijven."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Start of herstel een anderen portomonnee"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Deze actie zal je Litewallet wissen!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Als u uw portemonnee verwijdert, zijn de privésleutel en het wissen van de app-gegevens verdwenen. U kunt Litecoin voor altijd verliezen!\n\n\nNiemand in het Litewallet-team kan deze seed voor je ophalen. Wij zijn niet verantwoordelijk als u deze waarschuwing niet in acht neemt."; + +/* Warning title */ +"WipeWallet.warningTitle" = "GELIEVE TE LEZEN!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Wissen"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Aan het wissen..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Schrijf elk woord in de juiste volgorde op en bewaar het op een veilige plek."; + +/* button label */ +"WritePaperPhrase.next" = "Volgende"; + +/* button label */ +"WritePaperPhrase.previous" = "Vorige"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d van %2$d"; + +/* "Email address label" */ +"Notifications.emailLabel" = ""; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = ""; + +/* "Email title" */ +"Notifications.emailTitle" = ""; + +/* "Language preference label" */ +"Notifications.languagePreference" = ""; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = ""; + +/* Signup cancel */ +"Notifications.signupCancel" = ""; + +/* Fee: $0.01 */ +"Send.bareFee" = ""; + +/* Fees Blank: */ +"Send.feeBlank" = ""; + +/* Network */ +"Send.networkFee" = ""; + +/* Service */ +"Send.serviceFee" = ""; + +/* domain */ +"Send.UnstoppableDomains.domain" = ""; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = ""; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = ""; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = ""; + +/* Start view tagline */ +"StartViewController.tagline" = ""; diff --git a/litewallet/Strings/pt.lproj/Localizable.strings b/litewallet/Strings/pt.lproj/Localizable.strings new file mode 100755 index 000000000..ffb427769 --- /dev/null +++ b/litewallet/Strings/pt.lproj/Localizable.strings @@ -0,0 +1,1293 @@ +/* About screen blog label */ +"About.blog" = "Blog"; + +/* About screen footer */ +"About.footer" = "Criada pela equipa Litewallet global. Versão %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Política de Privacidade"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Sobre"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Fechar"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Centro de Ajuda"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "A Carregar Carteira"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Minha Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "GERIR"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Excluir banco de dados +"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Isso exclui o banco de dados, mas mantém o PIN e a frase. Confirme seu PIN existente, propague e aguarde para compelir a sincronização com o novo banco de dados"; + +/* Error alert title */ +"Alert.error" = "Erro"; + +/* No internet alert message */ +"Alert.noInternet" = "Não foi encontrada uma ligação à internet. Verifique a sua ligação e tente novamente."; + +/* Warning alert title */ +"Alert.warning" = "Aviso"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Endereços Copiados"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Todos os endereços de carteiras foram copiados com sucesso."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Chave de Papel Definida"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Incrível!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN Definido"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Resolução de Domínio"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Seu endereço foi resolvido!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Falha no envio"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Confirmação de Envio"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Dinheiro Enviado!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "Erro de Serialização JSON"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Carteira não preparada"; + +/* API Token error message */ +"ApiClient.tokenError" = "Não foi possível obter o token API"; + +/* buy button */ +"Button.buy" = "COMPRAR"; + +/* Cancel button label */ +"Button.cancel" = "Cancelar"; + +/* Ignore button label */ +"Button.ignore" = "Ignorar"; + +/* menu button */ +"Button.menu" = "menu"; + +/* No button */ +"Button.no" = "Não"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "receber"; + +/* resetFields */ +"Button.resetFields" = "Limpar campos"; + +/* send button */ +"Button.send" = "enviar"; + +/* Settings button label */ +"Button.settings" = "Definições"; + +/* Settings button label */ +"Button.submit" = "Submeter"; + +/* Yes button */ +"Button.yes" = "Sim"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "BUY"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Comprar cartões-presente \n • Reabastecer telefones pré-pagos \n • Steam, Amazon, Hotels.com \n • Funciona em 170 países"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Troque Litecoin por outras criptas \n • Não é necessário ID \n • Compre via cartão de crédito \n • Cobertura global"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Comprar Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Compre LTC com muitos pares fiat\n• Pague com vários métodos\n• Provedor de pagamento global"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Obtenha Litecoin em 5 minutos! \n • Compre Litecoin via cartão de crédito \n • Passaporte ou ID do estado"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Comprar Łitecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Centre a sua ID na caixa"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Exchange details:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Montante a enviar:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Valor para Doar:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Tempo de processamento: essas transações levarão %1$@ minutos para serem processadas."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Tempo de processamento: esta transação vai levar %1$@ minutos a ser processada."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Enviar"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "FEE:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADDRESS:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Confirmação"; + +/* To: (address) */ +"Confirmation.to" = "Para"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Custo total"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "As palavras inseridas não correspondem à sua chave de papel. Por favor, tente novamente."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Para ter certeza que tudo foi escrito corretamente, por favor introduza as seguintes palavras da sua chave de papel."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Palavra #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Copy"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Moeda de apresentação da Litecoin"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Escolher:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Taxa de Câmbio"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Este dispositivo não está configurado para enviar emails com a app Mail do iOS."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "Email Indisponível"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Este dispositivo não está configurado para enviar mensagens."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Mensagens Indisponíveis"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Economia"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Estimativa de entrega: mais de 10 minutos"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Esta opção não se recomenda para transações delicadas a nível de tempo."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Luxo"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Entrega estimada: 2,5 - 5 minutos"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Essa opção praticamente garante a aceitação de sua transação, embora você esteja pagando um prêmio."; + +/* Regular fee */ +"FeeSelector.regular" = "Normal."; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Entrega estimada: 2,5 - 5+ minutos"; + +/* Fee Selector title */ +"FeeSelector.title" = "Velocidade de processamento"; + +/* Confirm */ +"Fragment.confirm" = "confirme"; + +/* Or */ +"Fragment.or" = "ou"; + +/* sorry */ +"Fragment.sorry" = "Desculpa"; + +/* to */ +"Fragment.to" = "para"; + +/* History Bar Item Title */ +"History.barItemTitle" = "HISTORY"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Valor LTC atual em"; + +/* Checking private key balance progress view text */ +"Import.checking" = "A verificar o saldo da chave privada..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Enviar %1$@ desta chave privada para a sua carteira? A rede Litecoin receberá uma taxa de %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Esta chave privada já está na sua carteira."; + +/* empty private key error message */ +"Import.Error.empty" = "Esta chave privada está vazia."; + +/* High fees error message */ +"Import.Error.highFees" = "Os custos de transação seriam superiores aos fundos disponíveis nesta chave privada."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Não é uma chave privada válida"; + +/* Import signing error message */ +"Import.Error.signing" = "Erro a assinar transação"; + +/* Import button label */ +"Import.importButton" = "Importar"; + +/* Importing wallet progress view label */ +"Import.importing" = "A Importar Carteira"; + +/* Caption for graphics */ +"Import.leftCaption" = "Carteira a ser importada"; + +/* Import wallet intro screen message */ +"Import.message" = "Importar uma carteira transfere todo o dinheiro da sua outra carteira para a carteira Litewallet usando uma única transação."; + +/* Enter password alert view title */ +"Import.password" = "Esta chave privada está protegida com uma palavra-passe."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "palavra-passe"; + +/* Caption for graphics */ +"Import.rightCaption" = "A Sua Carteira Litewallet"; + +/* Scan Private key button label */ +"Import.scan" = "Digitalizar Chave Privada"; + +/* Import wallet success alert title */ +"Import.success" = "Sucesso"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Carteira importada com sucesso."; + +/* Import Wallet screen title */ +"Import.title" = "Importar Carteira"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "A Desbloquear Chave"; + +/* Import wallet intro warning message */ +"Import.warning" = "Importar uma carteira não inclui o histórico de transações ou outros detalhes."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Palavra-passe errada. Por favor, tente de novo."; + +/* Close app button */ +"JailbreakWarnings.close" = "Fechar"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignorar"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "SEGURANÇA DO DISPOSITIVO COMPROMETIDA\n Qualquer aplicação com \"jailbreak\" consegue aceder aos seus dados de porta-chaves e roubar as suas Litecoins! Apague esta carteira imediatamente e restaure-a num dispositivo seguro."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "SEGURANÇA DO DISPOSITIVO COMPROMETIDA\n Qualquer aplicação com \"jailbreak\" consegue aceder aos seus dados de porta-chaves da Litewallet e roubar as suas Litecoins. Por favor, use a Litewallet somente num dispositivo não desbloqueado."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "AVISO"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Apagar"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Os serviços de localização estão desativados."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "A Litewallet não tem permissão para aceder aos serviços de localização."; + +/* No comment provided by engineer. */ +"Malformed URI" = "URI malformado"; + +/* Balance */ +"ManageWallet.balance" = "Saldo"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Criou a sua carteira em %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "O nome da sua carteira só aparece no histórico de transações da sua conta e não pode ser visto por mais ninguém."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Nome da Carteira"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Gerir Carteira"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Comprar Litecoins"; + +/* Menu button title */ +"MenuButton.customer.support" = "Suporte ao cliente"; + +/* Menu button title */ +"MenuButton.lock" = "Bloquear Carteira"; + +/* Menu button title */ +"MenuButton.security" = "Centro de Segurança"; + +/* Menu button title */ +"MenuButton.settings" = "Definições"; + +/* Menu button title */ +"MenuButton.support" = "Ajuda"; + +/* button label */ +"MenuViewController.createButton" = "Criar Nova Carteira"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menu"; + +/* button label */ +"MenuViewController.recoverButton" = "Recuperar Carteira"; + +/* No comment provided by engineer. */ +"No wallet" = "Sem carteira"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Mudar para o modo automático"; + +/* Node is connected label */ +"NodeSelector.connected" = "Ligação estabelecida"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Introduza o endereço de IP e porta do nó (opcional)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Introduza o nó"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Trocar para modo manual"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Nó primário atual"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Ligação não estabelecida"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Estado da ligação do nó"; + +/* Node Selector view title */ +"NodeSelector.title" = "Nós de Litecoin"; + +/* "Email address label" */ +"Notifications.emailLabel" = "Endereço de email"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Entre aqui"; + +/* "Email title" */ +"Notifications.emailTitle" = "Não perca nada!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Língua preferida:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Inscreva -se para ouvir sobre atualizações e concursos."; + +/* Signup cancel */ +"Notifications.signupCancel" = "Não, obrigado."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Pedido de Pagamento Inválido"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Documento não suportado ou corrompido"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "certificado em falta"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "o pedido expirou"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Não foi possível fazer o pagamento"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Os pagamentos em Litecoin não podem ser inferiores a %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "As saídas de transações em Litecoin não podem ser inferiores a $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "tipo de assinatura não suportado"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "certificado não fidedigno"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DISMISS"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Para proteger a sua carteira, é necessário ter um código de acesso ao dispositivo. Vá às definições e ative o código de acesso."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Ativar o código de acesso ao dispositivo"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Continue"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "O seu Paper Key deve ser guardado para o caso de perder ou alterar o seu telemóvel. Clique aqui para continuar."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Cancel"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Enable"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Ação Necessária"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "A sua carteira pode estar dessincronizada. Isto pode ser frequentemente resolvido ao voltar a digitalizar a blockchain."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transação Rejeitada"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "A Litewallet foi atualizada para usar um PIN de 6-dígitos. Toque aqui para atualizar."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Atualizar PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Ajude a melhorar a Litewallet, partilhando connosco os seus dados anónimos"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Partilhar dados anónimos"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Toque aqui para ativar o Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Ativar Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "RECEIVE"; + +/* Address copied message. */ +"Receive.copied" = "Copiado para a área de transferência."; + +/* Share via email button label */ +"Receive.emailButton" = "Email"; + +/* Request button label */ +"Receive.request" = "Pedir uma Quantia"; + +/* Share button label */ +"Receive.share" = "Partilhar"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Mensagem de Texto"; + +/* Receive modal title */ +"Receive.title" = "Receber"; + +/* Done button text */ +"RecoverWallet.done" = "Feito"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Recuperar Carteira"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Redefinir PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Introduzir Chave de Papel"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Recupere a sua Litewallet com a sua chave de papel."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "A chave de papel que introduziu é inválida. Por favor verifique cada palavra e tente novamente."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Seta para a Esquerda"; + +/* Next button label */ +"RecoverWallet.next" = "Seguinte"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Toque aqui para mais informações."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Seta para a Direita"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Introduza a chave de papel para a carteira que quer recuperar."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Para redefinir o seu PIN, introduza as palavras da sua chave de papel nas caixas abaixo."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Primeiro, por favor, introduza um montante."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Pedir uma Quantia"; + +/* Alert action button label */ +"ReScan.alertAction" = "Sincronizar"; + +/* Alert message body */ +"ReScan.alertMessage" = "Não será capaz de enviar dinheiro enquanto estiver a sincronizar."; + +/* Alert message title */ +"ReScan.alertTitle" = "Sincronizar com a Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minutos"; + +/* Syncing explanation */ +"ReScan.body2" = "Se uma transação aparece como concluída na sua rede Litecoin mas não na sua Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Recebe repetidamente uma mensagem de erro dizendo que a sua transação foi rejeitada."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Começar Sincronização"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Não será capaz de enviar dinheiro enquanto estiver a sincronizar com a blockchain."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Sincronizar Blockchain"; + +/* Subheader label */ +"ReScan.subheader1" = "Tempo previsto"; + +/* Subheader label */ +"ReScan.subheader2" = "Quando sincronizar?"; + +/* Reset walet button title */ +"resetButton" = "Sim, redefinir carteira"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Excluir minha carteira Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Flash da Câmara"; + +/* Complete filter label */ +"Search.complete" = "concluído"; + +/* Pending filter label */ +"Search.pending" = "pendente"; + +/* Received filter label */ +"Search.received" = "recebido"; + +/* Sent filter label */ +"Search.sent" = "enviado"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Ativar todos os recursos de segurança para máxima proteção."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "A única forma de aceder às suas Litecoins se perder ou atualizar o seu telefone."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Chave de Papel"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Protege a sua Litewallet de utilizadores não autorizados."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "PIN de 6-Dígitos"; + +/* Security Center Title */ +"SecurityCenter.title" = "Centro de Segurança"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Desbloqueie convenientemente a sua Litewallet e envie dinheiro até um limite definido"; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Quantia"; + +/* Balance: $4.00 */ +"Send.balance" = "Saldo: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Taxa: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "SEND"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Vá às Definições para permitir acesso à câmara."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "A Litewallet não tem autorização para aceder à câmara"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "O destino é o seu próprio endereço. Não pode enviar a si mesmo."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Não foi possível criar a transação."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Nota"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "A área de transferência está vazia"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Introduzir um endereço Litecoin"; + +/* Fees: $0.01*/ +"Send.fee" = "Tarifas: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Tarifas:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "A identidade do beneficiário não está certificada."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "fundos insuficientes"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "O endereço de destino não é um endereço de Litecoin válido."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "A área de transferência não possui nenhum endereço de Litecoin válido."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Endereço Inválido"; + +/* Is rescanning error message */ +"Send.isRescanning" = "O envio encontra-se desativado durante a reanálise completa."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "A carregar o pedido"; + +/* Network */ +"Send.networkFee" = "Rede"; + +/* Empty address alert message */ +"Send.noAddress" = "Por favor, introduza o endereço do destinatário."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Por favor, introduza a quantia a enviar."; + +/* Paste button label */ +"Send.pasteLabel" = "Colar"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Não foi possível publicar a transação."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Não foi possível carregar a solicitação de pagamento"; + +/* Scan button label */ +"Send.scanLabel" = "Digitalizar"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Enviar"; + +/* Service */ +"Send.serviceFee" = "Serviço"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Enviar"; + +/* Send money to label */ +"Send.toLabel" = "Para"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "domínio"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Introduzir um"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Pesquisa"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Desculpe, o domínio não foi encontrado. [Erro: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Pesquisa falhou"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Insira um domínio .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao ou .x."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Digite o domínio"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Problema de pesquisa do sistema. [Erro: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Os endereços de Litecoin destinam-se apenas a uma única utilização."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "A reutilização reduz a sua privacidade e a do destinatário e pode resultar em perdas se o destinatário não controlar diretamente o endereço."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Endereço Já Utilizado"; + +/* About label */ +"Settings.about" = "Sobre"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Definições avançadas"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Cadeia bloco"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Tem certeza de que deseja alterar o idioma para %l?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Mostrar moeda"; + +/* Current Locale */ +"Settings.currentLocale" = "Localidade atual:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Registe-se no Acesso Antecipado"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Está a gostar da Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Importar Carteira"; + +/* Languages label */ +"Settings.languages" = "línguas"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Meio Ambiente:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Parceiros Litewallet"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Versão Litewallet:"; + +/* Manage settings section header */ +"Settings.manage" = "Gerir"; + +/* Notifications label */ +"Settings.notifications" = "Notificações"; + +/* Leave review button label */ +"Settings.review" = "Deixe-nos uma avaliação"; + +/* Share anonymous data label */ +"Settings.shareData" = "Partilhar Dados Anónimos"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Support settings section header */ +"Settings.support" = "Apoio, suporte"; + +/* Sync blockchain label */ +"Settings.sync" = "Sincronizar Blockchain"; + +/* Settings title */ +"Settings.title" = "Definições"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Limite de Gastos Touch ID"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Carteira"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Iniciar/recuperar outra carteira"; + +/* Share data view body */ +"ShareData.body" = "Ajude a melhorar a Litewallet partilhando os seus dados anónimos connosco. Isto não inclui quaisquer informações financeiras. Respeitamos a sua privacidade financeira."; + +/* Share data header */ +"ShareData.header" = "Partilhar Dados?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Partilhar Dados Anónimos?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Limite de gastos atual:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Escrever Novamente Chave de Papel"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "A chave de papel é a única forma de restaurar a sua Litewallet se o seu telefone for perdido, roubado, partido ou atualizado.\n\nVamos mostrar-lhe uma lista de palavras para escrever numa folha de papel e manter em segurança."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Escrever Chave de Papel"; + +/* Argument is date */ +"StartPaperPhrase.date" = "A última vez que escreveu a sua chave de papel foi em %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "A maneira mais segura e fácil de usar Litecoin."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Apoiar a Fundação Litecoin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Connecting..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Rescanning..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Success!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Syncing..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Conectando"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Sincronizando"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Pode ajustar o limite dos gastos quando usa o Touch ID, de acordo com a sua preferência, a partir de %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Utilize a impressão digital para desbloquear a sua Litewallet e enviar dinheiro até um limite definido."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Ecrã de limite dos gastos do Touch ID"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Limite de Gastos: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Ativar Touch ID para a Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Não definiu o Touch ID neste dispositivo. Vá a Definições->Touch ID e Código para o configurar agora."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID Não Configurado"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Pedir sempre um código de acesso"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Ser-lhe-á pedido para introduzir o seu PIN de 6-dígitos para enviar qualquer transação acima do seu limite de gastos, e a cada 48 horas após a última utilização do seu PIN de 6-dígitos."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Limite de Gastos Touch ID"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Amount Detail:"; + +/* Availability status text */ +"Transaction.available" = "Disponível para Gastar"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Block:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Memo:"; + +/* Transaction complete label */ +"Transaction.complete" = "Concluída"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Transaction end amount detail"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Saldo final: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Taxa de câmbio ao receber:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Taxa de câmbio ao enviar:"; + +/* (b600 fee) */ +"Transaction.fee" = "(taxa de %1$@)"; + +/* Invalid transaction */ +"Transaction.invalid" = "INVÁLIDA"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "mesmo agora"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "Em progresso: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "Em progresso: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Saldo inicial: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Detalhe do valor inicial da transação"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "À espera de confirmação. Alguns comerciantes necessitam de confirmação para concluir uma transação. Tempo estimado: 1-2 horas."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "conta"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Quantia"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Confirmado no bloco"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Nota"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Copiado"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Copie todos os detalhes"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "As suas transações aparecerão aqui."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "em %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Menos"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "Movimentou %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Movido %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Não confirmado"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "a partir de"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "Recebeu %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Recebido %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "RECEBER ENDEREÇO"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "Enviou %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Enviado %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Estado"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Detalhes da Transação"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "para %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "ID da Transação de Litecoin"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Recebida neste endereço"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Enviada para este endereço"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Desativado até: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Entrar no pino"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Desbloqueie com o FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Meu Endereço"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Redefinir PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "Digitalizar"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Desbloqueie a sua Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Desbloquear com Touch ID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Carteira Desbloqueada"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Lembre-se deste PIN. Se o esquecer, não será capaz de aceder às suas Litecoins."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "O seu PIN será usado para desbloquear a sua Litewallet e para enviar dinheiro."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Definir PIN"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Reintroduzir PIN"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Introduza o seu PIN atual."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Introduza o seu novo PIN."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Reintroduza o seu novo PIN."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Desculpe, não foi possível atualizar o PIN"; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Erro a Atualizar PIN"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Atualizar PIN"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Copiar endereços da carteira para a área de transferência?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Autorize para copiar o endereço da carteira para a área de transferência"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Copiar Endereços da Carteira"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Copiar"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Por favor, introduza o seu PIN, a fim de autorizar esta transação."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Por favor, introduza o seu PIN para continuar."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN Obrigatório"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Autorizar esta transação"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Abra a aplicação Litewallet para iPhone para configurar a sua carteira."; + +/* Dismiss button label */ +"Webview.dismiss" = "Ignorar"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Ocorreu um erro a carregar o conteúdo. Por favor, tente novamente."; + +/* Updating webview message */ +"Webview.updating" = "A Atualizar..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Bem-vindo ao Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Bem-vindo ao Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Erro de corrupção local"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Tem a certeza de que quer eliminar esta carteira?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Limpar carteira?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Excluir banco de dados"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Seu banco de dados local está corrompido. Vá para Configurações> Blockchain: Configurações> Excluir banco de dados para atualizar"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Excluir e sincronizar"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Esqueceu a semente ou o PIN?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Falha ao limpar a carteira."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Falhou"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Introduza a frase de recuperação desta carteira, a fim de limpar a mesma e iniciar ou recuperar outra. O seu saldo atual permanece nesta frase."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Começar ou recuperar outra carteira permite-lhe entrar e administrar uma carteira diferente da Litewallet neste dispositivo."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Não poderá mais aceder à sua Litewallet a partir deste dispositivo. O saldo permanecerá na frase."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Iniciar ou recuperar outra carteira"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Esta ação irá limpar sua carteira Lite!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Excluir sua carteira significa que a chave privada e a limpeza dos dados do aplicativo desaparecerão. Você pode perder Litecoin para sempre!\n\n\nNinguém da equipe Litewallet pode recuperar essa semente para você. Não nos responsabilizamos se você não atender a este aviso."; + +/* Warning title */ +"WipeWallet.warningTitle" = "POR FAVOR LEIA!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Limpar"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "A limpar..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Escreva cada palavra por ordem e guarde-as num lugar seguro."; + +/* button label */ +"WritePaperPhrase.next" = "Seguinte"; + +/* button label */ +"WritePaperPhrase.previous" = "Anterior"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d de %2$d"; diff --git a/litewallet/Strings/ru.lproj/Localizable.strings b/litewallet/Strings/ru.lproj/Localizable.strings new file mode 100755 index 000000000..4b8192767 --- /dev/null +++ b/litewallet/Strings/ru.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "Блог"; + +/* About screen footer */ +"About.footer" = "Создано международной командой Litewallet. Версия %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Политика конфиденциальности"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "О нас"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Закрыть"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Справочный центр"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Загружаем кошелек"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Мой Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "УПРАВЛЕНИЕ"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Ошибка локальной коррупции"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Ваша локальная база данных повреждена. Перейдите в Настройки> Блокчейн: Настройки> Удалить базу данных, чтобы обновить"; + +/* Error alert title */ +"Alert.error" = "Ошибка"; + +/* No internet alert message */ +"Alert.noInternet" = "Интернет-подключение не обнаружено. Проверьте подключение и повторите попытку."; + +/* Warning alert title */ +"Alert.warning" = "Предупреждение"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Адреса скопированы"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Все адреса для кошелька успешно скопированы."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Бумажный ключ задан"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Отлично!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN-код задан"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Разрешение домена"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Ваш адрес решен!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Ошибка отправки"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Подтверждение отправки"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Деньги отправлены!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "Ошибка JSON-сериализации"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Кошелек не готов"; + +/* API Token error message */ +"ApiClient.tokenError" = "Не удалось извлечь токен API"; + +/* buy button */ +"Button.buy" = "купить"; + +/* Cancel button label */ +"Button.cancel" = "Отменить"; + +/* Ignore button label */ +"Button.ignore" = "Игнорировать"; + +/* menu button */ +"Button.menu" = "меню"; + +/* No button */ +"Button.no" = "Нет"; + +/* OK button label */ +"Button.ok" = "ОК"; + +/* receive button */ +"Button.receive" = "получить"; + +/* resetFields */ +"Button.resetFields" = "Очистить поля"; + +/* send button */ +"Button.send" = "отправить"; + +/* Settings button label */ +"Button.settings" = "Настройки"; + +/* Settings button label */ +"Button.submit" = "Отправить"; + +/* Yes button */ +"Button.yes" = "Да"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "КУПИТЬ"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Купить подарочные карты \n • Пополнить предоплатные телефоны \n • Steam, Amazon, Hotels.com \n • Работает в 170 странах"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Поменять Litecoin на другие криптографии \n • Идентификатор не требуется \n • Купить с помощью кредитной карты \n • Глобальное покрытие"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Купить Litecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Покупайте LTC с помощью множества фиатных пар\n• Оплачивайте несколькими способами\n• Глобальный платежный провайдер"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Получить Litecoin за 5 минут! \n • Купить Litecoin с помощью кредитной карты \n • Паспорт или удостоверение личности штата"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Купить Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Поместите ваш ID-номер в центр данного поля"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Детали обмена:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Сумма для отправки:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Сумма пожертвования:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Время обработки: обработка этих транзакций займет% 1 $ @ минут."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Время обработки: для обработки этой транзакции потребуется %1$@ минут."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Отправить"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "СБОР:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "АДРЕС:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Подтверждение"; + +/* To: (address) */ +"Confirmation.to" = "На"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Общая стоимость:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Введенные слова не соответствуют вашему бумажному ключу. Пожалуйста, попробуйте еще раз."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Чтобы убедиться, что всё записано корректно, пожалуйста, введите следующие слова с вашего бумажного ключа."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Слово #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "копия"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Единица отображения биткойна"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "выбирать:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Обменный курс"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Для этого устройства не настроена отправка электронных писем через почтовое приложение iOS."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "Электронное письмо недоступно"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Для этого устройства отправка сообщений не настроена."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Опция обмена сообщениями недоступна"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Экономия"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Ориентировочная доставка: 10+ минут"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Этот параметр не рекомендуется для транзакций, чувствительных с точки зрения времени."; + +/* Luxury fee */ +"FeeSelector.luxury" = "люкс"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Ориентировочная доставка: 2,5 - 5 минут"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Эта опция фактически гарантирует принятие вашей транзакции, хотя вы платите премию."; + +/* Regular fee */ +"FeeSelector.regular" = "Обычна сумма"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Ориентировочная доставка: 2,5 - 5+ минут"; + +/* Fee Selector title */ +"FeeSelector.title" = "Скорость обработки"; + +/* Confirm */ +"Fragment.confirm" = "Подтверждать"; + +/* Or */ +"Fragment.or" = "или"; + +/* sorry */ +"Fragment.sorry" = "сожалею"; + +/* to */ +"Fragment.to" = "к"; + +/* History Bar Item Title */ +"History.barItemTitle" = "ИСТОРИЯ"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Текущее значение LTC в"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Проверяем баланс по персональному ключу..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Отправить %1$@ с этого персонального ключа на ваш кошелек? Биткойн-сети будет начислена плата в размере %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Этот персональный ключ уже находится в вашем кошельке."; + +/* empty private key error message */ +"Import.Error.empty" = "Этот персональный ключ пуст."; + +/* High fees error message */ +"Import.Error.highFees" = "Комиссия транзакции стоит больше, чем доступно средств на счете"; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Недействительный персональный ключ"; + +/* Import signing error message */ +"Import.Error.signing" = "Ошибка подписания транзакции"; + +/* Import button label */ +"Import.importButton" = "Импортировать"; + +/* Importing wallet progress view label */ +"Import.importing" = "Импортируем кошелек"; + +/* Caption for graphics */ +"Import.leftCaption" = "Кошелек, который будет импортирован"; + +/* Import wallet intro screen message */ +"Import.message" = "При импортировании кошелька все ваши деньги из другого кошелька будут перенесены в ваш кошелек Litewallet с помощью одной транзакции."; + +/* Enter password alert view title */ +"Import.password" = "Этот персональный ключ защищен паролем."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "пароль"; + +/* Caption for graphics */ +"Import.rightCaption" = "Ваш кошелек Litewallet"; + +/* Scan Private key button label */ +"Import.scan" = "Сканировать персональный ключ"; + +/* Import wallet success alert title */ +"Import.success" = "Готово"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Успешно импортированный кошелек."; + +/* Import Wallet screen title */ +"Import.title" = "Импортировать кошелек"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Ключ разблокировки"; + +/* Import wallet intro warning message */ +"Import.warning" = "При импортировании кошелька история транзакций и другая информация не переносятся."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Неправильный пароль, повторите попытку."; + +/* Close app button */ +"JailbreakWarnings.close" = "Закрыть"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Игнорировать"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "БЕЗОПАСНОСТЬ УСТРОЙСТВА ПОДВЕРГНУТА РИСКУ\n Какое-либо приложение по снятию ограничений с файловой системы может получить доступ к данным цепочки ключей Litewallet и украсть ваши биткойны! Немедленно очистите ваш кошелек и восстановите его на безопасном устройстве."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "БЕЗОПАСНОСТЬ УСТРОЙСТВА ПОДВЕРГНУТА РИСКУ\n Какое-либо приложение по снятию ограничений с файловой системы может получить доступ к данным цепочки ключей Litewallet и украсть ваши биткойны. Пожалуйста, используйте Litewallet только на устройствах с не снятыми с файловой системы ограничениями."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "ПРЕДУПРЕЖДЕНИЕ"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Очистить"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Сервисы определения местоположения отключены."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "У Litewallet не разрешения на доступ к сервисам определения местоположения."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Неверно сформированный URI"; + +/* Balance */ +"ManageWallet.balance" = "Баланс"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Вы создали ваш кошелек %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Название вашего кошелька отображается только в журнале транзакций вашей учетной записи, и ни один другой пользователь не может его видеть."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Название кошелька"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Управление кошельком"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Купить Litecoin"; + +/* Menu button title */ +"MenuButton.customer.support" = "Служба поддержки"; + +/* Menu button title */ +"MenuButton.lock" = "Заблокировать кошелек"; + +/* Menu button title */ +"MenuButton.security" = "Центр безопасности"; + +/* Menu button title */ +"MenuButton.settings" = "Настройки"; + +/* Menu button title */ +"MenuButton.support" = "Поддержка"; + +/* button label */ +"MenuViewController.createButton" = "Создать новый кошелек"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Меню"; + +/* button label */ +"MenuViewController.recoverButton" = "Восстановить кошелек"; + +/* No comment provided by engineer. */ +"No wallet" = "Нет кошелька"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Переключиться в автоматический режим"; + +/* Node is connected label */ +"NodeSelector.connected" = "Подключено"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Ввести IP-адрес и порт узла (необязательно)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Ввести узел"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Переключиться в ручной режим"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Текущий первичный узел"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Не подключено"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Состояние подключения узла"; + +/* Node Selector view title */ +"NodeSelector.title" = "Узлы Лайткоин"; + +/* "Email address label" */ +"Notifications.emailLabel" = "Адрес электронной почты"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Вход здесь"; + +/* "Email title" */ +"Notifications.emailTitle" = "Не пропустите ничего!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Предпочтительный язык:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Зарегистрируйтесь, чтобы услышать об обновлениях и конкурсах."; + +/* Signup cancel */ +"Notifications.signupCancel" = "Нет, спасибо."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Неверный запрос на совершение платежа"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Неподдерживаемый или поврежденный документ"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "недостающий сертификат"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "срок действия запроса истек"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Не удалось выполнить платеж"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Размер биткоин-платежей должен превышать %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Результаты биткойн-транзакции должны превышать $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "неподдерживаемый тип подписи"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "недостоверный сертификат"; + +/* Dismiss button. */ +"Prompts.dismiss" = "ЗАКРЫТЬ"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Для защиты вашего кошелька необходим код доступа к устройству. Перейдите к настройкам и включите код доступа."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Включить код доступа к устройству"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Продолжить"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Ваш бумажный ключ нужно сохранить на случай, если вы потеряете или смените свой телефон. Нажмите сюда, чтобы продолжить."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Отмена"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "включить"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Необходимо совершить действие"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Ваш кошелек, возможно, дал сбой. Зачастую это можно исправить, повторно просканировав цепочку ключей."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Транзакция отклонена"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet обновил свой интерфейс до использования 6-цифрового PIN-кода. Коснитесь здесь для обновления."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Обновить PIN-код"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Помогите улучшить Litewallet, поделившись с нами своими анонимными данными"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Поделиться анонимными данными"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Коснитесь здесь для активизации функции Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Активировать функцию Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "ПОЛУЧАТЬ"; + +/* Address copied message. */ +"Receive.copied" = "Скопировано в буфер обмена."; + +/* Share via email button label */ +"Receive.emailButton" = "Эл. почта"; + +/* Request button label */ +"Receive.request" = "Запросить сумму"; + +/* Share button label */ +"Receive.share" = "Поделиться"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Текстовое сообщение"; + +/* Receive modal title */ +"Receive.title" = "Получить"; + +/* Done button text */ +"RecoverWallet.done" = "Готово"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Восстановить кошелек"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Сбросить PIN-код"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Введите бумажный ключ"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Восстановите Ваш Litewallet-кошелек с помощью бумажного ключа."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "Введенный вами бумажный ключ недействителен. Пожалуйста, внимательно перепроверьте каждое слово и повторите попытку."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Стрелка влево"; + +/* Next button label */ +"RecoverWallet.next" = "Далее"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Коснитесь здесь для получения подробной информации."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Стрелка вправо"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Введите бумажный ключ для кошелька, который вы хотите восстановить."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Для сброса PIN-кода введите слова из вашего бумажного ключа в расположенные ниже поля."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Сначала введите сумму."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Запросить сумму"; + +/* Alert action button label */ +"ReScan.alertAction" = "Синхронизация"; + +/* Alert message body */ +"ReScan.alertMessage" = "Вы не сможете отправлять деньги во время синхронизации."; + +/* Alert message title */ +"ReScan.alertTitle" = "Синхронизировать с Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 минут"; + +/* Syncing explanation */ +"ReScan.body2" = "Если транзакция описывается как \"выполненная\" в биткойн-сети, но не в вашем Litewallet-кошельке."; + +/* Syncing explanation */ +"ReScan.body3" = "Вы несколько раз получили ошибку о том, что транзакция отвергнута."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Начать синхронизацию"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Вы не сможете отправлять деньги во время синхронизации с blockchain."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Синхронизация с Blockchain"; + +/* Subheader label */ +"ReScan.subheader1" = "Примерное время"; + +/* Subheader label */ +"ReScan.subheader2" = "Когда синхронизировать?"; + +/* Reset walet button title */ +"resetButton" = "Да, сбросить кошелек"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Удалить мой Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Вспышка камеры"; + +/* Complete filter label */ +"Search.complete" = "выполнено"; + +/* Pending filter label */ +"Search.pending" = "в ожидании"; + +/* Received filter label */ +"Search.received" = "получено"; + +/* Sent filter label */ +"Search.sent" = "отправлено"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Активируйте все средства защиты для обеспечения максимальной безопасности."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "Единственный способ получения доступа к вашим биткойнам в случае потери или изменения вашего телефона."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Бумажный ключ"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Защищает ваш Litewallet от несанкционированных пользователей."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-цифровой PIN-код"; + +/* Security Center Title */ +"SecurityCenter.title" = "Центр безопасности"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Легко и просто разблокируйте ваш Litewallet-кошелек и отправляйте суммы, не превышающие ваш лимит на расходы."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Сумма"; + +/* Balance: $4.00 */ +"Send.balance" = "Баланс: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Платеж: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "ОТПРАВИТЬ"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Перейдите в \"Настройки\", чтобы разрешить доступ к камере."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet не имеет разрешения на доступ к камере"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "Место назначения является вашим собственным адресом. Вы не можете отправить деньги самому себе."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Не удалось создать транзакцию."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Записка"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Буфер обмена пуст"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Введите адрес Litecoin"; + +/* Fees: $0.01*/ +"Send.fee" = "Сборы: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Сборы:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Личность получателя платежа не сертифицирована."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Недостаточно средств"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Недействительный биткоин-адрес получателя."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "В буфере обмена нет действительного биткоин-адреса."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Недействительный адрес"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Отправка отключена во время полного сканирования."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Загрузка запроса"; + +/* Network */ +"Send.networkFee" = "Сеть"; + +/* Empty address alert message */ +"Send.noAddress" = "Пожалуйста, введите адрес получателя."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Пожалуйста, введите сумму для отправки."; + +/* Paste button label */ +"Send.pasteLabel" = "Вставить"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Не удалось опубликовать транзакцию."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Не удалось загрузить запрос на оплату"; + +/* Scan button label */ +"Send.scanLabel" = "Сканировать"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Отправить"; + +/* Service */ +"Send.serviceFee" = "Услуга"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Отправить"; + +/* Send money to label */ +"Send.toLabel" = "На"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "домен"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Введите"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Искать"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "К сожалению, домен не найден. [Ошибка: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Ошибка поиска"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Введите домен .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao или .x."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Введите домен"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Проблема поиска в системе. [Ошибка: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Биткойн-адреса предназначены исключительно для однократного использования."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Повторное использование снижает как вашу конфиденциальность, так и конфиденциальность получателя, и может привести к потерям, если получатель не совершает непосредственный контроль за этим адресом."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Адрес уже используется"; + +/* About label */ +"Settings.about" = "О нас"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Расширенные настройки"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Blockchain"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Вы уверены, что хотите изменить язык на %l?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Отображать валюту"; + +/* Current Locale */ +"Settings.currentLocale" = "Текущая локаль:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Присоединитесь к пользователям с ранним доступом"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Нравится ли вам использовать Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Импортировать кошелек"; + +/* Languages label */ +"Settings.languages" = "Языки"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Окружающая среда:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Партнеры Litewallet"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet версия:"; + +/* Manage settings section header */ +"Settings.manage" = "Управление"; + +/* Notifications label */ +"Settings.notifications" = "Уведомления"; + +/* Leave review button label */ +"Settings.review" = "Оставьте свой отзыв"; + +/* Share anonymous data label */ +"Settings.shareData" = "Поделиться анонимными данными"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Социальная"; + +/* Support settings section header */ +"Settings.support" = "Служба поддержки"; + +/* Sync blockchain label */ +"Settings.sync" = "Синхронизация с Blockchain"; + +/* Settings title */ +"Settings.title" = "Настройки"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Лимит на расходы Touch ID"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Кошелек"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Запустить / восстановить другой кошелек"; + +/* Share data view body */ +"ShareData.body" = "Помогите нам улучшить Litewallet, поделившись своими анонимными данными. Такие данные не включают какую-либо финансовую информацию. Мы уважаем вашу финансовую конфиденциальность."; + +/* Share data header */ +"ShareData.header" = "Поделиться данными?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Поделиться анонимными данными?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Текущий лимит расходов:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Выписать бумажный ключ снова"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Бумажный ключ – это единственный способ восстановить ваш Litewallet-кошелек в случае потери, кражи, поломки или замены вашего телефона.\n\nМы покажем вам список слов, которые необходимо записать на листе бумаги и хранить в надежном месте."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Выписать бумажный ключ"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Вы выписывали свой бумажный ключ в последний раз %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "Самый безопасный и простой способ использовать Litecoin"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Поддержите фонд Litecoin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Подключение ..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Повторный осмотр ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Успех!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Синхронизации..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Соединение"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Синхронизация"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ д"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ ч"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ м"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ с"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Вы можете настроить лимит расходов Touch ID через %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Используйте отпечатки ваших пальцев для разблокирования Litewallet-кошелька и отправки сумм, не превышающих ваш лимит на расходы."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Экран лимита расходов Touch ID"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Лимит на расходы: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Активировать функцию Touch ID для Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Вы не задали функцию Touch ID на этом устройстве. Перейдите в \"Настройки\"->\"Touch ID и Код доступа\", чтобы задать ее сейчас."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Функция Touch ID не задана"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Всегда требовать код доступа к устройству"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Вас попросят ввести свой 6-цифровой PIN-код, чтобы отправить транзакции, суммы которых превышают ваш лимит на расходы, и делать это каждые 48 часов с момента последнего ввода вами вашего 6-цифрового PIN-кода."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Лимит на расходы Touch ID"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Деталь суммы:"; + +/* Availability status text */ +"Transaction.available" = "Доступно для использования"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Блок:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Напоминание:"; + +/* Transaction complete label */ +"Transaction.complete" = "Выполнено"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Детализация суммы транзакции"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Конечный баланс: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Обменный курс при получении:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Обменный курс при отправке:"; + +/* (b600 fee) */ +"Transaction.fee" = "(плата %1$@)"; + +/* Invalid transaction */ +"Transaction.invalid" = "НЕДЕЙСТВИТЕЛЬНАЯ"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "только что"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "Выполнено: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "Выполнено: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Начальный баланс: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Подробности стартовой суммы транзакции"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Ожидает подтверждения. Некоторые продавцы требуют подтверждения для выполнения транзакции. Предполагаемое время: 1-2 часа."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "счет"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Сумма"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Подтверждено в блоке"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Записка"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Скопировано"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Скопируйте все детали"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Ваши транзакции будут отображаться здесь."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "на %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Меньше..."; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "Перемещено %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Перемещено %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Не подтверждено"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "по состоянию на "; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "Получено %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Получено %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "TransactionDetails.receivedModalTitle"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "Отправить %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Отправлено %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Статус"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Детали транзакции"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "на %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "ID биткоин-транзакции"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Получено на адрес:"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Отправлено на адрес:"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Отключено до: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Введите PIN-код"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Разблокировать с помощью FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Мой адрес"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Сбросить PIN-код"; + +/* Scan button title */ +"UnlockScreen.scan" = "Сканировать"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Разблокируйте ваш Litewallet-кошелек."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Разблокировать с помощью TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Кошелек разблокирован"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Запомните этот PIN-код. Если вы его забудете, что не сможете получить доступ к вашим биткойнам."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Ваш PIN-код будет использоваться для разблокировки кошелька Litewallet и отправки денег."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Задайте PIN-код"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Введите PIN-код еще раз"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Введите ваш текущий PIN-код."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Введите ваш новый PIN-код."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Введите ваш новый PIN-код еще раз."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Очень жаль, но нам не удалось обновить ваш PIN-код."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Ошибка обновления PIN-кода"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Обновить PIN-код"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Скопировать адреса кошелька в буфер обмена?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Авторизовать копирование адресов кошелька в буфер обмена"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Скопировать адреса кошелька"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Копировать"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Для авторизации этой транзакции введите свой ПИН-код."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Для продолжения, пожалуйста, введите ваш PIN-код."; + +/* Verify PIN view title */ +"VerifyPin.title" = "Необходим PIN-код"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Разрешить эту транзакцию"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Запустите Litewallet-приложение для iPhone, чтобы настроить ваш кошелек."; + +/* Dismiss button label */ +"Webview.dismiss" = "Отклонить"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Во время загрузки контента возникла ошибка. Пожалуйста, повторите попытку."; + +/* Updating webview message */ +"Webview.updating" = "Идет обновление..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Добро пожаловать в Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Добро пожаловать в Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Удалить базу данных"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Вы действительно хотите удалить этот кошелек?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Удалить кошелек?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Удалить базу данных"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Это удаляет базу данных, но сохраняет PIN-код и фразу. Подтвердите свой существующий PIN-код, начните и ждите, чтобы синхронизировать синхронизацию с новым БД"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Удалить и синхронизировать"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Забыли свою начальную фразу или PIN-код?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Не удалось удалить кошелек."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Не удалось"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Введите фразу восстановления кошелька для того, чтобы его удалить и запустить или восстановить другой. Ваш текущий баланс остается на этой фразе."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Создание или восстановление другого кошелька позволит вам получить доступ или управлять другим кошельком Litewallet на этом устройстве."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Вы больше не сможете получить доступ к текущему кошельку Litewallet с этого устройства. Ваш баланс останется на этой фразе."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Запуск или восстановление другого кошелька"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Это действие сотрет ваш Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Удаление вашего кошелька означает, что закрытый ключ и данные приложения будут удалены. Вы можете потерять Litecoin навсегда!\n\n\nНикто из команды Litewallet не может получить для вас это семя. Мы не несем ответственности, если вы не прислушаетесь к этому предупреждению."; + +/* Warning title */ +"WipeWallet.warningTitle" = "ПОЖАЛУЙСТА, ПРОЧИТАЙТЕ!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Удалить"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Удаление..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Выпишите все слова по порядку и храните их в надежном месте."; + +/* button label */ +"WritePaperPhrase.next" = "Далее"; + +/* button label */ +"WritePaperPhrase.previous" = "Назад"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d из %2$d"; diff --git a/litewallet/Strings/sv.lproj/Localizable.strings b/litewallet/Strings/sv.lproj/Localizable.strings new file mode 100755 index 000000000..96bd3b45b --- /dev/null +++ b/litewallet/Strings/sv.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "Blogg"; + +/* About screen footer */ +"About.footer" = "Skapad av det lokala teamet för Litewallet. Version %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Sekretesspolicy"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Om"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Stäng"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Support Center"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Laddar plånbok"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Mitt Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "HANTERA"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Lokalt korruptionsfel"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Detta raderar databasen men behåller PIN-koden och frasen. Bekräfta din befintliga PIN-kod, frö och vänta på att synkronisera till den nya db"; + +/* Error alert title */ +"Alert.error" = "Fel"; + +/* No internet alert message */ +"Alert.noInternet" = "Ingen anslutning till internet hittades. Kontrollera din anslutning och försök igen."; + +/* Warning alert title */ +"Alert.warning" = "Varning"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Adresser kopierade"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Alla plånboksadresser har kopierats."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Ordnyckel är inställd"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Härligt!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN-kod är inställd"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Domänupplösning"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Din adress har lösts!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Sändningen misslyckades"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Skicka bekräftelse"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Pengarna skickade!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "Fel vid JSON-serialisering"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Plånboken ej redo"; + +/* API Token error message */ +"ApiClient.tokenError" = "Kan inte ta emot API-symbol"; + +/* buy button */ +"Button.buy" = "köpa"; + +/* Cancel button label */ +"Button.cancel" = "Avbryt"; + +/* Ignore button label */ +"Button.ignore" = "Ignorera"; + +/* menu button */ +"Button.menu" = "meny"; + +/* No button */ +"Button.no" = "Nej"; + +/* OK button label */ +"Button.ok" = "OK"; + +/* receive button */ +"Button.receive" = "ta emot"; + +/* resetFields */ +"Button.resetFields" = "Nollställ fält"; + +/* send button */ +"Button.send" = "skicka"; + +/* Settings button label */ +"Button.settings" = "Inställningar"; + +/* Settings button label */ +"Button.submit" = "Överlämna"; + +/* Yes button */ +"Button.yes" = "Ja"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "KÖPA"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Köp presentkort \n • Fyll på förbetalda telefoner \n • Steam, Amazon, Hotels.com \n • Fungerar i 170 länder"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Byt Litecoin för andra kryptor \n • Ingen ID krävs \n • Köp via kreditkort \n • Global täckning"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Köp Litecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Köp LTC med många fiatpar\n• Betala med flera metoder\n• Global betalningsleverantör"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Skaffa Litecoin inom 5 minuter! \n • Köp Litecoin via kreditkort \n • Pass eller statligt ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Köp Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Centrera ditt ID i boxen"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Byt information:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Belopp att skicka:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Belopp att donera:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Bearbetningstid: Dessa transaktioner tar %1$@ minuter att behandla."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Bearbetningstid: Denna transaktion kommer att ta %1$@ minuter att bearbeta."; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "Skicka"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "AVGIFT:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADRESS:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Bekräftelse"; + +/* To: (address) */ +"Confirmation.to" = "Till"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Total kostnad:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "De angivna orden matchar inte din pappersnyckel. Var god försök igen."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Ange följande ord från din ordnyckel, för att vara säker på att allt skrevs korrekt."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Ord #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Kopia"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin visningsenhet"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Välja:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Växelkurs"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Enheten är inte konfigurerad för att skicka e-post med post-appen i iOS."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "E-posten är inte tillgänglig"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Denna enhet är inte konfigurerad för att skicka meddelanden."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Meddelandehantering inte tillgänglig"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "Ekonomi"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "Beräknad leveranstid: 10+ minuter"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Detta alternativ rekommenderas inte för tidskänsliga transaktioner."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Lyx"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "Uppskattad leverans: 2,5 - 5 minuter"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Detta alternativ garanterar praktiskt taget accept av din transaktion även om du betalar en premie."; + +/* Regular fee */ +"FeeSelector.regular" = "Vanlig"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Uppskattad leverans: 2,5 - 5+ minuter"; + +/* Fee Selector title */ +"FeeSelector.title" = "Bearbetningshastighet"; + +/* Confirm */ +"Fragment.confirm" = "Bekräfta"; + +/* Or */ +"Fragment.or" = "eller"; + +/* sorry */ +"Fragment.sorry" = "Förlåt"; + +/* to */ +"Fragment.to" = "till"; + +/* History Bar Item Title */ +"History.barItemTitle" = "HISTORIA"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Aktuellt LTC-värde i"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Kontrollerar privat nyckelsaldo..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Skicka %1$@ från denna privata nyckel till din plånbok? Litecoins nätverk tar en avgift på %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Denna privata nyckel finns redan i din plånbok."; + +/* empty private key error message */ +"Import.Error.empty" = "Denna privata nyckel är tom."; + +/* High fees error message */ +"Import.Error.highFees" = "Transaktionsavgifter skulle kosta mer än de medel som finns tillgängliga på denna privata nyckel."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Inte någon privat nyckel"; + +/* Import signing error message */ +"Import.Error.signing" = "Fel vid signering av transaktion"; + +/* Import button label */ +"Import.importButton" = "Importera"; + +/* Importing wallet progress view label */ +"Import.importing" = "Importerar plånbok"; + +/* Caption for graphics */ +"Import.leftCaption" = "Plånbok som ska importeras"; + +/* Import wallet intro screen message */ +"Import.message" = "Genom att importera en plånbok överförs alla pengar från din andra plånbok till Litewallet genom en enda transaktion."; + +/* Enter password alert view title */ +"Import.password" = "Denna privata nyckel är lösenordsskyddad."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "lösenord"; + +/* Caption for graphics */ +"Import.rightCaption" = "Din Litewallet-plånbok"; + +/* Scan Private key button label */ +"Import.scan" = "Läs av privat nyckel"; + +/* Import wallet success alert title */ +"Import.success" = "Lyckades"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Importerad plånbok."; + +/* Import Wallet screen title */ +"Import.title" = "Importera plånbok"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Nyckel för upplåsning"; + +/* Import wallet intro warning message */ +"Import.warning" = "Importering av en plånbok inkluderar inte transaktionshistorik eller andra uppgifter."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Fel lösenord, var god försök igen."; + +/* Close app button */ +"JailbreakWarnings.close" = "Stäng"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ignorera"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "ENHETENS SÄKERHET ÄR ÄVENTYRAD\nEn \"intrångs\"-app kan få tillgång till Litewallets nyckeldata och stjäla dina Litecoin! Töm denna plånbok direkt och återupprätta den på en säker enhet."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "ENHETENS SÄKERHET ÄR ÄVENTYRAD\nEn \"intrångs\"-app kan få tillgång till Litewallets nyckeldata och stjäla dina Litecoin! Använd Litewallet endast på en säker enhet."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "VARNING"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Töm"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Platstjänster är inaktiverade."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet har inte rätt att utnyttja platstjänster"; + +/* No comment provided by engineer. */ +"Malformed URI" = "Felaktig URI"; + +/* Balance */ +"ManageWallet.balance" = "Balans"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Du skapade din plånbok på %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Din plånboks namn visas bara i din kontohistorik och kan inte ses av någon annan."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Plånbokens namn"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Hantera plånbok"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Köp Litecoin"; + +/* Menu button title */ +"MenuButton.customer.support" = "Kundsupport"; + +/* Menu button title */ +"MenuButton.lock" = "Lås plånbok"; + +/* Menu button title */ +"MenuButton.security" = "Säkerhetscenter"; + +/* Menu button title */ +"MenuButton.settings" = "Inställningar"; + +/* Menu button title */ +"MenuButton.support" = "Support"; + +/* button label */ +"MenuViewController.createButton" = "Ska ny plånbok"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Meny"; + +/* button label */ +"MenuViewController.recoverButton" = "Återupprätta plånbok"; + +/* No comment provided by engineer. */ +"No wallet" = "Ingen plånbok"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Byt till automatiskt läge"; + +/* Node is connected label */ +"NodeSelector.connected" = "Ansluten"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Ange nodens IP-adress och port (valfritt)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Ange nod"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Byt till manuellt läge"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Nuvarande primär nod"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Inte ansluten"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Nodens anslutningsstatus"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin-noder"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Felaktig utbetalningsbegäran"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Dokumentet är felaktigt eller utan stöd"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "certifikat saknas"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "begäran har utgått"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Kunde inte göra utbetalning"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Utbetalning av Litecoin får inte vara lägre än %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Utbetalningstransaktioner av Litecoin får inte vara lägre än $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "signaturtypen saknar stöd"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "opålitligt certifikat"; + +/* Dismiss button. */ +"Prompts.dismiss" = "AVFÄRDA"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Ett enhets-lösenord behövs för att skydda din plånbok. Gå till inställningar och aktivera lösenord."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Aktivera enhets-lösenord"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Fortsätta"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Din pappersnyckel måste sparas ifall du skulle förlora eller byta ut din telefon. Tryck här för att fortsätta."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Annullera"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Gör det möjligt"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Åtgärd krävs"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Din plånbok kan vara osynkroniserad. Detta kan ofta lösas genom att läsa om blockkedjan."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Transaktionen avslogs"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet har uppgraderat till att använda 6-siffrig PIN-kod. Klicka här för att uppgradera."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Uppgradera PIN-kod"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Hjälp till att förbättra Litewallet genom att dela dina anonyma data med oss"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Dela anonyma data"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Klicka här för att aktivera Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Aktivera Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "MOTTA"; + +/* Address copied message. */ +"Receive.copied" = "Kopierad till urklipp."; + +/* Share via email button label */ +"Receive.emailButton" = "E-post"; + +/* Request button label */ +"Receive.request" = "Begär ett belopp"; + +/* Share button label */ +"Receive.share" = "Dela"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Textmeddelande"; + +/* Receive modal title */ +"Receive.title" = "Ta emot"; + +/* Done button text */ +"RecoverWallet.done" = "Klar"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Återupprätta plånbok"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Återställ PIN-kod"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Ange ordnyckel"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Återupprätta Litewallet med din ordnyckel."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "Den ordnyckel du angav är ogiltig. Dubbelkolla varje ord och försök igen."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Vänster pil"; + +/* Next button label */ +"RecoverWallet.next" = "Nästa"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Klicka här för fler uppgifter."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Höger pil"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Ange ordnyckel till den plånbok du vill återupprätta."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "För att återställa din PIN-kod anger du orden från din ordnyckel i boxarna nedan."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Var god ange ett belopp först."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Begär ett belopp"; + +/* Alert action button label */ +"ReScan.alertAction" = "Synka"; + +/* Alert message body */ +"ReScan.alertMessage" = "Du kan inte skicka pengar under synkning."; + +/* Alert message title */ +"ReScan.alertTitle" = "Synka med blockkedja?"; + +/* extimated time */ +"ReScan.body1" = "20-45 minuter"; + +/* Syncing explanation */ +"ReScan.body2" = "Om en transaktion visar sig som klar i Litecoins nätverk men inte i Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Du får gång på gång ett meddelande som säger att din transaktion avvisades."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Starta synkning"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Du kan inte skicka pengar under synkningen med blockkedjan."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Synka blockkedja"; + +/* Subheader label */ +"ReScan.subheader1" = "Uppskattad tid"; + +/* Subheader label */ +"ReScan.subheader2" = "När ska man synka?"; + +/* Reset walet button title */ +"resetButton" = "Ja, återställ plånboken"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Ta bort min Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Kamerablixt"; + +/* Complete filter label */ +"Search.complete" = "avsluta"; + +/* Pending filter label */ +"Search.pending" = "väntar"; + +/* Received filter label */ +"Search.received" = "mottaget"; + +/* Sent filter label */ +"Search.sent" = "skickat"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "Aktivera alla säkerhetsfunktioner för maximal säkerhet."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "Det enda sättet att komma åt dina Litecoin om du förlorar eller uppgraderar din telefon."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Ordnyckel"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Skyddar ditt Litewallet från obehöriga användare."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-siffrig PIN-kod"; + +/* Security Center Title */ +"SecurityCenter.title" = "Säkerhetscenter"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "Lås enkelt upp ditt Litewallet och skicka pengar upp till en inställd gräns."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Belopp"; + +/* Balance: $4.00 */ +"Send.balance" = "Saldo: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "SKICKA"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Gå till Inställningar för att ge tillgång till kameran."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet har inte tillgång till kameran"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "Destinationen är din egen adress. Du kan inte skicka till dig själv."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Kunde inte skapa transaktion."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Memo"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "Urklipp är tomt"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Ange en Litecoin-adress"; + +/* Network Fee: $0.01 */ +"Send.fee" = "Nätverksavgift: %1$@"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Betalningsmottagaren är inte certifierad."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "otillräckliga medel"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Mottagaradressen är inte en giltig Litecoin-adress."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Urklipp innehåller inte en giltig Litecoinadress."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Ogiltig adress"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Skicka är avaktiverat under en full omskanning."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Laddar förfrågan"; + +/* Empty address alert message */ +"Send.noAddress" = "Ange mottagarens adress."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Ange ett belopp att skicka."; + +/* Paste button label */ +"Send.pasteLabel" = "Klistra in"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Kunde inte lägga ut transaktion."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Kunde inte ladda betalningsförfrågan"; + +/* Scan button label */ +"Send.scanLabel" = "Läs av"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "Skicka"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "Skicka"; + +/* Send money to label */ +"Send.toLabel" = "Till"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Slå upp"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Tyvärr hittades inte domänen. [Fel: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Sökningen misslyckades"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Ange en .crypto- eller .zil-domän"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Problem med systemuppslag. [Fel: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin-adresser är avsedda för engångsanvändning."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Återanvändning minskar sekretessen både för dig och mottagaren och kan sluta i förlust om mottagaren inte direkt kontrollerar adressen."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Adressen är redan använd"; + +/* About label */ +"Settings.about" = "Om"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Avancerade inställningar"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "blockchain"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "Visa valuta"; + +/* Current Locale */ +"Settings.currentLocale" = "Aktuell lokal:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Använd Snabb tillgång"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Gillar du Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "Importera plånbok"; + +/* Languages label */ +"Settings.languages" = "språk"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Miljö:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet-partners"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet version:"; + +/* Manage settings section header */ +"Settings.manage" = "Hantera"; + +/* Notifications label */ +"Settings.notifications" = "Meddelanden"; + +/* Leave review button label */ +"Settings.review" = "Betygssätt oss"; + +/* Share anonymous data label */ +"Settings.shareData" = "Dela anonyma uppgifter"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Social"; + +/* Support settings section header */ +"Settings.support" = "Stöd"; + +/* Sync blockchain label */ +"Settings.sync" = "Synka blockkedja"; + +/* Settings title */ +"Settings.title" = "Inställningar"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch ID utnyttjandegräns"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Plånbok"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Starta/Återställ en annan plånbok"; + +/* Share data view body */ +"ShareData.body" = "Hjälp till att förbättra Litewallet genom att dela dina anonyma uppgifter med oss. Detta inbegriper inga ekonomiska uppgifter. Vi respekterar din ekonomiska sekretess."; + +/* Share data header */ +"ShareData.header" = "Dela uppgifter?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Dela anonyma uppgifter?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Nuvarande utgiftsgräns: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Skriv ner ordnyckel på nytt"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Din ordnyckel är enda sättet att återupprätta Litewallet om du förlorat din telefon, blivit bestulen på den eller har uppgraderat den.\n\nVi visar dig en lista över ord som du kan skriva ner på ett papper och gömma undan."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Skriv ner ordnyckel"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Senast skrev du ner din ordnyckel på %1$@"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Stöd Litecoin Foundation"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Ansluter..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Omscanning ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Framgång!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Synkronisera ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Ansluter"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Synkroniserar"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ t"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Du kan anpassa din utgiftsgräns i Touch ID från %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Använd ditt fingeravtryck för att låsa upp Litewallet och skicka pengar upp till en inställd gräns."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Sida för utgiftsgräns i Touch ID"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Utnyttjandegräns: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Aktivera Touch ID för Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Du har inte ställt in Touch ID på denna enhet. Gå till Inställningar->Touch ID & Lösenkod för att ställa in det nu."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID är inte inställt"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Kräver alltid lösenord"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Du kommer att behöva ange din 6-siffriga PIN-kod för att skicka transaktioner som överskrider din utnyttjandegräns och varje gång efter 48 timmar sedan du senast angav din 6-siffriga PIN-kod."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch ID utnyttjandegräns"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Byt information:"; + +/* Availability status text */ +"Transaction.available" = "Tillgängligt att utnyttja"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Blockera:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "PM:"; + +/* Transaction complete label */ +"Transaction.complete" = "Avsluta"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Transaktionssluttbeloppdetalj"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Utgående saldo: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Växlingskurs vid mottagande:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Växlingskurs vid skickande:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ avgift)"; + +/* Invalid transaction */ +"Transaction.invalid" = "OGILTIG"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "just nu"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "Pågår: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "Pågår: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Ingående saldo: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Detaljer för transaktionsstartbelopp"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Väntar på bekräftelse. Vissa handlare kräver bekräftelse för att slutföra en transaktion. Uppskattad tid: 1-2 timmar."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "konto"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Belopp"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Bekräftad i block"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Memo"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Kopierad"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Kopiera alla detaljer"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Dina transaktioner visas här."; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "på %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Mindre"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "Flyttade %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Flyttade %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Inte bekräftad"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "från och med"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "Mottog %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Tog emot %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "FÅ ADDRESS"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "Skickade %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Skickade %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Status"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Transaktionsuppgifter"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "till %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin transaktions-ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Togs emot på den här adressen"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Skickat till den här adressen"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Inaktiverad till: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Ange PIN-koden"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Lås upp med FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Min adress"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Återställ PIN-kod"; + +/* Scan button title */ +"UnlockScreen.scan" = "Läs av"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "Lås upp Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Lås upp med TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Plånbok upplåst"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Kom ihåg PIN-koden. Om du glömmer den, kommer du inte åt dina Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Din PIN-kod används för att låsa upp ditt Litewallet och skicka pengar"; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Ställ in PIN-kod"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Ange PIN-koden igen"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Ange aktuell PIN-kod."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Ange din nya PIN-kod."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Ange din nya PIN-kod igen."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Förlåt, vi kunde inte uppdatera din PIN-kod."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Fel vid uppdatering av PIN-kod"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Uppdatera PIN-kod"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Kopiera plånboksadresser till urklipp?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Godkänn att kopiera plånbokens adress till urklipp"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Kopiera plånboksadresser"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Kopiera"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Var god ange din pinkod för att godkänna denna transaktion."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Ange din PIN-kod för att fortsätta."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN-kod krävs"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Godkänn denna transaktion"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Öppna iPhone-appen Litewallet för att ställa in din plånbok."; + +/* Dismiss button label */ +"Webview.dismiss" = "Avfärda"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Det blev fel vid uppladdning av innehållet. Försök igen"; + +/* Updating webview message */ +"Webview.updating" = "Uppdaterar..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "Välkommen till Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "Välkommen till Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Radera databas"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Är du säker på att du vill ta bort den här plånboken."; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Rensa plånbok?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Ta bort databasen"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Din lokala databas är skadad. Gå till Inställningar> Blockchain: Inställningar> Ta bort databas för att uppdatera"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Radera & synkronisera"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Glömt din fröfras eller PIN-kod?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Rensning av plånboken misslyckades."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Misslyckades"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "Ange den här plånbokens återställningsfras för att rensa den och starta eller återställa en annan. Ditt nuvarande saldo är kvar på den här frasen."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Att starta eller återställa en annan plånbok ger dig tillgång till och låter dig hantera en annan Litewalletplånbok på denna enhet."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Du kommer inte längre att kunna komma åt din aktuella Litewalletplånbok från denna enhet. Saldot kommer att kvarstå på frasen."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Starta eller hämta en annan plånbok"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Denna åtgärd kommer att torka din Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Om du tar bort din plånbok innebär det att den privata nyckeln och appdata försvinner. Du kan förlora Litecoin för alltid!\n\n\nIngen i Litewallet-teamet kan hämta detta frö åt dig. Vi är inte ansvariga om du inte följer denna varning."; + +/* Warning title */ +"WipeWallet.warningTitle" = "VÄNLIGEN LÄS!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Rensa"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Rensar..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Skriv ner varje ord i ordning och lagra dem på ett säkert ställe."; + +/* button label */ +"WritePaperPhrase.next" = "Nästa"; + +/* button label */ +"WritePaperPhrase.previous" = "Föregående"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d av %2$d"; + +/* "Email address label" */ +"Notifications.emailLabel" = ""; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = ""; + +/* "Email title" */ +"Notifications.emailTitle" = ""; + +/* "Language preference label" */ +"Notifications.languagePreference" = ""; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = ""; + +/* Signup cancel */ +"Notifications.signupCancel" = ""; + +/* Fee: $0.01 */ +"Send.bareFee" = ""; + +/* Fees Blank: */ +"Send.feeBlank" = ""; + +/* Network */ +"Send.networkFee" = ""; + +/* Service */ +"Send.serviceFee" = ""; + +/* domain */ +"Send.UnstoppableDomains.domain" = ""; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = ""; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = ""; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = ""; + +/* Start view tagline */ +"StartViewController.tagline" = ""; diff --git a/litewallet/Strings/tr.lproj/Localizable.strings b/litewallet/Strings/tr.lproj/Localizable.strings new file mode 100644 index 000000000..a3e6b0e09 --- /dev/null +++ b/litewallet/Strings/tr.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen website label */ +"About.blog" = "İnternet sitesi"; + +/* About screen footer */ +"About.footer" = "Litecoin Vakfı'nın LiteWallet\nEkibi tarafından yapılmıştır\n%1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Gizlilik Politikası"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Hakkında"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Kapat"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Destek Merkezi"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Cüzdan yükleniyor"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Litewallet'ım"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "YÖNET"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Yerel Bozulma Hatası"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Yerel veritabanınız bozuk. Yenilemek için Ayarlar> Blok Zinciri: Ayarlar> Veritabanını Sil'e gidin"; + +/* Error alert title */ +"Alert.error" = "Hata"; + +/* No internet alert message */ +"Alert.noInternet" = "İnternet bağlantısı bulunamadı. Bağlantınızı kontrol edin ve tekrar deneyin."; + +/* Warning alert title */ +"Alert.warning" = "Uyarı"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Kopyalanan Adresler"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Tüm cüzdan adresleri başarıyla kopyalandı."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Kağıt Anahtar Takımı"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Müthiş!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN Ayarı"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Etki Alanı Çözünürlüğü"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Adres çözüldü!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Gönderilemedi"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Onay Gönder"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Para gönderildi!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON Serileştirme Hatası"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Cüzdan hazır değil"; + +/* API Token error message */ +"ApiClient.tokenError" = "API jetonu alınamıyor"; + +/* buy button */ +"Button.buy" = "satın almak"; + +/* Cancel button label */ +"Button.cancel" = "İptal etmek"; + +/* Ignore button label */ +"Button.ignore" = "Göz ardı etmek"; + +/* menu button */ +"Button.menu" = "Menü"; + +/* No button */ +"Button.no" = "Hayır"; + +/* OK button label */ +"Button.ok" = "TAMAM MI"; + +/* receive button */ +"Button.receive" = "teslim almak"; + +/* resetFields */ +"Button.resetFields" = "Alanları sıfırla"; + +/* send button */ +"Button.send" = "göndermek"; + +/* Settings button label */ +"Button.settings" = "Ayarlar"; + +/* Settings button label */ +"Button.submit" = "Sunmak"; + +/* Yes button */ +"Button.yes" = "Evet"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "SATIN AL"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Hediye kartları satın alın\n• Ön ödemeli telefonları yeniden doldurun\n• Steam, Amazon, Hotels.com\n• 170 ülkede çalışıyor"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Litecoin'i diğer kriptolar için değiştirin\n• Kimlik Gerekmiyor\n• Kredi kartıyla satın al\n• Küresel kapsam"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Łitecoin satın alın"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Birçok fiat çiftiyle LTC satın alın \n • Birden çok yöntemle ödeme yapın \n • Global ödeme sağlayıcısı"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• 5 dakikada Litecoin edinin!\n• Kredi kartıyla Litecoin satın alın\n• Pasaport veya Eyalet Kimliği"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Litecoin satın alın"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Kimliğinizi kutuda ortalayın"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Değişim ayrıntıları:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Gönderilecek tutar:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Bağışlanacak Miktar:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "İşlem süresi: Bu işlemlerin işlenmesi %1$@ dakika sürecektir."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "İşlem süresi: Bu işlemin işlenmesi %1$@ dakika sürecektir."; + +/* Send: (amount) */ +"Confirmation.send" = "Gönder"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "ÜCRET:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "ADDRESS:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Onayla"; + +/* To: (address) */ +"Confirmation.to" = "İçin"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Toplam tutar:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Girilen sözcükler kağıt anahtarınızla eşleşmiyor. Lütfen tekrar deneyin."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Her şeyin doğru yazıldığından emin olmak için lütfen kağıt anahtarınızdan aşağıdaki kelimeleri girin."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "Kelime #%1$@"; + +/* Copy. */ +"Copy" = "Kopyala"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin Görüntü Birimi"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Seçin:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Döviz kuru"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Bu cihaz, iOS posta uygulamasıyla e-posta gönderecek şekilde yapılandırılmamış."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "E-posta Kullanılamıyor"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Bu cihaz mesaj gönderecek şekilde yapılandırılmamış."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Mesajlaşma Kullanılamıyor"; + +/* You can customize your Face ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "Face ID harcama limitinizi şuradan özelleştirebilirsiniz: %1$@."; + +/* Face ID screen label */ +"FaceIDSettings.label" = "Litewallet'ınızın kilidini açmak ve belirli bir limite kadar para göndermek için yüzünüzü kullanın."; + +/* Link Text (see TouchIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "Face ID Harcama Sınırı Ekranı"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "Litewallet için Face ID'yi etkinleştirin"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "Face ID"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "Bu cihazda Face ID kurmadınız. Şimdi kurmak için Ayarlar-> Face ID ve Parola'ya gidin."; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "Yüz Kimliği Ayarlanmadı"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "Face ID Harcama Sınırı"; + +/* Economy fee */ +"FeeSelector.economy" = "Ekonomi"; + +/* Fee Selector economy fee description */ +"FeeSelector.economyLabel" = "Tahmini Teslimat: 10+ dakika"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Bu seçenek, zamana duyarlı işlemler için önerilmez."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Lüks"; + +/* Fee Selector economy fee description */ +"FeeSelector.luxuryLabel" = "Tahmini Teslimat: 2,5 - 5+ dakika"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Bu seçenek, bir prim ödüyor olsanız bile işleminizin kabul edilmesini neredeyse garanti eder."; + +/* Regular fee */ +"FeeSelector.regular" = "Düzenli"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Tahmini Teslimat: 2,5 - 5+ dakika"; + +/* Fee Selector title */ +"FeeSelector.title" = "İşleme Hızı"; + +/* Confirm */ +"Fragment.confirm" = "Confirm"; + +/* Or */ +"Fragment.or" = "veya"; + +/* sorry */ +"Fragment.sorry" = "Üzgünüm"; + +/* to */ +"Fragment.to" = "ile"; + +/* History Bar Item Title */ +"History.barItemTitle" = "GEÇMİŞ"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Geçerli LTC değeri"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Özel anahtar bakiyesi kontrol ediliyor ..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Bu özel anahtardan cüzdanınıza%1$@ gönderilsin mi? Litecoin ağı%2$@ ücret alacak."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Bu özel anahtar zaten cüzdanınızda bulunuyor."; + +/* empty private key error message */ +"Import.Error.empty" = "Bu özel anahtar boş."; + +/* High fees error message */ +"Import.Error.highFees" = "İşlem ücretleri, bu özel anahtarda bulunan paradan daha pahalı olacaktır."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Geçerli bir özel anahtar değil"; + +/* Import signing error message */ +"Import.Error.signing" = "İşlem imzalanırken hata oluştu"; + +/* Import button label */ +"Import.importButton" = "İçe Aktar"; + +/* Importing wallet progress view label */ +"Import.importing" = "Cüzdan İçe Aktarılıyor"; + +/* Caption for graphics */ +"Import.leftCaption" = "İçe aktarılacak Cüzdan"; + +/* Import wallet intro screen message */ +"Import.message" = "Bir cüzdanı içe aktarmak, diğer cüzdanınızdaki tüm parayı tek bir işlem kullanarak Litewallet cüzdanınıza aktarır."; + +/* Enter password alert view title */ +"Import.password" = "Bu özel anahtar şifre korumalıdır."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "şifre"; + +/* Caption for graphics */ +"Import.rightCaption" = "Litewallet Cüzdanınız"; + +/* Scan Private key button label */ +"Import.scan" = "Özel Anahtarı Tara"; + +/* Import wallet success alert title */ +"Import.success" = "Başarılı"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Cüzdan başarıyla içe aktarıldı."; + +/* Import Wallet screen title */ +"Import.title" = "Cüzdanı İçe Aktar"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Kilit Açma Anahtarı"; + +/* Import wallet intro warning message */ +"Import.warning" = "Bir cüzdanı içe aktarmak işlem geçmişini veya diğer ayrıntıları içermez."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Yanlış şifre, lütfen tekrar deneyin."; + +/* Close app button */ +"JailbreakWarnings.close" = "Kapat"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Yoksay"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "CİHAZ GÜVENLİĞİ İHLAL EDİLDİ \n Herhangi bir 'jailbreak' uygulaması Litewallet'in anahtar zinciri verilerine erişebilir ve Litecoin'inizi çalabilir! Bu cüzdanı hemen silin ve güvenli bir cihazda geri yükleyin."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "CİHAZ GÜVENLİĞİ İHLAL EDİLDİ \n Herhangi bir 'jailbreak' uygulaması Litewallet'in anahtar zinciri verilerine erişebilir ve Litecoin'inizi çalabilir. Lütfen Litewallet'i yalnızca jailbreak yapılmamış bir cihazda kullanın."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "UYARI"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Sil"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Konum hizmetleri devre dışı bırakıldı."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet'in konum hizmetlerine erişim izni yok."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Hatalı URI"; + +/* Balance */ +"ManageWallet.balance" = "Bakiye"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Cüzdanınızı%1$@ üzerinde oluşturdunuz"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Cüzdan adınız yalnızca hesap işlem geçmişinizde görünür ve başkaları tarafından görülemez."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Cüzdan Adı"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Cüzdanı Yönetin"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Litecoin Satın Al"; + +/* Menu button title */ +"MenuButton.customer.support" = "Müşteri desteği"; + +/* Menu button title */ +"MenuButton.lock" = "Cüzdanı Kilitle"; + +/* Menu button title */ +"MenuButton.security" = "Güvenlik Merkezi"; + +/* Menu button title */ +"MenuButton.settings" = "Ayarlar"; + +/* Menu button title */ +"MenuButton.support" = "Destek"; + +/* button label */ +"MenuViewController.createButton" = "Yeni Cüzdan Oluştur"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Menü"; + +/* button label */ +"MenuViewController.recoverButton" = "Cüzdanı Kurtar"; + +/* No wallet. */ +"No wallet" = "Cüzdan yok"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Otomatik Moda Geç"; + +/* Node is connected label */ +"NodeSelector.connected" = "Bağlandı"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Düğüm IP adresini ve bağlantı noktasını girin (isteğe bağlı)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Düğüm Girin"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Manuel Moda Geç"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Geçerli Birincil Düğüm"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Bağlı Değil"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Düğüm Bağlantı Durumu"; + +/* Node Selector view title */ +"NodeSelector.title" = "Litecoin Düğümleri"; + +/* "Email address label" */ +"Notifications.emailLabel" = "E -posta adresi"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "Buraya Gir"; + +/* "Email title" */ +"Notifications.emailTitle" = "Bir şey kaçırmayın!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "Tercih edilen dil:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "Güncellemeleri ve yarışmaları duymak için kaydolun."; + +/* Signup cancel */ +"Notifications.signupCancel" = "Hayır, teşekkürler."; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Kötü Ödeme İsteği"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Desteklenmeyen veya bozuk belge"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "eksik sertifika"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "isteğin süresi doldu"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Ödeme yapılamadı"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Litecoin ödemeleri%1$@ 'den az olamaz."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Litecoin işlem çıktıları $@ 'dan küçük olamaz."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "desteklenmeyen imza tipi"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "güvenilmeyen sertifika"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DEVRE DIŞI BIRAK"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "Face ID'yi etkinleştirmek için buraya dokunun"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "Yüz Kimliğini Etkinleştir"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Cüzdanınızı korumak için bir cihaz şifresi gerekli."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Cihaz şifresini aç"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Devam Et"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Kağıt Anahtarınız güvenli bir yerde tutulmalıdır. Litewallet'ınızı değiştirmenin veya geri yüklemenin veya Litecoin'inizi aktarmanın tek yolu budur. Lütfen not edin."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "İptal"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Etkinleştir"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "İşlem Gerekli"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Cüzdanınız senkronize olmayabilir. Bu genellikle blok zinciri yeniden taranarak giderilebilir."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "İşlem Reddedildi"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet 6 haneli bir PIN gerektirir. Lütfen PIN kodunuzu güvenli bir yerde ayarlayın ve saklayın."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "PIN Ayarla"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Anonim verilerinizi bizimle paylaşarak Litewallet'in geliştirilmesine yardımcı olun"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Anonim Verileri Paylaşın"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Touch ID'yi etkinleştirmek için buraya dokunun"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Touch ID'yi Etkinleştir"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "AL"; + +/* Address copied message. */ +"Receive.copied" = "Panoya kopyalandı."; + +/* Share via email button label */ +"Receive.emailButton" = "E-posta"; + +/* Request button label */ +"Receive.request" = "Tutar İste"; + +/* Share button label */ +"Receive.share" = "Paylaş"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Kısa Mesaj"; + +/* Receive modal title */ +"Receive.title" = "Al"; + +/* Done button text */ +"RecoverWallet.done" = "Bitti"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Cüzdanı Kurtar"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "PIN'i Sıfırla"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Kağıt Anahtarını Girin"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Kağıt anahtarınızla Litewallet'inizi kurtarın."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "Girdiğiniz kağıt anahtarı geçersiz. Lütfen her kelimeyi iki kez kontrol edin ve tekrar deneyin."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Sol Ok"; + +/* Next button label */ +"RecoverWallet.next" = "Sonraki"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Daha fazla bilgi için buraya dokunun."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Sağ Ok"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Kurtarmak istediğiniz cüzdanın kağıt anahtarını girin."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "PIN kodunuzu sıfırlamak için kağıt anahtarınızdaki kelimeleri aşağıdaki kutulara girin."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Lütfen önce bir miktar girin."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Miktar İste"; + +/* Alert action button label */ +"ReScan.alertAction" = "Eşitle"; + +/* Alert message body */ +"ReScan.alertMessage" = "Eşitleme sırasında para gönderemeyeceksiniz."; + +/* Alert message title */ +"ReScan.alertTitle" = "Blockchain ile eşitlensin mi?"; + +/* extimated time */ +"ReScan.body1" = "20-45 dakika"; + +/* Syncing explanation */ +"ReScan.body2" = "Bir işlem Litecoin ağında tamamlandı olarak gösteriliyor, ancak Litewallet'ınızda gösterilmiyorsa."; + +/* Syncing explanation */ +"ReScan.body3" = "Sürekli olarak işleminizin reddedildiğini söyleyen bir hata alıyorsunuz."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Eşitlemeyi Başlat"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Blok zinciri ile eşzamanlarken para gönderemeyeceksiniz."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Blok Zincirini Eşitle"; + +/* Subheader label */ +"ReScan.subheader1" = "Tahmini süre"; + +/* Subheader label */ +"ReScan.subheader2" = "Ne Zaman Eşitlenmeli?"; + +/* Reset walet button title */ +"resetButton" = "Evet, cüzdanı sıfırla"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Litewallet'ımı sil"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Kamera Flaşı"; + +/* Complete filter label */ +"Search.complete" = "tamamlandı"; + +/* Pending filter label */ +"Search.pending" = "beklemede"; + +/* Received filter label */ +"Search.received" = "alındı"; + +/* Sent filter label */ +"Search.sent" = "gönderildi"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "Yüz Kimliği"; + +/* Security Center Info */ +"SecurityCenter.info" = "Maksimum koruma için tüm güvenlik özelliklerini etkinleştirin."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "Telefonunuzu kaybederseniz veya yükseltirseniz Litecoin'inize erişmenin tek yolu."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Kağıt Anahtarı"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Litewallet'ınızı yetkisiz kullanıcılardan korur."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6 Haneli PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "Güvenlik Merkezi"; + +/* Touch ID/FaceID button description */ +"SecurityCenter.touchIdDescription" = "Litewallet'ınızın kilidini rahatça açın ve belirlenen limite kadar para gönderin."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Dokunma Kimliği"; + +/* Send money amount label */ +"Send.amountLabel" = "Miktar"; + +/* Balance: $4.00 */ +"Send.balance" = "Bakiye:%1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Ücret: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "GÖNDER"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Kamera erişimine izin vermek için Ayarlar'a gidin."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet'in kameraya erişmesine izin verilmiyor"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "Hedef, kendi adresinizdir. Kendinize gönderemezsiniz."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "İşlem oluşturulamadı."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Not"; + +/* Empty pasteboard error message */ +"Send.emptyPasteboard" = "Çalışma alanı boş"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Bir Litecoin adresi girin"; + +/* Fees: $0.01*/ +"Send.fee" = "Ücretler: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Ücretler:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Alacaklı kimliği onaylı değil."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Yetersiz Para"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Hedef adresi geçerli bir Litecoin adresi değil."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Çalışma alanı geçerli bir Litecoin adresi içermiyor."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Geçersiz Adres"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Tam yeniden tarama sırasında gönderme devre dışı bırakılır."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Yükleme İsteği"; + +/* Network */ +"Send.networkFee" = "Ağ"; + +/* Empty address alert message */ +"Send.noAddress" = "Lütfen alıcının adresini girin."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Lütfen gönderilecek bir miktar girin."; + +/* Paste button label */ +"Send.pasteLabel" = "Yapıştır"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "İşlem yayınlanamadı."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Ödeme isteği yüklenemedi"; + +/* Scan button label */ +"Send.scanLabel" = "Tara"; + +/* Send button label */ +"Send.sendLabel" = "Gönder"; + +/* Service */ +"Send.serviceFee" = "Hizmet"; + +/* Send modal title */ +"Send.title" = "Gönder"; + +/* Send money to label */ +"Send.toLabel" = "Kime"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "alan adı"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Gir"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Arama"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "Üzgünüm, etki alanı bulunamadı. [Hata:%2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Arama başarısız oldu"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Bir .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao veya .x alan adı girin."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Alan adını girin"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Sistem arama sorunu. [Hata: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Litecoin adresleri tek kullanımlıktır."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Yeniden kullanım, hem sizin hem de alıcı için gizliliği azaltır ve alıcının adresi doğrudan kontrol etmemesi durumunda kayba neden olabilir."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Adres Zaten Kullanılıyor"; + +/* About label */ +"Settings.about" = "Hakkında"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Gelişmiş Ayarlar"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Blockchain"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Dili %l olarak değiştirmek istediğinizden emin misiniz?"; + +/* Default currency label */ +"Settings.currency" = "Para Birimi Görüntüle"; + +/* Current Locale */ +"Settings.currentLocale" = "Mevcut Yerel Ayar:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Erken Erişime Katılın"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Litewallet'ten hoşlanıyor musunuz?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "Yüz Kimliği Harcama Sınırı"; + +/* Import wallet label */ +"Settings.importTitle" = "Cüzdanı İçe Aktar"; + +/* Languages label */ +"Settings.languages" = "Diller"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Çevre:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet ortakları"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet sürümü:"; + +/* Manage settings section header */ +"Settings.manage" = "Yönet"; + +/* Notifications label */ +"Settings.notifications" = "Bildirimler"; + +/* Leave review button label */ +"Settings.review" = "Bize Bir İnceleme Bırakın"; + +/* Share anonymous data label */ +"Settings.shareData" = "Anonim Verileri Paylaşın"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Sosyal"; + +/* Support settings section header */ +"Settings.support" = "Destek"; + +/* Sync blockchain label */ +"Settings.sync" = "Blok Zincirini Eşitle"; + +/* Settings title */ +"Settings.title" = "Ayarlar"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Dokunma Kimliği Harcama Sınırı"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Cüzdan"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Başka Cüzdanı Başlat / Kurtar"; + +/* Share data view body */ +"ShareData.body" = "Anonim verilerinizi bizimle paylaşarak Litewallet'i geliştirmeye yardımcı olun. Bu herhangi bir finansal bilgi içermez. Finansal gizliliğinize saygı duyuyoruz."; + +/* Share data header */ +"ShareData.header" = "Veriler Paylaşılsın mı?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Anonim Veriler Paylaşılsın mı?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Mevcut Harcama Sınırı:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Kağıt Anahtarını Yeniden Yazın"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Telefonunuz kaybolursa, çalınırsa, bozulursa veya yükseltilirse, Litewallet'inizi geri yüklemenin tek yolu kağıt anahtarınızdır. \n \n Size bir kağıt parçasına yazabileceğiniz bir kelime listesi göstereceğiz ve güvende tutun. "; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Kağıt Anahtarını Yaz"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Kağıt anahtarınızı en son%1$@ üzerine yazdınız"; + +/* Start view tagline */ +"StartViewController.tagline" = "Litecoin kullanmanın en güvenli ve en kolay yolu."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Litecoin Foundation'ı Destekleyin"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Bağlanıyor ..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Yeniden taranıyor ..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Başarılı!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Eşitleniyor ..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Bağlanıyor"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Eşitleme"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Dokunma Kimliği harcama sınırınızı%1$@ üzerinden özelleştirebilirsiniz."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Litewallet'ınızın kilidini açmak ve belirli bir limite kadar para göndermek için parmak izinizi kullanın."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Dokunmatik Kimlik Harcama Sınırı Ekranı"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Harcama sınırı:%1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Litewallet için Dokunmatik Kimliğini Etkinleştir"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Dokunmatik Kimlik"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Bu cihazda Touch ID kurmadınız. Şimdi kurmak için Ayarlar-> Touch ID ve Parola'ya gidin."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID Ayarlanmadı"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Her zaman parola gerektir"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Harcama limitinizi aşan herhangi bir işlemi göndermek için 6 haneli PIN kodunuzu girmeniz ve 6 haneli PIN kodunuzu en son girdiğinizden bu yana her 48 saatte bir girmeniz istenecektir."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Dokunma Kimliği Harcama Sınırı"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Miktar Ayrıntısı:"; + +/* Availability status text */ +"Transaction.available" = "Harcanabilir"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Blok:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Not:"; + +/* Transaction complete label */ +"Transaction.complete" = "Tamamlandı"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "İşlem bitiş tutarı ayrıntısı"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Kapanış bakiyesi:%1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Alındığında döviz kuru:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Gönderildiğinde döviz kuru:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ ücret)"; + +/* Invalid transaction */ +"Transaction.invalid" = "GEÇERSİZ"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "hemen şimdi"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "Devam ediyor:%1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "Devam ediyor:%1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Başlangıç ​​bakiyesi:%1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "İşlem başlangıç ​​tutarı ayrıntısı"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx Kimliği:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Onaylanmayı bekliyor. Bazı satıcılar bir işlemi tamamlamak için onay gerektirir. Tahmini süre: 1-2 saat."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "hesap"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Miktar"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Blokta Onaylandı"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Not"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Kopyalandı"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Tüm ayrıntıları kopyala"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "İşlemleriniz burada görünecektir."; + +/* [received] at
(received title 2/2) */ +"TransactionDetails.from" = "%1$@ adresinde"; + +/* Less button title */ +"TransactionDetails.less" = "Daha Az ..."; + +/* Moved $5.00 */ +"TransactionDetails.moved" = "%1$@ taşındı"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "%1@ taşındı"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Onaylanmadı"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "itibarıyla"; + +/* Received $5.00 (received title 1/2) */ +"TransactionDetails.received" = "%1$@ Alındı"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "%1@ alındı"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "ADRES AL"; + +/* Sent $5.00 (sent title 1/2) */ +"TransactionDetails.sent" = "%1$@ gönderildi"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Gönderildi %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Durum"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "İşlem Ayrıntıları"; + +/* [sent] to
(sent title 2/2) */ +"TransactionDetails.to" = "%1$@'e"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Litecoin İşlem Kimliği"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Bu Adreste Alındı"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Bu Adrese Gönderildi"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Şu tarihe kadar devre dışı bırakıldı:%1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "PIN'i Girin"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "FaceID ile Kilidi Aç"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Adresim"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "PIN'i Sıfırla"; + +/* Scan button title */ +"UnlockScreen.scan" = "Tara"; + +/* TouchID/FaceID prompt text */ +"UnlockScreen.touchIdPrompt" = "Litewallet'ınızın kilidini açın."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "TouchID ile Kilidi Aç"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Cüzdan Kilidi Açıldı"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Bu PIN kodunu hatırlayın. Unutursanız, Litecoin'inize erişemezsiniz."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "PIN kodunuz Litewallet'inizin kilidini açmak ve para göndermek için kullanılacaktır."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "PIN Ayarla"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "PIN'i Yeniden Girin"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Mevcut PIN kodunuzu girin."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Yeni PIN kodunuzu girin."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Yeni PIN kodunuzu yeniden girin."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "Üzgünüm, PIN güncellenemedi."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "PIN Hatasını Güncelle"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "PIN Güncelle"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Cüzdan adresleri panoya kopyalansın mı?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Cüzdan adresini panoya kopyalama yetkisi ver"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Cüzdan Adreslerini Kopyala"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Kopyala"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Bu işlemi yetkilendirmek için lütfen PIN kodunuzu girin."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Devam etmek için lütfen PIN kodunuzu girin."; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN Gerekli"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Bu işlemi yetkilendirin"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Cüzdanınızı ayarlamak için Litewallet iPhone uygulamasını açın."; + +/* Dismiss button label */ +"Webview.dismiss" = "Kapat"; + +/* Webview loading error message */ +"Webview.errorMessage" = "İçerik yüklenirken bir hata oluştu. Lütfen tekrar deneyin."; + +/* Updating webview message */ +"Webview.updating" = "Güncelleniyor ..."; + +/* Welcome view body text */ +"Welcome.body" = "Litewallet artık yepyeni bir görünüme ve bazı yeni özelliklere sahip. \n \nTüm paralar lite (ł) olarak gösteriliyor. 1 Litecoin (Ł) = 1000 lite (ł)."; + +/* Welcome view title */ +"Welcome.title" = "Litewallet'e Hoş Geldiniz!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Veritabanını Sil"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Bu cüzdanı silmek istediğinizden emin misiniz?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Cüzdan Silinsin mi?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Veritabanını Sil"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Bu, veritabanını siler, ancak PIN'i ve tümceciği korur. Mevcut PIN kodunuzu onaylayın, tohumlayın ve yeni veritabanına eşitlemeyi tamamlamak için bekleyin"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Sil ve Senkronize Et"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Başlangıç ​​öbeğinizi veya PIN kodunuzu unuttunuz mu?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Cüzdan silinemedi."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Başarısız"; + +/* Enter key to wipe wallet instruction. */ +"WipeWallet.instruction" = "Yeni bir cüzdan başlatmak veya mevcut bir cüzdanı geri yüklemek için, önce şu anda kurulu olan cüzdanı silmelisiniz. Devam etmek için, mevcut cüzdanın Kağıt Anahtarını girin."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Başka bir cüzdanın başlatılması veya kurtarılması, bu cihazdaki farklı bir Litewallet cüzdanına erişmenize ve onu yönetmenize olanak sağlar."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Mevcut cüzdanınız bu cihazdan kaldırılacaktır. İleride onu geri yüklemek isterseniz, Kağıt Anahtarınızı girmeniz gerekecektir."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Başka Bir Cüzdanı Başlatın veya Kurtarın"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Bu eylem Litewallet'ınızı silecek!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Cüzdanınızı silmek, özel anahtarın ve uygulama verilerinin silinmesinin ortadan kalkacağı anlamına gelir. Litecoin'i sonsuza kadar kaybedebilirsiniz!\n\n\nLitewallet ekibindeki hiç kimse bu tohumu sizin için alamaz. Bu uyarıyı dikkate almazsanız sorumlu değiliz."; + +/* Warning title */ +"WipeWallet.warningTitle" = "LÜTFEN OKUYUN!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Sil"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Siliniyor ..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Her kelimeyi sırayla not edin ve güvenli bir yerde saklayın."; + +/* button label */ +"WritePaperPhrase.next" = "Sonraki"; + +/* button label */ +"WritePaperPhrase.previous" = "Önceki"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d/%2$d"; diff --git a/litewallet/Strings/uk.lproj/Localizable.strings b/litewallet/Strings/uk.lproj/Localizable.strings new file mode 100644 index 000000000..048c8a959 --- /dev/null +++ b/litewallet/Strings/uk.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen website label */ +"About.blog" = "веб-сайт"; + +/* About screen footer */ +"About.footer" = "Зроблено командою LiteWallet\of\Litecoin Foundation\n%1$@"; + +/* Privay Policy button label */ +"About.privacy" = "Політика конфіденційності"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "Про"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "Закрити"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "Центр підтримки"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "Завантаження гаманця"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "Мій Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "КЕРУВАТИ"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "Локальна корупційна помилка"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "Ваша локальна база даних пошкоджена. Щоб оновити, перейдіть до Налаштування > Блокчейн: Налаштування > Видалити базу даних"; + +/* Error alert title */ +"Alert.error" = "Помилка"; + +/* No internet alert message */ +"Alert.noInternet" = "Не знайдено підключення до Інтернету. Перевірте підключення та спробуйте ще раз."; + +/* Warning alert title */ +"Alert.warning" = "Увага"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "Адреси скопійовано"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "Усі адреси гаманців успішно скопійовано."; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "Набір паперових ключів"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "Чудово!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN набір"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "Роздільна здатність домену"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "Адреса вирішена!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "Не вдалося надіслати"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "Надіслати підтвердження"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "Гроші надіслані!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JПомилка серіалізації JSON"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "Гаманець не готовий"; + +/* API Token error message */ +"ApiClient.tokenError" = "Не вдається отримати маркер API"; + +/* buy button */ +"Button.buy" = "купити"; + +/* Cancel button label */ +"Button.cancel" = "Скасувати"; + +/* Ignore button label */ +"Button.ignore" = "Ігнорувати"; + +/* menu button */ +"Button.menu" = "меню"; + +/* No button */ +"Button.no" = "Ні"; + +/* OK button label */ +"Button.ok" = "гаразд"; + +/* receive button */ +"Button.receive" = "отримувати"; + +/* resetFields */ +"Button.resetFields" = "Скинути поля"; + +/* send button */ +"Button.send" = "відправити"; + +/* Settings button label */ +"Button.settings" = "Налаштування"; + +/* Settings button label */ +"Button.submit" = "Подати"; + +/* Yes button */ +"Button.yes" = "Так"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "КУПИТИ"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "• Купуйте подарункові карти\n• Поповніть передплачені телефони\n• Steam, Amazon, Hotels.comПрацює в 170 країнах"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "• Змініть Litecoin на інші криптовалюти\n• Ідентифікатор не потрібен\n• Купуйте за допомогою кредитної картки\n• Глобальне покриття"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "Купуйте Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "• Купуйте LTC за допомогою багатьох фіатних пар\n• Сплачуйте кількома способами\n• Глобальний постачальник платежів"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• Отримайте Litecoin за 5 хвилин!\n• Купуйте Litecoin за допомогою кредитної картки\n• Паспорт або посвідчення особи штату"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "Купуйте Litecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "Центруйте свій ідентифікатор у полі"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "Деталі обміну:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "Сума для відправки:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "Сума для пожертвування:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "Час обробки: обробка цих трансакцій займе %1$@ хвилин."; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "Час обробки: обробка цієї транзакції займе %1$@ хвилин."; + +/* Send: (amount) */ +"Confirmation.send" = "Надіслати"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "ЗБОР:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "АДРЕСА:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "Підтвердження"; + +/* To: (address) */ +"Confirmation.to" = "До"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "Загальна вартість:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "Введені слова не відповідають вашому паперовому ключу. Будь ласка спробуйте ще раз."; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "Щоб переконатися, що все було записано правильно, будь ласка, введіть наступні слова з паперового ключа."; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "слово #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "Копія"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "Litecoin Блок дисплея"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "Виберіть:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "Курс валюти"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "Цей пристрій не налаштовано на надсилання електронної пошти за допомогою поштової програми iOS."; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "Електронна пошта недоступна"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "Цей пристрій не налаштовано на надсилання повідомлень."; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "Повідомлення недоступні"; + +/* You can customize your Face ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "Ви можете налаштувати ліміт витрат на Face ID за допомогою %1$@."; + +/* Face ID screen label */ +"FaceIDSettings.label" = "Використовуйте своє обличчя, щоб розблокувати свій Litewallet і надсилати гроші до встановленого ліміту."; + +/* Link Text (see TouchIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "Екран ліміту витрат Face ID"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "Увімкнути Face ID для Litewallet"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "Face ID"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "Ви не налаштували Face ID на цьому пристрої. Перейдіть у Налаштування->Face ID і пароль, щоб налаштувати його зараз."; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "Face ID не налаштовано"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "Ліміт витрат на Face ID"; + +/* Economy fee */ +"FeeSelector.economy" = "Економіка"; + +/* Fee Selector economy fee description */ +"FeeSelector.economyLabel" = "Орієнтовна доставка: більше 10 хвилин"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "Цей параметр не рекомендується для чутливих до часу транзакцій."; + +/* Luxury fee */ +"FeeSelector.luxury" = "Розкіш"; + +/* Fee Selector economy fee description */ +"FeeSelector.luxuryLabel" = "Орієнтовна доставка: 2,5 - 5+ хвилин"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "Ця опція практично гарантує прийняття вашої транзакції, хоча ви платите премію."; + +/* Regular fee */ +"FeeSelector.regular" = "Звичайний"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "Орієнтовна доставка: 2,5 - 5+ хвилин"; + +/* Fee Selector title */ +"FeeSelector.title" = "Швидкість обробки"; + +/* Confirm */ +"Fragment.confirm" = "підтвердити"; + +/* Or */ +"Fragment.or" = "або"; + +/* sorry */ +"Fragment.sorry" = "вибачте"; + +/* to */ +"Fragment.to" = "до"; + +/* History Bar Item Title */ +"History.barItemTitle" = "ІСТОРІЯ"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "Поточне значення LTC в"; + +/* Checking private key balance progress view text */ +"Import.checking" = "Перевірка балансу приватного ключа..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "Надіслати %1$@ з цього приватного ключа у свій гаманець? Мережа Litecoin отримає комісію в розмірі %2$@."; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "Цей приватний ключ уже є у вашому гаманці."; + +/* empty private key error message */ +"Import.Error.empty" = "Цей закритий ключ порожній."; + +/* High fees error message */ +"Import.Error.highFees" = "Комісія за транзакцію буде коштувати більше, ніж кошти, доступні на цьому приватному ключі."; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "Недійсний закритий ключ"; + +/* Import signing error message */ +"Import.Error.signing" = "Помилка підписання транзакції"; + +/* Import button label */ +"Import.importButton" = "Імпорт"; + +/* Importing wallet progress view label */ +"Import.importing" = "Імпорт гаманця"; + +/* Caption for graphics */ +"Import.leftCaption" = "Гаманець для імпорту"; + +/* Import wallet intro screen message */ +"Import.message" = "Імпорт гаманця передає всі гроші з вашого іншого гаманця у ваш гаманець Litewallet за допомогою однієї транзакції."; + +/* Enter password alert view title */ +"Import.password" = "Цей закритий ключ захищений паролем."; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "пароль"; + +/* Caption for graphics */ +"Import.rightCaption" = "Ваш гаманець Litewallet"; + +/* Scan Private key button label */ +"Import.scan" = "Сканувати приватний ключ"; + +/* Import wallet success alert title */ +"Import.success" = "Успіх"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "Гаманець успішно імпортовано."; + +/* Import Wallet screen title */ +"Import.title" = "Імпорт гаманця"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "Ключ розблокування"; + +/* Import wallet intro warning message */ +"Import.warning" = "Імпорт гаманця не включає історію транзакцій чи інші деталі."; + +/* Wrong password alert message */ +"Import.wrongPassword" = "Неправильний пароль, спробуйте ще раз."; + +/* Close app button */ +"JailbreakWarnings.close" = "Закрити"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "Ігнорувати"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "БЕЗПЕКА ПРИСТРОЮ ПОГРУЗАНА\n Будь-який додаток для втечі з в’язниці може отримати доступ до даних брелока Litewallet і вкрасти ваш Litecoin! Негайно зітріть цей гаманець і відновіть його на захищеному пристрої."; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "БЕЗПЕКА ПРИСТРОЮ ПОГІДРЕНО\n Будь-який додаток для втечі з в’язниці може отримати доступ до даних брелока Litewallet і вкрасти ваш Litecoin. Будь ласка, використовуйте Litewallet лише на пристрої без джейлбрейка."; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "УВАГА"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "Витерти"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "Служби локації вимкнені."; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet не має дозволу на доступ до служб локації."; + +/* No comment provided by engineer. */ +"Malformed URI" = "Неправильний URI"; + +/* Balance */ +"ManageWallet.balance" = "Баланс"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "Ви створили свій гаманець на %1$@"; + +/* Manage wallet description text */ +"ManageWallet.description" = "Ім’я вашого гаманця з’являється лише в історії транзакцій вашого облікового запису і нікому не доступне."; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "Назва гаманця"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "Керування гаманцем"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "Купуйте Litecoin"; + +/* Menu button title */ +"MenuButton.customer.support" = "Підтримка клієнтів"; + +/* Menu button title */ +"MenuButton.lock" = "Блокування гаманця"; + +/* Menu button title */ +"MenuButton.security" = "Центр безпеки"; + +/* Menu button title */ +"MenuButton.settings" = "Налаштування"; + +/* Menu button title */ +"MenuButton.support" = "Підтримка"; + +/* button label */ +"MenuViewController.createButton" = "Створити новий гаманець"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "Меню"; + +/* button label */ +"MenuViewController.recoverButton" = "Відновити гаманець"; + +/* No wallet. */ +"No wallet" = "Немає гаманця"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "Перейдіть в автоматичний режим"; + +/* Node is connected label */ +"NodeSelector.connected" = "Підключено"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "Введіть IP-адресу та порт вузла (необов’язково)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "Введіть Node"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "Перейти в ручний режим"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "Поточний первинний вузол"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "Не під'єднано"; + +/* Node status label */ +"NodeSelector.statusLabel" = "Стан підключення вузла"; + +/* Node Selector view title */ +"NodeSelector.title" = "Вузли Litecoin"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "Поганий запит на оплату"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "Непідтримуваний або пошкоджений документ"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "відсутній сертифікат"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "термін дії запиту закінчився"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "Не вдалося здійснити платіж"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "Платежі Litecoin не можуть бути меншими %1$@."; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "Вихід транзакцій Litecoin не може бути меншим $@."; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "непідтримуваний тип підпису"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "ненадійний сертифікат"; + +/* Dismiss button. */ +"Prompts.dismiss" = "ВІДХІДИТИ"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "Торкніться тут, щоб увімкнути Face ID"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "Увімкніть Face ID"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "Для захисту вашого гаманця потрібен пароль пристрою."; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "Увімкніть пароль пристрою"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "Продовжуйте"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "Ваш паперовий ключ необхідно зберігати в безпечному місці. Це єдиний спосіб змінити або відновити свій гаманець Luno або передати біткойн. Будь ласка, запишіть це."; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "Скасувати"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "Увімкнути"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "Потрібна дія"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "Можливо, ваш гаманець не синхронізований. Часто це можна виправити шляхом повторного сканування блокчейну."; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "Транзакція відхилена"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Для Litewallet потрібен 6-значний PIN-код. Будь ласка, встановіть та зберігайте свій PIN-код у безпечному місці."; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "Встановити PIN-код"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "Допоможіть покращити Litewallet, поділившись з нами своїми анонімними даними"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "Поділіться анонімними даними"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "Торкніться тут, щоб увімкнути Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "Увімкнути Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "ОТРИМАТИ"; + +/* Address copied message. */ +"Receive.copied" = "Скопійовано в буфер обміну."; + +/* Share via email button label */ +"Receive.emailButton" = "Електронна пошта"; + +/* Request button label */ +"Receive.request" = "Запит на суму"; + +/* Share button label */ +"Receive.share" = "Поділіться"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "Текстове повідомлення"; + +/* Receive modal title */ +"Receive.title" = "Отримати"; + +/* Done button text */ +"RecoverWallet.done" = "Готово"; + +/* Recover wallet header */ +"RecoverWallet.header" = "Відновити гаманець"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "Скинути PIN-код"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "Введіть ключ паперу"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "Відновіть свій Litewallet за допомогою паперового ключа."; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "Паперовий ключ, який ви ввели, недійсний. Перевірте кожне слово і повторіть спробу."; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "Стрілка вліво"; + +/* Next button label */ +"RecoverWallet.next" = "Далі"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "Торкніться тут для отримання додаткової інформації."; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "Стрілка вправо"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "Введіть паперовий ключ для гаманця, який потрібно відновити."; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "Щоб скинути PIN-код, введіть слова з паперового ключа в поля нижче."; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "Спочатку введіть суму."; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "Запит на суму"; + +/* Alert action button label */ +"ReScan.alertAction" = "Синхронізувати"; + +/* Alert message body */ +"ReScan.alertMessage" = "Ви не зможете надсилати гроші під час синхронізації."; + +/* Alert message title */ +"ReScan.alertTitle" = "Синхронізація з Blockchain?"; + +/* extimated time */ +"ReScan.body1" = "20-45 хвилин"; + +/* Syncing explanation */ +"ReScan.body2" = "Якщо транзакція відображається як завершена в мережі Litecoin, але не у вашому Litewallet."; + +/* Syncing explanation */ +"ReScan.body3" = "Ви неодноразово отримуєте повідомлення про помилку, що ваша транзакція відхилена."; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "Запустити синхронізацію"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "Ви не зможете надсилати гроші під час синхронізації з блокчейном."; + +/* Sync Blockchain view header */ +"ReScan.header" = "Синхронізувати блокчейн"; + +/* Subheader label */ +"ReScan.subheader1" = "Розрахунковий час"; + +/* Subheader label */ +"ReScan.subheader2" = "Коли синхронізувати?"; + +/* Reset walet button title */ +"resetButton" = "Так, скинути гаманець"; + +/* Warning Empty Wipe title */ +"resetTitle" = "Видалити мій Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "Спалах камери"; + +/* Complete filter label */ +"Search.complete" = "завершено"; + +/* Pending filter label */ +"Search.pending" = "на розгляді"; + +/* Received filter label */ +"Search.received" = "отримано"; + +/* Sent filter label */ +"Search.sent" = "надісланий"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "Face ID"; + +/* Security Center Info */ +"SecurityCenter.info" = "Увімкніть усі функції безпеки для максимального захисту."; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "Єдиний спосіб отримати доступ до свого Litecoin, якщо ви втратите або оновите свій телефон."; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "Паперовий ключ"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "Захищає ваш Litewallet від неавторизованих користувачів."; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6-значний PIN-код"; + +/* Security Center Title */ +"SecurityCenter.title" = "Центр безпеки"; + +/* Touch ID/FaceID button description */ +"SecurityCenter.touchIdDescription" = "Зручно розблокуйте свій Litewallet і надсилайте гроші до встановленого ліміту."; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "Сума"; + +/* Balance: $4.00 */ +"Send.balance" = "Баланс: %1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "Комісія: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "НАДІСЛАТИ"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "Перейдіть до Налаштувань, щоб дозволити доступ до камери."; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet не має доступу до камери"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "Пунктом призначення є ваша власна адреса. Ви не можете відправити собі."; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "Не вдалося створити транзакцію."; + +/* Description for sending money label */ +"Send.descriptionLabel" = "Пам'ятка"; + +/* Empty pasteboard error message */ +"Send.emptyPasteboard" = "Картонний картон порожній"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "Введіть адресу Litecoin"; + +/* Fees: $0.01*/ +"Send.fee" = "Збори: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Збори:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "Особа одержувача не підтверджена."; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "Недостатньо коштів"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "Адреса призначення не є дійсною адресою Litecoin."; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "Pasteboard не містить дійсної адреси Litecoin."; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "Недійсна адреса"; + +/* Is rescanning error message */ +"Send.isRescanning" = "Під час повного повторного сканування надсилання вимкнено."; + +/* Loading request activity view message */ +"Send.loadingRequest" = "Запит на завантаження"; + +/* Empty address alert message */ +"Send.noAddress" = "Будь ласка, введіть адресу одержувача."; + +/* Emtpy amount alert message */ +"Send.noAmount" = "Будь ласка, введіть суму для відправки."; + +/* Paste button label */ +"Send.pasteLabel" = "Вставити"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "Не вдалося опублікувати транзакцію."; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "Не вдалося завантажити платіжний запит"; + +/* Scan button label */ +"Send.scanLabel" = "Сканувати"; + +/* Send button label */ +"Send.sendLabel" = "Надіслати"; + +/* Send modal title */ +"Send.title" = "Надіслати"; + +/* Send money to label */ +"Send.toLabel" = "До"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "домен"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "Введіть a"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "Пошук"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "На жаль, домен не знайдено. [Помилка: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "Помилка пошуку"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "Введіть домен .crypto, .wallet, .zil, .nft, .blockchain, \n.bitcoin, .coin, .888, .dao або .x."; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "Введіть домен"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "Проблема пошуку системи. [Помилка: %2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "Адреси Litecoin призначені лише для одноразового використання."; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "Повторне використання знижує конфіденційність як для вас, так і для одержувача і може призвести до втрати, якщо одержувач безпосередньо не контролює адресу."; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "Адреса вже використана"; + +/* About label */ +"Settings.about" = "Про"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "Розширені налаштування"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "Блокчейн"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "Ви впевнені, що хочете змінити мову на %l?"; + +/* Default currency label */ +"Settings.currency" = "Відображення валюти"; + +/* Current Locale */ +"Settings.currentLocale" = "Поточна мова:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "Приєднуйтесь до раннього доступу"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "Вам подобається Litewallet?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "Ліміт витрат на Face ID"; + +/* Import wallet label */ +"Settings.importTitle" = "Імпорт гаманця"; + +/* Languages label */ +"Settings.languages" = "Мови"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "Середовище:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Партнери Litewallet"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Версія Litewallet:"; + +/* Manage settings section header */ +"Settings.manage" = "Керувати"; + +/* Notifications label */ +"Settings.notifications" = "Сповіщення"; + +/* Leave review button label */ +"Settings.review" = "Залиште нам відгук"; + +/* Share anonymous data label */ +"Settings.shareData" = "Поділіться анонімними даними"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "Соціальний"; + +/* Support settings section header */ +"Settings.support" = "Підтримка"; + +/* Sync blockchain label */ +"Settings.sync" = "Синхронізувати блокчейн"; + +/* Settings title */ +"Settings.title" = "Налаштування"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Ліміт витрат Touch ID"; + +/* Wallet Settings section header */ +"Settings.wallet" = "Wallet"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "Запустити/Відновити інший гаманець"; + +/* Share data view body */ +"ShareData.body" = "Допоможіть покращити Litewallet, поділившись з нами своїми анонімними даними. Це не включає фінансову інформацію. Ми поважаємо вашу фінансову конфіденційність."; + +/* Share data header */ +"ShareData.header" = "Поділитися даними?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "Поділитися анонімними даними?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "Поточний ліміт витрат: "; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "Знову запишіть паперовий ключ"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "Ваш паперовий ключ – це єдиний спосіб відновити Litewallet, якщо ваш телефон втрачено, вкрадено, зламано чи оновлено.\n\nМи покажемо вам список слів, які потрібно записати на аркуші паперу та зберегти в безпеці."; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "Запишіть ключ на папері"; + +/* Argument is date */ +"StartPaperPhrase.date" = "Ви востаннє записали свій паперовий ключ %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "Найбезпечніший і найбезпечніший спосіб використання Litecoin."; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "Підтримайте Litecoin Foundation"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "Підключення..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "Повторне сканування..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "Успіх!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "Синхронізація..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "Підключення"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "Синхронізація"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ d"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ h"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ m"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ s"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "Ви можете налаштувати свій ліміт витрат Touch ID з %1$@."; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "Використовуйте відбиток пальця, щоб розблокувати свій Litewallet і надсилати гроші до встановленого ліміту."; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Екран ліміту витрат Touch ID"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "Ліміт витрат: %1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "Увімкнути Touch ID для Litewallet"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "Ви не налаштували Touch ID на цьому пристрої. Перейдіть у Налаштування->Touch ID і пароль, щоб налаштувати його зараз."; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "Touch ID не налаштовано"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "Завжди вимагайте пароль"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "Вам буде запропоновано ввести свій 6-значний PIN-код, щоб надіслати будь-яку транзакцію, яка перевищує ліміт витрат, і кожні 48 годин з моменту останнього введення 6-значного PIN-коду."; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Ліміт витрат Touch ID"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "Інформація про суму:"; + +/* Availability status text */ +"Transaction.available" = "Доступні для витрат"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "Блокувати:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "Пам'ятка:"; + +/* Transaction complete label */ +"Transaction.complete" = "Завершено"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "Інформація про кінцеву суму транзакції"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "Кінцевий баланс: %1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "Курс обміну при отриманні:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "Курс обміну при відправці:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ плата)"; + +/* Invalid transaction */ +"Transaction.invalid" = "НЕДІЙСНИЙ"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "just now"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "прямо зараз: %1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "прямо зараз: %1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "Початковий баланс: %1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "Інформація про початкову суму транзакції"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "Очікування підтвердження. Деякі продавці вимагають підтвердження для завершення транзакції. Орієнтовний час: 1-2 години."; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "рахунок"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "Сума"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "Підтверджено в блоку"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "Пам'ятка"; + +/* Copied */ +"TransactionDetails.copiedAll" = "Скопійовано"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "Скопіюйте всі деталі"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "Тут з’являться ваші транзакції."; + +/* [received] at
(received title 2/2) */ +"TransactionDetails.from" = "на %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "Менше..."; + +/* Moved $5.00 */ +"TransactionDetails.moved" = "Переміщено %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "Переміщено %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "Не підтверджено"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "щодо"; + +/* Received $5.00 (received title 1/2) */ +"TransactionDetails.received" = "Отримано %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "Отримано %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "ОТРИМАТИ АДРЕСУ"; + +/* Sent $5.00 (sent title 1/2) */ +"TransactionDetails.sent" = "Надісланий %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "Надісланий %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "TXID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "Статус"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "Transaction Details"; + +/* [sent] to
(sent title 2/2) */ +"TransactionDetails.to" = "до %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "Ідентифікатор транзакції Litecoin"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "Отримано за цією адресою"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "Надіслано на цю адресу"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "Вимкнено до: %1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "Enter PIN"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "Розблокування за допомогою FaceID"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "Моя адреса"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "Скинути PIN-код"; + +/* Scan button title */ +"UnlockScreen.scan" = "Сканувати"; + +/* TouchID/FaceID prompt text */ +"UnlockScreen.touchIdPrompt" = "Розблокуйте свій Litewallet."; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "Розблокування за допомогою TouchID"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "Гаманець розблоковано"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "Запам’ятайте цей PIN-код. Якщо ви забудете його, ви не зможете отримати доступ до свого Litecoin."; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "Ваш PIN-код буде використано для розблокування вашого Litewallet та надсилання грошей."; + +/* Update PIN title */ +"UpdatePin.createTitle" = "Встановити PIN-код"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "Повторно введіть PIN-код"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "Введіть свій поточний PIN-код."; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "Введіть новий PIN-код."; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "Повторно введіть новий PIN-код."; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "На жаль, не вдалося оновити PIN-код."; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "Помилка оновлення PIN-коду"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "Оновіть PIN-код"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "Скопіювати адреси гаманців у буфер обміну?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "Дозволити копіювати адресу гаманця в буфер обміну"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "Копіювати адреси гаманця"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "Копія"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "Будь ласка, введіть свій PIN-код, щоб авторизувати цю транзакцію."; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "Введіть свій PIN-код, щоб продовжити."; + +/* Verify PIN view title */ +"VerifyPin.title" = "Потрібний PIN-код"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "Авторизуйте цю трансакцію"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "Відкрийте додаток Litewallet для iPhone, щоб налаштувати гаманець."; + +/* Dismiss button label */ +"Webview.dismiss" = "Звільнити"; + +/* Webview loading error message */ +"Webview.errorMessage" = "Під час завантаження вмісту сталася помилка. Будь ласка спробуйте ще раз."; + +/* Updating webview message */ +"Webview.updating" = "Оновлення..."; + +/* Welcome view body text */ +"Welcome.body" = "Тепер Litewallet має абсолютно новий вигляд і деякі нові функції.\n\nУсі монети відображаються в lites (ł). 1 Litecoin (Ł) = 1000 lites (ł)."; + +/* Welcome view title */ +"Welcome.title" = "Ласкаво просимо до Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "Видалити базу даних"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "Ви впевнені, що хочете видалити цей гаманець?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "Витерти гаманець?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "Видалити базу даних"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "Це видаляє базу даних, але зберігає PIN-код і фразу. Підтвердьте свій наявний PIN-код, запустіть і дочекайтеся завершення синхронізації з новою базою даних"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "Видалити та синхронізувати"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "Забули початкову фразу або PIN-код?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "Не вдалося стерти гаманець."; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "Не вдалося"; + +/* Enter key to wipe wallet instruction. */ +"WipeWallet.instruction" = "Щоб запустити новий гаманець або відновити наявний гаманець, спочатку потрібно стерти гаманець, який зараз встановлено. Щоб продовжити, введіть паперовий ключ поточного гаманця."; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "Запуск або відновлення іншого гаманця дає змогу отримати доступ до іншого гаманця Litewallet і керувати ним на цьому пристрої."; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "Ваш поточний гаманець буде видалено з цього пристрою. Якщо ви хочете відновити його в майбутньому, вам потрібно буде ввести свій паперовий ключ."; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "Запустіть або відновіть інший гаманець"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "Ця дія видалить ваш Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "Видалення гаманця означає, що приватний ключ і стирання даних програми не буде. Ви можете втратити свій Litecoin назавжди! \n\n\nНіхто з команди Litewallet не може отримати це початкове значення для вас. Ми не несемо відповідальності, якщо ви не зважите на це попередження."; + +/* Warning title */ +"WipeWallet.warningTitle" = "БУДЬ ЛАСКА, ПРОЧИТАЙТЕ!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "Витерти"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "Витирання..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "Запишіть кожне слово по порядку і зберігайте його в надійному місці."; + +/* button label */ +"WritePaperPhrase.next" = "Далі"; + +/* button label */ +"WritePaperPhrase.previous" = "Попередній"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d з %2$d"; + +/* "Email address label" */ +"Notifications.emailLabel" = ""; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = ""; + +/* "Email title" */ +"Notifications.emailTitle" = ""; + +/* "Language preference label" */ +"Notifications.languagePreference" = ""; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = ""; + +/* Signup cancel */ +"Notifications.signupCancel" = ""; + +/* Network */ +"Send.networkFee" = ""; + +/* Service */ +"Send.serviceFee" = ""; diff --git a/litewallet/Strings/zh-Hans.lproj/Localizable.strings b/litewallet/Strings/zh-Hans.lproj/Localizable.strings new file mode 100755 index 000000000..65eadc5aa --- /dev/null +++ b/litewallet/Strings/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "博客"; + +/* About screen footer */ +"About.footer" = "由全球 Litewallet 团队制作。版本%1$@"; + +/* Privay Policy button label */ +"About.privacy" = "隐私政策"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "关于"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "关闭"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "支持中心"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "正在加载钱包"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "我的 Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "管理"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "本地腐败错误"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "您的本地数据库已损坏。转到设置>区块链:设置>删除数据库以刷新"; + +/* Error alert title */ +"Alert.error" = "错误"; + +/* No internet alert message */ +"Alert.noInternet" = "未发现互联网连接。检查您的连接并重试。"; + +/* Warning alert title */ +"Alert.warning" = "警告"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "地址已复制"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "所有的钱包地址已被成功复制。"; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "纸上密钥已设置"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "太棒了!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN 已设置"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "域解析"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "您的地址已解决!"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "发送失败"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "发送确认"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "钱已发送!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON 串行化错误"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "钱包尚未备好"; + +/* API Token error message */ +"ApiClient.tokenError" = "无法检索 API 令牌"; + +/* buy button */ +"Button.buy" = "购买"; + +/* Cancel button label */ +"Button.cancel" = "取消"; + +/* Ignore button label */ +"Button.ignore" = "忽略"; + +/* menu button */ +"Button.menu" = "菜单"; + +/* No button */ +"Button.no" = "不"; + +/* OK button label */ +"Button.ok" = "好"; + +/* receive button */ +"Button.receive" = "收到"; + +/* resetFields */ +"Button.resetFields" = "重新设置领域"; + +/* send button */ +"Button.send" = "发送"; + +/* Settings button label */ +"Button.settings" = "设置"; + +/* Settings button label */ +"Button.submit" = "提交"; + +/* Yes button */ +"Button.yes" = "是"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "购买"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "•购买礼品卡 \n •为预付费电话充值 \n •Steam,Amazon,Hotels.com \n •在170个国家/地区工作"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "•将Litecoin更改为其他加密货币 \n •无需ID \n •通过信用卡购买 \n •全球覆盖"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "购买 Łitecoin"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "•购买具有许多法定货币对的LTC\n•以多种方式支付\n•全球支付提供商"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "• 5分钟内获得Litecoin!\n •通过信用卡购买Litecoin \n •护照或州ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "购买 Łitecoin"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "将您的身份证号集中在方框中"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "交易所详情:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "发送金额:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "捐款金额"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "处理时间:这些交易将花费 %1$@ 分钟来处理。"; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "处理时间:此交易需要 %1$@ 分钟以进行处理。"; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "发送"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "费用"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "地址"; + +/* Confirmation Screen title */ +"Confirmation.title" = "确认"; + +/* To: (address) */ +"Confirmation.to" = "至"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "费用总计:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "输入的单词与您的纸键不匹配。请再试一遍。"; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "为确保一切均被正确地书写,请在您的纸键输入以下文字。"; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "单词#%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "复制"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "莱特币显示单位"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "选择:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "汇率"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "此设备未进行配置,无法用 iOS 邮件应用发送电子邮件。"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "电子邮箱不可用"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "本设备未被配置来发送消息。"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "消息发送不可用"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "经济"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "预计交付时间:10 分钟以上"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "对于有时效性的交易,不推荐使用此选项。"; + +/* Luxury fee */ +"FeeSelector.luxury" = "豪华"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "预计送达时间:2.5 - 5 分钟"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "尽管您要支付一定的费用,但此选项实际上可以保证您接受交易。"; + +/* Regular fee */ +"FeeSelector.regular" = "常规"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "预计送达时间:2.5 - 5+ 分钟"; + +/* Fee Selector title */ +"FeeSelector.title" = "处理速度"; + +/* Confirm */ +"Fragment.confirm" = "确认"; + +/* Or */ +"Fragment.or" = "要么"; + +/* sorry */ +"Fragment.sorry" = "对不起"; + +/* to */ +"Fragment.to" = "到"; + +/* History Bar Item Title */ +"History.barItemTitle" = "历史"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "当前的LTC值"; + +/* Checking private key balance progress view text */ +"Import.checking" = "正在查看私人密钥余额..."; + +/* Sweep private key confirmation message */ +"Import.confirm" = "从这个私人密钥中发送 %1$@ 到您的钱包吗?莱特币网络将收到 %2$@ 费用。"; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "本私钥已在您的钱包里。"; + +/* empty private key error message */ +"Import.Error.empty" = "本私钥为空。"; + +/* High fees error message */ +"Import.Error.highFees" = "交易费将超过本私钥的可用资金。"; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "并非有效私钥"; + +/* Import signing error message */ +"Import.Error.signing" = "错误签名交易"; + +/* Import button label */ +"Import.importButton" = "导入"; + +/* Importing wallet progress view label */ +"Import.importing" = "导入钱包"; + +/* Caption for graphics */ +"Import.leftCaption" = "待导入钱包"; + +/* Import wallet intro screen message */ +"Import.message" = "导入钱包,利用单笔交易将您其他钱包里的所有钱转移到您的 Litewallet 钱包。"; + +/* Enter password alert view title */ +"Import.password" = "本私钥受到密码保护。"; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "密码"; + +/* Caption for graphics */ +"Import.rightCaption" = "您的 Litewallet 钱包"; + +/* Scan Private key button label */ +"Import.scan" = "扫描私钥"; + +/* Import wallet success alert title */ +"Import.success" = "成功"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "成功导入钱包。"; + +/* Import Wallet screen title */ +"Import.title" = "导入钱包"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "解锁键"; + +/* Import wallet intro warning message */ +"Import.warning" = "导入钱包不包括交易记录或其他细节。"; + +/* Wrong password alert message */ +"Import.wrongPassword" = "密码错误,请重试。"; + +/* Close app button */ +"JailbreakWarnings.close" = "关闭"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "忽略"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "设备安全性受损\n任何'越狱'应用都可能访问 Litewallet 的密钥链数据并盗取您的莱特币!立即擦除这个钱包并在一台安全设备上进行恢复。"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "设备安全性受损\n任何'越狱'应用都可能访问 Litewallet 的密钥链数据并盗取您的莱特币!请您仅在未越狱设备上使用 Litewallet 。"; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "警告"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "擦除"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "位置服务被禁用。"; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet 无权限访问位置服务。"; + +/* No comment provided by engineer. */ +"Malformed URI" = "URI格式错误"; + +/* Balance */ +"ManageWallet.balance" = "平衡"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "您在%1$@上创建了您的钱包"; + +/* Manage wallet description text */ +"ManageWallet.description" = "您的钱包名称仅出现在您的帐户交易记录中,其他任何人无法看到。"; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "钱包名称"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "管理钱包"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "购买莱特币"; + +/* Menu button title */ +"MenuButton.customer.support" = "客户支持"; + +/* Menu button title */ +"MenuButton.lock" = "锁住钱包"; + +/* Menu button title */ +"MenuButton.security" = "安全中心"; + +/* Menu button title */ +"MenuButton.settings" = "设置"; + +/* Menu button title */ +"MenuButton.support" = "支持"; + +/* button label */ +"MenuViewController.createButton" = "创建新的钱包"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "菜单"; + +/* button label */ +"MenuViewController.recoverButton" = "恢复钱包"; + +/* No comment provided by engineer. */ +"No wallet" = "没有钱包"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "切换到自动模式"; + +/* Node is connected label */ +"NodeSelector.connected" = "已连接"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "输入节点 IP 地址和端口(可选)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "输入节点"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "切换到手动模式"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "当前主要节点"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "未连接"; + +/* Node status label */ +"NodeSelector.statusLabel" = "节点连接状态"; + +/* Node Selector view title */ +"NodeSelector.title" = "莱特币节点"; + +/* "Email address label" */ +"Notifications.emailLabel" = "电子邮件地址"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "在这里输入"; + +/* "Email title" */ +"Notifications.emailTitle" = "不要错过一件事!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "首选语言:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "注册以了解更新和竞赛。"; + +/* Signup cancel */ +"Notifications.signupCancel" = "不,谢谢。"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "假的付款请求"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "不受支持或受损的文档"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "缺失的证书"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "请求已过期"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "无法付款"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "莱特币支付不能小于%1$@。"; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "莱特币交易输出不能小于 $@。"; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "不受支持的签名类型"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "不可信证书"; + +/* Dismiss button. */ +"Prompts.dismiss" = "DISMISS"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "需要设备密码才能保护您的钱包。转到设置并开启密码。"; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "开启设备密码"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "继续"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "为了应对手机丢失或者更换, 请您务必写下你的纸键。点击此处继续。"; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "取消"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "启用"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "需要采取的行动"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "您的钱包可能不同步。通常可通过重新扫描区块链来修复。"; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "交易被拒"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet 已升级到使用 6 位 PIN。点击此处升级。"; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "升级 PIN"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "与我们分享您的匿名数据来帮助改善 Litewallet。"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "分享匿名数据"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "点击此处以启用触摸 ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "启用触摸 ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "接收"; + +/* Address copied message. */ +"Receive.copied" = "已复制到剪贴板。"; + +/* Share via email button label */ +"Receive.emailButton" = "电子邮件"; + +/* Request button label */ +"Receive.request" = "申请金额"; + +/* Share button label */ +"Receive.share" = "分享"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "短信"; + +/* Receive modal title */ +"Receive.title" = "接收"; + +/* Done button text */ +"RecoverWallet.done" = "完成"; + +/* Recover wallet header */ +"RecoverWallet.header" = "恢复钱包"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "复位 PIN"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "输入纸键"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "用您的纸键恢复您的 Litewallet。"; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "您输入的纸键无效。请仔细检查每个单词并重试。"; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "左箭头键"; + +/* Next button label */ +"RecoverWallet.next" = "下一个"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "点击此处以获取更多信息。"; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "右箭头键"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "输入您想恢复的钱包的纸键。"; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "要重置 PIN 码,请在下面的框中输入纸上密钥中的单词。"; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "请先输入金额。"; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "申请金额"; + +/* Alert action button label */ +"ReScan.alertAction" = "同步"; + +/* Alert message body */ +"ReScan.alertMessage" = "您不能在同步的同时寄钱。"; + +/* Alert message title */ +"ReScan.alertTitle" = "与区块链同步?"; + +/* extimated time */ +"ReScan.body1" = "20-45 分钟"; + +/* Syncing explanation */ +"ReScan.body2" = "如果某次交易在莱特币网络上显示为已完成,却不在您的 Litewallet 里。"; + +/* Syncing explanation */ +"ReScan.body3" = "你反复得到交易被拒绝的出错信息。"; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "开始同步"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "您不能在与区块链同步的同时寄钱。"; + +/* Sync Blockchain view header */ +"ReScan.header" = "同步区块链"; + +/* Subheader label */ +"ReScan.subheader1" = "估计时间"; + +/* Subheader label */ +"ReScan.subheader2" = "何时同步?"; + +/* Reset walet button title */ +"resetButton" = "是的,重置钱包"; + +/* Warning Empty Wipe title */ +"resetTitle" = "删除我的 Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "相机闪光灯"; + +/* Complete filter label */ +"Search.complete" = "完成"; + +/* Pending filter label */ +"Search.pending" = "待定"; + +/* Received filter label */ +"Search.received" = "已收到"; + +/* Sent filter label */ +"Search.sent" = "已发送"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "启用所有安全功能以获得最大保护。"; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "若您丢失或升级您的手机,访问您的莱特币的唯一方法。"; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "纸键"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "保护您的 Litewallet 免遭未授权用户盗用。"; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6 位 PIN"; + +/* Security Center Title */ +"SecurityCenter.title" = "安全中心"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "方便地解锁 Litewallet,在设定限额下汇钱。"; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "触摸 ID"; + +/* Send money amount label */ +"Send.amountLabel" = "金额"; + +/* Balance: $4.00 */ +"Send.balance" = "余额:%1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "费用: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "发送"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "前往“设置”以允许访问照相机。"; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet 不允许访问照相机"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "目标是您自己的地址。您不能发送给您自己。"; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "无法创建交易。"; + +/* Description for sending money label */ +"Send.descriptionLabel" = "备忘录"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "粘贴板为空"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "输入一个Litecoin地址"; + +/* Fees: $0.01*/ +"Send.fee" = "费用: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "费用:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "收款人身份未经认证。"; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "不充足的资金"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "目标地址不是有效的莱特币地址。"; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "粘贴板中没有有效的莱特币地址"; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "无效地址"; + +/* Is rescanning error message */ +"Send.isRescanning" = "完全扫描期间发送功能已被禁用。"; + +/* Loading request activity view message */ +"Send.loadingRequest" = "加载请求中"; + +/* Network */ +"Send.networkFee" = "网络"; + +/* Empty address alert message */ +"Send.noAddress" = "请输入收件人地址。"; + +/* Emtpy amount alert message */ +"Send.noAmount" = "请输入汇款金额。"; + +/* Paste button label */ +"Send.pasteLabel" = "粘贴"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "无法发布交易。"; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "无法加载支付请求"; + +/* Scan button label */ +"Send.scanLabel" = "扫描"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "发送"; + +/* Service */ +"Send.serviceFee" = "服务"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "发送"; + +/* Send money to label */ +"Send.toLabel" = "至"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "领域"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "进入一个"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "抬头"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "抱歉,找不到域。 [错误: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "查找失败"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "输入 .crypto、.wallet、.zil、.nft、.blockchain、.bitcoin、.coin、.888、.dao 或 .x 域。"; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "输入域"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "系统查找问题。 [错误:%2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "莱特币地址仅供一次性使用。"; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "重复使用会削弱您和收件人的隐私,如果收件人不直接控制地址,可能会导致损失。"; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "地址已被使用"; + +/* About label */ +"Settings.about" = "关于"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "高级设置"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "区块链"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "您确定要更改语言吗?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "显示币种"; + +/* Current Locale */ +"Settings.currentLocale" = "当前语言环境:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "加入早期访问"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "您喜欢 Litewallet 吗?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "导入钱包"; + +/* Languages label */ +"Settings.languages" = "语言能力"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "环境:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet合作伙伴"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet版本:"; + +/* Manage settings section header */ +"Settings.manage" = "管理"; + +/* Notifications label */ +"Settings.notifications" = "通知"; + +/* Leave review button label */ +"Settings.review" = "给我们评论"; + +/* Share anonymous data label */ +"Settings.shareData" = "分享匿名数据"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "社会的"; + +/* Support settings section header */ +"Settings.support" = "支持"; + +/* Sync blockchain label */ +"Settings.sync" = "同步区块链"; + +/* Settings title */ +"Settings.title" = "设置"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "触摸 ID 支出限制"; + +/* Wallet Settings section header */ +"Settings.wallet" = "钱包"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "开始/恢复另一钱包"; + +/* Share data view body */ +"ShareData.body" = "同我们分享您的匿名数据来帮助改善 Litewallet。这不包括任何财务信息。我们尊重您的财务隐私。"; + +/* Share data header */ +"ShareData.header" = "共享数据?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "共享匿名数据?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "当前支出限额:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "再次书写纸键"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "如果您的手机丢失、被盗、损坏或升级,纸上密钥是恢复 Litewallet 的唯一方法。\n\n\n我们将向您显示一张单词表,请将其写在纸上并保存在安全的地方。"; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "写下纸键"; + +/* Argument is date */ +"StartPaperPhrase.date" = "您最后一次记录纸上密钥是在 %1$@"; + +/* Start view tagline */ +"StartViewController.tagline" = "使用莱特币最安全且最可靠的方式。"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "支持Litecoin基金会"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "正在连线..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "重新扫描中..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "成功!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "同步中..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "连接中"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "同步中"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ 天"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ 小时"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ 分钟"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ 秒"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "你可以自定义从 %1$@ 的 Touch ID 支出限额。"; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "请用指纹解锁 Litewallet 并在设定限额下汇钱。"; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Touch ID 支出限额屏幕"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "支出限制:%1$@ (%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "针对 Litewallet 启用触摸 ID"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "触摸 ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "您尚未在本设备上设置触摸 ID。现在前往设置 -> 触摸 ID和密码进行设置。"; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "触摸 ID 未设置"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "始终需要密码"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "如需发送超过您支出金额限制的任何交易,您将需要输入您的 6 位数 PIN 码。在您输入 6 位数 PIN 码后每 48 小时您都需要重新输入该 PIN 码。"; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "触摸 ID 支出限制"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "金额明细:"; + +/* Availability status text */ +"Transaction.available" = "可供支出"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "块:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "备忘录:"; + +/* Transaction complete label */ +"Transaction.complete" = "完成"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "交易结束金额明细"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "期末余额:%1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "接收时汇率:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "发送时汇率:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ 费用)"; + +/* Invalid transaction */ +"Transaction.invalid" = "无效"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "刚刚"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "进行中:%1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "进行中:%1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "期初余额:%1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "交易开始金额明细"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "等待确认中。部分商家需要进行确认以完成交易。预估时间:1-2 小时。"; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "账户"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "金额"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "已在区块中确认"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "备忘录"; + +/* Copied */ +"TransactionDetails.copiedAll" = "复制的"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "复制所有详细信息"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "您的交易将显示在这里。"; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "在 %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "减"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "移出 %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "已移动 %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "未确认"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "作为"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "已接收 %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "已收到 %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "接收地址"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "已发送 %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "已发送 %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "状态"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "交易详情"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "到 %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "莱特币交易 ID"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "已在此地址接收"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "已汇至此地址"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "在此日期前禁用:%1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "输入密码"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "使用FaceID解锁"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "我的地址"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "复位 PIN"; + +/* Scan button title */ +"UnlockScreen.scan" = "扫描"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "解锁您的 Litewallet。"; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "使用 TouchID 解锁"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "钱包已解锁"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "请牢记此 PIN 码。如果您忘记,您将无法访问您的莱特币。"; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "您的 PIN 码将用于解锁您的 Litewallet 并发送款项。"; + +/* Update PIN title */ +"UpdatePin.createTitle" = "设置 PIN 码"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "重新输入 PIN 码"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "输入您当前的 PIN 码。"; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "输入您的新 PIN 码。"; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "重新输入您的新 PIN 码。"; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "抱歉,无法更新 PIN 码。"; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "更新 PIN 码错误"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "更新 PIN 码"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "是否拷贝钱包地址到剪贴板?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "授权以拷贝钱包地址到剪贴板"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "拷贝钱包地址"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "拷贝"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "请输入您的个人识别码(PIN)以便授权此交易。"; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "请输入您的 PIN 码以继续。"; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN 码为必填项"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "授权此交易"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "打开 Litewallet iPhone app 以设置您的钱包。"; + +/* Dismiss button label */ +"Webview.dismiss" = "忽略"; + +/* Webview loading error message */ +"Webview.errorMessage" = "载入内容时发生错误。请重试。"; + +/* Updating webview message */ +"Webview.updating" = "正在更新..."; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "欢迎使用Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "欢迎使用Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "删除数据库"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "您确认要删除此钱包吗?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "擦除钱包吗?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "删除数据库"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "这将删除数据库,但保留PIN和短语。确认您现有的PIN码,种子并等待同步到新数据库"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "删除并同步"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "忘记您的种子词组或PIN吗?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "擦除钱包失败。"; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "失败"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "输入此钱包的恢复短语以擦除并启动或恢复另一钱包。您的当前余额将保留。"; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "开始或恢复另一个钱包能让你在这个设备上管理和使用不同的Litewallet钱包"; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "您无法再在这个设备上使用现在的Litewallet钱包。 现在的一切资金都留在纸键上。"; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "启动或恢复另一钱包"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "此操作将擦除您的 Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "删除你的钱包意味着私钥和擦除应用程序数据将消失。你可能永远失去莱特币!\n\n\nLitewallet 团队中的任何人都无法为您检索此种子。如果您不注意此警告,我们概不负责。"; + +/* Warning title */ +"WipeWallet.warningTitle" = "请阅读!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "擦除"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "正在擦除..."; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "按顺序写下每个词并保存到安全位置。"; + +/* button label */ +"WritePaperPhrase.next" = "下一个"; + +/* button label */ +"WritePaperPhrase.previous" = "上一步"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%1$d / %2$d"; diff --git a/litewallet/Strings/zh-Hant.lproj/Localizable.strings b/litewallet/Strings/zh-Hant.lproj/Localizable.strings new file mode 100755 index 000000000..8ff9890be --- /dev/null +++ b/litewallet/Strings/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,1292 @@ +/* About screen blog label */ +"About.blog" = "部落格"; + +/* About screen footer */ +"About.footer" = "由全球 Litewallet 團隊製作。版本 %1$@"; + +/* Privay Policy button label */ +"About.privacy" = "隱私政策"; + +/* About screen reddit label */ +"About.reddit" = "Reddit"; + +/* About screen title */ +"About.title" = "關於"; + +/* About screen twitter label */ +"About.twitter" = "Twitter"; + +/* Close modal button accessibility label */ +"AccessibilityLabels.close" = "關閉"; + +/* Support center accessibiliy label */ +"AccessibilityLabels.faq" = "支援中心"; + +/* Loading Wallet Message */ +"Account.loadingMessage" = "正在載入電子錢包"; + +/* Default wallet name */ +"AccountHeader.defaultWalletName" = "我的 Litewallet"; + +/* Manage wallet button title */ +"AccountHeader.manageButtonName" = "管理"; + +/* Corruption Error alert title */ +"Alert.corruptionError" = "本地腐敗錯誤"; + +/* Corruption Error alert title */ +"Alert.corruptionMessage" = "您的本地數據庫已損壞。轉到設置>區塊鏈:設置>刪除數據庫以刷新"; + +/* Error alert title */ +"Alert.error" = "錯誤"; + +/* No internet alert message */ +"Alert.noInternet" = "找不到網路連線。請檢查您的連線,然後再試一次。"; + +/* Warning alert title */ +"Alert.warning" = "警告"; + +/* 'the addresses were copied'' Alert title */ +"Alerts.copiedAddressesHeader" = "已複製位址"; + +/* Addresses Copied Alert sub header */ +"Alerts.copiedAddressesSubheader" = "已成功複製所有的電子錢包位址。"; + +/* Alert Header Label (the paper key was set) */ +"Alerts.paperKeySet" = "紙上金鑰已設定"; + +/* Alert Subheader label (playfully positive) */ +"Alerts.paperKeySetSubheader" = "太棒了!"; + +/* Alert Header label (the PIN was set) */ +"Alerts.pinSet" = "PIN 已設定"; + +/* Resolved Success */ +"Alerts.resolvedSuccess" = "域解析"; + +/* No comment provided by engineer. */ +"Alerts.resolvedSuccessSubheader" = "您的地址已解決"; + +/* Send failure alert header label (the send failed to happen) */ +"Alerts.sendFailure" = "寄出失敗"; + +/* Send success alert header label (confirmation that the send happened) */ +"Alerts.sendSuccess" = "寄出確認"; + +/* Send success alert subheader label (e.g. the money was sent) */ +"Alerts.sendSuccessSubheader" = "錢已寄出!"; + +/* JSON Serialization error message */ +"ApiClient.jsonError" = "JSON 序列化錯誤"; + +/* Wallet not ready error message */ +"ApiClient.notReady" = "電子錢包尚未就緒"; + +/* API Token error message */ +"ApiClient.tokenError" = "無法擷取 API 權杖"; + +/* buy button */ +"Button.buy" = "購買"; + +/* Cancel button label */ +"Button.cancel" = "取消"; + +/* Ignore button label */ +"Button.ignore" = "略過"; + +/* menu button */ +"Button.menu" = "選單"; + +/* No button */ +"Button.no" = "不要"; + +/* OK button label */ +"Button.ok" = "確定"; + +/* receive button */ +"Button.receive" = "接收"; + +/* resetFields */ +"Button.resetFields" = "resetFields"; + +/* send button */ +"Button.send" = "寄出"; + +/* Settings button label */ +"Button.settings" = "設定"; + +/* Settings button label */ +"Button.submit" = "提交"; + +/* Yes button */ +"Button.yes" = "要"; + +/* Buy Bar Item Title */ +"BuyCenter.barItemTitle" = "購買"; + +/* Bitrefill buy financial details */ +"BuyCenter.bitrefillFinancialDetails" = "•購買禮品卡 \n •為預付費電話充值 \n •Steam,Amazon,Hotels.com \n •在170個國家/地區工作"; + +/* Bitrefill Title */ +"BuyCenter.BitrefillTitle" = "Bitrefill"; + +/* Changelly buy financial details */ +"BuyCenter.changellyFinancialDetails" = "•將Litecoin更改為其他加密貨幣 \n •無需ID \n •通過信用卡購買 \n •全球覆蓋"; + +/* Changelly Title */ +"BuyCenter.changellyTitle" = "Changelly"; + +/* Buy Modal Title */ +"BuyCenter.ModalTitle" = "購買萊特幣"; + +/* Moonpay buy financial details */ +"BuyCenter.moonpayFinancialDetails" = "•購買具有許多法定貨幣對的LTC\n•以多種方式支付\n•全球支付提供商"; + +/* Moonpay Title */ +"BuyCenter.moonpayTitle" = "Moonpay"; + +/* Simplex buy financial details */ +"BuyCenter.simplexFinancialDetails" = "•5分鐘內獲得Litecoin!\n •通過信用卡購買Litecoin \n •護照或州ID"; + +/* Simplex Title */ +"BuyCenter.simplexTitle" = "Simplex"; + +/* Buy Center Title */ +"BuyCenter.title" = "購買萊特幣"; + +/* Camera plugin instruction */ +"CameraPlugin.centerInstruction" = "將您的 ID 在方塊置中"; + +/* $53.09/L + 1.07% */ +"Confirmation.amountDetailLabel" = "交易所詳情:"; + +/* Amount to Send: ($1.00) */ +"Confirmation.amountLabel" = "寄送金額:"; + +/* Amount to Donate: ($1.00) */ +"Confirmation.donateLabel" = "捐款金額:"; + +/* eg. Processing with Donation time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingAndDonationTime" = "處理時間:這些交易將花費 %1$@ 分鐘來處理。"; + +/* eg. Processing time: This transaction will take 10-30 minutes to process. */ +"Confirmation.processingTime" = "處理時間:此交易將花費 %1$@ 分鐘進行處理。"; + +/* Label for "send" button on final confirmation screen */ +"Confirmation.send" = "寄送"; + +/* Short Network Fee: ($1.00) */ +"Confirmation.shortFeeLabel" = "費用:"; + +/* Address label */ +"Confirmation.staticAddressLabel" = "地址:"; + +/* Confirmation Screen title */ +"Confirmation.title" = "確認"; + +/* To: (address) */ +"Confirmation.to" = "給"; + +/* Total Cost: ($5.00) */ +"Confirmation.totalLabel" = "總計:"; + +/* Confirm paper phrase error message */ +"ConfirmPaperPhrase.error" = "輸入的單詞與您的紙鍵不匹配。請再試一遍。"; + +/* Confirm paper phrase view label. */ +"ConfirmPaperPhrase.label" = "要確保每項資料都正確寫下,請從紙上金鑰輸入下列字組。"; + +/* Word label eg. Word #1, Word #2 */ +"ConfirmPaperPhrase.word" = "字組 #%1$@"; + +/* No comment provided by engineer. */ +"Copy" = "複製"; + +/* Litecoin denomination picker label */ +"DefaultCurrency.bitcoinLabel" = "萊特幣顯示單位"; + +/* Label to pick fiat */ +"DefaultCurrency.chooseFiatLabel" = "選擇:"; + +/* Exchange rate label */ +"DefaultCurrency.rateLabel" = "匯率"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableMessage" = "本裝置並非設定來以 iOS 郵件應用程式傳送電子郵件。"; + +/* Email unavailable alert title */ +"ErrorMessages.emailUnavailableTitle" = "無法使用電子郵件"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableMessage" = "本裝置並非設定來傳送訊息。"; + +/* Messaging unavailable alert title */ +"ErrorMessages.messagingUnavailableTitle" = "無法使用傳訊"; + +/* You can customize your Face ID Spending Limit from the [FaceIdSettings.linkText gets added here as a button] */ +"FaceIDSettings.customizeText" = "PLACEHOLDER"; + +/* Face Id screen label */ +"FaceIDSettings.label" = "PLACEHOLDER"; + +/* Link Text (see FaceIdSettings.customizeText) */ +"FaceIDSettings.linkText" = "PLACEHOLDER"; + +/* Face id switch label. */ +"FaceIDSettings.switchLabel" = "PLACEHOLDER"; + +/* Face ID settings view title */ +"FaceIDSettings.title" = "PLACEHOLDER"; + +/* Face ID unavailable alert message */ +"FaceIDSettings.unavailableAlertMessage" = "PLACEHOLDER"; + +/* Face ID unavailable alert title */ +"FaceIDSettings.unavailableAlertTitle" = "PLACEHOLDER"; + +/* Face Id spending limit screen title */ +"FaceIDSpendingLimit.title" = "PLACEHOLDER"; + +/* Economy fee */ +"FeeSelector.economy" = "經濟"; + +/* Fee Selector economly fee description */ +"FeeSelector.economyLabel" = "預估送達:10 幾分鐘"; + +/* Warning message for economy fee */ +"FeeSelector.economyWarning" = "此選項不建議用於時效性的交易。"; + +/* Luxury fee */ +"FeeSelector.luxury" = "豪華"; + +/* Fee Selector luxury fee description */ +"FeeSelector.luxuryLabel" = "預計送達時間:2.5 - 5 分鐘"; + +/* Message for luxury fee */ +"FeeSelector.luxuryMessage" = "儘管您要支付一定的費用,但此選項實際上可以保證您接受交易。"; + +/* Regular fee */ +"FeeSelector.regular" = "標準"; + +/* Fee Selector regular fee description */ +"FeeSelector.regularLabel" = "預計送達時間:2.5 - 5+ 分鐘"; + +/* Fee Selector title */ +"FeeSelector.title" = "處理速度"; + +/* Confirm */ +"Fragment.confirm" = "確認"; + +/* Or */ +"Fragment.or" = "要么"; + +/* sorry */ +"Fragment.sorry" = "對不起"; + +/* to */ +"Fragment.to" = "到"; + +/* History Bar Item Title */ +"History.barItemTitle" = "歷史"; + +/* History Current Litecoin Value */ +"History.currentLitecoinValue" = "當前的LTC值"; + +/* Checking private key balance progress view text */ +"Import.checking" = "正在檢查私密金鑰餘額…"; + +/* Sweep private key confirmation message */ +"Import.confirm" = "要從這個私密金鑰寄出 %1$@ 到您的電子錢包嗎?萊特幣網路會收到 %2$@ 的費用。"; + +/* Duplicate key error message */ +"Import.Error.duplicate" = "此私密金鑰已經在您的電子錢包了。"; + +/* empty private key error message */ +"Import.Error.empty" = "此私密金鑰是空白的。"; + +/* High fees error message */ +"Import.Error.highFees" = "交易手續費可能會超過此私密金鑰上的可用資金。"; + +/* Not a valid private key error message */ +"Import.Error.notValid" = "不是有效的私密金鑰"; + +/* Import signing error message */ +"Import.Error.signing" = "簽署交易出錯"; + +/* Import button label */ +"Import.importButton" = "匯入"; + +/* Importing wallet progress view label */ +"Import.importing" = "正在匯入電子錢包"; + +/* Caption for graphics */ +"Import.leftCaption" = "待匯入的電子錢包"; + +/* Import wallet intro screen message */ +"Import.message" = "匯入一個電子錢包會以單一交易的方式將您其它電子錢包的所有資金轉帳至您的 Litewallet 錢包。"; + +/* Enter password alert view title */ +"Import.password" = "此私密金鑰受密碼保護。"; + +/* password textfield placeholder */ +"Import.passwordPlaceholder" = "密碼"; + +/* Caption for graphics */ +"Import.rightCaption" = "您的 Litewallet 錢包"; + +/* Scan Private key button label */ +"Import.scan" = "掃描私密金鑰"; + +/* Import wallet success alert title */ +"Import.success" = "成功"; + +/* Successfully imported wallet message body */ +"Import.SuccessBody" = "已成功匯入電子錢包。"; + +/* Import Wallet screen title */ +"Import.title" = "匯入電子錢包"; + +/* Unlocking Private key activity view message. */ +"Import.unlockingActivity" = "正在解除鎖定金鑰"; + +/* Import wallet intro warning message */ +"Import.warning" = "匯入電子錢包並不包括交易記錄或其它資料。"; + +/* Wrong password alert message */ +"Import.wrongPassword" = "密碼錯誤,請重試一次。"; + +/* Close app button */ +"JailbreakWarnings.close" = "關閉"; + +/* Ignore jailbreak warning button */ +"JailbreakWarnings.ignore" = "略過"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithBalance" = "裝置安全性遭破解\n 任何「越獄」應用程式都可以存取 Litewallet 的鑰匙圈資料、 竊取您的萊特幣!立即抹除這個錢包,在安全的裝置上還原。"; + +/* Jailbreak warning message */ +"JailbreakWarnings.messageWithoutBalance" = "裝置安全性遭破解\n 任何「越獄」應用程式都可以存取 Litewallet 的鑰匙圈資料、 竊取您的萊特幣。請只在未越獄的裝置上使用 Litewallet。"; + +/* Jailbreak warning title */ +"JailbreakWarnings.title" = "警告"; + +/* Wipe wallet button */ +"JailbreakWarnings.wipe" = "抹除"; + +/* Litewallet name */ +"Litewallet.name" = "Litewallet"; + +/* Location services disabled error */ +"LocationPlugin.disabled" = "定位服務已停用。"; + +/* No permissions for location services */ +"LocationPlugin.notAuthorized" = "Litewallet 沒有存取定位服務的權限。"; + +/* No comment provided by engineer. */ +"Malformed URI" = "URI格式錯誤"; + +/* Balance */ +"ManageWallet.balance" = "平衡"; + +/* Wallet creation date prefix */ +"ManageWallet.creationDatePrefix" = "您在 %1$@ 建立了電子錢包"; + +/* Manage wallet description text */ +"ManageWallet.description" = "您的錢包名稱只會出現在您的帳戶交易記錄中,任何其他人都看不到。"; + +/* Change Wallet name textfield label */ +"ManageWallet.textFeildLabel" = "電子錢包名稱"; + +/* Manage wallet modal title */ +"ManageWallet.title" = "管理電子錢包"; + +/* Buy Litecoin title */ +"MenuButton.buy" = "購買萊特幣"; + +/* Menu button title */ +"MenuButton.customer.support" = "客戶支持"; + +/* Menu button title */ +"MenuButton.lock" = "鎖定電子錢包"; + +/* Menu button title */ +"MenuButton.security" = "資訊安全中心"; + +/* Menu button title */ +"MenuButton.settings" = "設定"; + +/* Menu button title */ +"MenuButton.support" = "支援"; + +/* button label */ +"MenuViewController.createButton" = "建立新的電子錢包"; + +/* Menu modal title */ +"MenuViewController.modalTitle" = "選單"; + +/* button label */ +"MenuViewController.recoverButton" = "復原電子錢包"; + +/* No comment provided by engineer. */ +"No wallet" = "沒有錢包"; + +/* Switch to automatic mode button label */ +"NodeSelector.automaticButton" = "切換至自動模式"; + +/* Node is connected label */ +"NodeSelector.connected" = "已連線"; + +/* Enter node ip address view body */ +"NodeSelector.enterBody" = "輸入節點的 IP 位址及連接埠(選擇性)"; + +/* Enter Node ip address view title */ +"NodeSelector.enterTitle" = "輸入節點"; + +/* Switch to manual mode button label */ +"NodeSelector.manualButton" = "切換至手動模式"; + +/* Node address label */ +"NodeSelector.nodeLabel" = "目前主節點"; + +/* Node is not connected label */ +"NodeSelector.notConnected" = "未連線"; + +/* Node status label */ +"NodeSelector.statusLabel" = "節點連線狀態"; + +/* Node Selector view title */ +"NodeSelector.title" = "萊特幣節點"; + +/* "Email address label" */ +"Notifications.emailLabel" = "電子郵件地址"; + +/* "Email address placeholder" */ +"Notifications.emailPlaceholder" = "在這裡輸入"; + +/* "Email title" */ +"Notifications.emailTitle" = "不要錯過一件事!"; + +/* "Language preference label" */ +"Notifications.languagePreference" = "首選語言:"; + +/* "Pitch to get user to sign up" */ +"Notifications.pitchMessage" = "註冊以了解更新和競賽。"; + +/* Signup cancel */ +"Notifications.signupCancel" = "不,謝謝。"; + +/* Bad Payment request alert title */ +"PaymentProtocol.Errors.badPaymentRequest" = "不良的付款請求"; + +/* Error opening payment protocol file message */ +"PaymentProtocol.Errors.corruptedDocument" = "不支持的或損毀的文件"; + +/* Missing certificate payment protocol error message */ +"PaymentProtocol.Errors.missingCertificate" = "憑證遺失"; + +/* Request expired payment protocol error message */ +"PaymentProtocol.Errors.requestExpired" = "請求已過期"; + +/* Payment too small alert title */ +"PaymentProtocol.Errors.smallOutputError" = "無法進行付款"; + +/* Amount too small error message */ +"PaymentProtocol.Errors.smallPayment" = "萊特幣付款不能少於 %1$@。"; + +/* Output too small error message. */ +"PaymentProtocol.Errors.smallTransaction" = "萊特幣交易輸出不能少於 $@。"; + +/* Unsupported signature type payment protocol error message */ +"PaymentProtocol.Errors.unsupportedSignatureType" = "不受支援的簽章類型"; + +/* Untrusted certificate payment protocol error message */ +"PaymentProtocol.Errors.untrustedCertificate" = "不受信任的憑證"; + +/* Dismiss button. */ +"Prompts.dismiss" = "解僱"; + +/* Enable face ID prompt body */ +"Prompts.FaceId.body" = "PLACEHOLDER"; + +/* Enable face ID prompt title */ +"Prompts.FaceId.title" = "PLACEHOLDER"; + +/* No passcode set warning body */ +"Prompts.NoPasscode.body" = "必須有裝置密碼來保護您的錢包。進入設定並開啟密碼。"; + +/* No Passcode set warning title */ +"Prompts.NoPasscode.title" = "開啟裝置密碼。"; + +/* Affirm button title. */ +"Prompts.PaperKey.affirm" = "繼續"; + +/* Warning about paper key. */ +"Prompts.PaperKey.body" = "必須儲存紙上金鑰,以防萬一您遺失或換電話。點選這裡來繼續。"; + +/* Cancel button. */ +"Prompts.PaperKey.cancel" = "取消"; + +/* Enable button. */ +"Prompts.PaperKey.enable" = "啟用"; + +/* An action is required (You must do this action). */ +"Prompts.PaperKey.title" = "需要採取動作"; + +/* Transaction rejected prompt body */ +"Prompts.RecommendRescan.body" = "您的電子錢包可能不同步。這通常可以經由重新掃描區塊鏈來修正。"; + +/* Transaction rejected prompt title */ +"Prompts.RecommendRescan.title" = "交易被拒"; + +/* Upgrade PIN prompt body. */ +"Prompts.SetPin.body" = "Litewallet 已經升級至 6 位數 PIN 碼。請點這裡升級。"; + +/* Upgrade PIN prompt title. */ +"Prompts.SetPin.title" = "升級 PIN 碼"; + +/* Share data prompt body */ +"Prompts.ShareData.body" = "和我們分享您的匿名資料來協助改善 Litewallet"; + +/* Share data prompt title */ +"Prompts.ShareData.title" = "分享匿名資料"; + +/* Enable touch ID prompt body */ +"Prompts.TouchId.body" = "點選這裡來啟用 Touch ID"; + +/* Enable touch ID prompt title */ +"Prompts.TouchId.title" = "啟用 Touch ID"; + +/* Receive Bar Item Title */ +"Receive.barItemTitle" = "接收"; + +/* Address copied message. */ +"Receive.copied" = "已複製至剪貼簿。"; + +/* Share via email button label */ +"Receive.emailButton" = "電子郵件"; + +/* Request button label */ +"Receive.request" = "要求帳戶"; + +/* Share button label */ +"Receive.share" = "分享"; + +/* Share via text message (SMS) */ +"Receive.textButton" = "簡訊"; + +/* Receive modal title */ +"Receive.title" = "收取"; + +/* Done button text */ +"RecoverWallet.done" = "完成"; + +/* Recover wallet header */ +"RecoverWallet.header" = "復原電子錢包"; + +/* Reset PIN with paper key: header */ +"RecoverWallet.header_reset_pin" = "重設 PIN 碼"; + +/* Enter paper key instruction */ +"RecoverWallet.instruction" = "輸入紙本金鑰"; + +/* Recover wallet intro */ +"RecoverWallet.intro" = "以您的紙本金鑰復原您的 Litewallet"; + +/* Invalid paper key message */ +"RecoverWallet.invalid" = "紙本金鑰"; + +/* Previous button accessibility label */ +"RecoverWallet.leftArrow" = "左箭頭"; + +/* Next button label */ +"RecoverWallet.next" = "下一步"; + +/* Reset PIN with paper key: more information button. */ +"RecoverWallet.reset_pin_more_info" = "點此了解更多資訊。"; + +/* Next button accessibility label */ +"RecoverWallet.rightArrow" = "右箭頭"; + +/* Recover wallet sub-header */ +"RecoverWallet.subheader" = "針對您想要復原的錢包輸入紙本金鑰。"; + +/* Reset PIN with paper key: sub-header */ +"RecoverWallet.subheader_reset_pin" = "為重設 PIN 碼,請在下方空格內輸入您的紙本金鑰上的字串。"; + +/* No amount entered error message. */ +"RequestAnAmount.noAmount" = "請先輸入一個帳戶"; + +/* Request a specific amount of Litecoin */ +"RequestAnAmount.title" = "要求帳戶"; + +/* Alert action button label */ +"ReScan.alertAction" = "同步"; + +/* Alert message body */ +"ReScan.alertMessage" = "同步期間無法發款。"; + +/* Alert message title */ +"ReScan.alertTitle" = "要與區塊鏈同步嗎?"; + +/* extimated time */ +"ReScan.body1" = "20-45 分鐘"; + +/* Syncing explanation */ +"ReScan.body2" = "如果交易在萊特幣網上已經顯示為完成,但沒有出現在您的 Litewallet 上。"; + +/* Syncing explanation */ +"ReScan.body3" = "你不斷收到「交易被拒」的錯誤。"; + +/* Start Sync button label */ +"ReScan.buttonTitle" = "開始同步"; + +/* Sync blockchain view footer */ +"ReScan.footer" = "與區塊鏈同步期間無法發款。"; + +/* Sync Blockchain view header */ +"ReScan.header" = "與區塊鏈同步"; + +/* Subheader label */ +"ReScan.subheader1" = "預估時間"; + +/* Subheader label */ +"ReScan.subheader2" = "什麼時候該進行同步?"; + +/* Reset walet button title */ +"resetButton" = "是的,重置錢包"; + +/* Warning Empty Wipe title */ +"resetTitle" = "刪除我的 Litewallet"; + +/* Scan Litecoin address camera flash toggle */ +"Scanner.flashButtonLabel" = "相機閃光"; + +/* Complete filter label */ +"Search.complete" = "完成"; + +/* Pending filter label */ +"Search.pending" = "待決"; + +/* Received filter label */ +"Search.received" = "已收到"; + +/* Sent filter label */ +"Search.sent" = "已寄送"; + +/* Face ID button title */ +"SecurityCenter.faceIdTitle" = "PLACEHOLDER"; + +/* Security Center Info */ +"SecurityCenter.info" = "啟用所有安全功能,以獲得最大保障。"; + +/* Paper Key button description */ +"SecurityCenter.paperKeyDescription" = "您的手機遺失或升級時,存取您的萊特幣唯一的方式。"; + +/* Paper Key button title */ +"SecurityCenter.paperKeyTitle" = "紙本金鑰"; + +/* PIN button description */ +"SecurityCenter.pinDescription" = "保護您的 Litewallet,防範未經授權的使用者。"; + +/* PIN button title */ +"SecurityCenter.pinTitle" = "6 位數 PIN 碼"; + +/* Security Center Title */ +"SecurityCenter.title" = "資訊安全中心"; + +/* Touch ID button description */ +"SecurityCenter.touchIdDescription" = "輕鬆解鎖您的 Litewallet,並在設定好的限額內寄送款項。"; + +/* Touch ID button title */ +"SecurityCenter.touchIdTitle" = "Touch ID"; + +/* Send money amount label */ +"Send.amountLabel" = "金額"; + +/* Balance: $4.00 */ +"Send.balance" = "餘額:%1$@"; + +/* Fee: $0.01 */ +"Send.bareFee" = "費用: %1$@"; + +/* Send Bar Item Title */ +"Send.barItemTitle" = "發送"; + +/* Camera not allowed message */ +"Send.cameraunavailableMessage" = "請至「設定」允許存取相機。"; + +/* Camera not allowed alert title */ +"Send.cameraUnavailableTitle" = "Litewallet 未獲得存取相機的權限"; + +/* Warning when sending to self. */ +"Send.containsAddress" = "目的地為您自己的地址。您不能寄送給自己。"; + +/* Could not create transaction alert title */ +"Send.creatTransactionError" = "無法建立交易。"; + +/* Description for sending money label */ +"Send.descriptionLabel" = "備註"; + +/* Emtpy pasteboard error message */ +"Send.emptyPasteboard" = "剪貼板是空的"; + +/* Enter LTC Address */ +"Send.enterLTCAddress" = "輸入萊特幣地址"; + +/* Fees: $0.01*/ +"Send.fee" = "費用: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "費用:"; + +/* Payee identity not certified alert title. */ +"Send.identityNotCertified" = "受款人身分未經認證。"; + +/* Insufficient funds error */ +"Send.insufficientFunds" = "不充足的資金"; + +/* Invalid address alert message */ +"Send.invalidAddressMessage" = "目的地址不是一個有效的萊特幣地址。"; + +/* Invalid address on pasteboard message */ +"Send.invalidAddressOnPasteboard" = "剪貼板未包含有效的萊特幣地址。"; + +/* Invalid address alert title */ +"Send.invalidAddressTitle" = "位址無效"; + +/* Is rescanning error message */ +"Send.isRescanning" = "完全重新掃描期間無法寄送。"; + +/* Loading request activity view message */ +"Send.loadingRequest" = "正在載入要求"; + +/* Network */ +"Send.networkFee" = "網絡"; + +/* Empty address alert message */ +"Send.noAddress" = "請輸入收受者的地址。"; + +/* Emtpy amount alert message */ +"Send.noAmount" = "請輸入欲寄送的金額。"; + +/* Paste button label */ +"Send.pasteLabel" = "貼上"; + +/* Could not publish transaction alert title */ +"Send.publishTransactionError" = "無法公布交易。"; + +/* Could not load remote request error message */ +"Send.remoteRequestError" = "無法載入付款要求"; + +/* Scan button label */ +"Send.scanLabel" = "掃描"; + +/* Send button label (the action, "press here to send") */ +"Send.sendLabel" = "寄送"; + +/* Service */ +"Send.serviceFee" = "服務"; + +/* Send screen title (as in "this is the screen for sending money") */ +"Send.title" = "寄送"; + +/* Send money to label */ +"Send.toLabel" = "給"; + +/* domain */ +"Send.UnstoppableDomains.domain" = "領域"; + +/* Enter a */ +"Send.UnstoppableDomains.enterA" = "進入一個"; + +/* Lookup */ +"Send.UnstoppableDomains.lookup" = "抬頭"; + +/* LookupDomainError */ +"Send.UnstoppableDomains.lookupDomainError" = "抱歉,找不到域。 [錯誤: %2$d]"; + +/* lookupFailureHeader */ +"Send.UnstoppableDomains.lookupFailureHeader" = "查找失敗"; + +/* Enter to match domain name to LTC Address */ +"Send.UnstoppableDomains.placeholder" = "輸入 .crypto、.wallet、.zil、.nft、.blockchain、.bitcoin、.coin、.888、.dao 或 .x 域。"; + +/* Enter domain */ +"Send.UnstoppableDomains.simpleplaceholder" = "輸入域"; + +/* UDSystemError */ +"Send.UnstoppableDomains.udSystemError" = "系統查找問題。 [錯誤:%2$d]"; + +/* Adress already used alert message - first part */ +"Send.UsedAddress.firstLine" = "萊特幣位址專供一次性使用。"; + +/* Adress already used alert message - second part */ +"Send.UsedAddress.secondLIne" = "重複使用會降低您與收受者的隱私,如果收受者無法直接控制該位址,可能會造成損失。"; + +/* Adress already used alert title */ +"Send.UsedAddress.title" = "位址已有人使用"; + +/* About label */ +"Settings.about" = "關於"; + +/* Advanced Settings title */ +"Settings.advancedTitle" = "進階設定"; + +/* Blockchain settings section header */ +"Settings.blockchain" = "區塊鏈"; + +/* Change language alert message */ +"Settings.ChangeLanguage.alertMessage" = "您確定要更改語言嗎?"; + +/* i.e. the currency which will be displayed */ +"Settings.currency" = "顯示貨幣"; + +/* Current Locale */ +"Settings.currentLocale" = "當前語言環境:"; + +/* Join Early access label */ +"Settings.earlyAccess" = "參加「先行體驗」"; + +/* Are you enjoying Litewallet alert message body */ +"Settings.enjoying" = "您喜歡 Litewallet 嗎?"; + +/* Face ID spending limit label */ +"Settings.faceIdLimit" = "PLACEHOLDER"; + +/* Import wallet label */ +"Settings.importTitle" = "匯入電子錢包"; + +/* Languages label */ +"Settings.languages" = "語言能力"; + +/* Litewallet environment */ +"Settings.litewallet.environment" = "環境:"; + +/* Litewallet Partners */ +"Settings.litewallet.partners" = "Litewallet合作夥伴"; + +/* Litewallet version */ +"Settings.litewallet.version" = "Litewallet版本:"; + +/* Manage settings section header */ +"Settings.manage" = "管理"; + +/* Notifications label */ +"Settings.notifications" = "通知"; + +/* Leave review button label */ +"Settings.review" = "為我們撰寫評論"; + +/* Share anonymous data label */ +"Settings.shareData" = "分享匿名資料"; + +/* Litewallet Social links */ +"Settings.socialLinks" = "社會的"; + +/* Support settings section header */ +"Settings.support" = "支持"; + +/* Sync blockchain label */ +"Settings.sync" = "與區塊鏈同步"; + +/* Settings title */ +"Settings.title" = "設定"; + +/* Touch ID spending limit label */ +"Settings.touchIdLimit" = "Touch ID 花費上限"; + +/* Wallet Settings section header */ +"Settings.wallet" = "錢包"; + +/* Start or recover another wallet menu label. */ +"Settings.wipe" = "開啟/復原其他錢包"; + +/* Share data view body */ +"ShareData.body" = "請跟我們分享您的匿名資料,以協助我們改善 Litewallet。其中不會包含財務資訊。我們尊重您的財務隱私。"; + +/* Share data header */ +"ShareData.header" = "分享資料?"; + +/* Share data switch label. */ +"ShareData.toggleLabel" = "分享匿名資料?"; + +/* Current spending limit: */ +"SpendingLimit.title" = "當前支出限額:"; + +/* button label */ +"StartPaperPhrase.againButtonTitle" = "再寫一次紙本金鑰"; + +/* Paper key explanation text. */ +"StartPaperPhrase.body" = "您的手機遺失、損壞或升級時,紙本金鑰是復原\n Litewallet 唯一的方式。\n\n我們會向您出示一個字串,請寫在紙上並妥善保管。"; + +/* button label */ +"StartPaperPhrase.buttonTitle" = "寫下紙本金鑰"; + +/* Argument is date */ +"StartPaperPhrase.date" = "您最近一次在 %1$@ 寫下了您的紙本金鑰"; + +/* Start view tagline */ +"StartViewController.tagline" = "使用莱特币最安全、最简单的方式。"; + +/* Support the Litecoin Foundation */ +"SupportTheFoundation.title" = "支持萊特幣基金會"; + +/* Syncing view connection state header text */ +"SyncingHeader.connecting" = "正在連線..."; + +/* Rescanning header success state header text */ +"SyncingHeader.rescan" = "重新掃描中..."; + +/* Syncing header success state header text */ +"SyncingHeader.success" = "成功!"; + +/* Syncing view syncing state header text */ +"SyncingHeader.syncing" = "同步中..."; + +/* Syncing view connectiong state header text */ +"SyncingView.connecting" = "正在連線"; + +/* Syncing view syncing state header text */ +"SyncingView.syncing" = "正在同步"; + +/* 6 d (6 days) */ +"TimeSince.days" = "%1$@ 天"; + +/* 6 h (6 hours) */ +"TimeSince.hours" = "%1$@ 小時"; + +/* 6 m (6 minutes) */ +"TimeSince.minutes" = "%1$@ 分鐘"; + +/* 6 s (6 seconds) */ +"TimeSince.seconds" = "%1$@ 秒"; + +/* You can customize your Touch ID Spending Limit from the [TouchIdSettings.linkText gets added here as a button] */ +"TouchIdSettings.customizeText" = "你可以在%1$@自訂你的 Touch ID 支出限額。"; + +/* Touch Id screen label */ +"TouchIdSettings.label" = "以您的指紋解鎖 Litewallet,並在設定好的限額內寄送款項。"; + +/* ł100,000 ($100) */ +"TouchIdSettings.limitValue" = "%1$@ (%2$@)"; + +/* Link Text (see TouchIdSettings.customizeText) */ +"TouchIdSettings.linkText" = "Touch ID 支出限額畫面"; + +/* Spending Limit: b100,000 ($100) */ +"TouchIdSettings.spendingLimit" = "花費上限:%1$@(%2$@)"; + +/* Touch id switch label. */ +"TouchIdSettings.switchLabel" = "為 Litewallet 啟用 Touch ID"; + +/* Touch ID settings view title */ +"TouchIdSettings.title" = "Touch ID"; + +/* Touch ID unavailable alert message */ +"TouchIdSettings.unavailableAlertMessage" = "您尚未在此裝置設定 Touch ID。請立刻至「設定」->「 Touch ID 與密碼」進行設定。"; + +/* Touch ID unavailable alert title */ +"TouchIdSettings.unavailableAlertTitle" = "未設定 Touch ID"; + +/* Always require passcode option */ +"TouchIdSpendingLimit" = "總是要求密碼"; + +/* Touch ID spending limit screen body */ +"TouchIdSpendingLimit.body" = "您必須輸入 6 位數 PIN 碼,才能送出超過您花費上限的交易;上次輸入 6 位數 PIN 碼經過 48 小時後,也必須重新輸入。"; + +/* Touch Id spending limit screen title */ +"TouchIdSpendingLimit.title" = "Touch ID 花費上限"; + +/* Static amount Label */ +"Transaction.amountDetailLabel" = "交易所詳情:"; + +/* Availability status text */ +"Transaction.available" = "可花用"; + +/* Static blockHeight Label */ +"Transaction.blockHeightLabel" = "塊:"; + +/* Static comment Label */ +"Transaction.commentLabel" = "備忘錄:"; + +/* Transaction complete label */ +"Transaction.complete" = "完成"; + +/* Static end amount Label */ +"Transaction.endAmountDetailLabel" = "交易結束金額明細"; + +/* eg. Ending balance: $50.00 */ +"Transaction.ending" = "最終餘額:%1$@"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDayReceived" = "收取時之匯率:"; + +/* Exchange rate on date header */ +"Transaction.exchangeOnDaySent" = "送出時之匯率:"; + +/* (b600 fee) */ +"Transaction.fee" = "(%1$@ 費)"; + +/* Invalid transaction */ +"Transaction.invalid" = "無效"; + +/* Timestamp label for event that just happened */ +"Transaction.justNow" = "剛剛"; + +/* Receive status text: 'In progress: 20%' */ +"Transaction.receivedStatus" = "處理中:%1$@"; + +/* Send status text: 'In progress: 20%' */ +"Transaction.sendingStatus" = "處理中:%1$@"; + +/* eg. Starting balance: $50.00 */ +"Transaction.starting" = "開始餘額:%1$@"; + +/* Static starting amount Label */ +"Transaction.startingAmountDetailLabel" = "交易開始金額明細"; + +/* Static TX iD Label */ +"Transaction.txIDLabel" = "Tx ID:"; + +/* Waiting to be confirmed string */ +"Transaction.waiting" = "正在等待確認。部分商家要求確認完成交易。預估時間:1-2 小時。"; + +/* e.g. I received money from an account. */ +"TransactionDetails.account" = "帳戶"; + +/* Amount section header */ +"TransactionDetails.amountHeader" = "金額"; + +/* Block height label */ +"TransactionDetails.blockHeightLabel" = "已在區塊確認"; + +/* Memo section header */ +"TransactionDetails.commentsHeader" = "備註"; + +/* Copied */ +"TransactionDetails.copiedAll" = "複製的"; + +/* Copy all details */ +"TransactionDetails.copyAllDetails" = "複製所有詳細信息"; + +/* Empty transaction list message. */ +"TransactionDetails.emptyMessage" = "您的交易會顯示於此。"; + +/* "Received [$5] at [which of my addresses]" => This is the "at [which of my addresses]" part. */ +"TransactionDetails.from" = "於 %1$@"; + +/* Less button title */ +"TransactionDetails.less" = "減"; + +/* Moved $5.00 (as in "I moved $5 to another location") */ +"TransactionDetails.moved" = "已轉移 %1$@"; + +/* Moved $5.00 */ +"TransactionDetails.movedAmountDescription" = "已移動 %1@"; + +/* eg. Confirmed in Block: Not Confirmed */ +"TransactionDetails.notConfirmedBlockHeightLabel" = "未確認"; + +/* Prefix for price */ +"TransactionDetails.priceTimeStampPrefix" = "作為"; + +/* "Received [$5] at [which of my addresses]" => This is the "Received [$5]" part. */ +"TransactionDetails.received" = "已收取 %1$@"; + +/* Received $5.00 */ +"TransactionDetails.receivedAmountDescription" = "已收到 %1@"; + +/* RECEIVE LTCTitle */ +"TransactionDetails.receivedModalTitle" = "接收地址"; + +/* "Sent [$5] to [address]" => This is the "Sent [$5]" part. */ +"TransactionDetails.sent" = "已送出 %1$@"; + +/* Sent $5.00 */ +"TransactionDetails.sentAmountDescription" = "已寄出 %1@"; + +/* Label for TXID */ +"TransactionDetails.staticTXLabel" = "Tx ID:"; + +/* Status section header */ +"TransactionDetails.statusHeader" = "狀態"; + +/* Transaction Details Title */ +"TransactionDetails.title" = "交易詳情"; + +/* "Sent [$5] to [address]" => This is the "to [address]" part. */ +"TransactionDetails.to" = "至 %1$@"; + +/* Transaction ID header */ +"TransactionDetails.txHashHeader" = "萊特幣交易識別碼"; + +/* (this transaction was) Received at this address: */ +"TransactionDirection.address" = "已於此地址接收"; + +/* (this transaction was) Sent to this address: */ +"TransactionDirection.to" = "已傳送至此地址"; + +/* Disabled until date */ +"UnlockScreen.disabled" = "關閉至:%1$@"; + +/* Unlock Screen sub-header */ +"UnlockScreen.enterPin" = "輸入密碼"; + +/* Unlock with FaceID accessibility label */ +"UnlockScreen.faceIdText" = "使用FaceID解鎖"; + +/* My Address button title */ +"UnlockScreen.myAddress" = "我的地址"; + +/* Reset PIN with Paper Key button label. */ +"UnlockScreen.resetPin" = "重設 PIN 碼"; + +/* Scan button title */ +"UnlockScreen.scan" = "掃描"; + +/* TouchID prompt text */ +"UnlockScreen.touchIdPrompt" = "解鎖您的 Litewallet。"; + +/* Unlock with TouchID accessibility label */ +"UnlockScreen.touchIdText" = "以 TouchID 解鎖"; + +/* Wallet unlocked message */ +"UnlockScreen.unlocked" = "錢包已解鎖"; + +/* Update PIN caption text */ +"UpdatePin.caption" = "請記住此 PIN 碼。如果忘記,會無法存取您的萊特幣。"; + +/* PIN creation info. */ +"UpdatePin.createInstruction" = "您的 PIN 碼會用於解鎖 Litewallet 及送出款項。"; + +/* Update PIN title */ +"UpdatePin.createTitle" = "設定 PIN 碼"; + +/* Update PIN title */ +"UpdatePin.createTitleConfirm" = "重新輸入 PIN 碼"; + +/* Enter current PIN instruction */ +"UpdatePin.enterCurrent" = "輸入目前的 PIN 碼"; + +/* Enter new PIN instruction */ +"UpdatePin.enterNew" = "請輸入新 PIN 碼"; + +/* Re-Enter new PIN instruction */ +"UpdatePin.reEnterNew" = "重新輸入新 PIN 碼"; + +/* Update PIN failure error message. */ +"UpdatePin.setPinError" = "很抱歉,無法更新 PIN 碼。"; + +/* Update PIN failure alert view title */ +"UpdatePin.setPinErrorTitle" = "PIN 碼更新錯誤"; + +/* Update PIN title */ +"UpdatePin.updateTitle" = "更新 PIN 碼"; + +/* Authorize to copy wallet addresses alert message */ +"URLHandling.addressaddressListAlertMessage" = "複製錢包地址至剪貼簿?"; + +/* Authorize to copy wallet address PIN view prompt. */ +"URLHandling.addressList" = "授權複製錢包地址至剪貼簿"; + +/* Authorize to copy wallet address alert title */ +"URLHandling.addressListAlertTitle" = "複製錢包地址"; + +/* Copy wallet addresses alert button label */ +"URLHandling.copy" = "複製"; + +/* Verify PIN for transaction view body */ +"VerifyPin.authorize" = "請輸入您的 PIN 碼授權此交易。"; + +/* Verify PIN view body */ +"VerifyPin.continueBody" = "請輸入 PIN 碼以繼續"; + +/* Verify PIN view title */ +"VerifyPin.title" = "PIN 碼為必填"; + +/* Authorize transaction with touch id message */ +"VerifyPin.touchIdMessage" = "授權此交易"; + +/* 'No wallet' warning for watch app */ +"Watch.noWalletWarning" = "開啟 Litewallet iPhone 應用程式以設定您的錢包。"; + +/* Dismiss button label */ +"Webview.dismiss" = "駁回"; + +/* Webview loading error message */ +"Webview.errorMessage" = "載入內容時發生錯誤。請重試一次。"; + +/* Updating webview message */ +"Webview.updating" = "正在更新……"; + +/* Welcome screen text. (?) will be replaced with the help icon users should look for. */ +"Welcome.body" = "歡迎使用 Litewallet!"; + +/* Top title of welcome screen */ +"Welcome.title" = "歡迎使用 Litewallet!"; + +/* Delete database title */ +"WipeWallet.alertDeleteTitle" = "刪除數據庫"; + +/* Wipe wallet alert message */ +"WipeWallet.alertMessage" = "確定要刪除此錢包嗎?"; + +/* Wipe wallet alert title */ +"WipeWallet.alertTitle" = "抹除錢包?"; + +/* Delete db */ +"WipeWallet.deleteDatabase" = "刪除數據庫"; + +/* Delete database message */ +"WipeWallet.deleteMessageTitle" = "這將刪除數據庫,但保留PIN和短語。確認您現有的PIN碼,種子並等待同步到新數據庫"; + +/* Delete and sync */ +"WipeWallet.deleteSync" = "刪除並同步"; + +/* Warning if user lost phrase */ +"WipeWallet.emptyWallet" = "忘記您的種子短語或PIN碼嗎?"; + +/* Failed wipe wallet alert message */ +"WipeWallet.failedMessage" = "抹除錢包失敗。"; + +/* Failed wipe wallet alert title */ +"WipeWallet.failedTitle" = "失敗"; + +/* Enter phrase to wipe wallet instruction. */ +"WipeWallet.instruction" = "輸入此錢包的復原短語(recovery phrase)以進行抹除,並開啟或復原其他錢包。您目前的餘額會保留在於短語。"; + +/* Start wipe wallet view message */ +"WipeWallet.startMessage" = "開始或復原另一個電子錢包會讓您能夠在這項裝置上存取和管理不一樣的 Litewallet 錢包。"; + +/* Start wipe wallet view warning */ +"WipeWallet.startWarning" = "您將再也無法從這項裝置存取目前的 Litewallet 錢包。餘額會留在片段上。"; + +/* Wipe wallet navigation item title. */ +"WipeWallet.title" = "開啟或復原其他錢包"; + +/* Warning Alert */ +"WipeWallet.warningAlert" = "此操作將擦除您的 Litewallet!"; + +/* Warning description */ +"WipeWallet.warningDescription" = "刪除你的錢包意味著私鑰和擦除應用程序數據將消失。你可能永遠失去萊特幣!\n\n\nLitewallet 團隊中的任何人都無法為您檢索此種子。如果您不注意此警告,我們概不負責。"; + +/* Warning title */ +"WipeWallet.warningTitle" = "請閱讀!"; + +/* Wipe wallet button title */ +"WipeWallet.wipe" = "抹除"; + +/* Wiping activity message */ +"WipeWallet.wiping" = "正在抹除……"; + +/* Paper key instructions. */ +"WritePaperPhrase.instruction" = "請依序寫下每個字,保存在安全的地方。"; + +/* button label */ +"WritePaperPhrase.next" = "下一步"; + +/* button label */ +"WritePaperPhrase.previous" = "上一頁"; + +/* 1 of 3 */ +"WritePaperPhrase.step" = "%2$d 之 %1$d"; diff --git a/litewallet/TransactionModalView.swift b/litewallet/TransactionModalView.swift index bec7760f0..0dd622c46 100644 --- a/litewallet/TransactionModalView.swift +++ b/litewallet/TransactionModalView.swift @@ -25,7 +25,7 @@ struct TransactionModalView: View { var body: some View { VStack(spacing: 1.0) { HStack { - Text("Transaction Details") + Text(S.TransactionDetails.title.localize()) .font(Font(UIFont.barlowSemiBold(size: 18.0))) .foregroundColor(.white) .frame(minWidth: 0, maxWidth: .infinity) diff --git a/litewallet/UserDefaultsUpdater.swift b/litewallet/UserDefaultsUpdater.swift new file mode 100644 index 000000000..5cde83d75 --- /dev/null +++ b/litewallet/UserDefaultsUpdater.swift @@ -0,0 +1,23 @@ +import Foundation + +private enum AppGroup { + static let id = "group.com.litecoin.loafwallet" + static let requestDataKey = "kBRSharedContainerDataWalletRequestDataKey" + static let receiveAddressKey = "kBRSharedContainerDataWalletReceiveAddressKey" +} + +class UserDefaultsUpdater { + init(walletManager: WalletManager) { + self.walletManager = walletManager + } + + func refresh() { + guard let wallet = walletManager.wallet else { return } + defaults?.set(wallet.receiveAddress as NSString, forKey: AppGroup.receiveAddressKey) + defaults?.set(wallet.receiveAddress.data(using: .utf8), forKey: AppGroup.requestDataKey) + } + + private lazy var defaults: UserDefaults? = UserDefaults(suiteName: AppGroup.id) + + private let walletManager: WalletManager +} diff --git a/litewallet/ViewControllers/AboutViewController.swift b/litewallet/ViewControllers/AboutViewController.swift new file mode 100644 index 000000000..e2010d1c2 --- /dev/null +++ b/litewallet/ViewControllers/AboutViewController.swift @@ -0,0 +1,110 @@ +import SafariServices +import UIKit + +class AboutViewController: UIViewController { + private var titleLabel = UILabel(font: .customBold(size: 26.0), color: .darkText) + private let logo = UIImageView(image: #imageLiteral(resourceName: "coinBlueWhite")) + private let logoBackground = UIView() + private let blog = AboutCell(text: S.About.blog.localize()) + private let twitter = AboutCell(text: S.About.twitter.localize()) + private let reddit = AboutCell(text: S.About.reddit.localize()) + private let privacy = UIButton(type: .system) + private let footer = UILabel(font: .customBody(size: 13.0), color: .secondaryGrayText) + override func viewDidLoad() { + if #available(iOS 11.0, *), + let labelTextColor = UIColor(named: "labelTextColor"), + let backgroundColor = UIColor(named: "lfBackgroundColor") + { + titleLabel.textColor = labelTextColor + privacy.tintColor = labelTextColor + view.backgroundColor = backgroundColor + } else { + privacy.tintColor = .liteWalletBlue + view.backgroundColor = .whiteTint + } + + addSubviews() + addConstraints() + setData() + setActions() + } + + private func addSubviews() { + view.addSubview(titleLabel) + view.addSubview(logoBackground) + logoBackground.addSubview(logo) + view.addSubview(blog) + view.addSubview(twitter) + view.addSubview(reddit) + view.addSubview(privacy) + view.addSubview(footer) + } + + private func addConstraints() { + titleLabel.constrain([ + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: C.padding[2]), + ]) + logoBackground.constrain([ + logoBackground.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logoBackground.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: C.padding[3]), + logoBackground.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2), + logoBackground.heightAnchor.constraint(equalTo: logoBackground.widthAnchor, multiplier: 1.0), + ]) + logo.constrain(toSuperviewEdges: nil) + blog.constrain([ + blog.topAnchor.constraint(equalTo: logoBackground.bottomAnchor, constant: C.padding[2]), + blog.leadingAnchor.constraint(equalTo: view.leadingAnchor), + blog.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + twitter.constrain([ + twitter.topAnchor.constraint(equalTo: blog.bottomAnchor, constant: C.padding[2]), + twitter.leadingAnchor.constraint(equalTo: view.leadingAnchor), + twitter.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + reddit.constrain([ + reddit.topAnchor.constraint(equalTo: twitter.bottomAnchor, constant: C.padding[2]), + reddit.leadingAnchor.constraint(equalTo: view.leadingAnchor), + reddit.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + privacy.constrain([ + privacy.centerXAnchor.constraint(equalTo: view.centerXAnchor), + privacy.topAnchor.constraint(equalTo: reddit.bottomAnchor, constant: C.padding[2]), + ]) + footer.constrain([ + footer.centerXAnchor.constraint(equalTo: view.centerXAnchor), + footer.topAnchor.constraint(equalTo: privacy.bottomAnchor), + footer.heightAnchor.constraint(equalToConstant: 80), + ]) + } + + private func setData() { + titleLabel.text = S.Settings.socialLinks.localize() + privacy.setTitle(S.About.privacy.localize(), for: .normal) + privacy.titleLabel?.font = UIFont.customBody(size: 13.0) + footer.textAlignment = .center + footer.numberOfLines = 4 + footer.text = String(format: S.About.footer.localize(), AppVersion.string) + logo.contentMode = .scaleAspectFill + } + + private func setActions() { + blog.button.tap = strongify(self) { myself in + myself.presentURL(string: "https://lite-wallet.org") + } + twitter.button.tap = strongify(self) { myself in + myself.presentURL(string: "https://twitter.com/Litewallet_App") + } + reddit.button.tap = strongify(self) { myself in + myself.presentURL(string: "https://www.reddit.com/r/Litewallet/") + } + privacy.tap = strongify(self) { myself in + myself.presentURL(string: "https://litewallet.io/privacy/policy.html") + } + } + + private func presentURL(string: String) { + let vc = SFSafariViewController(url: URL(string: string)!) + present(vc, animated: true, completion: nil) + } +} diff --git a/litewallet/ViewControllers/AmountViewController.swift b/litewallet/ViewControllers/AmountViewController.swift new file mode 100644 index 000000000..7cc886861 --- /dev/null +++ b/litewallet/ViewControllers/AmountViewController.swift @@ -0,0 +1,409 @@ +import UIKit + +private let currencyToggleConstant: CGFloat = 20.0 +private let amountFont: UIFont = UIFont.barlowMedium(size: 14.0) +class AmountViewController: UIViewController, Trackable { + private let store: Store + private let isPinPadExpandedAtLaunch: Bool + private let isRequesting: Bool + var minimumFractionDigits = 2 + private var hasTrailingDecimal = false + private var pinPadHeight: NSLayoutConstraint? + private var feeSelectorHeight: NSLayoutConstraint? + private var feeSelectorTop: NSLayoutConstraint? + private var placeholder = PaddingLabel(font: amountFont, color: .grayTextTint) + private var amountLabel = UILabel(font: amountFont, color: .darkText) + private let pinPad: PinPadViewController + private let currencyToggle: ShadowButton + private let border = UIView(color: .secondaryShadow) + private let bottomBorder = UIView(color: .secondaryShadow) + private let cursor = BlinkingView(blinkColor: C.defaultTintColor) + private let balanceLabel = UILabel() + private let feesLabel = UILabel() + private let feeContainer = InViewAlert(type: .secondary) + private let tapView = UIView() + private let editFee = UIButton(type: .system) + private let feeSelector: FeeSelector + + private var amount: Satoshis? { + didSet { + updateAmountLabel() + updateBalanceLabel() + didUpdateAmount?(amount) + } + } + + var balanceTextForAmount: ((Satoshis?, Rate?) -> (NSAttributedString?, NSAttributedString?)?)? + var didUpdateAmount: ((Satoshis?) -> Void)? + var didChangeFirstResponder: ((Bool) -> Void)? + var didShowFiat: ((_ isShowingFiat: Bool) -> Void)? + + var currentOutput: String { + return amountLabel.text ?? "" + } + + var selectedRate: Rate? { + didSet { + fullRefresh() + } + } + + var didUpdateFee: ((FeeType) -> Void)? { + didSet { + feeSelector.didUpdateFee = didUpdateFee + } + } + + init(store: Store, + isPinPadExpandedAtLaunch: Bool, + hasAcceptedFees _: Bool, + isRequesting: Bool = false) + { + self.store = store + self.isPinPadExpandedAtLaunch = isPinPadExpandedAtLaunch + self.isRequesting = isRequesting + if let rate = store.state.currentRate, store.state.isLtcSwapped { + currencyToggle = ShadowButton(title: "\(rate.code)(\(rate.currencySymbol))", type: .tertiary) + } else { + currencyToggle = ShadowButton(title: S.Symbols.currencyButtonTitle(maxDigits: store.state.maxDigits), type: .tertiary) + } + feeSelector = FeeSelector(store: store) + pinPad = PinPadViewController(style: .white, keyboardType: .decimalPad, maxDigits: store.state.maxDigits) + super.init(nibName: nil, bundle: nil) + } + + func forceUpdateAmount(amount: Satoshis) { + self.amount = amount + fullRefresh() + } + + func expandPinPad() { + if pinPadHeight?.constant == 0.0 { + togglePinPad() + } + } + + override func viewDidLoad() { + if #available(iOS 11.0, *) { + guard let headerTextColor = UIColor(named: "headerTextColor"), + let textColor = UIColor(named: "labelTextColor") + else { + NSLog("ERROR: Custom color") + return + } + placeholder.textColor = headerTextColor + amountLabel.textColor = textColor + } else { + placeholder.textColor = .grayTextTint + amountLabel.textColor = .darkText + } + + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(placeholder) + view.addSubview(currencyToggle) + view.addSubview(feeContainer) + view.addSubview(border) + view.addSubview(cursor) + view.addSubview(balanceLabel) + view.addSubview(feesLabel) + view.addSubview(tapView) + view.addSubview(amountLabel) + view.addSubview(bottomBorder) + view.addSubview(editFee) + } + + private func addConstraints() { + amountLabel.constrain([ + amountLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: swiftUICellPadding), + amountLabel.centerYAnchor.constraint(equalTo: currencyToggle.centerYAnchor), + amountLabel.heightAnchor.constraint(equalToConstant: 44.0), + ]) + + placeholder.constrain([ + placeholder.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: swiftUICellPadding), + placeholder.centerYAnchor.constraint(equalTo: amountLabel.centerYAnchor), + placeholder.heightAnchor.constraint(equalToConstant: 44.0), + ]) + cursor.constrain([ + cursor.leadingAnchor.constraint(equalTo: amountLabel.trailingAnchor, constant: 2.0), + cursor.heightAnchor.constraint(equalToConstant: 24.0), + cursor.centerYAnchor.constraint(equalTo: amountLabel.centerYAnchor), + cursor.widthAnchor.constraint(equalToConstant: 2.0), + ]) + currencyToggle.constrain([ + currencyToggle.topAnchor.constraint(equalTo: view.topAnchor, constant: currencyToggleConstant), + currencyToggle.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + feeSelectorHeight = feeContainer.heightAnchor.constraint(equalToConstant: 0.0) + feeSelectorTop = feeContainer.topAnchor.constraint(equalTo: feesLabel.bottomAnchor, constant: 0.0) + + feeContainer.constrain([ + feeSelectorTop, + feeSelectorHeight, + feeContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), + feeContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + feeContainer.arrowXLocation = C.padding[4] + + let borderTop = isRequesting ? border.topAnchor.constraint(equalTo: currencyToggle.bottomAnchor, constant: C.padding[2]) : border.topAnchor.constraint(equalTo: feeContainer.bottomAnchor) + border.constrain([ + border.leadingAnchor.constraint(equalTo: view.leadingAnchor), + borderTop, + border.trailingAnchor.constraint(equalTo: view.trailingAnchor), + border.heightAnchor.constraint(equalToConstant: 1.0), + ]) + balanceLabel.constrain([ + balanceLabel.leadingAnchor.constraint(equalTo: amountLabel.leadingAnchor), + balanceLabel.topAnchor.constraint(equalTo: cursor.bottomAnchor, constant: 10.0), + ]) + feesLabel.constrain([ + feesLabel.leadingAnchor.constraint(equalTo: balanceLabel.leadingAnchor), + feesLabel.topAnchor.constraint(equalTo: balanceLabel.bottomAnchor), + ]) + pinPadHeight = pinPad.view.heightAnchor.constraint(equalToConstant: 0.0) + addChildViewController(pinPad, layout: { + pinPad.view.constrain([ + pinPad.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + pinPad.view.topAnchor.constraint(equalTo: border.bottomAnchor), + pinPad.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + pinPad.view.bottomAnchor.constraint(equalTo: bottomBorder.topAnchor), + pinPadHeight, + ]) + }) + editFee.constrain([ + editFee.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0), + editFee.centerYAnchor.constraint(equalTo: feesLabel.centerYAnchor, constant: -1.0), + editFee.widthAnchor.constraint(equalToConstant: 44.0), + editFee.heightAnchor.constraint(equalToConstant: 44.0), + ]) + bottomBorder.constrain([ + bottomBorder.topAnchor.constraint(greaterThanOrEqualTo: currencyToggle.bottomAnchor, constant: C.padding[3]), + bottomBorder.leadingAnchor.constraint(equalTo: view.leadingAnchor), + bottomBorder.bottomAnchor.constraint(equalTo: view.bottomAnchor), + bottomBorder.trailingAnchor.constraint(equalTo: view.trailingAnchor), + bottomBorder.heightAnchor.constraint(equalToConstant: 1.0), + ]) + + tapView.constrain([ + tapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tapView.topAnchor.constraint(equalTo: view.topAnchor), + tapView.trailingAnchor.constraint(equalTo: currencyToggle.leadingAnchor, constant: 4.0), + tapView.bottomAnchor.constraint(equalTo: feeContainer.topAnchor), + ]) + preventAmountOverflow() + } + + private func setInitialData() { + cursor.isHidden = true + cursor.startBlinking() + + amountLabel.text = "" + + placeholder.backgroundColor = .white + placeholder.layer.cornerRadius = 8.0 + placeholder.layer.masksToBounds = true + + amountLabel.backgroundColor = .white + amountLabel.layer.cornerRadius = 8.0 + amountLabel.layer.masksToBounds = true + + placeholder.text = S.Send.amountLabel.localize() + bottomBorder.isHidden = true + if store.state.isLtcSwapped { + if let rate = store.state.currentRate { + selectedRate = rate + } + } + pinPad.ouputDidUpdate = { [weak self] output in + self?.handlePinPadUpdate(output: output) + } + currencyToggle.tap = strongify(self) { myself in + myself.toggleCurrency() + } + let gr = UITapGestureRecognizer(target: self, action: #selector(didTap)) + tapView.addGestureRecognizer(gr) + tapView.isUserInteractionEnabled = true + + if isPinPadExpandedAtLaunch { + didTap() + } + + feeContainer.contentView = feeSelector + editFee.tap = { [weak self] in + self?.toggleFeeSelector() + } + editFee.setImage(#imageLiteral(resourceName: "Edit"), for: .normal) + editFee.imageEdgeInsets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + editFee.tintColor = .grayTextTint + editFee.isHidden = true + } + + private func toggleCurrency() { + saveEvent("amount.swapCurrency") + selectedRate = selectedRate == nil ? store.state.currentRate : nil + updateCurrencyToggleTitle() + } + + private func preventAmountOverflow() { + amountLabel.constrain([ + amountLabel.trailingAnchor.constraint(lessThanOrEqualTo: currencyToggle.leadingAnchor, constant: -C.padding[2]), + ]) + amountLabel.minimumScaleFactor = 0.95 + amountLabel.adjustsFontSizeToFitWidth = true + amountLabel.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: .horizontal) + } + + private func handlePinPadUpdate(output: String) { + let currencyDecimalSeparator = NumberFormatter().currencyDecimalSeparator ?? "." + placeholder.isHidden = output.utf8.count > 0 ? true : false + minimumFractionDigits = 0 // set default + if let decimalLocation = output.range(of: currencyDecimalSeparator)?.upperBound { + let locationValue = output.distance(from: output.endIndex, to: decimalLocation) + minimumFractionDigits = abs(locationValue) + } + + // If trailing decimal, append the decimal to the output + hasTrailingDecimal = false // set default + if let decimalLocation = output.range(of: currencyDecimalSeparator)?.upperBound { + if output.endIndex == decimalLocation { + hasTrailingDecimal = true + } + } + + var newAmount: Satoshis? + if let outputAmount = NumberFormatter().number(from: output)?.doubleValue { + if let rate = selectedRate { + newAmount = Satoshis(value: outputAmount, rate: rate) + } else { + if store.state.maxDigits == 5 { + let bits = Bits(rawValue: outputAmount * 1000) + newAmount = Satoshis(bits: bits) + } else { + let bitcoin = Bitcoin(rawValue: outputAmount) + newAmount = Satoshis(bitcoin: bitcoin) + } + } + } + + if let newAmount = newAmount { + if newAmount > C.maxMoney { + pinPad.removeLast() + } else { + amount = newAmount + } + } else { + amount = nil + } + } + + private func updateAmountLabel() { + guard let amount = amount else { amountLabel.text = ""; return } + let displayAmount = DisplayAmount(amount: amount, state: store.state, selectedRate: selectedRate, minimumFractionDigits: minimumFractionDigits) + var output = displayAmount.description + if hasTrailingDecimal { + output = output.appending(NumberFormatter().currencyDecimalSeparator) + } + amountLabel.text = output + placeholder.isHidden = output.utf8.count > 0 ? true : false + } + + func updateBalanceLabel() { + if let (balance, fees) = balanceTextForAmount?(amount, selectedRate) { + balanceLabel.attributedText = balance + feesLabel.attributedText = fees + if let amount = amount, amount > 0, !isRequesting { + editFee.isHidden = false + } else { + editFee.isHidden = true + } + balanceLabel.isHidden = cursor.isHidden + } + } + + private func toggleFeeSelector() { + guard let height = feeSelectorHeight else { return } + let isCollapsed: Bool = height.isActive + UIView.spring(C.animationDuration, animations: { + if isCollapsed { + NSLayoutConstraint.deactivate([height]) + self.feeSelector.addIntrinsicSize() + } else { + self.feeSelector.removeIntrinsicSize() + NSLayoutConstraint.activate([height]) + } + self.parent?.parent?.view?.layoutIfNeeded() + }, completion: { _ in }) + } + + @objc private func didTap() { + UIView.spring(C.animationDuration, animations: { + self.togglePinPad() + self.parent?.parent?.view.layoutIfNeeded() + }, completion: { _ in }) + } + + func closePinPad() { + pinPadHeight?.constant = 0.0 + cursor.isHidden = true + bottomBorder.isHidden = true + updateBalanceAndFeeLabels() + updateBalanceLabel() + } + + private func togglePinPad() { + let isCollapsed: Bool = pinPadHeight?.constant == 0.0 + pinPadHeight?.constant = isCollapsed ? pinPad.height : 0.0 + cursor.isHidden = isCollapsed ? false : true + bottomBorder.isHidden = isCollapsed ? false : true + updateBalanceAndFeeLabels() + updateBalanceLabel() + didChangeFirstResponder?(isCollapsed) + } + + private func updateBalanceAndFeeLabels() { + if let amount = amount, amount.rawValue > 0 { + balanceLabel.isHidden = false + if !isRequesting { + editFee.isHidden = false + } + } else { + balanceLabel.isHidden = cursor.isHidden + if !isRequesting { + editFee.isHidden = true + } + } + } + + private func fullRefresh() { + updateCurrencyToggleTitle() + updateBalanceLabel() + updateAmountLabel() + + // Update pinpad content to match currency change + // This must be done AFTER the amount label has updated + let currentOutput = amountLabel.text ?? "" + var set = CharacterSet.decimalDigits + set.formUnion(CharacterSet(charactersIn: NumberFormatter().currencyDecimalSeparator)) + pinPad.currentOutput = String(String.UnicodeScalarView(currentOutput.unicodeScalars.filter { set.contains($0) })) + } + + private func updateCurrencyToggleTitle() { + if let rate = selectedRate { + currencyToggle.title = "\(rate.code)(\(rate.currencySymbol))" + didShowFiat?(false) + } else { + currencyToggle.title = S.Symbols.currencyButtonTitle(maxDigits: store.state.maxDigits) + didShowFiat?(true) + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/BiometricsSettingsViewController.swift b/litewallet/ViewControllers/BiometricsSettingsViewController.swift new file mode 100644 index 000000000..97197ae1a --- /dev/null +++ b/litewallet/ViewControllers/BiometricsSettingsViewController.swift @@ -0,0 +1,180 @@ +import LocalAuthentication +import UIKit + +class BiometricsSettingsViewController: UIViewController, Subscriber { + var presentSpendingLimit: (() -> Void)? + var didDismiss: (() -> Void)? + + init(walletManager: WalletManager, store: Store) { + self.walletManager = walletManager + self.store = store + super.init(nibName: nil, bundle: nil) + } + + private let header = RadialGradientView(backgroundColor: .darkPurple) + private let illustration = LAContext.biometricType() == .face ? UIImageView(image: #imageLiteral(resourceName: "FaceId-Large")) : UIImageView(image: #imageLiteral(resourceName: "TouchId-Large")) + private var label = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private var switchLabel = UILabel(font: .customBold(size: 14.0), color: .darkText) + private var spendingLimitLabel = UILabel(font: .customBold(size: 14.0), color: .darkText) + private var spendingButton = ShadowButton(title: "-", type: .secondary) + private var dismissButton = UIButton() + + private let toggle = GradientSwitch() + private let separator = UIView(color: .secondaryShadow) + private let walletManager: WalletManager + private let store: Store + private var rate: Rate? + fileprivate var didTapSpendingLimit = false + + deinit { + store.unsubscribe(self) + } + + override func viewDidLoad() { + store.subscribe(self, selector: { $0.currentRate != $1.currentRate }, callback: { + self.rate = $0.currentRate + }) + + addSubviews() + addConstraints() + setData() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + didTapSpendingLimit = false + spendingButton.title = spendingButtonText + } + + private func addSubviews() { + view.addSubview(header) + header.addSubview(illustration) + view.addSubview(label) + view.addSubview(switchLabel) + view.addSubview(toggle) + view.addSubview(spendingLimitLabel) + view.addSubview(spendingButton) + view.addSubview(separator) + view.addSubview(dismissButton) + } + + private func addConstraints() { + header.constrainTopCorners(sidePadding: 0.0, topPadding: 0.0) + header.constrain([header.heightAnchor.constraint(equalToConstant: C.Sizes.largeHeaderHeight)]) + illustration.constrain([ + illustration.centerXAnchor.constraint(equalTo: header.centerXAnchor), + illustration.centerYAnchor.constraint(equalTo: header.centerYAnchor, constant: E.isIPhoneX ? C.padding[4] : C.padding[2]), + ]) + dismissButton.constrain([ + dismissButton.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: C.padding[2]), + dismissButton.topAnchor.constraint(equalTo: view.topAnchor, constant: C.padding[2]), + ]) + label.constrain([ + label.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: C.padding[2]), + label.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + label.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -C.padding[2]), + ]) + switchLabel.constrain([ + switchLabel.leadingAnchor.constraint(equalTo: label.leadingAnchor), + switchLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: C.padding[2]), + ]) + toggle.constrain([ + toggle.centerYAnchor.constraint(equalTo: switchLabel.centerYAnchor), + toggle.trailingAnchor.constraint(equalTo: label.trailingAnchor), + ]) + spendingLimitLabel.constrain([ + spendingLimitLabel.leadingAnchor.constraint(equalTo: label.leadingAnchor), + spendingLimitLabel.topAnchor.constraint(equalTo: switchLabel.bottomAnchor, constant: C.padding[4]), + ]) + spendingButton.constrain([ + spendingButton.centerYAnchor.constraint(equalTo: spendingLimitLabel.centerYAnchor), + spendingButton.trailingAnchor.constraint(equalTo: label.trailingAnchor), + ]) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: switchLabel.leadingAnchor), + separator.topAnchor.constraint(equalTo: spendingButton.bottomAnchor, constant: C.padding[1]), + separator.trailingAnchor.constraint(equalTo: spendingButton.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + } + + private func setData() { + spendingButton.addTarget(self, action: #selector(didTapSpendingButton), for: .touchUpInside) + if #available(iOS 11.0, *), let backGroundColor = UIColor(named: "lfBackgroundColor"), + let textColor = UIColor(named: "labelTextColor") + { + label.textColor = textColor + switchLabel.textColor = textColor + spendingLimitLabel.textColor = textColor + view.backgroundColor = backGroundColor + } else { + label.textColor = .darkText + switchLabel.textColor = .darkText + view.backgroundColor = .white + } + + title = LAContext.biometricType() == .face ? S.FaceIDSettings.title.localize() : S.TouchIdSettings.title.localize() + label.text = LAContext.biometricType() == .face ? S.FaceIDSettings.label.localize() : S.TouchIdSettings.label.localize() + switchLabel.text = LAContext.biometricType() == .face ? S.FaceIDSettings.switchLabel.localize() : S.TouchIdSettings.switchLabel.localize() + spendingLimitLabel.text = S.SpendingLimit.titleLabel.localize() + spendingButton.title = spendingButtonText + let hasSetToggleInitialValue = false + store.subscribe(self, selector: { $0.isBiometricsEnabled != $1.isBiometricsEnabled }, callback: { + self.toggle.isOn = $0.isBiometricsEnabled + if !hasSetToggleInitialValue { + self.toggle.sendActions(for: .valueChanged) // This event is needed because the gradient background gets set on valueChanged events + } + }) + toggle.valueChanged = { [weak self] in + guard let myself = self else { return } + + if LAContext.canUseBiometrics { + myself.store.perform(action: Biometrics.setIsEnabled(myself.toggle.isOn)) + myself.spendingButton.title = myself.spendingButtonText + } else { + myself.presentCantUseBiometricsAlert() + myself.toggle.isOn = false + } + } + + dismissButton.setImage(#imageLiteral(resourceName: "Back"), for: .normal) + dismissButton.addTarget(self, action: #selector(didTapDismissButton), for: .touchUpInside) + } + + private var spendingButtonText: String { + guard let rate = rate else { return "" } + let amount = Amount(amount: walletManager.spendingLimit, rate: rate, maxDigits: store.state.maxDigits) + let string = "\(String(format: S.TouchIdSettings.limitValue.localize(), amount.bits, amount.localCurrency))" + return string + } + + fileprivate func presentCantUseBiometricsAlert() { + let unavailableAlertTitle = LAContext.biometricType() == .face ? S.FaceIDSettings.unavailableAlertTitle.localize() : S.TouchIdSettings.unavailableAlertTitle.localize() + let unavailableAlertMessage = LAContext.biometricType() == .face ? S.FaceIDSettings.unavailableAlertMessage.localize() : S.TouchIdSettings.unavailableAlertMessage.localize() + let alert = UIAlertController(title: unavailableAlertTitle, message: unavailableAlertMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .cancel, handler: nil)) + present(alert, animated: true, completion: nil) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + @objc func didTapSpendingButton() { + if LAContext.canUseBiometrics { + didTapSpendingLimit = true + presentSpendingLimit?() + } else { + presentCantUseBiometricsAlert() + } + } + + @objc func didTapDismissButton() { + didDismiss?() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/BiometricsSpendingLimitViewController.swift b/litewallet/ViewControllers/BiometricsSpendingLimitViewController.swift new file mode 100644 index 000000000..0bc2e72c0 --- /dev/null +++ b/litewallet/ViewControllers/BiometricsSpendingLimitViewController.swift @@ -0,0 +1,114 @@ +import LocalAuthentication +import UIKit + +class BiometricsSpendingLimitViewController: UITableViewController, Subscriber { + private let cellIdentifier = "CellIdentifier" + private let store: Store + private let walletManager: WalletManager + private let limits: [UInt64] = [0, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000] + private var selectedLimit: UInt64? + private var header: UIView? + private let amount = UILabel(font: .customMedium(size: 26.0), color: .darkText) + private let body = UILabel.wrapping(font: .customBody(size: 13.0), color: .darkText) + + init(walletManager: WalletManager, store: Store) { + self.walletManager = walletManager + self.store = store + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + if limits.contains(walletManager.spendingLimit) { + selectedLimit = walletManager.spendingLimit + } + tableView.register(SeparatorCell.self, forCellReuseIdentifier: cellIdentifier) + tableView.sectionHeaderHeight = UITableView.automaticDimension + tableView.estimatedSectionHeaderHeight = 50.0 + tableView.backgroundColor = .whiteTint + tableView.separatorStyle = .none + + let titleLabel = UILabel(font: .customBold(size: 17.0), color: .darkText) + let biometricsTitle = LAContext.biometricType() == .face ? S.FaceIdSpendingLimit.title.localize() : S.TouchIdSpendingLimit.title.localize() + titleLabel.text = biometricsTitle + titleLabel.sizeToFit() + navigationItem.titleView = titleLabel + + let faqButton = UIButton.buildFaqButton(store: store, articleId: ArticleIds.nothing) + faqButton.tintColor = .darkText + navigationItem.rightBarButtonItems = [UIBarButtonItem.negativePadding, UIBarButtonItem(customView: faqButton)] + + body.text = S.TouchIdSpendingLimit.body.localize() + + // If the user has a limit that is not a current option, we display their limit + if !limits.contains(walletManager.spendingLimit) { + if let rate = store.state.currentRate { + let spendingLimit = Amount(amount: walletManager.spendingLimit, rate: rate, maxDigits: store.state.maxDigits) + setAmount(limitAmount: spendingLimit) + } + } + } + + override func numberOfSections(in _: UITableView) -> Int { + return 1 + } + + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + return limits.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell + { + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) + let limit = limits[indexPath.row] + if limit == 0 { + cell.textLabel?.text = S.TouchIdSpendingLimit.requirePasscode.localize() + } else { + let displayAmount = DisplayAmount(amount: Satoshis(rawValue: limit), state: store.state, selectedRate: nil, minimumFractionDigits: 0) + cell.textLabel?.text = displayAmount.combinedDescription + } + if limits[indexPath.row] == selectedLimit { + let check = UIImageView(image: #imageLiteral(resourceName: "CircleCheck").withRenderingMode(.alwaysTemplate)) + check.tintColor = C.defaultTintColor + cell.accessoryView = check + } else { + cell.accessoryView = nil + } + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let newLimit = limits[indexPath.row] + selectedLimit = newLimit + walletManager.spendingLimit = newLimit + amount.isHidden = true + amount.constrain([ + amount.heightAnchor.constraint(equalToConstant: 0.0), + ]) + tableView.reloadData() + } + + override func tableView(_: UITableView, viewForHeaderInSection _: Int) -> UIView? { + if let header = self.header { return header } + let header = UIView(color: .whiteTint) + header.addSubview(amount) + header.addSubview(body) + amount.pinTopLeft(padding: C.padding[2]) + body.constrain([ + body.leadingAnchor.constraint(equalTo: amount.leadingAnchor), + body.topAnchor.constraint(equalTo: amount.bottomAnchor), + body.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -C.padding[2]), + body.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -C.padding[2]), + ]) + self.header = header + return header + } + + private func setAmount(limitAmount: Amount) { + amount.text = "\(limitAmount.bits) = \(limitAmount.localCurrency)" + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/ConfirmPaperPhraseViewController.swift b/litewallet/ViewControllers/ConfirmPaperPhraseViewController.swift new file mode 100644 index 000000000..45f74fc47 --- /dev/null +++ b/litewallet/ViewControllers/ConfirmPaperPhraseViewController.swift @@ -0,0 +1,203 @@ +import UIKit + +class ConfirmPaperPhraseViewController: UITableViewController { + var didCompleteConfirmation: (() -> Void)? + + @IBOutlet var headerView: UIView! + @IBOutlet var backButton: UIButton! + @IBOutlet var headerTitleLabel: UILabel! + @IBOutlet var headerDescriptionLabel: UILabel! + @IBOutlet var firstWordCell: ConfirmPhraseTableViewCell! + @IBOutlet var secondWordCell: ConfirmPhraseTableViewCell! + @IBOutlet var thirdWordCell: ConfirmPhraseTableViewCell! + @IBOutlet var fourthWordCell: ConfirmPhraseTableViewCell! + @IBOutlet var footerView: UIView! + @IBOutlet var submitButton: UIButton! + + private let fourIndices: (Int, Int, Int, Int) = { + var indexSet = Set(arrayLiteral: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) + let first = indexSet.randomElement()! + indexSet.remove(first) + var second = indexSet.randomElement()! + indexSet.remove(second) + var third = indexSet.randomElement()! + indexSet.remove(third) + var fourth = indexSet.randomElement()! + return (first, second, third, fourth) + }() + + private lazy var words: [String] = { + guard let pin = self.pin, + let phraseString = self.walletManager?.seedPhrase(pin: pin) + else { + NSLog("Error: Phrase string empty") + return [] + } + var wordArray = phraseString.components(separatedBy: " ") + let lastWord = wordArray.last + if let trimmed = lastWord?.replacingOccurrences(of: "\0", with: "") { + wordArray[11] = trimmed // This end line \0 is being read as an element...removing it + } + return wordArray + }() + + private lazy var confirmFirstPhrase: ConfirmPhrase = { ConfirmPhrase(text: String(format: S.ConfirmPaperPhrase.word.localize(), "\(self.fourIndices.0 + 1)"), word: self.words[self.fourIndices.0]) + }() + + private lazy var confirmSecondPhrase: ConfirmPhrase = { ConfirmPhrase(text: String(format: S.ConfirmPaperPhrase.word.localize(), "\(self.fourIndices.1 + 1)"), word: self.words[self.fourIndices.1]) }() + private lazy var confirmThirdPhrase: ConfirmPhrase = { ConfirmPhrase(text: String(format: S.ConfirmPaperPhrase.word.localize(), "\(self.fourIndices.2 + 1)"), word: self.words[self.fourIndices.2]) }() + private lazy var confirmFourthPhrase: ConfirmPhrase = { ConfirmPhrase(text: String(format: S.ConfirmPaperPhrase.word.localize(), "\(self.fourIndices.3 + 1)"), word: self.words[self.fourIndices.3]) }() + + var store: Store? + var walletManager: WalletManager? + var pin: String? + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewWillAppear(_: Bool) { + firstWordCell.confirmPhraseView = confirmFirstPhrase + secondWordCell.confirmPhraseView = confirmSecondPhrase + thirdWordCell.confirmPhraseView = confirmThirdPhrase + fourthWordCell.confirmPhraseView = confirmFourthPhrase + } + + override func viewDidLoad() { + view.backgroundColor = .white + navigationController?.navigationBar.isHidden = true + setupSubViews() + firstWordCell.confirmPhraseView?.textField.becomeFirstResponder() + + NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, + object: nil, + queue: nil) { [weak self] _ in + self?.dismiss(animated: true, + completion: nil) + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override var prefersStatusBarHidden: Bool { + return true + } + + private func setupSubViews() { + headerView.backgroundColor = .liteWalletBlue + headerTitleLabel.font = UIFont.barlowBold(size: 18.0) + headerDescriptionLabel.font = UIFont.barlowRegular(size: 14.0) + + headerTitleLabel.text = S.SecurityCenter.Cells.paperKeyTitle.localize() + headerDescriptionLabel.text = S.ConfirmPaperPhrase.label.localize() + headerTitleLabel.textColor = .white + headerDescriptionLabel.textColor = .white + + firstWordCell.addSubview(confirmFirstPhrase) + firstWordCell.addConstraints(NSLayoutConstraint.constraints( + withVisualFormat: "|-[confirmFirstPhrase]-|", options: [], metrics: nil, + views: ["confirmFirstPhrase": confirmFirstPhrase] + )) + + secondWordCell.addSubview(confirmSecondPhrase) + secondWordCell.addConstraints(NSLayoutConstraint.constraints( + withVisualFormat: "|-[confirmSecondPhrase]-|", options: [], metrics: nil, + views: ["confirmSecondPhrase": confirmSecondPhrase] + )) + + thirdWordCell.addSubview(confirmThirdPhrase) + thirdWordCell.addConstraints(NSLayoutConstraint.constraints( + withVisualFormat: "|-[confirmThirdPhrase]-|", options: [], metrics: nil, + views: ["confirmThirdPhrase": confirmThirdPhrase] + )) + + fourthWordCell.addSubview(confirmFourthPhrase) + fourthWordCell.addConstraints(NSLayoutConstraint.constraints( + withVisualFormat: "|-[confirmFourthPhrase]-|", options: [], metrics: nil, + views: ["confirmFourthPhrase": confirmFourthPhrase] + )) + + backButton.addTarget(self, action: #selector(dismissController), for: .touchUpInside) + submitButton.setTitle(S.Button.submit.localize(), for: .normal) + submitButton.titleLabel?.font = UIFont.barlowBold(size: 18.0) + submitButton.backgroundColor = .liteWalletBlue + submitButton.layer.cornerRadius = 4.0 + submitButton.clipsToBounds = true + submitButton.addTarget(self, action: #selector(checkPhrases), for: .touchUpInside) + + confirmFirstPhrase.callback = { [weak self] in + if self?.confirmFirstPhrase.textField.text == self?.confirmFirstPhrase.word { + self?.confirmSecondPhrase.textField.becomeFirstResponder() + } + } + confirmFirstPhrase.isEditingCallback = { [weak self] in + self?.adjustScrollView(set: 1) + } + confirmSecondPhrase.callback = { [weak self] in + if self?.confirmSecondPhrase.textField.text == self?.confirmSecondPhrase.word { + self?.confirmThirdPhrase.textField.becomeFirstResponder() + } + } + confirmSecondPhrase.isEditingCallback = { [weak self] in + self?.adjustScrollView(set: 2) + } + confirmThirdPhrase.callback = { [weak self] in + if self?.confirmThirdPhrase.textField.text == self?.confirmThirdPhrase.word { + self?.confirmFourthPhrase.textField.becomeFirstResponder() + } + } + confirmThirdPhrase.isEditingCallback = { [weak self] in + self?.adjustScrollView(set: 3) + } + confirmFourthPhrase.isEditingCallback = { [weak self] in + self?.adjustScrollView(set: 4) + } + } + + private func adjustScrollView(set: Int) { + let constant = 20.0 + let offset = CGFloat(constant) * CGFloat(set) + tableView.setContentOffset(CGPoint(x: 0, y: offset), animated: true) + } + + @objc private func dismissController() { + dismiss(animated: true) + } + + @objc private func checkPhrases() { + guard let store = store + else { + NSLog("ERROR: Store not initialized") + return + } + + if firstWordCell.confirmPhraseView?.textField.text == words[fourIndices.0] && + secondWordCell.confirmPhraseView?.textField.text == words[fourIndices.1] && + thirdWordCell.confirmPhraseView?.textField.text == words[fourIndices.2] && + fourthWordCell.confirmPhraseView?.textField.text == words[fourIndices.3] + { + UserDefaults.writePaperPhraseDate = Date() + store.trigger(name: .didWritePaperKey) + didCompleteConfirmation?() + } else { + firstWordCell.confirmPhraseView?.validate() + secondWordCell.confirmPhraseView?.validate() + thirdWordCell.confirmPhraseView?.validate() + fourthWordCell.confirmPhraseView?.validate() + showErrorMessage(S.ConfirmPaperPhrase.error.localize()) + } + } +} + +class ConfirmPhraseTableViewCell: UITableViewCell { + var confirmPhraseView: ConfirmPhrase? + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + } +} diff --git a/litewallet/ViewControllers/ConfirmationViewController.swift b/litewallet/ViewControllers/ConfirmationViewController.swift new file mode 100644 index 000000000..22e3a6f94 --- /dev/null +++ b/litewallet/ViewControllers/ConfirmationViewController.swift @@ -0,0 +1,230 @@ +import LocalAuthentication +import UIKit + +class ConfirmationViewController: UIViewController, ContentBoxPresenter { + init(amount: Satoshis, + txFee: Satoshis, + opsFee: Satoshis, + feeType: FeeType, + state: ReduxState, + selectedRate: Rate?, + minimumFractionDigits: Int?, + address: String, isUsingBiometrics: Bool, isDonation _: Bool = false) + { + self.amount = amount + self.txFee = txFee + self.opsFee = opsFee + self.feeType = feeType + self.state = state + self.selectedRate = selectedRate + self.minimumFractionDigits = minimumFractionDigits + addressText = address + self.isUsingBiometrics = isUsingBiometrics + + header = ModalHeaderView(title: S.Confirmation.title.localize(), style: .dark) + super.init(nibName: nil, bundle: nil) + } + + private let amount: Satoshis + private let txFee: Satoshis + private let opsFee: Satoshis + private let feeType: FeeType + private let state: ReduxState + private let selectedRate: Rate? + private let minimumFractionDigits: Int? + private let addressText: String + private let isUsingBiometrics: Bool + + // ContentBoxPresenter + let contentBox = UIView(color: .white) + let blurView = UIVisualEffectView() + let effect = UIBlurEffect(style: .dark) + + var successCallback: (() -> Void)? + var cancelCallback: (() -> Void)? + + private var header: ModalHeaderView? + private let cancel = ShadowButton(title: S.Button.cancel.localize(), type: .flatWhiteBorder) + private let sendButton = ShadowButton(title: S.Confirmation.send.localize(), type: .flatLitecoinBlue, image: LAContext.biometricType() == .face ? #imageLiteral(resourceName: "FaceId") : #imageLiteral(resourceName: "TouchId")) + + private let payLabel = UILabel(font: .barlowLight(size: 15.0), color: .grayTextTint) + private let toLabel = UILabel(font: .barlowLight(size: 15.0), color: .grayTextTint) + private let amountLabel = UILabel(font: .barlowRegular(size: 15.0), color: .darkText) + private let address = UILabel(font: .barlowRegular(size: 15.0), color: .darkText) + + private let processingTime = UILabel.wrapping(font: .barlowLight(size: 14.0), color: .grayTextTint) + private let sendLabel = UILabel(font: .barlowLight(size: 14.0), color: .darkGray) + private let feeLabel = UILabel(font: .barlowLight(size: 14.0), color: .darkGray) + private let totalLabel = UILabel(font: .barlowLight(size: 14.0), color: .darkGray) + + private let send = UILabel(font: .barlowRegular(size: 15.0), color: .darkText) + private let fee = UILabel(font: .barlowRegular(size: 15.0), color: .darkText) + private let total = UILabel(font: .barlowMedium(size: 15.0), color: .darkText) + + override func viewDidLoad() { + DispatchQueue.main.async { + self.addSubviews() + self.addConstraints() + self.setInitialData() + } + } + + private func addSubviews() { + view.addSubview(contentBox) + + guard let header = header + else { + NSLog("ERROR: Header not initialized") + return + } + + contentBox.addSubview(header) + contentBox.addSubview(payLabel) + contentBox.addSubview(toLabel) + contentBox.addSubview(amountLabel) + contentBox.addSubview(address) + contentBox.addSubview(processingTime) + contentBox.addSubview(sendLabel) + contentBox.addSubview(feeLabel) + contentBox.addSubview(totalLabel) + contentBox.addSubview(send) + contentBox.addSubview(fee) + contentBox.addSubview(total) + contentBox.addSubview(cancel) + contentBox.addSubview(sendButton) + } + + private func addConstraints() { + guard let header = header + else { + NSLog("ERROR: Header not initialized") + return + } + + contentBox.constrain([ + contentBox.centerXAnchor.constraint(equalTo: view.centerXAnchor), + contentBox.centerYAnchor.constraint(equalTo: view.centerYAnchor), + contentBox.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -C.padding[6]), + ]) + header.constrainTopCorners(height: 49.0) + payLabel.constrain([ + payLabel.leadingAnchor.constraint(equalTo: contentBox.leadingAnchor, constant: C.padding[2]), + payLabel.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + ]) + amountLabel.constrain([ + amountLabel.leadingAnchor.constraint(equalTo: payLabel.leadingAnchor), + amountLabel.topAnchor.constraint(equalTo: payLabel.bottomAnchor), + ]) + toLabel.constrain([ + toLabel.leadingAnchor.constraint(equalTo: amountLabel.leadingAnchor), + toLabel.topAnchor.constraint(equalTo: amountLabel.bottomAnchor, constant: C.padding[2]), + ]) + address.constrain([ + address.leadingAnchor.constraint(equalTo: toLabel.leadingAnchor), + address.topAnchor.constraint(equalTo: toLabel.bottomAnchor), + address.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + ]) + processingTime.constrain([ + processingTime.leadingAnchor.constraint(equalTo: address.leadingAnchor), + processingTime.topAnchor.constraint(equalTo: address.bottomAnchor, constant: C.padding[2]), + processingTime.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + ]) + sendLabel.constrain([ + sendLabel.leadingAnchor.constraint(equalTo: processingTime.leadingAnchor), + sendLabel.topAnchor.constraint(equalTo: processingTime.bottomAnchor, constant: C.padding[2]), + ]) + send.constrain([ + send.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + sendLabel.firstBaselineAnchor.constraint(equalTo: send.firstBaselineAnchor), + ]) + feeLabel.constrain([ + feeLabel.leadingAnchor.constraint(equalTo: sendLabel.leadingAnchor), + feeLabel.topAnchor.constraint(equalTo: sendLabel.bottomAnchor), + ]) + fee.constrain([ + fee.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + fee.firstBaselineAnchor.constraint(equalTo: feeLabel.firstBaselineAnchor), + ]) + totalLabel.constrain([ + totalLabel.leadingAnchor.constraint(equalTo: feeLabel.leadingAnchor), + totalLabel.topAnchor.constraint(equalTo: feeLabel.bottomAnchor), + ]) + total.constrain([ + total.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + total.firstBaselineAnchor.constraint(equalTo: totalLabel.firstBaselineAnchor), + ]) + cancel.constrain([ + cancel.leadingAnchor.constraint(equalTo: contentBox.leadingAnchor, constant: C.padding[2]), + cancel.topAnchor.constraint(equalTo: totalLabel.bottomAnchor, constant: C.padding[2]), + cancel.trailingAnchor.constraint(equalTo: contentBox.centerXAnchor, constant: -C.padding[1]), + cancel.bottomAnchor.constraint(equalTo: contentBox.bottomAnchor, constant: -C.padding[2]), + ]) + sendButton.constrain([ + sendButton.leadingAnchor.constraint(equalTo: contentBox.centerXAnchor, constant: C.padding[1]), + sendButton.topAnchor.constraint(equalTo: totalLabel.bottomAnchor, constant: C.padding[2]), + sendButton.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + sendButton.bottomAnchor.constraint(equalTo: contentBox.bottomAnchor, constant: -C.padding[2]), + ]) + } + + private func setInitialData() { + view.backgroundColor = .clear + payLabel.text = S.Confirmation.send.localize() + guard let header = header + else { + NSLog("ERROR: Header not initialized") + return + } + + switch feeType { + case .luxury: + processingTime.text = String(format: S.Confirmation.processingTime.localize(), "2.5-5") + case .regular: + processingTime.text = String(format: S.Confirmation.processingTime.localize(), "2.5-5") + case .economy: + processingTime.text = String(format: S.Confirmation.processingTime.localize(), "5+") + } + + let displayAmount = DisplayAmount(amount: amount, state: state, selectedRate: selectedRate, minimumFractionDigits: 2) + let displayFee = DisplayAmount(amount: txFee + opsFee, state: state, selectedRate: selectedRate, minimumFractionDigits: 2) + let displayTotal = DisplayAmount(amount: amount + txFee + opsFee, state: state, selectedRate: selectedRate, minimumFractionDigits: 2) + + toLabel.text = S.Confirmation.to.localize() + feeLabel.text = S.Send.feeBlank.localize() + sendLabel.text = S.Confirmation.amountLabel.localize() + totalLabel.text = S.Confirmation.totalLabel.localize() + + amountLabel.text = displayAmount.combinedDescription + address.text = addressText + + send.text = displayAmount.description + fee.text = displayFee.description.replacingZeroFeeWithTenCents() + total.text = displayTotal.description + + cancel.tap = strongify(self) { myself in + myself.cancelCallback?() + } + header.closeCallback = strongify(self) { myself in + myself.cancelCallback?() + } + sendButton.tap = strongify(self) { myself in + myself.successCallback?() + } + + contentBox.layer.cornerRadius = 6.0 + contentBox.layer.masksToBounds = true + + if !isUsingBiometrics { + sendButton.image = nil + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var prefersStatusBarHidden: Bool { + return true + } +} diff --git a/litewallet/ViewControllers/EnterPhraseCollectionViewController.swift b/litewallet/ViewControllers/EnterPhraseCollectionViewController.swift new file mode 100644 index 000000000..1c3c479aa --- /dev/null +++ b/litewallet/ViewControllers/EnterPhraseCollectionViewController.swift @@ -0,0 +1,111 @@ +import UIKit + +private let itemHeight: CGFloat = 50.0 + +class EnterPhraseCollectionViewController: UICollectionViewController { + // MARK: - Public + + var didFinishPhraseEntry: ((String) -> Void)? + var height: CGFloat { + return itemHeight * 4.0 + } + + #if Debug || Testflight + // let mockPhraseString = MockSeeds.mockPhraseModelX + #endif + + init(walletManager: WalletManager) { + self.walletManager = walletManager + let layout = UICollectionViewFlowLayout() + let screenWidth = UIScreen.main.safeWidth + layout.itemSize = CGSize(width: (screenWidth - C.padding[4]) / 3.0, height: itemHeight) + layout.minimumLineSpacing = 0.0 + layout.minimumInteritemSpacing = 0.0 + layout.sectionInset = .zero + super.init(collectionViewLayout: layout) + } + + // MARK: - Private + + private let cellIdentifier = "CellIdentifier" + private let walletManager: WalletManager + private var phrase: String { + return (0 ... 11).map { index in + guard let phraseCell = collectionView?.cellForItem(at: IndexPath(item: index, section: 0)) as? EnterPhraseCell else { return "" } + return phraseCell.textField.text ?? "" + }.joined(separator: " ") + } + + override func viewDidLoad() { + collectionView = NonScrollingCollectionView(frame: view.bounds, collectionViewLayout: collectionViewLayout) + collectionView?.backgroundColor = .white + collectionView?.register(EnterPhraseCell.self, forCellWithReuseIdentifier: cellIdentifier) + collectionView?.delegate = self + collectionView?.dataSource = self + collectionView?.layer.borderColor = UIColor.secondaryBorder.cgColor + collectionView?.layer.borderWidth = 1.0 + collectionView?.layer.cornerRadius = 8.0 + collectionView?.isScrollEnabled = false + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + becomeFirstResponder(atIndex: 0) + } + + // MARK: - UICollectionViewDataSource + + override func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { + return 12 + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell + { + let item = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) + guard let enterPhraseCell = item as? EnterPhraseCell else { return item } + enterPhraseCell.index = indexPath.row + enterPhraseCell.didTapNext = { [weak self] in + self?.becomeFirstResponder(atIndex: indexPath.row + 1) + } + enterPhraseCell.didTapPrevious = { [weak self] in + self?.becomeFirstResponder(atIndex: indexPath.row - 1) + } + enterPhraseCell.didTapDone = { [weak self] in + guard let phrase = self?.phrase + else { + NSLog("Phrase not initialized") + return + } + #if Debug || Testflight + // phrase = MockSeeds.mockPhraseModelX + #endif + self?.didFinishPhraseEntry?(phrase) + } + enterPhraseCell.isWordValid = { [weak self] word in + guard let myself = self else { return false } + return myself.walletManager.isWordValid(word) + } + enterPhraseCell.didEnterSpace = { + enterPhraseCell.didTapNext?() + } + + if indexPath.item == 0 { + enterPhraseCell.disablePreviousButton() + } else if indexPath.item == 11 { + enterPhraseCell.disableNextButton() + } + return item + } + + // MARK: - Extras + + private func becomeFirstResponder(atIndex: Int) { + guard let phraseCell = collectionView?.cellForItem(at: IndexPath(item: atIndex, section: 0)) as? EnterPhraseCell else { return } + phraseCell.textField.becomeFirstResponder() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/EnterPhraseViewController.swift b/litewallet/ViewControllers/EnterPhraseViewController.swift new file mode 100644 index 000000000..09d5c449c --- /dev/null +++ b/litewallet/ViewControllers/EnterPhraseViewController.swift @@ -0,0 +1,217 @@ +import UIKit + +enum PhraseEntryReason { + case setSeed(EnterPhraseCallback) + case validateForResettingPin(EnterPhraseCallback) + case validateForWipingWallet(() -> Void) +} + +typealias EnterPhraseCallback = (String) -> Void + +class EnterPhraseViewController: UIViewController, UIScrollViewDelegate, CustomTitleView, Trackable +{ + init(store: Store, walletManager: WalletManager, reason: PhraseEntryReason) { + self.store = store + self.walletManager = walletManager + enterPhrase = EnterPhraseCollectionViewController(walletManager: walletManager) + faq = UIButton.buildFaqButton(store: store, articleId: ArticleIds.nothing) + self.reason = reason + + switch reason { + case .setSeed: + customTitle = S.RecoverWallet.header.localize() + case .validateForResettingPin: + customTitle = S.RecoverWallet.headerResetPin.localize() + case .validateForWipingWallet: + customTitle = S.WipeWallet.title.localize() + } + + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + private let store: Store + private let walletManager: WalletManager + private let reason: PhraseEntryReason + private let enterPhrase: EnterPhraseCollectionViewController + private let errorLabel = UILabel.wrapping(font: .customBody(size: 16.0), color: .litewalletOrange) + private let instruction = UILabel(font: .customBold(size: 14.0), color: .darkText) + internal let titleLabel = UILabel.wrapping(font: .customBold(size: 26.0), color: .darkText) + private let subheader = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private let faq: UIButton + private let scrollView = UIScrollView() + private let container = UIView() + private let moreInfoButton = UIButton(type: .system) + let customTitle: String + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + addSubviews() + addConstraints() + setData() + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + private func addSubviews() { + view.addSubview(scrollView) + scrollView.addSubview(container) + container.addSubview(titleLabel) + container.addSubview(subheader) + container.addSubview(errorLabel) + container.addSubview(instruction) + container.addSubview(faq) + container.addSubview(moreInfoButton) + addChild(enterPhrase) + container.addSubview(enterPhrase.view) + enterPhrase.didMove(toParent: self) + } + + private func addConstraints() { + scrollView.constrain(toSuperviewEdges: nil) + scrollView.constrain([ + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + container.constrain(toSuperviewEdges: nil) + container.constrain([ + container.widthAnchor.constraint(equalTo: view.widthAnchor), + ]) + titleLabel.constrain([ + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + titleLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: C.padding[1]), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: faq.leadingAnchor), + ]) + subheader.constrain([ + subheader.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + subheader.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), + subheader.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + instruction.constrain([ + instruction.topAnchor.constraint(equalTo: subheader.bottomAnchor, constant: C.padding[3]), + instruction.leadingAnchor.constraint(equalTo: subheader.leadingAnchor), + ]) + enterPhrase.view.constrain([ + enterPhrase.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + enterPhrase.view.topAnchor.constraint(equalTo: instruction.bottomAnchor, constant: C.padding[1]), + enterPhrase.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + enterPhrase.view.heightAnchor.constraint(equalToConstant: enterPhrase.height), + ]) + errorLabel.constrain([ + errorLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: C.padding[2]), + errorLabel.topAnchor.constraint(equalTo: enterPhrase.view.bottomAnchor, constant: C.padding[1]), + errorLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -C.padding[2]), + errorLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -C.padding[2]), + ]) + faq.constrain([ + faq.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + faq.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + faq.widthAnchor.constraint(equalToConstant: 44.0), + faq.heightAnchor.constraint(equalToConstant: 44.0), + ]) + moreInfoButton.constrain([ + moreInfoButton.topAnchor.constraint(equalTo: subheader.bottomAnchor, constant: C.padding[2]), + moreInfoButton.leadingAnchor.constraint(equalTo: subheader.leadingAnchor), + ]) + } + + private func setData() { + view.backgroundColor = .secondaryButton + errorLabel.text = S.RecoverWallet.invalid.localize() + errorLabel.isHidden = true + errorLabel.textAlignment = .center + enterPhrase.didFinishPhraseEntry = { [weak self] phrase in + self?.validatePhrase(phrase) + } + instruction.text = S.RecoverWallet.instruction.localize() + + switch reason { + case .setSeed: + saveEvent("enterPhrase.setSeed") + titleLabel.text = S.RecoverWallet.header.localize() + subheader.text = S.RecoverWallet.subheader.localize() + moreInfoButton.isHidden = true + case .validateForResettingPin: + saveEvent("enterPhrase.resettingPin") + titleLabel.text = S.RecoverWallet.headerResetPin.localize() + subheader.text = S.RecoverWallet.subheaderResetPin.localize() + instruction.isHidden = true + moreInfoButton.setTitle(S.RecoverWallet.resetPinInfo.localize(), for: .normal) + moreInfoButton.tap = { [weak self] in + self?.store.trigger(name: .presentFaq(ArticleIds.nothing)) + } + faq.isHidden = true + case .validateForWipingWallet: + saveEvent("enterPhrase.wipeWallet") + titleLabel.text = S.WipeWallet.title.localize() + subheader.text = S.WipeWallet.instruction.localize() + } + + scrollView.delegate = self + addCustomTitle() + } + + private func validatePhrase(_ phrase: String) { + guard walletManager.isPhraseValid(phrase) + else { + saveEvent("enterPhrase.invalid") + errorLabel.isHidden = false + return + } + saveEvent("enterPhrase.valid") + errorLabel.isHidden = true + switch reason { + case let .setSeed(callback): + guard walletManager.setSeedPhrase(phrase) else { errorLabel.isHidden = false; return } + // Since we know that the user had their phrase at this point, + // this counts as a write date + UserDefaults.writePaperPhraseDate = Date() + return callback(phrase) + case let .validateForResettingPin(callback): + guard walletManager.authenticate(phrase: phrase) else { errorLabel.isHidden = false; return } + UserDefaults.writePaperPhraseDate = Date() + return callback(phrase) + case let .validateForWipingWallet(callback): + guard walletManager.authenticate(phrase: phrase) else { errorLabel.isHidden = false; return } + return callback() + } + } + + @objc private func keyboardWillShow(notification: Notification) { + guard let userInfo = notification.userInfo else { return } + guard let frameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + var contentInset = scrollView.contentInset + if contentInset.bottom == 0.0 { + contentInset.bottom = frameValue.cgRectValue.height + 44.0 + } + scrollView.contentInset = contentInset + } + + @objc private func keyboardWillHide(notification _: Notification) { + var contentInset = scrollView.contentInset + if contentInset.bottom > 0.0 { + contentInset.bottom = 0.0 + } + scrollView.contentInset = contentInset + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + didScrollForCustomTitle(yOffset: scrollView.contentOffset.y) + } + + func scrollViewWillEndDragging(_: UIScrollView, withVelocity _: CGPoint, targetContentOffset: UnsafeMutablePointer) + { + scrollViewWillEndDraggingForCustomTitle(yOffset: targetContentOffset.pointee.y) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/Import/StartImportViewController.swift b/litewallet/ViewControllers/Import/StartImportViewController.swift new file mode 100644 index 000000000..3d59c9f1c --- /dev/null +++ b/litewallet/ViewControllers/Import/StartImportViewController.swift @@ -0,0 +1,281 @@ +import BRCore +import UIKit + +private let mainURL = "https://insight.litecore.io/api/addrs/utxo" +private let fallbackURL = "https://insight.litecore.io/api/addrs/utxo" +private let testnetURL = "https://testnet.litecore.io/api/addrs/utxo" + +class StartImportViewController: UIViewController { + init(walletManager: WalletManager, store: Store) { + self.walletManager = walletManager + self.store = store + super.init(nibName: nil, bundle: nil) + } + + private let walletManager: WalletManager + private let store: Store + private let header = RadialGradientView(backgroundColor: .blue, offset: 64.0) + private let illustration = UIImageView(image: #imageLiteral(resourceName: "ImportIllustration")) + private let message = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private let warning = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private let button = ShadowButton(title: S.Import.scan.localize(), type: .primary) + private let bullet = UIImageView(image: #imageLiteral(resourceName: "deletecircle")) + private let leftCaption = UILabel.wrapping(font: .customMedium(size: 13.0), color: .darkText) + private let rightCaption = UILabel.wrapping(font: .customMedium(size: 13.0), color: .darkText) + private let balanceActivity = BRActivityViewController(message: S.Import.checking.localize()) + private let importingActivity = BRActivityViewController(message: S.Import.importing.localize()) + private let unlockingActivity = BRActivityViewController(message: S.Import.unlockingActivity.localize()) + + override func viewDidLoad() { + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(header) + header.addSubview(illustration) + header.addSubview(leftCaption) + header.addSubview(rightCaption) + view.addSubview(message) + view.addSubview(button) + view.addSubview(bullet) + view.addSubview(warning) + } + + private func addConstraints() { + header.constrainTopCorners(sidePadding: 0, topPadding: 0) + header.constrain([ + header.constraint(.height, constant: 220.0), + ]) + illustration.constrain([ + illustration.constraint(.width, constant: 64.0), + illustration.constraint(.height, constant: 84.0), + illustration.constraint(.centerX, toView: header, constant: 0.0), + illustration.constraint(.centerY, toView: header, constant: -C.padding[1]), + ]) + leftCaption.constrain([ + leftCaption.topAnchor.constraint(equalTo: illustration.bottomAnchor, constant: C.padding[1]), + leftCaption.trailingAnchor.constraint(equalTo: header.centerXAnchor, constant: -C.padding[2]), + leftCaption.widthAnchor.constraint(equalToConstant: 80.0), + ]) + rightCaption.constrain([ + rightCaption.topAnchor.constraint(equalTo: illustration.bottomAnchor, constant: C.padding[1]), + rightCaption.leadingAnchor.constraint(equalTo: header.centerXAnchor, constant: C.padding[2]), + rightCaption.widthAnchor.constraint(equalToConstant: 80.0), + ]) + message.constrain([ + message.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + message.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + message.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + bullet.constrain([ + bullet.leadingAnchor.constraint(equalTo: message.leadingAnchor), + bullet.topAnchor.constraint(equalTo: message.bottomAnchor, constant: C.padding[4]), + bullet.widthAnchor.constraint(equalToConstant: 16.0), + bullet.heightAnchor.constraint(equalToConstant: 16.0), + ]) + warning.constrain([ + warning.leadingAnchor.constraint(equalTo: bullet.trailingAnchor, constant: C.padding[2]), + warning.topAnchor.constraint(equalTo: bullet.topAnchor, constant: 0.0), + warning.trailingAnchor.constraint(equalTo: message.trailingAnchor), + ]) + button.constrain([ + button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[3]), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[4]), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[3]), + button.constraint(.height, constant: C.Sizes.buttonHeight), + ]) + } + + private func setInitialData() { + view.backgroundColor = .white + illustration.contentMode = .scaleAspectFill + message.text = S.Import.importMessage.localize() + leftCaption.text = S.Import.leftCaption.localize() + leftCaption.textAlignment = .center + rightCaption.text = S.Import.rightCaption.localize() + rightCaption.textAlignment = .center + warning.text = S.Import.importWarning.localize() + + button.tap = { [weak self] in + let scan = ScanViewController(scanKeyCompletion: { keyString in + self?.didReceiveAddress(keyString) + }, isValidURI: { string -> Bool in + string.isValidPrivateKey || string.isValidBip38Key + }) + self?.parent?.present(scan, animated: true, completion: nil) + } + } + + private func didReceiveAddress(_ addressOrKeyString: String) { + if addressOrKeyString.isValidPrivateKey { + if let key = BRKey(privKey: addressOrKeyString) { + checkBalance(key: key) + } + } else if addressOrKeyString.isValidBip38Key { + unlock(address: addressOrKeyString, callback: { key in + self.checkBalance(key: key) + }) + } else { + NSLog("ERROR ADDRESS OR KEY STRING: \(addressOrKeyString)") + } + } + + private func unlock(address: String, callback: @escaping (BRKey) -> Void) { + let alert = UIAlertController(title: S.Import.title.localize(), message: S.Import.password.localize(), preferredStyle: .alert) + alert.addTextField(configurationHandler: { textField in + textField.placeholder = S.Import.passwordPlaceholder.localize() + textField.isSecureTextEntry = true + textField.returnKeyType = .done + }) + alert.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: { _ in + self.present(self.unlockingActivity, animated: true, completion: { + if let password = alert.textFields?.first?.text { + if let key = BRKey(bip38Key: address, passphrase: password) { + self.unlockingActivity.dismiss(animated: true, completion: { + callback(key) + }) + return + } + } + self.unlockingActivity.dismiss(animated: true, completion: { + self.showErrorMessage(S.Import.wrongPassword.localize()) + }) + }) + })) + present(alert, animated: true, completion: nil) + } + + private func checkBalance(key: BRKey) { + present(balanceActivity, animated: true, completion: { + var key = key + + guard let address = key.address() + else { + NSLog("KEY ADDRESS: No Key Address") + return + } + + let urlString = E.isTestnet ? testnetURL : mainURL + let request = NSMutableURLRequest(url: URL(string: urlString)!, + cachePolicy: .reloadIgnoringLocalCacheData, + timeoutInterval: 20.0) + request.httpMethod = "POST" + request.httpBody = "addrs=\(address)".data(using: .utf8) + let task = URLSession.shared.dataTask(with: request as URLRequest) + { [weak self] data, _, error in + guard let myself = self else { return } + guard error == nil else { print("error: \(error!)"); return } + guard let data = data, + let jsonData = try? JSONSerialization.jsonObject(with: data, options: []), + let json = jsonData as? [[String: Any]] else { return } + + DispatchQueue.main.async { + myself.handleData(data: json, key: key) + } + } + task.resume() + }) + } + + private func handleData(data: [[String: Any]], key: BRKey) { + var key = key + guard let tx = UnsafeMutablePointer() else { return } + guard let wallet = walletManager.wallet else { return } + guard let address = key.address() else { return } + guard !wallet.containsAddress(address) + else { + return showErrorMessage(S.Import.Error.duplicate.localize()) + } + let outputs = data.compactMap { SimpleUTXO(json: $0) } + let balance = outputs.map { $0.satoshis }.reduce(0, +) + outputs.forEach { output in + tx.addInput(txHash: output.hash, index: output.index, amount: output.satoshis, script: output.script) + } + + let pubKeyLength = key.pubKey()?.count ?? 0 + let fee = wallet.feeForTxSize(tx.size + 34 + (pubKeyLength - 34) * tx.inputs.count) + balanceActivity.dismiss(animated: true, completion: { + guard !outputs.isEmpty, balance > 0 + else { + return self.showErrorMessage(S.Import.Error.empty.localize()) + } + guard fee + wallet.minOutputAmount <= balance + else { + return self.showErrorMessage(S.Import.Error.highFees.localize()) + } + guard let rate = self.store.state.currentRate else { return } + let balanceAmount = Amount(amount: balance, rate: rate, maxDigits: self.store.state.maxDigits) + let feeAmount = Amount(amount: fee, rate: rate, maxDigits: self.store.state.maxDigits) + let balanceText = self.store.state.isLtcSwapped ? balanceAmount.localCurrency : balanceAmount.bits + let feeText = self.store.state.isLtcSwapped ? feeAmount.localCurrency : feeAmount.bits + let message = String(format: S.Import.confirm.localize(), balanceText, feeText) + let alert = UIAlertController(title: S.Import.title.localize(), message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: S.Import.importButton.localize(), style: .default, handler: { _ in + self.publish(tx: tx, balance: balance, fee: fee, key: key) + })) + self.present(alert, animated: true, completion: nil) + }) + } + + private func publish(tx: UnsafeMutablePointer, balance: UInt64, fee: UInt64, key: BRKey) + { + present(importingActivity, animated: true, completion: { + guard let wallet = self.walletManager.wallet else { return } + guard let script = BRAddress(string: wallet.receiveAddress)?.scriptPubKey else { return } + tx.addOutput(amount: balance - fee, script: script) + var keys = [key] + _ = tx.sign(keys: &keys) + + guard tx.isSigned + else { + DispatchQueue.main.async { + self.importingActivity.dismiss(animated: true, completion: { + self.showErrorMessage(S.Import.Error.signing.localize()) + }) + } + return + } + self.walletManager.peerManager?.publishTx(tx, completion: { [weak self] _, error in + guard let myself = self else { return } + DispatchQueue.main.async { + myself.importingActivity.dismiss(animated: true, completion: { + DispatchQueue.main.async { + if let error = error { + myself.showErrorMessage(error.localizedDescription) + return + } + myself.showSuccess() + } + }) + } + }) + }) + } + + private func showSuccess() { + store.perform(action: SimpleReduxAlert.Show(.sweepSuccess(callback: { [weak self] in + guard let myself = self else { return } + myself.dismiss(animated: true, completion: nil) + }))) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension Data { + var reverse: Data { + let tempBytes = Array([UInt8](self).reversed()) + return Data(bytes: tempBytes) + } +} diff --git a/litewallet/ViewControllers/LoginViewController.swift b/litewallet/ViewControllers/LoginViewController.swift new file mode 100644 index 000000000..4f2445f07 --- /dev/null +++ b/litewallet/ViewControllers/LoginViewController.swift @@ -0,0 +1,493 @@ +import Firebase +import LocalAuthentication +import SwiftUI +import UIKit + +private let squareButtonSize: CGFloat = 32.0 +private let headerHeight: CGFloat = 110 + +protocol LoginViewControllerDelegate { + func didUnlockLogin() +} + +class LoginViewController: UIViewController, Subscriber, Trackable { + // MARK: - Public + + var walletManager: WalletManager? { + didSet { + guard walletManager != nil else { return } + pinView = PinView(style: .login, length: store.state.pinLength) + } + } + + var shouldSelfDismiss = false + + init(store: Store, isPresentedForLock: Bool, walletManager: WalletManager? = nil) { + self.store = store + self.walletManager = walletManager + self.isPresentedForLock = isPresentedForLock + disabledView = WalletDisabledView(store: store) + if walletManager != nil { + pinView = PinView(style: .login, length: store.state.pinLength) + } + + let viewModel = LockScreenViewModel(store: self.store) + headerView = UIHostingController(rootView: LockScreenHeaderView(viewModel: viewModel)) + + super.init(nibName: nil, bundle: nil) + } + + deinit { + store.unsubscribe(self) + } + + // MARK: - Private + + private let store: Store + + private let backgroundView: UIView = { + let view = UIView() + view.backgroundColor = .liteWalletDarkBlue + return view + }() + + private let headerView: UIHostingController + private let pinPadViewController = PinPadViewController(style: .clear, keyboardType: .pinPad, maxDigits: 0) + private let pinViewContainer = UIView() + private var pinView: PinView? + private let isPresentedForLock: Bool + private let disabledView: WalletDisabledView + private let activityView = UIActivityIndicatorView(style: .large) + private let wipeBannerButton = UIButton() + + var delegate: LoginViewControllerDelegate? + + private var logo: UIImageView = { + let image = UIImageView(image: UIImage(named: "new-logotype-white")) + image.contentMode = .scaleAspectFit + image.alpha = 0.8 + return image + }() + + private let biometricsButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setImage(LAContext.biometricType() == .face ? #imageLiteral(resourceName: "FaceId") : #imageLiteral(resourceName: "TouchId"), for: .normal) + button.layer.borderColor = UIColor.white.cgColor + button.layer.borderWidth = 1.0 + button.layer.cornerRadius = squareButtonSize / 2.0 + button.layer.masksToBounds = true + button.accessibilityLabel = LAContext.biometricType() == .face ? S.UnlockScreen.faceIdText.localize() : S.UnlockScreen.touchIdText.localize() + return button + }() + + private let showLTCAddressButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setImage(#imageLiteral(resourceName: "genericqricon"), for: .normal) + button.layer.masksToBounds = true + return button + }() + + private let enterPINLabel = UILabel(font: .barlowSemiBold(size: 18), color: .white) + private var pinPadBottom: NSLayoutConstraint? + private var topControlTop: NSLayoutConstraint? + private var unlockTimer: Timer? + private var pinPadBackground = UIView() + private var hasAttemptedToShowBiometrics = false + private let lockedOverlay = UIVisualEffectView() + private var isResetting = false + private let versionLabel = UILabel(font: .barlowRegular(size: 12), color: .white) + private var isWalletEmpty = false + + override func viewDidLoad() { + checkWalletBalance() + addSubviews() + addConstraints() + addBiometricsButton() + + addPinPadCallback() + if pinView != nil { + addPinView() + } + addWipeWalletView() + disabledView.didTapReset = { [weak self] in + guard let store = self?.store else { return } + guard let walletManager = self?.walletManager else { return } + self?.isResetting = true + let nc = UINavigationController() + let recover = EnterPhraseViewController(store: store, walletManager: walletManager, reason: .validateForResettingPin + { phrase in + let updatePin = UpdatePinViewController(store: store, walletManager: walletManager, type: .creationWithPhrase, showsBackButton: false, phrase: phrase) + nc.pushViewController(updatePin, animated: true) + updatePin.resetFromDisabledWillSucceed = { + self?.disabledView.isHidden = true + } + updatePin.resetFromDisabledSuccess = { + self?.authenticationSucceded() + LWAnalytics.logEventWithParameters(itemName: ._20200217_DUWP) + } + }) + recover.addCloseNavigationItem() + nc.viewControllers = [recover] + nc.navigationBar.tintColor = .darkText + nc.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.font: UIFont.customBold(size: 17.0), + ] + nc.setClearNavbar() + nc.navigationBar.isTranslucent = false + nc.navigationBar.barTintColor = .whiteTint + nc.viewControllers = [recover] + self?.present(nc, animated: true, completion: nil) + } + store.subscribe(self, name: .loginFromSend, callback: { _ in + self.authenticationSucceded() + }) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + guard UIApplication.shared.applicationState != .background else { return } + if shouldUseBiometrics, !hasAttemptedToShowBiometrics, !isPresentedForLock { + hasAttemptedToShowBiometrics = true + biometricsTapped() + } + + addShowAddressButton() + + if !isResetting { + lockIfNeeded() + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + unlockTimer?.invalidate() + } + + private func addPinView() { + guard let pinView = pinView else { return } + pinViewContainer.addSubview(pinView) + + logo.constrain([ + logo.topAnchor.constraint(equalTo: view.centerYAnchor, constant: -100), + logo.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logo.constraint(.height, constant: 45), + logo.constraint(.width, constant: 201), + ]) + enterPINLabel.constrain([ + enterPINLabel.topAnchor.constraint(equalTo: pinView.topAnchor, constant: -40), + enterPINLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + ]) + + pinView.constrain([ + pinView.centerYAnchor.constraint(equalTo: pinPadViewController.view.topAnchor, constant: -40), + pinView.centerXAnchor.constraint(equalTo: pinViewContainer.centerXAnchor), + pinView.widthAnchor.constraint(equalToConstant: pinView.width), + pinView.heightAnchor.constraint(equalToConstant: pinView.itemSize), + ]) + } + + private func addSubviews() { + view.addSubview(backgroundView) + view.addSubview(headerView.view) + view.addSubview(pinViewContainer) + view.addSubview(logo) + view.addSubview(versionLabel) + view.addSubview(enterPINLabel) + + pinPadBackground.backgroundColor = .clear + if walletManager != nil { + view.addSubview(pinPadBackground) + } else { + view.addSubview(activityView) + } + } + + private func addConstraints() { + backgroundView.constrain(toSuperviewEdges: nil) + headerView.view.constrain([ + headerView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + headerView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + headerView.view.topAnchor.constraint(equalTo: backgroundView.topAnchor), + headerView.view.heightAnchor.constraint(equalToConstant: headerHeight), + ]) + + if walletManager != nil { + addChildViewController(pinPadViewController, layout: { + pinPadBottom = pinPadViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -120) + pinPadViewController.view.constrain([ + pinPadViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + pinPadViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + pinPadBottom, + pinPadViewController.view.heightAnchor.constraint(equalToConstant: pinPadViewController.height), + ]) + }) + } + pinViewContainer.constrain(toSuperviewEdges: nil) + + versionLabel.constrain([ + versionLabel.constraint(.bottom, toView: view, constant: -15), + versionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor), + versionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor), + versionLabel.heightAnchor.constraint(equalToConstant: 24.0), + ]) + + if walletManager != nil { + pinPadBackground.constrain([ + pinPadBackground.leadingAnchor.constraint(equalTo: pinPadViewController.view.leadingAnchor), + pinPadBackground.trailingAnchor.constraint(equalTo: pinPadViewController.view.trailingAnchor), + pinPadBackground.topAnchor.constraint(equalTo: pinPadViewController.view.topAnchor), + pinPadBackground.bottomAnchor.constraint(equalTo: pinPadViewController.view.bottomAnchor), + ]) + } else { + activityView.constrain([ + activityView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + activityView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20.0), + ]) + activityView.startAnimating() + } + + enterPINLabel.text = S.UnlockScreen.enterPIN.localize() + versionLabel.text = AppVersion.string + versionLabel.textAlignment = .center + } + + private func deviceTopConstraintConstant() -> CGFloat { + let screenHeight = E.screenHeight + var constant = 0.0 + if screenHeight <= 640 { + constant = 35 + } else if screenHeight > 640, screenHeight < 800 { + constant = 45 + } else { + constant = 55 + } + return C.padding[1] + CGFloat(constant) + } + + private func addWipeWalletView() { + view.addSubview(wipeBannerButton) + wipeBannerButton.translatesAutoresizingMaskIntoConstraints = true + wipeBannerButton.backgroundColor = .clear + wipeBannerButton.adjustsImageWhenHighlighted = true + + wipeBannerButton.constrain([ + wipeBannerButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -35), + wipeBannerButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), + wipeBannerButton.trailingAnchor.constraint(equalTo: view.trailingAnchor), + wipeBannerButton.heightAnchor.constraint(equalToConstant: 60), + ]) + + wipeBannerButton.setTitle(S.WipeWallet.emptyWallet.localize(), for: .normal) + wipeBannerButton.setTitleColor(UIColor.white.withAlphaComponent(0.7), for: .normal) + wipeBannerButton.titleLabel?.font = .barlowSemiBold(size: 17) + wipeBannerButton.addTarget(self, action: #selector(wipeTapped), for: .touchUpInside) + } + + private func addBiometricsButton() { + guard shouldUseBiometrics else { return } + view.addSubview(biometricsButton) + biometricsButton.addTarget(self, action: #selector(biometricsTapped), for: .touchUpInside) + biometricsButton.constrain([ + biometricsButton.widthAnchor.constraint(equalToConstant: squareButtonSize), + biometricsButton.heightAnchor.constraint(equalToConstant: squareButtonSize), + biometricsButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + biometricsButton.topAnchor.constraint(equalTo: view.topAnchor, constant: headerHeight + C.padding[2]), + ]) + } + + private func addShowAddressButton() { + view.addSubview(showLTCAddressButton) + showLTCAddressButton.addTarget(self, action: #selector(showLTCAddress), for: .touchUpInside) + showLTCAddressButton.constrain([ + showLTCAddressButton.widthAnchor.constraint(equalToConstant: squareButtonSize), + showLTCAddressButton.heightAnchor.constraint(equalToConstant: squareButtonSize), + showLTCAddressButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + showLTCAddressButton.topAnchor.constraint(equalTo: view.topAnchor, constant: headerHeight + C.padding[2]), + ]) + } + + private func addPinPadCallback() { + pinPadViewController.ouputDidUpdate = { [weak self] pin in + guard let myself = self else { return } + guard let pinView = self?.pinView else { return } + let attemptLength = pin.utf8.count + pinView.fill(attemptLength) + self?.pinPadViewController.isAppendingDisabled = attemptLength < myself.store.state.pinLength ? false : true + if attemptLength == myself.store.state.pinLength { + self?.authenticate(pin: pin) + } + } + } + + private func checkWalletBalance() { + if let wallet = walletManager?.wallet { + if wallet.balance == 0 { + isWalletEmpty = true + } else { + isWalletEmpty = false + } + } + } + + private func authenticate(pin: String) { + guard let walletManager = walletManager else { return } + guard !E.isScreenshots else { return authenticationSucceded() } + guard walletManager.authenticate(pin: pin) else { return authenticationFailed() } + authenticationSucceded() + LWAnalytics.logEventWithParameters(itemName: ._20200217_DUWP) + } + + private func authenticationSucceded() { + saveEvent("login.success") + let label = UILabel(font: enterPINLabel.font) + label.textColor = .white + label.text = S.UnlockScreen.unlocked.localize() + let lock = UIImageView(image: #imageLiteral(resourceName: "unlock")) + lock.transform = .init(scaleX: 0.6, y: 0.6) + + if let _pinView = pinView { + enterPINLabel.removeFromSuperview() + _pinView.removeFromSuperview() + } + + view.addSubview(label) + view.addSubview(lock) + + label.constrain([ + label.bottomAnchor.constraint(equalTo: view.centerYAnchor, constant: -C.padding[1]), + label.centerXAnchor.constraint(equalTo: view.centerXAnchor), + ]) + lock.constrain([ + lock.topAnchor.constraint(equalTo: label.bottomAnchor, constant: C.padding[1]), + lock.centerXAnchor.constraint(equalTo: label.centerXAnchor), + ]) + view.layoutIfNeeded() + + logo.alpha = 0.0 + wipeBannerButton.alpha = 1.0 + + UIView.spring(0.6, delay: 0.4, animations: { + self.pinPadBottom?.constant = self.pinPadViewController.height + self.topControlTop?.constant = -100.0 + + lock.alpha = 0.0 + label.alpha = 0.0 + self.wipeBannerButton.alpha = 0.0 + self.enterPINLabel.alpha = 0.0 + self.pinView?.alpha = 0.0 + + self.view.layoutIfNeeded() + }) { _ in + self.delegate?.didUnlockLogin() + if self.shouldSelfDismiss { + self.dismiss(animated: true, completion: nil) + } + self.store.perform(action: LoginSuccess()) + self.store.trigger(name: .showStatusBar) + } + } + + private func authenticationFailed() { + saveEvent("login.failed") + guard let pinView = pinView else { return } + pinPadViewController.view.isUserInteractionEnabled = false + pinView.shake { [weak self] in + self?.pinPadViewController.view.isUserInteractionEnabled = true + } + pinPadViewController.clear() + DispatchQueue.main.asyncAfter(deadline: .now() + pinView.shakeDuration) { [weak self] in + pinView.fill(0) + self?.lockIfNeeded() + } + } + + private var shouldUseBiometrics: Bool { + guard let walletManager = walletManager else { return false } + return LAContext.canUseBiometrics && !walletManager.pinLoginRequired && store.state.isBiometricsEnabled + } + + @objc func biometricsTapped() { + guard !isWalletDisabled else { return } + walletManager?.authenticate(biometricsPrompt: S.UnlockScreen.touchIdPrompt.localize(), completion: { result in + if result == .success { + self.authenticationSucceded() + LWAnalytics.logEventWithParameters(itemName: ._20200217_DUWB) + } + }) + } + + @objc func showLTCAddress() { + guard !isWalletDisabled else { return } + store.perform(action: RootModalActions.Present(modal: .loginAddress)) + } + + @objc func wipeTapped() { + store.perform(action: RootModalActions.Present(modal: .wipeEmptyWallet)) + } + + private func lockIfNeeded() { + if let disabledUntil = walletManager?.walletDisabledUntil { + let now = Date().timeIntervalSince1970 + if disabledUntil > now { + saveEvent("login.locked") + let disabledUntilDate = Date(timeIntervalSince1970: disabledUntil) + let unlockInterval = disabledUntil - now + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate(unlockInterval > C.secondsInDay ? "h:mm:ss a MMM d, yyy" : "h:mm:ss a") + + disabledView.setTimeLabel(string: String(format: S.UnlockScreen.disabled.localize(), df.string(from: disabledUntilDate))) + + pinPadViewController.view.isUserInteractionEnabled = false + unlockTimer?.invalidate() + unlockTimer = Timer.scheduledTimer(timeInterval: unlockInterval, target: self, selector: #selector(LoginViewController.unlock), userInfo: nil, repeats: false) + + if disabledView.superview == nil { + view.addSubview(disabledView) + setNeedsStatusBarAppearanceUpdate() + disabledView.constrain(toSuperviewEdges: nil) + disabledView.show() + } + } else { + pinPadViewController.view.isUserInteractionEnabled = true + disabledView.hide { [weak self] in + self?.disabledView.removeFromSuperview() + self?.setNeedsStatusBarAppearanceUpdate() + } + } + } + } + + private var isWalletDisabled: Bool { + guard let walletManager = walletManager else { return false } + let now = Date().timeIntervalSince1970 + return walletManager.walletDisabledUntil > now + } + + @objc private func unlock() { + saveEvent("login.unlocked") + delegate?.didUnlockLogin() + enterPINLabel.pushNewText(S.UnlockScreen.enterPIN.localize()) + pinPadViewController.view.isUserInteractionEnabled = true + unlockTimer = nil + disabledView.hide { [weak self] in + self?.disabledView.removeFromSuperview() + self?.setNeedsStatusBarAppearanceUpdate() + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + if disabledView.superview == nil { + return .lightContent + } else { + return .default + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/ModalNavigationController.swift b/litewallet/ViewControllers/ModalNavigationController.swift new file mode 100644 index 000000000..d31ded1a1 --- /dev/null +++ b/litewallet/ViewControllers/ModalNavigationController.swift @@ -0,0 +1,8 @@ +import UIKit + +class ModalNavigationController: UINavigationController { + override var preferredStatusBarStyle: UIStatusBarStyle { + guard let vc = topViewController else { return .default } + return vc.preferredStatusBarStyle + } +} diff --git a/litewallet/ViewControllers/NodeSelectorViewController.swift b/litewallet/ViewControllers/NodeSelectorViewController.swift new file mode 100644 index 000000000..49fd952dc --- /dev/null +++ b/litewallet/ViewControllers/NodeSelectorViewController.swift @@ -0,0 +1,146 @@ +import BRCore +import UIKit + +class NodeSelectorViewController: UIViewController, Trackable { + let titleLabel = UILabel(font: .customBold(size: 26.0), color: .darkText) + private let nodeLabel = UILabel(font: .customBody(size: 14.0), color: .grayTextTint) + private let node = UILabel(font: .customBody(size: 14.0), color: .darkText) + private let statusLabel = UILabel(font: .customBody(size: 14.0), color: .grayTextTint) + private let status = UILabel(font: .customBody(size: 14.0), color: .darkText) + private let button: ShadowButton + private let walletManager: WalletManager + private var okAction: UIAlertAction? + private var timer: Timer? + private let leftJustPadding = C.padding[6] + + init(walletManager: WalletManager) { + self.walletManager = walletManager + if UserDefaults.customNodeIP == nil { + button = ShadowButton(title: S.NodeSelector.manualButton.localize(), type: .primary) + } else { + button = ShadowButton(title: S.NodeSelector.automaticButton.localize(), type: .primary) + } + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(titleLabel) + view.addSubview(nodeLabel) + view.addSubview(node) + view.addSubview(statusLabel) + view.addSubview(status) + view.addSubview(button) + } + + private func addConstraints() { + titleLabel.pinTopLeft(padding: leftJustPadding) + nodeLabel.pinTopLeft(toView: titleLabel, topPadding: leftJustPadding) + node.pinTopLeft(toView: nodeLabel, topPadding: 0) + statusLabel.pinTopLeft(toView: node, topPadding: leftJustPadding) + status.pinTopLeft(toView: statusLabel, topPadding: 0) + button.constrain([ + button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: leftJustPadding), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -leftJustPadding), + button.topAnchor.constraint(equalTo: status.bottomAnchor, constant: leftJustPadding), + button.heightAnchor.constraint(equalToConstant: 44.0), + ]) + } + + private func setInitialData() { + view.backgroundColor = .whiteTint + titleLabel.text = S.NodeSelector.title.localize() + nodeLabel.text = S.NodeSelector.nodeLabel.localize() + statusLabel.text = S.NodeSelector.statusLabel.localize() + button.tap = strongify(self) { myself in + if UserDefaults.customNodeIP == nil { + myself.switchToManual() + } else { + myself.switchToAuto() + } + } + setStatusText() + timer = Timer.scheduledTimer(timeInterval: 0.25, target: self, selector: #selector(setStatusText), userInfo: nil, repeats: true) + } + + @objc private func setStatusText() { + if let peerManager = walletManager.peerManager { + status.text = peerManager.isConnected ? S.NodeSelector.connected.localize() : S.NodeSelector.notConnected.localize() + } + node.text = walletManager.peerManager?.downloadPeerName + } + + private func switchToAuto() { + guard UserDefaults.customNodeIP != nil else { return } // noop if custom node is already nil + saveEvent("nodeSelector.switchToAuto") + UserDefaults.customNodeIP = nil + UserDefaults.customNodePort = nil + button.title = S.NodeSelector.manualButton.localize() + DispatchQueue.walletQueue.async { + self.walletManager.peerManager?.setFixedPeer(address: 0, port: 0) + self.walletManager.peerManager?.connect() + } + } + + private func switchToManual() { + let alert = UIAlertController(title: S.NodeSelector.enterTitle.localize(), message: S.NodeSelector.enterBody.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .cancel, handler: nil)) + let okAction = UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: { _ in + guard let ip = alert.textFields?.first, let port = alert.textFields?.last else { return } + if let addressText = ip.text { + self.saveEvent("nodeSelector.switchToManual") + var address = in_addr() + ascii2addr(AF_INET, addressText, &address) + UserDefaults.customNodeIP = Int(address.s_addr) + if let portText = port.text { + UserDefaults.customNodePort = Int(portText) + } + DispatchQueue.walletQueue.async { + self.walletManager.peerManager?.connect() + } + self.button.title = S.NodeSelector.automaticButton.localize() + } + }) + self.okAction = okAction + self.okAction?.isEnabled = false + alert.addAction(okAction) + alert.addTextField { textField in + textField.placeholder = "192.168.0.1" + textField.keyboardType = .decimalPad + textField.addTarget(self, action: #selector(self.ipAddressDidChange(textField:)), for: .editingChanged) + } + alert.addTextField { textField in + textField.placeholder = "9333" + textField.keyboardType = .decimalPad + } + present(alert, animated: true, completion: nil) + } + + private func setCustomNodeText() { + if var customNode = UserDefaults.customNodeIP { + if let buf = addr2ascii(AF_INET, &customNode, Int32(MemoryLayout.size), nil) { + node.text = String(cString: buf) + } + } + } + + @objc private func ipAddressDidChange(textField: UITextField) { + if let text = textField.text { + if text.components(separatedBy: ".").count == 4, ascii2addr(AF_INET, text, nil) > 0 { + okAction?.isEnabled = true + return + } + } + okAction?.isEnabled = false + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/PinPadViewController.swift b/litewallet/ViewControllers/PinPadViewController.swift new file mode 100644 index 000000000..786400383 --- /dev/null +++ b/litewallet/ViewControllers/PinPadViewController.swift @@ -0,0 +1,407 @@ +import UIKit + +enum PinPadColorStyle { + case white + case clear +} + +enum KeyboardType { + case decimalPad + case pinPad +} + +let deleteKeyIdentifier = "del" +let kDecimalPadItemHeight: CGFloat = 48.0 +let kPinPadItemHeight: CGFloat = 54.0 + +class PinPadViewController: UICollectionViewController { + let currencyDecimalSeparator = NumberFormatter().currencyDecimalSeparator ?? "." + var isAppendingDisabled = false + var ouputDidUpdate: ((String) -> Void)? + var didUpdateFrameWidth: ((CGRect) -> Void)? + + var height: CGFloat { + switch keyboardType { + case .decimalPad: + return kDecimalPadItemHeight * 4.0 // for four rows tall + case .pinPad: + return kPinPadItemHeight * 4.0 // for four rows tall + } + } + + var currentOutput = "" + + func clear() { + isAppendingDisabled = false + currentOutput = "" + } + + func removeLast() { + if !currentOutput.utf8.isEmpty { + currentOutput = String(currentOutput[.. Int { + return items.count + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell + { + let item = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) + guard let pinPadCell = item as? GenericPinPadCell else { return item } + pinPadCell.text = items[indexPath.item] + + // produces a frame for lining up other subviews + if indexPath.item == 0 { + didUpdateFrameWidth?(collectionView.convert(pinPadCell.frame, to: view)) + } + return pinPadCell + } + + // MARK: - UICollectionViewDelegate + + override func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let item = items[indexPath.row] + if item == "del" { + if !currentOutput.isEmpty { + if currentOutput == ("0" + currencyDecimalSeparator) { + currentOutput = "" + } else { + currentOutput.remove(at: currentOutput.index(before: currentOutput.endIndex)) + } + } + } else { + if shouldAppendChar(char: item), !isAppendingDisabled { + currentOutput = currentOutput + item + } + } + ouputDidUpdate?(currentOutput) + } + + func shouldAppendChar(char: String) -> Bool { + let decimalLocation = currentOutput.range(of: currencyDecimalSeparator)?.lowerBound + + // Don't allow more that maxDigits decimal points + if let location = decimalLocation { + let locationValue = currentOutput.distance(from: currentOutput.endIndex, to: location) + if locationValue < -maxDigits { + return false + } + } + + // Don't allow more than 2 decimal separators + if currentOutput.contains("\(currencyDecimalSeparator)"), char == currencyDecimalSeparator { + return false + } + + if keyboardType == .decimalPad { + if currentOutput == "0" { + // Append . to 0 + if char == currencyDecimalSeparator { + return true + + // Dont append 0 to 0 + } else if char == "0" { + return false + + // Replace 0 with any other digit + } else { + currentOutput = char + return false + } + } + } + + if char == currencyDecimalSeparator { + if decimalLocation == nil { + // Prepend a 0 if the first character is a decimal point + if currentOutput.isEmpty { + currentOutput = "0" + } + return true + } else { + return false + } + } + return true + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class GenericPinPadCell: UICollectionViewCell { + var text: String? { + didSet { + if text == deleteKeyIdentifier { + imageView.image = #imageLiteral(resourceName: "Delete") + topLabel.text = "" + centerLabel.text = "" + } else { + imageView.image = nil + topLabel.text = text + centerLabel.text = text + } + setAppearance() + setSublabel() + } + } + + let sublabels = [ + "2": "ABC", + "3": "DEF", + "4": "GHI", + "5": "JKL", + "6": "MNO", + "7": "PORS", + "8": "TUV", + "9": "WXYZ", + ] + + override var isHighlighted: Bool { + didSet { + guard text != "" else { return } // We don't want the blank cell to highlight + setAppearance() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + internal let topLabel = UILabel(font: .customBody(size: 28.0)) + internal let centerLabel = UILabel(font: .customBody(size: 28.0)) + internal let sublabel = UILabel(font: .customBody(size: 11.0)) + internal let imageView = UIImageView() + + private func setup() { + setAppearance() + topLabel.textAlignment = .center + centerLabel.textAlignment = .center + sublabel.textAlignment = .center + addSubview(topLabel) + addSubview(centerLabel) + addSubview(sublabel) + addSubview(imageView) + imageView.contentMode = .center + addConstraints() + } + + func addConstraints() { + imageView.constrain(toSuperviewEdges: nil) + centerLabel.constrain(toSuperviewEdges: nil) + topLabel.constrain([ + topLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + topLabel.topAnchor.constraint(equalTo: topAnchor, constant: 2.5), + ]) + sublabel.constrain([ + sublabel.centerXAnchor.constraint(equalTo: centerXAnchor), + sublabel.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: -3.0), + ]) + } + + override var isAccessibilityElement: Bool { + get { + return true + } + set {} + } + + override var accessibilityLabel: String? { + get { + return topLabel.text + } + set {} + } + + override var accessibilityTraits: UIAccessibilityTraits { + get { + return UIAccessibilityTraits.staticText + } + set {} + } + + func setAppearance() {} + func setSublabel() {} + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class ClearNumberPad: GenericPinPadCell { + override func setAppearance() { + if text == "0" { + topLabel.isHidden = true + centerLabel.isHidden = false + } else { + topLabel.isHidden = false + centerLabel.isHidden = true + } + + topLabel.textColor = .white + centerLabel.textColor = .white + sublabel.textColor = .white + + if isHighlighted { + backgroundColor = .transparentBlack + } else { + backgroundColor = .clear + + if text == "" || text == deleteKeyIdentifier { + imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate) + imageView.tintColor = .white + } + } + } + + override func setSublabel() { + guard let text = text else { return } + if sublabels[text] != nil { + sublabel.text = sublabels[text] + } + } +} + +class ClearDecimalPad: GenericPinPadCell { + override func setAppearance() { + centerLabel.backgroundColor = .clear + imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate) + + if isHighlighted { + centerLabel.textColor = .grayTextTint + imageView.tintColor = .grayTextTint + } else { + centerLabel.textColor = .white + imageView.tintColor = .white + } + } + + override func addConstraints() { + centerLabel.constrain(toSuperviewEdges: nil) + imageView.constrain(toSuperviewEdges: nil) + } +} + +class WhiteDecimalPad: GenericPinPadCell { + override func setAppearance() { + if isHighlighted { + centerLabel.backgroundColor = .secondaryShadow + centerLabel.textColor = .darkText + } else { + centerLabel.backgroundColor = .white + centerLabel.textColor = .grayTextTint + } + } + + override func addConstraints() { + centerLabel.constrain(toSuperviewEdges: nil) + imageView.constrain(toSuperviewEdges: nil) + } +} + +class WhiteNumberPad: GenericPinPadCell { + override func setAppearance() { + if text == "0" { + topLabel.isHidden = true + centerLabel.isHidden = false + } else { + topLabel.isHidden = false + centerLabel.isHidden = true + } + + if isHighlighted { + backgroundColor = .secondaryShadow + topLabel.textColor = .darkText + centerLabel.textColor = .darkText + sublabel.textColor = .darkText + } else { + if text == "" || text == deleteKeyIdentifier { + backgroundColor = .whiteTint + imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate) + imageView.tintColor = .grayTextTint + } else { + backgroundColor = .whiteTint + topLabel.textColor = .grayTextTint + centerLabel.textColor = .grayTextTint + sublabel.textColor = .grayTextTint + } + } + } + + override func setSublabel() { + guard let text = text else { return } + if sublabels[text] != nil { + sublabel.text = sublabels[text] + } + } +} diff --git a/litewallet/ViewControllers/ReScanViewController.swift b/litewallet/ViewControllers/ReScanViewController.swift new file mode 100644 index 000000000..caef3efd1 --- /dev/null +++ b/litewallet/ViewControllers/ReScanViewController.swift @@ -0,0 +1,104 @@ +import UIKit + +class ReScanViewController: UIViewController, Subscriber { + init(store: Store) { + self.store = store + faq = .buildFaqButton(store: store, articleId: ArticleIds.nothing) + super.init(nibName: nil, bundle: nil) + } + + private let header = UILabel(font: .customBold(size: 26.0), color: .darkText) + private let body = UILabel.wrapping(font: .systemFont(ofSize: 15.0)) + private let button = ShadowButton(title: S.ReScan.buttonTitle.localize(), type: .primary) + private let footer = UILabel.wrapping(font: .customBody(size: 16.0), color: .secondaryGrayText) + private let store: Store + private let faq: UIButton + + deinit { + store.unsubscribe(self) + } + + override func viewDidLoad() { + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(header) + view.addSubview(faq) + view.addSubview(body) + view.addSubview(button) + view.addSubview(footer) + } + + private func addConstraints() { + header.constrain([ + header.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + header.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: C.padding[2]), + ]) + faq.constrain([ + faq.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + faq.centerYAnchor.constraint(equalTo: header.centerYAnchor), + faq.widthAnchor.constraint(equalToConstant: 44.0), + faq.heightAnchor.constraint(equalToConstant: 44.0), + ]) + body.constrain([ + body.leadingAnchor.constraint(equalTo: header.leadingAnchor), + body.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + body.trailingAnchor.constraint(equalTo: faq.trailingAnchor), + ]) + footer.constrain([ + footer.leadingAnchor.constraint(equalTo: header.leadingAnchor), + footer.trailingAnchor.constraint(equalTo: faq.trailingAnchor), + footer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -C.padding[3]), + ]) + button.constrain([ + button.leadingAnchor.constraint(equalTo: footer.leadingAnchor), + button.trailingAnchor.constraint(equalTo: footer.trailingAnchor), + button.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -C.padding[2]), + button.heightAnchor.constraint(equalToConstant: C.Sizes.buttonHeight), + ]) + } + + private func setInitialData() { + view.backgroundColor = .whiteTint + header.text = S.ReScan.header.localize() + body.attributedText = bodyText + footer.text = S.ReScan.footer.localize() + button.tap = { [weak self] in + self?.presentRescanAlert() + } + } + + private func presentRescanAlert() { + let alert = UIAlertController(title: S.ReScan.alertTitle.localize(), message: S.ReScan.alertMessage.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: S.ReScan.alertAction.localize(), style: .default, handler: { _ in + self.store.trigger(name: .rescan) + LWAnalytics.logEventWithParameters(itemName: ._20200112_DSR) + + self.dismiss(animated: true, completion: nil) + })) + present(alert, animated: true, completion: nil) + } + + private var bodyText: NSAttributedString { + let body = NSMutableAttributedString() + let headerAttributes = [NSAttributedString.Key.font: UIFont.customBold(size: 16.0), + NSAttributedString.Key.foregroundColor: UIColor.darkText] + let bodyAttributes = [NSAttributedString.Key.font: UIFont.customBody(size: 16.0), + NSAttributedString.Key.foregroundColor: UIColor.darkText] + + body.append(NSAttributedString(string: "\(S.ReScan.subheader1)\n", attributes: headerAttributes)) + body.append(NSAttributedString(string: "\(S.ReScan.body1)\n\n", attributes: bodyAttributes)) + body.append(NSAttributedString(string: "\(S.ReScan.subheader2)\n", attributes: headerAttributes)) + body.append(NSAttributedString(string: "\(S.ReScan.body2)\n\n\(S.ReScan.body3)", attributes: bodyAttributes)) + return body + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/RecoverWalletIntroViewController.swift b/litewallet/ViewControllers/RecoverWalletIntroViewController.swift new file mode 100644 index 000000000..bc0c6867e --- /dev/null +++ b/litewallet/ViewControllers/RecoverWalletIntroViewController.swift @@ -0,0 +1,69 @@ +import UIKit + +class RecoverWalletIntroViewController: UIViewController { + // MARK: - Public + + init(didTapNext: @escaping () -> Void) { + self.didTapNext = didTapNext + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + private let didTapNext: () -> Void + private let header = RadialGradientView(backgroundColor: .purple) + private let nextButton = ShadowButton(title: S.RecoverWallet.next.localize(), type: .primary) + private let label = UILabel(font: .customBody(size: 16.0)) + private let illustration = UIImageView(image: #imageLiteral(resourceName: "RecoverWalletIllustration")) + + override func viewDidLoad() { + addSubviews() + addConstraints() + setData() + } + + private func addSubviews() { + view.addSubview(header) + header.addSubview(illustration) + view.addSubview(nextButton) + view.addSubview(label) + } + + private func addConstraints() { + header.constrainTopCorners(sidePadding: 0.0, topPadding: 0.0) + header.constrain([header.heightAnchor.constraint(equalToConstant: C.Sizes.largeHeaderHeight)]) + illustration.constrain([ + illustration.centerXAnchor.constraint(equalTo: header.centerXAnchor), + illustration.centerYAnchor.constraint(equalTo: header.centerYAnchor, constant: C.padding[2]), + ]) + label.constrain([ + label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + label.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + nextButton.constrain([ + nextButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + nextButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[3]), + nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + nextButton.heightAnchor.constraint(equalToConstant: C.Sizes.buttonHeight), + ]) + } + + private func setData() { + view.backgroundColor = .white + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.text = S.RecoverWallet.intro.localize() + nextButton.tap = didTapNext + title = S.RecoverWallet.header.localize() + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/RequestAmountViewController.swift b/litewallet/ViewControllers/RequestAmountViewController.swift new file mode 100644 index 000000000..7966efee6 --- /dev/null +++ b/litewallet/ViewControllers/RequestAmountViewController.swift @@ -0,0 +1,250 @@ +import UIKit + +private let qrSize: CGSize = .init(width: 186.0, height: 186.0) +private let smallButtonHeight: CGFloat = 32.0 +private let buttonPadding: CGFloat = 20.0 +private let smallSharePadding: CGFloat = 12.0 +private let largeSharePadding: CGFloat = 20.0 + +class RequestAmountViewController: UIViewController { + var presentEmail: PresentShare? + var presentText: PresentShare? + + init(wallet: BRWallet, store: Store) { + self.wallet = wallet + amountView = AmountViewController(store: store, + isPinPadExpandedAtLaunch: true, + hasAcceptedFees: false, + isRequesting: true) + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + private let amountView: AmountViewController + private let qrCode = UIImageView() + private let address = UILabel(font: .customBody(size: 14.0)) + private let addressPopout = InViewAlert(type: .primary) + private let share = ShadowButton(title: S.Receive.share.localize(), type: .tertiary, image: #imageLiteral(resourceName: "Share")) + private let sharePopout = InViewAlert(type: .secondary) + private let border = UIView() + private var topSharePopoutConstraint: NSLayoutConstraint? + private let wallet: BRWallet + + // MARK: - PinPad State + + private var amount: Satoshis? { + didSet { + setQrCode() + } + } + + override func viewDidLoad() { + addSubviews() + addConstraints() + setData() + addActions() + setupCopiedMessage() + setupShareButtons() + } + + private func addSubviews() { + view.addSubview(qrCode) + view.addSubview(address) + view.addSubview(addressPopout) + view.addSubview(share) + view.addSubview(sharePopout) + view.addSubview(border) + } + + private func addConstraints() { + addChildViewController(amountView, layout: { + amountView.view.constrain([ + amountView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + amountView.view.topAnchor.constraint(equalTo: view.topAnchor), + amountView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + }) + qrCode.constrain([ + qrCode.constraint(.width, constant: qrSize.width), + qrCode.constraint(.height, constant: qrSize.height), + qrCode.topAnchor.constraint(equalTo: amountView.view.bottomAnchor, constant: C.padding[2]), + qrCode.constraint(.centerX, toView: view), + ]) + address.constrain([ + address.constraint(toBottom: qrCode, constant: C.padding[1]), + address.constraint(.centerX, toView: view), + ]) + addressPopout.heightConstraint = addressPopout.constraint(.height, constant: 0.0) + addressPopout.constrain([ + addressPopout.constraint(toBottom: address, constant: 0.0), + addressPopout.constraint(.centerX, toView: view), + addressPopout.constraint(.width, toView: view), + addressPopout.heightConstraint, + ]) + share.constrain([ + share.constraint(toBottom: addressPopout, constant: C.padding[2]), + share.constraint(.centerX, toView: view), + share.constraint(.width, constant: qrSize.width), + share.constraint(.height, constant: smallButtonHeight), + ]) + sharePopout.heightConstraint = sharePopout.constraint(.height, constant: 0.0) + topSharePopoutConstraint = sharePopout.constraint(toBottom: share, constant: largeSharePadding) + sharePopout.constrain([ + topSharePopoutConstraint, + sharePopout.constraint(.centerX, toView: view), + sharePopout.constraint(.width, toView: view), + sharePopout.heightConstraint, + ]) + border.constrain([ + border.constraint(.width, toView: view), + border.constraint(toBottom: sharePopout, constant: 0.0), + border.constraint(.centerX, toView: view), + border.constraint(.height, constant: 1.0), + border.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[2]), + ]) + } + + private func setData() { + view.backgroundColor = .white + address.text = wallet.receiveAddress + address.textColor = .grayTextTint + border.backgroundColor = .secondaryBorder + qrCode.image = UIImage.qrCode(data: "\(wallet.receiveAddress)".data(using: .utf8)!, color: CIColor(color: .black))? + .resize(qrSize)! + share.isToggleable = true + sharePopout.clipsToBounds = true + } + + private func addActions() { + let gr = UITapGestureRecognizer(target: self, action: #selector(RequestAmountViewController.addressTapped)) + address.addGestureRecognizer(gr) + address.isUserInteractionEnabled = true + share.addTarget(self, action: #selector(RequestAmountViewController.shareTapped), for: .touchUpInside) + amountView.didUpdateAmount = { [weak self] amount in + self?.amount = amount + } + } + + private func setQrCode() { + guard let amount = amount else { return } + let request = PaymentRequest.requestString(withAddress: wallet.receiveAddress, forAmount: amount.rawValue) + qrCode.image = UIImage.qrCode(data: request.data(using: .utf8)!, color: CIColor(color: .black))? + .resize(qrSize)! + } + + private func setupCopiedMessage() { + let copiedMessage = UILabel(font: .customMedium(size: 14.0)) + copiedMessage.textColor = .white + copiedMessage.text = S.Receive.copied.localize() + copiedMessage.textAlignment = .center + addressPopout.contentView = copiedMessage + } + + private func setupShareButtons() { + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + let email = ShadowButton(title: S.Receive.emailButton.localize(), type: .tertiary) + let text = ShadowButton(title: S.Receive.textButton.localize(), type: .tertiary) + container.addSubview(email) + container.addSubview(text) + email.constrain([ + email.constraint(.leading, toView: container, constant: C.padding[2]), + email.constraint(.top, toView: container, constant: buttonPadding), + email.constraint(.bottom, toView: container, constant: -buttonPadding), + email.trailingAnchor.constraint(equalTo: container.centerXAnchor, constant: -C.padding[1]), + ]) + text.constrain([ + text.constraint(.trailing, toView: container, constant: -C.padding[2]), + text.constraint(.top, toView: container, constant: buttonPadding), + text.constraint(.bottom, toView: container, constant: -buttonPadding), + text.leadingAnchor.constraint(equalTo: container.centerXAnchor, constant: C.padding[1]), + ]) + sharePopout.contentView = container + email.addTarget(self, action: #selector(RequestAmountViewController.emailTapped), for: .touchUpInside) + text.addTarget(self, action: #selector(RequestAmountViewController.textTapped), for: .touchUpInside) + } + + @objc private func shareTapped() { + toggle(alertView: sharePopout, shouldAdjustPadding: true) + if addressPopout.isExpanded { + toggle(alertView: addressPopout, shouldAdjustPadding: false) + } + } + + @objc private func addressTapped() { + guard let text = address.text else { return } + UIPasteboard.general.string = text + toggle(alertView: addressPopout, shouldAdjustPadding: false, shouldShrinkAfter: true) + if sharePopout.isExpanded { + toggle(alertView: sharePopout, shouldAdjustPadding: true) + } + } + + @objc private func emailTapped() { + guard let amount = amount else { return showErrorMessage(S.RequestAnAmount.noAmount.localize()) } + let text = PaymentRequest.requestString(withAddress: wallet.receiveAddress, forAmount: amount.rawValue) + presentEmail?(text, qrCode.image!) + } + + @objc private func textTapped() { + guard let amount = amount else { return showErrorMessage(S.RequestAnAmount.noAmount.localize()) } + let text = PaymentRequest.requestString(withAddress: wallet.receiveAddress, forAmount: amount.rawValue) + presentText?(text, qrCode.image!) + } + + private func toggle(alertView: InViewAlert, shouldAdjustPadding: Bool, shouldShrinkAfter: Bool = false) + { + share.isEnabled = false + address.isUserInteractionEnabled = false + + var deltaY = alertView.isExpanded ? -alertView.height : alertView.height + if shouldAdjustPadding { + if deltaY > 0 { + deltaY -= (largeSharePadding - smallSharePadding) + } else { + deltaY += (largeSharePadding - smallSharePadding) + } + } + + if alertView.isExpanded { + alertView.contentView?.isHidden = true + } + + UIView.spring(C.animationDuration, animations: { + if shouldAdjustPadding { + let newPadding = self.sharePopout.isExpanded ? largeSharePadding : smallSharePadding + self.topSharePopoutConstraint?.constant = newPadding + } + alertView.toggle() + self.parent?.view.layoutIfNeeded() + }, completion: { _ in + alertView.isExpanded = !alertView.isExpanded + self.share.isEnabled = true + self.address.isUserInteractionEnabled = true + alertView.contentView?.isHidden = false + if shouldShrinkAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + if alertView.isExpanded { + self.toggle(alertView: alertView, shouldAdjustPadding: shouldAdjustPadding) + } + } + } + }) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension RequestAmountViewController: ModalDisplayable { + var faqArticleId: String? { + return ArticleIds.nothing + } + + var modalTitle: String { + return S.Receive.request.localize() + } +} diff --git a/litewallet/ViewControllers/RootModals/ManageWalletViewController.swift b/litewallet/ViewControllers/RootModals/ManageWalletViewController.swift new file mode 100644 index 000000000..b47dee2b4 --- /dev/null +++ b/litewallet/ViewControllers/RootModals/ManageWalletViewController.swift @@ -0,0 +1,147 @@ +import UIKit + +class ManageWalletViewController: UIViewController, ModalPresentable, Subscriber { + var parentView: UIView? // ModalPresentable + private let textFieldLabel = UILabel(font: .customBold(size: 14.0), color: .grayTextTint) + private let textField = UITextField() + private let separator = UIView(color: .secondaryShadow) + fileprivate let body = UILabel.wrapping(font: .customBody(size: 13.0), color: .secondaryGrayText) + private let store: Store + fileprivate let maxWalletNameLength = 20 + + init(store: Store) { + self.store = store + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + addSubviews() + addConstraints() + setData() + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + saveWalletName() + } + + deinit { + NotificationCenter.default.removeObserver(self) + store.unsubscribe(self) + } + + private func addSubviews() { + view.addSubview(textFieldLabel) + view.addSubview(textField) + view.addSubview(separator) + view.addSubview(body) + } + + private func addConstraints() { + textFieldLabel.constrain([ + textFieldLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + textFieldLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: C.padding[2]), + textFieldLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + textField.constrain([ + textField.leadingAnchor.constraint(equalTo: textFieldLabel.leadingAnchor), + textField.topAnchor.constraint(equalTo: textFieldLabel.bottomAnchor), + textField.trailingAnchor.constraint(equalTo: textFieldLabel.trailingAnchor), + ]) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: textField.leadingAnchor), + separator.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: C.padding[2]), + separator.trailingAnchor.constraint(equalTo: textField.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + body.constrain([ + body.leadingAnchor.constraint(equalTo: separator.leadingAnchor), + body.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: C.padding[2]), + body.trailingAnchor.constraint(equalTo: separator.trailingAnchor), + body.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[2]), + ]) + } + + private func setData() { + view.backgroundColor = .white + textField.textColor = .darkText + textField.font = .customBody(size: 14.0) + textField.returnKeyType = .done + textFieldLabel.text = S.ManageWallet.textFieldLabel.localize() + textField.delegate = self + + textField.text = store.state.walletState.name + let creationDate = store.state.walletState.creationDate + if creationDate.timeIntervalSince1970 > 0 { + let df = DateFormatter() + df.dateFormat = "MMMM d, yyyy" + body.text = "\(S.ManageWallet.description.localize())\n\n\(S.ManageWallet.creationDatePrefix.localize()) \(df.string(from: creationDate))" + } else { + body.text = S.ManageWallet.description.localize() + } + } + + // MARK: - Keyboard Notifications + + @objc private func keyboardWillShow(notification: Notification) { + copyKeyboardChangeAnimation(notification: notification) + } + + @objc private func keyboardWillHide(notification: Notification) { + copyKeyboardChangeAnimation(notification: notification) + } + + private func copyKeyboardChangeAnimation(notification: Notification) { + guard let info = KeyboardNotificationInfo(notification.userInfo) else { return } + UIView.animate(withDuration: info.animationDuration, delay: 0, options: info.animationOptions, animations: { + guard let parentView = self.parentView else { return } + parentView.frame = parentView.frame.offsetBy(dx: 0, dy: info.deltaY) + }, completion: nil) + } + + func saveWalletName() { + guard var name = textField.text else { return } + if name.utf8.count > maxWalletNameLength { + name = String(name[.. Bool { + textField.resignFirstResponder() + return true + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn _: NSRange, replacementString string: String) -> Bool + { + guard let text = textField.text else { return true } + if text.utf8.count + string.utf8.count > maxWalletNameLength { + return false + } else { + return true + } + } +} + +extension ManageWalletViewController: ModalDisplayable { + var faqArticleId: String? { + return ArticleIds.nothing + } + + var modalTitle: String { + return S.ManageWallet.title.localize() + } +} diff --git a/litewallet/ViewControllers/RootModals/MenuViewController.swift b/litewallet/ViewControllers/RootModals/MenuViewController.swift new file mode 100644 index 000000000..cc6fa9a7e --- /dev/null +++ b/litewallet/ViewControllers/RootModals/MenuViewController.swift @@ -0,0 +1,76 @@ +import UIKit + +class MenuViewController: UIViewController, Trackable { + // MARK: - Public + + var didTapSecurity: (() -> Void)? + var didTapSupport: (() -> Void)? + var didTapSettings: (() -> Void)? + var didTapLock: (() -> Void)? + + // MARK: - Private + + fileprivate let buttonHeight: CGFloat = 72.0 + fileprivate let buttons: [MenuButton] = { + let types: [MenuButtonType] = [.security, .customerSupport, .settings, .lock] + return types.compactMap { + return MenuButton(type: $0) + } + }() + + fileprivate let bottomPadding: CGFloat = 32.0 + + override func viewDidLoad() { + var previousButton: UIView? + buttons.forEach { button in + button.addTarget(self, action: #selector(MenuViewController.didTapButton(button:)), for: .touchUpInside) + view.addSubview(button) + var topConstraint: NSLayoutConstraint? + if let viewAbove = previousButton { + topConstraint = button.constraint(toBottom: viewAbove, constant: 0.0) + } else { + topConstraint = button.constraint(.top, toView: view, constant: 0.0) + } + button.constrain([ + topConstraint, + button.constraint(.leading, toView: view, constant: 0.0), + button.constraint(.trailing, toView: view, constant: 0.0), + button.constraint(.height, constant: buttonHeight), + ]) + previousButton = button + } + + previousButton?.constrain([ + previousButton?.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[2]), + ]) + + if #available(iOS 11.0, *) { + view.backgroundColor = UIColor(named: "lfBackgroundColor") + } else { + view.backgroundColor = .white + } + } + + @objc private func didTapButton(button: MenuButton) { + switch button.type { + case .security: + didTapSecurity?() + case .customerSupport: + didTapSupport?() + case .settings: + didTapSettings?() + case .lock: + didTapLock?() + } + } +} + +extension MenuViewController: ModalDisplayable { + var faqArticleId: String? { + return nil + } + + var modalTitle: String { + return S.MenuViewController.modalTitle.localize() + } +} diff --git a/litewallet/ViewControllers/RootModals/ModalDisplayable.swift b/litewallet/ViewControllers/RootModals/ModalDisplayable.swift new file mode 100644 index 000000000..b3f553c6a --- /dev/null +++ b/litewallet/ViewControllers/RootModals/ModalDisplayable.swift @@ -0,0 +1,10 @@ +import UIKit + +protocol ModalDisplayable { + var modalTitle: String { get } + var faqArticleId: String? { get } +} + +protocol ModalPresentable { + var parentView: UIView? { get set } +} diff --git a/litewallet/ViewControllers/RootModals/ModalViewController.swift b/litewallet/ViewControllers/RootModals/ModalViewController.swift new file mode 100644 index 000000000..2a46b8c9b --- /dev/null +++ b/litewallet/ViewControllers/RootModals/ModalViewController.swift @@ -0,0 +1,158 @@ +import UIKit + +class ModalViewController: UIViewController, Subscriber { + // MARK: - Public + + var childViewController: UIViewController + + init(childViewController: T, store: Store) where T: ModalDisplayable { + self.childViewController = childViewController + modalInfo = childViewController + self.store = store + header = ModalHeaderView(title: modalInfo.modalTitle, style: .dark) + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + private let modalInfo: ModalDisplayable + private let headerHeight: CGFloat = 49.0 + fileprivate let header: ModalHeaderView + private let tapGestureRecognizer = UITapGestureRecognizer() + private let store: Store + private let scrollView = UIScrollView() + private let scrollViewContent = UIView() + + deinit { + store.unsubscribe(self) + } + + override func viewDidLoad() { + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(header) + view.addSubview(scrollView) + scrollView.addSubview(scrollViewContent) + addChild(childViewController) + scrollViewContent.addSubview(childViewController.view) + childViewController.didMove(toParent: self) + } + + private func addConstraints() { + header.constrain([ + header.leadingAnchor.constraint(equalTo: view.leadingAnchor), + header.bottomAnchor.constraint(equalTo: scrollView.topAnchor), + header.trailingAnchor.constraint(equalTo: view.trailingAnchor), + header.heightAnchor.constraint(equalToConstant: headerHeight), + ]) + scrollView.constrain([ + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + scrollViewContent.constrain([ + scrollViewContent.topAnchor.constraint(equalTo: scrollView.topAnchor), + scrollViewContent.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + scrollViewContent.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + scrollViewContent.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + scrollViewContent.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + ]) + + childViewController.view.constrain(toSuperviewEdges: nil) + + // Two stage layout is required here because we need the height constant + // of the content at initial layout + view.layoutIfNeeded() + let height = scrollViewContent.bounds.size.height + 60.0 + let minHeight = scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: height) + let midHeight = scrollView.heightAnchor.constraint(equalTo: scrollViewContent.heightAnchor) + let maxHeight = scrollView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor, constant: -headerHeight) + midHeight.priority = UILayoutPriority.defaultLow + scrollView.constrain([ + minHeight, + midHeight, + maxHeight, + ]) + } + + private func setInitialData() { + view.backgroundColor = .clear + scrollView.backgroundColor = UIColor.litecoinGray + + scrollView.delaysContentTouches = false + if var modalPresentable = childViewController as? ModalPresentable { + modalPresentable.parentView = view + } + + tapGestureRecognizer.delegate = self + tapGestureRecognizer.addTarget(self, action: #selector(didTap)) + view.addGestureRecognizer(tapGestureRecognizer) + store.subscribe(self, name: .blockModalDismissal, callback: { _ in + self.tapGestureRecognizer.isEnabled = false + }) + + store.subscribe(self, name: .unblockModalDismissal, callback: { _ in + self.tapGestureRecognizer.isEnabled = true + }) + addTopCorners() + header.closeCallback = { [weak self] in + if let delegate = self?.transitioningDelegate as? ModalTransitionDelegate { + delegate.reset() + } + self?.dismiss(animated: true, completion: {}) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.15 + view.layer.shadowRadius = 4.0 + view.layer.shadowOffset = .zero + } + + // Even though the status bar is hidden for this view, + // it still needs to be set to light as it will temporarily + // transition to black when this view gets presented + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override var prefersStatusBarHidden: Bool { + return true + } + + @objc private func didTap() { + guard let modalTransitionDelegate = transitioningDelegate as? ModalTransitionDelegate else { return } + modalTransitionDelegate.reset() + dismiss(animated: true, completion: nil) + } + + private func addTopCorners() { + let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 6.0, height: 6.0)).cgPath + let maskLayer = CAShapeLayer() + maskLayer.path = path + header.layer.mask = maskLayer + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension ModalViewController: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let location = gestureRecognizer.location(in: view) + if location.y < header.frame.minY { + return true + } else { + return false + } + } +} diff --git a/litewallet/ViewControllers/RootModals/ReceiveViewController.swift b/litewallet/ViewControllers/RootModals/ReceiveViewController.swift new file mode 100644 index 000000000..0a389cc7d --- /dev/null +++ b/litewallet/ViewControllers/RootModals/ReceiveViewController.swift @@ -0,0 +1,282 @@ +import UIKit + +private let qrSize: CGFloat = 186.0 +private let smallButtonHeight: CGFloat = 32.0 +private let buttonPadding: CGFloat = 20.0 +private let smallSharePadding: CGFloat = 12.0 +private let largeSharePadding: CGFloat = 20.0 + +typealias PresentShare = (String, UIImage) -> Void + +class ReceiveViewController: UIViewController, Subscriber, Trackable { + // MARK: - Public + + var presentEmail: PresentShare? + var presentText: PresentShare? + + init(wallet: BRWallet, store: Store, isRequestAmountVisible: Bool) { + self.wallet = wallet + self.isRequestAmountVisible = isRequestAmountVisible + self.store = store + LWAnalytics.logEventWithParameters(itemName: ._20202116_VRC) + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + private let qrCode = UIImageView() + private let address = UILabel(font: .customBody(size: 14.0)) + private let addressPopout = InViewAlert(type: .primary) + private let share = ShadowButton(title: S.Receive.share.localize(), type: .tertiary, image: #imageLiteral(resourceName: "Share")) + private let sharePopout = InViewAlert(type: .secondary) + private let border = UIView() + private let request = ShadowButton(title: S.Receive.request.localize(), type: .flatLitecoinBlue) + private let addressButton = UIButton(type: .system) + private var topSharePopoutConstraint: NSLayoutConstraint? + private let wallet: BRWallet + private let store: Store + private var balance: UInt64? { + didSet { + if let newValue = balance, let oldValue = oldValue { + if newValue > oldValue { + setReceiveAddress() + } + } + } + } + + fileprivate let isRequestAmountVisible: Bool + private var requestTop: NSLayoutConstraint? + private var requestBottom: NSLayoutConstraint? + + override func viewDidLoad() { + addSubviews() + addConstraints() + setStyle() + addActions() + setupCopiedMessage() + setupShareButtons() + store.subscribe(self, selector: { $0.walletState.balance != $1.walletState.balance }, callback: { + self.balance = $0.walletState.balance + }) + } + + private func addSubviews() { + view.addSubview(qrCode) + view.addSubview(address) + view.addSubview(addressPopout) + view.addSubview(share) + view.addSubview(sharePopout) + view.addSubview(border) + view.addSubview(request) + view.addSubview(addressButton) + } + + private func addConstraints() { + qrCode.constrain([ + qrCode.constraint(.width, constant: qrSize), + qrCode.constraint(.height, constant: qrSize), + qrCode.constraint(.top, toView: view, constant: C.padding[4]), + qrCode.constraint(.centerX, toView: view), + ]) + address.constrain([ + address.constraint(toBottom: qrCode, constant: C.padding[1]), + address.constraint(.centerX, toView: view), + ]) + addressPopout.heightConstraint = addressPopout.constraint(.height, constant: 0.0) + addressPopout.constrain([ + addressPopout.constraint(toBottom: address, constant: 0.0), + addressPopout.constraint(.centerX, toView: view), + addressPopout.constraint(.width, toView: view), + addressPopout.heightConstraint, + ]) + share.constrain([ + share.constraint(toBottom: addressPopout, constant: C.padding[2]), + share.constraint(.centerX, toView: view), + share.constraint(.width, constant: qrSize), + share.constraint(.height, constant: smallButtonHeight), + ]) + sharePopout.heightConstraint = sharePopout.constraint(.height, constant: 0.0) + topSharePopoutConstraint = sharePopout.constraint(toBottom: share, constant: largeSharePadding) + sharePopout.constrain([ + topSharePopoutConstraint, + sharePopout.constraint(.centerX, toView: view), + sharePopout.constraint(.width, toView: view), + sharePopout.heightConstraint, + ]) + border.constrain([ + border.constraint(.width, toView: view), + border.constraint(toBottom: sharePopout, constant: 0.0), + border.constraint(.centerX, toView: view), + border.constraint(.height, constant: 1.0), + ]) + requestTop = request.constraint(toBottom: border, constant: C.padding[3]) + requestBottom = request.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: E.isIPhoneX ? -C.padding[5] : -C.padding[2]) + request.constrain([ + requestTop, + request.constraint(.leading, toView: view, constant: C.padding[2]), + request.constraint(.trailing, toView: view, constant: -C.padding[2]), + request.constraint(.height, constant: C.Sizes.buttonHeight), + requestBottom, + ]) + addressButton.constrain([ + addressButton.leadingAnchor.constraint(equalTo: address.leadingAnchor, constant: -C.padding[1]), + addressButton.topAnchor.constraint(equalTo: qrCode.topAnchor), + addressButton.trailingAnchor.constraint(equalTo: address.trailingAnchor, constant: C.padding[1]), + addressButton.bottomAnchor.constraint(equalTo: address.bottomAnchor, constant: C.padding[1]), + ]) + } + + private func setStyle() { + view.backgroundColor = UIColor.litecoinGray + address.textColor = .liteWalletBlue + border.backgroundColor = .secondaryBorder + share.isToggleable = true + if !isRequestAmountVisible { + border.isHidden = true + request.isHidden = true + request.constrain([ + request.heightAnchor.constraint(equalToConstant: 0.0), + ]) + requestTop?.constant = 0.0 + requestBottom?.constant = 0.0 + } + sharePopout.clipsToBounds = true + addressButton.setBackgroundImage(UIImage.imageForColor(.secondaryShadow), for: .highlighted) + addressButton.layer.cornerRadius = 4.0 + addressButton.layer.masksToBounds = true + setReceiveAddress() + } + + private func setReceiveAddress() { + address.text = wallet.receiveAddress + qrCode.image = UIImage.qrCode(data: "\(address.text!)".data(using: .utf8)!, color: CIColor(color: .black))? + .resize(CGSize(width: qrSize, height: qrSize))! + } + + private func addActions() { + addressButton.tap = { [weak self] in + self?.addressTapped() + } + request.tap = { [weak self] in + guard let modalTransitionDelegate = self?.parent?.transitioningDelegate as? ModalTransitionDelegate else { return } + modalTransitionDelegate.reset() + self?.dismiss(animated: true, completion: { + self?.store.perform(action: RootModalActions.Present(modal: .requestAmount)) + }) + } + share.addTarget(self, action: #selector(ReceiveViewController.shareTapped), for: .touchUpInside) + } + + private func setupCopiedMessage() { + let copiedMessage = UILabel(font: .customMedium(size: 14.0)) + copiedMessage.textColor = .white + copiedMessage.text = S.Receive.copied.localize() + copiedMessage.textAlignment = .center + addressPopout.contentView = copiedMessage + } + + private func setupShareButtons() { + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + let email = ShadowButton(title: S.Receive.emailButton.localize(), type: .tertiary) + let text = ShadowButton(title: S.Receive.textButton.localize(), type: .tertiary) + container.addSubview(email) + container.addSubview(text) + email.constrain([ + email.constraint(.leading, toView: container, constant: C.padding[2]), + email.constraint(.top, toView: container, constant: buttonPadding), + email.constraint(.bottom, toView: container, constant: -buttonPadding), + email.trailingAnchor.constraint(equalTo: container.centerXAnchor, constant: -C.padding[1]), + ]) + text.constrain([ + text.constraint(.trailing, toView: container, constant: -C.padding[2]), + text.constraint(.top, toView: container, constant: buttonPadding), + text.constraint(.bottom, toView: container, constant: -buttonPadding), + text.leadingAnchor.constraint(equalTo: container.centerXAnchor, constant: C.padding[1]), + ]) + sharePopout.contentView = container + email.addTarget(self, action: #selector(ReceiveViewController.emailTapped), for: .touchUpInside) + text.addTarget(self, action: #selector(ReceiveViewController.textTapped), for: .touchUpInside) + } + + @objc private func shareTapped() { + toggle(alertView: sharePopout, shouldAdjustPadding: true) + if addressPopout.isExpanded { + toggle(alertView: addressPopout, shouldAdjustPadding: false) + } + } + + @objc private func addressTapped() { + guard let text = address.text else { return } + saveEvent("receive.copiedAddress") + UIPasteboard.general.string = text + toggle(alertView: addressPopout, shouldAdjustPadding: false, shouldShrinkAfter: true) + if sharePopout.isExpanded { + toggle(alertView: sharePopout, shouldAdjustPadding: true) + } + } + + @objc private func emailTapped() { + presentEmail?(address.text!, qrCode.image!) + } + + @objc private func textTapped() { + presentText?(address.text!, qrCode.image!) + } + + private func toggle(alertView: InViewAlert, shouldAdjustPadding: Bool, shouldShrinkAfter: Bool = false) + { + share.isEnabled = false + address.isUserInteractionEnabled = false + + var deltaY = alertView.isExpanded ? -alertView.height : alertView.height + if shouldAdjustPadding { + if deltaY > 0 { + deltaY -= (largeSharePadding - smallSharePadding) + } else { + deltaY += (largeSharePadding - smallSharePadding) + } + } + + if alertView.isExpanded { + alertView.contentView?.isHidden = true + } + + UIView.spring(C.animationDuration, animations: { + if shouldAdjustPadding { + let newPadding = self.sharePopout.isExpanded ? largeSharePadding : smallSharePadding + self.topSharePopoutConstraint?.constant = newPadding + } + alertView.toggle() + self.parent?.view.layoutIfNeeded() + }, completion: { _ in + alertView.isExpanded = !alertView.isExpanded + self.share.isEnabled = true + self.address.isUserInteractionEnabled = true + alertView.contentView?.isHidden = false + if shouldShrinkAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + if alertView.isExpanded { + self.toggle(alertView: alertView, shouldAdjustPadding: shouldAdjustPadding) + } + } + } + }) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension ReceiveViewController: ModalDisplayable { + var faqArticleId: String? { + return ArticleIds.nothing + } + + var modalTitle: String { + return S.Receive.title.localize() + } +} diff --git a/litewallet/ViewControllers/RootModals/SendViewController.swift b/litewallet/ViewControllers/RootModals/SendViewController.swift new file mode 100644 index 000000000..8c184dee6 --- /dev/null +++ b/litewallet/ViewControllers/RootModals/SendViewController.swift @@ -0,0 +1,555 @@ +import BRCore +import FirebaseAnalytics +import KeychainAccess +import LocalAuthentication +import SwiftUI +import UIKit + +typealias PresentScan = (@escaping ScanCompletion) -> Void + +class SendViewController: UIViewController, Subscriber, ModalPresentable, Trackable { + // MARK: - Public + + var presentScan: PresentScan? + var presentVerifyPin: ((String, @escaping VerifyPinCallback) -> Void)? + var onPublishSuccess: (() -> Void)? + var onResolvedSuccess: (() -> Void)? + var onResolutionFailure: ((String) -> Void)? + var parentView: UIView? // ModalPresentable + var initialAddress: String? + var isPresentedFromLock = false + var hasActivatedInlineFees: Bool = true + + // MARK: - Private + + private let store: Store + private let sender: Sender + private let walletManager: WalletManager + private let amountView: AmountViewController + private let sendAddressCell = AddressCell() + private let memoCell = DescriptionSendCell(placeholder: S.Send.descriptionLabel.localize()) + private var sendButtonCell = SendButtonHostingController() + private let currency: ShadowButton + private var balance: UInt64 = 0 + private var amount: Satoshis? + private var combinedFee: Satoshis? + private var didIgnoreUsedAddressWarning = false + private var didIgnoreIdentityNotCertified = false + private let initialRequest: PaymentRequest? + private let confirmTransitioningDelegate = TransitioningDelegate() + private var feeType: FeeType? + private let keychainPreferences = Keychain(service: "litewallet.user-prefs") + private var adjustmentHeight: CGFloat = 0.0 + private var buttonToBorder: CGFloat = 0.0 + + init(store: Store, sender: Sender, walletManager: WalletManager, initialAddress: String? = nil, initialRequest: PaymentRequest? = nil) + { + self.store = store + self.sender = sender + self.walletManager = walletManager + self.initialAddress = initialAddress + self.initialRequest = initialRequest + + currency = ShadowButton(title: S.Symbols.currencyButtonTitle(maxDigits: store.state.maxDigits), type: .tertiary) + + /// User Preference + if let opsPreference = keychainPreferences["hasAcceptedFees"], + opsPreference == "false" + { + hasActivatedInlineFees = false + } else { + keychainPreferences["has-accepted-fees"] = "true" + } + + amountView = AmountViewController(store: store, isPinPadExpandedAtLaunch: false, hasAcceptedFees: hasActivatedInlineFees) + + LWAnalytics.logEventWithParameters(itemName: ._20191105_VSC) + + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + deinit { + store.unsubscribe(self) + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + view.backgroundColor = UIColor.litecoinGray + + // set as regular at didLoad + walletManager.wallet?.feePerKb = store.state.fees.regular + + // polish parameters + memoCell.backgroundColor = UIColor.litecoinGray + amountView.view.backgroundColor = UIColor.litecoinGray + + view.addSubview(sendAddressCell) + view.addSubview(memoCell) + view.addSubview(sendButtonCell.view) + + sendAddressCell.invalidateIntrinsicContentSize() + sendAddressCell.constrainTopCorners(height: SendCell.defaultHeight) + + memoCell.constrain([ + memoCell.widthAnchor.constraint(equalTo: sendAddressCell.widthAnchor), + memoCell.topAnchor.constraint(equalTo: sendAddressCell.bottomAnchor), + memoCell.leadingAnchor.constraint(equalTo: sendAddressCell.leadingAnchor), + memoCell.heightAnchor.constraint(equalTo: memoCell.textView.heightAnchor, constant: C.padding[3]), + ]) + memoCell.accessoryView.constrain([ + memoCell.accessoryView.constraint(.width, constant: 0.0), + ]) + addChildViewController(amountView, layout: { + amountView.view.constrain([ + amountView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + amountView.view.topAnchor.constraint(equalTo: memoCell.bottomAnchor), + amountView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + }) + + sendButtonCell.view.constrain([ + sendButtonCell.view.constraint(.leading, toView: view), + sendButtonCell.view.constraint(.trailing, toView: view), + sendButtonCell.view.constraint(toBottom: amountView.view, constant: buttonToBorder), + sendButtonCell.view.constraint(.height, constant: C.Sizes.sendButtonHeight), + sendButtonCell.view + .bottomAnchor + .constraint(equalTo: view.bottomAnchor, constant: -C.padding[8]), + ]) + + addButtonActions() + store.subscribe(self, selector: { $0.walletState.balance != $1.walletState.balance }, + callback: { + if let balance = $0.walletState.balance { + self.balance = balance + } + }) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if initialAddress != nil { + amountView.expandPinPad() + } else if let initialRequest = initialRequest { + handleRequest(initialRequest) + } + } + + private func addButtonActions() { + // MARK: - MemoCell Callbacks + + memoCell.didReturn = { textView in + textView.resignFirstResponder() + } + memoCell.didBeginEditing = { [weak self] in + self?.amountView.closePinPad() + } + + // MARK: - AmountView Callbacks + + amountView.balanceTextForAmount = { [weak self] amount, rate in + self?.balanceTextForAmountWithFormattedFees(amount: amount, rate: rate) + } + + amountView.didUpdateAmount = { [weak self] amount in + self?.amount = amount + } + amountView.didUpdateFee = strongify(self) { myself, feeType in + myself.feeType = feeType + let fees = myself.store.state.fees + + switch feeType { + case .regular: myself.walletManager.wallet?.feePerKb = fees.regular + case .economy: myself.walletManager.wallet?.feePerKb = fees.economy + case .luxury: myself.walletManager.wallet?.feePerKb = fees.luxury + } + + myself.amountView.updateBalanceLabel() + } + + amountView.didChangeFirstResponder = { [weak self] isFirstResponder in + if isFirstResponder { + self?.memoCell.textView.resignFirstResponder() + self?.sendAddressCell.textField.resignFirstResponder() + /// copyKeyboardChangeAnimation(willShow: true, notification: notification) + } + } + + // MARK: - SendAddressView Model Callbacks / Actions + + sendAddressCell.paste.addTarget(self, action: #selector(SendViewController.pasteTapped), for: .touchUpInside) + sendAddressCell.scan.addTarget(self, action: #selector(SendViewController.scanTapped), for: .touchUpInside) + + sendAddressCell.didBeginEditing = strongify(self) { _ in + // myself.amountView.closePinPad() + } + + sendAddressCell.didEndEditing = strongify(self) { myself in + myself.resignFirstResponder() + } + + sendAddressCell.didReceivePaymentRequest = { [weak self] request in + self?.handleRequest(request) + } + + // MARK: - SendButton Model Callbacks / Actions + + sendButtonCell.rootView.doSendTransaction = { + guard let sendAddress = self.sendAddressCell.address else { return } + if sendAddress.isValidAddress { + self.sendTapped() + } else { + self.showAlert(title: S.LitewalletAlert.error.localize(), message: S.Transaction.invalid.localize(), + buttonLabel: S.Button.ok.localize()) + } + } + } + + private func balanceTextForAmountWithFormattedFees(amount: Satoshis?, rate: Rate?) -> (NSAttributedString?, NSAttributedString?) + { + // DEV: KCW 12-FEB-24 + // The results of this output is doing double duty and the method is a nightmare. + // The parent view controller uses the numbers and the text is used in this View Controller + + let balanceAmount = DisplayAmount(amount: Satoshis(rawValue: balance), state: store.state, selectedRate: rate, minimumFractionDigits: 2) + let balanceText = balanceAmount.description + + let balanceOutput = String(format: S.Send.balance.localize(), balanceText) + var combinedFeesOutput = "" + var balanceColor: UIColor = .grayTextTint + + let balanceStyle = [ + NSAttributedString.Key.font: UIFont.customBody(size: 14.0), + NSAttributedString.Key.foregroundColor: balanceColor, + ] + + /// Check the amount is greater than zero and if user is opting out of fees + if let amount = amount, amount > 0 { + let tieredOpsFee = tieredOpsFee(amount: amount.rawValue) + let totalAmountToCalculateFees = (amount.rawValue + tieredOpsFee) + + let networkFee = sender.feeForTx(amount: totalAmountToCalculateFees) + + let networkFeeAmount = DisplayAmount(amount: Satoshis(rawValue: networkFee), + state: store.state, + selectedRate: rate, + minimumFractionDigits: 2).description + + let serviceFeeAmount = DisplayAmount(amount: Satoshis(rawValue: tieredOpsFee), + state: store.state, + selectedRate: rate, + minimumFractionDigits: 2).description + + let totalFeeAmount = DisplayAmount(amount: Satoshis(rawValue: networkFee + tieredOpsFee), + state: store.state, + selectedRate: rate, + minimumFractionDigits: 2).description + let combinedfeeText = networkFeeAmount.description.replacingZeroFeeWithTenCents() + + serviceFeeAmount.description.replacingZeroFeeWithTenCents() + + totalFeeAmount.description.replacingZeroFeeWithTenCents() + + combinedFeesOutput = "(\(S.Send.networkFee.localize()) + \(S.Send.serviceFee.localize())): \(networkFeeAmount) + \(serviceFeeAmount) = \(totalFeeAmount)" + + if balance >= (networkFee + tieredOpsFee), amount.rawValue > (balance - (networkFee + tieredOpsFee)) { + balanceColor = .litewalletOrange + } + } + + return (NSAttributedString(string: balanceOutput, attributes: balanceStyle), NSAttributedString(string: combinedFeesOutput, attributes: balanceStyle)) + } + + @objc private func pasteTapped() { + guard let pasteboard = UIPasteboard.general.string, !pasteboard.utf8.isEmpty + else { + return showAlert(title: S.Send.invalidAddressTitle.localize(), message: S.Send.noAddress.localize(), buttonLabel: S.Button.ok.localize()) + } + guard let request = PaymentRequest(string: pasteboard) + else { + return showAlert(title: S.Send.invalidAddressTitle.localize(), message: S.Send.noAddress.localize(), buttonLabel: S.Button.ok.localize()) + } + + handleRequest(request) + sendAddressCell.textField.text = pasteboard + sendAddressCell.textField.layoutIfNeeded() + } + + @objc private func scanTapped() { + memoCell.textView.resignFirstResponder() + + presentScan? { [weak self] paymentRequest in + guard let request = paymentRequest else { return } + guard let destinationAddress = paymentRequest?.toAddress else { return } + + self?.handleRequest(request) + self?.sendAddressCell.textField.text = destinationAddress + } + } + + @objc private func sendTapped() { + if sendAddressCell.textField.isFirstResponder { + sendAddressCell.textField.resignFirstResponder() + } + let bareAmount: Satoshis? + if sender.transaction == nil { + guard let address = sendAddressCell.address else { + return showAlert(title: S.LitewalletAlert.error.localize(), + message: S.Send.noAddress.localize(), buttonLabel: S.Button.ok.localize()) + } + + if !address.isValidAddress { + return showAlert(title: S.LitewalletAlert.error.localize(), + message: S.Send.noAddress.localize(), + buttonLabel: S.Button.ok.localize()) + } + + guard var amountToSend = amount + else { + return showAlert(title: S.LitewalletAlert.error.localize(), + message: S.Send.noAmount.localize(), + buttonLabel: S.Button.ok.localize()) + } + + let opsFeeAmount = Satoshis(rawValue: tieredOpsFee(amount: amountToSend.rawValue)) + let fee = walletManager.wallet?.feeForTx(amount: amountToSend.rawValue + opsFeeAmount.rawValue) + let feeInSatoshis = Satoshis(rawValue: fee ?? 0) + bareAmount = amountToSend + + /// Set ops fees + if hasActivatedInlineFees { + amountToSend = amountToSend + opsFeeAmount + } + + if let minOutput = walletManager.wallet?.minOutputAmount { + guard amountToSend.rawValue >= minOutput + else { + let minOutputAmount = Amount(amount: minOutput, rate: Rate.empty, maxDigits: store.state.maxDigits) + let message = String(format: S.PaymentProtocol.Errors.smallPayment.localize(), + minOutputAmount.string(isLtcSwapped: store.state.isLtcSwapped)) + return showAlert(title: S.LitewalletAlert.error.localize(), + message: message, + buttonLabel: S.Button.ok.localize()) + } + } + guard !(walletManager.wallet?.containsAddress(address) ?? false) + else { + return showAlert(title: S.LitewalletAlert.error.localize(), + message: S.Send.containsAddress.localize(), + buttonLabel: S.Button.ok.localize()) + } + guard amountToSend.rawValue <= (walletManager.wallet?.maxOutputAmount ?? 0) + else { + return showAlert(title: S.LitewalletAlert.error.localize(), + message: S.Send.insufficientFunds.localize(), + buttonLabel: S.Button.ok.localize()) + } + + /// Set Ops or Single Output + if hasActivatedInlineFees { + guard let bareAmt = bareAmount?.rawValue, + sender.createTransactionWithOpsOutputs(amount: bareAmt, to: address) + else { + return showAlert(title: S.LitewalletAlert.error.localize(), + message: S.Send.createTransactionError.localize(), + buttonLabel: S.Button.ok.localize()) + } + } else { + guard let bareAmt = bareAmount?.rawValue, + sender.createTransaction(amount: bareAmt, to: address) + else { + return showAlert(title: S.LitewalletAlert.error.localize(), + message: S.Send.createTransactionError.localize(), + buttonLabel: S.Button.ok.localize()) + } + } + + let confirm = ConfirmationViewController(amount: bareAmount ?? Satoshis(0), + txFee: feeInSatoshis, + opsFee: opsFeeAmount, + feeType: feeType ?? .regular, state: store.state, + selectedRate: amountView.selectedRate, + minimumFractionDigits: amountView.minimumFractionDigits, + address: address, isUsingBiometrics: sender.canUseBiometrics) + + confirm.successCallback = { + confirm.dismiss(animated: true, completion: { + self.send() + }) + } + confirm.cancelCallback = { + confirm.dismiss(animated: true, completion: { + self.sender.transaction = nil + }) + } + confirmTransitioningDelegate.shouldShowMaskView = false + confirm.transitioningDelegate = confirmTransitioningDelegate + confirm.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + confirm.modalPresentationCapturesStatusBarAppearance = true + present(confirm, animated: true, completion: nil) + + } else { + NSLog("Error: transaction is nil") + } + } + + private func handleRequest(_ request: PaymentRequest) { + switch request.type { + case .local: + + if let amount = request.amount { + amountView.forceUpdateAmount(amount: amount) + } + if request.label != nil { + memoCell.content = request.label + } + + case .remote: + let loadingView = BRActivityViewController(message: S.Send.loadingRequest.localize()) + present(loadingView, animated: true, completion: nil) + request.fetchRemoteRequest(completion: { [weak self] request in + DispatchQueue.main.async { + loadingView.dismiss(animated: true, completion: { + if let paymentProtocolRequest = request?.paymentProtocolRequest { + self?.confirmProtocolRequest(protoReq: paymentProtocolRequest) + } else { + self?.showErrorMessage(S.Send.remoteRequestError.localize()) + } + }) + } + }) + } + } + + private func send() { + guard let rate = store.state.currentRate else { return } + guard let feePerKb = walletManager.wallet?.feePerKb else { return } + + sender.send(biometricsMessage: S.VerifyPin.touchIdMessage.localize(), + rate: rate, + comment: memoCell.textView.text, + feePerKb: feePerKb, + verifyPinFunction: { [weak self] pinValidationCallback in + self?.presentVerifyPin?(S.VerifyPin.authorize.localize()) { [weak self] pin, vc in + if pinValidationCallback(pin) { + vc.dismiss(animated: true, completion: { + self?.parent?.view.isFrameChangeBlocked = false + }) + return true + } else { + return false + } + } + }, completion: { [weak self] result in + switch result { + case .success: + self?.dismiss(animated: true, completion: { + guard let myself = self else { return } + myself.store.trigger(name: .showStatusBar) + if myself.isPresentedFromLock { + myself.store.trigger(name: .loginFromSend) + } + myself.onPublishSuccess?() + }) + self?.saveEvent("send.success") + self?.sendAddressCell.textField.text = "" + self?.memoCell.textView.text = "" + LWAnalytics.logEventWithParameters(itemName: ._20191105_DSL) + + case let .creationError(message): + self?.showAlert(title: S.Send.createTransactionError.localize(), message: message, buttonLabel: S.Button.ok.localize()) + self?.saveEvent("send.publishFailed", attributes: ["errorMessage": message]) + case let .publishFailure(error): + if case let .posixError(code, description) = error { + self?.showAlert(title: S.SecurityAlerts.sendFailure.localize(), message: "\(description) (\(code))", buttonLabel: S.Button.ok.localize()) + self?.saveEvent("send.publishFailed", attributes: ["errorMessage": "\(description) (\(code))"]) + } + } + }) + } + + func confirmProtocolRequest(protoReq: PaymentProtocolRequest) { + guard let firstOutput = protoReq.details.outputs.first else { return } + guard let wallet = walletManager.wallet else { return } + + let address = firstOutput.updatedSwiftAddress + let isValid = protoReq.isValid() + var isOutputTooSmall = false + + if let errorMessage = protoReq.errorMessage, errorMessage == S.PaymentProtocol.Errors.requestExpired.localize(), !isValid + { + return showAlert(title: S.PaymentProtocol.Errors.badPaymentRequest.localize(), message: errorMessage, buttonLabel: S.Button.ok.localize()) + } + + // TODO: check for duplicates of already paid requests + var requestAmount = Satoshis(0) + protoReq.details.outputs.forEach { output in + if output.amount > 0, output.amount < wallet.minOutputAmount { + isOutputTooSmall = true + } + requestAmount += output.amount + } + + if wallet.containsAddress(address) { + return showAlert(title: S.LitewalletAlert.warning.localize(), message: S.Send.containsAddress.localize(), buttonLabel: S.Button.ok.localize()) + } else if wallet.addressIsUsed(address), !didIgnoreUsedAddressWarning { + let message = "\(S.Send.UsedAddress.title.localize())\n\n\(S.Send.UsedAddress.firstLine.localize())\n\n\(S.Send.UsedAddress.secondLine.localize())" + return showError(title: S.LitewalletAlert.warning.localize(), message: message, ignore: { [weak self] in + self?.didIgnoreUsedAddressWarning = true + self?.confirmProtocolRequest(protoReq: protoReq) + }) + } else if let message = protoReq.errorMessage, !message.utf8.isEmpty, (protoReq.commonName?.utf8.count)! > 0, !didIgnoreIdentityNotCertified + { + return showError(title: S.Send.identityNotCertified.localize(), message: message, ignore: { [weak self] in + self?.didIgnoreIdentityNotCertified = true + self?.confirmProtocolRequest(protoReq: protoReq) + }) + } else if requestAmount < wallet.minOutputAmount { + let amount = Amount(amount: wallet.minOutputAmount, rate: Rate.empty, maxDigits: store.state.maxDigits) + let message = String(format: S.PaymentProtocol.Errors.smallPayment.localize(), amount.bits) + return showAlert(title: S.PaymentProtocol.Errors.smallOutputErrorTitle.localize(), message: message, buttonLabel: S.Button.ok.localize()) + } else if isOutputTooSmall { + let amount = Amount(amount: wallet.minOutputAmount, rate: Rate.empty, maxDigits: store.state.maxDigits) + let message = String(format: S.PaymentProtocol.Errors.smallTransaction.localize(), amount.bits) + return showAlert(title: S.PaymentProtocol.Errors.smallOutputErrorTitle.localize(), message: message, buttonLabel: S.Button.ok.localize()) + } + + if requestAmount > 0 { + amountView.forceUpdateAmount(amount: requestAmount) + } + memoCell.content = protoReq.details.memo + + if requestAmount == 0 { + if let amount = amount { + guard sender.createTransaction(amount: amount.rawValue, to: address) + else { + return showAlert(title: S.LitewalletAlert.error.localize(), message: S.Send.createTransactionError.localize(), buttonLabel: S.Button.ok.localize()) + } + } + } + } + + private func showError(title: String, message: String, ignore: @escaping () -> Void) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: S.Button.ignore.localize(), style: .default, handler: { _ in + ignore() + })) + alertController.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .cancel, handler: nil)) + present(alertController, animated: true, completion: nil) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SendViewController: ModalDisplayable { + var faqArticleId: String? { + return ArticleIds.nothing + } + + var modalTitle: String { + return S.Send.title.localize() + } +} diff --git a/litewallet/ViewControllers/ScanViewController.swift b/litewallet/ViewControllers/ScanViewController.swift new file mode 100644 index 000000000..4690c5b96 --- /dev/null +++ b/litewallet/ViewControllers/ScanViewController.swift @@ -0,0 +1,233 @@ +import AVFoundation +import UIKit + +typealias ScanCompletion = (PaymentRequest?) -> Void +typealias KeyScanCompletion = (String) -> Void + +class ScanViewController: UIViewController, Trackable { + // TODO: Add a storyboard + @IBOutlet var cameraOverlayView: UIView! + @IBOutlet var toolbarView: UIView! + @IBOutlet var overlayViewTitleLabel: UILabel! + + @IBOutlet var closeButton: UIButton! + @IBOutlet var flashButton: UIButton! + + static func presentCameraUnavailableAlert(fromRoot: UIViewController) { + let alertController = UIAlertController(title: S.Send.cameraUnavailableTitle.localize(), message: S.Send.cameraUnavailableMessage.localize(), preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: S.Button.cancel.localize(), style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: S.Button.settings.localize(), style: .default, handler: { _ in + if let appSettings = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(appSettings, options: [:], completionHandler: nil) + } + })) + fromRoot.present(alertController, animated: true, completion: nil) + } + + static var isCameraAllowed: Bool { + return AVCaptureDevice.authorizationStatus(for: AVMediaType.video) != .denied + } + + let completion: ScanCompletion? + let scanKeyCompletion: KeyScanCompletion? + let isValidURI: (String) -> Bool + + fileprivate let guide = CameraGuideView() + fileprivate let session = AVCaptureSession() + private let toolbar = UIView() + private let close = UIButton.close + private let flash = UIButton.icon(image: UIImage(named: "flashIcon")!, accessibilityLabel: S.Scanner.flashButtonLabel.localize()) + fileprivate var currentUri = "" + + init(completion: @escaping ScanCompletion, isValidURI: @escaping (String) -> Bool) { + self.completion = completion + scanKeyCompletion = nil + self.isValidURI = isValidURI + super.init(nibName: nil, bundle: nil) + } + + init(scanKeyCompletion: @escaping KeyScanCompletion, isValidURI: @escaping (String) -> Bool) { + self.scanKeyCompletion = scanKeyCompletion + completion = nil + self.isValidURI = isValidURI + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + view.backgroundColor = .black + toolbar.backgroundColor = .secondaryButton + + view.addSubview(toolbar) + toolbar.addSubview(close) + toolbar.addSubview(flash) + view.addSubview(guide) + + toolbar.constrainBottomCorners(sidePadding: 0, bottomPadding: 0) + if E.isIPhoneX { + toolbar.constrain([toolbar.constraint(.height, constant: 60.0)]) + + close.constrain([ + close.constraint(.leading, toView: toolbar), + close.constraint(.top, toView: toolbar, constant: 2.0), + close.constraint(.width, constant: 50.0), + close.constraint(.height, constant: 50.0), + ]) + + flash.constrain([ + flash.constraint(.trailing, toView: toolbar), + flash.constraint(.top, toView: toolbar, constant: 2.0), + flash.constraint(.width, constant: 50.0), + flash.constraint(.height, constant: 50.0), + ]) + } else { + toolbar.constrain([toolbar.constraint(.height, constant: 60.0)]) + + close.constrain([ + close.constraint(.leading, toView: toolbar, constant: 10.0), + close.constraint(.top, toView: toolbar, constant: 2.0), + close.constraint(.bottom, toView: toolbar, constant: -2.0), + close.constraint(.width, constant: 50.0), + ]) + + flash.constrain([ + flash.constraint(.trailing, toView: toolbar, constant: -10.0), + flash.constraint(.top, toView: toolbar, constant: 2.0), + flash.constraint(.bottom, toView: toolbar, constant: -2.0), + flash.constraint(.width, constant: 50.0), + ]) + } + + guide.constrain([ + guide.constraint(.leading, toView: view, constant: C.padding[6]), + guide.constraint(.trailing, toView: view, constant: -C.padding[6]), + guide.constraint(.centerY, toView: view), + NSLayoutConstraint(item: guide, attribute: .width, relatedBy: .equal, toItem: guide, attribute: .height, multiplier: 1.0, constant: 0.0), + ]) + guide.transform = CGAffineTransform(scaleX: 0.0, y: 0.0) + + close.tap = { [weak self] in + self?.saveEvent("scan.dismiss") + self?.dismiss(animated: true, completion: { + self?.completion?(nil) + }) + } + + addCameraPreview() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIView.spring(0.8, animations: { + self.guide.transform = .identity + }, completion: { _ in }) + } + + private func addCameraPreview() { + guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { return } + guard let input = try? AVCaptureDeviceInput(device: device) else { return } + session.addInput(input) + let previewLayer = AVCaptureVideoPreviewLayer(session: session) + previewLayer.frame = view.bounds + view.layer.insertSublayer(previewLayer, at: 0) + + let output = AVCaptureMetadataOutput() + output.setMetadataObjectsDelegate(self, queue: .main) + session.addOutput(output) + + if output.availableMetadataObjectTypes.contains(where: { objectType in + objectType == AVMetadataObject.ObjectType.qr + }) { + output.metadataObjectTypes = [AVMetadataObject.ObjectType.qr] + } else { + print("no qr code support") + } + + DispatchQueue(label: "qrscanner").async { + self.session.startRunning() + } + + if device.hasTorch { + flash.tap = { [weak self] in + do { + try device.lockForConfiguration() + device.torchMode = device.torchMode == .on ? .off : .on + device.unlockForConfiguration() + if device.torchMode == .on { + self?.saveEvent("scan.torchOn") + } else { + self?.saveEvent("scan.torchOn") + } + } catch { + print("Camera Torch error: \(error)") + } + } + } + } + + override var prefersStatusBarHidden: Bool { + return true + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension ScanViewController: AVCaptureMetadataOutputObjectsDelegate { + func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) + { + if let data = metadataObjects as? [AVMetadataMachineReadableCodeObject] { + if data.isEmpty { + guide.state = .normal + } else { + data.forEach { + guard let uri = $0.stringValue + else { + NSLog("ERROR: URI String not found") + return + } + if completion != nil, guide.state != .positive { + handleURI(uri) + } else if scanKeyCompletion != nil, guide.state != .positive { + handleKey(uri) + } + } + } + } else { + NSLog("ERROR: data metadata objects not accessed") + } + } + + func handleURI(_ uri: String) { + if currentUri != uri { + currentUri = uri + if let paymentRequest = PaymentRequest(string: uri) { + saveEvent("scan.litecoinUri") + guide.state = .positive + // Add a small delay so the green guide will be seen + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.dismiss(animated: true, completion: { + self.completion?(paymentRequest) + }) + } + } else { + guide.state = .negative + } + } + } + + func handleKey(_ keyString: String) { + if isValidURI(keyString) { + saveEvent("scan.privateKey") + guide.state = .positive + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.dismiss(animated: true, completion: { + self.scanKeyCompletion?(keyString) + }) + } + } else { + guide.state = .negative + } + } +} diff --git a/litewallet/ViewControllers/SecurityCenter/SecurityCenterViewController.swift b/litewallet/ViewControllers/SecurityCenter/SecurityCenterViewController.swift new file mode 100644 index 000000000..acb4809da --- /dev/null +++ b/litewallet/ViewControllers/SecurityCenter/SecurityCenterViewController.swift @@ -0,0 +1,210 @@ +import LocalAuthentication +import UIKit + +private let headerHeight: CGFloat = 222.0 +private let fadeStart: CGFloat = 185.0 +private let fadeEnd: CGFloat = 160.0 + +class SecurityCenterViewController: UIViewController, Subscriber { + var didTapPin: (() -> Void)? { + didSet { + pinCell.tap = didTapPin + } + } + + var didTapBiometrics: (() -> Void)? { + didSet { + biometricsCell.tap = didTapBiometrics + } + } + + var didTapPaperKey: (() -> Void)? { + didSet { paperKeyCell.tap = didTapPaperKey } + } + + init(store: Store, walletManager: WalletManager) { + self.store = store + self.walletManager = walletManager + header = ModalHeaderView(title: S.SecurityCenter.title.localize(), style: .light, faqInfo: (store, ArticleIds.nothing)) + + if #available(iOS 11.0, *) { + shield.tintColor = UIColor(named: "labelTextColor") + headerBackground.backgroundColor = UIColor(named: "mainColor") + } + + super.init(nibName: nil, bundle: nil) + } + + fileprivate var headerBackgroundHeight: NSLayoutConstraint? + private var headerBackground = UIView() + private let header: ModalHeaderView + fileprivate var shield = UIImageView(image: #imageLiteral(resourceName: "shield")) + private let scrollView = UIScrollView() + private let info = UILabel(font: .customBody(size: 16.0)) + private let pinCell = SecurityCenterCell(title: S.SecurityCenter.Cells.pinTitle.localize(), descriptionText: S.SecurityCenter.Cells.pinDescription.localize()) + private let biometricsCell = SecurityCenterCell(title: LAContext.biometricType() == .face ? S.SecurityCenter.Cells.faceIdTitle.localize() : S.SecurityCenter.Cells.touchIdTitle.localize(), descriptionText: S.SecurityCenter.Cells.touchIdDescription.localize()) + private let paperKeyCell = SecurityCenterCell(title: S.SecurityCenter.Cells.paperKeyTitle.localize(), descriptionText: S.SecurityCenter.Cells.paperKeyDescription.localize()) + private var separator = UIView(color: .secondaryShadow) + private let store: Store + private let walletManager: WalletManager + fileprivate var didViewAppear = false + + deinit { + store.unsubscribe(self) + } + + override func viewDidLoad() { + setupSubviewProperties() + addSubviews() + addConstraints() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setPinAndPhraseChecks() + didViewAppear = false + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + didViewAppear = true + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + didViewAppear = false + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + private func setupSubviewProperties() { + if #available(iOS 11.0, *) { + guard let backgroundColor = UIColor(named: "lfBackgroundColor") + else { + NSLog("ERROR: Main color") + return + } + view.backgroundColor = backgroundColor + } else { + view.backgroundColor = .white + } + + header.closeCallback = { + self.dismiss(animated: true, completion: nil) + } + scrollView.alwaysBounceVertical = true + scrollView.panGestureRecognizer.delaysTouchesBegan = false + scrollView.delegate = self + info.text = S.SecurityCenter.info.localize() + info.numberOfLines = 0 + info.lineBreakMode = .byWordWrapping + header.backgroundColor = .clear + + setPinAndPhraseChecks() + store.subscribe(self, selector: { $0.isBiometricsEnabled != $1.isBiometricsEnabled }, callback: { + self.biometricsCell.isCheckHighlighted = $0.isBiometricsEnabled + }) + store.subscribe(self, selector: { $1.alert == .paperKeySet(callback: {}) + }, callback: { _ in + self.setPinAndPhraseChecks() // When paper phrase is confirmed, we need to update the check mark status + }) + } + + private func addSubviews() { + view.addSubview(scrollView) + scrollView.addSubview(headerBackground) + headerBackground.addSubview(header) + headerBackground.addSubview(shield) + scrollView.addSubview(pinCell) + scrollView.addSubview(biometricsCell) + scrollView.addSubview(paperKeyCell) + scrollView.addSubview(info) + } + + private func addConstraints() { + scrollView.constrain([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + headerBackground.constrain([ + headerBackground.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + headerBackground.topAnchor.constraint(equalTo: view.topAnchor), + headerBackground.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + headerBackground.widthAnchor.constraint(equalTo: view.widthAnchor), + ]) + headerBackgroundHeight = headerBackground.heightAnchor.constraint(equalToConstant: headerHeight) + headerBackground.constrain([headerBackgroundHeight]) + header.constrain([ + header.leadingAnchor.constraint(equalTo: view.leadingAnchor), + header.topAnchor.constraint(equalTo: headerBackground.topAnchor, constant: E.isIPhoneX ? 30.0 : 20.0), + header.trailingAnchor.constraint(equalTo: view.trailingAnchor), + header.heightAnchor.constraint(equalToConstant: C.Sizes.headerHeight), + ]) + shield.constrain([ + shield.centerXAnchor.constraint(equalTo: view.centerXAnchor), + shield.centerYAnchor.constraint(equalTo: headerBackground.centerYAnchor, constant: C.padding[3]), + ]) + info.constrain([ + info.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: C.padding[2]), + info.topAnchor.constraint(equalTo: headerBackground.bottomAnchor, constant: C.padding[2]), + info.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -C.padding[4]), + ]) + scrollView.addSubview(separator) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: C.padding[2]), + separator.topAnchor.constraint(equalTo: info.bottomAnchor, constant: C.padding[2]), + separator.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -C.padding[2]), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + pinCell.constrain([ + pinCell.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + pinCell.topAnchor.constraint(equalTo: separator.bottomAnchor), + pinCell.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + ]) + biometricsCell.constrain([ + biometricsCell.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + biometricsCell.topAnchor.constraint(equalTo: pinCell.bottomAnchor), + biometricsCell.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + ]) + paperKeyCell.constrain([ + paperKeyCell.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + paperKeyCell.topAnchor.constraint(equalTo: biometricsCell.bottomAnchor), + paperKeyCell.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + paperKeyCell.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -C.padding[2]), + ]) + + if !LAContext.isBiometricsAvailable { + biometricsCell.constrain([biometricsCell.heightAnchor.constraint(equalToConstant: 0.0)]) + } + } + + private func setPinAndPhraseChecks() { + pinCell.isCheckHighlighted = store.state.pinLength == 6 + paperKeyCell.isCheckHighlighted = !UserDefaults.walletRequiresBackup + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SecurityCenterViewController: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard didViewAppear else { return } // We don't want to be doing an stretchy header stuff during interactive pop gestures + let yOffset = scrollView.contentOffset.y + 20.0 + let newHeight = headerHeight - yOffset + headerBackgroundHeight?.constant = newHeight + + if newHeight < fadeStart { + let range = fadeStart - fadeEnd + let alpha = (newHeight - fadeEnd) / range + shield.alpha = max(alpha, 0.0) + } else { + shield.alpha = 1.0 + } + } +} diff --git a/litewallet/ViewControllers/SecurityCenter/UpdatePinViewController.swift b/litewallet/ViewControllers/SecurityCenter/UpdatePinViewController.swift new file mode 100644 index 000000000..ab00fe2c8 --- /dev/null +++ b/litewallet/ViewControllers/SecurityCenter/UpdatePinViewController.swift @@ -0,0 +1,289 @@ +import LocalAuthentication +import UIKit + +enum UpdatePinType { + case creationNoPhrase + case creationWithPhrase + case update +} + +class UpdatePinViewController: UIViewController, Subscriber { + // MARK: - Public + + var setPinSuccess: ((String) -> Void)? + var resetFromDisabledSuccess: (() -> Void)? + var resetFromDisabledWillSucceed: (() -> Void)? + + init(store: Store, walletManager: WalletManager, type: UpdatePinType, showsBackButton: Bool = true, phrase: String? = nil) + { + self.store = store + self.walletManager = walletManager + self.phrase = phrase + pinView = PinView(style: .create, length: store.state.pinLength) + self.showsBackButton = showsBackButton + self.type = type + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + private var header = UILabel.wrapping(font: .customBold(size: 26.0), color: .darkText) + private var instruction = UILabel.wrapping(font: .customBody(size: 14.0), color: .darkText) + private var caption = UILabel.wrapping(font: .customBody(size: 13.0), color: .secondaryGrayText) + private var pinView: PinView + private let pinPad = PinPadViewController(style: .clear, keyboardType: .pinPad, maxDigits: 0) + private let spacer = UIView() + private let store: Store + private let walletManager: WalletManager + private var step: Step = .verify { + didSet { + switch step { + case .verify: + instruction.text = isCreatingPin ? S.UpdatePin.createInstruction.localize() : S.UpdatePin.enterCurrent.localize() + caption.isHidden = true + case .new: + let instructionText = isCreatingPin ? S.UpdatePin.createInstruction.localize() : S.UpdatePin.enterNew.localize() + if instruction.text != instructionText { + instruction.pushNewText(instructionText) + } + header.text = S.UpdatePin.createTitle.localize() + caption.isHidden = false + case .confirmNew: + caption.isHidden = true + if isCreatingPin { + header.text = S.UpdatePin.createTitleConfirm.localize() + } else { + instruction.pushNewText(S.UpdatePin.reEnterNew.localize()) + } + } + } + } + + private var currentPin: String? + private var newPin: String? + private var phrase: String? + private let type: UpdatePinType + private var isCreatingPin: Bool { + return type != .update + } + + private let newPinLength = 6 + private let showsBackButton: Bool + + private enum Step { + case verify + case new + case confirmNew + } + + override func viewDidLoad() { + addSubviews() + addConstraints() + setData() + } + + private func addSubviews() { + view.addSubview(header) + view.addSubview(instruction) + view.addSubview(caption) + view.addSubview(pinView) + view.addSubview(spacer) + } + + private func addConstraints() { + header.constrain([ + header.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: C.padding[2]), + header.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + header.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + instruction.constrain([ + instruction.leadingAnchor.constraint(equalTo: header.leadingAnchor), + instruction.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + instruction.trailingAnchor.constraint(equalTo: header.trailingAnchor), + ]) + pinView.constrain([ + pinView.centerYAnchor.constraint(equalTo: spacer.centerYAnchor), + pinView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + pinView.widthAnchor.constraint(equalToConstant: pinView.width), + pinView.heightAnchor.constraint(equalToConstant: pinView.itemSize), + ]) + + addChildViewController(pinPad, layout: { + pinPad.view.constrainBottomCorners(sidePadding: 0.0, bottomPadding: 0.0) + pinPad.view.constrain([ + pinPad.view.heightAnchor.constraint(equalToConstant: pinPad.height), + pinPad.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[3]), + ]) + }) + spacer.constrain([ + spacer.topAnchor.constraint(equalTo: instruction.bottomAnchor), + spacer.bottomAnchor.constraint(equalTo: caption.topAnchor), + ]) + caption.constrain([ + caption.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + caption.bottomAnchor.constraint(equalTo: pinPad.view.topAnchor, constant: -C.padding[2]), + caption.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + } + + private func setData() { + caption.text = S.UpdatePin.caption.localize() + view.addSubview(spacer) + + header.textColor = .whiteTint + instruction.textColor = .whiteTint + caption.textColor = .whiteTint + if #available(iOS 11.0, *) { + guard let mainColor = UIColor(named: "mainColor") + else { + NSLog("ERROR: Custom color not found") + return + } + view.backgroundColor = mainColor + } else { + view.backgroundColor = .liteWalletBlue + } + header.text = isCreatingPin ? S.UpdatePin.createTitle.localize() : S.UpdatePin.updateTitle.localize() + instruction.text = isCreatingPin ? S.UpdatePin.createInstruction.localize() : S.UpdatePin.enterCurrent.localize() + + pinPad.ouputDidUpdate = { [weak self] text in + guard let step = self?.step else { return } + switch step { + case .verify: + self?.didUpdateForCurrent(pin: text) + case .new: + self?.didUpdateForNew(pin: text) + case .confirmNew: + self?.didUpdateForConfirmNew(pin: text) + } + } + + if isCreatingPin { + step = .new + caption.isHidden = false + } else { + caption.isHidden = true + } + + if !showsBackButton { + navigationItem.leftBarButtonItem = nil + navigationItem.hidesBackButton = true + } + } + + private func didUpdateForCurrent(pin: String) { + pinView.fill(pin.utf8.count) + if pin.utf8.count == store.state.pinLength { + if walletManager.authenticate(pin: pin) { + pushNewStep(.new) + currentPin = pin + replacePinView() + } else { + if walletManager.walletDisabledUntil > 0 { + dismiss(animated: true, completion: { + self.store.perform(action: RequireLogin()) + }) + } else { + clearAfterFailure() + } + } + } + } + + private func didUpdateForNew(pin: String) { + pinView.fill(pin.utf8.count) + if pin.utf8.count == newPinLength { + newPin = pin + pushNewStep(.confirmNew) + } + } + + private func didUpdateForConfirmNew(pin: String) { + guard let newPin = newPin else { return } + pinView.fill(pin.utf8.count) + if pin.utf8.count == newPinLength { + if pin == newPin { + didSetNewPin() + } else { + clearAfterFailure() + pushNewStep(.new) + } + } + } + + private func clearAfterFailure() { + pinPad.view.isUserInteractionEnabled = false + pinView.shake { [weak self] in + self?.pinPad.view.isUserInteractionEnabled = true + self?.pinView.fill(0) + } + pinPad.clear() + } + + private func replacePinView() { + pinView.removeFromSuperview() + pinView = PinView(style: .create, length: newPinLength) + view.addSubview(pinView) + pinView.constrain([ + pinView.centerYAnchor.constraint(equalTo: spacer.centerYAnchor), + pinView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + pinView.widthAnchor.constraint(equalToConstant: pinView.width), + pinView.heightAnchor.constraint(equalToConstant: pinView.itemSize), + ]) + } + + private func pushNewStep(_ newStep: Step) { + step = newStep + pinPad.clear() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.pinView.fill(0) + } + } + + private func didSetNewPin() { + DispatchQueue.walletQueue.async { [weak self] in + guard let newPin = self?.newPin else { return } + var success: Bool? = false + if let seedPhrase = self?.phrase { + success = self?.walletManager.forceSetPin(newPin: newPin, seedPhrase: seedPhrase) + } else if let currentPin = self?.currentPin { + success = self?.walletManager.changePin(newPin: newPin, pin: currentPin) + DispatchQueue.main.async { self?.store.trigger(name: .didUpgradePin) } + } else if self?.type == .creationNoPhrase { + success = self?.walletManager.forceSetPin(newPin: newPin) + } + + DispatchQueue.main.async { + if let success = success, success == true { + if self?.resetFromDisabledSuccess != nil { + self?.resetFromDisabledWillSucceed?() + self?.store.perform(action: SimpleReduxAlert.Show(.pinSet(callback: { [weak self] in + self?.dismiss(animated: true, completion: { + self?.resetFromDisabledSuccess?() + }) + }))) + } else { + self?.store.perform(action: SimpleReduxAlert.Show(.pinSet(callback: { [weak self] in + self?.setPinSuccess?(newPin) + if self?.type != .creationNoPhrase { + self?.parent?.dismiss(animated: true, completion: nil) + } + }))) + } + } else { + let alert = UIAlertController(title: S.UpdatePin.updateTitle.localize(), message: S.UpdatePin.setPinError.localize(), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: S.Button.ok.localize(), style: .default, handler: { [weak self] _ in + self?.clearAfterFailure() + self?.pushNewStep(.new) + })) + self?.present(alert, animated: true, completion: nil) + } + } + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/SettingsViewController.swift b/litewallet/ViewControllers/SettingsViewController.swift new file mode 100644 index 000000000..7bf3321c9 --- /dev/null +++ b/litewallet/ViewControllers/SettingsViewController.swift @@ -0,0 +1,174 @@ +// +// SettingsViewController.swift +// breadwallet +// +// Created by Adrian Corscadden on 2017-03-30. +// Copyright © 2017 breadwallet LLC. All rights reserved. +// +import LocalAuthentication +import UIKit + +class SettingsViewController: UITableViewController, CustomTitleView { + init(sections: [String], rows: [String: [Setting]], optionalTitle _: String? = nil) { + self.sections = sections + if UserDefaults.isBiometricsEnabled { + self.rows = rows + } else { + var tempRows = rows + let biometricsLimit = LAContext.biometricType() == .face ? S.Settings.faceIdLimit.localize() : S.Settings.touchIdLimit.localize() + tempRows["Manage"] = tempRows["Manage"]?.filter { $0.title != biometricsLimit } + self.rows = tempRows + } + + customTitle = S.Settings.title.localize() // optionalTitle ?? S.Settings.title.localize() + titleLabel.text = S.Settings.title.localize() // "optionalTitle" // ?? S.Settings.title.localize() + super.init(style: .plain) + } + + private let sections: [String] + private var rows: [String: [Setting]] + private let cellIdentifier = "CellIdentifier" + internal var titleLabel = UILabel(font: .customBold(size: 26.0), color: .darkText) + let customTitle: String + private var walletIsEmpty = true + + override func viewDidLoad() { + let headerView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 48.0)) + + guard let textColor = UIColor(named: "labelTextColor"), + let backGroundColor = UIColor(named: "lfBackgroundColor") + else { + NSLog("ERROR: Custom color not found") + return + } + + headerView.backgroundColor = backGroundColor + tableView.backgroundColor = backGroundColor + titleLabel = UILabel(font: .customBold(size: 26.0), color: textColor) + + headerView.addSubview(titleLabel) + titleLabel.constrain(toSuperviewEdges: UIEdgeInsets(top: 0, left: C.padding[2], bottom: 0, right: 0)) + tableView.register(SeparatorCell.self, forCellReuseIdentifier: cellIdentifier) + tableView.tableHeaderView = headerView + tableView.tableFooterView = UIView() + tableView.separatorStyle = .none + + addCustomTitle() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + override func numberOfSections(in _: UITableView) -> Int { + return sections.count + } + + override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + return rows[sections[section]]?.count ?? 0 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell + { + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) + + if let setting = rows[sections[indexPath.section]]?[indexPath.row] { + cell.textLabel?.text = setting.title + cell.textLabel?.font = .customBody(size: 16.0) + + let label = UILabel(font: .customMedium(size: 14.0), color: .grayTextTint) + label.text = setting.accessoryText?() + label.sizeToFit() + cell.accessoryView = label + if sections[indexPath.section] == "About" { + cell.selectionStyle = .none + } + + if #available(iOS 11.0, *), + let textColor = UIColor(named: "labelTextColor") + { + cell.textLabel?.textColor = textColor + label.textColor = textColor + } else { + cell.textLabel?.textColor = .darkText + } + } + return cell + } + + override func tableView(_: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let view = UIView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 44)) + let label = UILabel(font: .customBold(size: 14.0), color: .grayTextTint) + let separator = UIView() + + if #available(iOS 11.0, *), + let backgroundColor = UIColor(named: "lfBackgroundColor"), + let labelTextColor = UIColor(named: "labelTextColor") + { + view.backgroundColor = backgroundColor + label.textColor = labelTextColor + } else { + view.backgroundColor = .whiteTint + } + + view.addSubview(label) + switch sections[section] { + case "About": + label.text = S.Settings.about.localize() + case "Wallet": + label.text = S.Settings.wallet.localize() + case "Manage": + label.text = S.Settings.manage.localize() + case "Support": + label.text = S.Settings.support.localize() + case "Blockchain": + label.text = S.Settings.blockchain.localize() + default: + label.text = "" + } + + separator.backgroundColor = .secondaryShadow + view.addSubview(separator) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: view.leadingAnchor), + separator.bottomAnchor.constraint(equalTo: view.bottomAnchor), + separator.trailingAnchor.constraint(equalTo: view.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + + label.constrain([ + label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + label.bottomAnchor.constraint(equalTo: separator.topAnchor, constant: -4.0), + ]) + + return view + } + + override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + if let setting = rows[sections[indexPath.section]]?[indexPath.row] { + setting.callback() + } + } + + override func tableView(_: UITableView, heightForHeaderInSection _: Int) -> CGFloat { + return 47.0 + } + + override func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + return 48.0 + } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + didScrollForCustomTitle(yOffset: scrollView.contentOffset.y) + } + + override func scrollViewWillEndDragging(_: UIScrollView, withVelocity _: CGPoint, targetContentOffset: UnsafeMutablePointer) + { + scrollViewWillEndDraggingForCustomTitle(yOffset: targetContentOffset.pointee.y) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/ShareDataViewController.swift b/litewallet/ViewControllers/ShareDataViewController.swift new file mode 100644 index 000000000..6308f30cd --- /dev/null +++ b/litewallet/ViewControllers/ShareDataViewController.swift @@ -0,0 +1,79 @@ +import UIKit + +class ShareDataViewController: UIViewController { + init(store: Store) { + self.store = store + super.init(nibName: nil, bundle: nil) + } + + private let store: Store + private let titleLabel = UILabel(font: .customBold(size: 26.0), color: .darkText) + private let body = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private let label = UILabel(font: .customBold(size: 16.0), color: .darkText) + private let toggle = GradientSwitch() + private let separator = UIView(color: .secondaryShadow) + + override func viewDidLoad() { + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(titleLabel) + view.addSubview(body) + view.addSubview(label) + view.addSubview(toggle) + view.addSubview(separator) + } + + private func addConstraints() { + titleLabel.constrain([ + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: C.padding[2]), + ]) + body.constrain([ + body.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + body.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: C.padding[1]), + body.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + label.constrain([ + label.leadingAnchor.constraint(equalTo: body.leadingAnchor), + label.topAnchor.constraint(equalTo: body.bottomAnchor, constant: C.padding[3]), + ]) + toggle.constrain([ + toggle.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + toggle.centerYAnchor.constraint(equalTo: label.centerYAnchor), + ]) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: label.leadingAnchor), + separator.topAnchor.constraint(equalTo: toggle.bottomAnchor, constant: C.padding[2]), + separator.trailingAnchor.constraint(equalTo: toggle.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + } + + private func setInitialData() { + view.backgroundColor = .whiteTint + titleLabel.text = S.ShareData.header.localize() + body.text = S.ShareData.body.localize() + label.text = S.ShareData.toggleLabel.localize() + + if UserDefaults.hasAquiredShareDataPermission { + toggle.isOn = true + toggle.sendActions(for: .valueChanged) + } + + toggle.valueChanged = strongify(self) { myself in + UserDefaults.hasAquiredShareDataPermission = myself.toggle.isOn + if myself.toggle.isOn { + myself.store.trigger(name: .didEnableShareData) + } + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/StartPaperPhraseViewController.swift b/litewallet/ViewControllers/StartPaperPhraseViewController.swift new file mode 100644 index 000000000..e65eb9edc --- /dev/null +++ b/litewallet/ViewControllers/StartPaperPhraseViewController.swift @@ -0,0 +1,92 @@ +import UIKit + +class StartPaperPhraseViewController: UIViewController { + init(store: Store, callback: @escaping () -> Void) { + self.store = store + self.callback = callback + let buttonTitle = UserDefaults.walletRequiresBackup ? S.StartPaperPhrase.buttonTitle.localize() : S.StartPaperPhrase.againButtonTitle.localize() + button = ShadowButton(title: buttonTitle, type: .flatLitecoinBlue) + super.init(nibName: nil, bundle: nil) + explanation.textColor = .darkText + footer.textColor = .secondaryGrayText + } + + private let button: ShadowButton + private let illustration = UIImageView(image: #imageLiteral(resourceName: "PaperKey")) + private let pencil = UIImageView(image: #imageLiteral(resourceName: "Pencil")) + private var explanation = UILabel.wrapping(font: UIFont.barlowMedium(size: 20.0)) + private let store: Store + private let header = RadialGradientView(backgroundColor: .liteWalletBlue, offset: 64.0) + private var footer = UILabel.wrapping(font: .customBody(size: 13.0), color: .secondaryGrayText) + private let callback: () -> Void + + override func viewDidLoad() { + view.backgroundColor = .white + explanation.text = S.StartPaperPhrase.body.localize() + + addSubviews() + addConstraints() + button.tap = { [weak self] in + self?.callback() + } + if let writePaperPhraseDate = UserDefaults.writePaperPhraseDate { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate("MMMM d, yyyy") + footer.text = String(format: S.StartPaperPhrase.date.localize(), df.string(from: writePaperPhraseDate)) + footer.textAlignment = .center + } + } + + private func addSubviews() { + view.addSubview(header) + header.addSubview(illustration) + illustration.addSubview(pencil) + view.addSubview(explanation) + view.addSubview(button) + view.addSubview(footer) + } + + private func addConstraints() { + header.constrainTopCorners(sidePadding: 0, topPadding: 0) + header.constrain([ + header.constraint(.height, constant: 220.0), + ]) + illustration.constrain([ + illustration.constraint(.width, constant: 64.0), + illustration.constraint(.height, constant: 84.0), + illustration.constraint(.centerX, toView: header, constant: nil), + illustration.constraint(.bottom, toView: header, constant: -C.padding[4]), + ]) + pencil.constrain([ + pencil.constraint(.width, constant: 32.0), + pencil.constraint(.height, constant: 32.0), + pencil.constraint(.leading, toView: illustration, constant: 44.0), + pencil.constraint(.top, toView: illustration, constant: -4.0), + ]) + explanation.constrain([ + explanation.constraint(toBottom: header, constant: C.padding[3]), + explanation.constraint(.leading, toView: view, constant: C.padding[2]), + explanation.constraint(.trailing, toView: view, constant: -C.padding[2]), + ]) + button.constrain([ + button.leadingAnchor.constraint(equalTo: footer.leadingAnchor), + button.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -C.padding[2]), + button.trailingAnchor.constraint(equalTo: footer.trailingAnchor), + button.constraint(.height, constant: C.Sizes.buttonHeight), + ]) + footer.constrain([ + footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[2]), + footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/StartViewController.swift b/litewallet/ViewControllers/StartViewController.swift new file mode 100644 index 000000000..490602b9b --- /dev/null +++ b/litewallet/ViewControllers/StartViewController.swift @@ -0,0 +1,122 @@ +import UIKit + +class StartViewController: UIViewController { + // MARK: - Public + + init(store: Store, didTapCreate: @escaping () -> Void, didTapRecover: @escaping () -> Void) { + self.store = store + self.didTapRecover = didTapRecover + self.didTapCreate = didTapCreate + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Private + + private let message = UILabel(font: .barlowLight(size: 22), color: .whiteTint) + + private let create = ShadowButton(title: S.StartViewController.createButton.localize(), type: .flatWhite) + private let recover = ShadowButton(title: S.StartViewController.recoverButton.localize(), type: .flatLitecoinBlue) + private let store: Store + private let didTapRecover: () -> Void + private let didTapCreate: () -> Void + private let backgroundView: UIView = { + let view = UIView() + view.backgroundColor = .liteWalletBlue + return view + }() + + private var logo: UIImageView = { + let image = UIImageView(image: UIImage(named: "new-logotype-white")) + image.contentMode = .scaleAspectFit + image.alpha = 0.8 + return image + }() + + private let versionLabel = UILabel(font: .barlowMedium(size: 14), color: .transparentWhite) + + override func viewDidLoad() { + view.backgroundColor = .white + setData() + addSubviews() + addConstraints() + addButtonActions() + } + + private func setData() { + message.lineBreakMode = .byWordWrapping + message.numberOfLines = 0 + message.textAlignment = .center + versionLabel.text = AppVersion.string + versionLabel.textAlignment = .right + message.textColor = .white + versionLabel.textColor = .white + + if #available(iOS 11.0, *) { + guard let mainColor = UIColor(named: "mainColor") + else { + NSLog("ERROR: Custom color not found") + return + } + view.backgroundColor = mainColor + } else { + view.backgroundColor = .liteWalletBlue + } + } + + private func addSubviews() { + view.addSubview(backgroundView) + view.addSubview(logo) + view.addSubview(message) + view.addSubview(create) + view.addSubview(recover) + view.addSubview(versionLabel) + } + + private func addConstraints() { + backgroundView.constrain(toSuperviewEdges: nil) + + logo.constrain([ + logo.topAnchor.constraint(equalTo: view.centerYAnchor, constant: -120), + logo.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logo.constraint(.height, constant: 45), + logo.constraint(.width, constant: 201), + ]) + message.constrain([ + message.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50), + message.topAnchor.constraint(equalTo: logo.bottomAnchor, constant: C.padding[3]), + message.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50), + ]) + recover.constrain([ + recover.constraint(.leading, toView: view, constant: C.padding[2]), + recover.constraint(.bottom, toView: view, constant: -60), + recover.constraint(.trailing, toView: view, constant: -C.padding[2]), + recover.constraint(.height, constant: C.Sizes.buttonHeight), + ]) + create.constrain([ + create.constraint(toTop: recover, constant: -C.padding[2]), + create.constraint(.centerX, toView: recover, constant: nil), + create.constraint(.width, toView: recover, constant: nil), + create.constraint(.height, constant: C.Sizes.buttonHeight), + ]) + versionLabel.constrain([ + versionLabel.constraint(.top, toView: view, constant: 30), + versionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + versionLabel.widthAnchor.constraint(equalToConstant: 120.0), + versionLabel.heightAnchor.constraint(equalToConstant: 44.0), + ]) + } + + private func addButtonActions() { + recover.tap = didTapRecover + create.tap = didTapCreate + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/StartWipeWalletViewController.swift b/litewallet/ViewControllers/StartWipeWalletViewController.swift new file mode 100644 index 000000000..f69aa647f --- /dev/null +++ b/litewallet/ViewControllers/StartWipeWalletViewController.swift @@ -0,0 +1,85 @@ +import UIKit + +class StartWipeWalletViewController: UIViewController { + init(didTapNext: @escaping () -> Void) { + self.didTapNext = didTapNext + super.init(nibName: nil, bundle: nil) + } + + private let didTapNext: () -> Void + private let header = RadialGradientView(backgroundColor: .blue, offset: 64.0) + private let illustration = UIImageView(image: #imageLiteral(resourceName: "RestoreIllustration")) + private let message = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private let warning = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private let button = ShadowButton(title: S.RecoverWallet.next.localize(), type: .primary) + private let bullet = UIImageView(image: #imageLiteral(resourceName: "deletecircle")) + + override func viewDidLoad() { + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(header) + header.addSubview(illustration) + view.addSubview(message) + view.addSubview(warning) + view.addSubview(bullet) + view.addSubview(button) + } + + private func addConstraints() { + header.constrainTopCorners(sidePadding: 0, topPadding: 0) + header.constrain([ + header.constraint(.height, constant: 220.0), + ]) + illustration.constrain([ + illustration.constraint(.width, constant: 64.0), + illustration.constraint(.height, constant: 84.0), + illustration.constraint(.centerX, toView: header, constant: 0.0), + illustration.constraint(.centerY, toView: header, constant: C.padding[3]), + ]) + message.constrain([ + message.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + message.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + message.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + ]) + bullet.constrain([ + bullet.leadingAnchor.constraint(equalTo: message.leadingAnchor), + bullet.topAnchor.constraint(equalTo: message.bottomAnchor, constant: C.padding[4]), + bullet.widthAnchor.constraint(equalToConstant: 16.0), + bullet.heightAnchor.constraint(equalToConstant: 16.0), + ]) + warning.constrain([ + warning.leadingAnchor.constraint(equalTo: bullet.trailingAnchor, constant: C.padding[2]), + warning.topAnchor.constraint(equalTo: bullet.topAnchor, constant: 0.0), + warning.trailingAnchor.constraint(equalTo: message.trailingAnchor), + ]) + button.constrain([ + button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[3]), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -C.padding[4]), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[3]), + button.constraint(.height, constant: C.Sizes.buttonHeight), + ]) + } + + private func setInitialData() { + view.backgroundColor = .white + illustration.contentMode = .scaleAspectFill + message.text = S.WipeWallet.startMessage.localize() + warning.text = S.WipeWallet.startWarning.localize() + button.tap = { [weak self] in + self?.didTapNext() + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/VerifyPinViewController.swift b/litewallet/ViewControllers/VerifyPinViewController.swift new file mode 100644 index 000000000..064b12323 --- /dev/null +++ b/litewallet/ViewControllers/VerifyPinViewController.swift @@ -0,0 +1,157 @@ +import LocalAuthentication +import UIKit + +typealias VerifyPinCallback = (String, UIViewController) -> Bool + +protocol ContentBoxPresenter { + var contentBox: UIView { get } + var blurView: UIVisualEffectView { get } + var effect: UIBlurEffect { get } +} + +class VerifyPinViewController: UIViewController, ContentBoxPresenter { + init(bodyText: String, pinLength: Int, callback: @escaping VerifyPinCallback) { + self.bodyText = bodyText + self.callback = callback + self.pinLength = pinLength + pinView = PinView(style: .create, length: pinLength) + super.init(nibName: nil, bundle: nil) + } + + var didCancel: (() -> Void)? + let blurView = UIVisualEffectView() + let effect = UIBlurEffect(style: .dark) + let contentBox = UIView() + private let callback: VerifyPinCallback + private let pinPad = PinPadViewController(style: .clear, keyboardType: .pinPad, maxDigits: 0) + private let titleLabel = UILabel(font: .customBold(size: 17.0), color: .white) + private let body = UILabel(font: .customBody(size: 14.0), color: .white) + private let pinView: PinView + private let toolbar = UIView(color: .whiteTint) + private let toolbarBorder = UIView(color: .whiteTint) + private let cancel = UIButton(type: .system) + private let bodyText: String + private let pinLength: Int + + override func viewDidLoad() { + addSubviews() + addConstraints() + setupSubviews() + } + + private func addSubviews() { + view.addSubview(contentBox) + view.addSubview(toolbarBorder) + view.addSubview(toolbar) + toolbar.addSubview(cancel) + + contentBox.addSubview(titleLabel) + contentBox.addSubview(body) + contentBox.addSubview(pinView) + addChildViewController(pinPad, layout: { + pinPad.view.constrain([ + pinPad.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + pinPad.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: LAContext.biometricType() == .face ? -C.padding[3] : 0.0), + pinPad.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + pinPad.view.heightAnchor.constraint(equalToConstant: pinPad.height), + ]) + }) + } + + private func addConstraints() { + contentBox.constrain([ + contentBox.centerXAnchor.constraint(equalTo: view.centerXAnchor), + contentBox.bottomAnchor.constraint(equalTo: pinPad.view.topAnchor, constant: -C.padding[12]), + contentBox.widthAnchor.constraint(equalToConstant: 256.0), + ]) + titleLabel.constrainTopCorners(sidePadding: C.padding[2], topPadding: C.padding[2]) + body.constrain([ + body.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + body.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), + body.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + ]) + pinView.constrain([ + pinView.topAnchor.constraint(equalTo: body.bottomAnchor, constant: C.padding[2]), + pinView.centerXAnchor.constraint(equalTo: body.centerXAnchor), + pinView.widthAnchor.constraint(equalToConstant: pinView.width), + pinView.heightAnchor.constraint(equalToConstant: pinView.itemSize), + pinView.bottomAnchor.constraint(equalTo: contentBox.bottomAnchor, constant: -C.padding[2]), + ]) + toolbar.constrain([ + toolbar.leadingAnchor.constraint(equalTo: pinPad.view.leadingAnchor), + toolbar.bottomAnchor.constraint(equalTo: pinPad.view.topAnchor), + toolbar.trailingAnchor.constraint(equalTo: pinPad.view.trailingAnchor), + toolbar.heightAnchor.constraint(equalToConstant: 44.0), + ]) + cancel.constrain([ + cancel.centerYAnchor.constraint(equalTo: toolbar.centerYAnchor), + cancel.trailingAnchor.constraint(equalTo: toolbar.trailingAnchor, constant: -C.padding[2]), + ]) + toolbarBorder.constrain([ + toolbarBorder.leadingAnchor.constraint(equalTo: pinPad.view.leadingAnchor), + toolbarBorder.bottomAnchor.constraint(equalTo: toolbar.topAnchor), + toolbarBorder.trailingAnchor.constraint(equalTo: pinPad.view.trailingAnchor), + toolbarBorder.heightAnchor.constraint(equalToConstant: 1.0), + ]) + } + + private func setupSubviews() { + contentBox.backgroundColor = .liteWalletBlue + contentBox.layer.cornerRadius = 8.0 + contentBox.layer.borderWidth = 1.0 + contentBox.layer.borderColor = UIColor.secondaryShadow.cgColor + contentBox.layer.shadowColor = UIColor.black.cgColor + contentBox.layer.shadowOpacity = 0.15 + contentBox.layer.shadowRadius = 4.0 + contentBox.layer.shadowOffset = .zero + + toolbar.backgroundColor = .clear + + titleLabel.text = S.VerifyPin.title.localize() + body.text = bodyText + body.numberOfLines = 0 + body.lineBreakMode = .byWordWrapping + + pinPad.ouputDidUpdate = { [weak self] output in + guard let myself = self else { return } + let attemptLength = output.utf8.count + myself.pinView.fill(attemptLength) + myself.pinPad.isAppendingDisabled = attemptLength < myself.pinLength ? false : true + + if attemptLength == myself.pinLength { + if !myself.callback(output, myself) { + myself.authenticationFailed() + } else { + NSLog("FAILED") + } + } else { + NSLog("FAILED") + } + } + cancel.tap = { [weak self] in + self?.didCancel?() + self?.dismiss(animated: true, completion: nil) + } + cancel.setTitle(S.Button.cancel.localize(), for: .normal) + cancel.tintColor = .white + view.backgroundColor = .clear + } + + private func authenticationFailed() { + pinPad.view.isUserInteractionEnabled = false + pinView.shake { [weak self] in + self?.pinPad.view.isUserInteractionEnabled = true + self?.pinView.fill(0) + } + pinPad.clear() + } + + override var prefersStatusBarHidden: Bool { + return true + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewControllers/ViewControllerTransitions/DismissLoginAnimator.swift b/litewallet/ViewControllers/ViewControllerTransitions/DismissLoginAnimator.swift new file mode 100644 index 000000000..797f4330a --- /dev/null +++ b/litewallet/ViewControllers/ViewControllerTransitions/DismissLoginAnimator.swift @@ -0,0 +1,18 @@ +import UIKit + +class DismissLoginAnimator: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.4 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard transitionContext.isAnimated else { return } + let duration = transitionDuration(using: transitionContext) + guard let fromView = transitionContext.view(forKey: .from) else { assertionFailure("Missing from view"); return } + UIView.animate(withDuration: duration, animations: { + fromView.alpha = 0.0 + }) { _ in + transitionContext.completeTransition(true) + } + } +} diff --git a/litewallet/ViewControllers/ViewControllerTransitions/DismissModalAnimator.swift b/litewallet/ViewControllers/ViewControllerTransitions/DismissModalAnimator.swift new file mode 100644 index 000000000..e0669b4a3 --- /dev/null +++ b/litewallet/ViewControllers/ViewControllerTransitions/DismissModalAnimator.swift @@ -0,0 +1,23 @@ +import UIKit + +// TODO: - figure out who should own this +let blurView = UIVisualEffectView() + +class DismissModalAnimator: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.4 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard transitionContext.isAnimated else { return } + let duration = transitionDuration(using: transitionContext) + guard let fromView = transitionContext.view(forKey: .from) else { assertionFailure("Missing from view"); return } + + UIView.animate(withDuration: duration, animations: { + blurView.alpha = 0.0 // Preferrably, this would animatate .effect, but it's not playing nicely with UIPercentDrivenInteractiveTransition + fromView.frame = fromView.frame.offsetBy(dx: 0, dy: fromView.frame.height) + }, completion: { _ in + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + }) + } +} diff --git a/litewallet/ViewControllers/ViewControllerTransitions/LoginTransitionDelegate.swift b/litewallet/ViewControllers/ViewControllerTransitions/LoginTransitionDelegate.swift new file mode 100644 index 000000000..260a9a72f --- /dev/null +++ b/litewallet/ViewControllers/ViewControllerTransitions/LoginTransitionDelegate.swift @@ -0,0 +1,8 @@ +import UIKit + +class LoginTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { + func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? + { + return DismissLoginAnimator() + } +} diff --git a/litewallet/ViewControllers/ViewControllerTransitions/ModalTransitionDelegate.swift b/litewallet/ViewControllers/ViewControllerTransitions/ModalTransitionDelegate.swift new file mode 100644 index 000000000..6bd995a3d --- /dev/null +++ b/litewallet/ViewControllers/ViewControllerTransitions/ModalTransitionDelegate.swift @@ -0,0 +1,105 @@ +import UIKit + +enum ModalType { + case regular + case transactionDetail +} + +class ModalTransitionDelegate: NSObject, Subscriber { + // MARK: - Public + + init(type: ModalType, store: Store) { + self.type = type + self.store = store + super.init() + } + + func reset() { + isInteractive = false + presentedViewController = nil + if let panGr = panGestureRecognizer { + LWAnalytics.logEventWithParameters(itemName: ._20210427_HCIEEH) + UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.removeGestureRecognizer(panGr) + } + store.trigger(name: .showStatusBar) + } + + var shouldDismissInteractively = true + + // MARK: - Private + + fileprivate let type: ModalType + fileprivate let store: Store + fileprivate var isInteractive: Bool = false + fileprivate let interactiveTransition = UIPercentDrivenInteractiveTransition() + fileprivate var presentedViewController: UIViewController? + fileprivate var panGestureRecognizer: UIPanGestureRecognizer? + + private var yVelocity: CGFloat = 0.0 + private var progress: CGFloat = 0.0 + private let velocityThreshold: CGFloat = 50.0 + private let progressThreshold: CGFloat = 0.5 + + @objc fileprivate func didUpdate(gr: UIPanGestureRecognizer) { + guard shouldDismissInteractively else { return } + switch gr.state { + case .began: + isInteractive = true + presentedViewController?.dismiss(animated: true, completion: nil) + case .changed: + guard let vc = presentedViewController else { break } + let yOffset = gr.translation(in: vc.view).y + let progress = yOffset / vc.view.bounds.height + yVelocity = gr.velocity(in: vc.view).y + self.progress = progress + interactiveTransition.update(progress) + case .cancelled: + reset() + interactiveTransition.cancel() + case .ended: + if transitionShouldFinish { + reset() + interactiveTransition.finish() + } else { + isInteractive = false + interactiveTransition.cancel() + } + case .failed: + break + case .possible: + break + } + } + + private var transitionShouldFinish: Bool { + if progress > progressThreshold || yVelocity > velocityThreshold { + return true + } else { + return false + } + } +} + +extension ModalTransitionDelegate: UIViewControllerTransitioningDelegate { + func animationController(forPresented presented: UIViewController, presenting _: UIViewController, source _: UIViewController) -> UIViewControllerAnimatedTransitioning? + { + presentedViewController = presented + return PresentModalAnimator(shouldCoverBottomGap: type == .regular, completion: { + let panGr = UIPanGestureRecognizer(target: self, action: #selector(ModalTransitionDelegate.didUpdate(gr:))) + + LWAnalytics.logEventWithParameters(itemName: ._20210427_HCIEEH) + UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.removeGestureRecognizer(panGr) + self.panGestureRecognizer = panGr + }) + } + + func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? + { + return DismissModalAnimator() + } + + func interactionControllerForDismissal(using _: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? + { + return isInteractive ? interactiveTransition : nil + } +} diff --git a/litewallet/ViewControllers/ViewControllerTransitions/PinTransitioningDelegate.swift b/litewallet/ViewControllers/ViewControllerTransitions/PinTransitioningDelegate.swift new file mode 100644 index 000000000..de8527564 --- /dev/null +++ b/litewallet/ViewControllers/ViewControllerTransitions/PinTransitioningDelegate.swift @@ -0,0 +1,94 @@ +import UIKit + +private let duration: TimeInterval = 0.4 + +class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { + var shouldShowMaskView = true + + func animationController(forPresented _: UIViewController, presenting _: UIViewController, source _: UIViewController) -> UIViewControllerAnimatedTransitioning? + { + return PresentGenericAnimator(shouldShowMaskView: shouldShowMaskView) + } + + func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? + { + return DismissGenericAnimator() + } +} + +class PresentGenericAnimator: NSObject, UIViewControllerAnimatedTransitioning { + init(shouldShowMaskView: Bool) { + self.shouldShowMaskView = shouldShowMaskView + } + + private let shouldShowMaskView: Bool + + func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { + return duration + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + let duration = transitionDuration(using: transitionContext) + let container = transitionContext.containerView + guard let toView = transitionContext.view(forKey: .to) else { return } + guard let toVc = transitionContext.viewController(forKey: .to) as? ContentBoxPresenter else { return } + + let blurView = toVc.blurView + blurView.frame = container.frame + blurView.effect = nil + container.addSubview(blurView) + + let fromFrame = container.frame + let maskView = UIView(frame: CGRect(x: 0, y: fromFrame.height, width: fromFrame.width, height: 40.0)) + maskView.backgroundColor = .whiteTint + if shouldShowMaskView { + container.addSubview(maskView) + } + + let scaleFactor: CGFloat = 0.1 + let deltaX = toVc.contentBox.frame.width * (1 - scaleFactor) + let deltaY = toVc.contentBox.frame.height * (1 - scaleFactor) + let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor) + toVc.contentBox.transform = scale.translatedBy(x: -deltaX, y: deltaY / 2.0) + + let finalToViewFrame = toView.frame + toView.frame = toView.frame.offsetBy(dx: 0, dy: toView.frame.height) + container.addSubview(toView) + + UIView.spring(duration, animations: { + maskView.frame = CGRect(x: 0, y: fromFrame.height - 30.0, width: fromFrame.width, height: 40.0) + blurView.effect = toVc.effect + toView.frame = finalToViewFrame + toVc.contentBox.transform = .identity + }, completion: { _ in + maskView.removeFromSuperview() + transitionContext.completeTransition(true) + }) + } +} + +class DismissGenericAnimator: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { + return duration + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + let duration = transitionDuration(using: transitionContext) + guard let fromView = transitionContext.view(forKey: .from) else { assertionFailure("Missing from view"); return } + guard let fromVc = transitionContext.viewController(forKey: .from) as? ContentBoxPresenter else { return } + + UIView.animate(withDuration: duration, animations: { + fromVc.blurView.effect = nil + fromView.frame = fromView.frame.offsetBy(dx: 0, dy: fromView.frame.height) + + let scaleFactor: CGFloat = 0.1 + let deltaX = fromVc.contentBox.frame.width * (1 - scaleFactor) + let deltaY = fromVc.contentBox.frame.height * (1 - scaleFactor) + let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor) + fromVc.contentBox.transform = scale.translatedBy(x: -deltaX, y: deltaY / 2.0) + + }, completion: { _ in + transitionContext.completeTransition(true) + }) + } +} diff --git a/litewallet/ViewControllers/ViewControllerTransitions/PresentModalAnimator.swift b/litewallet/ViewControllers/ViewControllerTransitions/PresentModalAnimator.swift new file mode 100644 index 000000000..9d478a2b5 --- /dev/null +++ b/litewallet/ViewControllers/ViewControllerTransitions/PresentModalAnimator.swift @@ -0,0 +1,60 @@ +import UIKit + +class PresentModalAnimator: NSObject { + // MARK: - Public + + let heightOffset: CGFloat = 40.0 + init(shouldCoverBottomGap: Bool, completion: @escaping () -> Void) { + self.completion = completion + self.shouldCoverBottomGap = shouldCoverBottomGap + } + + // MARK: - Private + + fileprivate let completion: () -> Void + fileprivate let shouldCoverBottomGap: Bool +} + +extension PresentModalAnimator: UIViewControllerAnimatedTransitioning { + func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.4 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard transitionContext.isAnimated else { return } + let duration = transitionDuration(using: transitionContext) + guard let toView = transitionContext.view(forKey: .to) else { assertionFailure("Missing to view"); return } + let container = transitionContext.containerView + + blurView.frame = container.frame + blurView.effect = nil + blurView.alpha = 1.0 + container.addSubview(blurView) + + // This mask view is placed below the bottom of the modal being presented. + // It needs to be there to cover up the gap left below the modal during the + // spring animation. It looks weird if it isn't there. + let fromFrame = container.frame + var maskView: UIView? + if shouldCoverBottomGap { + maskView = UIView(frame: CGRect(x: 0, y: fromFrame.height, width: fromFrame.width, height: heightOffset)) + maskView?.backgroundColor = .white + container.addSubview(maskView!) + } + + let finalToViewFrame = toView.frame + toView.frame = toView.frame.offsetBy(dx: 0, dy: toView.frame.height) + container.addSubview(toView) + + UIView.spring(duration, animations: { + // fromFrame.height - 30.0 + maskView?.frame = CGRect(x: 0, y: fromFrame.height, width: fromFrame.width, height: self.heightOffset) + blurView.effect = UIBlurEffect(style: .dark) + toView.frame = finalToViewFrame + }, completion: { _ in + transitionContext.completeTransition(true) + maskView?.removeFromSuperview() + self.completion() + }) + } +} diff --git a/litewallet/ViewControllers/WelcomeViewController.swift b/litewallet/ViewControllers/WelcomeViewController.swift new file mode 100644 index 000000000..249cb3053 --- /dev/null +++ b/litewallet/ViewControllers/WelcomeViewController.swift @@ -0,0 +1,86 @@ +import UIKit + +class WelcomeViewController: UIViewController, ContentBoxPresenter { + let blurView = UIVisualEffectView() + let effect = UIBlurEffect(style: .dark) + let contentBox = UIView(color: .white) + + private let header = GradientView() + private let titleLabel = UILabel.wrapping(font: .customBody(size: 26.0), color: .darkText) + private let body = UILabel.wrapping(font: .customBody(size: 16.0), color: .darkText) + private let button = ShadowButton(title: S.Button.ok.localize(), type: .primary) + + override func viewDidLoad() { + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + view.addSubview(contentBox) + contentBox.addSubview(header) + contentBox.addSubview(titleLabel) + contentBox.addSubview(body) + contentBox.addSubview(button) + } + + private func addConstraints() { + contentBox.constrain([ + contentBox.centerXAnchor.constraint(equalTo: view.centerXAnchor), + contentBox.centerYAnchor.constraint(equalTo: view.centerYAnchor), + contentBox.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -C.padding[6]), + ]) + header.constrainTopCorners(height: 44.0) + titleLabel.constrain([ + titleLabel.leadingAnchor.constraint(equalTo: contentBox.leadingAnchor, constant: C.padding[2]), + titleLabel.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + titleLabel.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + ]) + + body.constrain([ + body.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + body.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: C.padding[2]), + body.trailingAnchor.constraint(equalTo: contentBox.trailingAnchor, constant: -C.padding[2]), + ]) + + button.constrain([ + button.leadingAnchor.constraint(equalTo: body.leadingAnchor), + button.topAnchor.constraint(equalTo: body.bottomAnchor, constant: C.padding[2]), + button.trailingAnchor.constraint(equalTo: body.trailingAnchor), + button.bottomAnchor.constraint(equalTo: contentBox.bottomAnchor, constant: -C.padding[2]), + ]) + } + + private func setInitialData() { + view.backgroundColor = .clear + contentBox.layer.cornerRadius = 6.0 + contentBox.layer.masksToBounds = true + titleLabel.textAlignment = .center + titleLabel.text = S.Welcome.title.localize() + setBodyText() + button.tap = strongify(self) { myself in + myself.dismiss(animated: true, completion: nil) + } + } + + private func setBodyText() { + let bodyText = S.Welcome.body.localize() + let attributedString = NSMutableAttributedString(string: S.Welcome.body.localize()) + let icon = NSTextAttachment() + icon.image = #imageLiteral(resourceName: "Faq") + icon.bounds = CGRect(x: 0, y: -3.0, width: body.font.pointSize, height: body.font.pointSize) + guard let range = S.Welcome.body.localize().range(of: "(?)") + else { + NSLog("ERROR: Range not found") + return + } + + let nsRange = bodyText.nsRange(from: range) + attributedString.replaceCharacters(in: nsRange, with: NSAttributedString(attachment: icon)) + body.attributedText = attributedString + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } +} diff --git a/litewallet/ViewControllers/WritePaperPhraseViewController.swift b/litewallet/ViewControllers/WritePaperPhraseViewController.swift new file mode 100644 index 000000000..14218dbd8 --- /dev/null +++ b/litewallet/ViewControllers/WritePaperPhraseViewController.swift @@ -0,0 +1,183 @@ +import UIKit + +class WritePaperPhraseViewController: UIViewController { + private let store: Store + private let walletManager: WalletManager + private let pin: String + private let label = UILabel.wrapping(font: UIFont.customBody(size: 16.0)) + private let stepLabel = UILabel.wrapping(font: UIFont.customMedium(size: 16.0)) + private let header = RadialGradientView(backgroundColor: .liteWalletBlue) + + private lazy var phraseViews: [PhraseView] = { + guard let phraseString = self.walletManager.seedPhrase(pin: self.pin) else { return [] } + let words = phraseString.components(separatedBy: " ") + return words.map { PhraseView(phrase: $0) } + }() + + // This is awkwardly named because nextResponder is now named next is swift 3 :(, + private let proceed = ShadowButton(title: S.WritePaperPhrase.next.localize(), type: .secondary) + private let previous = ShadowButton(title: S.WritePaperPhrase.previous.localize(), type: .secondary) + private var proceedWidth: NSLayoutConstraint? + private var previousWidth: NSLayoutConstraint? + + private var phraseOffscreenOffset: CGFloat { + return view.bounds.width / 2.0 + PhraseView.defaultSize.width / 2.0 + } + + private var currentPhraseIndex = 0 { + didSet { + stepLabel.text = String(format: S.WritePaperPhrase.step.localize(), currentPhraseIndex + 1, phraseViews.count) + } + } + + init(store: Store, walletManager: WalletManager, pin: String, callback: @escaping () -> Void) { + self.store = store + self.walletManager = walletManager + self.pin = pin + self.callback = callback + super.init(nibName: nil, bundle: nil) + } + + private let callback: () -> Void + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + view.backgroundColor = .white + + label.text = S.WritePaperPhrase.instruction.localize() + label.textAlignment = .center + label.textColor = .white + + stepLabel.text = String(format: S.WritePaperPhrase.step.localize(), 1, phraseViews.count) + stepLabel.textAlignment = .center + + stepLabel.textColor = UIColor(white: 170.0 / 255.0, alpha: 1.0) + + addSubviews() + addConstraints() + addButtonTargets() + + NotificationCenter.default.addObserver(forName: UIScene.willDeactivateNotification, + object: nil, + queue: nil) { [weak self] _ in + self?.dismiss(animated: true, completion: nil) + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + private func addSubviews() { + view.addSubview(header) + header.addSubview(label) + view.addSubview(stepLabel) + view.addSubview(proceed) + view.addSubview(previous) + phraseViews.forEach { view.addSubview($0) } + } + + private func addConstraints() { + header.constrainTopCorners(sidePadding: 0, topPadding: 0) + header.constrain([ + header.constraint(.height, constant: 152.0), + ]) + label.constrainBottomCorners(sidePadding: C.padding[3], bottomPadding: C.padding[2]) + + phraseViews.enumerated().forEach { index, phraseView in + // The first phrase should initially be on the screen + let constant = index == 0 ? 0.0 : phraseOffscreenOffset + let xConstraint = NSLayoutConstraint(item: phraseView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: constant) + phraseView.xConstraint = xConstraint + phraseView.constrain([ + phraseView.constraint(.width, constant: PhraseView.defaultSize.width), + phraseView.constraint(.height, constant: PhraseView.defaultSize.height), + phraseView.constraint(.centerY, toView: view, constant: 0.0), + xConstraint, + ]) + } + + stepLabel.constrain([ + NSLayoutConstraint(item: stepLabel, attribute: .top, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: PhraseView.defaultSize.height / 2.0 + C.padding[1]), + stepLabel.constraint(.centerX, toView: view, constant: 0.0), + stepLabel.constraint(.width, constant: 200.0), // The transitions are smoother if this view is forced to be wider than it needs to be + ]) + + proceedWidth = proceed.constraint(.width, toView: view, constant: -C.padding[2] * 2) + proceed.constrain([ + proceed.constraint(.trailing, toView: view, constant: -C.padding[2]), + proceed.constraint(.height, constant: C.Sizes.buttonHeight), + proceed.constraint(.bottom, toView: view, constant: -(C.padding[4] + C.Sizes.buttonHeight)), + proceedWidth!, + ]) + + previousWidth = previous.constraint(.width, toView: view, constant: -view.bounds.width) + previous.constrain([ + previous.constraint(.leading, toView: view, constant: C.padding[2]), + previous.constraint(.height, constant: C.Sizes.buttonHeight), + previous.constraint(.bottom, toView: view, constant: -(C.padding[4] + C.Sizes.buttonHeight)), + previousWidth!, + ]) + } + + private func addButtonTargets() { + proceed.addTarget(self, action: #selector(proceedTapped), for: .touchUpInside) + previous.addTarget(self, action: #selector(previousTapped), for: .touchUpInside) + } + + @objc private func proceedTapped() { + guard currentPhraseIndex < phraseViews.count - 1 else { callback(); return } + if currentPhraseIndex == 0 { + showBothButtons() + } + transitionTo(isNext: true) + } + + @objc private func previousTapped() { + guard currentPhraseIndex > 0 else { return } + if currentPhraseIndex == 1 { + showOneButton() + } + transitionTo(isNext: false) + } + + private func transitionTo(isNext: Bool) { + let viewToHide = phraseViews[currentPhraseIndex] + let viewToShow = phraseViews[isNext ? currentPhraseIndex + 1 : currentPhraseIndex - 1] + if isNext { + currentPhraseIndex += 1 + } else { + currentPhraseIndex -= 1 + } + + UIView.spring(0.6, animations: { + viewToHide.xConstraint?.constant = isNext ? -self.phraseOffscreenOffset : self.phraseOffscreenOffset + viewToShow.xConstraint?.constant = 0 + self.view.layoutIfNeeded() + }, completion: { _ in }) + } + + private func showBothButtons() { + UIView.animate(withDuration: 0.4) { + self.proceedWidth?.constant = -self.view.bounds.width / 2.0 - C.padding[2] - C.padding[1] / 2.0 + self.previousWidth?.constant = -self.view.bounds.width / 2.0 - C.padding[2] - C.padding[1] / 2.0 + self.view.layoutIfNeeded() + } + } + + private func showOneButton() { + UIView.animate(withDuration: 0.4) { + self.proceedWidth?.constant = -C.padding[2] * 2 + self.previousWidth?.constant = -self.view.bounds.width + self.view.layoutIfNeeded() + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/ViewModels/Amount.swift b/litewallet/ViewModels/Amount.swift new file mode 100644 index 000000000..99f0044b7 --- /dev/null +++ b/litewallet/ViewModels/Amount.swift @@ -0,0 +1,166 @@ +import Foundation + +struct Amount { + // MARK: - Public + + let amount: UInt64 // amount in satoshis + let rate: Rate + let maxDigits: Int + + var amountForLtcFormat: Double { + var decimal = Decimal(self.amount) + var amount: Decimal = 0.0 + NSDecimalMultiplyByPowerOf10(&amount, &decimal, Int16(-maxDigits), .up) + return NSDecimalNumber(decimal: amount).doubleValue + } + + var localAmount: Double { + return Double(amount) / 100_000_000.0 * rate.rate + } + + var bits: String { + var decimal = Decimal(self.amount) + var amount: Decimal = 0.0 + NSDecimalMultiplyByPowerOf10(&amount, &decimal, Int16(-maxDigits), .up) + let number = NSDecimalNumber(decimal: amount) + guard let string = ltcFormat.string(from: number) else { return "" } + return string + } + + var localCurrency: String { + guard let string = localFormat.string(from: Double(amount) / 100_000_000.0 * rate.rate as NSNumber) else { return "" } + return string + } + + func string(forLocal local: Locale) -> String { + let format = NumberFormatter() + format.locale = local + format.isLenient = true + format.numberStyle = .currency + format.generatesDecimalNumbers = true + format.negativePrefix = "-" + guard let string = format.string(from: Double(amount) / 100_000_000.0 * rate.rate as NSNumber) else { return "" } + return string + } + + func string(isLtcSwapped: Bool) -> String { + return isLtcSwapped ? localCurrency : bits + } + + var ltcFormat: NumberFormatter { + let format = NumberFormatter() + format.isLenient = true + format.numberStyle = .currency + format.generatesDecimalNumbers = true + format.negativePrefix = "-" + format.currencyCode = "LTC" + + switch maxDigits { + case 2: // photons + format.currencySymbol = "m\(S.Symbols.lites)\(S.Symbols.narrowSpace)" + format.maximum = (C.maxMoney / C.satoshis) * 100_000 as NSNumber + case 5: // lites + format.currencySymbol = "\(S.Symbols.lites)\(S.Symbols.narrowSpace)" + format.maximum = (C.maxMoney / C.satoshis) * 1000 as NSNumber + case 8: // litecoin + format.currencySymbol = "\(S.Symbols.ltc)\(S.Symbols.narrowSpace)" + format.maximum = C.maxMoney / C.satoshis as NSNumber + default: + format.currencySymbol = "\(S.Symbols.lites)\(S.Symbols.narrowSpace)" + } + + format.maximumFractionDigits = maxDigits + format.minimumFractionDigits = 0 // iOS 8 bug, minimumFractionDigits now has to be set after currencySymbol + format.maximum = Decimal(C.maxMoney) / pow(10.0, maxDigits) as NSNumber + + return format + } + + var localFormat: NumberFormatter { + let format = NumberFormatter() + format.isLenient = true + format.numberStyle = .currency + format.generatesDecimalNumbers = true + format.negativePrefix = "-" + format.currencySymbol = rate.currencySymbol + return format + } +} + +struct DisplayAmount { + let amount: Satoshis + let state: ReduxState + let selectedRate: Rate? + let minimumFractionDigits: Int? + + var description: String { + return selectedRate != nil ? fiatDescription : litecoinDescription + } + + var combinedDescription: String { + return state.isLtcSwapped ? "\(fiatDescription) (\(litecoinDescription))" : "\(litecoinDescription) (\(fiatDescription))" + } + + private var fiatDescription: String { + guard let rate = selectedRate ?? state.currentRate else { return "" } + guard let string = localFormat.string(from: Double(amount.rawValue) / 100_000_000.0 * rate.rate as NSNumber) else { return "" } + return string + } + + private var litecoinDescription: String { + var decimal = Decimal(self.amount.rawValue) + var amount: Decimal = 0.0 + NSDecimalMultiplyByPowerOf10(&amount, &decimal, Int16(-state.maxDigits), .up) + let number = NSDecimalNumber(decimal: amount) + guard let string = ltcFormat.string(from: number) else { return "" } + return string + } + + var localFormat: NumberFormatter { + let format = NumberFormatter() + format.isLenient = true + format.numberStyle = .currency + format.generatesDecimalNumbers = true + format.negativePrefix = "-" + if let rate = selectedRate { + format.currencySymbol = rate.currencySymbol + } else if let rate = state.currentRate { + format.currencySymbol = rate.currencySymbol + } + if let minimumFractionDigits = minimumFractionDigits { + format.minimumFractionDigits = minimumFractionDigits + } + return format + } + + var ltcFormat: NumberFormatter { + let format = NumberFormatter() + format.isLenient = true + format.numberStyle = .currency + format.generatesDecimalNumbers = true + format.negativePrefix = "-" + format.currencyCode = "LTC" + + switch state.maxDigits { + case 2: + format.currencySymbol = "m\(S.Symbols.lites)\(S.Symbols.narrowSpace)" + format.maximum = (C.maxMoney / C.satoshis) * 100_000 as NSNumber + case 5: + format.currencySymbol = "\(S.Symbols.lites)\(S.Symbols.narrowSpace)" + format.maximum = (C.maxMoney / C.satoshis) * 1000 as NSNumber + case 8: + format.currencySymbol = "\(S.Symbols.ltc)\(S.Symbols.narrowSpace)" + format.maximum = C.maxMoney / C.satoshis as NSNumber + default: + format.currencySymbol = "\(S.Symbols.lites)\(S.Symbols.narrowSpace)" + } + + format.maximumFractionDigits = state.maxDigits + if let minimumFractionDigits = minimumFractionDigits { + format.minimumFractionDigits = minimumFractionDigits + } + format.maximum = Decimal(C.maxMoney) / pow(10.0, state.maxDigits) as NSNumber + + return format + } +} diff --git a/litewallet/ViewModels/Transaction.swift b/litewallet/ViewModels/Transaction.swift new file mode 100644 index 000000000..d20c8d2a1 --- /dev/null +++ b/litewallet/ViewModels/Transaction.swift @@ -0,0 +1,397 @@ +import BRCore +import UIKit + +// Ideally this would be a struct, but it needs to be a class to allow +// for lazy variables + +struct TransactionStatusTuple { + var percentageString: String + var units: Int +} + +class Transaction { + // MARK: - Public + + init?(_ tx: BRTxRef, walletManager: WalletManager, kvStore: BRReplicatedKVStore?, rate: Rate?) { + guard let wallet = walletManager.wallet else { return nil } + guard let peerManager = walletManager.peerManager else { return nil } + + self.tx = tx + self.wallet = wallet + self.kvStore = kvStore + + let fee = wallet.feeForTx(tx) ?? 0 + + let opsOutput = tx.outputs.filter { $0.updatedSwiftAddress == Partner.partnerKeyPath(name: .litewalletOps) }.first + guard let opsAmount = opsOutput?.amount else { return nil } + + self.fee = fee + opsAmount + + let amountReceived = wallet.amountReceivedFromTx(tx) + let amountSent = wallet.amountSentByTx(tx) - opsAmount + + if amountSent > 0, (amountReceived + fee) == amountSent { + direction = .moved + satoshis = amountSent + } else if amountSent > 0 { + direction = .sent + satoshis = amountSent - amountReceived - fee + } else { + direction = .received + satoshis = amountReceived + } + timestamp = Int(tx.pointee.timestamp) + + isValid = wallet.transactionIsValid(tx) + let transactionBlockHeight = tx.pointee.blockHeight + self.blockHeight = tx.pointee.blockHeight == UInt32(INT32_MAX) ? S.TransactionDetails.notConfirmedBlockHeightLabel.localize() : "\(tx.pointee.blockHeight)" + + let blockHeight = peerManager.lastBlockHeight + confirms = transactionBlockHeight > blockHeight ? 0 : Int(blockHeight - transactionBlockHeight) + 1 + status = makeStatus(tx, wallet: wallet, peerManager: peerManager, confirms: confirms, direction: direction) + + hash = tx.pointee.txHash.description + metaDataKey = tx.pointee.txHash.txKey + + if let rate = rate, confirms < 6, direction == .received { + attemptCreateMetaData(tx: tx, rate: rate) + } + } + + func amountDescription(isLtcSwapped: Bool, rate: Rate, maxDigits: Int) -> String { + let amount = Amount(amount: satoshis, rate: rate, maxDigits: maxDigits) + return isLtcSwapped ? amount.localCurrency : amount.bits + } + + func descriptionString(isLtcSwapped: Bool, rate: Rate, maxDigits: Int) -> NSAttributedString { + let amount = Amount(amount: satoshis, rate: rate, maxDigits: maxDigits).string(isLtcSwapped: isLtcSwapped) + let format = direction.amountDescriptionFormat + let string = String(format: format, amount) + return string.attributedStringForTags + } + + var detailsAddressText: String { + let address = toAddress?.largeCondensed + return String(format: direction.addressTextFormat, address ?? S.TransactionDetails.account.localize()) + } + + func amountDetails(isLtcSwapped: Bool, rate: Rate, rates: [Rate], maxDigits: Int) -> String { + let feeAmount = Amount(amount: fee, rate: rate, maxDigits: maxDigits) + let feeString = direction == .sent ? String(format: S.Transaction.fee.localize(), "\(feeAmount.string(isLtcSwapped: isLtcSwapped))") : "" + let amountString = "\(direction.sign)\(Amount(amount: satoshis, rate: rate, maxDigits: maxDigits).string(isLtcSwapped: isLtcSwapped)) \(feeString)" + var startingString = String(format: S.Transaction.starting.localize(), "\(Amount(amount: startingBalance, rate: rate, maxDigits: maxDigits).string(isLtcSwapped: isLtcSwapped))") + var endingString = String(format: String(format: S.Transaction.ending.localize(), "\(Amount(amount: balanceAfter, rate: rate, maxDigits: maxDigits).string(isLtcSwapped: isLtcSwapped))")) + + if startingBalance > C.maxMoney { + startingString = "" + endingString = "" + } + + var exchangeRateInfo = "" + if let metaData = metaData, let currentRate = rates.filter({ $0.code.lowercased() == metaData.exchangeRateCurrency.lowercased() }).first + { + let difference = (currentRate.rate - metaData.exchangeRate) / metaData.exchangeRate * 100.0 + let prefix = difference > 0.0 ? "+" : "-" + let firstLine = direction == .sent ? S.Transaction.exchangeOnDaySent : S.Transaction.exchangeOnDayReceived + let nf = NumberFormatter() + nf.currencySymbol = currentRate.currencySymbol + nf.numberStyle = .currency + if let rateString = nf.string(from: metaData.exchangeRate as NSNumber) { + let secondLine = "\(rateString)/LTC \(prefix)\(String(format: "%.2f", difference))%" + exchangeRateInfo = "\(firstLine)\n\(secondLine)" + } + } + + return "\(amountString)\n\(startingString)\n\(endingString)\n\(exchangeRateInfo)" + } + + func amountDetailsAmountString(isLtcSwapped: Bool, rate: Rate, rates _: [Rate], maxDigits: Int) -> String + { + let feeAmount = Amount(amount: fee, rate: rate, maxDigits: maxDigits) + let feeString = direction == .sent ? String(format: S.Transaction.fee.localize(), "\(feeAmount.string(isLtcSwapped: isLtcSwapped))") : "" + return "\(direction.sign)\(Amount(amount: satoshis, rate: rate, maxDigits: maxDigits).string(isLtcSwapped: isLtcSwapped)) \(feeString)" + } + + func amountDetailsStartingBalanceString(isLtcSwapped: Bool, rate: Rate, rates _: [Rate], maxDigits: Int) -> String + { + return String(format: S.Transaction.starting.localize(), "\(Amount(amount: startingBalance, rate: rate, maxDigits: maxDigits).string(isLtcSwapped: isLtcSwapped))") + } + + func amountDetailsEndingBalanceString(isLtcSwapped: Bool, rate: Rate, rates _: [Rate], maxDigits: Int) -> String + { + return String(format: String(format: S.Transaction.ending.localize(), "\(Amount(amount: balanceAfter, rate: rate, maxDigits: maxDigits).string(isLtcSwapped: isLtcSwapped))")) + } + + func amountExchangeString(isLtcSwapped _: Bool, rate _: Rate, rates: [Rate], maxDigits _: Int) -> String + { + var exchangeRateInfo = "" + if let metaData = metaData, let currentRate = rates.filter({ $0.code.lowercased() == metaData.exchangeRateCurrency.lowercased() }).first + { + let difference = (currentRate.rate - metaData.exchangeRate) / metaData.exchangeRate * 100.0 + let prefix = difference > 0.0 ? "+" : "-" + let nf = NumberFormatter() + nf.currencySymbol = currentRate.currencySymbol + nf.numberStyle = .currency + if let rateString = nf.string(from: metaData.exchangeRate as NSNumber) { + // TODO: Decide the usefulness of the rate percentage or a better way to describe it + let secondLine = "\(rateString)/LTC \(prefix)\(String(format: "%.2f", difference))%" + exchangeRateInfo = "\(secondLine)" + } + } + return exchangeRateInfo + } + + let direction: TransactionDirection + let status: String + let timestamp: Int + let fee: UInt64 + let hash: String + let isValid: Bool + let blockHeight: String + private let confirms: Int + private let metaDataKey: String + + // MARK: - Private + + private let tx: BRTxRef + private let wallet: BRWallet + private let satoshis: UInt64 + private var kvStore: BRReplicatedKVStore? + + lazy var toAddress: String? = { + switch self.direction { + case .sent: + + let allOutputs = self.tx.outputs.filter { $0.updatedSwiftAddress != Partner.partnerKeyPath(name: .litewalletOps) } + guard let output = allOutputs.filter({ output in + !self.wallet.containsAddress(output.updatedSwiftAddress) + }).first + else { + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR) + return nil + } + + return output.updatedSwiftAddress + + case .received: + guard let output = self.tx.outputs.filter({ output in + self.wallet.containsAddress(output.updatedSwiftAddress) + + }).first + else { + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR) + return nil + } + return output.updatedSwiftAddress + + case .moved: + guard let output = self.tx.outputs.filter({ output in + self.wallet.containsAddress(output.updatedSwiftAddress) + }).first else { return nil } + return output.updatedSwiftAddress + } + }() + + var exchangeRate: Double? { + return metaData?.exchangeRate + } + + var comment: String? { + return metaData?.comment + } + + var hasKvStore: Bool { + return kvStore != nil + } + + var _metaData: TxMetaData? + var metaData: TxMetaData? { + if _metaData != nil { + return _metaData + } else { + guard let kvStore = kvStore else { return nil } + if let data = TxMetaData(txKey: metaDataKey, store: kvStore) { + _metaData = data + return _metaData + } else { + return nil + } + } + } + + private var balanceAfter: UInt64 { + return wallet.balanceAfterTx(tx) + } + + private lazy var startingBalance: UInt64 = { + switch self.direction { + case .received: + return + self.balanceAfter.subtractingReportingOverflow(self.satoshis).0.subtractingReportingOverflow(self.fee).0 + case .sent: + return self.balanceAfter.addingReportingOverflow(self.satoshis).0.addingReportingOverflow(self.fee).0 + case .moved: + return self.balanceAfter.addingReportingOverflow(self.fee).0 + } + }() + + var timeSince: (String, Bool) { + if let cached = timeSinceCache { + return cached + } + + let result: (String, Bool) + guard timestamp > 0 + else { + result = (S.Transaction.justNow.localize(), false) + timeSinceCache = result + return result + } + let then = Date(timeIntervalSince1970: TimeInterval(timestamp)) + let now = Date() + + if !now.hasEqualYear(then) { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate("dd/MM/yy") + result = (df.string(from: then), false) + timeSinceCache = result + return result + } + + if !now.hasEqualMonth(then) { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate("MMM dd") + result = (df.string(from: then), false) + timeSinceCache = result + return result + } + + let difference = Int(Date().timeIntervalSince1970) - timestamp + let secondsInMinute = 60 + let secondsInHour = 3600 + let secondsInDay = 86400 + let secondsInWeek = secondsInDay * 7 + if difference < secondsInMinute { + result = (String(format: S.TimeSince.seconds.localize(), "\(difference)"), true) + } else if difference < secondsInHour { + result = (String(format: S.TimeSince.minutes.localize(), "\(difference / secondsInMinute)"), true) + } else if difference < secondsInDay { + result = (String(format: S.TimeSince.hours.localize(), "\(difference / secondsInHour)"), false) + } else if difference < secondsInWeek { + result = (String(format: S.TimeSince.days.localize(), "\(difference / secondsInDay)"), false) + } else { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate("MMM dd") + result = (df.string(from: then), false) + } + if result.1 == false { + timeSinceCache = result + } + return result + } + + private var timeSinceCache: (String, Bool)? + + var longTimestamp: String { + guard timestamp > 0 else { return wallet.transactionIsValid(tx) ? S.Transaction.justNow.localize() : "" } + let date = Date(timeIntervalSince1970: Double(timestamp)) + return Transaction.longDateFormatter.string(from: date) + } + + var shortTimestamp: String { + guard timestamp > 0 else { return wallet.transactionIsValid(tx) ? S.Transaction.justNow.localize() : "" } + let date = Date(timeIntervalSince1970: Double(timestamp)) + return Transaction.shortDateFormatter.string(from: date) + } + + var rawTransaction: BRTransaction { + return tx.pointee + } + + var isPending: Bool { + return confirms < 6 + } + + static let longDateFormatter: DateFormatter = { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate("MMMM d, yyy h:mm a") + return df + }() + + static let shortDateFormatter: DateFormatter = { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate("MMM d, h:mm a") + return df + }() + + private func attemptCreateMetaData(tx: BRTxRef, rate: Rate) { + guard metaData == nil else { return } + let newData = TxMetaData(transaction: tx.pointee, + exchangeRate: rate.rate, + exchangeRateCurrency: rate.code, + feeRate: 0.0, + deviceId: UserDefaults.standard.deviceID) + do { + _ = try kvStore?.set(newData) + } catch { + print("could not update metadata: \(error)") + } + } + + var shouldDisplayAvailableToSpend: Bool { + return confirms > 1 && confirms < 6 && direction == .received + } +} + +private extension String { + var smallCondensed: String { + let start = String(self[.. String +{ + let tx = txRef.pointee + guard wallet.transactionIsValid(txRef) + else { + return S.Transaction.invalid.localize() + } + + if confirms < 6 { + var percentageString = "" + if confirms == 0 { + let relayCount = peerManager.relayCount(tx.txHash) + if relayCount == 0 { + percentageString = "0%" + } else if relayCount == 1 { + percentageString = "20%" + } else if relayCount > 1 { + percentageString = "40%" + } + } else if confirms == 1 { + percentageString = "60%" + } else if confirms == 2 { + percentageString = "80%" + } else if confirms > 2 { + percentageString = "100%" + } + let format = direction == .sent ? S.Transaction.sendingStatus.localize() : S.Transaction.receivedStatus.localize() + return String(format: format, percentageString) + } else { + return S.Transaction.complete.localize() + } +} + +extension Transaction: Equatable {} + +func == (lhs: Transaction, rhs: Transaction) -> Bool { + return lhs.hash == rhs.hash && lhs.status == rhs.status && lhs.comment == rhs.comment && lhs.hasKvStore == rhs.hasKvStore +} diff --git a/litewallet/ViewModels/TransactionDirection.swift b/litewallet/ViewModels/TransactionDirection.swift new file mode 100644 index 000000000..9499e35f0 --- /dev/null +++ b/litewallet/ViewModels/TransactionDirection.swift @@ -0,0 +1,62 @@ +import Foundation + +enum TransactionDirection: String { + case sent = "Sent" + case received = "Received" + case moved = "Moved" + + var amountFormat: String { + switch self { + case .sent: + return S.TransactionDetails.sent.localize() + case .received: + return S.TransactionDetails.received.localize() + case .moved: + return S.TransactionDetails.moved.localize() + } + } + + var sign: String { + switch self { + case .sent: + return "-" + case .received: + return "" + case .moved: + return "" + } + } + + var addressHeader: String { + switch self { + case .sent: + return S.TransactionDirection.to.localize() + case .received: + return S.TransactionDirection.received.localize() + case .moved: + return S.TransactionDirection.to.localize() + } + } + + var amountDescriptionFormat: String { + switch self { + case .sent: + return S.TransactionDetails.sentAmountDescription.localize() + case .received: + return S.TransactionDetails.receivedAmountDescription.localize() + case .moved: + return S.TransactionDetails.movedAmountDescription.localize() + } + } + + var addressTextFormat: String { + switch self { + case .sent: + return S.TransactionDetails.to.localize() + case .received: + return S.TransactionDetails.from.localize() + case .moved: + return S.TransactionDetails.to.localize() + } + } +} diff --git a/litewallet/Views/AboutCell.swift b/litewallet/Views/AboutCell.swift new file mode 100644 index 000000000..c263a7d97 --- /dev/null +++ b/litewallet/Views/AboutCell.swift @@ -0,0 +1,52 @@ +import UIKit + +class AboutCell: UIView { + let button: UIButton + + init(text: String) { + button = UIButton.icon(image: #imageLiteral(resourceName: "OpenBrowser"), accessibilityLabel: text) + label.text = text + super.init(frame: .zero) + + if #available(iOS 11.0, *) { + guard let textColor = UIColor(named: "labelTextColor") + else { + NSLog("ERROR: Main color") + return + } + label.textColor = textColor + } + setup() + } + + private var label = UILabel(font: .customBody(size: 16.0), color: .darkText) + private let separator = UIView(color: .secondaryShadow) + + private func setup() { + addSubview(label) + addSubview(button) + addSubview(separator) + + label.constrain([ + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: C.padding[2]), + label.topAnchor.constraint(equalTo: topAnchor, constant: C.padding[2]), + label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -C.padding[2]), + ]) + button.constrain([ + button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + button.centerYAnchor.constraint(equalTo: label.centerYAnchor), + ]) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: label.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: button.trailingAnchor), + separator.bottomAnchor.constraint(equalTo: bottomAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + button.tintColor = C.defaultTintColor + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/AlertView.swift b/litewallet/Views/AlertView.swift new file mode 100644 index 000000000..58a72e7db --- /dev/null +++ b/litewallet/Views/AlertView.swift @@ -0,0 +1,168 @@ +import UIKit + +enum AlertType { + case pinSet(callback: () -> Void) + case paperKeySet(callback: () -> Void) + case sendSuccess + case resolvedSuccess + case addressesCopied + case sweepSuccess(callback: () -> Void) + + // Failure(s) + case failedResolution + + var header: String { + switch self { + case .pinSet: + return S.SecurityAlerts.pinSet.localize() + case .paperKeySet: + return S.SecurityAlerts.paperKeySet.localize() + case .sendSuccess: + return S.SecurityAlerts.sendSuccess.localize() + case .resolvedSuccess: + return S.SecurityAlerts.resolvedSuccess.localize() + case .addressesCopied: + return S.SecurityAlerts.copiedAddressesHeader.localize() + case .sweepSuccess: + return S.Import.success.localize() + + // Failure(s) + case .failedResolution: + return S.SecurityAlerts.sendFailure.localize() + } + } + + var subheader: String { + switch self { + case .pinSet: + return "" + case .paperKeySet: + return S.SecurityAlerts.paperKeySetSubheader.localize() + case .sendSuccess: + return S.SecurityAlerts.sendSuccessSubheader.localize() + case .resolvedSuccess: + return S.SecurityAlerts.resolvedSuccessSubheader.localize() + case .addressesCopied: + return S.SecurityAlerts.copiedAddressesSubheader.localize() + case .sweepSuccess: + return S.Import.successBody.localize() + + // Failure(s) + case .failedResolution: + return S.SecurityAlerts.resolvedSuccessSubheader.localize() + } + } + + var icon: UIView { + return CheckView() + } +} + +extension AlertType: Equatable {} + +func == (lhs: AlertType, rhs: AlertType) -> Bool { + switch (lhs, rhs) { + case (.pinSet(_), .pinSet(_)): + return true + case (.paperKeySet(_), .paperKeySet(_)): + return true + case (.sendSuccess, .sendSuccess): + return true + case (.resolvedSuccess, .resolvedSuccess): + return true + case (.addressesCopied, .addressesCopied): + return true + case (.sweepSuccess(_), .sweepSuccess(_)): + return true + + // Failure(s) + case (.failedResolution, .failedResolution): + return true + + default: + return false + } +} + +class AlertView: UIView, SolidColorDrawable { + private let type: AlertType + private let header = UILabel() + private let subheader = UILabel() + private let separator = UIView() + private let icon: UIView + private let iconSize: CGFloat = 96.0 + private let separatorYOffset: CGFloat = 48.0 + + init(type: AlertType) { + self.type = type + icon = type.icon + super.init(frame: .zero) + layer.cornerRadius = 6.0 + layer.masksToBounds = true + setupSubviews() + } + + func animate() { + guard let animatableIcon = icon as? AnimatableIcon else { return } + animatableIcon.animate() + } + + private func setupSubviews() { + addSubview(header) + addSubview(subheader) + addSubview(icon) + addSubview(separator) + + setData() + addConstraints() + } + + private func setData() { + header.text = type.header + header.textAlignment = .center + header.font = UIFont.barlowBold(size: 18.0) + header.textColor = .white + + icon.backgroundColor = .clear + separator.backgroundColor = .transparentWhite + + subheader.text = type.subheader + subheader.textAlignment = .center + subheader.font = UIFont.barlowSemiBold(size: 16.0) + subheader.textColor = .white + } + + private func addConstraints() { + // NB - In this alert view, constraints shouldn't be pinned to the bottom + // of the view because the bottom actually extends off the bottom of the screen a bit. + // It extends so that it still covers up the underlying view when it bounces on screen. + + header.constrainTopCorners(sidePadding: C.padding[2], topPadding: C.padding[2]) + separator.constrain([ + separator.constraint(.height, constant: 1.0), + separator.constraint(.width, toView: self, constant: 0.0), + separator.constraint(.top, toView: self, constant: separatorYOffset), + separator.constraint(.leading, toView: self, constant: nil), + ]) + icon.constrain([ + icon.constraint(.centerX, toView: self, constant: nil), + icon.constraint(.centerY, toView: self, constant: nil), + icon.constraint(.width, constant: iconSize), + icon.constraint(.height, constant: iconSize), + ]) + subheader.constrain([ + subheader.constraint(.leading, toView: self, constant: C.padding[2]), + subheader.constraint(.trailing, toView: self, constant: -C.padding[2]), + subheader.constraint(toBottom: icon, constant: C.padding[3]), + ]) + } + + override func draw(_ rect: CGRect) { + drawColor(color: .liteWalletBlue, rect) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/AnimatedIcons/AnimatableIcon.swift b/litewallet/Views/AnimatedIcons/AnimatableIcon.swift new file mode 100644 index 000000000..b9209e05e --- /dev/null +++ b/litewallet/Views/AnimatedIcons/AnimatableIcon.swift @@ -0,0 +1,5 @@ +import UIKit + +protocol AnimatableIcon { + func animate() +} diff --git a/litewallet/Views/AnimatedIcons/CheckView.swift b/litewallet/Views/AnimatedIcons/CheckView.swift new file mode 100644 index 000000000..c93f7f215 --- /dev/null +++ b/litewallet/Views/AnimatedIcons/CheckView.swift @@ -0,0 +1,68 @@ +import UIKit + +class CheckView: UIView, AnimatableIcon { + public func animate() { + let check = UIBezierPath() + check.move(to: CGPoint(x: 32.5, y: 47.0)) + check.addLine(to: CGPoint(x: 43.0, y: 57.0)) + check.addLine(to: CGPoint(x: 63, y: 37.4)) + + let shape = CAShapeLayer() + shape.path = check.cgPath + shape.lineWidth = 9.0 + shape.strokeColor = UIColor.white.cgColor + shape.fillColor = UIColor.clear.cgColor + shape.strokeStart = 0.0 + shape.strokeEnd = 0.0 + shape.lineCap = .round + shape.lineJoin = .round + layer.addSublayer(shape) + + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.toValue = 1.0 + animation.isRemovedOnCompletion = false + animation.fillMode = CAMediaTimingFillMode.forwards + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default) + animation.duration = 0.3 + + shape.add(animation, forKey: nil) + } + + override func draw(_: CGRect) { + let checkcircle = UIBezierPath() + checkcircle.move(to: CGPoint(x: 47.76, y: -0)) + checkcircle.addCurve(to: CGPoint(x: 0, y: 47.76), controlPoint1: CGPoint(x: 21.38, y: -0), controlPoint2: CGPoint(x: 0, y: 21.38)) + checkcircle.addCurve(to: CGPoint(x: 47.76, y: 95.52), controlPoint1: CGPoint(x: 0, y: 74.13), controlPoint2: CGPoint(x: 21.38, y: 95.52)) + checkcircle.addCurve(to: CGPoint(x: 95.52, y: 47.76), controlPoint1: CGPoint(x: 74.14, y: 95.52), controlPoint2: CGPoint(x: 95.52, y: 74.13)) + checkcircle.addCurve(to: CGPoint(x: 47.76, y: -0), controlPoint1: CGPoint(x: 95.52, y: 21.38), controlPoint2: CGPoint(x: 74.14, y: -0)) + checkcircle.addLine(to: CGPoint(x: 47.76, y: -0)) + checkcircle.close() + checkcircle.move(to: CGPoint(x: 47.99, y: 85.97)) + checkcircle.addCurve(to: CGPoint(x: 9.79, y: 47.76), controlPoint1: CGPoint(x: 26.89, y: 85.97), controlPoint2: CGPoint(x: 9.79, y: 68.86)) + checkcircle.addCurve(to: CGPoint(x: 47.99, y: 9.55), controlPoint1: CGPoint(x: 9.79, y: 26.66), controlPoint2: CGPoint(x: 26.89, y: 9.55)) + checkcircle.addCurve(to: CGPoint(x: 86.2, y: 47.76), controlPoint1: CGPoint(x: 69.1, y: 9.55), controlPoint2: CGPoint(x: 86.2, y: 26.66)) + checkcircle.addCurve(to: CGPoint(x: 47.99, y: 85.97), controlPoint1: CGPoint(x: 86.2, y: 68.86), controlPoint2: CGPoint(x: 69.1, y: 85.97)) + checkcircle.close() + + UIColor.white.setFill() + checkcircle.fill() + + // This is the non-animated check left here for now as a reference + // let check = UIBezierPath() + // check.move(to: CGPoint(x: 30.06, y: 51.34)) + // check.addCurve(to: CGPoint(x: 30.06, y: 44.75), controlPoint1: CGPoint(x: 28.19, y: 49.52), controlPoint2: CGPoint(x: 28.19, y: 46.57)) + // check.addCurve(to: CGPoint(x: 36.9, y: 44.69), controlPoint1: CGPoint(x: 32, y: 42.87), controlPoint2: CGPoint(x: 35.03, y: 42.87)) + // check.addLine(to: CGPoint(x: 42.67, y: 50.3)) + // check.addLine(to: CGPoint(x: 58.62, y: 34.79)) + // check.addCurve(to: CGPoint(x: 65.39, y: 34.8), controlPoint1: CGPoint(x: 60.49, y: 32.98), controlPoint2: CGPoint(x: 63.53, y: 32.98)) + // check.addCurve(to: CGPoint(x: 65.46, y: 41.45), controlPoint1: CGPoint(x: 67.33, y: 36.68), controlPoint2: CGPoint(x: 67.33, y: 39.63)) + // check.addLine(to: CGPoint(x: 45.33, y: 61.02)) + // check.addCurve(to: CGPoint(x: 40.01, y: 61.02), controlPoint1: CGPoint(x: 43.86, y: 62.44), controlPoint2: CGPoint(x: 41.48, y: 62.44)) + // check.addLine(to: CGPoint(x: 30.06, y: 51.34)) + // check.close() + // check.move(to: CGPoint(x: 30.06, y: 51.34)) +// + // UIColor.green.setFill() + // check.fill() + } +} diff --git a/litewallet/Views/BlinkingView.swift b/litewallet/Views/BlinkingView.swift new file mode 100644 index 000000000..2352411ce --- /dev/null +++ b/litewallet/Views/BlinkingView.swift @@ -0,0 +1,28 @@ +import UIKit + +class BlinkingView: UIView { + init(blinkColor: UIColor) { + self.blinkColor = blinkColor + super.init(frame: .zero) + } + + func startBlinking() { + timer = Timer.scheduledTimer(timeInterval: 0.53, target: self, selector: #selector(update), userInfo: nil, repeats: true) + } + + func stopBlinking() { + timer?.invalidate() + } + + @objc private func update() { + backgroundColor = backgroundColor == .clear ? blinkColor : .clear + } + + private let blinkColor: UIColor + private var timer: Timer? + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/CameraGuideView.swift b/litewallet/Views/CameraGuideView.swift new file mode 100644 index 000000000..7692bf1e4 --- /dev/null +++ b/litewallet/Views/CameraGuideView.swift @@ -0,0 +1,72 @@ +import UIKit + +private let guideSize: CGFloat = 64.0 +private let lineWidth: CGFloat = 8.0 + +enum CameraGuideState { + case normal, positive, negative + + var color: UIColor { + switch self { + case .normal: return .darkLine + case .negative: return .litewalletOrange + case .positive: return .cameraGuidePositive + } + } +} + +class CameraGuideView: UIView { + var state: CameraGuideState = .normal { + didSet { + setNeedsDisplay() + } + } + + init() { + super.init(frame: .zero) + backgroundColor = .clear + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } + + // top left + context.addLineThrough([ + (0 + lineWidth / 2.0, guideSize), + (0 + lineWidth / 2.0, 0 + lineWidth / 2.0), + (guideSize, 0 + lineWidth / 2.0), + ]) + + // top right + context.addLineThrough([ + (rect.maxX - guideSize, 0 + lineWidth / 2.0), + (rect.maxX - lineWidth / 2.0, 0 + lineWidth / 2.0), + (rect.maxX - lineWidth / 2.0, guideSize), + ]) + + // bottom right + context.addLineThrough([ + (rect.maxX - lineWidth / 2.0, rect.maxY - guideSize), + (rect.maxY - lineWidth / 2.0, rect.maxY - lineWidth / 2.0), + (rect.maxX - guideSize, rect.maxY - lineWidth / 2.0), + ]) + + // bottom left + context.addLineThrough([ + (lineWidth / 2.0, rect.maxY - guideSize), + (lineWidth / 2.0, rect.maxY - lineWidth / 2.0), + (guideSize, rect.maxY - lineWidth / 2.0), + ]) + + state.color.setStroke() + context.setLineCap(.round) + context.setLineJoin(.round) + context.setLineWidth(lineWidth) + context.strokePath() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/Circle.swift b/litewallet/Views/Circle.swift new file mode 100644 index 000000000..ac2218fa3 --- /dev/null +++ b/litewallet/Views/Circle.swift @@ -0,0 +1,25 @@ +import UIKit + +class Circle: UIView { + private let color: UIColor + + static let defaultSize: CGFloat = 64.0 + + init(color: UIColor) { + self.color = color + super.init(frame: .zero) + backgroundColor = .clear + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } + context.addEllipse(in: rect) + context.setFillColor(color.cgColor) + context.fillPath() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/ConfirmPhrase.swift b/litewallet/Views/ConfirmPhrase.swift new file mode 100644 index 000000000..320a39673 --- /dev/null +++ b/litewallet/Views/ConfirmPhrase.swift @@ -0,0 +1,105 @@ +import UIKit + +private let circleRadius: CGFloat = 12.0 + +class ConfirmPhrase: UIView { + let word: String + let textField = UITextField() + var callback: (() -> Void)? + var doneCallback: (() -> Void)? + var isEditingCallback: (() -> Void)? + + init(text: String, word: String) { + self.word = word + super.init(frame: CGRect()) + translatesAutoresizingMaskIntoConstraints = false + label.text = text + setupSubviews() + } + + private let label = UILabel() + private let separator = UIView() + private let circle = DrawableCircle() + + private func setupSubviews() { + label.font = UIFont.customBody(size: 14.0) + label.textColor = UIColor(white: 170.0 / 255.0, alpha: 1.0) + separator.backgroundColor = .separatorGray + + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.font = UIFont.customBody(size: 16.0) + textField.textColor = .darkText + textField.delegate = self + + addSubview(label) + addSubview(textField) + addSubview(separator) + addSubview(circle) + + label.constrain([ + label.constraint(.leading, toView: self, constant: C.padding[1]), + label.constraint(.top, toView: self, constant: C.padding[1]), + ]) + textField.constrain([ + textField.constraint(.leading, toView: label, constant: nil), + textField.constraint(toBottom: label, constant: C.padding[1] / 2.0), + textField.constraint(.width, toView: self, constant: -C.padding[1] * 2), + ]) + + separator.constrainBottomCorners(sidePadding: 0.0, bottomPadding: 0.0) + separator.constrain([ + // This contraint to the bottom of the textField is pretty crucial. Without it, + // this view will have an intrinsicHeight of 0 + separator.constraint(toBottom: textField, constant: C.padding[1]), + separator.constraint(.height, constant: 1.0), + ]) + circle.constrain([ + circle.centerYAnchor.constraint(equalTo: textField.centerYAnchor), + circle.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + circle.heightAnchor.constraint(equalToConstant: circleRadius * 2.0), + circle.widthAnchor.constraint(equalToConstant: circleRadius * 2.0), + ]) + + textField.addTarget(self, action: #selector(textFieldChanged), for: .editingChanged) + } + + func validate() { + if textField.text != word { + textField.textColor = .litewalletOrange + } + } + + @objc private func textFieldChanged() { + textField.textColor = .darkText + guard textField.markedTextRange == nil else { return } + if textField.text == word { + circle.show() + textField.isEnabled = false + } + callback?() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension ConfirmPhrase: UITextFieldDelegate { + func textFieldDidEndEditing(_: UITextField) { + validate() + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + textField.textColor = .darkText + isEditingCallback?() + } + + func textFieldShouldReturn(_: UITextField) -> Bool { + if E.isIPhone4 { + doneCallback?() + } + return true + } +} diff --git a/litewallet/Views/DefaultCurrencyViewController.swift b/litewallet/Views/DefaultCurrencyViewController.swift new file mode 100644 index 000000000..35624ca83 --- /dev/null +++ b/litewallet/Views/DefaultCurrencyViewController.swift @@ -0,0 +1,171 @@ +import UIKit + +class DefaultCurrencyViewController: UITableViewController, Subscriber { + init(walletManager: WalletManager, store: Store) { + self.walletManager = walletManager + self.store = store + rates = store.state.rates.filter { $0.code != C.btcCurrencyCode } + super.init(style: .plain) + } + + private let walletManager: WalletManager + private let store: Store + private let cellIdentifier = "CellIdentifier" + private var rates: [Rate] = [] { + didSet { + tableView.reloadData() + setExchangeRateLabel() + } + } + + private var defaultCurrencyCode: String? { + didSet { + // Grab index paths of new and old rows when the currency changes + let paths: [IndexPath] = rates.enumerated().filter { $0.1.code == defaultCurrencyCode || $0.1.code == oldValue }.map { IndexPath(row: $0.0, section: 0) } + tableView.beginUpdates() + tableView.reloadRows(at: paths, with: .automatic) + tableView.endUpdates() + + setExchangeRateLabel() + } + } + + private let bitcoinLabel = UILabel(font: .customBold(size: 14.0), color: .grayTextTint) + private let bitcoinSwitch = UISegmentedControl(items: ["photons (\(S.Symbols.photons))", "lites (\(S.Symbols.lites))", "LTC (\(S.Symbols.ltc))"]) + private let rateLabel = UILabel(font: .customBody(size: 16.0), color: .darkText) + private var header: UIView? + + deinit { + store.unsubscribe(self) + } + + override func viewDidLoad() { + tableView.register(SeparatorCell.self, forCellReuseIdentifier: cellIdentifier) + store.subscribe(self, selector: { $0.defaultCurrencyCode != $1.defaultCurrencyCode }, callback: { + self.defaultCurrencyCode = $0.defaultCurrencyCode + + }) + store.subscribe(self, selector: { $0.maxDigits != $1.maxDigits }, callback: { _ in + self.setExchangeRateLabel() + }) + + tableView.sectionHeaderHeight = UITableView.automaticDimension + tableView.estimatedSectionHeaderHeight = 140.0 + tableView.backgroundColor = .whiteTint + tableView.separatorStyle = .none + + let titleLabel = UILabel(font: .customBold(size: 17.0), color: .darkText) + titleLabel.text = S.Settings.currency.localize() + titleLabel.sizeToFit() + navigationItem.titleView = titleLabel + + let faqButton = UIButton.buildFaqButton(store: store, articleId: ArticleIds.nothing) + faqButton.tintColor = .darkText + navigationItem.rightBarButtonItems = [UIBarButtonItem.negativePadding, UIBarButtonItem(customView: faqButton)] + } + + private func setExchangeRateLabel() { + if let currentRate = rates.filter({ $0.code == defaultCurrencyCode }).first { + let amount = Amount(amount: C.satoshis, rate: currentRate, maxDigits: store.state.maxDigits) + let bitsAmount = Amount(amount: C.satoshis, rate: currentRate, maxDigits: store.state.maxDigits) + rateLabel.textColor = .darkText + rateLabel.text = "\(bitsAmount.bits) = \(amount.string(forLocal: currentRate.locale))" + } + } + + override func numberOfSections(in _: UITableView) -> Int { + return 1 + } + + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + return rates.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell + { + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) + let rate = rates[indexPath.row] + cell.textLabel?.text = "\(rate.code) (\(rate.currencySymbol))" + + if rate.code == defaultCurrencyCode { + let check = UIImageView(image: #imageLiteral(resourceName: "CircleCheck").withRenderingMode(.alwaysTemplate)) + check.tintColor = C.defaultTintColor + cell.accessoryView = check + } else { + cell.accessoryView = nil + } + + return cell + } + + override func tableView(_: UITableView, viewForHeaderInSection _: Int) -> UIView? { + if let header = self.header { return header } + + let header = UIView(color: .whiteTint) + let rateLabelTitle = UILabel(font: .customBold(size: 14.0), color: .grayTextTint) + + header.addSubview(rateLabelTitle) + header.addSubview(rateLabel) + header.addSubview(bitcoinLabel) + header.addSubview(bitcoinSwitch) + + rateLabelTitle.constrain([ + rateLabelTitle.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: C.padding[2]), + rateLabelTitle.topAnchor.constraint(equalTo: header.topAnchor, constant: C.padding[1]), + ]) + rateLabel.constrain([ + rateLabel.leadingAnchor.constraint(equalTo: rateLabelTitle.leadingAnchor), + rateLabel.topAnchor.constraint(equalTo: rateLabelTitle.bottomAnchor), + ]) + + bitcoinLabel.constrain([ + bitcoinLabel.leadingAnchor.constraint(equalTo: rateLabelTitle.leadingAnchor), + bitcoinLabel.topAnchor.constraint(equalTo: rateLabel.bottomAnchor, constant: C.padding[2]), + ]) + bitcoinSwitch.constrain([ + bitcoinSwitch.leadingAnchor.constraint(equalTo: bitcoinLabel.leadingAnchor), + bitcoinSwitch.topAnchor.constraint(equalTo: bitcoinLabel.bottomAnchor, constant: C.padding[1]), + bitcoinSwitch.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -C.padding[2]), + bitcoinSwitch.widthAnchor.constraint(equalTo: header.widthAnchor, constant: -C.padding[4]), + ]) + + let settingSegment = store.state.maxDigits + switch settingSegment { + case 2: bitcoinSwitch.selectedSegmentIndex = 0 + case 5: bitcoinSwitch.selectedSegmentIndex = 1 + case 8: bitcoinSwitch.selectedSegmentIndex = 2 + default: bitcoinSwitch.selectedSegmentIndex = 2 + } + + bitcoinSwitch.valueChanged = strongify(self) { myself in + let newIndex = myself.bitcoinSwitch.selectedSegmentIndex + + switch newIndex { + case 0: // photons + myself.store.perform(action: MaxDigits.set(2)) + case 1: // lites + myself.store.perform(action: MaxDigits.set(5)) + case 2: // LTC + myself.store.perform(action: MaxDigits.set(8)) + default: // LTC + myself.store.perform(action: MaxDigits.set(8)) + } + } + + bitcoinLabel.text = S.DefaultCurrency.bitcoinLabel.localize() + rateLabelTitle.text = S.DefaultCurrency.rateLabel.localize() + + self.header = header + return header + } + + override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + let rate = rates[indexPath.row] + store.perform(action: DefaultCurrency.setDefault(rate.code)) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/DrawableCircle.swift b/litewallet/Views/DrawableCircle.swift new file mode 100644 index 000000000..9500a8d34 --- /dev/null +++ b/litewallet/Views/DrawableCircle.swift @@ -0,0 +1,58 @@ +import UIKit + +class DrawableCircle: UIView { + private let circleLayer = CAShapeLayer() + private let checkLayer = CAShapeLayer() + private var hasPerformedLayout = false + private let originalCheckSize: CGFloat = 96.0 + private let animationDuration: TimeInterval = 0.4 + + override func layoutSubviews() { + guard !hasPerformedLayout else { hasPerformedLayout = true; return } + clipsToBounds = false + backgroundColor = .clear + + let path = UIBezierPath(arcCenter: bounds.center, radius: bounds.width / 2.0, startAngle: .pi / 2.0, endAngle: (.pi / 2.0) - .pi * 2.0, clockwise: false) + circleLayer.path = path.cgPath + circleLayer.fillColor = UIColor.clear.cgColor + circleLayer.strokeColor = C.defaultTintColor.cgColor + circleLayer.lineWidth = 1.0 + circleLayer.opacity = 0 + layer.addSublayer(circleLayer) + + let check = UIBezierPath() + let scaleFactor = (bounds.width) / originalCheckSize + check.move(to: CGPoint(x: 32.5 * scaleFactor, y: 47.0 * scaleFactor)) + check.addLine(to: CGPoint(x: 43.0 * scaleFactor, y: 57.0 * scaleFactor)) + check.addLine(to: CGPoint(x: 63 * scaleFactor, y: 37.4 * scaleFactor)) + + checkLayer.path = check.cgPath + checkLayer.lineWidth = 2.0 + checkLayer.strokeColor = UIColor.white.cgColor + checkLayer.strokeColor = C.defaultTintColor.cgColor + checkLayer.fillColor = UIColor.clear.cgColor + checkLayer.strokeEnd = 0.0 + checkLayer.lineCap = .round + checkLayer.lineJoin = .round + layer.addSublayer(checkLayer) + } + + func show() { + let circleAnimation = CABasicAnimation(keyPath: "opacity") + circleAnimation.duration = animationDuration + circleAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + circleAnimation.fromValue = 0.0 + circleAnimation.toValue = 1.0 + circleLayer.opacity = 1.0 + circleLayer.add(circleAnimation, forKey: "drawCircle") + + let checkAnimation = CABasicAnimation(keyPath: "strokeEnd") + checkAnimation.fromValue = 0.0 + checkAnimation.toValue = 1.0 + checkAnimation.fillMode = .forwards + checkAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + checkAnimation.duration = animationDuration + checkLayer.strokeEnd = 1.0 + checkLayer.add(checkAnimation, forKey: "drawCheck") + } +} diff --git a/litewallet/Views/EnterPhraseCell.swift b/litewallet/Views/EnterPhraseCell.swift new file mode 100644 index 000000000..ab7eae008 --- /dev/null +++ b/litewallet/Views/EnterPhraseCell.swift @@ -0,0 +1,170 @@ +import UIKit + +class EnterPhraseCell: UICollectionViewCell { + // MARK: - Public + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + var index: Int? { + didSet { + guard let index = index else { return } + label.text = "\(index + 1)" + } + } + + private(set) var text: String? + + var didTapPrevious: (() -> Void)? { + didSet { + previousField.tap = didTapPrevious + } + } + + var didTapNext: (() -> Void)? { + didSet { + nextField.tap = didTapNext + } + } + + var didTapDone: (() -> Void)? { + didSet { + done.tap = { + self.textField.resignFirstResponder() + self.didTapDone?() + } + } + } + + var didEnterSpace: (() -> Void)? + var isWordValid: ((String) -> Bool)? + + func disablePreviousButton() { + previousField.tintColor = .secondaryShadow + previousField.isEnabled = false + } + + func disableNextButton() { + nextField.tintColor = .secondaryShadow + nextField.isEnabled = false + } + + // MARK: - Private + + let textField = UITextField() + private let label = UILabel(font: .customBody(size: 13.0), color: .secondaryShadow) + private let nextField = UIButton.icon(image: #imageLiteral(resourceName: "RightArrow"), accessibilityLabel: S.RecoverWallet.rightArrow.localize()) + private let previousField = UIButton.icon(image: #imageLiteral(resourceName: "LeftArrow"), accessibilityLabel: S.RecoverWallet.leftArrow.localize()) + private let done = UIButton(type: .system) + fileprivate let separator = UIView(color: .secondaryShadow) + fileprivate var hasDisplayedInvalidState = false + + private func setup() { + contentView.addSubview(textField) + contentView.addSubview(separator) + contentView.addSubview(label) + + textField.constrain([ + textField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + textField.topAnchor.constraint(equalTo: contentView.topAnchor, constant: C.padding[1]), + textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: textField.leadingAnchor, constant: C.padding[1]), + separator.topAnchor.constraint(equalTo: textField.bottomAnchor), + separator.trailingAnchor.constraint(equalTo: textField.trailingAnchor, constant: -C.padding[1]), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + label.constrain([ + label.leadingAnchor.constraint(equalTo: separator.leadingAnchor), + label.topAnchor.constraint(equalTo: separator.bottomAnchor), + label.trailingAnchor.constraint(equalTo: separator.trailingAnchor), + ]) + setData() + } + + private func setData() { + textField.inputAccessoryView = accessoryView + textField.autocorrectionType = .no + textField.textAlignment = .center + textField.autocapitalizationType = .none + textField.delegate = self + textField.addTarget(self, action: #selector(EnterPhraseCell.textChanged(textField:)), for: .editingChanged) + + label.textAlignment = .center + previousField.tintColor = .secondaryGrayText + nextField.tintColor = .secondaryGrayText + done.setTitle(S.RecoverWallet.done.localize(), for: .normal) + } + + private var accessoryView: UIView { + let view = UIView(color: .secondaryButton) + view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 44) + let topBorder = UIView(color: .secondaryShadow) + view.addSubview(topBorder) + view.addSubview(previousField) + view.addSubview(nextField) + view.addSubview(done) + + topBorder.constrainTopCorners(height: 1.0) + previousField.constrain([ + previousField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: C.padding[2]), + previousField.topAnchor.constraint(equalTo: view.topAnchor), + previousField.bottomAnchor.constraint(equalTo: view.bottomAnchor), + previousField.widthAnchor.constraint(equalToConstant: 44.0), + ]) + + nextField.constrain([ + nextField.leadingAnchor.constraint(equalTo: previousField.trailingAnchor), + nextField.topAnchor.constraint(equalTo: view.topAnchor), + nextField.bottomAnchor.constraint(equalTo: view.bottomAnchor), + nextField.widthAnchor.constraint(equalToConstant: 44.0), + ]) + + done.constrain([ + done.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -C.padding[2]), + done.topAnchor.constraint(equalTo: view.topAnchor), + done.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension EnterPhraseCell: UITextFieldDelegate { + func textFieldDidEndEditing(_ textField: UITextField) { + setColors(textField: textField) + } + + @objc func textChanged(textField: UITextField) { + if let text = textField.text { + if text.last == " " { + textField.text = text.replacingOccurrences(of: " ", with: "") + didEnterSpace?() + } + } + if hasDisplayedInvalidState { + setColors(textField: textField) + } + } + + private func setColors(textField: UITextField) { + guard let isWordValid = isWordValid else { return } + guard let word = textField.text else { return } + if isWordValid(word) || word == "" { + textField.textColor = .darkText + separator.backgroundColor = .secondaryShadow + } else { + textField.textColor = .litewalletOrange + separator.backgroundColor = .litewalletOrange + hasDisplayedInvalidState = true + } + } +} diff --git a/litewallet/Views/GradientCircle.swift b/litewallet/Views/GradientCircle.swift new file mode 100644 index 000000000..4a26177fa --- /dev/null +++ b/litewallet/Views/GradientCircle.swift @@ -0,0 +1,26 @@ +import UIKit + +class GradientCircle: UIView, GradientDrawable { + static let defaultSize: CGFloat = 64.0 + + init() { + super.init(frame: CGRect()) + backgroundColor = .clear + } + + override func draw(_ rect: CGRect) { + drawGradient(rect) + maskToCircle(rect) + } + + private func maskToCircle(_ rect: CGRect) { + let maskLayer = CAShapeLayer() + maskLayer.path = UIBezierPath(ovalIn: rect).cgPath + layer.mask = maskLayer + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/GradientSwitch.swift b/litewallet/Views/GradientSwitch.swift new file mode 100644 index 000000000..6028b6f5b --- /dev/null +++ b/litewallet/Views/GradientSwitch.swift @@ -0,0 +1,34 @@ +import UIKit + +class GradientSwitch: UISwitch { + init() { + super.init(frame: .zero) + setup() + } + + private let background: GradientView = { + let view = GradientView() + view.clipsToBounds = true + view.layer.cornerRadius = 16.0 + view.alpha = 0.0 + return view + }() + + private func setup() { + onTintColor = .clear + insertSubview(background, at: 0) + background.constrain(toSuperviewEdges: nil) + addTarget(self, action: #selector(toggleBackground), for: .valueChanged) + } + + @objc private func toggleBackground() { + UIView.animate(withDuration: 0.1, animations: { + self.background.alpha = self.isOn ? 1.0 : 0.0 + }) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/GradientView.swift b/litewallet/Views/GradientView.swift new file mode 100644 index 000000000..3b673430c --- /dev/null +++ b/litewallet/Views/GradientView.swift @@ -0,0 +1,49 @@ +import UIKit + +protocol GradientDrawable { + func drawGradient(_ rect: CGRect) +} + +extension UIView { + func drawGradient(_ rect: CGRect) { + guard !E.isIPhone4, !E.isIPhone5 + else { + addFallbackImageBackground() + return + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let colors = [UIColor.gradientStart.cgColor, UIColor.gradientEnd.cgColor] as CFArray + let locations: [CGFloat] = [0.2, 0.9] // eyeball attempt! REDO + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: locations) else { return } + guard let context = UIGraphicsGetCurrentContext() else { return } + context.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: rect.width, y: 0.0), options: []) + } + + private func addFallbackImageBackground() { + let image = UIImageView(image: #imageLiteral(resourceName: "HeaderGradient")) + image.contentMode = .scaleToFill + addSubview(image) + image.constrain(toSuperviewEdges: nil) + sendSubviewToBack(image) + } +} + +class GradientView: UIView { + override func draw(_ rect: CGRect) { + drawGradient(rect) + } +} + +protocol SolidColorDrawable { + func drawColor(color: UIColor, _ rect: CGRect) +} + +extension UIView { + func drawColor(color _: UIColor = .liteWalletBlue, _: CGRect) { + let image = UIImageView(image: #imageLiteral(resourceName: "colorImageBlue")) + image.contentMode = .scaleToFill + addSubview(image) + image.constrain(toSuperviewEdges: nil) + sendSubviewToBack(image) + } +} diff --git a/litewallet/Views/InAppAlert.swift b/litewallet/Views/InAppAlert.swift new file mode 100644 index 000000000..1a6b113e4 --- /dev/null +++ b/litewallet/Views/InAppAlert.swift @@ -0,0 +1,67 @@ +import UIKit + +class InAppAlert: UIView { + init(message: String, image: UIImage) { + super.init(frame: .zero) + setup() + self.image.image = image + self.message.text = message + } + + static let height: CGFloat = 136.0 + var bottomConstraint: NSLayoutConstraint? + var hide: (() -> Void)? + + private let close = UIButton.close + private let message = UILabel.wrapping(font: .customBody(size: 16.0), color: .whiteTint) + private let image = UIImageView() + + override func draw(_ rect: CGRect) { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let colors = [UIColor.blueGradientStart.cgColor, UIColor.blueGradientEnd.cgColor] as CFArray + let locations: [CGFloat] = [0.0, 1.0] + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: locations) else { return } + guard let context = UIGraphicsGetCurrentContext() else { return } + context.drawLinearGradient(gradient, start: CGPoint(x: rect.midX, y: 0.0), end: CGPoint(x: rect.midX, y: rect.height), options: []) + } + + private func setup() { + addSubview(close) + addSubview(image) + addSubview(message) + close.constrain([ + close.topAnchor.constraint(equalTo: topAnchor, constant: C.padding[2]), + close.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + close.widthAnchor.constraint(equalToConstant: 44.0), + close.heightAnchor.constraint(equalToConstant: 44.0), + ]) + image.constrain([ + image.centerXAnchor.constraint(equalTo: centerXAnchor), + image.topAnchor.constraint(equalTo: topAnchor, constant: C.padding[4]), + ]) + message.constrain([ + message.leadingAnchor.constraint(equalTo: leadingAnchor, constant: C.padding[2]), + message.topAnchor.constraint(equalTo: image.bottomAnchor, constant: C.padding[1]), + message.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + ]) + close.tap = { [weak self] in + self?.dismiss() + } + close.tintColor = .whiteTint + message.textAlignment = .center + } + + func dismiss() { + UIView.animate(withDuration: C.animationDuration, animations: { + self.bottomConstraint?.constant = 0.0 + self.superview?.layoutIfNeeded() + }, completion: { _ in + self.removeFromSuperview() + }) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/InViewAlert.swift b/litewallet/Views/InViewAlert.swift new file mode 100644 index 000000000..bdbb85224 --- /dev/null +++ b/litewallet/Views/InViewAlert.swift @@ -0,0 +1,99 @@ +import UIKit + +enum InViewAlertType { + case primary + case secondary +} + +private let arrowHeight: CGFloat = 8.0 +private let arrowWidth: CGFloat = 16.0 + +class InViewAlert: UIView { + var heightConstraint: NSLayoutConstraint? + var isExpanded = false + var contentView: UIView? { + didSet { + guard let view = contentView else { return } + addSubview(view) + view.constrain(toSuperviewEdges: UIEdgeInsets(top: arrowHeight, left: 0, bottom: 0, right: 0)) + } + } + + var arrowXLocation: CGFloat? + static var arrowSize: CGSize { + return CGSize(width: arrowWidth, height: arrowHeight) + } + + var height: CGFloat { + switch type { + case .primary: + return 72.0 + case .secondary: + return 81.0 + } + } + + init(type: InViewAlertType) { + self.type = type + super.init(frame: .zero) + setupSubViews() + } + + func toggle() { + heightConstraint?.constant = isExpanded ? 0.0 : height + } + + private let type: InViewAlertType + + private func setupSubViews() { + contentMode = .redraw + backgroundColor = .clear + } + + override func draw(_ rect: CGRect) { + let background = UIBezierPath(rect: rect.offsetBy(dx: 0, dy: arrowHeight)) + fillColor.setFill() + background.fill() + + let context = UIGraphicsGetCurrentContext() + let center = arrowXLocation != nil ? arrowXLocation! : rect.width / 2.0 + + let triangle = CGMutablePath() + triangle.move(to: CGPoint(x: center - arrowWidth / 2.0 + 0.5, y: arrowHeight + 0.5)) + triangle.addLine(to: CGPoint(x: center + 0.5, y: 0.5)) + triangle.addLine(to: CGPoint(x: center + arrowWidth / 2.0 + 0.5, y: arrowHeight + 0.5)) + triangle.closeSubpath() + context?.setLineJoin(.miter) + context?.setFillColor(fillColor.cgColor) + context?.addPath(triangle) + context?.fillPath() + + // Add Gray border for secondary style + if type == .secondary { + let topBorder = CGMutablePath() + topBorder.move(to: CGPoint(x: 0, y: arrowHeight)) + topBorder.addLine(to: CGPoint(x: center - arrowWidth / 2.0 + 0.5, y: arrowHeight + 0.5)) + topBorder.addLine(to: CGPoint(x: center + 0.5, y: 0.5)) + topBorder.addLine(to: CGPoint(x: center + arrowWidth / 2.0 + 0.5, y: arrowHeight + 0.5)) + topBorder.addLine(to: CGPoint(x: rect.width + 0.5, y: arrowHeight + 0.5)) + context?.setLineWidth(1.0) + context?.setStrokeColor(UIColor.secondaryShadow.cgColor) + context?.addPath(topBorder) + context?.strokePath() + } + } + + private var fillColor: UIColor { + switch type { + case .primary: + return .primaryButton + case .secondary: + return .grayBackgroundTint + } + } + + @available(*, unavailable) + required init(coder _: NSCoder) { + fatalError("This class does not support NSCoding") + } +} diff --git a/litewallet/Views/LightWeightAlert.swift b/litewallet/Views/LightWeightAlert.swift new file mode 100644 index 000000000..debaa1218 --- /dev/null +++ b/litewallet/Views/LightWeightAlert.swift @@ -0,0 +1,30 @@ +import UIKit + +class LightWeightAlert: UIView { + init(message: String) { + super.init(frame: .zero) + label.text = message + setup() + } + + let effect = UIBlurEffect(style: .dark) + let background = UIVisualEffectView() + let container = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark))) + private let label = UILabel(font: .customBold(size: 16.0)) + + private func setup() { + addSubview(background) + background.constrain(toSuperviewEdges: nil) + background.contentView.addSubview(container) + container.contentView.addSubview(label) + container.constrain(toSuperviewEdges: nil) + label.constrain(toSuperviewEdges: UIEdgeInsets(top: C.padding[2], left: C.padding[2], bottom: -C.padding[2], right: -C.padding[2])) + layer.cornerRadius = 4.0 + layer.masksToBounds = true + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/LoadingProgressView.swift b/litewallet/Views/LoadingProgressView.swift new file mode 100644 index 000000000..15e206ec9 --- /dev/null +++ b/litewallet/Views/LoadingProgressView.swift @@ -0,0 +1,88 @@ +import UIKit + +private let progressHeight: CGFloat = 4.0 +private let progressWidth: CGFloat = 150.0 + +class LoadingProgressView: UIView, GradientDrawable { + var progress: Double = 0.0 { + didSet { + progressWidthConstraint?.constant = CGFloat(progress) * progressWidth + } + } + + init() { + super.init(frame: .zero) + } + + private var hasSetup = false + + private lazy var progressBackground: UIView = self.makeProgressView(backgroundColor: .transparentBlack) + private lazy var progressForeground: UIView = self.makeProgressView(backgroundColor: .white) + + private func makeProgressView(backgroundColor: UIColor) -> UIView { + let view = UIView() + view.layer.cornerRadius = progressHeight / 2.0 + view.layer.masksToBounds = true + view.backgroundColor = backgroundColor + return view + } + + private let label = UILabel(font: .customBold(size: 14.0)) + private let shadowView = UIView() + private var progressWidthConstraint: NSLayoutConstraint? + + private func setupView() { + label.textColor = .white + label.text = S.Account.loadingMessage + label.textAlignment = .center + + addSubview(label) + addSubview(progressBackground) + addSubview(shadowView) + + label.constrain([ + label.centerXAnchor.constraint(equalTo: centerXAnchor), + label.bottomAnchor.constraint(equalTo: progressBackground.topAnchor, constant: -4.0), + ]) + progressBackground.constrain([ + progressBackground.centerXAnchor.constraint(equalTo: centerXAnchor), + progressBackground.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -C.padding[1]), + progressBackground.heightAnchor.constraint(equalToConstant: progressHeight), + progressBackground.widthAnchor.constraint(equalToConstant: progressWidth), + ]) + progressBackground.addSubview(progressForeground) + progressWidthConstraint = progressForeground.widthAnchor.constraint(equalToConstant: 0.0) + progressForeground.constrain([ + progressWidthConstraint, + progressForeground.leadingAnchor.constraint(equalTo: progressBackground.leadingAnchor), + progressForeground.heightAnchor.constraint(equalTo: progressBackground.heightAnchor), + progressForeground.centerYAnchor.constraint(equalTo: progressBackground.centerYAnchor), + ]) + shadowView.backgroundColor = .transparentWhite + shadowView.constrainTopCorners(height: 0.5) + } + + override func layoutSubviews() { + if !hasSetup { + setupView() + hasSetup = true + } + addBottomCorners() + } + + private func addBottomCorners() { + let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 6.0, height: 6.0)).cgPath + let maskLayer = CAShapeLayer() + maskLayer.path = path + layer.mask = maskLayer + } + + override func draw(_ rect: CGRect) { + drawGradient(rect) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/ModalHeaderView.swift b/litewallet/Views/ModalHeaderView.swift new file mode 100644 index 000000000..84e0ff2f4 --- /dev/null +++ b/litewallet/Views/ModalHeaderView.swift @@ -0,0 +1,80 @@ +import UIKit + +enum ModalHeaderViewStyle { + case light + case dark +} + +class ModalHeaderView: UIView { + // MARK: - Public + + var closeCallback: (() -> Void)? { + didSet { close.tap = closeCallback } + } + + init(title: String, style: ModalHeaderViewStyle, faqInfo _: (Store, String)? = nil, showCloseButton: Bool = true) + { + self.title.text = title + self.style = style + + self.showCloseButton = showCloseButton + super.init(frame: .zero) + setupSubviews() + setColors() + } + + let showCloseButton: Bool + + // MARK: - Private + + private let title = UILabel(font: .barlowSemiBold(size: 17.0)) + private let close = UIButton.close + private let border = UIView() + private let buttonSize: CGFloat = 44.0 + private let style: ModalHeaderViewStyle + + private func setupSubviews() { + addSubview(title) + addSubview(border) + if showCloseButton { + addSubview(close) + close.constrain([ + close.constraint(.leading, toView: self, constant: 0.0), + close.constraint(.centerY, toView: self, constant: 0.0), + close.constraint(.height, constant: buttonSize), + close.constraint(.width, constant: buttonSize), + ]) + } + + title.constrain([ + title.constraint(.centerX, toView: self, constant: 0.0), + title.constraint(.centerY, toView: self, constant: 0.0), + ]) + border.constrain([ + border.constraint(.height, constant: 1.0), + ]) + border.constrainBottomCorners(sidePadding: 0, bottomPadding: 0) + } + + private func setColors() { + if #available(iOS 11.0, *), + let textColor = UIColor(named: "inverseTextColor") + { + backgroundColor = textColor + } else { + backgroundColor = .white + } + switch style { + case .light: + title.textColor = .white + close.tintColor = .white + case .dark: + border.backgroundColor = .secondaryShadow + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/NonScrollingCollectionView.swift b/litewallet/Views/NonScrollingCollectionView.swift new file mode 100644 index 000000000..5f3fd5822 --- /dev/null +++ b/litewallet/Views/NonScrollingCollectionView.swift @@ -0,0 +1,10 @@ +import UIKit + +// This class disables all scrolling. This is desired +// when we don't want the scrollView to scroll to the active +// textField +class NonScrollingCollectionView: UICollectionView { + override func setContentOffset(_: CGPoint, animated _: Bool) { + // noop + } +} diff --git a/litewallet/Views/PhraseView.swift b/litewallet/Views/PhraseView.swift new file mode 100644 index 000000000..080771c7f --- /dev/null +++ b/litewallet/Views/PhraseView.swift @@ -0,0 +1,32 @@ +import UIKit + +class PhraseView: UIView { + private let phrase: String + private let label = UILabel() + + static let defaultSize = CGSize(width: 200, height: 88.0) + + var xConstraint: NSLayoutConstraint? + + init(phrase: String) { + self.phrase = phrase + super.init(frame: CGRect()) + setupSubviews() + } + + private func setupSubviews() { + addSubview(label) + label.constrainToCenter() + label.textColor = .white + label.adjustsFontSizeToFitWidth = true + label.text = phrase + label.font = UIFont.customBold(size: 30.0) + backgroundColor = .liteWalletBlue + layer.cornerRadius = 10.0 + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/PinPadCells/PinPadCells.swift b/litewallet/Views/PinPadCells/PinPadCells.swift new file mode 100644 index 000000000..1db5470c8 --- /dev/null +++ b/litewallet/Views/PinPadCells/PinPadCells.swift @@ -0,0 +1 @@ +import UIKit diff --git a/litewallet/Views/PinView.swift b/litewallet/Views/PinView.swift new file mode 100644 index 000000000..82e9056e6 --- /dev/null +++ b/litewallet/Views/PinView.swift @@ -0,0 +1,110 @@ +import UIKit + +enum PinViewStyle { + case create + case login +} + +class PinView: UIView { + // MARK: - Public + + var itemSize: CGFloat { + switch style { + case .create: + return 24.0 + case .login: + return 16.0 + } + } + + var width: CGFloat { + return (itemSize + C.padding[1]) * CGFloat(length) + } + + let shakeDuration: CFTimeInterval = 0.6 + fileprivate var shakeCompletion: (() -> Void)? + + init(style: PinViewStyle, length: Int) { + self.style = style + self.length = length + switch style { + case .create: + unFilled = (0 ... (length - 1)).map { _ in Circle(color: .transparentWhite) } + case .login: + unFilled = (0 ... (length - 1)).map { _ in Circle(color: .transparentWhite) } + } + filled = (0 ... (length - 1)).map { _ in Circle(color: .white) } + super.init(frame: CGRect()) + setupSubviews() + } + + func fill(_ number: Int) { + filled.enumerated().forEach { index, circle in + circle.isHidden = index > number - 1 + } + } + + func shake(completion: (() -> Void)? = nil) { + shakeCompletion = completion + let translation = CAKeyframeAnimation(keyPath: "transform.translation.x") + translation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + translation.values = [-5, 5, -5, 5, -3, 3, -2, 2, 0] + + let rotation = CAKeyframeAnimation(keyPath: "transform.rotation.y") + rotation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + rotation.values = [-5, 5, -5, 5, -3, 3, -2, 2, 0].map { + self.toRadian(value: $0) + } + let shakeGroup = CAAnimationGroup() + shakeGroup.animations = [translation, rotation] + shakeGroup.duration = shakeDuration + shakeGroup.delegate = self + layer.add(shakeGroup, forKey: "shakeIt") + } + + // MARK: - Private + + private let unFilled: [Circle] + private var filled: [Circle] + private let style: PinViewStyle + private let length: Int + + private func toRadian(value: Int) -> CGFloat { + return CGFloat(Double(value) / 180.0 * .pi) + } + + private func setupSubviews() { + addCircleContraints(unFilled) + addCircleContraints(filled) + filled.forEach { $0.isHidden = true } + } + + private func addCircleContraints(_ circles: [Circle]) { + circles.enumerated().forEach { index, circle in + addSubview(circle) + let leadingConstraint: NSLayoutConstraint? + if index == 0 { + leadingConstraint = circle.constraint(.leading, toView: self, constant: 0.0) + } else { + leadingConstraint = NSLayoutConstraint(item: circle, attribute: .leading, relatedBy: .equal, toItem: circles[index - 1], attribute: .trailing, multiplier: 1.0, constant: 8.0) + } + circle.constrain([ + circle.constraint(.width, constant: itemSize), + circle.constraint(.height, constant: itemSize), + circle.constraint(.centerY, toView: self, constant: nil), + leadingConstraint, + ]) + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension PinView: CAAnimationDelegate { + func animationDidStop(_: CAAnimation, finished _: Bool) { + shakeCompletion?() + } +} diff --git a/litewallet/Views/RadialGradientView.swift b/litewallet/Views/RadialGradientView.swift new file mode 100644 index 000000000..ab295ca19 --- /dev/null +++ b/litewallet/Views/RadialGradientView.swift @@ -0,0 +1,33 @@ +import UIKit + +class RadialGradientView: UIView { + // MARK: - Public + + init(backgroundColor: UIColor, offset: CGFloat = 0.0) { + self.offset = offset + super.init(frame: .zero) + self.backgroundColor = backgroundColor + } + + // MARK: - Private + + private let offset: CGFloat + + override func draw(_ rect: CGRect) { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let startColor = UIColor.transparentWhite.cgColor + let endColor = UIColor(white: 1.0, alpha: 0.0).cgColor + let colors = [startColor, endColor] as CFArray + let locations: [CGFloat] = [0.0, 1.0] + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: locations) else { return } + guard let context = UIGraphicsGetCurrentContext() else { return } + let center = CGPoint(x: rect.midX, y: rect.midY + offset) + let endRadius = rect.height + context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: []) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/SearchHeaderView.swift b/litewallet/Views/SearchHeaderView.swift new file mode 100644 index 000000000..f0eecd854 --- /dev/null +++ b/litewallet/Views/SearchHeaderView.swift @@ -0,0 +1,263 @@ +import UIKit + +enum SearchFilterType { + case sent + case received + case pending + case complete + case text(String) + + var description: String { + switch self { + case .sent: + return S.Search.sent.localize() + case .received: + return S.Search.received.localize() + case .pending: + return S.Search.pending.localize() + case .complete: + return S.Search.complete.localize() + case .text: + return "" + } + } + + var filter: TransactionFilter { + switch self { + case .sent: + return { $0.direction == .sent } + case .received: + return { $0.direction == .received } + case .pending: + return { $0.isPending } + case .complete: + return { !$0.isPending } + case let .text(text): + return { transaction in + let loweredText = text.lowercased() + if transaction.hash.lowercased().contains(loweredText) { + return true + } + if let address = transaction.toAddress { + if address.lowercased().contains(loweredText) { + return true + } + } + if let metaData = transaction.metaData { + if metaData.comment.lowercased().contains(loweredText) { + return true + } + } + return false + } + } + } +} + +extension SearchFilterType: Equatable {} + +func == (lhs: SearchFilterType, rhs: SearchFilterType) -> Bool { + switch (lhs, rhs) { + case (.sent, .sent): + return true + case (.received, .received): + return true + case (.pending, .pending): + return true + case (.complete, .complete): + return true + case (.text(_), .text(_)): + return true + default: + return false + } +} + +typealias TransactionFilter = (Transaction) -> Bool + +class SearchHeaderView: UIView { + init() { + super.init(frame: .zero) + } + + var didCancel: (() -> Void)? + var didChangeFilters: (([TransactionFilter]) -> Void)? + var hasSetup = false + + func triggerUpdate() { + didChangeFilters?(filters.map { $0.filter }) + } + + private let searchBar = UISearchBar() + private let sent = ShadowButton(title: S.Search.sent.localize(), type: .search) + private let received = ShadowButton(title: S.Search.received.localize(), type: .search) + private let pending = ShadowButton(title: S.Search.pending.localize(), type: .search) + private let complete = ShadowButton(title: S.Search.complete.localize(), type: .search) + private let cancel = UIButton(type: .system) + fileprivate var filters: [SearchFilterType] = [] { + didSet { + didChangeFilters?(filters.map { $0.filter }) + } + } + + private let sentFilter: TransactionFilter = { $0.direction == .sent } + private let receivedFilter: TransactionFilter = { $0.direction == .received } + + override func layoutSubviews() { + guard !hasSetup else { return } + setup() + hasSetup = true + } + + private func setup() { + addSubviews() + addFilterButtons() + addConstraints() + setData() + } + + private func addSubviews() { + addSubview(searchBar) + addSubview(cancel) + } + + private func addConstraints() { + cancel.setTitle(S.Button.cancel.localize(), for: .normal) + let titleSize = NSString(string: cancel.titleLabel!.text!).size(withAttributes: [NSAttributedString.Key.font: cancel.titleLabel!.font]) + cancel.constrain([ + cancel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + cancel.centerYAnchor.constraint(equalTo: searchBar.centerYAnchor), + cancel.widthAnchor.constraint(equalToConstant: titleSize.width + C.padding[4]), + ]) + searchBar.constrain([ + searchBar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: C.padding[1]), + searchBar.topAnchor.constraint(equalTo: topAnchor, constant: E.isIPhoneX ? C.padding[5] : C.padding[2]), + searchBar.trailingAnchor.constraint(equalTo: cancel.leadingAnchor, constant: -C.padding[1]), + ]) + } + + private func setData() { + backgroundColor = .whiteTint + searchBar.backgroundImage = UIImage() + searchBar.delegate = self + + cancel.tap = { [weak self] in + self?.didChangeFilters?([]) + self?.searchBar.resignFirstResponder() + self?.didCancel?() + } + cancel.tintColor = UIColor(red: 52.0 / 255.0, green: 52.0 / 255.0, blue: 157.0 / 255.0, alpha: 1.0) + cancel.titleLabel?.font = UIFont.customBody(size: 14.0) + + sent.isToggleable = true + received.isToggleable = true + pending.isToggleable = true + complete.isToggleable = true + + sent.tap = { [weak self] in + guard let myself = self else { return } + if myself.toggleFilterType(.sent) { + if myself.received.isSelected { + myself.received.isSelected = false + myself.toggleFilterType(.received) + } + } + } + + received.tap = { [weak self] in + guard let myself = self else { return } + if myself.toggleFilterType(.received) { + if myself.sent.isSelected { + myself.sent.isSelected = false + myself.toggleFilterType(.sent) + } + } + } + + pending.tap = { [weak self] in + guard let myself = self else { return } + if myself.toggleFilterType(.pending) { + if myself.complete.isSelected { + myself.complete.isSelected = false + myself.toggleFilterType(.complete) + } + } + } + + complete.tap = { [weak self] in + guard let myself = self else { return } + if myself.toggleFilterType(.complete) { + if myself.pending.isSelected { + myself.pending.isSelected = false + myself.toggleFilterType(.pending) + } + } + } + } + + @discardableResult private func toggleFilterType(_ filterType: SearchFilterType) -> Bool { + if let index = filters.index(of: filterType) { + filters.remove(at: index) + return false + } else { + filters.append(filterType) + return true + } + } + + private func addFilterButtons() { + if #available(iOS 9, *) { + let stackView = UIStackView() + addSubview(stackView) + stackView.distribution = .fillProportionally + stackView.spacing = C.padding[1] + stackView.constrain([ + stackView.leadingAnchor.constraint(equalTo: searchBar.leadingAnchor), + stackView.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: C.padding[1]), + stackView.trailingAnchor.constraint(equalTo: cancel.trailingAnchor), + ]) + stackView.addArrangedSubview(sent) + stackView.addArrangedSubview(received) + stackView.addArrangedSubview(pending) + stackView.addArrangedSubview(complete) + } else { + addSubview(sent) + addSubview(received) + addSubview(pending) + addSubview(complete) + sent.constrain([ + sent.leadingAnchor.constraint(equalTo: searchBar.leadingAnchor), + sent.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: C.padding[1]), + ]) + received.constrain([ + received.leadingAnchor.constraint(equalTo: sent.trailingAnchor, constant: C.padding[1]), + received.topAnchor.constraint(equalTo: sent.topAnchor), + ]) + pending.constrain([ + pending.leadingAnchor.constraint(equalTo: received.trailingAnchor, constant: C.padding[1]), + pending.topAnchor.constraint(equalTo: received.topAnchor), + ]) + complete.constrain([ + complete.leadingAnchor.constraint(equalTo: pending.trailingAnchor, constant: C.padding[1]), + complete.topAnchor.constraint(equalTo: sent.topAnchor), + ]) + } + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SearchHeaderView: UISearchBarDelegate { + func searchBar(_: UISearchBar, textDidChange searchText: String) { + let filter: SearchFilterType = .text(searchText) + if let index = filters.index(of: filter) { + filters.remove(at: index) + } + if searchText != "" { + filters.append(filter) + } + } +} diff --git a/litewallet/Views/SecurityCenterCell.swift b/litewallet/Views/SecurityCenterCell.swift new file mode 100644 index 000000000..7cdf17298 --- /dev/null +++ b/litewallet/Views/SecurityCenterCell.swift @@ -0,0 +1,92 @@ +import UIKit + +private let buttonSize: CGFloat = 16.0 + +class SecurityCenterCell: UIControl { + // MARK: - Public + + var isCheckHighlighted: Bool = false { + didSet { + check.tintColor = isCheckHighlighted ? .primaryButton : .grayTextTint + } + } + + init(title: String, descriptionText: String) { + super.init(frame: .zero) + titleLabel.text = title + + if #available(iOS 11.0, *) { + guard let headerTextColor = UIColor(named: "headerTextColor"), + let labelTextColor = UIColor(named: "labelTextColor"), + let backgroundColor = UIColor(named: "lfBackgroundColor") + else { + NSLog("ERROR: Custom colors not set") + return + } + check.tintColor = headerTextColor + titleLabel.textColor = labelTextColor + descriptionLabel.textColor = labelTextColor + self.backgroundColor = backgroundColor + } + + descriptionLabel.text = descriptionText + setup() + } + + // MARK: - Private + + private func setup() { + addSubview(titleLabel) + addSubview(descriptionLabel) + addSubview(separator) + addSubview(check) + check.constrain([ + check.leadingAnchor.constraint(equalTo: leadingAnchor, constant: C.padding[2]), + check.topAnchor.constraint(equalTo: topAnchor, constant: C.padding[2]), + check.widthAnchor.constraint(equalToConstant: buttonSize), + check.heightAnchor.constraint(equalToConstant: buttonSize), + ]) + titleLabel.constrain([ + titleLabel.leadingAnchor.constraint(equalTo: check.trailingAnchor, constant: C.padding[1]), + titleLabel.topAnchor.constraint(equalTo: check.topAnchor), + titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + ]) + descriptionLabel.constrain([ + descriptionLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + ]) + separator.constrain([ + separator.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: C.padding[3]), + separator.leadingAnchor.constraint(equalTo: check.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0), + separator.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + descriptionLabel.numberOfLines = 0 + descriptionLabel.lineBreakMode = .byWordWrapping + check.setImage(#imageLiteral(resourceName: "CircleCheck"), for: .normal) + isCheckHighlighted = false + } + + override var isHighlighted: Bool { + didSet { + if isHighlighted { + backgroundColor = .litecoinSilver + } else { + backgroundColor = .white + } + } + } + + private var titleLabel = UILabel(font: .customBold(size: 13.0)) + private var descriptionLabel = UILabel(font: .customBody(size: 13.0)) + private var separator = UIView(color: .secondaryShadow) + private var check = UIButton(type: .system) + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/SendAmountCell.swift b/litewallet/Views/SendAmountCell.swift new file mode 100644 index 000000000..413fd107c --- /dev/null +++ b/litewallet/Views/SendAmountCell.swift @@ -0,0 +1,105 @@ +import UIKit + +class SendAmountCell: SendCell { + init(placeholder: String) { + super.init() + let attributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.foregroundColor: UIColor.grayTextTint, + NSAttributedString.Key.font: placeholderFont, + ] + textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes) + textField.delegate = self + textField.textColor = .darkText + textField.inputView = UIView() + setupViews() + } + + var textFieldDidBeginEditing: (() -> Void)? + var textFieldDidReturn: ((UITextField) -> Void)? + var textFieldDidChange: ((String) -> Void)? + var content: String? { + didSet { + textField.text = content + textField.sendActions(for: .editingChanged) + guard let count = content?.count else { return } + textField.font = count > 0 ? textFieldFont : placeholderFont + } + } + + func setLabel(text: String, color: UIColor) { + label.text = text + label.textColor = color + } + + func setAmountLabel(text: String) { + textField.isHidden = text.utf8.count > 0 // Textfield should be hidden if amount label has text + cursor.isHidden = !textField.isHidden + amountLabel.text = text + } + + private let placeholderFont = UIFont.customBody(size: 16.0) + private let textFieldFont = UIFont.customBody(size: 26.0) + let textField = UITextField() + let label = UILabel(font: .customBody(size: 14.0), color: .grayTextTint) + let amountLabel = UILabel(font: .customBody(size: 26.0), color: .darkText) + private let cursor = BlinkingView(blinkColor: C.defaultTintColor) + + private func setupViews() { + addSubview(textField) + addSubview(label) + addSubview(amountLabel) + addSubview(cursor) + + textField.constrain([ + textField.constraint(.leading, toView: self, constant: C.padding[2]), + textField.centerYAnchor.constraint(equalTo: accessoryView.centerYAnchor), + textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 30.0), + textField.constraint(toLeading: accessoryView, constant: 0.0), + ]) + label.constrain([ + label.leadingAnchor.constraint(equalTo: textField.leadingAnchor), + label.topAnchor.constraint(equalTo: amountLabel.bottomAnchor, constant: C.padding[2]), + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + label.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + amountLabel.constrain([ + amountLabel.leadingAnchor.constraint(equalTo: textField.leadingAnchor), + amountLabel.topAnchor.constraint(equalTo: textField.topAnchor), + amountLabel.bottomAnchor.constraint(equalTo: textField.bottomAnchor), + ]) + cursor.constrain([ + cursor.leadingAnchor.constraint(equalTo: amountLabel.trailingAnchor, constant: 2.0), + cursor.heightAnchor.constraint(equalTo: amountLabel.heightAnchor, constant: -4.0), + cursor.centerYAnchor.constraint(equalTo: amountLabel.centerYAnchor), + cursor.widthAnchor.constraint(equalToConstant: 2.0), + ]) + + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + + textField.addTarget(self, action: #selector(SendAmountCell.editingChanged(textField:)), for: .editingChanged) + cursor.startBlinking() + cursor.isHidden = true + } + + @objc private func editingChanged(textField: UITextField) { + guard let text = textField.text else { return } + textFieldDidChange?(text) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SendAmountCell: UITextFieldDelegate { + func textFieldDidBeginEditing(_: UITextField) { + textFieldDidBeginEditing?() + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textFieldDidReturn?(textField) + return true + } +} diff --git a/litewallet/Views/SendViewCells/AddressCell.swift b/litewallet/Views/SendViewCells/AddressCell.swift new file mode 100644 index 000000000..7e0a42942 --- /dev/null +++ b/litewallet/Views/SendViewCells/AddressCell.swift @@ -0,0 +1,110 @@ +import UIKit + +class AddressCell: UIView { + init() { + super.init(frame: .zero) + setupViews() + } + + var address: String? { + return textField.text + } + + var didBeginEditing: (() -> Void)? + var didEndEditing: (() -> Void)? + var didReceivePaymentRequest: ((PaymentRequest) -> Void)? + + let textField = UITextField() + let paste = ShadowButton(title: S.Send.pasteLabel.localize(), type: .tertiary) + let scan = ShadowButton(title: S.Send.scanLabel.localize(), type: .tertiary) + private let dividerView = UIView(color: .secondaryShadow) + + private func setupViews() { + if #available(iOS 11.0, *) { + guard let textColor = UIColor(named: "labelTextColor") + else { + NSLog("ERROR: Main color") + return + } + textField.textColor = textColor + } else { + textField.textColor = .darkText + } + addSubviews() + addConstraints() + setInitialData() + } + + private func addSubviews() { + addSubview(textField) + addSubview(dividerView) + addSubview(paste) + addSubview(scan) + } + + private func addConstraints() { + textField.constrain([ + textField.constraint(.leading, toView: self, constant: 11.0), + textField.constraint(.centerY, toView: self), + textField.trailingAnchor.constraint(equalTo: paste.leadingAnchor, constant: -C.padding[1]), + ]) + scan.constrain([ + scan.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + scan.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + paste.constrain([ + paste.centerYAnchor.constraint(equalTo: centerYAnchor), + paste.trailingAnchor.constraint(equalTo: scan.leadingAnchor, constant: -C.padding[0.625]), + ]) + dividerView.constrain([ + dividerView.leadingAnchor.constraint(equalTo: leadingAnchor), + dividerView.bottomAnchor.constraint(equalTo: bottomAnchor), + dividerView.trailingAnchor.constraint(equalTo: trailingAnchor), + dividerView.heightAnchor.constraint(equalToConstant: 1.0), + ]) + } + + private func setInitialData() { + textField.font = .customBody(size: 15.0) + textField.adjustsFontSizeToFitWidth = true + textField.minimumFontSize = 10.0 + textField.placeholder = S.Send.enterLTCAddressLabel.localize() + textField.returnKeyType = .done + textField.delegate = self + textField.clearButtonMode = .whileEditing + } + + @objc private func didTap() { + textField.becomeFirstResponder() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension AddressCell: UITextFieldDelegate { + func textFieldDidBeginEditing(_: UITextField) { + didBeginEditing?() + } + + func textFieldDidEndEditing(_: UITextField) { + didEndEditing?() + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + func textField(_: UITextField, shouldChangeCharactersIn _: NSRange, replacementString string: String) -> Bool + { + if let request = PaymentRequest(string: string) { + didReceivePaymentRequest?(request) + return false + } else { + return true + } + } +} diff --git a/litewallet/Views/SendViewCells/DescriptionSendCell.swift b/litewallet/Views/SendViewCells/DescriptionSendCell.swift new file mode 100644 index 000000000..80a08fbbd --- /dev/null +++ b/litewallet/Views/SendViewCells/DescriptionSendCell.swift @@ -0,0 +1,106 @@ +import UIKit + +class DescriptionSendCell: SendCell { + init(placeholder: String) { + super.init() + textView.delegate = self + textView.font = .customBody(size: 20.0) + textView.returnKeyType = .done + self.placeholder.text = placeholder + + if #available(iOS 11.0, *) { + guard let headerTextColor = UIColor(named: "headerTextColor"), + let textColor = UIColor(named: "labelTextColor") + else { + NSLog("ERROR: Custom color") + return + } + textView.textColor = textColor + self.placeholder.textColor = headerTextColor + } else { + textView.textColor = .darkText + } + + setupViews() + } + + var didBeginEditing: (() -> Void)? + var didReturn: ((UITextView) -> Void)? + var didChange: ((String) -> Void)? + var content: String? { + didSet { + textView.text = content + textViewDidChange(textView) + } + } + + var textView = UITextView() + fileprivate var placeholder = UILabel(font: .customBody(size: 16.0), color: .grayTextTint) + private func setupViews() { + textView.isScrollEnabled = false + + textView.clipsToBounds = true + textView.layer.cornerRadius = 8.0 + + addSubview(textView) + textView.constrain([ + textView.constraint(.leading, toView: self, constant: 11.0), + textView.centerYAnchor.constraint(equalTo: centerYAnchor), + textView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0), + textView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0), + ]) + + textView.addSubview(placeholder) + placeholder.constrain([ + placeholder.centerYAnchor.constraint(equalTo: textView.centerYAnchor), + placeholder.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 16.0), + ]) + } + + func clearPlaceholder() { + placeholder.text = "" + placeholder.isHidden = true + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension DescriptionSendCell: UITextViewDelegate { + func textViewDidBeginEditing(_: UITextView) { + didBeginEditing?() + } + + func textViewDidChange(_ textView: UITextView) { + placeholder.isHidden = textView.text.utf8.count > 0 + if let text = textView.text { + didChange?(text) + } + } + + func textViewShouldEndEditing(_: UITextView) -> Bool { + return true + } + + func textView(_ textView: UITextView, shouldChangeTextIn _: NSRange, replacementText text: String) -> Bool + { + guard text.rangeOfCharacter(from: CharacterSet.newlines) == nil + else { + textView.resignFirstResponder() + return false + } + + let count = (textView.text ?? "").utf8.count + text.utf8.count + if count > C.maxMemoLength { + return false + } else { + return true + } + } + + func textViewDidEndEditing(_ textView: UITextView) { + didReturn?(textView) + } +} diff --git a/litewallet/Views/SendViewCells/SendCell.swift b/litewallet/Views/SendViewCells/SendCell.swift new file mode 100644 index 000000000..fbd348226 --- /dev/null +++ b/litewallet/Views/SendViewCells/SendCell.swift @@ -0,0 +1,29 @@ +import UIKit + +class SendCell: UIView { + static let defaultHeight: CGFloat = 55.0 // 95.0 + + init() { + super.init(frame: .zero) + setupViews() + } + + let accessoryView = UIView() + let border = UIView(color: .secondaryShadow) + + private func setupViews() { + addSubview(accessoryView) + addSubview(border) + accessoryView.constrain([ + accessoryView.constraint(.top, toView: self), + accessoryView.constraint(.trailing, toView: self), + accessoryView.heightAnchor.constraint(equalToConstant: SendCell.defaultHeight), + ]) + border.constrainBottomCorners(height: 1.0) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/SeparatorCell.swift b/litewallet/Views/SeparatorCell.swift new file mode 100644 index 000000000..e33320584 --- /dev/null +++ b/litewallet/Views/SeparatorCell.swift @@ -0,0 +1,21 @@ +import UIKit + +class SeparatorCell: UITableViewCell { + override init(style: CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + let separator = UIView() + separator.backgroundColor = .secondaryShadow + addSubview(separator) + separator.constrain([ + separator.leadingAnchor.constraint(equalTo: leadingAnchor), + separator.bottomAnchor.constraint(equalTo: bottomAnchor), + separator.trailingAnchor.constraint(equalTo: trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1.0), + ]) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/ShadowButton.swift b/litewallet/Views/ShadowButton.swift new file mode 100644 index 000000000..8b3468e1d --- /dev/null +++ b/litewallet/Views/ShadowButton.swift @@ -0,0 +1,254 @@ +import UIKit + +enum ButtonType { + case primary + case secondary + case tertiary + case blackTransparent + case search + case warning + case boldWarning + case flatWhite + case flatWhiteBorder + case flatLitecoinBlue +} + +let minTargetSize: CGFloat = 48.0 + +class ShadowButton: UIControl { + init(title: String, type: ButtonType) { + self.title = title + self.type = type + super.init(frame: .zero) + accessibilityLabel = title + setupViews() + } + + init(title: String, type: ButtonType, image: UIImage) { + self.title = title + self.type = type + self.image = image + super.init(frame: .zero) + accessibilityLabel = title + setupViews() + } + + var isToggleable = false + var title: String { + didSet { + label.text = title + } + } + + var image: UIImage? { + didSet { + imageView?.image = image + } + } + + private let type: ButtonType + private let container = UIView() + private let shadowView = UIView() + private let label = UILabel() + private let shadowYOffset: CGFloat = 4.0 + private let cornerRadius: CGFloat = 4.0 + private var imageView: UIImageView? + + override var isHighlighted: Bool { + didSet { + if isHighlighted { + UIView.animate(withDuration: 0.04, animations: { + let shrink = CATransform3DMakeScale(0.97, 0.97, 1.0) + let translate = CATransform3DTranslate(shrink, 0, 4.0, 0) + self.container.layer.transform = translate + }) + } else { + UIView.animate(withDuration: 0.04, animations: { + self.container.transform = CGAffineTransform.identity + }) + } + } + } + + override var isSelected: Bool { + didSet { + guard isToggleable else { return } + if type == .tertiary || type == .search { + if isSelected { + container.layer.borderColor = UIColor.primaryButton.cgColor + imageView?.tintColor = .primaryButton + label.textColor = .primaryButton + } else { + setColors() + } + } + } + } + + private func setupViews() { + addShadowView() + addContent() + setColors() + addTarget(self, action: #selector(ShadowButton.touchUpInside), for: .touchUpInside) + setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal) + label.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal) + setContentHuggingPriority(UILayoutPriority.required, for: .horizontal) + label.setContentHuggingPriority(UILayoutPriority.required, for: .horizontal) + } + + private func addShadowView() { + addSubview(shadowView) + shadowView.constrain([ + NSLayoutConstraint(item: shadowView, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 0.5, constant: 0.0), + shadowView.constraint(.bottom, toView: self), + shadowView.constraint(.centerX, toView: self), + NSLayoutConstraint(item: shadowView, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.8, constant: 0.0), + ]) + shadowView.layer.cornerRadius = 4.0 + shadowView.layer.shadowOffset = CGSize(width: 0, height: 4) + shadowView.backgroundColor = .white + shadowView.isUserInteractionEnabled = false + } + + private func addContent() { + addSubview(container) + container.backgroundColor = .primaryButton + container.layer.cornerRadius = cornerRadius + container.isUserInteractionEnabled = false + container.constrain(toSuperviewEdges: nil) + label.text = title + label.textColor = .white + label.textAlignment = .center + label.isUserInteractionEnabled = false + label.font = UIFont.customMedium(size: 16.0) + configureContentType() + } + + private func configureContentType() { + if let icon = image { + setupImageOption(icon: icon) + } else { + setupLabelOnly() + } + } + + private func setupImageOption(icon: UIImage) { + let content = UIView() + let iconImageView = UIImageView(image: icon.withRenderingMode(.alwaysTemplate)) + iconImageView.contentMode = .scaleAspectFit + container.addSubview(content) + content.addSubview(label) + content.addSubview(iconImageView) + content.constrainToCenter() + iconImageView.constrainLeadingCorners() + label.constrainTrailingCorners() + iconImageView.constrain([ + iconImageView.constraint(toLeading: label, constant: -C.padding[1]), + ]) + imageView = iconImageView + } + + private func setupLabelOnly() { + container.addSubview(label) + label.constrain(toSuperviewEdges: UIEdgeInsets(top: C.padding[1], left: C.padding[1], bottom: -C.padding[1], right: -C.padding[1])) + } + + private func setColors() { + switch type { + case .flatLitecoinBlue: + container.backgroundColor = .liteWalletBlue + label.textColor = .primaryText + container.layer.borderColor = UIColor.white.cgColor + container.layer.borderWidth = 1.0 + imageView?.tintColor = .white + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOpacity = 0.15 + case .flatWhite: + container.backgroundColor = .white + label.textColor = .liteWalletBlue + container.layer.borderColor = nil + container.layer.borderWidth = 0.0 + imageView?.tintColor = .liteWalletBlue + case .flatWhiteBorder: + container.backgroundColor = .white + label.textColor = .liteWalletBlue + container.layer.borderColor = UIColor.liteWalletBlue.cgColor + container.layer.borderWidth = 1.0 + imageView?.tintColor = .liteWalletBlue + case .primary: + container.backgroundColor = .primaryButton + label.textColor = .primaryText + container.layer.borderColor = nil + container.layer.borderWidth = 0.0 + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOpacity = 0.3 + imageView?.tintColor = .white + case .secondary: + container.backgroundColor = .secondaryButton + label.textColor = .darkText + container.layer.borderColor = UIColor.secondaryBorder.cgColor + container.layer.borderWidth = 1.0 + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOpacity = 0.15 + imageView?.tintColor = .darkText + case .tertiary: + container.backgroundColor = .secondaryButton + label.textColor = .grayTextTint + container.layer.borderColor = UIColor.secondaryBorder.cgColor + container.layer.borderWidth = 1.0 + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOpacity = 0.15 + imageView?.tintColor = .grayTextTint + case .blackTransparent: + container.backgroundColor = .clear + label.textColor = .darkText + container.layer.borderColor = UIColor.darkText.cgColor + container.layer.borderWidth = 1.0 + imageView?.tintColor = .grayTextTint + shadowView.isHidden = true + case .search: + label.font = UIFont.customBody(size: 13.0) + container.backgroundColor = .secondaryButton + label.textColor = .grayTextTint + container.layer.borderColor = UIColor.secondaryBorder.cgColor + container.layer.borderWidth = 1.0 + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOpacity = 0.15 + imageView?.tintColor = .grayTextTint + case .warning: + label.font = UIFont.customBody(size: 13.0) + container.backgroundColor = .pink + label.textColor = .white + container.layer.borderColor = UIColor.secondaryBorder.cgColor + container.layer.borderWidth = 1.0 + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOpacity = 0.15 + // imageView?.tintColor = .grayTextTint + case .boldWarning: + label.font = UIFont.customBold(size: 15.0) + container.backgroundColor = .pink + label.textColor = .white + container.layer.borderColor = UIColor.secondaryBorder.cgColor + container.layer.borderWidth = 1.0 + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOpacity = 0.15 + } + } + + override open func hitTest(_ point: CGPoint, with _: UIEvent?) -> UIView? { + guard !isHidden || isUserInteractionEnabled else { return nil } + let deltaX = max(minTargetSize - bounds.width, 0) + let deltaY = max(minTargetSize - bounds.height, 0) + let hitFrame = bounds.insetBy(dx: -deltaX / 2.0, dy: -deltaY / 2.0) + return hitFrame.contains(point) ? self : nil + } + + @objc private func touchUpInside() { + isSelected = !isSelected + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/SyncingView.swift b/litewallet/Views/SyncingView.swift new file mode 100644 index 000000000..a406f7a35 --- /dev/null +++ b/litewallet/Views/SyncingView.swift @@ -0,0 +1,106 @@ +import UIKit + +private let progressHeight: CGFloat = 8.0 + +class SyncingView: UIView { + init() { + super.init(frame: .zero) + setup() + } + + var progress: CGFloat = 0.0 { + didSet { + progressForegroundWidth?.isActive = false + progressForegroundWidth = progressForeground.widthAnchor.constraint(equalTo: progressBackground.widthAnchor, multiplier: progress) + progressForegroundWidth?.isActive = true + progressForeground.setNeedsDisplay() + } + } + + var timestamp: UInt32 = 0 { + didSet { + date.text = dateFormatter.string(from: Date(timeIntervalSince1970: Double(timestamp))) + } + } + + func setIsConnecting() { + header.text = S.SyncingView.connecting.localize() + date.text = "" + } + + func reset() { + setInitialData() + } + + private let header = UILabel(font: .customBold(size: 14.0)) + private let date = UILabel(font: .customBody(size: 13.0)) + + private let progressBackground: UIView = { + let view = UIView() + view.layer.cornerRadius = progressHeight / 2.0 + view.layer.masksToBounds = true + view.backgroundColor = .secondaryShadow + return view + }() + + private let progressForeground: UIView = { + let view = GradientView() + view.layer.cornerRadius = progressHeight / 2.0 + view.layer.masksToBounds = true + return view + }() + + private let dateFormatter: DateFormatter = { + let df = DateFormatter() + df.setLocalizedDateFormatFromTemplate("MMM d, yyyy") + return df + }() + + private var progressForegroundWidth: NSLayoutConstraint? + + private func setup() { + addSubview(header) + addSubview(date) + addSubview(progressBackground) + + progressBackground.addSubview(progressForeground) + + header.constrain([ + header.leadingAnchor.constraint(equalTo: leadingAnchor, constant: C.padding[2]), + header.topAnchor.constraint(equalTo: topAnchor, constant: C.padding[2]), + ]) + + progressBackground.constrain([ + progressBackground.leadingAnchor.constraint(equalTo: header.leadingAnchor), + progressBackground.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + progressBackground.topAnchor.constraint(equalTo: header.bottomAnchor, constant: C.padding[2]), + progressBackground.heightAnchor.constraint(equalToConstant: progressHeight), + ]) + + date.constrain([ + date.leadingAnchor.constraint(equalTo: progressBackground.leadingAnchor), + date.topAnchor.constraint(equalTo: progressBackground.bottomAnchor, constant: C.padding[1]), + ]) + + progressForegroundWidth = progressForeground.widthAnchor.constraint(equalTo: progressBackground.widthAnchor, multiplier: progress) + progressForeground.constrain([ + progressForegroundWidth, + progressForeground.leadingAnchor.constraint(equalTo: progressBackground.leadingAnchor), + progressForeground.centerYAnchor.constraint(equalTo: progressBackground.centerYAnchor), + progressForeground.heightAnchor.constraint(equalTo: progressBackground.heightAnchor), + ]) + + setInitialData() + } + + private func setInitialData() { + header.text = S.SyncingView.syncing.localize() + header.textColor = .darkText + date.text = "" + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/UnEditableTextView.swift b/litewallet/Views/UnEditableTextView.swift new file mode 100644 index 000000000..ed91bb69b --- /dev/null +++ b/litewallet/Views/UnEditableTextView.swift @@ -0,0 +1,7 @@ +import UIKit + +class UnEditableTextView: UITextView { + override var canBecomeFirstResponder: Bool { + return false + } +} diff --git a/litewallet/Views/UpdatingLabel.swift b/litewallet/Views/UpdatingLabel.swift new file mode 100644 index 000000000..953ec93f3 --- /dev/null +++ b/litewallet/Views/UpdatingLabel.swift @@ -0,0 +1,79 @@ +import UIKit + +class UpdatingLabel: UILabel { + var formatter: NumberFormatter { + didSet { + setFormattedText(forValue: value) + } + } + + init(formatter: NumberFormatter) { + self.formatter = formatter + super.init(frame: .zero) + text = self.formatter.string(from: 0 as NSNumber) + } + + var completion: (() -> Void)? + private var value: Double = 0.0 + + func setValue(_ value: Double) { + self.value = value + setFormattedText(forValue: value) + } + + func setValueAnimated(_ endingValue: Double, completion: @escaping () -> Void) { + self.completion = completion + guard let currentText = text else { return } + guard let startingValue = formatter.number(from: currentText)?.doubleValue else { return } + self.startingValue = startingValue + self.endingValue = endingValue + + timer?.invalidate() + lastUpdate = CACurrentMediaTime() + progress = 0.0 + + startTimer() + } + + private let duration = 0.6 + private var easingRate: Double = 3.0 + private var timer: CADisplayLink? + private var startingValue: Double = 0.0 + private var endingValue: Double = 0.0 + private var progress: Double = 0.0 + private var lastUpdate: CFTimeInterval = 0.0 + + private func startTimer() { + timer = CADisplayLink(target: self, selector: #selector(UpdatingLabel.update)) + timer?.preferredFramesPerSecond = 2 + timer?.add(to: .main, forMode: .default) + timer?.add(to: .main, forMode: .tracking) + } + + @objc private func update() { + let now = CACurrentMediaTime() + progress = progress + (now - lastUpdate) + lastUpdate = now + if progress >= duration { + timer?.invalidate() + timer = nil + setFormattedText(forValue: endingValue) + completion?() + } else { + let percentProgress = progress / duration + let easedVal = 1.0 - pow(1.0 - percentProgress, easingRate) + setFormattedText(forValue: startingValue + (easedVal * (endingValue - startingValue))) + } + } + + private func setFormattedText(forValue: Double) { + value = forValue + text = formatter.string(from: forValue as NSNumber) + sizeToFit() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Views/WalletDisabledView.swift b/litewallet/Views/WalletDisabledView.swift new file mode 100644 index 000000000..4a2304c42 --- /dev/null +++ b/litewallet/Views/WalletDisabledView.swift @@ -0,0 +1,84 @@ +import UIKit + +class WalletDisabledView: UIView { + func setTimeLabel(string: String) { + label.text = string + } + + init(store: Store) { + self.store = store + faq = UIButton.buildFaqButton(store: store, articleId: ArticleIds.nothing) + blur = UIVisualEffectView() + super.init(frame: .zero) + setup() + } + + func show() { + UIView.animate(withDuration: C.animationDuration, animations: { + self.blur.effect = self.effect + }) + } + + func hide(completion: @escaping () -> Void) { + UIView.animate(withDuration: C.animationDuration, animations: { + self.blur.effect = nil + }, completion: { _ in + completion() + }) + } + + var didTapReset: (() -> Void)? { + didSet { + reset.tap = didTapReset + } + } + + private let label = UILabel(font: .customBold(size: 20.0), color: .darkText) + private let store: Store + private let faq: UIButton + private let blur: UIVisualEffectView + private let reset = ShadowButton(title: S.UnlockScreen.resetPin.localize(), type: .blackTransparent) + private let effect = UIBlurEffect(style: .light) + + private func setup() { + addSubviews() + addConstraints() + setData() + } + + private func addSubviews() { + addSubview(blur) + addSubview(label) + addSubview(faq) + addSubview(reset) + } + + private func addConstraints() { + blur.constrain(toSuperviewEdges: nil) + label.constrain([ + label.centerYAnchor.constraint(equalTo: blur.centerYAnchor), + label.centerXAnchor.constraint(equalTo: blur.centerXAnchor), + ]) + faq.constrain([ + faq.leadingAnchor.constraint(equalTo: leadingAnchor, constant: C.padding[2]), + faq.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -C.padding[2]), + faq.widthAnchor.constraint(equalToConstant: 44.0), + faq.heightAnchor.constraint(equalToConstant: 44.0), + ]) + reset.constrain([ + reset.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -C.padding[2]), + reset.centerYAnchor.constraint(equalTo: faq.centerYAnchor), + reset.heightAnchor.constraint(equalToConstant: C.Sizes.buttonHeight), + reset.widthAnchor.constraint(equalToConstant: 200.0), + ]) + } + + private func setData() { + label.textAlignment = .center + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/litewallet/Wallet/ExchangeUpdater.swift b/litewallet/Wallet/ExchangeUpdater.swift new file mode 100644 index 000000000..5b52eb0a0 --- /dev/null +++ b/litewallet/Wallet/ExchangeUpdater.swift @@ -0,0 +1,30 @@ +import Foundation + +class ExchangeUpdater: Subscriber { + // MARK: - Public + + init(store: Store, walletManager: WalletManager) { + self.store = store + self.walletManager = walletManager + store.subscribe(self, + selector: { $0.defaultCurrencyCode != $1.defaultCurrencyCode }, + callback: { state in + guard let currentRate = state.rates.first(where: { $0.code == state.defaultCurrencyCode }) else { return } + self.store.perform(action: ExchangeRates.setRate(currentRate)) + }) + } + + func refresh(completion: @escaping () -> Void) { + walletManager.apiClient?.exchangeRates { rates, _ in + + guard let currentRate = rates.first(where: { $0.code == self.store.state.defaultCurrencyCode }) else { completion(); return } + self.store.perform(action: ExchangeRates.setRates(currentRate: currentRate, rates: rates)) + completion() + } + } + + // MARK: - Private + + let store: Store + let walletManager: WalletManager +} diff --git a/litewallet/WalletCoordinator.swift b/litewallet/WalletCoordinator.swift new file mode 100644 index 000000000..d77141399 --- /dev/null +++ b/litewallet/WalletCoordinator.swift @@ -0,0 +1,286 @@ +import AVFoundation +import Foundation +import UIKit + +private let lastBlockHeightKey = "LastBlockHeightKey" +private let progressUpdateInterval: TimeInterval = 0.5 +private let updateDebounceInterval: TimeInterval = 0.4 + +class WalletCoordinator: Subscriber, Trackable { + var kvStore: BRReplicatedKVStore? { + didSet { + requestTxUpdate() + } + } + + private let walletManager: WalletManager + private let store: Store + private var progressTimer: Timer? + private var updateTimer: Timer? + private let defaults = UserDefaults.standard + private var backgroundTaskId: UIBackgroundTaskIdentifier? + private var reachability = ReachabilityMonitor() + private var retryTimer: RetryTimer? + + init(walletManager: WalletManager, store: Store) { + self.walletManager = walletManager + self.store = store + addWalletObservers() + addSubscriptions() + updateBalance() + reachability.didChange = { [weak self] isReachable in + self?.reachabilityDidChange(isReachable: isReachable) + } + } + + private var lastBlockHeight: UInt32 { + set { + defaults.set(newValue, forKey: lastBlockHeightKey) + } + get { + return UInt32(defaults.integer(forKey: lastBlockHeightKey)) + } + } + + @objc private func updateProgress() { + DispatchQueue.walletQueue.async { + guard let progress = self.walletManager.peerManager?.syncProgress(fromStartHeight: self.lastBlockHeight), let timestamp = self.walletManager.peerManager?.lastBlockTimestamp else { return } + DispatchQueue.main.async { + self.store.perform(action: WalletChange.setProgress(progress: progress, timestamp: timestamp)) + } + } + updateBalance() + } + + private func onSyncStart() { + endBackgroundTask() + startBackgroundTask() + progressTimer = Timer.scheduledTimer(timeInterval: progressUpdateInterval, target: self, selector: #selector(WalletCoordinator.updateProgress), userInfo: nil, repeats: true) + store.perform(action: WalletChange.setSyncingState(.syncing)) + startActivity() + } + + private func onSyncStop(notification: Notification) { + if UIApplication.shared.applicationState != .active { + DispatchQueue.walletQueue.async { + self.walletManager.peerManager?.disconnect() + } + } + endBackgroundTask() + if notification.userInfo != nil { + guard let code = notification.userInfo?["errorCode"] else { return } + guard let message = notification.userInfo?["errorDescription"] else { return } + store.perform(action: WalletChange.setSyncingState(.connecting)) + saveEvent("event.syncErrorMessage", attributes: ["message": "\(message) (\(code))"]) + endActivity() + + if retryTimer == nil, reachability.isReachable { + retryTimer = RetryTimer() + retryTimer?.callback = strongify(self) { myself in + myself.store.trigger(name: .retrySync) + } + retryTimer?.start() + } + + return + } + retryTimer?.stop() + retryTimer = nil + if let height = walletManager.peerManager?.lastBlockHeight { + lastBlockHeight = height + } + progressTimer?.invalidate() + progressTimer = nil + store.perform(action: WalletChange.setSyncingState(.success)) + endActivity() + } + + private func endBackgroundTask() { + if let taskId = backgroundTaskId { + UIApplication.shared.endBackgroundTask(taskId) + backgroundTaskId = nil + } + } + + private func startBackgroundTask() { + backgroundTaskId = UIApplication.shared.beginBackgroundTask(expirationHandler: { + DispatchQueue.walletQueue.async { + self.walletManager.peerManager?.disconnect() + } + }) + } + + private func requestTxUpdate() { + if updateTimer == nil { + updateTimer = Timer.scheduledTimer(timeInterval: updateDebounceInterval, target: self, selector: #selector(updateTransactions), userInfo: nil, repeats: false) + } + } + + @objc private func updateTransactions() { + updateTimer?.invalidate() + updateTimer = nil + DispatchQueue.walletQueue.async { + guard let txRefs = self.walletManager.wallet?.transactions else { + let properties = ["error_message": "wallet_tx_refs_are_nil"] + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR, properties: properties) + return + } + let transactions = self.makeTransactionViewModels(transactions: txRefs, walletManager: self.walletManager, kvStore: self.kvStore, rate: self.store.state.currentRate) + if !transactions.isEmpty { + DispatchQueue.main.async { + self.store.perform(action: WalletChange.setTransactions(transactions)) + } + } else { + let properties = ["transactions_info": "no_txs_found_in_wallet"] + LWAnalytics.logEventWithParameters(itemName: ._20240214_TI, properties: properties) + } + } + } + + func makeTransactionViewModels(transactions: [BRTxRef?], walletManager: WalletManager, kvStore: BRReplicatedKVStore?, rate: Rate?) -> [Transaction] + { + return transactions.compactMap { $0 }.sorted { + if $0.pointee.timestamp == 0 { + return true + } else if $1.pointee.timestamp == 0 { + return false + } else { + return $0.pointee.timestamp > $1.pointee.timestamp + } + }.compactMap { + Transaction($0, walletManager: walletManager, kvStore: kvStore, rate: rate) + } + } + + private func addWalletObservers() { + weak var myself = self + NotificationCenter.default.addObserver(forName: .walletBalanceChangedNotification, object: nil, queue: nil, using: { + _ in + myself?.updateBalance() + myself?.requestTxUpdate() + }) + + NotificationCenter.default.addObserver(forName: .walletTxStatusUpdateNotification, object: nil, queue: nil, using: { _ in + myself?.requestTxUpdate() + }) + + NotificationCenter.default.addObserver(forName: .walletTxRejectedNotification, object: nil, queue: nil, using: { note in + guard let recommendRescan = note.userInfo?["recommendRescan"] as? Bool else { return } + myself?.requestTxUpdate() + if recommendRescan { + myself?.store.perform(action: RecommendRescan.set(recommendRescan)) + } + }) + + NotificationCenter.default.addObserver(forName: .walletSyncStartedNotification, object: nil, queue: nil, using: { _ in + myself?.onSyncStart() + }) + + NotificationCenter.default.addObserver(forName: .walletSyncStoppedNotification, object: nil, queue: nil, using: { note in + myself?.onSyncStop(notification: note) + }) + + NotificationCenter.default.addObserver(forName: .languageChangedNotification, object: nil, queue: nil, using: { _ in + myself?.updateTransactions() + }) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + private func updateBalance() { + DispatchQueue.walletQueue.async { + guard let newBalance = self.walletManager.wallet?.balance else { return } + DispatchQueue.main.async { + self.checkForReceived(newBalance: newBalance) + self.store.perform(action: WalletChange.setBalance(newBalance)) + } + } + } + + private func checkForReceived(newBalance: UInt64) { + if let oldBalance = store.state.walletState.balance { + if newBalance > oldBalance { + if store.state.walletState.syncState == .success { + showReceived(amount: newBalance - oldBalance) + } + } + } + } + + private func showReceived(amount: UInt64) { + if let rate = store.state.currentRate { + let amount = Amount(amount: amount, rate: rate, maxDigits: store.state.maxDigits) + let primary = store.state.isLtcSwapped ? amount.localCurrency : amount.bits + let secondary = store.state.isLtcSwapped ? amount.bits : amount.localCurrency + let message = String(format: S.TransactionDetails.received.localize(), "\(primary) (\(secondary))") + store.trigger(name: .lightWeightAlert(message)) + showLocalNotification(message: message) + ping() + } + } + + private func ping() { + if let url = Bundle.main.url(forResource: "coinflip", withExtension: "aiff") { + var id: SystemSoundID = 0 + AudioServicesCreateSystemSoundID(url as CFURL, &id) + AudioServicesAddSystemSoundCompletion(id, nil, nil, { soundId, _ in + AudioServicesDisposeSystemSoundID(soundId) + }, nil) + AudioServicesPlaySystemSound(id) + } + } + + private func showLocalNotification(message: String) { + guard UIApplication.shared.applicationState == .background || UIApplication.shared.applicationState == .inactive else { return } + guard store.state.isPushNotificationsEnabled else { return } + UIApplication.shared.applicationIconBadgeNumber = UIApplication.shared.applicationIconBadgeNumber + 1 + let notification = + UILocalNotification() + notification.alertBody = message + notification.soundName = "coinflip.aiff" + UIApplication.shared.presentLocalNotificationNow(notification) + } + + private func reachabilityDidChange(isReachable: Bool) { + if !isReachable { + DispatchQueue.walletQueue.async { + self.walletManager.peerManager?.disconnect() + DispatchQueue.main.async { + self.store.perform(action: WalletChange.setSyncingState(.connecting)) + } + } + } + } + + private func addSubscriptions() { + store.subscribe(self, name: .retrySync, callback: { _ in + DispatchQueue.walletQueue.async { + self.walletManager.peerManager?.connect() + } + }) + + store.subscribe(self, name: .rescan, callback: { _ in + self.store.perform(action: RecommendRescan.set(false)) + // In case rescan is called while a sync is in progess + // we need to make sure it's false before a rescan starts + // self.store.perform(action: WalletChange.setIsSyncing(false)) + DispatchQueue.walletQueue.async { + self.walletManager.peerManager?.rescan() + } + }) + + store.subscribe(self, name: .rescan, callback: { _ in + self.store.perform(action: WalletChange.setIsRescanning(true)) + }) + } + + private func startActivity() { + UIApplication.shared.isIdleTimerDisabled = true + } + + private func endActivity() { + UIApplication.shared.isIdleTimerDisabled = false + } +} diff --git a/litewallet/WalletManager+Auth.swift b/litewallet/WalletManager+Auth.swift new file mode 100644 index 000000000..fbe8cb4c9 --- /dev/null +++ b/litewallet/WalletManager+Auth.swift @@ -0,0 +1,613 @@ +import BRCore +import FirebaseAnalytics +import Foundation +import LocalAuthentication +import SQLite3 +import UIKit + +private let WalletSecAttrService = "com.litecoin.loafwallet" +private let BIP39CreationTime = TimeInterval(BIP39_CREATION_TIME) - NSTimeIntervalSince1970 + +/// WalletAuthenticator is a protocol whose implementors are able to interact with wallet authentication +public protocol WalletAuthenticator { + var noWallet: Bool { get } + var apiAuthKey: String? { get } + var userAccount: [AnyHashable: Any]? { get set } +} + +struct NoAuthAuthenticator: WalletAuthenticator { + let noWallet = true + let apiAuthKey: String? = nil + var userAccount: [AnyHashable: Any]? = nil +} + +enum BiometricsResult { + case success + case cancel + case fallback + case failure +} + +enum TransferCardResult { + case success + case cancel + case fallback + case failure +} + +extension WalletManager: WalletAuthenticator { + private static var failedPins = [String]() + + convenience init(store: Store, dbPath: String? = nil) throws { + Task { + if await !UIApplication.shared.isProtectedDataAvailable { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecNotAvailable)) + } + } + + if try keychainItem(key: KeychainKey.seed) as Data? != nil { // upgrade from old keychain scheme + let seedPhrase: String? = try keychainItem(key: KeychainKey.mnemonic) + var seed = UInt512() + print("upgrading to authenticated keychain scheme") + BRBIP39DeriveKey(&seed, seedPhrase, nil) + let mpk = BRBIP32MasterPubKey(&seed, MemoryLayout.size) + seed = UInt512() // clear seed + try setKeychainItem(key: KeychainKey.mnemonic, item: seedPhrase, authenticated: true) + try setKeychainItem(key: KeychainKey.masterPubKey, item: Data(masterPubKey: mpk)) + try setKeychainItem(key: KeychainKey.seed, item: nil as Data?) + } + + let mpkData: Data? = try keychainItem(key: KeychainKey.masterPubKey) + guard let masterPubKey = mpkData?.masterPubKey + else { + try self.init(masterPubKey: BRMasterPubKey(), + earliestKeyTime: 0, + dbPath: dbPath, + store: store, + fpRate: FalsePositiveRates.semiPrivate.rawValue) + return + } + + var earliestKeyTime = BIP39CreationTime + if let creationTime: Data = try keychainItem(key: KeychainKey.creationTime), + creationTime.count == MemoryLayout.stride + { + creationTime.withUnsafeBytes { earliestKeyTime = $0.pointee } + } + + try self.init(masterPubKey: masterPubKey, + earliestKeyTime: earliestKeyTime, + dbPath: dbPath, + store: store, + fpRate: FalsePositiveRates.semiPrivate.rawValue) + } + + // true if keychain is available and we know that no wallet exists on it + var noWallet: Bool { + if didInitWallet { return false } + return WalletManager.staticNoWallet + } + + static var staticNoWallet: Bool { + do { + if try keychainItem(key: KeychainKey.masterPubKey) as Data? != nil { return false } + if try keychainItem(key: KeychainKey.seed) as Data? != nil { return false } // check for old keychain scheme + return true + } catch { return false } + } + + // Login with pin should be required if the pin hasn't been used within a week + var pinLoginRequired: Bool { + let pinUnlockTime = UserDefaults.standard.double(forKey: DefaultsKey.pinUnlockTime) + let now = Date.timeIntervalSinceReferenceDate + let secondsInWeek = 60.0 * 60.0 * 24.0 * 7.0 + return now - pinUnlockTime > secondsInWeek + } + + // true if the given transaction can be signed with biometric authentication + func canUseBiometrics(forTx: BRTxRef) -> Bool { + guard LAContext.canUseBiometrics else { return false } + + do { + let spendLimit: Int64 = try keychainItem(key: KeychainKey.spendLimit) ?? 0 + guard let wallet = wallet else { assertionFailure("No wallet!"); return false } + return wallet.amountSentByTx(forTx) - wallet.amountReceivedFromTx(forTx) + wallet.totalSent <= UInt64(spendLimit) + } catch { return false } + } + + var spendingLimit: UInt64 { + get { + guard UserDefaults.standard.object(forKey: DefaultsKey.spendLimitAmount) != nil + else { + return 0 + } + return UInt64(UserDefaults.standard.double(forKey: DefaultsKey.spendLimitAmount)) + } + set { + guard let wallet = wallet + else { + assertionFailure("No wallet!") + return + } + do { + try setKeychainItem(key: KeychainKey.spendLimit, item: Int64(wallet.totalSent + newValue)) + UserDefaults.standard.set(newValue, forKey: DefaultsKey.spendLimitAmount) + } catch { + print("Set spending limit error: \(error)") + } + } + } + + // number of unique failed pin attempts remaining before wallet is wiped + var pinAttemptsRemaining: Int { + do { + let failCount: Int64 = try keychainItem(key: KeychainKey.pinFailCount) ?? 0 + return Int(8 - failCount) + } catch { return -1 } + } + + // after 3 or more failed pin attempts, authentication is disabled until this time (interval since reference date) + var walletDisabledUntil: TimeInterval { + do { + let failCount: Int64 = try keychainItem(key: KeychainKey.pinFailCount) ?? 0 + guard failCount >= 3 else { return 0 } + let failTime: Int64 = try keychainItem(key: KeychainKey.pinFailTime) ?? 0 + return Double(failTime) + pow(6, Double(failCount - 3)) * 60 + } catch { + assertionFailure("Error: \(error)") + return 0 + } + } + + // Can be expensive...result should be cached + var pinLength: Int { + do { + if let pin: String = try keychainItem(key: KeychainKey.pin) { + return pin.utf8.count + } else { + return 6 + } + } catch { + print("Pin keychain error: \(error)") + return 6 + } + } + + // true if pin is correct + func authenticate(pin: String) -> Bool { + do { + let secureTime = Date().timeIntervalSince1970 // TODO: XXX use secure time from https request + var failCount: Int64 = try keychainItem(key: KeychainKey.pinFailCount) ?? 0 + + if failCount >= 3 { + let failTime: Int64 = try keychainItem(key: KeychainKey.pinFailTime) ?? 0 + + if secureTime < Double(failTime) + pow(6, Double(failCount - 3)) * 60 { // locked out + return false + } + } + + if !WalletManager.failedPins.contains(pin) { // count unique attempts before checking success + failCount += 1 + try setKeychainItem(key: KeychainKey.pinFailCount, item: failCount) + } + + if try pin == keychainItem(key: KeychainKey.pin) { // successful pin attempt + try authenticationSuccess() + return true + } else if !WalletManager.failedPins.contains(pin) { // unique failed attempt + WalletManager.failedPins.append(pin) + + if failCount >= 8 { // wipe wallet after 8 failed pin attempts and 24+ hours of lockout + if !wipeWallet() { return false } + return false + } + let pinFailTime: Int64 = try keychainItem(key: KeychainKey.pinFailTime) ?? 0 + if secureTime > Double(pinFailTime) { + try setKeychainItem(key: KeychainKey.pinFailTime, item: Int64(secureTime)) + } + } + + return false + } catch { + assertionFailure("Error: \(error)") + return false + } + } + + // true if phrase is correct + func authenticate(phrase: String) -> Bool { + do { + var seed = UInt512() + guard let nfkdPhrase = CFStringCreateMutableCopy(secureAllocator, 0, phrase as CFString) + else { return false } + CFStringNormalize(nfkdPhrase, .KD) + BRBIP39DeriveKey(&seed, nfkdPhrase as String, nil) + let mpk = BRBIP32MasterPubKey(&seed, MemoryLayout.size) + seed = UInt512() // clear seed + let mpkData: Data? = try keychainItem(key: KeychainKey.masterPubKey) + guard mpkData?.masterPubKey == mpk else { return false } + return true + } catch { + return false + } + } + + private func authenticationSuccess() throws { + let limit = Int64(UserDefaults.standard.double(forKey: DefaultsKey.spendLimitAmount)) + + WalletManager.failedPins.removeAll() + UserDefaults.standard.set(Date.timeIntervalSinceReferenceDate, forKey: DefaultsKey.pinUnlockTime) + try setKeychainItem(key: KeychainKey.pinFailTime, item: Int64(0)) + try setKeychainItem(key: KeychainKey.pinFailCount, item: Int64(0)) + + if let wallet = wallet, limit > 0 { + try setKeychainItem(key: KeychainKey.spendLimit, + item: Int64(wallet.totalSent) + limit) + } + } + + // show biometric dialog and call completion block with success or failure + func authenticate(biometricsPrompt: String, completion: @escaping (BiometricsResult) -> Void) { + let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics + LAContext().evaluatePolicy(policy, localizedReason: biometricsPrompt, + reply: { success, error in + DispatchQueue.main.async { + if success { return completion(.success) } + guard let error = error else { return completion(.failure) } + if error._code == Int(kLAErrorUserCancel) { + return completion(.cancel) + } else if error._code == Int(kLAErrorUserFallback) { + return completion(.fallback) + } + completion(.failure) + } + }) + } + + // sign the given transaction using pin authentication + func signTransaction(_ tx: BRTxRef, forkId: Int = 0, pin: String) -> Bool { + guard authenticate(pin: pin) else { return false } + return signTx(tx, forkId: forkId) + } + + // sign the given transaction using biometric authentication + func signTransaction(_ tx: BRTxRef, biometricsPrompt: String, completion: @escaping (BiometricsResult) -> Void) + { + do { + let spendLimit: Int64 = try keychainItem(key: KeychainKey.spendLimit) ?? 0 + guard let wallet = wallet, wallet.amountSentByTx(tx) - wallet.amountReceivedFromTx(tx) + wallet.totalSent <= UInt64(spendLimit) + else { + return completion(.failure) + } + } catch { return completion(.failure) } + store.perform(action: biometricsActions.setIsPrompting(true)) + authenticate(biometricsPrompt: biometricsPrompt) { result in + self.store.perform(action: biometricsActions.setIsPrompting(false)) + guard result == .success else { return completion(result) } + completion(self.signTx(tx) == true ? .success : .failure) + } + } + + // the 12 word wallet recovery phrase + func seedPhrase(pin: String) -> String? { + guard authenticate(pin: pin) else { return nil } + + do { + return try keychainItem(key: KeychainKey.mnemonic) + } catch { return nil } + } + + // recover an existing wallet using 12 word wallet recovery phrase + // will fail if a wallet already exists on the keychain + func setSeedPhrase(_ phrase: String) -> Bool { + guard noWallet else { return false } + + do { + guard let nfkdPhrase = CFStringCreateMutableCopy(secureAllocator, 0, phrase as CFString) + else { return false } + CFStringNormalize(nfkdPhrase, .KD) + var seed = UInt512() + try setKeychainItem(key: KeychainKey.mnemonic, item: nfkdPhrase as String?, authenticated: true) + BRBIP39DeriveKey(&seed, nfkdPhrase as String, nil) + masterPubKey = BRBIP32MasterPubKey(&seed, MemoryLayout.size) + seed = UInt512() // clear seed + if earliestKeyTime < BIP39CreationTime { earliestKeyTime = BIP39CreationTime } + try setKeychainItem(key: KeychainKey.masterPubKey, item: Data(masterPubKey: masterPubKey)) + return true + } catch { return false } + } + + // create a new wallet and return the 12 word wallet recovery phrase + // will fail if a wallet already exists on the keychain + func setRandomSeedPhrase() -> String? { + guard noWallet else { return nil } + guard var words = rawWordList else { return nil } + let time = Date.timeIntervalSinceReferenceDate + + // we store the wallet creation time on the keychain because keychain data persists even when app is deleted + do { + try setKeychainItem(key: KeychainKey.creationTime, + item: [time].withUnsafeBufferPointer { Data(buffer: $0) }) + earliestKeyTime = time + } catch { return nil } + + // wrapping in an autorelease pool ensures sensitive memory is wiped and released immediately + return autoreleasepool { + var entropy = UInt128() + let entropyRef = UnsafeMutableRawPointer(mutating: &entropy).assumingMemoryBound(to: UInt8.self) + guard SecRandomCopyBytes(kSecRandomDefault, MemoryLayout.size, entropyRef) == 0 + else { return nil } + let phraseLen = BRBIP39Encode(nil, 0, &words, entropyRef, MemoryLayout.size) + var phraseData = CFDataCreateMutable(secureAllocator, phraseLen) as Data + phraseData.count = phraseLen + guard phraseData.withUnsafeMutableBytes({ + BRBIP39Encode($0, phraseLen, &words, entropyRef, MemoryLayout.size) + }) == phraseData.count else { return nil } + entropy = UInt128() + let phrase = CFStringCreateFromExternalRepresentation(secureAllocator, phraseData as CFData, + CFStringBuiltInEncodings.UTF8.rawValue) as String + guard setSeedPhrase(phrase) else { return nil } + return phrase + } + } + + // change wallet authentication pin + func changePin(newPin: String, pin: String) -> Bool { + guard authenticate(pin: pin) else { return false } + do { + store.perform(action: PinLength.set(newPin.utf8.count)) + try setKeychainItem(key: KeychainKey.pin, item: newPin) + return true + } catch { return false } + } + + // change wallet authentication pin using the wallet recovery phrase + // recovery phrase is optional if no pin is currently set + func forceSetPin(newPin: String, seedPhrase: String? = nil) -> Bool { + do { + if let phrase = seedPhrase { + var seed = UInt512() + guard let nfkdPhrase = CFStringCreateMutableCopy(secureAllocator, 0, phrase as CFString) + else { return false } + CFStringNormalize(nfkdPhrase, .KD) + BRBIP39DeriveKey(&seed, nfkdPhrase as String, nil) + let mpk = BRBIP32MasterPubKey(&seed, MemoryLayout.size) + seed = UInt512() // clear seed + let mpkData: Data? = try keychainItem(key: KeychainKey.masterPubKey) + guard mpkData?.masterPubKey == mpk else { return false } + } else if try keychainItem(key: KeychainKey.pin) as String? != nil { + return authenticate(pin: newPin) + } + store.perform(action: PinLength.set(newPin.utf8.count)) + try setKeychainItem(key: KeychainKey.pin, item: newPin) + try authenticationSuccess() + return true + } catch { return false } + } + + // wipe the existing wallet from the keychain + func wipeWallet(pin: String = "forceWipe") -> Bool { + guard pin == "forceWipe" || authenticate(pin: pin) else { return false } + + do { + lazyWallet = nil + lazyPeerManager = nil + if db != nil { sqlite3_close(db) } + db = nil + masterPubKey = BRMasterPubKey() + didInitWallet = false + earliestKeyTime = 0 + if let bundleId = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: bundleId) + } + try BRAPIClient(authenticator: self).kv?.rmdb() + try? FileManager.default.removeItem(atPath: dbPath) + try? FileManager.default.removeItem(at: BRReplicatedKVStore.dbPath) + try setKeychainItem(key: KeychainKey.apiAuthKey, item: nil as Data?) + try setKeychainItem(key: KeychainKey.spendLimit, item: nil as Int64?) + try setKeychainItem(key: KeychainKey.creationTime, item: nil as Data?) + try setKeychainItem(key: KeychainKey.pinFailTime, item: nil as Int64?) + try setKeychainItem(key: KeychainKey.pinFailCount, item: nil as Int64?) + try setKeychainItem(key: KeychainKey.pin, item: nil as String?) + try setKeychainItem(key: KeychainKey.masterPubKey, item: nil as Data?) + try setKeychainItem(key: KeychainKey.seed, item: nil as Data?) + try setKeychainItem(key: KeychainKey.mnemonic, item: nil as String?, authenticated: true) + NotificationCenter.default.post(name: .walletDidWipeNotification, object: nil) + return true + } catch { + print("Wipe wallet error: \(error)") + return false + } + } + + func deleteWalletDatabase(pin: String = "forceWipe") -> Bool { + guard pin == "forceWipe" || authenticate(pin: pin) else { return false } + + do { + lazyWallet = nil + lazyPeerManager = nil + if db != nil { sqlite3_close(db) } + db = nil + didInitWallet = false + earliestKeyTime = 0 + if let bundleId = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: bundleId) + } + try BRAPIClient(authenticator: self).kv?.rmdb() + try? FileManager.default.removeItem(atPath: dbPath) + try? FileManager.default.removeItem(at: BRReplicatedKVStore.dbPath) + NotificationCenter.default.post(name: .didDeleteWalletDBNotification, object: nil) + return true + } catch { + print("Wipe wallet error: \(error)") + return false + } + } + + // key used for authenticated API calls + var apiAuthKey: String? { + return autoreleasepool { + do { + if let apiKey: String? = try? keychainItem(key: KeychainKey.apiAuthKey) { + if apiKey != nil { + return apiKey + } + } + var key = BRKey() + var seed = UInt512() + guard let phrase: String = try keychainItem(key: KeychainKey.mnemonic) else { return nil } + BRBIP39DeriveKey(&seed, phrase, nil) + BRBIP32APIAuthKey(&key, &seed, MemoryLayout.size) + seed = UInt512() // clear seed + let pkLen = BRKeyPrivKey(&key, nil, 0) + var pkData = CFDataCreateMutable(secureAllocator, pkLen) as Data + pkData.count = pkLen + guard pkData.withUnsafeMutableBytes({ BRKeyPrivKey(&key, $0, pkLen) }) == pkLen else { return nil } + let privKey = CFStringCreateFromExternalRepresentation(secureAllocator, pkData as CFData, + CFStringBuiltInEncodings.UTF8.rawValue) as String + try setKeychainItem(key: KeychainKey.apiAuthKey, item: privKey) + return privKey + } catch { + print("apiAuthKey error: \(error)") + return nil + } + } + } + + // sensitive user information stored on the keychain + var userAccount: [AnyHashable: Any]? { + get { + do { + return try keychainItem(key: KeychainKey.userAccount) + } catch { return nil } + } + + set(value) { + do { + try setKeychainItem(key: KeychainKey.userAccount, item: value) + } catch {} + } + } + + private struct KeychainKey { + public static let mnemonic = "mnemonic" + public static let creationTime = "creationtime" + public static let masterPubKey = "masterpubkey" + public static let spendLimit = "spendlimit" + public static let pin = "pin" + public static let pinFailCount = "pinfailcount" + public static let pinFailTime = "pinfailheight" + public static let apiAuthKey = "authprivkey" + public static let userAccount = "https://api.breadwallet.com" + public static let seed = "seed" // deprecated + } + + private struct DefaultsKey { + public static let spendLimitAmount = "SPEND_LIMIT_AMOUNT" + public static let pinUnlockTime = "PIN_UNLOCK_TIME" + } + + private func signTx(_ tx: BRTxRef, forkId: Int = 0) -> Bool { + return autoreleasepool { + do { + var seed = UInt512() + defer { seed = UInt512() } + guard let wallet = wallet + else { + LWAnalytics.logEventWithParameters(itemName: ._20200111_WNI) + return false + } + guard let phrase: String = try keychainItem(key: KeychainKey.mnemonic) + else { + LWAnalytics.logEventWithParameters(itemName: ._20200111_PNI) + return false + } + + BRBIP39DeriveKey(&seed, phrase, nil) + return wallet.signTransaction(tx, forkId: forkId, seed: &seed) + } catch { + LWAnalytics.logEventWithParameters(itemName: ._20200111_UTST) + return false + } + } + } +} + +private func keychainItem(key: String) throws -> T? { + let query = [kSecClass as String: kSecClassGenericPassword as String, + kSecAttrService as String: WalletSecAttrService, + kSecAttrAccount as String: key, + kSecReturnData as String: true as Any] + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + guard status == noErr || status == errSecItemNotFound + else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) + } + guard let data = result as? Data else { return nil } + + switch T.self { + case is Data.Type: + return data as? T + case is String.Type: + return CFStringCreateFromExternalRepresentation(secureAllocator, data as CFData, + CFStringBuiltInEncodings.UTF8.rawValue) as? T + case is Int64.Type: + guard data.count == MemoryLayout.stride else { return nil } + return data.withUnsafeBytes { $0.pointee } + case is [AnyHashable: Any].Type: + return NSKeyedUnarchiver.unarchiveObject(with: data) as? T + default: + throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecParam)) + } +} + +private func setKeychainItem(key: String, item: T?, authenticated: Bool = false) throws { + let accessible = authenticated ? kSecAttrAccessibleWhenUnlockedThisDeviceOnly as String + : kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + let query = [kSecClass as String: kSecClassGenericPassword as String, + kSecAttrService as String: WalletSecAttrService, + kSecAttrAccount as String: key] + var status = noErr + var data: Data? + if let item = item { + switch T.self { + case is Data.Type: + data = item as? Data + case is String.Type: + data = CFStringCreateExternalRepresentation(secureAllocator, item as! CFString, + CFStringBuiltInEncodings.UTF8.rawValue, 0) as Data + case is Int64.Type: + data = CFDataCreateMutable(secureAllocator, MemoryLayout.stride) as Data + [item].withUnsafeBufferPointer { data?.append($0) } + case is [AnyHashable: Any].Type: + data = NSKeyedArchiver.archivedData(withRootObject: item) + default: + throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecParam)) + } + } + + if data == nil { // delete item + if SecItemCopyMatching(query as CFDictionary, nil) != errSecItemNotFound { + status = SecItemDelete(query as CFDictionary) + } + } else if SecItemCopyMatching(query as CFDictionary, nil) != errSecItemNotFound + { // update existing item + let update = [kSecAttrAccessible as String: accessible, + kSecValueData as String: data as Any] + status = SecItemUpdate(query as CFDictionary, update as CFDictionary) + } else { // add new item + let item = [kSecClass as String: kSecClassGenericPassword as String, + kSecAttrService as String: WalletSecAttrService, + kSecAttrAccount as String: key, + kSecAttrAccessible as String: accessible, + kSecValueData as String: data as Any] + status = SecItemAdd(item as CFDictionary, nil) + } + + guard status == noErr + else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) + } +} diff --git a/litewallet/WalletManager.swift b/litewallet/WalletManager.swift new file mode 100644 index 000000000..71250d313 --- /dev/null +++ b/litewallet/WalletManager.swift @@ -0,0 +1,652 @@ +import BRCore +import Foundation +// import sqlite3 +import SQLite3 +import SystemConfiguration + +internal let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self) +internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +enum WalletManagerError: Error { + case sqliteError(errorCode: Int32, description: String) +} + +private func SafeSqlite3ColumnBlob(statement: OpaquePointer, iCol: Int32) -> UnsafePointer? { + guard let result = sqlite3_column_blob(statement, iCol) else { return nil } + return result.assumingMemoryBound(to: T.self) +} + +// A WalletManger instance manages a single wallet, and that wallet's individual connection to the litecoin network. +// After instantiating a WalletManager object, call myWalletManager.peerManager.connect() to begin syncing. + +class WalletManager: BRWalletListener, BRPeerManagerListener { + internal var didInitWallet = false + internal let dbPath: String + internal var db: OpaquePointer? + private var txEnt: Int32 = 0 + private var blockEnt: Int32 = 0 + private var peerEnt: Int32 = 0 + internal let store: Store + var masterPubKey = BRMasterPubKey() + var earliestKeyTime: TimeInterval = 0 + + var userPreferredfpRate: Double = FalsePositiveRates.semiPrivate.rawValue + + static let sharedInstance: WalletManager = { + var instance: WalletManager? + do { + instance = try WalletManager(store: Store(), dbPath: nil) + } catch { + NSLog("ERROR: Instance of WalletManager not initialized") + } + return instance! + }() + + var wallet: BRWallet? { + guard masterPubKey != BRMasterPubKey() else { return nil } + guard let wallet = lazyWallet + else { + // stored transactions don't match masterPubKey + #if !Debug + do { try FileManager.default.removeItem(atPath: dbPath) } catch {} + #endif + return nil + } + + didInitWallet = true + return wallet + } + + var apiClient: BRAPIClient? { + guard masterPubKey != BRMasterPubKey() else { return nil } + return lazyAPIClient + } + + var peerManager: BRPeerManager? { + guard wallet != nil else { return nil } + return lazyPeerManager + } + + // TODO: Pass the fpRate from User Preferences + internal lazy var lazyPeerManager: BRPeerManager? = { + if let wallet = self.wallet { + return BRPeerManager(wallet: wallet, earliestKeyTime: self.earliestKeyTime, blocks: self.loadBlocks(), peers: self.loadPeers(), listener: self, fpRate: FalsePositiveRates.semiPrivate.rawValue) + } else { + return nil + } + }() + + internal lazy var lazyWallet: BRWallet? = BRWallet(transactions: self.loadTransactions(), + masterPubKey: self.masterPubKey, + listener: self) + + private lazy var lazyAPIClient: BRAPIClient? = { + guard let wallet = self.wallet else { return nil } + return BRAPIClient(authenticator: self) + }() + + var wordList: [NSString]? { + guard let path = Bundle.main.path(forResource: "BIP39Words", ofType: "plist") else { return nil } + return NSArray(contentsOfFile: path) as? [NSString] + } + + lazy var allWordsLists: [[NSString]] = { + var array: [[NSString]] = [] + Bundle.main.localizations.forEach { lang in + if let path = Bundle.main.path(forResource: "BIP39Words", ofType: "plist", inDirectory: nil, forLocalization: lang) + { + if let words = NSArray(contentsOfFile: path) as? [NSString] { + array.append(words) + } + } + } + return array + }() + + lazy var allWords: Set = { + var set: Set = Set() + + Bundle.main.localizations.forEach { lang in + if let path = Bundle.main.path(forResource: "BIP39Words", ofType: "plist", inDirectory: nil, forLocalization: lang) + { + if let words = NSArray(contentsOfFile: path) as? [NSString] { + set.formUnion(words.map { $0 as String }) + } + } + } + return set + }() + + var rawWordList: [UnsafePointer?]? { + guard let wordList = wordList, wordList.count == 2048 else { return nil } + return wordList.map { $0.utf8String } + } + + init(masterPubKey: BRMasterPubKey, + earliestKeyTime: TimeInterval, + dbPath: String? = nil, + store: Store, + fpRate: Double) throws + { + self.masterPubKey = masterPubKey + self.earliestKeyTime = earliestKeyTime + self.dbPath = try dbPath ?? + FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, + create: false).appendingPathComponent("BreadWallet.sqlite").path + self.store = store + userPreferredfpRate = fpRate + + // open sqlite database + if sqlite3_open_v2(self.dbPath, &db, + SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil) != SQLITE_OK + { + print(String(cString: sqlite3_errmsg(db))) + let properties: [String: String] = ["ERROR_MESSAGE": String(cString: sqlite3_errmsg(db)), "ERROR_CODE": String(describing: sqlite3_errcode(db))] + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR, properties: properties) + + #if DEBUG + throw WalletManagerError.sqliteError(errorCode: sqlite3_errcode(db), + description: String(cString: sqlite3_errmsg(db))) + #else + try FileManager.default.removeItem(atPath: self.dbPath) + + if sqlite3_open_v2(self.dbPath, &db, + SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil) != SQLITE_OK + { + throw WalletManagerError.sqliteError(errorCode: sqlite3_errcode(db), + description: String(cString: sqlite3_errmsg(db))) + } + #endif + } + + // create tables and indexes (these are inherited from CoreData) + + // tx table + sqlite3_exec(db, "create table if not exists ZBRTXMETADATAENTITY (" + + "Z_PK integer primary key," + + "Z_ENT integer," + + "Z_OPT integer," + + "ZTYPE integer," + + "ZBLOB blob," + + "ZTXHASH blob)", nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRTXMETADATAENTITY_ZTXHASH_INDEX " + + "on ZBRTXMETADATAENTITY (ZTXHASH)", nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRTXMETADATAENTITY_ZTYPE_INDEX " + + "on ZBRTXMETADATAENTITY (ZTYPE)", nil, nil, nil) + if sqlite3_errcode(db) != SQLITE_OK { print(String(cString: sqlite3_errmsg(db))) } + + // blocks table + sqlite3_exec(db, "create table if not exists ZBRMERKLEBLOCKENTITY (" + + "Z_PK integer primary key," + + "Z_ENT integer," + + "Z_OPT integer," + + "ZHEIGHT integer," + + "ZNONCE integer," + + "ZTARGET integer," + + "ZTOTALTRANSACTIONS integer," + + "ZVERSION integer," + + "ZTIMESTAMP timestamp," + + "ZBLOCKHASH blob," + + "ZFLAGS blob," + + "ZHASHES blob," + + "ZMERKLEROOT blob," + + "ZPREVBLOCK blob)", nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRMERKLEBLOCKENTITY_ZBLOCKHASH_INDEX " + + "on ZBRMERKLEBLOCKENTITY (ZBLOCKHASH)", nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRMERKLEBLOCKENTITY_ZHEIGHT_INDEX " + + "on ZBRMERKLEBLOCKENTITY (ZHEIGHT)", nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRMERKLEBLOCKENTITY_ZPREVBLOCK_INDEX " + + "on ZBRMERKLEBLOCKENTITY (ZPREVBLOCK)", nil, nil, nil) + if sqlite3_errcode(db) != SQLITE_OK { print(String(cString: sqlite3_errmsg(db))) } + + // peers table + sqlite3_exec(db, "create table if not exists ZBRPEERENTITY (" + + "Z_PK integer PRIMARY KEY," + + "Z_ENT integer," + + "Z_OPT integer," + + "ZADDRESS integer," + + "ZMISBEHAVIN integer," + + "ZPORT integer," + + "ZSERVICES integer," + + "ZTIMESTAMP timestamp)", nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRPEERENTITY_ZADDRESS_INDEX on ZBRPEERENTITY (ZADDRESS)", + nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRPEERENTITY_ZMISBEHAVIN_INDEX on ZBRPEERENTITY (ZMISBEHAVIN)", + nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRPEERENTITY_ZPORT_INDEX on ZBRPEERENTITY (ZPORT)", + nil, nil, nil) + sqlite3_exec(db, "create index if not exists ZBRPEERENTITY_ZTIMESTAMP_INDEX on ZBRPEERENTITY (ZTIMESTAMP)", + nil, nil, nil) + if sqlite3_errcode(db) != SQLITE_OK { print(String(cString: sqlite3_errmsg(db))) } + + // primary keys + sqlite3_exec(db, "create table if not exists Z_PRIMARYKEY (" + + "Z_ENT INTEGER PRIMARY KEY," + + "Z_NAME VARCHAR," + + "Z_SUPER INTEGER," + + "Z_MAX INTEGER)", nil, nil, nil) + sqlite3_exec(db, "insert into Z_PRIMARYKEY (Z_ENT, Z_NAME, Z_SUPER, Z_MAX) " + + "select 6, 'BRTxMetadataEntity', 0, 0 except " + + "select 6, Z_NAME, 0, 0 from Z_PRIMARYKEY where Z_NAME = 'BRTxMetadataEntity'", nil, nil, nil) + sqlite3_exec(db, "insert into Z_PRIMARYKEY (Z_ENT, Z_NAME, Z_SUPER, Z_MAX) " + + "select 2, 'BRMerkleBlockEntity', 0, 0 except " + + "select 2, Z_NAME, 0, 0 from Z_PRIMARYKEY where Z_NAME = 'BRMerkleBlockEntity'", nil, nil, nil) + sqlite3_exec(db, "insert into Z_PRIMARYKEY (Z_ENT, Z_NAME, Z_SUPER, Z_MAX) " + + "select 3, 'BRPeerEntity', 0, 0 except " + + "select 3, Z_NAME, 0, 0 from Z_PRIMARYKEY where Z_NAME = 'BRPeerEntity'", nil, nil, nil) + if sqlite3_errcode(db) != SQLITE_OK { print(String(cString: sqlite3_errmsg(db))) } + + var sql: OpaquePointer? + sqlite3_prepare_v2(db, "select Z_ENT, Z_NAME from Z_PRIMARYKEY", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + while sqlite3_step(sql) == SQLITE_ROW { + let name = String(cString: sqlite3_column_text(sql, 1)) + if name == "BRTxMetadataEntity" { txEnt = sqlite3_column_int(sql, 0) } + else if name == "BRMerkleBlockEntity" { blockEnt = sqlite3_column_int(sql, 0) } + else if name == "BRPeerEntity" { peerEnt = sqlite3_column_int(sql, 0) } + } + + if sqlite3_errcode(db) != SQLITE_DONE { print(String(cString: sqlite3_errmsg(db))) } + } + + func balanceChanged(_ balance: UInt64) { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .walletBalanceChangedNotification, object: nil, + userInfo: ["balance": balance]) + } + } + + func txAdded(_ tx: BRTxRef) { + DispatchQueue.walletQueue.async { + var buf = [UInt8](repeating: 0, count: BRTransactionSerialize(tx, nil, 0)) + let timestamp = (tx.pointee.timestamp > UInt32(NSTimeIntervalSince1970)) ? tx.pointee.timestamp - UInt32(NSTimeIntervalSince1970) : 0 + guard BRTransactionSerialize(tx, &buf, buf.count) == buf.count else { return } + [tx.pointee.blockHeight.littleEndian, timestamp.littleEndian].withUnsafeBytes { buf.append(contentsOf: $0) } + sqlite3_exec(self.db, "begin exclusive", nil, nil, nil) + + var sql: OpaquePointer? + sqlite3_prepare_v2(self.db, "select Z_MAX from Z_PRIMARYKEY where Z_ENT = \(self.txEnt)", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + guard sqlite3_step(sql) == SQLITE_ROW + else { + print(String(cString: sqlite3_errmsg(self.db))) + sqlite3_exec(self.db, "rollback", nil, nil, nil) + return + } + + let pk = sqlite3_column_int(sql, 0) + var sql2: OpaquePointer? + sqlite3_prepare_v2(self.db, "insert or rollback into ZBRTXMETADATAENTITY " + + "(Z_PK, Z_ENT, Z_OPT, ZTYPE, ZBLOB, ZTXHASH) " + + "values (\(pk + 1), \(self.txEnt), 1, 1, ?, ?)", -1, &sql2, nil) + defer { sqlite3_finalize(sql2) } + sqlite3_bind_blob(sql2, 1, buf, Int32(buf.count), SQLITE_TRANSIENT) + sqlite3_bind_blob(sql2, 2, [tx.pointee.txHash], Int32(MemoryLayout.size), SQLITE_TRANSIENT) + + guard sqlite3_step(sql2) == SQLITE_DONE + else { + print(String(cString: sqlite3_errmsg(self.db))) + return + } + + sqlite3_exec(self.db, "update or rollback Z_PRIMARYKEY set Z_MAX = \(pk + 1) " + + "where Z_ENT = \(self.txEnt) and Z_MAX = \(pk)", nil, nil, nil) + + guard sqlite3_errcode(self.db) == SQLITE_OK + else { + print(String(cString: sqlite3_errmsg(self.db))) + let properties: [String: String] = ["ERROR_MESSAGE": String(cString: sqlite3_errmsg(self.db)), "ERROR_CODE": String(describing: sqlite3_errcode(self.db))] + LWAnalytics.logEventWithParameters(itemName: ._20200112_ERR, properties: properties) + return + } + + sqlite3_exec(self.db, "commit", nil, nil, nil) + } + } + + func txUpdated(_ txHashes: [UInt256], blockHeight: UInt32, timestamp: UInt32) { + DispatchQueue.walletQueue.async { + guard !txHashes.isEmpty else { return } + let timestamp = (timestamp > UInt32(NSTimeIntervalSince1970)) ? timestamp - UInt32(NSTimeIntervalSince1970) : 0 + var sql: OpaquePointer?, sql2: OpaquePointer? + sqlite3_prepare_v2(self.db, "select ZTXHASH, ZBLOB from ZBRTXMETADATAENTITY where ZTYPE = 1 and " + + "ZTXHASH in (" + String(repeating: "?, ", count: txHashes.count - 1) + "?)", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + for i in 0 ..< txHashes.count { + sqlite3_bind_blob(sql, Int32(i + 1), UnsafePointer(txHashes) + i, Int32(MemoryLayout.size), + SQLITE_TRANSIENT) + } + + sqlite3_prepare_v2(self.db, "update ZBRTXMETADATAENTITY set ZBLOB = ? where ZTXHASH = ?", -1, &sql2, nil) + defer { sqlite3_finalize(sql2) } + + while sqlite3_step(sql) == SQLITE_ROW { + let hash = sqlite3_column_blob(sql, 0) + let buf = sqlite3_column_blob(sql, 1).assumingMemoryBound(to: UInt8.self) + var blob = [UInt8](UnsafeBufferPointer(start: buf, count: Int(sqlite3_column_bytes(sql, 1)))) + + [blockHeight.littleEndian, timestamp.littleEndian].withUnsafeBytes { + if blob.count > $0.count { + blob.replaceSubrange(blob.count - $0.count ..< blob.count, with: $0) + sqlite3_bind_blob(sql2, 1, blob, Int32(blob.count), SQLITE_TRANSIENT) + sqlite3_bind_blob(sql2, 2, hash, Int32(MemoryLayout.size), SQLITE_TRANSIENT) + sqlite3_step(sql2) + sqlite3_reset(sql2) + } + } + } + + if sqlite3_errcode(self.db) != SQLITE_DONE { print(String(cString: sqlite3_errmsg(self.db))) } + } + } + + func txDeleted(_ txHash: UInt256, notifyUser: Bool, recommendRescan: Bool) { + DispatchQueue.walletQueue.async { + var sql: OpaquePointer? + sqlite3_prepare_v2(self.db, "delete from ZBRTXMETADATAENTITY where ZTYPE = 1 and ZTXHASH = ?", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + sqlite3_bind_blob(sql, 1, [txHash], Int32(MemoryLayout.size), SQLITE_TRANSIENT) + + guard sqlite3_step(sql) == SQLITE_DONE + else { + print(String(cString: sqlite3_errmsg(self.db))) + return + } + + if notifyUser { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .walletTxRejectedNotification, object: nil, + userInfo: ["txHash": txHash, "recommendRescan": recommendRescan]) + } + } + } + } + + func syncStarted() { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .walletSyncStartedNotification, object: nil) + } + } + + func syncStopped(_ error: BRPeerManagerError?) { + switch error { + case .some(let .posixError(errorCode, description)): + DispatchQueue.main.async { + NotificationCenter.default.post(name: .walletSyncStoppedNotification, object: nil, + userInfo: ["errorCode": errorCode, + "errorDescription": description]) + } + case .none: + DispatchQueue.main.async { + NotificationCenter.default.post(name: .walletSyncStoppedNotification, object: nil) + } + } + } + + func txStatusUpdate() { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .walletTxStatusUpdateNotification, object: nil) + } + } + + func saveBlocks(_ replace: Bool, _ blocks: [BRBlockRef?]) { + DispatchQueue.walletQueue.async { + var pk: Int32 = 0 + sqlite3_exec(self.db, "begin exclusive", nil, nil, nil) + + if replace { // delete existing blocks and replace + sqlite3_exec(self.db, "delete from ZBRMERKLEBLOCKENTITY", nil, nil, nil) + } else { // add to existing blocks + var sql: OpaquePointer? + sqlite3_prepare_v2(self.db, "select Z_MAX from Z_PRIMARYKEY where Z_ENT = \(self.blockEnt)", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + guard sqlite3_step(sql) == SQLITE_ROW + else { + print(String(cString: sqlite3_errmsg(self.db))) + + sqlite3_exec(self.db, "rollback", nil, nil, nil) + return + } + + pk = sqlite3_column_int(sql, 0) // get last primary key + } + + var sql2: OpaquePointer? + sqlite3_prepare_v2(self.db, "insert or rollback into ZBRMERKLEBLOCKENTITY (Z_PK, Z_ENT, Z_OPT, ZHEIGHT, " + + "ZNONCE, ZTARGET, ZTOTALTRANSACTIONS, ZVERSION, ZTIMESTAMP, ZBLOCKHASH, ZFLAGS, ZHASHES, " + + "ZMERKLEROOT, ZPREVBLOCK) values (?, \(self.blockEnt), 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", -1, &sql2, nil) + defer { sqlite3_finalize(sql2) } + + for b in blocks { + guard let b = b + else { + sqlite3_exec(self.db, "rollback", nil, nil, nil) + return + } + + let timestampResult = Int32(bitPattern: b.pointee.timestamp).subtractingReportingOverflow(Int32(NSTimeIntervalSince1970)) + guard !timestampResult.1 else { print("skipped block with overflowed timestamp"); continue } + + pk = pk + 1 + sqlite3_bind_int(sql2, 1, pk) + sqlite3_bind_int(sql2, 2, Int32(bitPattern: b.pointee.height)) + sqlite3_bind_int(sql2, 3, Int32(bitPattern: b.pointee.nonce)) + sqlite3_bind_int(sql2, 4, Int32(bitPattern: b.pointee.target)) + sqlite3_bind_int(sql2, 5, Int32(bitPattern: b.pointee.totalTx)) + sqlite3_bind_int(sql2, 6, Int32(bitPattern: b.pointee.version)) + sqlite3_bind_int(sql2, 7, timestampResult.0) + sqlite3_bind_blob(sql2, 8, [b.pointee.blockHash], Int32(MemoryLayout.size), SQLITE_TRANSIENT) + sqlite3_bind_blob(sql2, 9, [b.pointee.flags], Int32(b.pointee.flagsLen), SQLITE_TRANSIENT) + sqlite3_bind_blob(sql2, 10, [b.pointee.hashes], Int32(MemoryLayout.size * b.pointee.hashesCount), + SQLITE_TRANSIENT) + sqlite3_bind_blob(sql2, 11, [b.pointee.merkleRoot], Int32(MemoryLayout.size), SQLITE_TRANSIENT) + sqlite3_bind_blob(sql2, 12, [b.pointee.prevBlock], Int32(MemoryLayout.size), SQLITE_TRANSIENT) + + guard sqlite3_step(sql2) == SQLITE_DONE + else { + print(String(cString: sqlite3_errmsg(self.db))) + return + } + + sqlite3_reset(sql2) + } + + sqlite3_exec(self.db, "update or rollback Z_PRIMARYKEY set Z_MAX = \(pk) where Z_ENT = \(self.blockEnt)", + nil, nil, nil) + + guard sqlite3_errcode(self.db) == SQLITE_OK + else { + print(String(cString: sqlite3_errmsg(self.db))) + return + } + + sqlite3_exec(self.db, "commit", nil, nil, nil) + } + } + + func savePeers(_ replace: Bool, _ peers: [BRPeer]) { + DispatchQueue.walletQueue.async { + var pk: Int32 = 0 + sqlite3_exec(self.db, "begin exclusive", nil, nil, nil) + + if replace { // delete existing peers and replace + sqlite3_exec(self.db, "delete from ZBRPEERENTITY", nil, nil, nil) + } else { // add to existing peers + var sql: OpaquePointer? + sqlite3_prepare_v2(self.db, "select Z_MAX from Z_PRIMARYKEY where Z_ENT = \(self.peerEnt)", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + guard sqlite3_step(sql) == SQLITE_ROW + else { + print(String(cString: sqlite3_errmsg(self.db))) + sqlite3_exec(self.db, "rollback", nil, nil, nil) + return + } + + pk = sqlite3_column_int(sql, 0) // get last primary key + } + + var sql2: OpaquePointer? + sqlite3_prepare_v2(self.db, "insert or rollback into ZBRPEERENTITY " + + "(Z_PK, Z_ENT, Z_OPT, ZADDRESS, ZMISBEHAVIN, ZPORT, ZSERVICES, ZTIMESTAMP) " + + "values (?, \(self.peerEnt), 1, ?, 0, ?, ?, ?)", -1, &sql2, nil) + defer { sqlite3_finalize(sql2) } + + for p in peers { + pk = pk + 1 + sqlite3_bind_int(sql2, 1, pk) + sqlite3_bind_int(sql2, 2, Int32(bitPattern: p.address.u32.3.bigEndian)) + sqlite3_bind_int(sql2, 3, Int32(p.port)) + sqlite3_bind_int64(sql2, 4, Int64(bitPattern: p.services)) + sqlite3_bind_int64(sql2, 5, Int64(bitPattern: p.timestamp) - Int64(NSTimeIntervalSince1970)) + + guard sqlite3_step(sql2) == SQLITE_DONE + else { + print(String(cString: sqlite3_errmsg(self.db))) + return + } + + sqlite3_reset(sql2) + } + + sqlite3_exec(self.db, "update or rollback Z_PRIMARYKEY set Z_MAX = \(pk) where Z_ENT = \(self.peerEnt)", + nil, nil, nil) + + guard sqlite3_errcode(self.db) == SQLITE_OK + else { + print(String(cString: sqlite3_errmsg(self.db))) + return + } + + sqlite3_exec(self.db, "commit", nil, nil, nil) + } + } + + func networkIsReachable() -> Bool { + var flags: SCNetworkReachabilityFlags = [] + var zeroAddress = sockaddr() + zeroAddress.sa_len = UInt8(MemoryLayout.size) + zeroAddress.sa_family = sa_family_t(AF_INET) + guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else { return false } + if !SCNetworkReachabilityGetFlags(reachability, &flags) { return false } + return flags.contains(.reachable) && !flags.contains(.connectionRequired) + } + + private func loadTransactions() -> [BRTxRef?] { + DispatchQueue.main.async { self.store.perform(action: LoadTransactions.set(true)) } + var transactions = [BRTxRef?]() + var sql: OpaquePointer? + sqlite3_prepare_v2(db, "select ZBLOB from ZBRTXMETADATAENTITY where ZTYPE = 1", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + while sqlite3_step(sql) == SQLITE_ROW { + let len = Int(sqlite3_column_bytes(sql, 0)) + let buf = sqlite3_column_blob(sql, 0).assumingMemoryBound(to: UInt8.self) + guard len >= MemoryLayout.size * 2 else { return transactions } + var off = len - MemoryLayout.size * 2 + guard let tx = BRTransactionParse(buf, off) else { return transactions } + tx.pointee.blockHeight = + UnsafeRawPointer(buf).advanced(by: off).assumingMemoryBound(to: UInt32.self).pointee.littleEndian + off = off + MemoryLayout.size + let timestamp = UnsafeRawPointer(buf).advanced(by: off).assumingMemoryBound(to: UInt32.self).pointee.littleEndian + tx.pointee.timestamp = (timestamp == 0) ? timestamp : timestamp + UInt32(NSTimeIntervalSince1970) + transactions.append(tx) + } + + if sqlite3_errcode(db) != SQLITE_DONE { print(String(cString: sqlite3_errmsg(db))) } + DispatchQueue.main.async { self.store.perform(action: LoadTransactions.set(false)) } + return transactions + } + + private func loadBlocks() -> [BRBlockRef?] { + var blocks = [BRBlockRef?]() + var sql: OpaquePointer? + sqlite3_prepare_v2(db, "select ZHEIGHT, ZNONCE, ZTARGET, ZTOTALTRANSACTIONS, ZVERSION, ZTIMESTAMP, " + + "ZBLOCKHASH, ZFLAGS, ZHASHES, ZMERKLEROOT, ZPREVBLOCK from ZBRMERKLEBLOCKENTITY", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + while sqlite3_step(sql) == SQLITE_ROW { + guard let b = BRMerkleBlockNew() else { return blocks } + let maxTime: UInt32 = 0xC5B0_3780 + b.pointee.height = UInt32(bitPattern: sqlite3_column_int(sql, 0)) + b.pointee.nonce = UInt32(bitPattern: sqlite3_column_int(sql, 1)) + b.pointee.target = UInt32(bitPattern: sqlite3_column_int(sql, 2)) + b.pointee.totalTx = UInt32(bitPattern: sqlite3_column_int(sql, 3)) + b.pointee.version = UInt32(bitPattern: sqlite3_column_int(sql, 4)) + if UInt32(bitPattern: sqlite3_column_int(sql, 5)) >= maxTime { + b.pointee.timestamp = UInt32(NSTimeIntervalSince1970) + } else { + b.pointee.timestamp = UInt32(bitPattern: sqlite3_column_int(sql, 5)) + UInt32(NSTimeIntervalSince1970) + } + b.pointee.blockHash = sqlite3_column_blob(sql, 6).assumingMemoryBound(to: UInt256.self).pointee + + let flags: UnsafePointer? = SafeSqlite3ColumnBlob(statement: sql!, iCol: 7) + let flagsLen = Int(sqlite3_column_bytes(sql, 7)) + let hashes: UnsafePointer? = SafeSqlite3ColumnBlob(statement: sql!, iCol: 8) + let hashesCount = Int(sqlite3_column_bytes(sql, 8)) / MemoryLayout.size + BRMerkleBlockSetTxHashes(b, hashes, hashesCount, flags, flagsLen) + b.pointee.merkleRoot = sqlite3_column_blob(sql, 9).assumingMemoryBound(to: UInt256.self).pointee + b.pointee.prevBlock = sqlite3_column_blob(sql, 10).assumingMemoryBound(to: UInt256.self).pointee + blocks.append(b) + } + + if sqlite3_errcode(db) != SQLITE_DONE { print(String(cString: sqlite3_errmsg(db))) } + return blocks + } + + private func loadPeers() -> [BRPeer] { + var peers = [BRPeer]() + var sql: OpaquePointer? + sqlite3_prepare_v2(db, "select ZADDRESS, ZPORT, ZSERVICES, ZTIMESTAMP from ZBRPEERENTITY", -1, &sql, nil) + defer { sqlite3_finalize(sql) } + + while sqlite3_step(sql) == SQLITE_ROW { + var p = BRPeer() + p.address = UInt128(u32: (0, 0, UInt32(0xFFFF).bigEndian, + UInt32(bitPattern: sqlite3_column_int(sql, 0)).bigEndian)) + p.port = UInt16(truncatingIfNeeded: sqlite3_column_int(sql, 1)) + p.services = UInt64(bitPattern: sqlite3_column_int64(sql, 2)) + + let result = UInt64(bitPattern: sqlite3_column_int64(sql, 3)).addingReportingOverflow(UInt64(NSTimeIntervalSince1970)) + if result.1 { + print("skipped overflowed timestamp: \(sqlite3_column_int64(sql, 3))") + continue + } else { + p.timestamp = result.0 + peers.append(p) + } + } + + if sqlite3_errcode(db) != SQLITE_DONE { print(String(cString: sqlite3_errmsg(db))) } + return peers + } + + func isPhraseValid(_ phrase: String) -> Bool { + for wordList in allWordsLists { + var words = wordList.map { $0.utf8String } + guard let nfkdPhrase = CFStringCreateMutableCopy(secureAllocator, 0, phrase as CFString) else { return false } + CFStringNormalize(nfkdPhrase, .KD) + if BRBIP39PhraseIsValid(&words, nfkdPhrase as String) != 0 { + return true + } + } + return false + } + + func isWordValid(_ word: String) -> Bool { + return allWords.contains(word) + } + + var isWatchOnly: Bool { + let mpkData = Data(masterPubKey: masterPubKey) + return mpkData.isEmpty + } + + deinit { + if db != nil { sqlite3_close(db) } + } +} diff --git a/litewallet/da.lproj/BIP39Words.plist b/litewallet/da.lproj/BIP39Words.plist new file mode 100644 index 000000000..01e69ff4c --- /dev/null +++ b/litewallet/da.lproj/BIP39Words.plist @@ -0,0 +1,2054 @@ + + + + + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + + diff --git a/litewallet/de.lproj/Localizable.strings b/litewallet/de.lproj/Localizable.strings index c4f47abdc..b5ba77126 100755 --- a/litewallet/de.lproj/Localizable.strings +++ b/litewallet/de.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "Guthaben: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Gebühr: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "SENDEN"; @@ -898,8 +901,26 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Geben Sie eine Litecoin-Adresse ein"; -/* Network Fee: $0.01 */ -"Send.fee" = "Netzwerkgebühr: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Gebühren: %1$@"; + +/* Fees: $0.01*/ +"Send.fee" = "Gebühren: %1$@"; + +/* Fees: $0.01*/ +"Send.fee" = "Gebühren: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Gebühren:"; + +/* Fees Blank: */ +"Send.feeBlank" = "Gebühren:"; + +/* Fees Blank: */ +"Send.feeBlank" = "Gebühren:"; + +/* Fees Blank: */ +"Send.feeBlank" = "Gebühren:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "die Identität des Zahlungsempfängers ist nicht bestätigt."; @@ -1080,7 +1101,7 @@ /* Argument is date */ "StartPaperPhrase.date" = "Du hast dir deinen Offlineschlüssel zuletzt am %1$@ notiert"; - + /* Start view tagline */ "StartViewController.tagline" = "Die sicherste Option zur Nutzung von Litecoin."; @@ -1447,14 +1468,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d von %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "Gebühr: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Gebühren: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Gebühren:"; - - - +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/el.lproj/BIP39Words.plist b/litewallet/el.lproj/BIP39Words.plist new file mode 100644 index 000000000..01e69ff4c --- /dev/null +++ b/litewallet/el.lproj/BIP39Words.plist @@ -0,0 +1,2054 @@ + + + + + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + + diff --git a/litewallet/en.lproj/Localizable.strings b/litewallet/en.lproj/Localizable.strings index 592980777..4cc13379c 100644 --- a/litewallet/en.lproj/Localizable.strings +++ b/litewallet/en.lproj/Localizable.strings @@ -877,6 +877,9 @@ /* Balance: $4.00 */ "Send.balance" = "Balance: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Fee: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "SEND"; @@ -901,8 +904,23 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Enter a Litecoin address"; -/* Network Fee: $0.01 */ -"Send.fee" = "Network Fee: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Fees: %1$@"; + +/* Fees: $0.01*/ +"Send.fee" = "Fees: %1$@"; + +/* Fees: $0.01*/ +"Send.fee" = "Fees: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Fees:"; + +/* Fees Blank: */ +"Send.feeBlank" = "Fees:"; + +/* Fees Blank: */ +"Send.feeBlank" = "Fees:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "Payee identity isn't certified."; @@ -1449,12 +1467,3 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d of %2$d"; - -/* Fee: $0.01 */ -"Send.bareFee" = "Fee: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Fees: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Fees:"; diff --git a/litewallet/es.lproj/Localizable.strings b/litewallet/es.lproj/Localizable.strings index 465f926b0..9dc4beea1 100755 --- a/litewallet/es.lproj/Localizable.strings +++ b/litewallet/es.lproj/Localizable.strings @@ -876,6 +876,9 @@ /* Balance: $4.00 */ "Send.balance" = "Saldo: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Tarifa: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "ENVIAR"; @@ -900,8 +903,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Introduzca una dirección de Litecoin"; -/* Network Fee: $0.01 */ -"Send.fee" = "Tarifa de red: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Honorarios: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Honorarios:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "La identidad del beneficiario no está certificada."; @@ -1449,12 +1455,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d de %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "Tarifa: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Honorarios: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Honorarios:"; - +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/fr.lproj/Localizable.strings b/litewallet/fr.lproj/Localizable.strings index 9a95a062a..9ffdcb27d 100755 --- a/litewallet/fr.lproj/Localizable.strings +++ b/litewallet/fr.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "Solde : %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Frais: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "ENVOYER"; @@ -898,8 +901,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Entrez une adresse Litecoin"; -/* Network Fee: $0.01 */ -"Send.fee" = "Frais de réseau : %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Frais: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Frais:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "L'identité du bénéficiaire n'est pas certifiée."; @@ -1084,7 +1090,6 @@ /* Start view tagline */ "StartViewController.tagline" = "La manière la plus sûre et la plus simple d'utiliser Litecoin."; - /* Support the Litecoin Foundation */ "SupportTheFoundation.title" = "Soutenez la Fondation Litecoin"; @@ -1448,11 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d de %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "Frais: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Frais: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Frais:"; +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/hu.lproj/BIP39Words.plist b/litewallet/hu.lproj/BIP39Words.plist new file mode 100644 index 000000000..01e69ff4c --- /dev/null +++ b/litewallet/hu.lproj/BIP39Words.plist @@ -0,0 +1,2054 @@ + + + + + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + + diff --git a/litewallet/id.lproj/Localizable.strings b/litewallet/id.lproj/Localizable.strings index 95d1a995c..26291fa90 100644 --- a/litewallet/id.lproj/Localizable.strings +++ b/litewallet/id.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "Saldo: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Biaya: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "KIRIM"; @@ -898,8 +901,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Masukkan alamat Litecoin"; -/* Network Fee: $0.01 */ -"Send.fee" = "Biaya Jaringan: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Biaya: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Biaya:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "Identitas penerima pembayaran tidak disertifikasi."; @@ -1447,12 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d dari %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "Biaya: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Biaya: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Biaya:"; - +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/it.lproj/Localizable.strings b/litewallet/it.lproj/Localizable.strings index 919275a56..969ec4e6e 100755 --- a/litewallet/it.lproj/Localizable.strings +++ b/litewallet/it.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "Saldo: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Tassa: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "SPEDIRE"; @@ -901,6 +904,9 @@ /* Network Fee: $0.01 */ "Send.fee" = "Spese network: %1$@"; +/* Fees Blank: */ +"Send.feeBlank" = "Commissioni:"; + /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "Identità del beneficiario non certificata."; @@ -1447,11 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d di %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "Tassa: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Commissioni: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Commissioni:"; +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/ja.lproj/Localizable.strings b/litewallet/ja.lproj/Localizable.strings index 141663d73..5ffbb3afd 100755 --- a/litewallet/ja.lproj/Localizable.strings +++ b/litewallet/ja.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "残高: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "手数料: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "送信"; @@ -898,8 +901,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Litecoinのアドレスを入力してください。"; -/* Network Fee: $0.01 */ -"Send.fee" = "ネットワーク手数料: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "料金: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "料金:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "受取人の本人確認が行われていません。"; @@ -1080,7 +1086,7 @@ /* Argument is date */ "StartPaperPhrase.date" = "あなたが最後に紙の鍵を書き留めたのは %1$@です"; - + /* Start view tagline */ "StartViewController.tagline" = "最も安全にリテコインを使う手段。"; @@ -1447,12 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%2$dの%1$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "手数料: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "料金: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "料金:"; - +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/ko.lproj/Localizable.strings b/litewallet/ko.lproj/Localizable.strings index 760a9b90a..f75cd34cd 100755 --- a/litewallet/ko.lproj/Localizable.strings +++ b/litewallet/ko.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "잔액: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "요금: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "보내다"; @@ -898,8 +901,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "라이트 코인 주소를 입력하세요"; -/* Network Fee: $0.01 */ -"Send.fee" = "네트워크 비용: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "수수료: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "수수료:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "인증되지 않은 수취인입니다."; @@ -1447,13 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%2$d의 %1$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "요금: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "수수료: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "수수료:"; - - +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/nb.lproj/BIP39Words.plist b/litewallet/nb.lproj/BIP39Words.plist new file mode 100644 index 000000000..01e69ff4c --- /dev/null +++ b/litewallet/nb.lproj/BIP39Words.plist @@ -0,0 +1,2054 @@ + + + + + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + + diff --git a/litewallet/nl.lproj/BIP39Words.plist b/litewallet/nl.lproj/BIP39Words.plist new file mode 100644 index 000000000..01e69ff4c --- /dev/null +++ b/litewallet/nl.lproj/BIP39Words.plist @@ -0,0 +1,2054 @@ + + + + + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + + diff --git a/litewallet/pl.lproj/BIP39Words.plist b/litewallet/pl.lproj/BIP39Words.plist new file mode 100644 index 000000000..01e69ff4c --- /dev/null +++ b/litewallet/pl.lproj/BIP39Words.plist @@ -0,0 +1,2054 @@ + + + + + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + + diff --git a/litewallet/pt.lproj/Localizable.strings b/litewallet/pt.lproj/Localizable.strings index fd14d60e3..76b6953b5 100755 --- a/litewallet/pt.lproj/Localizable.strings +++ b/litewallet/pt.lproj/Localizable.strings @@ -875,6 +875,9 @@ /* Balance: $4.00 */ "Send.balance" = "Saldo: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Taxa: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "SEND"; @@ -899,8 +902,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Introduzir um endereço Litecoin"; -/* Network Fee: $0.01 */ -"Send.fee" = "Taxa de rede: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Tarifas: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Tarifas:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "A identidade do beneficiário não está certificada."; @@ -1081,9 +1087,9 @@ /* Argument is date */ "StartPaperPhrase.date" = "A última vez que escreveu a sua chave de papel foi em %1$@"; - + /* Start view tagline */ -"StartViewController.tagline" = "A maneira mais segura e fácil de usar Litecoin."; +"StartViewController.tagline" = "A maneira mais segura e fácil de usar Litecoin."; /* Support the Litecoin Foundation */ "SupportTheFoundation.title" = "Apoiar a Fundação Litecoin"; @@ -1448,11 +1454,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d de %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "Taxa: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Tarifas: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Tarifas:"; +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/ru.lproj/Localizable.strings b/litewallet/ru.lproj/Localizable.strings index f47482cee..b57e4445c 100755 --- a/litewallet/ru.lproj/Localizable.strings +++ b/litewallet/ru.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "Баланс: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Платеж: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "ОТПРАВИТЬ"; @@ -898,8 +901,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Введите адрес Litecoin"; -/* Network Fee: $0.01 */ -"Send.fee" = "Комиссия сети: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Сборы: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Сборы:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "Личность получателя платежа не сертифицирована."; @@ -1080,7 +1086,7 @@ /* Argument is date */ "StartPaperPhrase.date" = "Вы выписывали свой бумажный ключ в последний раз %1$@"; - + /* Start view tagline */ "StartViewController.tagline" = "Самый безопасный и простой способ использовать Litecoin"; @@ -1447,14 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d из %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "Платеж: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Сборы: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Сборы:"; - - - +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/sv.lproj/BIP39Words.plist b/litewallet/sv.lproj/BIP39Words.plist new file mode 100644 index 000000000..01e69ff4c --- /dev/null +++ b/litewallet/sv.lproj/BIP39Words.plist @@ -0,0 +1,2054 @@ + + + + + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + + diff --git a/litewallet/tr.lproj/Localizable.strings b/litewallet/tr.lproj/Localizable.strings index 48af23c33..d346bb581 100644 --- a/litewallet/tr.lproj/Localizable.strings +++ b/litewallet/tr.lproj/Localizable.strings @@ -877,6 +877,9 @@ /* Balance: $4.00 */ "Send.balance" = "Bakiye:%1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Ücret: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "GÖNDER"; @@ -901,8 +904,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Bir Litecoin adresi girin"; -/* Network Fee: $0.01 */ -"Send.fee" = "Ağ Ücreti:%1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Ücretler: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Ücretler:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "Alacaklı kimliği onaylı değil."; @@ -1449,14 +1455,3 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d/%2$d"; - -/* Fee: $0.01 */ -"Send.bareFee" = "Ücret: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Ücretler: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Ücretler:"; - - diff --git a/litewallet/uk.lproj/Localizable.strings b/litewallet/uk.lproj/Localizable.strings index 4163bbbc1..25cf423fe 100644 --- a/litewallet/uk.lproj/Localizable.strings +++ b/litewallet/uk.lproj/Localizable.strings @@ -877,6 +877,9 @@ /* Balance: $4.00 */ "Send.balance" = "Баланс: %1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "Комісія: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "НАДІСЛАТИ"; @@ -901,8 +904,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "Введіть адресу Litecoin"; -/* Network Fee: $0.01 */ -"Send.fee" = "Плата за мережу: %1$@"; +/* Fees: $0.01*/ +"Send.fee" = "Збори: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "Збори:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "Особа одержувача не підтверджена."; @@ -1449,14 +1455,3 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d з %2$d"; - -/* Fee: $0.01 */ -"Send.bareFee" = "Комісія: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "Збори: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "Збори:"; - - diff --git a/litewallet/zh-Hans.lproj/Localizable.strings b/litewallet/zh-Hans.lproj/Localizable.strings index 3f7c482c0..06418412f 100755 --- a/litewallet/zh-Hans.lproj/Localizable.strings +++ b/litewallet/zh-Hans.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "余额:%1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "费用: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "发送"; @@ -898,8 +901,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "输入一个Litecoin地址"; -/* Network Fee: $0.01 */ -"Send.fee" = "网络费:%1$@"; +/* Fees: $0.01*/ +"Send.fee" = "费用: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "费用:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "收款人身份未经认证。"; @@ -1447,11 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%1$d / %2$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "费用: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "费用: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "费用:"; +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewallet/zh-Hant.lproj/Localizable.strings b/litewallet/zh-Hant.lproj/Localizable.strings index f701eb776..82e8441a9 100755 --- a/litewallet/zh-Hant.lproj/Localizable.strings +++ b/litewallet/zh-Hant.lproj/Localizable.strings @@ -874,6 +874,9 @@ /* Balance: $4.00 */ "Send.balance" = "餘額:%1$@"; +/* Fee: $0.01 */ +"Send.bareFee" = "費用: %1$@"; + /* Send Bar Item Title */ "Send.barItemTitle" = "發送"; @@ -898,8 +901,11 @@ /* Enter LTC Address */ "Send.enterLTCAddress" = "輸入萊特幣地址"; -/* Network Fee: $0.01 */ -"Send.fee" = "網路費:%1$@"; +/* Fees: $0.01*/ +"Send.fee" = "費用: %1$@"; + +/* Fees Blank: */ +"Send.feeBlank" = "費用:"; /* Payee identity not certified alert title. */ "Send.identityNotCertified" = "受款人身分未經認證。"; @@ -973,12 +979,6 @@ /* UDSystemError */ "Send.UnstoppableDomains.udSystemError" = "系統查找問題。 [錯誤:%2$d]"; -/* UDSystemError */ -"Send.UnstoppableDomains.udSystemError" = "系統查找問題。 [錯誤:%2$d]"; - -/* UDSystemError */ -"Send.UnstoppableDomains.udSystemError" = "系統查找問題。 [錯誤:%2$d]"; - /* Adress already used alert message - first part */ "Send.UsedAddress.firstLine" = "萊特幣位址專供一次性使用。"; @@ -1086,7 +1086,7 @@ /* Argument is date */ "StartPaperPhrase.date" = "您最近一次在 %1$@ 寫下了您的紙本金鑰"; - + /* Start view tagline */ "StartViewController.tagline" = "使用莱特币最安全、最简单的方式。"; @@ -1453,11 +1453,5 @@ /* 1 of 3 */ "WritePaperPhrase.step" = "%2$d 之 %1$d"; -/* Fee: $0.01 */ -"Send.bareFee" = "費用: %1$@"; - -/* Fees: $0.01*/ -"Send.fee" = "費用: %1$@"; - -/* Fees Blank: */ -"Send.feeBlank" = "費用:"; +/* Network Fee: ($1.00) */ +"Confirmation.feeLabel" = ""; diff --git a/litewalletTests/Constants Tests/ConstantsTests.swift b/litewalletTests/Constants Tests/ConstantsTests.swift index 0c2b871d5..8debc2187 100644 --- a/litewalletTests/Constants Tests/ConstantsTests.swift +++ b/litewalletTests/Constants Tests/ConstantsTests.swift @@ -3,6 +3,6 @@ import XCTest class ConstantsTests: XCTestCase { func testLFDonationAddressPage() throws { - XCTAssertTrue(FoundationSupport.dashboard == "https://litecoinfoundation.zendesk.com/") + XCTAssertTrue(FoundationSupport.dashboard == "https://support.litewallet.io/") } } diff --git a/litewalletTests/Legacy BRTests/BRAddressTests.swift b/litewalletTests/Legacy BRTests/BRAddressTests.swift new file mode 100644 index 000000000..ab503c921 --- /dev/null +++ b/litewalletTests/Legacy BRTests/BRAddressTests.swift @@ -0,0 +1,17 @@ +@testable import litewallet +import XCTest + +class BRAddressTests: XCTestCase { + private let walletManager: WalletManager = try! WalletManager(store: Store(), dbPath: nil) + var newAddress: String = "" + + func testNewAddressGeneration() throws { + if let address = walletManager.wallet?.receiveAddress { + newAddress = address + XCTAssertTrue(newAddress == "") + XCTAssertTrue(newAddress.isValidAddress) + } else { + XCTAssertNil(walletManager.wallet?.receiveAddress) + } + } +}