Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- added: Sign Message option in the wallet list menu for Bitcoin-family wallets, letting users prove self-hosted wallet ownership to exchanges by signing an exchange-provided message.
- added: `edge://buy` and `edge://sell` deep links (and their `https://deep.edge.app` equivalents) that open the buy/sell flow, optionally pinning a provider and payment method to the top of the quote options for that visit.
- added: Provider priority in the buy/sell options for affiliated accounts, configured through the info server promo card data.
- changed: Adopt the iOS UIScene lifecycle, removing the deprecated app-delegate window and lifecycle APIs ahead of Xcode 27.
- changed: Target Android 16 (API level 36), which Google Play requires for app updates submitted after Aug 30, 2026. Predictive back is opted out of for now, since React Native 0.79 cannot handle it, so the back button behaves exactly as it did before.
- changed: Sign MoonPay buy/sell widget URLs and bind them to the customer's IP via the info server, for MoonPay's on-ramp IP-matching security upgrade.
- changed: Style the entire "Already have an account? Sign in" line in the getting-started USP carousel with the tertiary link color, not just "Sign in".
Expand Down
4 changes: 4 additions & 0 deletions ios/edge.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
3D5BD9CF2A4D27F300590088 /* custom.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D5BD9AD2A4D277B00590088 /* custom.ttf */; };
3D88EE2D2E3C3BE80086BA9D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D88EE2C2E3C3BE80086BA9D /* AppDelegate.swift */; };
3D88EE312E3D670F0086BA9D /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D88EE302E3D670F0086BA9D /* Sentry.swift */; };
3DA1F0012E7A11B000C0FFEE /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA1F0022E7A11B000C0FFEE /* SceneDelegate.swift */; };
3DB299A62BCEF2A600D867B0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3DB299A52BCEF2A600D867B0 /* PrivacyInfo.xcprivacy */; };
812B284944A10A722B22763D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08ADAD14914ABC9FD7D54456 /* ExpoModulesProvider.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
Expand Down Expand Up @@ -80,6 +81,7 @@
3D5BD9BC2A4D277B00590088 /* Quicksand-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Quicksand-Medium.ttf"; path = "../android/app/src/main/assets/fonts/Quicksand-Medium.ttf"; sourceTree = "<group>"; };
3D88EE2C2E3C3BE80086BA9D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = edge/AppDelegate.swift; sourceTree = "<group>"; };
3D88EE302E3D670F0086BA9D /* Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentry.swift; sourceTree = "<group>"; };
3DA1F0022E7A11B000C0FFEE /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = edge/SceneDelegate.swift; sourceTree = "<group>"; };
3DB299A52BCEF2A600D867B0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = edge/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
5709B34CF0A7D63546082F79 /* Pods-edge.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-edge.release.xcconfig"; path = "Target Support Files/Pods-edge/Pods-edge.release.xcconfig"; sourceTree = "<group>"; };
5B7EB9410499542E8C5724F5 /* Pods-edge-edgeTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-edge-edgeTests.debug.xcconfig"; path = "Target Support Files/Pods-edge-edgeTests/Pods-edge-edgeTests.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -108,6 +110,7 @@
isa = PBXGroup;
children = (
3D88EE2C2E3C3BE80086BA9D /* AppDelegate.swift */,
3DA1F0022E7A11B000C0FFEE /* SceneDelegate.swift */,
3D5BD98B2A4D245500590088 /* edge.entitlements */,
3D5BD9892A4CF04C00590088 /* GoogleService-Info.plist */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
Expand Down Expand Up @@ -464,6 +467,7 @@
3D88EE312E3D670F0086BA9D /* Sentry.swift in Sources */,
3D5BD9882A4CEFC700590088 /* Base58.swift in Sources */,
3D88EE2D2E3C3BE80086BA9D /* AppDelegate.swift in Sources */,
3DA1F0012E7A11B000C0FFEE /* SceneDelegate.swift in Sources */,
3D5BD9862A4CEFB900590088 /* EdgeCore.swift in Sources */,
812B284944A10A722B22763D /* ExpoModulesProvider.swift in Sources */,
04DBAACE71E94A3B9BCAF10A /* EdgeAttestation.swift in Sources */,
Expand Down
110 changes: 27 additions & 83 deletions ios/edge/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ExpoQuickActions
import Firebase
import FirebaseMessaging
import RNBootSplash
Expand All @@ -10,41 +9,22 @@ import UserNotifications

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
/// The scene owns the window, but third-party code still reaches for it here.
/// `@react-native-firebase/messaging` reads `application.delegate.window` on
/// `UIApplicationDidFinishLaunchingNotification`, which raises
/// `doesNotRecognizeSelector` if the delegate has no `window` property at all,
/// so `SceneDelegate` mirrors its window into this.
var window: UIWindow?
var securityView: UIView?

var reactNativeDelegate: ReactNativeDelegate?
var reactNativeFactory: RCTReactNativeFactory?

/**
* Handles deep links.
* From https://reactnative.dev/docs/0.79/linking?ios-language=swift#enabling-deep-links
*/
func application(
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return RCTLinkingManager.application(app, open: url, options: options)
}

/**
* Handles deep links.
* From https://reactnative.dev/docs/0.79/linking?ios-language=swift#enabling-deep-links
*/
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return RCTLinkingManager.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
}

/**
* Handles app start-up.
* React Native template code.
*
* The window and everything tied to its lifecycle lives in `SceneDelegate`,
* since this app adopts the `UIScene` lifecycle.
*/
func application(
_ application: UIApplication,
Expand All @@ -57,14 +37,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Client-side background fetch interval:
application.setMinimumBackgroundFetchInterval(60 * 60 * 12)

// Capture a home-screen quick action on a cold launch so
// expo-quick-actions can report it as the initial action once JS loads.
// This delegate is not an ExpoAppDelegate, so the library's own
// subscriber never runs. Edge addition.
if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
ExpoQuickActions.initialAction = shortcutItem
}

// React Native template code:
let delegate = ReactNativeDelegate()
let factory = RCTReactNativeFactory(delegate: delegate)
Expand All @@ -73,30 +45,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
reactNativeDelegate = delegate
reactNativeFactory = factory

window = UIWindow(frame: UIScreen.main.bounds)

factory.startReactNative(
withModuleName: "edge",
in: window,
launchOptions: launchOptions
)

return true
}

/**
* Handles home-screen quick action taps while the app is running.
* Mirrors ExpoQuickActionsAppDelegate, which never runs because this
* delegate is not an ExpoAppDelegate. Edge addition.
* Points every new scene at our `SceneDelegate`.
* The scene manifest in Info.plist names the same configuration.
*/
func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
NotificationCenter.default.post(
name: Notification.Name("onQuickAction"), object: shortcutItem)
completionHandler(true)
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
return UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
}

/**
Expand Down Expand Up @@ -130,7 +94,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
print("Background notification: \(message)")

DispatchQueue.main.async {
application.applicationIconBadgeNumber = problemUsers.count
setBadgeCount(problemUsers.count)

let content = UNMutableNotificationContent()
content.title = "Urgent Security Issue"
Expand All @@ -147,36 +111,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

}
}

/**
* Hides the app when we go into the background.
* Edge addition.
*/
func applicationDidEnterBackground(_ application: UIApplication) {
guard
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil) as UIStoryboard?,
let launchScreen = storyboard.instantiateInitialViewController(),
let launchView = launchScreen.view,
let window = self.window
else {
return
}

launchView.frame = window.bounds
window.addSubview(launchView)
window.makeKeyAndVisible()
self.securityView = launchView
}

/**
* Shows the app when we come into the foreground.
* Edge addition.
*/
func applicationWillEnterForeground(_ application: UIApplication) {
if let view = securityView {
view.removeFromSuperview()
securityView = nil
}
/// Sets the home-screen badge.
/// `UIApplication.applicationIconBadgeNumber` is deprecated since iOS 17,
/// but our deployment target still includes iOS 15.
private func setBadgeCount(_ count: Int) {
if #available(iOS 16.0, *) {
UNUserNotificationCenter.current().setBadgeCount(count)
} else {
UIApplication.shared.applicationIconBadgeNumber = count
}
}

Expand Down
17 changes: 17 additions & 0 deletions ios/edge/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,23 @@
<string>Quicksand-SemiBold.ttf</string>
<string>Quicksand-Bold.ttf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
Expand Down
160 changes: 160 additions & 0 deletions ios/edge/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import ExpoQuickActions
import React
import UIKit

/**
* Owns the app's single window and its lifecycle.
*
* iOS 26 deprecates the `UIApplicationDelegate` window and lifecycle callbacks
* for apps that adopt the `UIScene` lifecycle, and Xcode 27 is expected to drop
* the `UIDesignRequiresCompatibility` opt-out entirely, so window creation,
* foreground/background transitions, deep links and quick actions all live here
* instead of on `AppDelegate`.
*/
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var securityView: UIView?

/**
* Handles app start-up for this scene.
*
* React Native is created once by `AppDelegate`, but the root view is mounted
* into the window this scene owns.
*/
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard
let windowScene = scene as? UIWindowScene,
let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let factory = appDelegate.reactNativeFactory
else {
return
}

// Capture a home-screen quick action on a cold launch so
// expo-quick-actions can report it as the initial action once JS loads.
// Under the scene lifecycle this arrives in the connection options rather
// than in the application launch options. Edge addition.
if let shortcutItem = connectionOptions.shortcutItem {
ExpoQuickActions.initialAction = shortcutItem
}

let window = UIWindow(windowScene: windowScene)
self.window = window
appDelegate.window = window

factory.startReactNative(
withModuleName: "edge",
in: window,
launchOptions: Self.launchOptions(from: connectionOptions)
)
}

/**
* Handles deep links while the app is running.
* The scene lifecycle replaces `application(_:open:options:)`.
*/
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for context in URLContexts {
RCTLinkingManager.application(
UIApplication.shared,
open: context.url,
options: [:]
)
}
}

/**
* Handles universal links.
* The scene lifecycle replaces
* `application(_:continue:restorationHandler:)`.
*/
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
RCTLinkingManager.application(
UIApplication.shared,
continue: userActivity,
restorationHandler: { _ in }
)
}

/**
* Handles home-screen quick action taps while the app is running.
* Mirrors ExpoQuickActionsAppDelegate, which never runs because our
* app delegate is not an ExpoAppDelegate. Edge addition.
*/
func windowScene(
_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
NotificationCenter.default.post(
name: Notification.Name("onQuickAction"), object: shortcutItem)
completionHandler(true)
}

/**
* Hides the app when we go into the background.
* Edge addition.
*/
func sceneDidEnterBackground(_ scene: UIScene) {
guard
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil) as UIStoryboard?,
let launchScreen = storyboard.instantiateInitialViewController(),
let launchView = launchScreen.view,
let window = self.window
else {
return
}

launchView.frame = window.bounds
window.addSubview(launchView)
window.makeKeyAndVisible()
self.securityView = launchView
}

/**
* Shows the app when we come into the foreground.
* Edge addition.
*/
func sceneWillEnterForeground(_ scene: UIScene) {
if let view = securityView {
view.removeFromSuperview()
securityView = nil
}
}

/**
* Rebuilds the launch options React Native expects from the scene's
* connection options.
*
* `RCTLinkingManager.getInitialURL` reads the cold-launch URL out of the
* bridge's launch options, but iOS does not put deep-link keys into
* `didFinishLaunchingWithOptions` once the scene lifecycle is adopted, so
* without this the first `Linking.getInitialURL()` after a cold launch
* would always resolve to null.
*/
private static func launchOptions(
from connectionOptions: UIScene.ConnectionOptions
) -> [AnyHashable: Any] {
if let url = connectionOptions.urlContexts.first?.url {
return [UIApplication.LaunchOptionsKey.url: url]
}

let webActivity = connectionOptions.userActivities.first { userActivity in
userActivity.activityType == NSUserActivityTypeBrowsingWeb
}
if let webActivity = webActivity {
return [
UIApplication.LaunchOptionsKey.userActivityDictionary: [
UIApplication.LaunchOptionsKey.userActivityType: webActivity.activityType,
"UIApplicationLaunchOptionsUserActivityKey": webActivity
]
]
}

return [:]
}
}
Loading