Skip to content

Commit

Permalink
Merge pull request #368 from Concordium/feature/deeplink-support
Browse files Browse the repository at this point in the history
WalletConenct deeplink and universal link support
  • Loading branch information
prinshamlet authored Oct 27, 2023
2 parents 3e8cbd6 + d399d4d commit eef7bfd
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 28 deletions.
32 changes: 31 additions & 1 deletion ConcordiumWallet/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ class AppCoordinator: NSObject, Coordinator, ShowAlert, RequestPasswordDelegate
loginCoordinator.start()
}

func openWalletConnectAccountSelection(url: URL) {
guard let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems else {
showErrorAlert(ViewError.simpleError(localizedReason: "WalletConnect URL format incorrect."))
return
}

if let uriValue = queryItems.first(where: { $0.name == "uri" })?.value {
initializeWalletConnectCoordinatorFlow().start(with: uriValue)
}
}

func initializeWalletConnectCoordinatorFlow() -> WalletConnectCoordinator {
if childCoordinators.contains(where: { $0 is WalletConnectCoordinator }) {
navigationController.popToRootViewController(animated: true)
dismissWalletConnectCoordinator()
}
let c = WalletConnectCoordinator(
navigationController: navigationController,
dependencyProvider: defaultProvider,
parentCoordinator: self
)
childCoordinators.append(c)
return c
}

func showMainTabbar() {
navigationController.setupBaseNavigationControllerStyle()

Expand Down Expand Up @@ -255,7 +280,6 @@ class AppCoordinator: NSObject, Coordinator, ShowAlert, RequestPasswordDelegate
}

extension AppCoordinator: AccountsPresenterDelegate {

func userPerformed(action: AccountCardAction, on account: AccountDataType) {
accountsCoordinator?.userPerformed(action: action, on: account)
}
Expand Down Expand Up @@ -332,6 +356,12 @@ extension AppCoordinator: LoginCoordinatorDelegate {
}
}

extension AppCoordinator: WalletConnectCoordiantorDelegate {
func dismissWalletConnectCoordinator() {
childCoordinators.removeAll { $0 is WalletConnectCoordinator }
}
}

extension AppCoordinator: ImportCoordinatorDelegate {
func importCoordinatorDidFinish(_ coordinator: ImportCoordinator) {
navigationController.dismiss(animated: true)
Expand Down
40 changes: 37 additions & 3 deletions ConcordiumWallet/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
setupMatomoTracker()
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let webpageURL = userActivity.webpageURL {
DispatchQueue.main.async {
self.appCoordinator.openWalletConnectAccountSelection(url: webpageURL)
}
}
}
return true
}


/// Checks if the given URL string matches a specified URL scheme pattern.
/// - Parameters:
/// - url: The URL string to be checked.
/// - Returns:
/// - `true` if the URL string matches the specified pattern, otherwise `false`.
/// - Note:
/// The function checks if the provided `url` matches one of two patterns:
/// 1. The URL scheme specified by the `ApiConstants.scheme` constant, followed by "://wc".
/// 2. The exact URL scheme "concordiumwallet" followed by "://wc".
/// The reason for these check is that we want to check if url follows `concordiumwallet` scheme for WalletConnect,
/// but need to support also DeepLinks per scheme, for instance `concordiumwallettest` or `concordiumwalletstaging`
private func matchesURLScheme(_ url: String) -> Bool {
let regexPattern = #"^\#(ApiConstants.scheme)://wc.*|concordiumwallet://wc.*"#
guard let regex = try? NSRegularExpression(pattern: regexPattern, options: []) else { return false }
let range = NSRange(location: 0, length: url.utf16.count)
if let match = regex.firstMatch(in: url, options: [], range: range) {
return match.range == range
}
return false
}

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
Logger.trace("application:openUrl: \(url)")
if url.absoluteString.starts(with: ApiConstants.notabeneCallback) {
receivedCreateIdentityCallback(url)
} else {
// importing file
appCoordinator.importWallet(from: url)
} else if matchesURLScheme(url.absoluteString) {
DispatchQueue.main.async {
self.appCoordinator.openWalletConnectAccountSelection(url: url)
}
}
return true
}
Expand Down
14 changes: 9 additions & 5 deletions ConcordiumWallet/Resources/ConcordiumWalletStagingNet-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array/>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
Expand All @@ -22,10 +20,18 @@
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLSchemes</key>
<array>
<string>concordiumwallet</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>concordiumwalletstaging</string>
</array>
Expand Down Expand Up @@ -76,7 +82,5 @@
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>NSMicrophoneUsageDescription</key>
<string></string>
</dict>
</plist>
16 changes: 11 additions & 5 deletions ConcordiumWallet/Resources/ConcordiumWalletTestNet-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array/>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
Expand All @@ -22,10 +20,18 @@
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLSchemes</key>
<array>
<string>concordiumwallet</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>concordiumwallettest</string>
</array>
Expand All @@ -52,6 +58,8 @@
<string>Used to scan QR code and to take a picture for your identity</string>
<key>NSFaceIDUsageDescription</key>
<string>Enable FaceID for more convenient login</string>
<key>NSMicrophoneUsageDescription</key>
<string></string>
<key>UIAppFonts</key>
<array>
<string>RobotoMono.ttf</string>
Expand All @@ -74,8 +82,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSMicrophoneUsageDescription</key>
<string></string>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
</dict>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:wallet.concordium.software</string>
</array>
<key>com.apple.developer.default-data-protection</key>
<string>NSFileProtectionComplete</string>
</dict>
Expand Down
1 change: 1 addition & 0 deletions ConcordiumWallet/Service/Network/ApiConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct ApiConstants {
static let scheme = "concordiumwalletstaging"
#endif
static let notabeneCallback = "\(scheme)://identity-issuer/callback"
static let walletConnectURI = "\(scheme)://wc"

static let ipInfo = proxyUrl.appendingPathComponent("/v0/ip_info")
static let ipInfoV1 = proxyUrl.appendingPathComponent("/v1/ip_info")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@ extension AccountsCoordinator: SeedIdentitiesCoordinatorDelegate {
func seedIdentityCoordinatorWasFinished(for identity: IdentityDataType) {
navigationController.dismiss(animated: true)
childCoordinators.removeAll(where: { $0 is SeedIdentitiesCoordinator })

NotificationCenter.default.post(name: Notification.Name("seedAccountCoordinatorWasFinishedNotification"), object: nil)
}
}
Expand All @@ -254,7 +253,7 @@ extension AccountsCoordinator: WalletConnectDelegate {
let walletConnectCoordinator = WalletConnectCoordinator(
navigationController: navigationController,
dependencyProvider: dependencyProvider,
parentCoordiantor: self
parentCoordinator: self
)

walletConnectCoordinator.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ struct WalletConnectAccountSelectView: View {
viewModel.didSelect(account: account.wrappedValue)
}
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
viewModel.dismissView()
} label: {
HStack {
Image(systemName: "chevron.backward")
Text("Cancel")
}
}
}
}
.listRowSeparator(.hidden)
.listStyle(.plain)
.onAppear(perform: viewModel.loadAccounts)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ class WalletConnectAccountSelectViewModel: ObservableObject {
private var storageManager: StorageManagerProtocol
@Published var accounts: [AccountDataType] = []
var didSelect: ((_ account: AccountDataType) -> ())
var dismissView: (() -> Void)

init(storageManager: StorageManagerProtocol, didSelect: @escaping (_ account: AccountDataType) -> ()) {
init(storageManager: StorageManagerProtocol, didSelect: @escaping (_ account: AccountDataType) -> (), dismissView: @escaping (() -> Void)) {
self.storageManager = storageManager
self.didSelect = didSelect
self.dismissView = dismissView
}

func loadAccounts() {
Expand Down
41 changes: 30 additions & 11 deletions ConcordiumWallet/Views/WalletConnect/WalletConnectCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ class WalletConnectCoordinator: Coordinator {
init(
navigationController: UINavigationController,
dependencyProvider: DependencyProvider,
parentCoordiantor: WalletConnectCoordiantorDelegate
parentCoordinator: WalletConnectCoordiantorDelegate
) {
self.dependencyProvider = dependencyProvider
self.navigationController = navigationController
parentCoordinator = parentCoordiantor
self.parentCoordinator = parentCoordinator

let metadata = AppMetadata(
name: "Concordium",
Expand All @@ -49,6 +49,10 @@ class WalletConnectCoordinator: Coordinator {
showWalletConnectScanner()
}

func start(with uri: String) {
pairWalletConnect(with: uri)
}

func nukeWalletConnectSessionsAndPairings() {
Sign.instance.nuke()
Pair.instance.nuke()
Expand Down Expand Up @@ -108,6 +112,7 @@ private extension WalletConnectCoordinator {

let viewModel = WalletConnectAccountSelectViewModel(
storageManager: self.dependencyProvider.storageManager(),

didSelect: { [weak self] account in
self?.navigationController.pushViewController(
UIHostingController(
Expand Down Expand Up @@ -146,7 +151,6 @@ private extension WalletConnectCoordinator {
self?.presentError(with: "errorAlert.title".localized, message: err.localizedDescription)
}
}

// Pop the VC without waiting for rejection to complete.
self?.navigationController.popToRootViewController(animated: true)
},
Expand All @@ -156,6 +160,10 @@ private extension WalletConnectCoordinator {
),
animated: true
)
}, dismissView: { [weak self] in
self?.navigationController.popViewController(animated: true)
self?.parentCoordinator?.dismissWalletConnectCoordinator()
self?.nukeWalletConnectSessionsAndPairings()
}
)

Expand Down Expand Up @@ -587,14 +595,7 @@ extension WalletConnectCoordinator: WalletConnectDelegate {
if !value.hasPrefix("wc:") {
return false
}
Task {
do {
try await Pair.instance.pair(uri: WalletConnectURI(string: value)!)
} catch let err {
self?.navigationController.popViewController(animated: true)
self?.presentError(with: "errorAlert.title".localized, message: err.localizedDescription)
}
}
self?.pairWalletConnect(with: value)
return true
}, viewDidDisappear: { [weak self] in
self?.nukeWalletConnectSessionsAndPairings()
Expand All @@ -605,6 +606,22 @@ extension WalletConnectCoordinator: WalletConnectDelegate {
navigationController.pushViewController(vc, animated: true)
}

private func pairWalletConnect(with uri: String) {
Task { [weak self] in
do {
guard let uri = WalletConnectURI(string: uri) else {
self?.presentError(with: "errorAlert.title".localized, message: "Unable to initialize WalletConnectURI.")
return
}
try await Pair.instance.pair(uri: uri)
} catch let err {
self?.navigationController.popViewController(animated: true)
self?.presentError(with: "errorAlert.title".localized, message: err.localizedDescription)
self?.parentCoordinator?.dismissWalletConnectCoordinator()
}
}
}

func presentError(with title: String, message: String) {
let ac = UIAlertController(title: title, message: message, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "errorAlert.okButton".localized, style: .default))
Expand All @@ -622,6 +639,7 @@ extension WalletConnectCoordinator: WalletConnectDelegate {
)
} catch let err {
self?.presentError(with: "errorAlert.title".localized, message: "Cannot respond status to the dApp: \(err.localizedDescription)")
self?.parentCoordinator?.dismissWalletConnectCoordinator()
}
}
if shouldPresent {
Expand All @@ -641,6 +659,7 @@ extension WalletConnectCoordinator: WalletConnectDelegate {
with: "errorAlert.title".localized,
message: "Cannot repsond status to the dApp: \(err.localizedDescription)"
)
self?.parentCoordinator?.dismissWalletConnectCoordinator()
}
}

Expand Down

0 comments on commit eef7bfd

Please sign in to comment.