diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index c99b9fdff97..6d1f237a372 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -12,6 +12,7 @@ - [*] My Store: A new button to share the current store is added on the top right of the screen. [https://github.com/woocommerce/woocommerce-ios/pull/9796] - [*] Mobile Payments: The screen brightness is increased when showing the Scan to Pay view so the QR code can be scanned more easily [https://github.com/woocommerce/woocommerce-ios/pull/9807] +- [*] Mobile Payments: The Woo logo is added to the QR code on the Scan to Pay screen [https://github.com/woocommerce/woocommerce-ios/pull/9823] - [*] Allow EU merchants to have better control of their privacy choices. A privacy choices banner will be shown the next time they open the app. 13.7 diff --git a/WooCommerce/Classes/ViewRelated/Orders/ScanToPay/ScanToPayViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/ScanToPay/ScanToPayViewModel.swift index 3d6401956c9..e8e9e3b3b30 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/ScanToPay/ScanToPayViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/ScanToPay/ScanToPayViewModel.swift @@ -1,6 +1,6 @@ import Foundation -import CoreImage.CIFilterBuiltins import UIKit +import WooFoundation struct ScanToPayViewModel { private let paymentURL: URL? @@ -10,19 +10,12 @@ struct ScanToPayViewModel { } func generateQRCodeImage() -> UIImage? { - guard let paymentURLString = paymentURL?.absoluteString else { - return nil + guard let logoImage = UIImage + .wooLogoImage()? + .withBackground(color: .black) else { + return paymentURL?.generateQRCode() } - let context = CIContext() - let filter = CIFilter.qrCodeGenerator() - filter.message = Data(paymentURLString.utf8) - - guard let outputImage = filter.outputImage, - let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { - return nil - } - - return UIImage(cgImage: cgImage) + return paymentURL?.generateQRCode(combinedWith: logoImage) } } diff --git a/WooFoundation/WooFoundation.xcodeproj/project.pbxproj b/WooFoundation/WooFoundation.xcodeproj/project.pbxproj index fe37c3c7442..f2c58ec8e8b 100644 --- a/WooFoundation/WooFoundation.xcodeproj/project.pbxproj +++ b/WooFoundation/WooFoundation.xcodeproj/project.pbxproj @@ -35,6 +35,9 @@ B97190D1292CF3BC0065E413 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97190D0292CF3BC0065E413 /* Result+Extensions.swift */; }; B987B06F284540D300C53CF6 /* CurrencyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B987B06E284540D300C53CF6 /* CurrencyCode.swift */; }; B99686DE2A13B38B00D1AF62 /* FullScreenCoverClearBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99686DD2A13B38B00D1AF62 /* FullScreenCoverClearBackgroundView.swift */; }; + B99BC2122A1FAE5100E6008A /* CIImage+ImageCombination.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99BC2112A1FAE5100E6008A /* CIImage+ImageCombination.swift */; }; + B99BC2142A1FAEBC00E6008A /* URL+QRCodeGeneration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99BC2132A1FAEBC00E6008A /* URL+QRCodeGeneration.swift */; }; + B99BC2162A1FB21700E6008A /* UIImage+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99BC2152A1FB21700E6008A /* UIImage+Background.swift */; }; B9C9C63F283E703C001B879F /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9C9C635283E703C001B879F /* WooFoundation.framework */; }; B9C9C659283E7195001B879F /* NSDecimalNumber+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C9C658283E7195001B879F /* NSDecimalNumber+Helpers.swift */; }; B9C9C65D283E71C8001B879F /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C9C65B283E71C8001B879F /* CurrencyFormatter.swift */; }; @@ -84,6 +87,9 @@ B97190D0292CF3BC0065E413 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = ""; }; B987B06E284540D300C53CF6 /* CurrencyCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyCode.swift; sourceTree = ""; }; B99686DD2A13B38B00D1AF62 /* FullScreenCoverClearBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCoverClearBackgroundView.swift; sourceTree = ""; }; + B99BC2112A1FAE5100E6008A /* CIImage+ImageCombination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CIImage+ImageCombination.swift"; sourceTree = ""; }; + B99BC2132A1FAEBC00E6008A /* URL+QRCodeGeneration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+QRCodeGeneration.swift"; sourceTree = ""; }; + B99BC2152A1FB21700E6008A /* UIImage+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Background.swift"; sourceTree = ""; }; B9AED558283E7553002A2668 /* Yosemite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Yosemite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B9AED55B283E755A002A2668 /* Hardware.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Hardware.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B9C9C635283E703C001B879F /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -252,6 +258,9 @@ 68FBC5B228926B2C00A05461 /* Collection+Extensions.swift */, 03B8C3882914083F002235B1 /* Bundle+Woo.swift */, B97190D0292CF3BC0065E413 /* Result+Extensions.swift */, + B99BC2112A1FAE5100E6008A /* CIImage+ImageCombination.swift */, + B99BC2132A1FAEBC00E6008A /* URL+QRCodeGeneration.swift */, + B99BC2152A1FB21700E6008A /* UIImage+Background.swift */, ); path = Extensions; sourceTree = ""; @@ -481,7 +490,9 @@ buildActionMask = 2147483647; files = ( 03B8C3892914083F002235B1 /* Bundle+Woo.swift in Sources */, + B99BC2122A1FAE5100E6008A /* CIImage+ImageCombination.swift in Sources */, B9C9C659283E7195001B879F /* NSDecimalNumber+Helpers.swift in Sources */, + B99BC2162A1FB21700E6008A /* UIImage+Background.swift in Sources */, 26AF1F5328B8362800937BA9 /* UIColor+SemanticColors.swift in Sources */, 03597A9B28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift in Sources */, B9C9C663283E7296001B879F /* Logging.swift in Sources */, @@ -502,6 +513,7 @@ 26AF1F5528B8362800937BA9 /* UIColor+ColorStudio.swift in Sources */, 26AF1F5428B8362800937BA9 /* ColorStudio.swift in Sources */, 68FBC5B328926B2C00A05461 /* Collection+Extensions.swift in Sources */, + B99BC2142A1FAEBC00E6008A /* URL+QRCodeGeneration.swift in Sources */, B99686DE2A13B38B00D1AF62 /* FullScreenCoverClearBackgroundView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/WooFoundation/WooFoundation/Extensions/CIImage+ImageCombination.swift b/WooFoundation/WooFoundation/Extensions/CIImage+ImageCombination.swift new file mode 100644 index 00000000000..ce0e0469a74 --- /dev/null +++ b/WooFoundation/WooFoundation/Extensions/CIImage+ImageCombination.swift @@ -0,0 +1,17 @@ +import CoreImage.CIFilterBuiltins + +extension CIImage { + /// Combines the current image with the given image centered. + /// + func combined(with image: CIImage) -> CIImage? { + guard let combinedFilter = CIFilter(name: "CISourceOverCompositing") else { + return nil + } + + let centerTransform = CGAffineTransform(translationX: extent.midX - (image.extent.size.width / 2), y: extent.midY - (image.extent.size.height / 2)) + combinedFilter.setValue(image.transformed(by: centerTransform), forKey: "inputImage") + combinedFilter.setValue(self, forKey: "inputBackgroundImage") + + return combinedFilter.outputImage + } +} diff --git a/WooFoundation/WooFoundation/Extensions/UIImage+Background.swift b/WooFoundation/WooFoundation/Extensions/UIImage+Background.swift new file mode 100644 index 00000000000..e12c7fb75f0 --- /dev/null +++ b/WooFoundation/WooFoundation/Extensions/UIImage+Background.swift @@ -0,0 +1,28 @@ +import UIKit + +public extension UIImage { + /// Adds a background color to the given UIImage, setting also whether it should be opaque or not + /// + func withBackground(color: UIColor, opaque: Bool = true) -> UIImage { + UIGraphicsBeginImageContextWithOptions(size, opaque, scale) + + guard let currentContext = UIGraphicsGetCurrentContext(), + let image = cgImage else { + return self + } + + defer { UIGraphicsEndImageContext() } + + let rect = CGRect(origin: .zero, size: size) + currentContext.setFillColor(color.cgColor) + currentContext.fill(rect) + + // Because the coordinate system in Core Graphics is different from that of UIKit, + // we need to flip the context vertically, and then translate it vertically + currentContext.scaleBy(x: 1, y: -1) + currentContext.translateBy(x: 0, y: -size.height) + currentContext.draw(image, in: rect) + + return UIGraphicsGetImageFromCurrentImageContext() ?? self + } +} diff --git a/WooFoundation/WooFoundation/Extensions/URL+QRCodeGeneration.swift b/WooFoundation/WooFoundation/Extensions/URL+QRCodeGeneration.swift new file mode 100644 index 00000000000..d4422a09c73 --- /dev/null +++ b/WooFoundation/WooFoundation/Extensions/URL+QRCodeGeneration.swift @@ -0,0 +1,39 @@ +import Foundation +import UIKit + +public extension URL { + /// Returns a black and white QR UIImage code for this URL. + /// + func generateQRCode() -> UIImage? { + guard let outputImage = generateQRCodeCIImage(), + let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else { + return nil + } + + return UIImage(cgImage: cgImage) + } + + + /// Returns a black and white QR code for this URL, adding the passed image centered. + /// + func generateQRCode(combinedWith image: UIImage) -> UIImage? { + guard let outputImage = generateQRCodeCIImage(), + let cgLogoImage = image.cgImage, + let combinedImage = outputImage.combined(with: CIImage(cgImage: cgLogoImage)), + let cgImage = CIContext().createCGImage(combinedImage, from: combinedImage.extent) else { + return nil + } + + return UIImage(cgImage: cgImage) + } + + /// Returns a black and white QR CIImage code for this URL. + /// + private func generateQRCodeCIImage() -> CIImage? { + let filter = CIFilter.qrCodeGenerator() + filter.message = Data(absoluteString.utf8) + + let qrTransform = CGAffineTransform(scaleX: 12, y: 12) + return filter.outputImage?.transformed(by: qrTransform) + } +}