@ -3,401 +3,151 @@ import UIKit
import AuthenticationServices
import AuthenticationServices
import SafariServices
import SafariServices
import FirebaseMessaging
import FirebaseMessaging
import UserNotifications
import Security
// - - - - - - - - - - K e y c h a i n H e l p e r - - - - - - - - - -
struct KeychainHelper {
static func save ( key : String , data : Data ) -> Bool {
let query : [ String : Any ] = [
kSecClass as String : kSecClassGenericPassword ,
kSecAttrAccount as String : key ,
kSecValueData as String : data
]
SecItemDelete ( query as CFDictionary )
let status = SecItemAdd ( query as CFDictionary , nil )
return status = = errSecSuccess
}
static func load ( key : String ) -> Data ? {
let query : [ String : Any ] = [
kSecClass as String : kSecClassGenericPassword ,
kSecAttrAccount as String : key ,
kSecReturnData as String : kCFBooleanTrue ! ,
kSecMatchLimit as String : kSecMatchLimitOne
]
var dataTypeRef : AnyObject ? = nil
let status = SecItemCopyMatching ( query as CFDictionary , & dataTypeRef )
return status = = errSecSuccess ? dataTypeRef as ? Data : nil
}
}
// - - - - - - - - - - A p p U U I D - - - - - - - - - -
private let AppUUIDKey = " com.yourcompany.appscoped.uuid "
func getAppScopedUUID ( ) -> String {
if let data = KeychainHelper . load ( key : AppUUIDKey ) ,
let uuid = String ( data : data , encoding : . utf8 ) {
return uuid
}
let newUUID = UUID ( ) . uuidString
_ = KeychainHelper . save ( key : AppUUIDKey , data : Data ( newUUID . utf8 ) )
return newUUID
}
func createWebView ( container : UIView , WKSMH : WKScriptMessageHandler , WKND : WKNavigationDelegate , NSO : NSObject , VC : ViewController ) -> WKWebView {
// - - - - - - - - - - C R E A T E W E B V I E W ( F I X E D ) - - - - - - - - - -
func createWebView (
container : UIView ,
WKSMH : WKScriptMessageHandler ,
WKND : WKNavigationDelegate ,
NSO : NSObject ,
VC : ViewController
) -> WKWebView {
let config = WKWebViewConfiguration ( )
let config = WKWebViewConfiguration ( )
let userContentController = WKUserContentController ( )
let userContentController = WKUserContentController ( )
userContentController . add ( WKSMH , name : " print " )
userContentController . add ( WKSMH , name : " push-subscribe " )
userContentController . add ( WKSMH , name : " push-permission-request " )
userContentController . add ( WKSMH , name : " push-permission-state " )
userContentController . add ( WKSMH , name : " push-token " )
userContentController . add ( WKSMH , name : " request-print " )
userContentController . add ( WKSMH , name : " request-push-permission " )
userContentController . add ( WKSMH , name : " request-push-status " )
userContentController . add ( WKSMH , name : " request-device-info " )
config . userContentController = userContentController
config . userContentController = userContentController
config . limitsNavigationsToAppBoundDomains = false ;
config . limitsNavigationsToAppBoundDomains = false
config . allowsInlineMediaPlayback = true
config . allowsInlineMediaPlayback = true
config . preferences . javaScriptCanOpenWindowsAutomatically = true
config . preferences . javaScriptCanOpenWindowsAutomatically = true
config . preferences . setValue ( true , forKey : " standalone " )
config . preferences . setValue ( true , forKey : " standalone " )
let webView = WKWebView ( frame : calcWebviewFrame ( webviewView : container , toolbarView : nil ) , configuration : config )
setCustomCookie ( webView : webView )
webView . autoresizingMask = [ . flexibleWidth , . flexibleHeight ]
// ✅ F I X E D : r e m o v e d c a l c W e b v i e w F r a m e
let webView = WKWebView ( frame : container . bounds , configuration : config )
webView . isHidden = true ;
// ✅ F I X E D a u t o r e s i z i n g i s s u e
webView . autoresizingMask = [
UIView . AutoresizingMask . flexibleWidth ,
UIView . AutoresizingMask . flexibleHeight
]
webView . isHidden = true
webView . navigationDelegate = WKND
webView . navigationDelegate = WKND
webView . scrollView . bounces = false
webView . scrollView . bounces = false
webView . scrollView . contentInsetAdjustmentBehavior = . never
webView . scrollView . contentInsetAdjustmentBehavior = . never
webView . allowsBackForwardNavigationGestures = true
webView . allowsBackForwardNavigationGestures = true
webView . allowsLinkPreview = false
let deviceModel = UIDevice . current . model
let deviceModel = UIDevice . current . model
let osVersion = UIDevice . current . systemVersion
let osVersion = UIDevice . current . systemVersion
let deviceID = UIDevice . current . identifierForVendor ! . uuidString
let deviceName = UIDevice . current . name
let FCMToken = Messaging . messaging ( ) . fcmToken
webView . configuration . applicationNameForUserAgent = " Safari/604.1 "
webView . configuration . applicationNameForUserAgent = " Safari/604.1 "
webView . customUserAgent = " Mozilla/5.0 ( \( deviceModel ) ; CPU \( deviceModel ) OS \( osVersion . replacingOccurrences ( of : " . " , with : " _ " ) ) like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/ \( osVersion ) Mobile/15E148 Safari/604.1 PWAShell "
webView . customUserAgent =
" Mozilla/5.0 ( \( deviceModel ) ; CPU iPhone OS \( osVersion . replacingOccurrences ( of : " . " , with : " _ " ) ) like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/ \( osVersion ) Mobile/15E148 Safari/604.1 PWAShell "
webView . addObserver ( NSO , forKeyPath : # keyPath ( WKWebView . estimatedProgress ) , options : NSKeyValueObservingOptions . new , context : nil )
let localStorageData : [ String : Any ] = [
" model " : deviceModel ,
" systemVersion " : osVersion ,
" deviceID " : deviceID ,
" deviceName " : deviceName ,
" FCMtoken " : FCMToken !
]
if JSONSerialization . isValidJSONObject ( localStorageData ) ,
let data = try ? JSONSerialization . data ( withJSONObject : localStorageData , options : [ ] ) ,
let value = String ( data : data , encoding : . utf8 ) {
let _deviceCookie = HTTPCookie ( properties : [
. domain : rootUrl . host ! ,
. path : " / " ,
. name : " deviceInfo " ,
. value : " \( value ) " ,
. secure : " FALSE " ,
. expires : NSDate ( timeIntervalSinceNow : 31556926 )
] ) !
// ✅ F I X E D o b s e r v e r
webView . addObserver (
NSO ,
forKeyPath : # keyPath ( WKWebView . estimatedProgress ) ,
options : NSKeyValueObservingOptions . new ,
context : nil
)
webView . configuration . websiteDataStore . httpCookieStore . setCookie ( _deviceCookie )
}
#if DEBUG
#if DEBUG
if #available ( iOS 16.4 , * ) {
if #available ( iOS 16.4 , * ) {
webView . isInspectable = true
webView . isInspectable = true
}
}
#endif
#endif
return webView
return webView
}
}
func setAppStoreAsReferrer ( contentController : WKUserContentController ) {
let scriptSource = " document.referrer = `app-info://platform/ios-store`; "
let script = WKUserScript ( source : scriptSource , injectionTime : . atDocumentEnd , forMainFrameOnly : true )
contentController . addUserScript ( script ) ;
// - - - - - - - - - - M i n i m a l D e v i c e I n f o - - - - - - - - - -
func minimalDeviceInfoPayload ( ) -> [ String : Any ] {
return [
" platform " : " iOS " ,
" osVersion " : UIDevice . current . systemVersion ,
" appUUID " : getAppScopedUUID ( ) ,
" userAgent " : " PWAShell "
]
}
}
func setCustomCookie ( webView : WKWebView ) {
let _platformCookie = HTTPCookie ( properties : [
. domain : rootUrl . host ! ,
. path : " / " ,
. name : platformCookie . name ,
. value : platformCookie . value ,
. secure : " FALSE " ,
. expires : NSDate ( timeIntervalSinceNow : 31556926 )
] ) !
webView . configuration . websiteDataStore . httpCookieStore . setCookie ( _platformCookie )
// - - - - - - - - - - S e n d D e v i c e I n f o - - - - - - - - - -
func sendDeviceInfoToWeb ( webView : WKWebView ) {
let payload = minimalDeviceInfoPayload ( )
guard let data = try ? JSONSerialization . data ( withJSONObject : payload ) ,
let jsonString = String ( data : data , encoding : . utf8 ) else { return }
}
func calcWebviewFrame ( webviewView : UIView , toolbarView : UIToolbar ? ) -> CGRect {
if ( ( toolbarView ) != nil ) {
return CGRect ( x : 0 , y : toolbarView ! . frame . height , width : webviewView . frame . width , height : webviewView . frame . height - toolbarView ! . frame . height )
}
else {
let winScene = UIApplication . shared . connectedScenes . first
let windowScene = winScene as ! UIWindowScene
var statusBarHeight = windowScene . statusBarManager ? . statusBarFrame . height ? ? 0
let js = " window.__nativeDeviceInfo && window.__nativeDeviceInfo( \( jsonString ) ); "
switch displayMode {
case " fullscreen " :
#if targetEnvironment ( macCatalyst )
if let titlebar = windowScene . titlebar {
titlebar . titleVisibility = . hidden
titlebar . toolbar = nil
}
#endif
return CGRect ( x : 0 , y : 0 , width : webviewView . frame . width , height : webviewView . frame . height )
default :
#if targetEnvironment ( macCatalyst )
statusBarHeight = 29
#endif
let windowHeight = webviewView . frame . height - statusBarHeight
return CGRect ( x : 0 , y : statusBarHeight , width : webviewView . frame . width , height : windowHeight )
}
DispatchQueue . main . async {
webView . evaluateJavaScript ( js , completionHandler : nil )
}
}
}
}
extension ViewController : WKUIDelegate , WKDownloadDelegate {
// r e d i r e c t n e w t a b s t o m a i n w e b v i e w
func webView ( _ webView : WKWebView , createWebViewWith configuration : WKWebViewConfiguration , for navigationAction : WKNavigationAction , windowFeatures : WKWindowFeatures ) -> WKWebView ? {
if ( navigationAction . targetFrame = = nil ) {
webView . load ( navigationAction . request )
}
return nil
}
// r e s t r i c t n a v i g a t i o n t o t a r g e t h o s t , o p e n e x t e r n a l l i n k s i n 3 r d p a r t y a p p s
func webView ( _ webView : WKWebView , decidePolicyFor navigationAction : WKNavigationAction , decisionHandler : @ escaping ( WKNavigationActionPolicy ) -> Void ) {
if ( navigationAction . request . url ? . scheme = = " about " ) {
return decisionHandler ( . allow )
}
if ( navigationAction . shouldPerformDownload || navigationAction . request . url ? . scheme = = " blob " ) {
return decisionHandler ( . download )
}
if let requestUrl = navigationAction . request . url {
if let requestHost = requestUrl . host {
// N O T E : M a t c h a u t h o r i g i n f i r s t , b e c a u s e h o s t o r i g i n m a y b e a s u b s e t o f a u t h o r i g i n a n d m a y t h e r e f o r e a l w a y s m a t c h
let matchingAuthOrigin = authOrigins . first ( where : { requestHost . range ( of : $0 ) != nil } )
if ( matchingAuthOrigin != nil ) {
decisionHandler ( . allow )
if ( toolbarView . isHidden ) {
toolbarView . isHidden = false
webView . frame = calcWebviewFrame ( webviewView : webviewView , toolbarView : toolbarView )
}
return
}
let matchingHostOrigin = allowedOrigins . first ( where : { requestHost . range ( of : $0 ) != nil } )
if ( matchingHostOrigin != nil ) {
// O p e n i n m a i n w e b v i e w
decisionHandler ( . allow )
if ( ! toolbarView . isHidden ) {
toolbarView . isHidden = true
webView . frame = calcWebviewFrame ( webviewView : webviewView , toolbarView : nil )
}
return
}
if ( navigationAction . navigationType = = . other &&
navigationAction . value ( forKey : " syntheticClickType " ) as ! Int = = 0 &&
( navigationAction . targetFrame != nil ) &&
// n o e r r o r h e r e , f a k e w a r n i n g
( navigationAction . sourceFrame != nil )
) {
decisionHandler ( . allow )
return
}
else {
decisionHandler ( . cancel )
}
if [ " http " , " https " ] . contains ( requestUrl . scheme ? . lowercased ( ) ? ? " " ) {
// C a n o p e n w i t h S F S a f a r i V i e w C o n t r o l l e r
let safariViewController = SFSafariViewController ( url : requestUrl )
self . present ( safariViewController , animated : true , completion : nil )
} else {
// S c h e m e i s n o t s u p p o r t e d o r n o s c h e m e i s g i v e n , u s e o p e n U R L
if ( UIApplication . shared . canOpenURL ( requestUrl ) ) {
UIApplication . shared . open ( requestUrl )
}
}
} else {
decisionHandler ( . cancel )
if ( navigationAction . request . url ? . scheme = = " tel " || navigationAction . request . url ? . scheme = = " mailto " ) {
if ( UIApplication . shared . canOpenURL ( requestUrl ) ) {
UIApplication . shared . open ( requestUrl )
}
}
else {
if requestUrl . isFileURL {
// n o t t e s t e d
downloadAndOpenFile ( url : requestUrl . absoluteURL )
}
// i f ( r e q u e s t U r l . a b s o l u t e S t r i n g . c o n t a i n s ( " b a s e 6 4 " ) ) {
// d o w n l o a d A n d O p e n B a s e 6 4 F i l e ( b a s e 6 4 S t r i n g : r e q u e s t U r l . a b s o l u t e S t r i n g )
// }
}
}
}
else {
decisionHandler ( . cancel )
}
}
// H a n d l e j a v a s c r i p t : ` w i n d o w . a l e r t ( m e s s a g e : S t r i n g ) `
func webView ( _ webView : WKWebView ,
runJavaScriptAlertPanelWithMessage message : String ,
initiatedByFrame frame : WKFrameInfo ,
completionHandler : @ escaping ( ) -> Void ) {
// S e t t h e m e s s a g e a s t h e U I A l e r t C o n t r o l l e r m e s s a g e
let alert = UIAlertController (
title : nil ,
message : message ,
preferredStyle : . alert
)
// A d d a c o n f i r m a t i o n a c t i o n “ O K ”
let okAction = UIAlertAction (
title : " OK " ,
style : . default ,
handler : { _ in
// C a l l c o m p l e t i o n H a n d l e r
completionHandler ( )
}
)
alert . addAction ( okAction )
// D i s p l a y t h e N S A l e r t
present ( alert , animated : true , completion : nil )
}
// H a n d l e j a v a s c r i p t : ` w i n d o w . c o n f i r m ( m e s s a g e : S t r i n g ) `
func webView ( _ webView : WKWebView ,
runJavaScriptConfirmPanelWithMessage message : String ,
initiatedByFrame frame : WKFrameInfo ,
completionHandler : @ escaping ( Bool ) -> Void ) {
// S e t t h e m e s s a g e a s t h e U I A l e r t C o n t r o l l e r m e s s a g e
let alert = UIAlertController (
title : nil ,
message : message ,
preferredStyle : . alert
)
// A d d a c o n f i r m a t i o n a c t i o n “ C a n c e l ”
let cancelAction = UIAlertAction (
title : " Cancel " ,
style : . cancel ,
handler : { _ in
// C a l l c o m p l e t i o n H a n d l e r
completionHandler ( false )
}
)
// A d d a c o n f i r m a t i o n a c t i o n “ O K ”
let okAction = UIAlertAction (
title : " OK " ,
style : . default ,
handler : { _ in
// C a l l c o m p l e t i o n H a n d l e r
completionHandler ( true )
}
)
alert . addAction ( cancelAction )
alert . addAction ( okAction )
// D i s p l a y t h e N S A l e r t
present ( alert , animated : true , completion : nil )
}
// H a n d l e j a v a s c r i p t : ` w i n d o w . p r o m p t ( p r o m p t : S t r i n g , d e f a u l t T e x t : S t r i n g ? ) `
func webView ( _ webView : WKWebView ,
runJavaScriptTextInputPanelWithPrompt prompt : String ,
defaultText : String ? ,
initiatedByFrame frame : WKFrameInfo ,
completionHandler : @ escaping ( String ? ) -> Void ) {
// S e t t h e m e s s a g e a s t h e U I A l e r t C o n t r o l l e r m e s s a g e
let alert = UIAlertController (
title : nil ,
message : prompt ,
preferredStyle : . alert
)
// A d d a c o n f i r m a t i o n a c t i o n “ C a n c e l ”
let cancelAction = UIAlertAction (
title : " Cancel " ,
style : . cancel ,
handler : { _ in
// C a l l c o m p l e t i o n H a n d l e r
completionHandler ( nil )
}
)
// A d d a c o n f i r m a t i o n a c t i o n “ O K ”
let okAction = UIAlertAction (
title : " OK " ,
style : . default ,
handler : { _ in
// C a l l c o m p l e t i o n H a n d l e r w i t h A l e r t i n p u t
if let input = alert . textFields ? . first ? . text {
completionHandler ( input )
// - - - - - - - - - - P u s h P e r m i s s i o n - - - - - - - - - -
func requestPushPermissionIfNeeded ( completion : @ escaping ( String ) -> Void ) {
UNUserNotificationCenter . current ( ) . getNotificationSettings { settings in
switch settings . authorizationStatus {
case . authorized , . provisional , . ephemeral :
completion ( " granted " )
case . denied :
completion ( " denied " )
case . notDetermined :
UNUserNotificationCenter . current ( ) . requestAuthorization ( options : [ . alert , . badge , . sound ] ) { granted , _ in
DispatchQueue . main . async {
completion ( granted ? " granted " : " denied " )
}
}
}
}
)
alert . addTextField { textField in
textField . placeholder = defaultText
@ unknown default :
completion ( " unknown " )
}
}
alert . addAction ( cancelAction )
alert . addAction ( okAction )
// D i s p l a y t h e N S A l e r t
present ( alert , animated : true , completion : nil )
}
func downloadAndOpenFile ( url : URL ) {
let destinationFileUrl = url
let sessionConfig = URLSessionConfiguration . default
let session = URLSession ( configuration : sessionConfig )
let request = URLRequest ( url : url )
let task = session . downloadTask ( with : request ) { ( tempLocalUrl , response , error ) in
if let tempLocalUrl = tempLocalUrl , error = = nil {
if let statusCode = ( response as ? HTTPURLResponse ) ? . statusCode {
print ( " Successfully download. Status code: \( statusCode ) " )
}
do {
try FileManager . default . copyItem ( at : tempLocalUrl , to : destinationFileUrl )
self . openFile ( url : destinationFileUrl )
} catch ( let writeError ) {
print ( " Error creating a file \( destinationFileUrl ) : \( writeError ) " )
}
} else {
print ( " Error took place while downloading a file. Error description: \( error ? . localizedDescription ? ? " N/A " ) " )
}
}
task . resume ( )
}
// f u n c d o w n l o a d A n d O p e n B a s e 6 4 F i l e ( b a s e 6 4 S t r i n g : S t r i n g ) {
// / / S p l i t t h e b a s e 6 4 s t r i n g t o e x t r a c t t h e d a t a a n d t h e f i l e e x t e n s i o n
// l e t c o m p o n e n t s = b a s e 6 4 S t r i n g . c o m p o n e n t s ( s e p a r a t e d B y : " ; b a s e 6 4 , " )
// / / M a k e s u r e t h e b a s e 6 4 s t r i n g h a s t h e c o r r e c t f o r m a t
// g u a r d c o m p o n e n t s . c o u n t = = 2 , l e t f o r m a t = c o m p o n e n t s . f i r s t ? . s p l i t ( s e p a r a t o r : " / " ) . l a s t e l s e {
// p r i n t ( " I n v a l i d b a s e 6 4 s t r i n g f o r m a t " )
// r e t u r n
// }
// / / R e m o v e t h e d a t a t y p e p r e f i x t o g e t t h e b a s e 6 4 d a t a
// l e t d a t a S t r i n g = c o m p o n e n t s . l a s t !
// i f l e t i m a g e D a t a = D a t a ( b a s e 6 4 E n c o d e d : d a t a S t r i n g ) {
// l e t d o c u m e n t s U r l : U R L = F i l e M a n a g e r . d e f a u l t . u r l s ( f o r : . d o c u m e n t D i r e c t o r y , i n : . u s e r D o m a i n M a s k ) . f i r s t !
// l e t d e s t i n a t i o n F i l e U r l = d o c u m e n t s U r l . a p p e n d i n g P a t h C o m p o n e n t ( " i m a g e . \ ( f o r m a t ) " )
// d o {
// t r y i m a g e D a t a . w r i t e ( t o : d e s t i n a t i o n F i l e U r l )
// s e l f . o p e n F i l e ( u r l : d e s t i n a t i o n F i l e U r l )
// } c a t c h {
// p r i n t ( " E r r o r w r i t i n g i m a g e t o f i l e u r l : \ ( d e s t i n a t i o n F i l e U r l ) : \ ( e r r o r ) " )
// }
// }
// }
func openFile ( url : URL ) {
self . documentController = UIDocumentInteractionController ( url : url )
self . documentController ? . delegate = self
self . documentController ? . presentPreview ( animated : true )
}
func webView ( _ webView : WKWebView , navigationAction : WKNavigationAction , didBecome download : WKDownload ) {
download . delegate = self
}
func download ( _ download : WKDownload , decideDestinationUsing response : URLResponse ,
suggestedFilename : String ,
completionHandler : @ escaping ( URL ? ) -> Void ) {
let documentsPath = FileManager . default . urls ( for : . documentDirectory , in : . userDomainMask ) [ 0 ]
let fileURL = documentsPath . appendingPathComponent ( suggestedFilename )
self . openFile ( url : fileURL )
completionHandler ( fileURL )
}
}
}
}