From bb299335adc33e1c41da2714483333f56b18e00f Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 8 Apr 2019 21:10:28 +0200 Subject: [PATCH 1/5] Add activity cell --- .../Cells/ActivityTableViewCell.swift | 49 +++++++++++++++++++ Passepartout-iOS/Global/Theme+Cells.swift | 7 +++ Passepartout-iOS/Global/Theme.swift | 3 ++ Passepartout.xcodeproj/project.pbxproj | 4 ++ 4 files changed, 63 insertions(+) create mode 100644 Passepartout-iOS/Cells/ActivityTableViewCell.swift diff --git a/Passepartout-iOS/Cells/ActivityTableViewCell.swift b/Passepartout-iOS/Cells/ActivityTableViewCell.swift new file mode 100644 index 00000000..eebc8c02 --- /dev/null +++ b/Passepartout-iOS/Cells/ActivityTableViewCell.swift @@ -0,0 +1,49 @@ +// +// ActivityTableViewCell.swift +// Passepartout-iOS +// +// Created by Davide De Rosa on 4/8/19. +// Copyright (c) 2019 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import UIKit + +extension Cells { + static let activity = ActivityTableViewCell.Provider() +} + +class ActivityTableViewCell: UITableViewCell { + private lazy var activityIndicator = UIActivityIndicatorView() +} + +extension ActivityTableViewCell { + class Provider: CellProvider { + typealias T = ActivityTableViewCell + + func dequeue(from tableView: UITableView, for indexPath: IndexPath) -> ActivityTableViewCell { + let cell = tableView.dequeue(T.self, identifier: Provider.identifier, for: indexPath) + cell.apply(Theme.current) + cell.activityIndicator.startAnimating() + cell.accessoryView = cell.activityIndicator + cell.selectionStyle = .none + return cell + } + } +} diff --git a/Passepartout-iOS/Global/Theme+Cells.swift b/Passepartout-iOS/Global/Theme+Cells.swift index 7f30d320..c02abc98 100644 --- a/Passepartout-iOS/Global/Theme+Cells.swift +++ b/Passepartout-iOS/Global/Theme+Cells.swift @@ -61,6 +61,13 @@ extension ToggleTableViewCell { } } +extension ActivityTableViewCell { + func apply(_ theme: Theme) { + textLabel?.text = nil + detailTextLabel?.text = nil + } +} + extension SettingTableViewCell { func applyAction(_ theme: Theme) { leftTextColor = theme.palette.action diff --git a/Passepartout-iOS/Global/Theme.swift b/Passepartout-iOS/Global/Theme.swift index 9a3d4038..1588e688 100644 --- a/Passepartout-iOS/Global/Theme.swift +++ b/Passepartout-iOS/Global/Theme.swift @@ -102,6 +102,9 @@ extension Theme { let toggle = UISwitch.appearance() toggle.onTintColor = palette.accessory + + let activity = UIActivityIndicatorView.appearance() + activity.color = palette.accessory } } diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 83a7ee2e..6f3e2b25 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -63,6 +63,7 @@ 0E3152DD223FA06100F61841 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E05C5DF20D198B9006EE732 /* Localizable.strings */; }; 0E3152DE223FA06400F61841 /* Web in Resources */ = {isa = PBXBuildFile; fileRef = 0E0EABC721DF853C0069DAE7 /* Web */; }; 0E3152DF223FA1DD00F61841 /* ConnectionService.json in Resources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F121822B4D00106008 /* ConnectionService.json */; }; + 0E3586FE225BD34800509A4D /* ActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */; }; 0E36D24D2240234B006AF062 /* ShortcutsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E36D24C2240234B006AF062 /* ShortcutsAddViewController.swift */; }; 0E36D25822403469006AF062 /* Shortcuts.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E36D25A22403469006AF062 /* Shortcuts.storyboard */; }; 0E36D25C224034AD006AF062 /* ShortcutsConnectToViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E36D25B224034AD006AF062 /* ShortcutsConnectToViewController.swift */; }; @@ -189,6 +190,7 @@ 0E31529E223F9EF500F61841 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0E3152A3223F9EF500F61841 /* Passepartout-CoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Passepartout-CoreTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 0E3152AC223F9EF500F61841 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTableViewCell.swift; sourceTree = ""; }; 0E36D24C2240234B006AF062 /* ShortcutsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsAddViewController.swift; sourceTree = ""; }; 0E36D25B224034AD006AF062 /* ShortcutsConnectToViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutsConnectToViewController.swift; sourceTree = ""; }; 0E39BCEF214B9EF10035E9DE /* WebServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServices.swift; sourceTree = ""; }; @@ -335,6 +337,7 @@ 0E1066CA20E0F85C004F98B7 /* Cells */ = { isa = PBXGroup; children = ( + 0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */, 0E1066C820E0F84A004F98B7 /* Cells.swift */, 0E4C9CBA20DCF0D600A0C59C /* DestructiveTableViewCell.swift */, 0E05C5CE20D139AF006EE732 /* FieldTableViewCell.swift */, @@ -1078,6 +1081,7 @@ 0EFBFAC121AC464800887A8C /* CreditsViewController.swift in Sources */, 0EFD943E215BE10800529B64 /* IssueReporter.swift in Sources */, 0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */, + 0E3586FE225BD34800509A4D /* ActivityTableViewCell.swift in Sources */, 0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */, 0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */, 0E36D24D2240234B006AF062 /* ShortcutsAddViewController.swift in Sources */, From 84c54933487678f539358d49d076dacf4a976308 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 8 Apr 2019 22:12:44 +0200 Subject: [PATCH 2/5] Remap donations to local RowType Allow different cells easily. --- .../Organizer/DonationViewController.swift | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift index 1848955b..e6afd14f 100644 --- a/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift @@ -28,6 +28,8 @@ import StoreKit import Passepartout_Core class DonationViewController: UITableViewController, TableModelHost { + private var donationList: [InApp.Donation] = [] + private var productsByIdentifier: [String: SKProduct] = [:] private func setProducts(_ products: [SKProduct]) { @@ -40,25 +42,23 @@ class DonationViewController: UITableViewController, TableModelHost { // MARK: TableModel - var model: TableModel = TableModel() + var model: TableModel = TableModel() func reloadModel() { + donationList = [] model.clear() let completeList: [InApp.Donation] = [.tiny, .small, .medium, .big, .huge, .maxi] - var list: [InApp.Donation] = [] for row in completeList { guard let _ = productsByIdentifier[row.rawValue] else { continue } - list.append(row) + donationList.append(row) } model.add(.oneTime) -// model.add(.recurring) model.setHeader(L10n.Donation.Sections.OneTime.header, for: .oneTime) model.setFooter(L10n.Donation.Sections.OneTime.footer, for: .oneTime) -// model.setHeader(L10n.Donation.Sections.Recurring.header, for: .recurring) - model.set(list, in: .oneTime) + model.set(.donation, count: donationList.count, in: .oneTime) } // MARK: UIViewController @@ -104,25 +104,31 @@ class DonationViewController: UITableViewController, TableModelHost { } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let productId = productIdentifier(at: indexPath) - guard let product = productsByIdentifier[productId] else { - fatalError("Row with no associated product") + switch model.row(at: indexPath) { + case .donation: + let productId = productIdentifier(at: indexPath) + guard let product = productsByIdentifier[productId] else { + fatalError("Row with no associated product") + } + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = product.localizedTitle + cell.rightText = product.localizedPrice + return cell } - let cell = Cells.setting.dequeue(from: tableView, for: indexPath) - cell.leftText = product.localizedTitle - cell.rightText = product.localizedPrice - return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let productId = productIdentifier(at: indexPath) - guard let product = productsByIdentifier[productId] else { - fatalError("Row with no associated product") - } - InAppHelper.shared.purchase(product: product) { - self.handlePurchase(result: $0, error: $1) + switch model.row(at: indexPath) { + case .donation: + tableView.deselectRow(at: indexPath, animated: true) + + let productId = productIdentifier(at: indexPath) + guard let product = productsByIdentifier[productId] else { + fatalError("Row with no associated product") + } + InAppHelper.shared.purchase(product: product) { + self.handlePurchase(result: $0, error: $1) + } } } @@ -145,12 +151,19 @@ class DonationViewController: UITableViewController, TableModelHost { extension DonationViewController { enum SectionType { + case inProgress + case oneTime - - case recurring + } + + enum RowType { + case donation } private func productIdentifier(at indexPath: IndexPath) -> String { - return model.row(at: indexPath).rawValue + guard model.row(at: indexPath) == .donation else { + fatalError("Not a donation row") + } + return donationList[indexPath.row].rawValue } } From 1a7f2d745b5120a84ab6f2fac97fd75f27c64c22 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 8 Apr 2019 22:25:12 +0200 Subject: [PATCH 3/5] Show activity when loading donations --- .../Base.lproj/Organizer.storyboard | 17 ++++++++++ .../Organizer/DonationViewController.swift | 34 ++++++++++++++----- .../Resources/en.lproj/Localizable.strings | 1 + Passepartout/Sources/SwiftGen+Strings.swift | 6 ++++ 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/Passepartout-iOS/Base.lproj/Organizer.storyboard b/Passepartout-iOS/Base.lproj/Organizer.storyboard index 02679026..b326da5c 100644 --- a/Passepartout-iOS/Base.lproj/Organizer.storyboard +++ b/Passepartout-iOS/Base.lproj/Organizer.storyboard @@ -279,6 +279,23 @@ + + + + + + + + + + + diff --git a/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift index e6afd14f..40b15c6d 100644 --- a/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift @@ -32,6 +32,8 @@ class DonationViewController: UITableViewController, TableModelHost { private var productsByIdentifier: [String: SKProduct] = [:] + private var isLoading = true + private func setProducts(_ products: [SKProduct]) { for p in products { productsByIdentifier[p.productIdentifier] = p @@ -43,11 +45,20 @@ class DonationViewController: UITableViewController, TableModelHost { // MARK: TableModel var model: TableModel = TableModel() - + func reloadModel() { donationList = [] model.clear() + model.add(.oneTime) + model.setHeader(L10n.Donation.Sections.OneTime.header, for: .oneTime) + model.setFooter(L10n.Donation.Sections.OneTime.footer, for: .oneTime) + + guard !isLoading else { + model.set([.loading], in: .oneTime) + return + } + let completeList: [InApp.Donation] = [.tiny, .small, .medium, .big, .huge, .maxi] for row in completeList { guard let _ = productsByIdentifier[row.rawValue] else { @@ -55,9 +66,6 @@ class DonationViewController: UITableViewController, TableModelHost { } donationList.append(row) } - model.add(.oneTime) - model.setHeader(L10n.Donation.Sections.OneTime.header, for: .oneTime) - model.setFooter(L10n.Donation.Sections.OneTime.footer, for: .oneTime) model.set(.donation, count: donationList.count, in: .oneTime) } @@ -67,16 +75,16 @@ class DonationViewController: UITableViewController, TableModelHost { super.viewDidLoad() title = L10n.Donation.title + reloadModel() let inApp = InAppHelper.shared if inApp.products.isEmpty { - let hud = HUD() - hud.show() inApp.requestProducts { - hud.hide() + self.isLoading = false self.setProducts($0) } } else { + isLoading = false setProducts(inApp.products) } } @@ -105,6 +113,11 @@ class DonationViewController: UITableViewController, TableModelHost { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { switch model.row(at: indexPath) { + case .loading: + let cell = Cells.activity.dequeue(from: tableView, for: indexPath) + cell.textLabel?.text = L10n.Donation.Cells.Loading.caption + return cell + case .donation: let productId = productIdentifier(at: indexPath) guard let product = productsByIdentifier[productId] else { @@ -129,6 +142,9 @@ class DonationViewController: UITableViewController, TableModelHost { InAppHelper.shared.purchase(product: product) { self.handlePurchase(result: $0, error: $1) } + + default: + break } } @@ -151,12 +167,12 @@ class DonationViewController: UITableViewController, TableModelHost { extension DonationViewController { enum SectionType { - case inProgress - case oneTime } enum RowType { + case loading + case donation } diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index c0a35f3a..37362ca5 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -251,6 +251,7 @@ "donation.sections.one_time.header" = "One time"; "donation.sections.one_time.footer" = "If you want to display gratitude for my free work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times."; //"donation.sections.recurring.header" = "Recurring"; +"donation.cells.loading.caption" = "Loading donations"; "donation.alerts.purchase.success.title" = "Thank you"; "donation.alerts.purchase.success.message" = "This means a lot to me and I really hope you keep using and promoting this app."; "donation.alerts.purchase.failure.message" = "Unable to perform the donation. %@"; diff --git a/Passepartout/Sources/SwiftGen+Strings.swift b/Passepartout/Sources/SwiftGen+Strings.swift index 334c11bb..7f93a9de 100644 --- a/Passepartout/Sources/SwiftGen+Strings.swift +++ b/Passepartout/Sources/SwiftGen+Strings.swift @@ -314,6 +314,12 @@ public enum L10n { } } } + public enum Cells { + public enum Loading { + /// Loading donations + public static let caption = L10n.tr("Localizable", "donation.cells.loading.caption") + } + } public enum Sections { public enum OneTime { /// If you want to display gratitude for my free work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times. From db6aa1059058ad88b5f899ce7068369a0289fa48 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 8 Apr 2019 22:53:00 +0200 Subject: [PATCH 4/5] Show activity when purchasing donation --- .../Organizer/DonationViewController.swift | 37 +++++++++++++++++-- .../Resources/en.lproj/Localizable.strings | 1 + Passepartout/Sources/SwiftGen+Strings.swift | 4 ++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift index 40b15c6d..1e45a440 100644 --- a/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift @@ -34,6 +34,8 @@ class DonationViewController: UITableViewController, TableModelHost { private var isLoading = true + private var isPurchasing = false + private func setProducts(_ products: [SKProduct]) { for p in products { productsByIdentifier[p.productIdentifier] = p @@ -67,6 +69,11 @@ class DonationViewController: UITableViewController, TableModelHost { donationList.append(row) } model.set(.donation, count: donationList.count, in: .oneTime) + + if isPurchasing { + model.add(.activity) + model.set([.purchasing], in: .activity) + } } // MARK: UIViewController @@ -118,6 +125,11 @@ class DonationViewController: UITableViewController, TableModelHost { cell.textLabel?.text = L10n.Donation.Cells.Loading.caption return cell + case .purchasing: + let cell = Cells.activity.dequeue(from: tableView, for: indexPath) + cell.textLabel?.text = L10n.Donation.Cells.Purchasing.caption + return cell + case .donation: let productId = productIdentifier(at: indexPath) guard let product = productsByIdentifier[productId] else { @@ -126,6 +138,7 @@ class DonationViewController: UITableViewController, TableModelHost { let cell = Cells.setting.dequeue(from: tableView, for: indexPath) cell.leftText = product.localizedTitle cell.rightText = product.localizedPrice + cell.isTappable = !isPurchasing return cell } } @@ -133,12 +146,19 @@ class DonationViewController: UITableViewController, TableModelHost { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch model.row(at: indexPath) { case .donation: + guard !isPurchasing else { + return + } tableView.deselectRow(at: indexPath, animated: true) - let productId = productIdentifier(at: indexPath) guard let product = productsByIdentifier[productId] else { fatalError("Row with no associated product") } + + isPurchasing = true + reloadModel() + tableView.reloadData() + InAppHelper.shared.purchase(product: product) { self.handlePurchase(result: $0, error: $1) } @@ -152,6 +172,9 @@ class DonationViewController: UITableViewController, TableModelHost { let alert: UIAlertController switch result { case .cancelled: + isPurchasing = false + reloadModel() + tableView.reloadData() return case .success: @@ -160,18 +183,26 @@ class DonationViewController: UITableViewController, TableModelHost { case .failure: alert = Macros.alert(title, L10n.Donation.Alerts.Purchase.Failure.message(error?.localizedDescription ?? "")) } - alert.addCancelAction(L10n.Global.ok) - present(alert, animated: true, completion: nil) + alert.addCancelAction(L10n.Global.ok) { + self.isPurchasing = false + self.reloadModel() + self.tableView.reloadData() + } + present(alert, animated: true) } } extension DonationViewController { enum SectionType { case oneTime + + case activity } enum RowType { case loading + + case purchasing case donation } diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index 37362ca5..fddeea0d 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -252,6 +252,7 @@ "donation.sections.one_time.footer" = "If you want to display gratitude for my free work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times."; //"donation.sections.recurring.header" = "Recurring"; "donation.cells.loading.caption" = "Loading donations"; +"donation.cells.purchasing.caption" = "Performing donation"; "donation.alerts.purchase.success.title" = "Thank you"; "donation.alerts.purchase.success.message" = "This means a lot to me and I really hope you keep using and promoting this app."; "donation.alerts.purchase.failure.message" = "Unable to perform the donation. %@"; diff --git a/Passepartout/Sources/SwiftGen+Strings.swift b/Passepartout/Sources/SwiftGen+Strings.swift index 7f93a9de..2012db18 100644 --- a/Passepartout/Sources/SwiftGen+Strings.swift +++ b/Passepartout/Sources/SwiftGen+Strings.swift @@ -319,6 +319,10 @@ public enum L10n { /// Loading donations public static let caption = L10n.tr("Localizable", "donation.cells.loading.caption") } + public enum Purchasing { + /// Performing donation + public static let caption = L10n.tr("Localizable", "donation.cells.purchasing.caption") + } } public enum Sections { public enum OneTime { From ffc85ed59c2adfcd381e99178aaff42c978867e9 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 8 Apr 2019 23:02:34 +0200 Subject: [PATCH 5/5] Add missing translations --- Passepartout/Resources/en.lproj/Localizable.strings | 1 - Passepartout/Resources/it.lproj/Localizable.strings | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index fddeea0d..9469add6 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -250,7 +250,6 @@ "donation.title" = "Donate"; "donation.sections.one_time.header" = "One time"; "donation.sections.one_time.footer" = "If you want to display gratitude for my free work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times."; -//"donation.sections.recurring.header" = "Recurring"; "donation.cells.loading.caption" = "Loading donations"; "donation.cells.purchasing.caption" = "Performing donation"; "donation.alerts.purchase.success.title" = "Thank you"; diff --git a/Passepartout/Resources/it.lproj/Localizable.strings b/Passepartout/Resources/it.lproj/Localizable.strings index f94ef1a2..0d7b3513 100644 --- a/Passepartout/Resources/it.lproj/Localizable.strings +++ b/Passepartout/Resources/it.lproj/Localizable.strings @@ -250,6 +250,8 @@ "donation.title" = "Donazione"; "donation.sections.one_time.header" = "Unica"; "donation.sections.one_time.footer" = "Se vuoi mostrare gratitudine per il mio lavoro a titolo gratuito, qui trovi varie somme da donare all'istante.\n\nLa donazione ti sarà addebitata solo una volta, e puoi effettuare più donazioni."; +"donation.cells.loading.caption" = "Caricando donazioni"; +"donation.cells.purchasing.caption" = "Effettuando donazione"; "donation.alerts.purchase.success.title" = "Grazie"; "donation.alerts.purchase.success.message" = "Questo significa molto per me e spero vivamente che tu continui ad usare e promuovere quest'applicazione."; "donation.alerts.purchase.failure.message" = "Impossibile effettuare la donazione. %@";