Skip to content

Instantly share code, notes, and snippets.

@jvlad
Created August 19, 2020 07:18
Show Gist options
  • Save jvlad/7f07b20fdd392ca274c18628dafee6bf to your computer and use it in GitHub Desktop.
Save jvlad/7f07b20fdd392ca274c18628dafee6bf to your computer and use it in GitHub Desktop.
iOS. Interacting with JS code rendered within a WebView
//
// 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