Skip to content

Commit

Permalink
Merge pull request #20 from moratori/fix-wkwebview
Browse files Browse the repository at this point in the history
Add custom scheme handler to WKWebView
  • Loading branch information
ryosuke-wakaba authored Jul 3, 2024
2 parents 1feabfc + 9d603bb commit 42bce66
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 42 deletions.
28 changes: 28 additions & 0 deletions tw2023_wallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
A84AB70D2B50C2DD00E8C88B /* IdTokenSharingHistoryManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84AB70C2B50C2DD00E8C88B /* IdTokenSharingHistoryManagerTest.swift */; };
A8509C292B82315D00B28C35 /* RecipientClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8509C282B82315D00B28C35 /* RecipientClaims.swift */; };
A857C25A2C0DD3200059A82F /* swift-format in Frameworks */ = {isa = PBXBuildFile; productRef = A857C2592C0DD3200059A82F /* swift-format */; };
A88779BD2C33DC08002EE9C2 /* WebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88779BC2C33DC08002EE9C2 /* WebViewTests.swift */; };
A89108022B82D1650060DD71 /* RecipientClaimsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A89108012B82D1650060DD71 /* RecipientClaimsViewModel.swift */; };
A89108042B82D2880060DD71 /* RecipientClaimsPreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A89108032B82D2880060DD71 /* RecipientClaimsPreviewModel.swift */; };
A891080C2B84785F0060DD71 /* credential_sharing_history.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = A891080B2B84785F0060DD71 /* credential_sharing_history.pb.swift */; };
Expand Down Expand Up @@ -388,6 +389,7 @@
A84AB70A2B50B98C00E8C88B /* CredentialSharingHistoryManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialSharingHistoryManagerTest.swift; sourceTree = "<group>"; };
A84AB70C2B50C2DD00E8C88B /* IdTokenSharingHistoryManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdTokenSharingHistoryManagerTest.swift; sourceTree = "<group>"; };
A8509C282B82315D00B28C35 /* RecipientClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientClaims.swift; sourceTree = "<group>"; };
A88779BC2C33DC08002EE9C2 /* WebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTests.swift; sourceTree = "<group>"; };
A89108012B82D1650060DD71 /* RecipientClaimsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientClaimsViewModel.swift; sourceTree = "<group>"; };
A89108032B82D2880060DD71 /* RecipientClaimsPreviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientClaimsPreviewModel.swift; sourceTree = "<group>"; };
A891080B2B84785F0060DD71 /* credential_sharing_history.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = credential_sharing_history.pb.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -663,6 +665,7 @@
8B81E2AC2B33CC4300ED3B4E /* tw2023_walletTests */ = {
isa = PBXGroup;
children = (
A88779B92C33DBDA002EE9C2 /* Feature */,
A8AF22892C12A61E00D6EDA5 /* Helper */,
A84AB6BF2B4F725000E8C88B /* datastore */,
A83039B62B4E4229004139A7 /* Utils */,
Expand Down Expand Up @@ -988,6 +991,30 @@
path = datastore;
sourceTree = "<group>";
};
A88779B92C33DBDA002EE9C2 /* Feature */ = {
isa = PBXGroup;
children = (
A88779BA2C33DBE5002EE9C2 /* ShareCredential */,
);
path = Feature;
sourceTree = "<group>";
};
A88779BA2C33DBE5002EE9C2 /* ShareCredential */ = {
isa = PBXGroup;
children = (
A88779BB2C33DBEE002EE9C2 /* Views */,
);
path = ShareCredential;
sourceTree = "<group>";
};
A88779BB2C33DBEE002EE9C2 /* Views */ = {
isa = PBXGroup;
children = (
A88779BC2C33DC08002EE9C2 /* WebViewTests.swift */,
);
path = Views;
sourceTree = "<group>";
};
A8AF22892C12A61E00D6EDA5 /* Helper */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1583,6 +1610,7 @@
8B43AE382B3A93C60016CF83 /* AuthServerMetadata.swift in Sources */,
A83039C22B4E4229004139A7 /* SerializeUtilTest.swift in Sources */,
8B0E0AB02B403D510080F6A3 /* PresentationExchange.swift in Sources */,
A88779BD2C33DC08002EE9C2 /* WebViewTests.swift in Sources */,
8B297C372B39538500D2998D /* VCIMetadataTests.swift in Sources */,
8BB513942B3BB69A00D4EFB3 /* AsynTestRunner.swift in Sources */,
8BB5138D2B3AD0CC00D4EFB3 /* VCIClient.swift in Sources */,
Expand Down
120 changes: 78 additions & 42 deletions tw2023_wallet/Feature/ShareCredential/Views/RedirectView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ import WebKit
struct WebView: UIViewRepresentable {
let urlString: String
let cookieStrings: [String]
// let cookies: [HTTPCookie]
var onClose: () -> Void
var openURL: (URL, @escaping (Bool) -> Void) -> Void = { url, completion in
UIApplication.shared.open(url, options: [:], completionHandler: completion)
}

func makeUIView(context: Context) -> WKWebView {
return WKWebView()
let webConfiguration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = context.coordinator
return webView
}

func updateUIView(_ webView: WKWebView, context: Context) {
Expand All @@ -25,7 +31,6 @@ struct WebView: UIViewRepresentable {
}

let cookies = cookieStrings.compactMap { cookieString -> HTTPCookie? in
// シンプルな`key=value`形式を想定
let parts = cookieString.split(separator: "=", maxSplits: 1).map(String.init)
guard parts.count == 2 else { return nil }

Expand All @@ -36,19 +41,63 @@ struct WebView: UIViewRepresentable {
.domain: url.host ?? "",
]

// 必要に応じて`Secure`やその他の属性を設定
return HTTPCookie(properties: properties)
}

// クッキーをWebViewのセッションに設定
let dataStore = webView.configuration.websiteDataStore
for cookie in cookies {
dataStore.httpCookieStore.setCookie(cookie)
}

// リクエストを実行
webView.load(URLRequest(url: url))
}

func makeCoordinator() -> Coordinator {
Coordinator(self, onClose: onClose, openURL: openURL)
}

class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
var onClose: () -> Void
var openURL: (URL, @escaping (Bool) -> Void) -> Void

init(
_ parent: WebView, onClose: @escaping () -> Void,
openURL: @escaping (URL, @escaping (Bool) -> Void) -> Void
) {
self.parent = parent
self.onClose = onClose
self.openURL = openURL
}

func webView(
_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
if let url = navigationAction.request.url {
if url.scheme == "openid-credential-offer" {
handleCustomSchemeInWKWebView(url: url)
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}

func handleCustomSchemeInWKWebView(url: URL) {
print("Handling custom scheme URL: \(url)")
openURL(url) { success in
if success {
DispatchQueue.main.async {
self.onClose()
}
}
else {
print("Failed to open URL: \(url)")
}
}
}
}
}

struct RedirectView: View {
Expand All @@ -63,45 +112,32 @@ struct RedirectView: View {
var cookieStrings: [String] = []

var body: some View {
WebView(urlString: urlString, cookieStrings: cookieStrings)
// EmptyView().onAppear {
// openURLInSafari(urlString: urlString)
// }
.alert(isPresented: $showAlert) {
Alert(
title: Text(alertTitle),
message: Text(alertMessage),
dismissButton: .default(Text("OK")) {
presentationMode.wrappedValue.dismiss()
}
)
WebView(
urlString: urlString, cookieStrings: cookieStrings,
onClose: {
self.presentationMode.wrappedValue.dismiss()
},
openURL: { url, completion in
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: completion)
}
else {
print("Cannot open URL: \(url)")
completion(false)
}
}
)
.alert(isPresented: $showAlert) {
Alert(
title: Text(alertTitle),
message: Text(alertMessage),
dismissButton: .default(Text("OK")) {
presentationMode.wrappedValue.dismiss()
}
)
}
}

// func openURLWithCookies(urlString: String, cookieValue: String) {
// guard let url = URL(string: urlString) else {
// print("Invalid URL specified.")
// return
// }
//
// let webView = WKWebView(frame: .zero)
// let cookie = HTTPCookie(properties: [
// .domain: url.host!,
// .path: "/",
// .name: "username_mapping_session",
// .value: cookieValue,
// .secure: "FALSE",
// .expires: NSDate(timeIntervalSinceNow: 3600)
// ])!
//
// webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie) {
// DispatchQueue.main.async {
// webView.load(URLRequest(url: url))
// // WKWebViewを表示するためのUI処理をここに実装
// }
// }
// }

func openURLInSafari(urlString: String) {
if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
Expand Down
150 changes: 150 additions & 0 deletions tw2023_walletTests/Feature/ShareCredential/Views/WebViewTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// RedirectViewTests.swift
// tw2023_walletTests
//
// Created by katsuyoshi ozaki on 2024/07/02.
//

import Foundation


import XCTest
import SwiftUI
import WebKit
@testable import tw2023_wallet



class WebViewTests: XCTestCase {

var webView: WebView!
var webViewController: WKWebView!
var mockCoordinator: MockCoordinator!

override func setUpWithError() throws {
webView = WebView(urlString: "https://example.com", cookieStrings: [], onClose: {})
webViewController = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
mockCoordinator = MockCoordinator(parent: webView, onClose: {})
webViewController.navigationDelegate = mockCoordinator
}

func testWebViewLoadsCorrectURL() {
let expectation = XCTestExpectation(description: "WebView loads correct URL")

guard let url = URL(string: webView.urlString) else {
XCTFail("Invalid URL")
return
}

webViewController.load(URLRequest(url: url))

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
XCTAssertEqual(self.webViewController.url?.absoluteString, "https://example.com/")
expectation.fulfill()
}

wait(for: [expectation], timeout: 2.0)
}

func testWebViewSetsCookies() {
let expectation = XCTestExpectation(description: "WebView sets cookies")

let cookieString = "testCookie=testValue"
webView = WebView(urlString: "https://example.com", cookieStrings: [cookieString], onClose: {})

guard let url = URL(string: webView.urlString) else {
XCTFail("Invalid URL")
return
}

let cookies = webView.cookieStrings.compactMap { cookieString -> HTTPCookie? in
let parts = cookieString.split(separator: "=", maxSplits: 1).map(String.init)
guard parts.count == 2 else { return nil }

let properties: [HTTPCookiePropertyKey: Any] = [
.name: parts[0],
.value: parts[1],
.path: "/",
.domain: url.host ?? "",
]

return HTTPCookie(properties: properties)
}

for cookie in cookies {
webViewController.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}

webViewController.load(URLRequest(url: url))

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.webViewController.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
XCTAssertTrue(cookies.contains { $0.name == "testCookie" && $0.value == "testValue" })
expectation.fulfill()
}
}

wait(for: [expectation], timeout: 2.0)
}

func testHandleCustomScheme() {
let expectation = XCTestExpectation(description: "WebView handles custom scheme and calls onClose")

var onCloseCalled = false
webView = WebView(urlString: "https://example.com", cookieStrings: [], onClose: {
onCloseCalled = true
expectation.fulfill()
})

let mockCoordinator = MockCoordinator(parent: webView, onClose: webView.onClose)
webViewController.navigationDelegate = mockCoordinator

let url = URL(string: "openid-credential-offer://example")!
mockCoordinator.handleCustomSchemeInWKWebView(url: url)

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
XCTAssertTrue(onCloseCalled)
expectation.fulfill()
}

wait(for: [expectation], timeout: 2.0)
}

}

class MockCoordinator: NSObject, WKNavigationDelegate {
var parent: WebView
var onClose: () -> Void

init(parent: WebView, onClose: @escaping () -> Void) {
self.parent = parent
self.onClose = onClose
}

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, url.scheme == "openid-credential-offer" {
handleCustomSchemeInWKWebView(url: url)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}

func handleCustomSchemeInWKWebView(url: URL) {
print("Handling custom scheme URL: \(url)")
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
DispatchQueue.main.async {
self.onClose()
}
} else {
print("Failed to open URL: \(url)")
}
}
} else {
print("Cannot open URL: \(url)")
}
}
}

0 comments on commit 42bce66

Please sign in to comment.