Created
August 19, 2020 07:18
-
-
Save jvlad/7f07b20fdd392ca274c18628dafee6bf to your computer and use it in GitHub Desktop.
iOS. Interacting with JS code rendered within a WebView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Created by Vlad Zamskoi on 2019-08-20. | |
// Licenced under MIT | |
// | |
// This is an example of interacting with JS code rendered within a WebView | |
// See `private func buildWebView() -> WKWebView` at line 69 | |
import Foundation | |
import UIKit | |
import SnapKit | |
import WebKit | |
private let oneMonth_priceLabelIdInHtml = "price-1m" | |
private let sixMonth_priceLabelIdInHtml = "price-6m" | |
private let oneYear_priceLabelIdInHtml = "price-1y" | |
class WebShopVC: UIViewController { | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
setupBackground() | |
setupTopBar() | |
self.alert.show() | |
self.webView = buildWebView() | |
loadContentToWebView() | |
} | |
@objc init(userName: String, purchasePlans: [PurchasePlan]) { | |
self.userName = userName | |
self.purchasePlans = purchasePlans | |
super.init(nibName: nil, bundle: nil) | |
self.view.backgroundColor = .white | |
} | |
@objc func exit() { | |
navigationController?.popViewController(animated: true) | |
} | |
private let userName: String | |
private let purchasePlans: [PurchasePlan] | |
private let alert = AlertFactory.waitingSpinner() | |
private let uiSizes = CommonUISizes() | |
private let htmlId_toPurchaseIdMap = [ | |
oneMonth_priceLabelIdInHtml: "com.example.itunes.inapp.xxx", | |
sixMonth_priceLabelIdInHtml: "com.example.itunes.inapp.xxx2", | |
oneYear_priceLabelIdInHtml: "com.example.12months" | |
] | |
private var webView: WKWebView! | |
private func loadContentToWebView() { | |
let shopURLString = "https://example.com" | |
guard let shopURL = URL(string: shopURLString) else { | |
SPLog.debug("oops, something went wrong :(") | |
return; | |
} | |
let request = URLRequest( | |
url: shopURL, | |
cachePolicy: .reloadIgnoringLocalCacheData) | |
self.webView.load(request) | |
} | |
private func buildWebView() -> WKWebView { | |
let oneMonthSubscription = self.purchasePlans.first(where: { $0.billerPlanId == self.htmlId_toPurchaseIdMap[oneMonth_priceLabelIdInHtml] }) | |
let sixMonthsSubscription = self.purchasePlans.first(where: { $0.billerPlanId == self.htmlId_toPurchaseIdMap[sixMonth_priceLabelIdInHtml] }) | |
let oneYearSubscription = self.purchasePlans.first(where: { $0.billerPlanId == self.htmlId_toPurchaseIdMap[oneYear_priceLabelIdInHtml] }) | |
let source = | |
""" | |
let elementsToSearch = [ | |
{ | |
"id": "\(oneMonth_priceLabelIdInHtml)", | |
"price" : "\(oneMonthSubscription?.product.price.stringValue ?? "None")", | |
"currencySymbol": "\(oneMonthSubscription?.product.priceLocale.currencySymbol ?? "")", | |
"postMessage": "\(oneMonthSubscription?.billerPlanId ?? String())" | |
}, | |
{ | |
"id": "\(sixMonth_priceLabelIdInHtml)", | |
"price" : "\(sixMonthsSubscription?.product.price.stringValue ?? "None")", | |
"currencySymbol": "\(sixMonthsSubscription?.product.priceLocale.currencySymbol ?? "")", | |
"postMessage": "\(sixMonthsSubscription?.billerPlanId ?? String())" | |
}, | |
{ | |
"id": "\(oneYear_priceLabelIdInHtml)", | |
"price" : "\(oneYearSubscription?.product.price.stringValue ?? "None")", | |
"currencySymbol": "\(oneYearSubscription?.product.priceLocale.currencySymbol ?? "")", | |
"postMessage": "\(oneYearSubscription?.billerPlanId ?? String())" | |
} | |
] | |
let clickableAreas = document.getElementsByClassName("purchase-plan clickable"); | |
for (let clickableArea of clickableAreas) { | |
for (let item of elementsToSearch) { | |
let element = clickableArea.querySelector(`#${item.id}`); | |
if (element) { | |
element.innerHTML = item.price; | |
let currencyElement = clickableArea.getElementsByClassName("currency")[0]; | |
if (currencyElement) { | |
currencyElement.innerHTML = item.currencySymbol; | |
} | |
clickableArea.addEventListener("click", function () { | |
window.webkit.messageHandlers.clickListener.postMessage(item.postMessage); | |
}); | |
break; | |
} else { | |
continue; | |
} | |
} | |
} | |
""" | |
let script = WKUserScript(source: source, | |
injectionTime: .atDocumentEnd, | |
forMainFrameOnly: false) | |
let controller = WKUserContentController() | |
controller.addUserScript(script) | |
controller.add(self, name: "clickListener") | |
let config = WKWebViewConfiguration() | |
config.userContentController = controller | |
let webView = WKWebView(frame: .zero, configuration: config) | |
webView.navigationDelegate = self | |
webView.scrollView.delegate = self | |
view.addSubview(webView) | |
webView.snp.makeConstraints { make in | |
make.left.right.bottom.equalTo(view) | |
make.top.equalTo(self.topBarTitle.snp.bottom) | |
.offset(self.uiSizes.verticalPadding) | |
} | |
return webView | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
private func setupTopBar() { | |
setupTopBackButton() | |
setupScreenTitle() | |
} | |
private func setupTopBackButton() { | |
let b = UIButton() | |
b.setImage(UIImage(named: "header_btn_back_idle.png"), for: .normal) | |
b.addTarget(self, action: #selector(exit), for: .touchUpInside) | |
view.addSubview(b) | |
b.snp.makeConstraints { make in | |
make.height.width.equalTo(uiSizes.topBarHeight) | |
make.top.left.equalToSuperview() | |
} | |
} | |
private let topBarTitle = UILabel() | |
private func setupScreenTitle() { | |
/* TODO: 1/25/20 @IlyaShvedikov: move to presenter */ | |
let formattedName = self.userName.components(separatedBy: "@").first ?? "" | |
topBarTitle.text = "Hello \(formattedName)" | |
topBarTitle.textColor = .white | |
topBarTitle.setFontSize(uiSizes.topBarTitleFontSize) | |
view.addSubview(topBarTitle) | |
topBarTitle.snp.makeConstraints { make in | |
make.top.centerX.equalToSuperview() | |
make.height.equalTo(uiSizes.topBarHeight) | |
} | |
} | |
private func setupBackground() { | |
let v = UIImageView() | |
v.image = UIImage(named: "background.jpg") | |
view.addSubview(v) | |
v.snp.makeConstraints { make in | |
make.edges.equalToSuperview() | |
} | |
} | |
} | |
// MARK: For Tests Only | |
extension WebShopVC { | |
static func stubSubscriptions() -> [PurchasePlan] { | |
//ids are fake | |
let subsriptionsDictionary = [["price": "7.99", "priceCurrency": "$", "id": "com.example.monthly"], | |
["price": "17.99", "priceCurrency": "$", "id": "com.example.sixmonthly"], | |
["price": "49.99", "priceCurrency": "$", "id": "com.example.annual"]] | |
//mocked price and priceCurrency can't be initialized, see line 75 within PurchasePlan.m | |
let plans = subsriptionsDictionary.map { PurchasePlan(dictionary: $0)! } | |
return plans | |
} | |
static func stubUserName() -> String { | |
Config.instance().userDisplayName | |
} | |
} | |
extension WebShopVC: WKScriptMessageHandler { | |
func userContentController(_ userContentController: WKUserContentController, | |
didReceive message: WKScriptMessage) { | |
guard let subscriptionId = message.body as? String, | |
!subscriptionId.isEmpty, | |
let plan = self.purchasePlans.first(where: { | |
$0.billerPlanId == subscriptionId}) else { return }; SPLog.debug("subscriptionId is: \(subscriptionId)") | |
self.purchase(plan: plan) | |
} | |
} | |
//MARK: - WKNavigationDelegate | |
extension WebShopVC: WKNavigationDelegate { | |
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { | |
AlertFactory.dismissWaitingSpinner(self.alert) | |
} | |
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { | |
AlertFactory.dismissWaitingSpinner(self.alert) | |
} | |
} | |
//MARK: - UIScrollViewDelegate | |
extension WebShopVC: UIScrollViewDelegate { | |
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { | |
scrollView.pinchGestureRecognizer?.isEnabled = false | |
} | |
} | |
private extension WebShopVC { | |
func purchase(plan: PurchasePlan) { | |
let purchaseFlow = AppDelegate.purchaseFlow()! | |
if !purchaseFlow.isProcessingPayment() { | |
purchaseFlow.purchaseProduct(plan) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment