diff --git a/Afterpay.xcodeproj/project.pbxproj b/Afterpay.xcodeproj/project.pbxproj index 836a963f..ed0c7591 100644 --- a/Afterpay.xcodeproj/project.pbxproj +++ b/Afterpay.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 15F7DDB725393BD30011EC25 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F7DDB625393BD30011EC25 /* CurrencyFormatter.swift */; }; 6602EF0F25358A8000A0468C /* ColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6602EF0E25358A8000A0468C /* ColorScheme.swift */; }; 6605666324E5199500DA588E /* Locales.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6605666224E5199500DA588E /* Locales.swift */; }; 6615F99B24D14620005036F1 /* SVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6615F99A24D14620005036F1 /* SVG.swift */; }; @@ -21,8 +22,8 @@ 6672982A25357D80001D1C5A /* SVGConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6672982925357D80001D1C5A /* SVGConfiguration.swift */; }; 667AD3542497121200BF94E5 /* CheckoutWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667AD3532497121100BF94E5 /* CheckoutWebViewController.swift */; }; 6689536C24C96CB5005090B4 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6689536B24C96CB5005090B4 /* Configuration.swift */; }; + 66C3F7FB25397A810086DD0A /* CurrencyFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C3F7FA25397A810086DD0A /* CurrencyFormatterTests.swift */; }; 66D685B224BD3FB900C7287C /* SwiftUIWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D685B124BD3FB900C7287C /* SwiftUIWrapper.swift */; }; - 66DAAC8924E0CE7700127460 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DAAC8824E0CE7700127460 /* Currency.swift */; }; 66DAAC8B24E0CF0100127460 /* PriceBreakdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DAAC8A24E0CF0100127460 /* PriceBreakdown.swift */; }; 66DAAC8D24E109D200127460 /* PriceBreakdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DAAC8C24E109D200127460 /* PriceBreakdownTests.swift */; }; 66E255AE24E3C14600C81F20 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E255AD24E3C14600C81F20 /* Strings.swift */; }; @@ -43,6 +44,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 15F7DDB625393BD30011EC25 /* CurrencyFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatter.swift; sourceTree = ""; }; 6602EF0E25358A8000A0468C /* ColorScheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorScheme.swift; sourceTree = ""; }; 6605666224E5199500DA588E /* Locales.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locales.swift; sourceTree = ""; }; 6615F99A24D14620005036F1 /* SVG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVG.swift; sourceTree = ""; }; @@ -70,8 +72,8 @@ 66B57E55248F5C7D0020C642 /* Project-Shared.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Project-Shared.xcconfig"; sourceTree = ""; }; 66B57E56248F5C7D0020C642 /* Project-Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; 66B57E57248F5C7D0020C642 /* Afterpay-Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Afterpay-Release.xcconfig"; sourceTree = ""; }; + 66C3F7FA25397A810086DD0A /* CurrencyFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatterTests.swift; sourceTree = ""; }; 66D685B124BD3FB900C7287C /* SwiftUIWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIWrapper.swift; sourceTree = ""; }; - 66DAAC8824E0CE7700127460 /* Currency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Currency.swift; sourceTree = ""; }; 66DAAC8A24E0CF0100127460 /* PriceBreakdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceBreakdown.swift; sourceTree = ""; }; 66DAAC8C24E109D200127460 /* PriceBreakdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceBreakdownTests.swift; sourceTree = ""; }; 66E255AD24E3C14600C81F20 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; @@ -142,6 +144,7 @@ isa = PBXGroup; children = ( 6635B95E24CAA9F000EBB3A6 /* ConfigurationTests.swift */, + 66C3F7FA25397A810086DD0A /* CurrencyFormatterTests.swift */, 665FC57B2488766C00A5A93E /* Info.plist */, 66DAAC8C24E109D200127460 /* PriceBreakdownTests.swift */, ); @@ -209,7 +212,7 @@ isa = PBXGroup; children = ( 6689536B24C96CB5005090B4 /* Configuration.swift */, - 66DAAC8824E0CE7700127460 /* Currency.swift */, + 15F7DDB625393BD30011EC25 /* CurrencyFormatter.swift */, 6605666224E5199500DA588E /* Locales.swift */, 66DAAC8A24E0CF0100127460 /* PriceBreakdown.swift */, ); @@ -418,6 +421,7 @@ files = ( 6635B95F24CAA9F000EBB3A6 /* ConfigurationTests.swift in Sources */, 66DAAC8D24E109D200127460 /* PriceBreakdownTests.swift in Sources */, + 66C3F7FB25397A810086DD0A /* CurrencyFormatterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -432,6 +436,7 @@ 6602EF0F25358A8000A0468C /* ColorScheme.swift in Sources */, 6672982A25357D80001D1C5A /* SVGConfiguration.swift in Sources */, 6689536C24C96CB5005090B4 /* Configuration.swift in Sources */, + 15F7DDB725393BD30011EC25 /* CurrencyFormatter.swift in Sources */, 66DAAC8B24E0CF0100127460 /* PriceBreakdown.swift in Sources */, 662A3AED24A999A500EFD826 /* CheckoutResult.swift in Sources */, 667AD3542497121200BF94E5 /* CheckoutWebViewController.swift in Sources */, @@ -440,7 +445,6 @@ 66D685B224BD3FB900C7287C /* SwiftUIWrapper.swift in Sources */, 666D334C24A48F5C00FCD464 /* ObjcWrapper.swift in Sources */, 946388FE24DD077F00A1227A /* InfoWebViewController.swift in Sources */, - 66DAAC8924E0CE7700127460 /* Currency.swift in Sources */, 66EE9BD724DCEC3E00A81C19 /* LinkTextView.swift in Sources */, 6605666324E5199500DA588E /* Locales.swift in Sources */, ); diff --git a/AfterpayTests/CurrencyFormatterTests.swift b/AfterpayTests/CurrencyFormatterTests.swift new file mode 100644 index 00000000..32862570 --- /dev/null +++ b/AfterpayTests/CurrencyFormatterTests.swift @@ -0,0 +1,104 @@ +// +// CurrencyFormatterTests.swift +// AfterpayTests +// +// Created by Adam Campbell on 16/10/20. +// Copyright © 2020 Afterpay. All rights reserved. +// + +@testable import Afterpay +import XCTest + +class CurrencyFormatterTests: XCTestCase { + + func testAustraliaLocale() { + let formatter: (String?) -> CurrencyFormatter = { currencyCode in + CurrencyFormatter(locale: Locales.australia, currencyCode: currencyCode!) + } + + let audFormatter = formatter(Locales.australia.currencyCode) + let cadFormatter = formatter(Locales.canada.currencyCode) + let gbpFormatter = formatter(Locales.greatBritain.currencyCode) + let nzdFormatter = formatter(Locales.newZealand.currencyCode) + let usdFormatter = formatter(Locales.unitedStates.currencyCode) + + XCTAssertEqual(audFormatter.string(from: 120), "$120.00") + XCTAssertEqual(cadFormatter.string(from: 120), "$120.00 CAD") + XCTAssertEqual(gbpFormatter.string(from: 120), "£120.00") + XCTAssertEqual(nzdFormatter.string(from: 120), "$120.00 NZD") + XCTAssertEqual(usdFormatter.string(from: 120), "$120.00 USD") + } + + func testCanadaLocale() { + let formatter: (String?) -> CurrencyFormatter = { currencyCode in + CurrencyFormatter(locale: Locales.canada, currencyCode: currencyCode!) + } + + let audFormatter = formatter(Locales.australia.currencyCode) + let cadFormatter = formatter(Locales.canada.currencyCode) + let gbpFormatter = formatter(Locales.greatBritain.currencyCode) + let nzdFormatter = formatter(Locales.newZealand.currencyCode) + let usdFormatter = formatter(Locales.unitedStates.currencyCode) + + XCTAssertEqual(audFormatter.string(from: 120), "$120.00 AUD") + XCTAssertEqual(cadFormatter.string(from: 120), "$120.00") + XCTAssertEqual(gbpFormatter.string(from: 120), "£120.00") + XCTAssertEqual(nzdFormatter.string(from: 120), "$120.00 NZD") + XCTAssertEqual(usdFormatter.string(from: 120), "$120.00 USD") + } + + func testGreatBritainLocale() { + let formatter: (String?) -> CurrencyFormatter = { currencyCode in + CurrencyFormatter(locale: Locales.greatBritain, currencyCode: currencyCode!) + } + + let audFormatter = formatter(Locales.australia.currencyCode) + let cadFormatter = formatter(Locales.canada.currencyCode) + let gbpFormatter = formatter(Locales.greatBritain.currencyCode) + let nzdFormatter = formatter(Locales.newZealand.currencyCode) + let usdFormatter = formatter(Locales.unitedStates.currencyCode) + + XCTAssertEqual(audFormatter.string(from: 120), "$120.00 AUD") + XCTAssertEqual(cadFormatter.string(from: 120), "$120.00 CAD") + XCTAssertEqual(gbpFormatter.string(from: 120), "£120.00") + XCTAssertEqual(nzdFormatter.string(from: 120), "$120.00 NZD") + XCTAssertEqual(usdFormatter.string(from: 120), "$120.00 USD") + } + + func testNewZealandLocale() { + let formatter: (String?) -> CurrencyFormatter = { currencyCode in + CurrencyFormatter(locale: Locales.newZealand, currencyCode: currencyCode!) + } + + let audFormatter = formatter(Locales.australia.currencyCode) + let cadFormatter = formatter(Locales.canada.currencyCode) + let gbpFormatter = formatter(Locales.greatBritain.currencyCode) + let nzdFormatter = formatter(Locales.newZealand.currencyCode) + let usdFormatter = formatter(Locales.unitedStates.currencyCode) + + XCTAssertEqual(audFormatter.string(from: 120), "$120.00 AUD") + XCTAssertEqual(cadFormatter.string(from: 120), "$120.00 CAD") + XCTAssertEqual(gbpFormatter.string(from: 120), "£120.00") + XCTAssertEqual(nzdFormatter.string(from: 120), "$120.00") + XCTAssertEqual(usdFormatter.string(from: 120), "$120.00 USD") + } + + func testUnitedStatesLocale() { + let formatter: (String?) -> CurrencyFormatter = { currencyCode in + CurrencyFormatter(locale: Locales.unitedStates, currencyCode: currencyCode!) + } + + let audFormatter = formatter(Locales.australia.currencyCode) + let cadFormatter = formatter(Locales.canada.currencyCode) + let gbpFormatter = formatter(Locales.greatBritain.currencyCode) + let nzdFormatter = formatter(Locales.newZealand.currencyCode) + let usdFormatter = formatter(Locales.unitedStates.currencyCode) + + XCTAssertEqual(audFormatter.string(from: 120), "A$120.00") + XCTAssertEqual(cadFormatter.string(from: 120), "CA$120.00") + XCTAssertEqual(gbpFormatter.string(from: 120), "£120.00") + XCTAssertEqual(nzdFormatter.string(from: 120), "NZ$120.00") + XCTAssertEqual(usdFormatter.string(from: 120), "$120.00") + } + +} diff --git a/Sources/Afterpay/Model/Configuration.swift b/Sources/Afterpay/Model/Configuration.swift index 4b3322ef..a1df4fa1 100644 --- a/Sources/Afterpay/Model/Configuration.swift +++ b/Sources/Afterpay/Model/Configuration.swift @@ -39,7 +39,7 @@ public struct Configuration { let minimumAmount: Decimal? let maximumAmount: Decimal - let currency: Currency + let currencyCode: String let locale: Locale /// Creates a new configuration by taking in a minimum and maximum amount as well as a currency @@ -71,8 +71,8 @@ public struct Configuration { /// rounded to 2 decimal places. However values convertible to a Swift Decimal are accepted. /// - maximumAmount: A amount string representation of a decimal number, rounded to 2 /// decimal places. However values convertible to a Swift Decimal are accepted. - /// - currencyCode: The currency in ISO 4217 format. Supported values include "AUD", "NZD", - /// "USD", and "CAD". However values recognized by Foundation are accepted. + /// - currencyCode: The currency in ISO 4217 format. Supported values include "AUD", "CAD", + /// "GBP", "NZD" and "USD". /// - locale: The locale required for display of the appropriate terms and conditions and used /// for formatting of currency. For example if the locale is set to en_AU are specified as USD. /// More examples are available in the currency tab of the js sandbox: @@ -101,7 +101,7 @@ public struct Configuration { throw ConfigurationError.invalidOrdering(minimum: minimumAmount!, maximum: maximumAmount) } - guard let currency = Currency(currencyCode: currencyCode) else { + guard Locales.validSet.map(\.currencyCode).contains(currencyCode) else { throw ConfigurationError.invalidCurrencyCode(currencyCode) } @@ -111,7 +111,7 @@ public struct Configuration { self.minimumAmount = minimumDecimal self.maximumAmount = maximumDecimal - self.currency = currency + self.currencyCode = currencyCode self.locale = locale } diff --git a/Sources/Afterpay/Model/Currency.swift b/Sources/Afterpay/Model/Currency.swift deleted file mode 100644 index 433a71d9..00000000 --- a/Sources/Afterpay/Model/Currency.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Currency.swift -// Afterpay -// -// Created by Adam Campbell on 10/8/20. -// Copyright © 2020 Afterpay. All rights reserved. -// - -import Foundation - -private let unknownCurrencyName = "Unknown Currency" - -struct Currency { - - let code: String - - var symbol: String { - switch code { - case Locales.australia.currencyCode: - return "A$" - case Locales.newZealand.currencyCode: - return "NZ$" - case Locales.canada.currencyCode: - return "CA$" - default: - return "$" - } - } - - var locale: Locale { - switch code { - case Locales.australia.currencyCode: - return Locales.australia - case Locales.newZealand.currencyCode: - return Locales.newZealand - case Locales.canada.currencyCode: - return Locales.canada - default: - return Locales.unitedStates - } - } - - init?(currencyCode: String) { - let currencyName = Locales.posix.localizedString(forCurrencyCode: currencyCode) - - guard currencyName != nil, currencyName != unknownCurrencyName else { - return nil - } - - self.code = currencyCode - } - -} diff --git a/Sources/Afterpay/Model/CurrencyFormatter.swift b/Sources/Afterpay/Model/CurrencyFormatter.swift new file mode 100644 index 00000000..46e1c17a --- /dev/null +++ b/Sources/Afterpay/Model/CurrencyFormatter.swift @@ -0,0 +1,38 @@ +// +// CurrencyFormatter.swift +// Afterpay +// +// Created by Adam Campbell on 16/10/20. +// Copyright © 2020 Afterpay. All rights reserved. +// + +import Foundation + +private let formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + return formatter +}() + +struct CurrencyFormatter { + + let locale: Locale + let currencyCode: String + + func string(from decimal: Decimal) -> String? { + if locale == Locales.unitedStates || locale.currencyCode == currencyCode { + formatter.locale = locale + formatter.currencyCode = currencyCode + return formatter.string(from: decimal as NSDecimalNumber) + } else { + let currencyLocale = Locales.validSet.first { $0.currencyCode == currencyCode } + formatter.locale = currencyLocale + formatter.currencyCode = currencyCode + let formattedString = formatter.string(from: decimal as NSDecimalNumber) + return currencyLocale?.currencySymbol == Locales.unitedStates.currencySymbol + ? formattedString?.appending(" \(currencyCode)") + : formattedString + } + } + +} diff --git a/Sources/Afterpay/Model/PriceBreakdown.swift b/Sources/Afterpay/Model/PriceBreakdown.swift index 71356f95..46f082e9 100644 --- a/Sources/Afterpay/Model/PriceBreakdown.swift +++ b/Sources/Afterpay/Model/PriceBreakdown.swift @@ -8,12 +8,6 @@ import Foundation -private let formatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - return formatter -}() - struct PriceBreakdown { enum BadgePlacement: Equatable { @@ -26,10 +20,10 @@ struct PriceBreakdown { init(totalAmount: Decimal) { let configuration = getConfiguration() + let formatter = configuration + .map { CurrencyFormatter(locale: $0.locale, currencyCode: $0.currencyCode) } + let format = { formatter?.string(from: $0) } - formatter.currencySymbol = configuration?.currency.symbol - - let format: (Decimal) -> String? = { formatter.string(from: $0 as NSDecimalNumber) } let formattedMinimum = configuration?.minimumAmount.flatMap(format) let formattedMaximum = (configuration?.maximumAmount).flatMap(format) let formattedPayment = format(totalAmount / 4) diff --git a/Sources/Afterpay/Views/SVGView.swift b/Sources/Afterpay/Views/SVGView.swift index 68e86b37..c84242f4 100644 --- a/Sources/Afterpay/Views/SVGView.swift +++ b/Sources/Afterpay/Views/SVGView.swift @@ -16,17 +16,13 @@ import UIKit final class SVGView: Macaw.SVGView { - var svg: SVG { - svgConfiguration.svg(localizedFor: locale, withTraits: traitCollection) - } + var svg: SVG { svgConfiguration.svg(localizedFor: locale, withTraits: traitCollection) } var svgConfiguration: SVGConfiguration { didSet { svgDidChange() } } - private var locale: Locale { - getConfiguration()?.locale ?? Locales.unitedStates - } + private var locale: Locale { getConfiguration()?.locale ?? Locales.unitedStates } init(svgConfiguration: SVGConfiguration) { self.svgConfiguration = svgConfiguration