Browse Source

Refactor app flows and clean up state handling

main
Akhilesh 1 month ago
parent
commit
ee29cebab3
17 changed files with 2535 additions and 830 deletions
  1. BIN
      .DS_Store
  2. BIN
      src/.DS_Store
  3. +20
    -0
      src/AppManager.swift
  4. +151
    -0
      src/Nivesh 2026-05-11 12-04-41/DistributionSummary.plist
  5. +18
    -0
      src/Nivesh 2026-05-11 12-04-41/ExportOptions.plist
  6. +1684
    -0
      src/Nivesh 2026-05-11 12-04-41/Packaging.log
  7. +6
    -2
      src/Nivesh.xcodeproj/project.pbxproj
  8. +7
    -7
      src/Nivesh.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  9. +10
    -0
      src/Nivesh.xcodeproj/xcshareddata/xcschemes/Nivesh.xcscheme
  10. BIN
      src/Nivesh/.DS_Store
  11. +85
    -151
      src/Nivesh/AppDelegate.swift
  12. BIN
      src/Nivesh/Assets.xcassets/.DS_Store
  13. +4
    -6
      src/Nivesh/Info.plist
  14. +176
    -173
      src/Nivesh/PushNotifications.swift
  15. +13
    -2
      src/Nivesh/SceneDelegate.swift
  16. +257
    -135
      src/Nivesh/ViewController.swift
  17. +104
    -354
      src/Nivesh/WebView.swift

BIN
.DS_Store View File


BIN
src/.DS_Store View File


+ 20
- 0
src/AppManager.swift View File

@ -0,0 +1,20 @@
import FirebaseMessaging
class FCMTokenManager {
static let shared = FCMTokenManager()
var fcmToken: String?
private init() {} // Prevents external instantiation
func fetchToken() {
Messaging.messaging().token { token, error in
if let error = error {
#if DEBUG
print("Error fetching FCM token: \(error.localizedDescription)")
#endif
} else if let token = token {
self.fcmToken = token // Store globally in the singleton
}
}
}
}

+ 151
- 0
src/Nivesh 2026-05-11 12-04-41/DistributionSummary.plist View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Nivesh.ipa</key>
<array>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>buildNumber</key>
<string>5</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>B9C389DA51584C4FFC3DABB6E28FA51588CEF558</string>
<key>dateExpires</key>
<string>19/01/27</string>
<key>type</key>
<string>Cloud Managed Apple Distribution</string>
</dict>
<key>embeddedBinaries</key>
<array>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>buildNumber</key>
<string>1</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>B9C389DA51584C4FFC3DABB6E28FA51588CEF558</string>
<key>dateExpires</key>
<string>19/01/27</string>
<key>type</key>
<string>Cloud Managed Apple Distribution</string>
</dict>
<key>name</key>
<string>FirebaseAnalytics.framework</string>
<key>team</key>
<dict>
<key>id</key>
<string>892H6H36S9</string>
<key>name</key>
<string>Providential Platforms Private Limited</string>
</dict>
<key>versionNumber</key>
<string>11.7.0</string>
</dict>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>buildNumber</key>
<string>1</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>B9C389DA51584C4FFC3DABB6E28FA51588CEF558</string>
<key>dateExpires</key>
<string>19/01/27</string>
<key>type</key>
<string>Cloud Managed Apple Distribution</string>
</dict>
<key>name</key>
<string>GoogleAppMeasurement.framework</string>
<key>team</key>
<dict>
<key>id</key>
<string>892H6H36S9</string>
<key>name</key>
<string>Providential Platforms Private Limited</string>
</dict>
<key>versionNumber</key>
<string>11.7.0</string>
</dict>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>buildNumber</key>
<string>1</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>B9C389DA51584C4FFC3DABB6E28FA51588CEF558</string>
<key>dateExpires</key>
<string>19/01/27</string>
<key>type</key>
<string>Cloud Managed Apple Distribution</string>
</dict>
<key>name</key>
<string>GoogleAppMeasurementIdentitySupport.framework</string>
<key>team</key>
<dict>
<key>id</key>
<string>892H6H36S9</string>
<key>name</key>
<string>Providential Platforms Private Limited</string>
</dict>
<key>versionNumber</key>
<string>11.7.0</string>
</dict>
</array>
<key>entitlements</key>
<dict>
<key>application-identifier</key>
<string>892H6H36S9.com.nivesh.app</string>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:app.nivesh.com</string>
<string>webcredentials:app.nivesh.com</string>
<string>nivesh.app.link</string>
<string>nivesh-alternate.app.link</string>
</array>
<key>com.apple.developer.team-identifier</key>
<string>892H6H36S9</string>
<key>get-task-allow</key>
<false/>
</dict>
<key>name</key>
<string>Nivesh.app</string>
<key>profile</key>
<dict>
<key>UUID</key>
<string>b7ad68a5-d036-47cc-8920-f5f91c05742d</string>
<key>dateExpires</key>
<string>19/01/27</string>
<key>name</key>
<string>iOS Team Ad Hoc Provisioning Profile: com.nivesh.app</string>
</dict>
<key>team</key>
<dict>
<key>id</key>
<string>892H6H36S9</string>
<key>name</key>
<string>Providential Platforms Private Limited</string>
</dict>
<key>versionNumber</key>
<string>1.3</string>
</dict>
</array>
</dict>
</plist>

+ 18
- 0
src/Nivesh 2026-05-11 12-04-41/ExportOptions.plist View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>release-testing</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>892H6H36S9</string>
<key>thinning</key>
<string>&lt;none&gt;</string>
</dict>
</plist>

+ 1684
- 0
src/Nivesh 2026-05-11 12-04-41/Packaging.log
File diff suppressed because it is too large
View File


+ 6
- 2
src/Nivesh.xcodeproj/project.pbxproj View File

@ -21,6 +21,7 @@
1EBE678A2D5495A900F6C6D0 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EBE67892D5495A900F6C6D0 /* SystemConfiguration.framework */; };
1EBE678C2D5495B900F6C6D0 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EBE678B2D5495B900F6C6D0 /* WebKit.framework */; };
1EBE678E2D5495D000F6C6D0 /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EBE678D2D5495D000F6C6D0 /* CoreSpotlight.framework */; };
1EBE67982D5C782000F6C6D0 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBE67972D5C780100F6C6D0 /* AppManager.swift */; };
595F23A525CEFBFE0053416C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595F239A25CEFBFD0053416C /* Settings.swift */; };
595F23A625CEFBFE0053416C /* Entitlements in Resources */ = {isa = PBXBuildFile; fileRef = 595F239B25CEFBFD0053416C /* Entitlements */; };
595F23A725CEFBFE0053416C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 595F239C25CEFBFD0053416C /* Assets.xcassets */; };
@ -41,6 +42,7 @@
1EBE67892D5495A900F6C6D0 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
1EBE678B2D5495B900F6C6D0 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
1EBE678D2D5495D000F6C6D0 /* CoreSpotlight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreSpotlight.framework; path = System/Library/Frameworks/CoreSpotlight.framework; sourceTree = SDKROOT; };
1EBE67972D5C780100F6C6D0 /* AppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = "<group>"; };
59333BAA25CFF706003392A4 /* Nivesh.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nivesh.app; sourceTree = BUILT_PRODUCTS_DIR; };
595F239A25CEFBFD0053416C /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Settings.swift; path = Nivesh/Settings.swift; sourceTree = "<group>"; };
595F239B25CEFBFD0053416C /* Entitlements */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Entitlements; path = Nivesh/Entitlements; sourceTree = "<group>"; };
@ -103,6 +105,7 @@
children = (
1EBE67822D54865300F6C6D0 /* NiveshRelease.entitlements */,
595F23A225CEFBFE0053416C /* AppDelegate.swift */,
1EBE67972D5C780100F6C6D0 /* AppManager.swift */,
595F239C25CEFBFD0053416C /* Assets.xcassets */,
595F239B25CEFBFD0053416C /* Entitlements */,
595F23A025CEFBFE0053416C /* Info.plist */,
@ -205,6 +208,7 @@
595F23A525CEFBFE0053416C /* Settings.swift in Sources */,
595F23AE25CEFBFE0053416C /* Printer.swift in Sources */,
595F23AC25CEFBFE0053416C /* ViewController.swift in Sources */,
1EBE67982D5C782000F6C6D0 /* AppManager.swift in Sources */,
595F23AA25CEFBFE0053416C /* PushNotifications.swift in Sources */,
595F23AF25CEFBFE0053416C /* WebView.swift in Sources */,
);
@ -362,7 +366,7 @@
CODE_SIGN_ENTITLEMENTS = Nivesh/Entitlements/Entitlements.plist;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 892H6H36S9;
INFOPLIST_FILE = Nivesh/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
@ -393,7 +397,7 @@
CODE_SIGN_ENTITLEMENTS = Nivesh/NiveshRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 892H6H36S9;
INFOPLIST_FILE = Nivesh/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";


+ 7
- 7
src/Nivesh.xcodeproj/project.xcworkspace/contents.xcworkspacedata View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Nivesh.xcodeproj">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

+ 10
- 0
src/Nivesh.xcodeproj/xcshareddata/xcschemes/Nivesh.xcscheme View File

@ -54,6 +54,16 @@
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "WebKitDebugModeEnabled"
value = "YES"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "WebKitLoggingEnabled"
value = "YES"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>


BIN
src/Nivesh/.DS_Store View File


+ 85
- 151
src/Nivesh/AppDelegate.swift View File

@ -1,151 +1,85 @@
import UIKit
import FirebaseCore
import FirebaseMessaging
import UserNotifications
import BranchSDK
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window : UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// TODO: if we're using Firebase, uncomment next string
FirebaseApp.configure()
// [START set_messaging_delegate]
Messaging.messaging().delegate = self
// [END set_messaging_delegate]
// Register for remote notifications. This shows a permission dialog on first run, to
// show the dialog at a more appropriate time move this registration accordingly.
// [START register_for_notifications]
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: {_, _ in })
// TODO: if we're using Firebase, uncomment next string
application.registerForRemoteNotifications()
// [END register_for_notifications]
// This version of `initSession` includes the source UIScene in the callback
BranchScene.shared().initSession(launchOptions: launchOptions, registerDeepLinkHandler: { (params, error, scene) in
})
return true
}
// [START receive_message]
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID 1: \(messageID)")
}
// Print full message.
print("push userInfo 1:", userInfo)
sendPushToWebView(userInfo: userInfo)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID 2: \(messageID)")
}
// Print full message. **
print("push userInfo 2:", userInfo)
sendPushToWebView(userInfo: userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
// [END receive_message]
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Unable to register for remote notifications: \(error.localizedDescription)")
}
// This function is added here only for debugging purposes, and can be removed if swizzling is enabled.
// If swizzling is disabled then this function must be implemented so that the APNs token can be paired to
// the FCM registration token.
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
print("APNs token retrieved: \(deviceToken)")
// With swizzling disabled you must set the APNs token here.
Messaging.messaging().apnsToken = deviceToken
}
}
// [START ios_10_message_handling]
extension AppDelegate : UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: 3 \(messageID)")
}
// Print full message.
print("push userInfo 3:", userInfo)
sendPushToWebView(userInfo: userInfo)
// Change this to your preferred presentation option
completionHandler([[.banner, .list, .sound]])
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID 4: \(messageID)")
}
// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print full message.
print("push userInfo 4:", userInfo)
sendPushToWebView(userInfo: userInfo)
completionHandler()
}
}
// [END ios_10_message_handling]
extension AppDelegate : MessagingDelegate {
// [START refresh_token]
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
let dataDict:[String: String] = ["token": fcmToken ?? ""]
NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
handleFCMToken()
// TODO: If necessary send token to application server.
// Note: This callback is fired at each app startup and whenever a new token is generated.
}
// [END refresh_token]
}
import UIKit
import FirebaseCore
import FirebaseMessaging
import UserNotifications
import BranchSDK
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let gcmMessageIDKey = "gcm.message_id"
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self // critical line
registerForPushNotifications()
#if DEBUG
print("Requesting notification permission...")
UNUserNotificationCenter.current().getNotificationSettings { settings in
print("Notification settings: \(settings)")
}
#endif
// Branch SDK setup
BranchScene.shared().initSession(launchOptions: launchOptions, registerDeepLinkHandler: { (params, error, scene) in
})
return true
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
#if DEBUG
print("❌ Unable to register for remote notifications: \(error.localizedDescription)")
#endif
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
}
}
// MARK: - Push Registration
func registerForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
} else {
#if DEBUG
print("Notification permission denied: \(error?.localizedDescription ?? "No error")")
#endif
}
}
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([[.banner, .list, .sound]])
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
completionHandler()
}
}
// MARK: - Firebase Messaging Delegate
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
UserDefaults.standard.set(fcmToken, forKey: "FCMToken")
NotificationCenter.default.post(name: Notification.Name("FCMToken"),
object: nil,
userInfo: ["token": fcmToken ?? ""])
}
}

BIN
src/Nivesh/Assets.xcassets/.DS_Store View File


+ 4
- 6
src/Nivesh/Info.plist View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSFaceIDUsageDescription</key>
<string>Unlock Nivesh using Face ID</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
@ -31,7 +33,7 @@
<string>com.nivesh.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>app</string>
<string>com.nivesh.app</string>
</array>
</dict>
</array>
@ -43,11 +45,6 @@
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>App needs to capture user document for KYC mandated by Indian regulation (SEBI)</string>
<key>NSLocationWhenInUseUsageDescription</key>
@ -77,6 +74,7 @@
<array>
<string>processing</string>
<string>remote-notification</string>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>


+ 176
- 173
src/Nivesh/PushNotifications.swift View File

@ -1,173 +1,176 @@
import WebKit
import FirebaseMessaging
class SubscribeMessage {
var topic = ""
var eventValue = ""
var unsubscribe = false
struct Keys {
static var TOPIC = "topic"
static var UNSUBSCRIBE = "unsubscribe"
static var EVENTVALUE = "eventValue"
}
convenience init(dict: Dictionary<String,Any>) {
self.init()
if let topic = dict[Keys.TOPIC] as? String {
self.topic = topic
}
if let unsubscribe = dict[Keys.UNSUBSCRIBE] as? Bool {
self.unsubscribe = unsubscribe
}
if let eventValue = dict[Keys.EVENTVALUE] as? String {
self.eventValue = eventValue
}
}
}
func handleSubscribeTouch(message: WKScriptMessage) {
// [START subscribe_topic]
let subscribeMessages = parseSubscribeMessage(message: message)
if (subscribeMessages.count > 0){
let _message = subscribeMessages[0]
if (_message.unsubscribe) {
Messaging.messaging().unsubscribe(fromTopic: _message.topic) { error in }
}
else {
Messaging.messaging().subscribe(toTopic: _message.topic) { error in }
}
}
// [END subscribe_topic]
}
func parseSubscribeMessage(message: WKScriptMessage) -> [SubscribeMessage] {
var subscribeMessages = [SubscribeMessage]()
if let objStr = message.body as? String {
let data: Data = objStr.data(using: .utf8)!
do {
let jsObj = try JSONSerialization.jsonObject(with: data, options: .init(rawValue: 0))
if let jsonObjDict = jsObj as? Dictionary<String, Any> {
let subscribeMessage = SubscribeMessage(dict: jsonObjDict)
subscribeMessages.append(subscribeMessage)
} else if let jsonArr = jsObj as? [Dictionary<String, Any>] {
for jsonObj in jsonArr {
let sMessage = SubscribeMessage(dict: jsonObj)
subscribeMessages.append(sMessage)
}
}
} catch _ {
}
}
return subscribeMessages
}
func returnPermissionResult(isGranted: Bool){
DispatchQueue.main.async(execute: {
if (isGranted){
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-request', { detail: 'granted' }))")
}
else {
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-request', { detail: 'denied' }))")
}
})
}
func returnPermissionState(state: String){
DispatchQueue.main.async(execute: {
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-state', { detail: '\(state)' }))")
})
}
func handlePushPermission() {
UNUserNotificationCenter.current().getNotificationSettings () { settings in
switch settings.authorizationStatus {
case .notDetermined:
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: { (success, error) in
if error == nil {
if success == true {
returnPermissionResult(isGranted: true)
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
else {
returnPermissionResult(isGranted: false)
}
}
else {
returnPermissionResult(isGranted: false)
}
}
)
case .denied:
returnPermissionResult(isGranted: false)
case .authorized, .ephemeral, .provisional:
returnPermissionResult(isGranted: true)
@unknown default:
return;
}
}
}
func handlePushState() {
UNUserNotificationCenter.current().getNotificationSettings () { settings in
switch settings.authorizationStatus {
case .notDetermined:
returnPermissionState(state: "notDetermined")
case .denied:
returnPermissionState(state: "denied")
case .authorized:
returnPermissionState(state: "authorized")
case .ephemeral:
returnPermissionState(state: "ephemeral")
case .provisional:
returnPermissionState(state: "provisional")
@unknown default:
returnPermissionState(state: "unknown")
return;
}
}
}
func checkViewAndEvaluate(event: String, detail: String) {
if (!Nivesh.webView.isHidden && !Nivesh.webView.isLoading ) {
DispatchQueue.main.async(execute: {
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('\(event)', { detail: \(detail) }))")
})
}
else {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
checkViewAndEvaluate(event: event, detail: detail)
}
}
}
func handleFCMToken(){
DispatchQueue.main.async(execute: {
Messaging.messaging().token { token, error in
if let error = error {
print("Error fetching FCM registration token: \(error)")
checkViewAndEvaluate(event: "push-token", detail: "ERROR GET TOKEN")
} else if let token = token {
print("FCM registration token: \(token)")
checkViewAndEvaluate(event: "push-token", detail: "'\(token)'")
}
}
})
}
func sendPushToWebView(userInfo: [AnyHashable: Any]){
var json = "";
do {
let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
json = String(data: jsonData, encoding: .utf8)!
} catch {
print("ERROR: userInfo parsing problem")
return
}
checkViewAndEvaluate(event: "push-notification", detail: json)
}
import WebKit
import FirebaseMessaging
class SubscribeMessage {
var topic = ""
var eventValue = ""
var unsubscribe = false
struct Keys {
static var TOPIC = "topic"
static var UNSUBSCRIBE = "unsubscribe"
static var EVENTVALUE = "eventValue"
}
convenience init(dict: Dictionary<String,Any>) {
self.init()
if let topic = dict[Keys.TOPIC] as? String {
self.topic = topic
}
if let unsubscribe = dict[Keys.UNSUBSCRIBE] as? Bool {
self.unsubscribe = unsubscribe
}
if let eventValue = dict[Keys.EVENTVALUE] as? String {
self.eventValue = eventValue
}
}
}
func handleSubscribeTouch(message: WKScriptMessage) {
// [START subscribe_topic]
let subscribeMessages = parseSubscribeMessage(message: message)
if (subscribeMessages.count > 0){
let _message = subscribeMessages[0]
if (_message.unsubscribe) {
Messaging.messaging().unsubscribe(fromTopic: _message.topic) { error in }
}
else {
Messaging.messaging().subscribe(toTopic: _message.topic) { error in }
}
}
// [END subscribe_topic]
}
func parseSubscribeMessage(message: WKScriptMessage) -> [SubscribeMessage] {
var subscribeMessages = [SubscribeMessage]()
if let objStr = message.body as? String {
let data: Data = objStr.data(using: .utf8)!
do {
let jsObj = try JSONSerialization.jsonObject(with: data, options: .init(rawValue: 0))
if let jsonObjDict = jsObj as? Dictionary<String, Any> {
let subscribeMessage = SubscribeMessage(dict: jsonObjDict)
subscribeMessages.append(subscribeMessage)
} else if let jsonArr = jsObj as? [Dictionary<String, Any>] {
for jsonObj in jsonArr {
let sMessage = SubscribeMessage(dict: jsonObj)
subscribeMessages.append(sMessage)
}
}
} catch _ {
}
}
return subscribeMessages
}
func returnPermissionResult(isGranted: Bool){
DispatchQueue.main.async(execute: {
if (isGranted){
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-request', { detail: 'granted' }))")
}
else {
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-request', { detail: 'denied' }))")
}
})
}
func returnPermissionState(state: String){
DispatchQueue.main.async(execute: {
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('push-permission-state', { detail: '\(state)' }))")
})
}
func handlePushPermission() {
UNUserNotificationCenter.current().getNotificationSettings () { settings in
switch settings.authorizationStatus {
case .notDetermined:
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: { (success, error) in
if error == nil {
if success == true {
returnPermissionResult(isGranted: true)
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
else {
returnPermissionResult(isGranted: false)
}
}
else {
returnPermissionResult(isGranted: false)
}
}
)
case .denied:
returnPermissionResult(isGranted: false)
case .authorized, .ephemeral, .provisional:
returnPermissionResult(isGranted: true)
@unknown default:
return;
}
}
}
func handlePushState() {
UNUserNotificationCenter.current().getNotificationSettings () { settings in
switch settings.authorizationStatus {
case .notDetermined:
returnPermissionState(state: "notDetermined")
case .denied:
returnPermissionState(state: "denied")
case .authorized:
returnPermissionState(state: "authorized")
case .ephemeral:
returnPermissionState(state: "ephemeral")
case .provisional:
returnPermissionState(state: "provisional")
@unknown default:
returnPermissionState(state: "unknown")
return;
}
}
}
func checkViewAndEvaluate(event: String, detail: String) {
if (!Nivesh.webView.isHidden && !Nivesh.webView.isLoading ) {
DispatchQueue.main.async(execute: {
Nivesh.webView.evaluateJavaScript("this.dispatchEvent(new CustomEvent('\(event)', { detail: \(detail) }))")
})
}
else {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
checkViewAndEvaluate(event: event, detail: detail)
}
}
}
func handleFCMToken(){
DispatchQueue.main.async(execute: {
Messaging.messaging().token { token, error in
if let error = error {
#if DEBUG
print("Error fetching FCM registration token: \(error)")
#endif
checkViewAndEvaluate(event: "push-token", detail: "ERROR GET TOKEN")
} else if let token = token {
checkViewAndEvaluate(event: "push-token", detail: "'\(token)'")
}
}
})
}
func sendPushToWebView(userInfo: [AnyHashable: Any]){
var json = "";
do {
let jsonData = try JSONSerialization.data(withJSONObject: userInfo)
json = String(data: jsonData, encoding: .utf8)!
} catch {
#if DEBUG
print("ERROR: userInfo parsing problem")
#endif
return
}
checkViewAndEvaluate(event: "push-notification", detail: json)
}

+ 13
- 2
src/Nivesh/SceneDelegate.swift View File

@ -95,9 +95,20 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
var hasLaunched = false
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
// 🚫 Prevent double call on first launch
if !hasLaunched {
hasLaunched = true
return
}
if let window = self.window,
let rootVC = window.rootViewController as? ViewController {
rootVC.authenticateUser()
}
}
func sceneDidEnterBackground(_ scene: UIScene) {


+ 257
- 135
src/Nivesh/ViewController.swift View File

@ -1,11 +1,17 @@
import UIKit
import WebKit
import LocalAuthentication
var webView: WKWebView! = nil
class ViewController: UIViewController, WKNavigationDelegate, UIDocumentInteractionControllerDelegate {
class ViewController: UIViewController,
WKNavigationDelegate,
UIDocumentInteractionControllerDelegate,
WKUIDelegate,
WKScriptMessageHandler {
var documentController: UIDocumentInteractionController?
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
@ -15,13 +21,18 @@ class ViewController: UIViewController, WKNavigationDelegate, UIDocumentInteract
@IBOutlet weak var connectionProblemView: UIImageView!
@IBOutlet weak var webviewView: UIView!
var toolbarView: UIToolbar!
var htmlIsLoaded = false;
var htmlIsLoaded = false
// 🔐 AUTH STATE
var isAuthenticating = false
var hasAuthenticatedOnce = false
var lockView: UIView?
private var themeObservation: NSKeyValueObservation?
var currentWebViewTheme: UIUserInterfaceStyle = .unspecified
override var preferredStatusBarStyle : UIStatusBarStyle {
if #available(iOS 13, *), overrideStatusBar{
if #available(iOS 13, *), overrideStatusBar {
if #available(iOS 15, *) {
return .default
} else {
@ -33,40 +44,59 @@ class ViewController: UIViewController, WKNavigationDelegate, UIDocumentInteract
override func viewDidLoad() {
super.viewDidLoad()
initWebView()
initToolbarView()
loadRootUrl()
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification , object: nil)
// 🔐 Face ID FIRST
authenticateUser()
NotificationCenter.default.addObserver(self,
selector: #selector(self.keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
Nivesh.webView.frame = calcWebviewFrame(webviewView: webviewView, toolbarView: nil)
Nivesh.webView.frame = view.safeAreaLayoutGuide.layoutFrame
}
@objc func keyboardWillHide(_ notification: NSNotification) {
Nivesh.webView.setNeedsLayout()
}
func initWebView() {
Nivesh.webView = createWebView(container: webviewView, WKSMH: self, WKND: self, NSO: self, VC: self)
webviewView.addSubview(Nivesh.webView);
Nivesh.webView.uiDelegate = self;
Nivesh.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
Nivesh.webView = createWebView(
container: webviewView,
WKSMH: self,
WKND: self,
NSO: self,
VC: self
)
if(pullToRefresh){
webviewView.addSubview(Nivesh.webView)
Nivesh.webView.uiDelegate = self
Nivesh.webView.addObserver(
self,
forKeyPath: #keyPath(WKWebView.estimatedProgress),
options: .new,
context: nil
)
if pullToRefresh {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshWebView(_:)), for: UIControl.Event.valueChanged)
refreshControl.addTarget(self,
action: #selector(refreshWebView(_:)),
for: .valueChanged)
Nivesh.webView.scrollView.addSubview(refreshControl)
Nivesh.webView.scrollView.bounces = true
}
if #available(iOS 15.0, *), adaptiveUIStyle {
themeObservation = Nivesh.webView.observe(\.underPageBackgroundColor) { [unowned self] webView, _ in
themeObservation = Nivesh.webView.observe(\.underPageBackgroundColor) { [unowned self] _, _ in
currentWebViewTheme = Nivesh.webView.underPageBackgroundColor.isLight() ?? true ? .light : .dark
self.overrideUIStyle()
}
@ -78,172 +108,264 @@ class ViewController: UIViewController, WKNavigationDelegate, UIDocumentInteract
sender.endRefreshing()
}
func createToolbarView() -> UIToolbar{
func createToolbarView() -> UIToolbar {
let winScene = UIApplication.shared.connectedScenes.first
let windowScene = winScene as! UIWindowScene
var statusBarHeight = windowScene.statusBarManager?.statusBarFrame.height ?? 60
#if targetEnvironment(macCatalyst)
if (statusBarHeight == 0){
statusBarHeight = 30
}
#endif
let statusBarHeight = windowScene.statusBarManager?.statusBarFrame.height ?? 60
let toolbarView = UIToolbar(frame: CGRect(x: 0, y: 0, width: webviewView.frame.width, height: 0))
toolbarView.sizeToFit()
toolbarView.frame = CGRect(x: 0, y: 0, width: webviewView.frame.width, height: toolbarView.frame.height + statusBarHeight)
// toolbarView.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin, .flexibleWidth]
toolbarView.frame = CGRect(
x: 0,
y: 0,
width: webviewView.frame.width,
height: toolbarView.frame.height + statusBarHeight
)
let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let close = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(loadRootUrl))
toolbarView.setItems([close,flex], animated: true)
toolbarView.setItems([close, flex], animated: true)
toolbarView.isHidden = true
return toolbarView
}
func overrideUIStyle(toDefault: Bool = false) {
if #available(iOS 15.0, *), adaptiveUIStyle {
if (((htmlIsLoaded && !Nivesh.webView.isHidden) || toDefault) && self.currentWebViewTheme != .unspecified) {
UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.overrideUserInterfaceStyle = toDefault ? .unspecified : self.currentWebViewTheme;
if (((htmlIsLoaded && !Nivesh.webView.isHidden) || toDefault)
&& self.currentWebViewTheme != .unspecified) {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }?
.overrideUserInterfaceStyle = toDefault ? .unspecified : self.currentWebViewTheme
}
}
}
func initToolbarView() {
toolbarView = createToolbarView()
toolbarView = createToolbarView()
webviewView.addSubview(toolbarView)
}
@objc func loadRootUrl() {
Nivesh.webView.load(URLRequest(url: SceneDelegate.universalLinkToLaunch ?? SceneDelegate.shortcutLinkToLaunch ?? rootUrl))
func showLockScreen() {
let lockView = UIView(frame: view.bounds)
lockView.backgroundColor = UIColor.systemBackground
let icon = UIImageView(image: UIImage(systemName: "faceid"))
icon.tintColor = .systemBlue
icon.translatesAutoresizingMaskIntoConstraints = false
icon.contentMode = .scaleAspectFit
let label = UILabel()
label.text = "Face ID Required\nto open Nivesh"
label.textAlignment = .center
label.numberOfLines = 2
label.font = UIFont.systemFont(ofSize: 22, weight: .semibold)
icon.tintColor = .systemBlue
label.translatesAutoresizingMaskIntoConstraints = false
lockView.addSubview(icon)
lockView.addSubview(label)
NSLayoutConstraint.activate([
icon.centerXAnchor.constraint(equalTo: lockView.centerXAnchor),
icon.centerYAnchor.constraint(equalTo: lockView.centerYAnchor, constant: -40),
icon.heightAnchor.constraint(equalToConstant: 60),
icon.widthAnchor.constraint(equalToConstant: 60),
label.topAnchor.constraint(equalTo: icon.bottomAnchor, constant: 20),
label.centerXAnchor.constraint(equalTo: lockView.centerXAnchor)
])
self.view.addSubview(lockView)
self.lockView = lockView
}
// 🔐 Face ID
func authenticateUser() {
if isAuthenticating { return }
showLockScreen() // 👈 show UI first
isAuthenticating = true
let context = LAContext()
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
context.evaluatePolicy(.deviceOwnerAuthentication,
localizedReason: "Unlock Nivesh") { success, _ in
DispatchQueue.main.async {
self.isAuthenticating = false
if success {
self.hideLockScreen() // 👈 remove UI
self.loadRootUrl()
} else {
self.showRetry()
}
}
}
} else {
isAuthenticating = false
hideLockScreen()
loadRootUrl()
}
}
func showRetry() {
let alert = UIAlertController(
title: "Authentication Failed",
message: "Try again",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Retry", style: .default) { _ in
self.authenticateUser()
})
present(alert, animated: true)
}
func hideLockScreen() {
lockView?.removeFromSuperview()
lockView = nil
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
@objc func loadRootUrl() {
Nivesh.webView.load(URLRequest(
url: SceneDelegate.universalLinkToLaunch
?? SceneDelegate.shortcutLinkToLaunch
?? rootUrl
))
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
htmlIsLoaded = true
self.setProgress(1.0, true)
self.animateConnectionProblem(false)
setProgress(1.0, true)
animateConnectionProblem(false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
Nivesh.webView.isHidden = false
self.loadingView.isHidden = true
self.setProgress(0.0, false)
self.overrideUIStyle()
}
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
htmlIsLoaded = false;
if (error as NSError)._code != (-999) {
self.overrideUIStyle(toDefault: true);
webView.isHidden = true;
loadingView.isHidden = false;
animateConnectionProblem(true);
setProgress(0.05, true);
htmlIsLoaded = false
if (error as NSError)._code != -999 {
overrideUIStyle(toDefault: true)
webView.isHidden = true
loadingView.isHidden = false
animateConnectionProblem(true)
setProgress(0.05, true)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.setProgress(0.1, true);
self.setProgress(0.1, true)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.loadRootUrl();
self.loadRootUrl()
}
}
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(WKWebView.estimatedProgress) &&
Nivesh.webView.isLoading &&
!self.loadingView.isHidden &&
!self.htmlIsLoaded) {
var progress = Float(Nivesh.webView.estimatedProgress);
if (progress >= 0.8) { progress = 1.0; };
if (progress >= 0.3) { self.animateConnectionProblem(false); }
self.setProgress(progress, true);
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(WKWebView.estimatedProgress),
Nivesh.webView.isLoading,
!loadingView.isHidden,
!htmlIsLoaded {
var progress = Float(Nivesh.webView.estimatedProgress)
if progress >= 0.8 { progress = 1.0 }
if progress >= 0.3 { animateConnectionProblem(false) }
setProgress(progress, true)
}
}
func setProgress(_ progress: Float, _ animated: Bool) {
self.progressView.setProgress(progress, animated: animated);
progressView.setProgress(progress, animated: animated)
}
func animateConnectionProblem(_ show: Bool) {
if (show) {
self.connectionProblemView.isHidden = false;
self.connectionProblemView.alpha = 0
UIView.animate(withDuration: 0.7, delay: 0, options: [.repeat, .autoreverse], animations: {
if show {
connectionProblemView.isHidden = false
connectionProblemView.alpha = 0
UIView.animate(withDuration: 0.7,
delay: 0,
options: [.repeat, .autoreverse]) {
self.connectionProblemView.alpha = 1
})
}
else {
UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: {
self.connectionProblemView.alpha = 0 // Here you will get the animation you want
}, completion: { _ in
self.connectionProblemView.isHidden = true;
self.connectionProblemView.layer.removeAllAnimations();
})
}
} else {
UIView.animate(withDuration: 0.3) {
self.connectionProblemView.alpha = 0
} completion: { _ in
self.connectionProblemView.isHidden = true
self.connectionProblemView.layer.removeAllAnimations()
}
}
}
deinit {
Nivesh.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
Nivesh.webView.removeObserver(self,
forKeyPath: #keyPath(WKWebView.estimatedProgress))
}
}
// 🔗 JS Bridge
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
extension UIColor {
// Check if the color is light or dark, as defined by the injected lightness threshold.
// Some people report that 0.7 is best. I suggest to find out for yourself.
// A nil value is returned if the lightness couldn't be determined.
func isLight(threshold: Float = 0.5) -> Bool? {
let originalCGColor = self.cgColor
switch message.name {
case "request-print":
print("Print requested")
// Now we need to convert it to the RGB colorspace. UIColor.white / UIColor.black are greyscale and not RGB.
// If you don't do this then you will crash when accessing components index 2 below when evaluating greyscale colors.
let RGBCGColor = originalCGColor.converted(to: CGColorSpaceCreateDeviceRGB(), intent: .defaultIntent, options: nil)
guard let components = RGBCGColor?.components else {
return nil
}
guard components.count >= 3 else {
return nil
}
case "request-push-permission":
requestPushPermissionIfNeeded { status in
print("Push permission: \(status)")
}
let brightness = Float(((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000)
return (brightness > threshold)
case "request-device-info":
sendDeviceInfoToWeb(webView: Nivesh.webView)
default:
break
}
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "print" {
printView(webView: Nivesh.webView)
}
if message.name == "push-subscribe" {
handleSubscribeTouch(message: message)
}
if message.name == "push-permission-request" {
handlePushPermission()
}
if message.name == "push-permission-state" {
handlePushState()
}
if message.name == "push-token" {
handleFCMToken()
}
}
// UIColor extension
extension UIColor {
func isLight(threshold: Float = 0.5) -> Bool? {
guard let components = self.cgColor.converted(
to: CGColorSpaceCreateDeviceRGB(),
intent: .defaultIntent,
options: nil
)?.components, components.count >= 3 else { return nil }
let brightness = Float(
((components[0]*299)+(components[1]*587)+(components[2]*114))/1000
)
return brightness > threshold
}
}

+ 104
- 354
src/Nivesh/WebView.swift View File

@ -3,401 +3,151 @@ import UIKit
import AuthenticationServices
import SafariServices
import FirebaseMessaging
import UserNotifications
import Security
// ---------- Keychain Helper ----------
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
}
}
// ---------- App UUID ----------
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{
// ---------- CREATE WEBVIEW (FIXED) ----------
func createWebView(
container: UIView,
WKSMH: WKScriptMessageHandler,
WKND: WKNavigationDelegate,
NSO: NSObject,
VC: ViewController
) -> WKWebView {
let config = WKWebViewConfiguration()
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.limitsNavigationsToAppBoundDomains = false;
config.limitsNavigationsToAppBoundDomains = false
config.allowsInlineMediaPlayback = true
config.preferences.javaScriptCanOpenWindowsAutomatically = true
config.preferences.setValue(true, forKey: "standalone")
let webView = WKWebView(frame: calcWebviewFrame(webviewView: container, toolbarView: nil), configuration: config)
setCustomCookie(webView: webView)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// FIXED: removed calcWebviewFrame
let webView = WKWebView(frame: container.bounds, configuration: config)
webView.isHidden = true;
// FIXED autoresizing issue
webView.autoresizingMask = [
UIView.AutoresizingMask.flexibleWidth,
UIView.AutoresizingMask.flexibleHeight
]
webView.isHidden = true
webView.navigationDelegate = WKND
webView.scrollView.bounces = false
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.allowsBackForwardNavigationGestures = true
webView.allowsLinkPreview = false
let deviceModel = UIDevice.current.model
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.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)
])!
// FIXED observer
webView.addObserver(
NSO,
forKeyPath: #keyPath(WKWebView.estimatedProgress),
options: NSKeyValueObservingOptions.new,
context: nil
)
webView.configuration.websiteDataStore.httpCookieStore.setCookie(_deviceCookie)
}
#if DEBUG
if #available(iOS 16.4, *) {
webView.isInspectable = true
}
#endif
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);
// ---------- Minimal Device Info ----------
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)
// ---------- Send Device Info ----------
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 {
// redirect new tabs to main webview
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if (navigationAction.targetFrame == nil) {
webView.load(navigationAction.request)
}
return nil
}
// restrict navigation to target host, open external links in 3rd party apps
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 {
// NOTE: Match auth origin first, because host origin may be a subset of auth origin and may therefore always match
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) {
// Open in main webview
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) &&
// no error here, fake warning
(navigationAction.sourceFrame != nil)
) {
decisionHandler(.allow)
return
}
else {
decisionHandler(.cancel)
}
if ["http", "https"].contains(requestUrl.scheme?.lowercased() ?? "") {
// Can open with SFSafariViewController
let safariViewController = SFSafariViewController(url: requestUrl)
self.present(safariViewController, animated: true, completion: nil)
} else {
// Scheme is not supported or no scheme is given, use openURL
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 {
// not tested
downloadAndOpenFile(url: requestUrl.absoluteURL)
}
// if (requestUrl.absoluteString.contains("base64")){
// downloadAndOpenBase64File(base64String: requestUrl.absoluteString)
// }
}
}
}
else {
decisionHandler(.cancel)
}
}
// Handle javascript: `window.alert(message: String)`
func webView(_ webView: WKWebView,
runJavaScriptAlertPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping () -> Void) {
// Set the message as the UIAlertController message
let alert = UIAlertController(
title: nil,
message: message,
preferredStyle: .alert
)
// Add a confirmation action OK
let okAction = UIAlertAction(
title: "OK",
style: .default,
handler: { _ in
// Call completionHandler
completionHandler()
}
)
alert.addAction(okAction)
// Display the NSAlert
present(alert, animated: true, completion: nil)
}
// Handle javascript: `window.confirm(message: String)`
func webView(_ webView: WKWebView,
runJavaScriptConfirmPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (Bool) -> Void) {
// Set the message as the UIAlertController message
let alert = UIAlertController(
title: nil,
message: message,
preferredStyle: .alert
)
// Add a confirmation action Cancel
let cancelAction = UIAlertAction(
title: "Cancel",
style: .cancel,
handler: { _ in
// Call completionHandler
completionHandler(false)
}
)
// Add a confirmation action OK
let okAction = UIAlertAction(
title: "OK",
style: .default,
handler: { _ in
// Call completionHandler
completionHandler(true)
}
)
alert.addAction(cancelAction)
alert.addAction(okAction)
// Display the NSAlert
present(alert, animated: true, completion: nil)
}
// Handle javascript: `window.prompt(prompt: String, defaultText: String?)`
func webView(_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (String?) -> Void) {
// Set the message as the UIAlertController message
let alert = UIAlertController(
title: nil,
message: prompt,
preferredStyle: .alert
)
// Add a confirmation action Cancel
let cancelAction = UIAlertAction(
title: "Cancel",
style: .cancel,
handler: { _ in
// Call completionHandler
completionHandler(nil)
}
)
// Add a confirmation action OK
let okAction = UIAlertAction(
title: "OK",
style: .default,
handler: { _ in
// Call completionHandler with Alert input
if let input = alert.textFields?.first?.text {
completionHandler(input)
// ---------- Push Permission ----------
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)
// Display the NSAlert
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()
}
// func downloadAndOpenBase64File(base64String: String) {
// // Split the base64 string to extract the data and the file extension
// let components = base64String.components(separatedBy: ";base64,")
// // Make sure the base64 string has the correct format
// guard components.count == 2, let format = components.first?.split(separator: "/").last else {
// print("Invalid base64 string format")
// return
// }
// // Remove the data type prefix to get the base64 data
// let dataString = components.last!
// if let imageData = Data(base64Encoded: dataString) {
// let documentsUrl: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// let destinationFileUrl = documentsUrl.appendingPathComponent("image.\(format)")
// do {
// try imageData.write(to: destinationFileUrl)
// self.openFile(url: destinationFileUrl)
// } catch {
// print("Error writing image to file url: \(destinationFileUrl): \(error)")
// }
// }
// }
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)
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.