chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,17 @@
// Copyright 2018-present 650 Industries. All rights reserved.
import ExpoModulesCore
@objc
public class AppDelegatesLoaderDelegate: NSObject {
/**
Gets and registers AppDelegate subscribers.
*/
@objc
public static func registerAppDelegateSubscribers(_ legacySubscriber: ExpoAppDelegateSubscriberProtocol) {
let modulesProvider = AppContext.modulesProvider(withName: "ExpoModulesProvider")
ExpoAppDelegateSubscriberRepository.registerSubscriber(legacySubscriber)
ExpoAppDelegateSubscriberRepository.registerSubscribersFrom(modulesProvider: modulesProvider)
ExpoAppDelegateSubscriberRepository.registerReactDelegateHandlersFrom(modulesProvider: modulesProvider)
}
}
@@ -0,0 +1,30 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/Platform.h>
#import <ExpoModulesCore/EXReactDelegateWrapper.h>
#import <Expo/RCTAppDelegateUmbrella.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTAppDelegate ()
@end
__deprecated_msg("EXAppDelegateWrapper is deprecated. Migrate your AppDelegate to Swift and use ExpoAppDelegate instead. EXAppDelegateWrapper will be removed in SDK 55.")
@interface EXAppDelegateWrapper : NSObject <UIApplicationDelegate, UISceneDelegate, RCTReactNativeFactoryDelegate>
@property (nonatomic, strong, readonly) EXReactDelegateWrapper *reactDelegate;
@property (nonatomic, strong, nullable) NSString *moduleName;
@property (nonatomic, strong, nullable) NSDictionary *initialProps;
/**
Currently (RN 0.76) `customizeRootView` signature in `RCTAppDelegate` is broken as it uses `RCTRootView` type,
but this type is no longer used. It should rather be `RCTSurfaceHostingView`, but for simplicity it could be just `UIView`.
We need a helper function in Objective-C to actually make it to work, otherwise the types will conflict in Swift.
*/
+ (void)customizeRootView:(nonnull UIView *)rootView byAppDelegate:(nonnull RCTAppDelegate *)appDelegate;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,112 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Expo/EXAppDelegateWrapper.h>
#import <Expo/RCTAppDelegateUmbrella.h>
#import <Expo/Swift.h>
#import <React/RCTComponentViewFactory.h> // Allows non-umbrella since it's coming from React-RCTFabric
#import <ReactCommon/RCTHost.h> // Allows non-umbrella because the header is not inside a clang module
@implementation EXAppDelegateWrapper {
EXExpoAppDelegate *_expoAppDelegate;
}
- (instancetype)init
{
if (self = [super init]) {
_expoAppDelegate = [EXExpoAppDelegate new];
}
return self;
}
// This needs to be implemented, otherwise forwarding won't be called.
// When the app starts, `UIApplication` uses it to check beforehand
// which `UIApplicationDelegate` selectors are implemented.
- (BOOL)respondsToSelector:(SEL)selector
{
return [super respondsToSelector:selector] || [_expoAppDelegate respondsToSelector:selector];
}
// Forwards all invocations to `ExpoAppDelegate` object.
- (id)forwardingTargetForSelector:(SEL)selector
{
return _expoAppDelegate;
}
#pragma mark - UIApplicationDelegate
// Make sure to override all necessary methods from `RCTAppDelegate` here, explicitly forwarding everything to `_expoAppDelegate`.
// `forwardingTargetForSelector` works only for methods that are not specified in this and `RCTAppDelegate` classes.
#if !TARGET_OS_OSX
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
return [_expoAppDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
return [_expoAppDelegate applicationDidBecomeActive:application];
}
#else
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
return [_expoAppDelegate applicationDidFinishLaunching:notification];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
return [_expoAppDelegate applicationDidBecomeActive:notification];
}
#endif
- (UIViewController *)createRootViewController
{
return [_expoAppDelegate.factory.delegate createRootViewController];
}
- (void)customizeRootView:(UIView *)rootView
{
[_expoAppDelegate.factory.delegate customizeRootView:rootView];
}
#pragma mark - RCTComponentViewFactoryComponentProvider
- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
return self.dependencyProvider.thirdPartyFabricComponents;
}
#pragma mark - RCTHostDelegate
- (void)hostDidStart:(RCTHost *)host
{
}
- (void)host:(RCTHost *)host
didReceiveJSErrorStack:(NSArray<NSDictionary<NSString *, id> *> *)stack
message:(NSString *)message
exceptionId:(NSUInteger)exceptionId
isFatal:(BOOL)isFatal
{
}
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
return [_expoAppDelegate.factory.delegate getModuleInstanceFromClass:moduleClass];
}
- (Class)getModuleClassFromName:(const char *)name {
return [_expoAppDelegate.factory.delegate getModuleClassFromName:name];
}
#pragma mark - Helpers
+ (void)customizeRootView:(nonnull UIView *)rootView byAppDelegate:(nonnull RCTAppDelegate *)appDelegate
{
[appDelegate customizeRootView:(RCTRootView *)rootView];
}
@end
@@ -0,0 +1,15 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
Loads `ExpoAppDelegate` subscribers based on
the list from the generated `ExpoModulesProvider`.
*/
@interface EXAppDelegatesLoader : NSObject
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,22 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Expo/EXLegacyAppDelegateWrapper.h>
#import <Expo/EXAppDelegatesLoader.h>
#import <Expo/Swift.h>
// Make the legacy wrapper conform to the protocol for subscribers.
@interface EXLegacyAppDelegateWrapper () <EXAppDelegateSubscriberProtocol>
@end
@implementation EXAppDelegatesLoader
// App delegate providers must be registered before any `AppDelegate` life-cycle event is called.
// Unfortunately it's not possible in Swift to run code right after the binary is loaded
// and before any code is executed, so we switch back to Objective-C just to do this one thing.
+ (void)load
{
[AppDelegatesLoaderDelegate registerAppDelegateSubscribers:[[EXLegacyAppDelegateWrapper alloc] init]];
}
@end
@@ -0,0 +1,19 @@
// Copyright © 2018 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
The legacy wrapper is still used to forward app delegate calls to singleton modules.
See `EXAppDelegatesLoader.m` which registers this class as a subscriber of `ExpoAppDelegate`.
*/
#if TARGET_OS_OSX
@interface EXLegacyAppDelegateWrapper : NSResponder <NSApplicationDelegate>
#else
@interface EXLegacyAppDelegateWrapper : UIResponder <UIApplicationDelegate>
#endif
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,272 @@
// Copyright © 2018 650 Industries. All rights reserved.
#import <Expo/EXLegacyAppDelegateWrapper.h>
#import <ExpoModulesCore/EXSingletonModule.h>
#import <ExpoModulesCore/Platform.h>
#import <ExpoModulesCore/EXModuleRegistryProvider.h>
#import <Foundation/FoundationErrors.h>
#if !TARGET_OS_OSX
static NSMutableArray<id<UIApplicationDelegate>> *subcontractors;
static NSMutableDictionary<NSString *,NSArray<id<UIApplicationDelegate>> *> *subcontractorsForSelector;
static dispatch_once_t onceToken;
#endif
@implementation EXLegacyAppDelegateWrapper
// The legacy app delegate wrapper is not supported on macOS, but we keep it no-op for convenience.
#if !TARGET_OS_OSX
@synthesize window = _window;
- (void)forwardInvocation:(NSInvocation *)invocation {
#if DEBUG
SEL selector = [invocation selector];
NSArray<id<UIApplicationDelegate>> *delegatesToBeCalled = [self getSubcontractorsImplementingSelector:selector];
NSString *selectorName = NSStringFromSelector(selector);
if ([delegatesToBeCalled count] > 0) {
[NSException raise:@"Method not implemented in UIApplicationDelegate" format:@"Some modules: %@ have registered for `%@` UIApplicationDelegate's callback, however, neither your AppDelegate nor %@ can handle this method. You'll need to either implement this method in your AppDelegate or submit a pull request to handle it in %@.", delegatesToBeCalled, selectorName, NSStringFromClass([self class]), NSStringFromClass([self class])];
}
#endif
[super forwardInvocation:invocation];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
BOOL answer = NO;
SEL selector = @selector(application:didFinishLaunchingWithOptions:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
BOOL subcontractorAnswer = NO;
subcontractorAnswer = [subcontractor application:application didFinishLaunchingWithOptions:launchOptions];
answer |= subcontractorAnswer;
}
return answer;
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
SEL selector = @selector(applicationWillEnterForeground:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor applicationWillEnterForeground:application];
}
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
SEL selector = @selector(application:openURL:options:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
if ([subcontractor application:app openURL:url options:options]) {
return YES;
}
}
return NO;
}
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
SEL selector = @selector(application:performFetchWithCompletionHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block NSUInteger subcontractorsLeft = [subcontractorsArray count];
__block UIBackgroundFetchResult fetchResult = UIBackgroundFetchResultNoData;
__block NSObject *lock = [NSObject new];
void (^handler)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult result) {
@synchronized (lock) {
if (result == UIBackgroundFetchResultFailed) {
fetchResult = UIBackgroundFetchResultFailed;
} else if (fetchResult != UIBackgroundFetchResultFailed && result == UIBackgroundFetchResultNewData) {
fetchResult = UIBackgroundFetchResultNewData;
}
subcontractorsLeft--;
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
}
}
};
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
} else {
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application performFetchWithCompletionHandler:handler];
}
}
}
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
SEL selector = @selector(application:continueUserActivity:restorationHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block NSMutableArray<id<UIUserActivityRestoring>> * _Nullable mergedParams = [NSMutableArray new];
__block NSUInteger subcontractorsLeft = [subcontractorsArray count];
__block NSObject *lock = [NSObject new];
void (^handler)(NSArray<id<UIUserActivityRestoring>> * _Nullable) = ^(NSArray<id<UIUserActivityRestoring>> * _Nullable param) {
@synchronized (lock) {
[mergedParams addObjectsFromArray:param];
subcontractorsLeft--;
if (subcontractorsLeft == 0) {
restorationHandler(mergedParams);
}
}
};
BOOL result = NO;
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
result = result || [subcontractor application:application continueUserActivity:userActivity restorationHandler:handler];
}
return result;
}
#pragma mark - BackgroundSession
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
{
SEL selector = @selector(application:handleEventsForBackgroundURLSession:completionHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block BOOL delegatingCompleted = NO;
__block int delegatesCompleted = 0;
__block unsigned long allDelegates = subcontractorsArray.count;
__block void (^completionHandlerCaller)(void) = ^ {
if (delegatesCompleted && delegatingCompleted == allDelegates) {
completionHandler();
}
};
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application handleEventsForBackgroundURLSession:identifier completionHandler:^(){
@synchronized (self) {
delegatesCompleted += 1;
completionHandlerCaller();
}
}];
}
@synchronized (self) {
delegatingCompleted = YES;
completionHandlerCaller();
}
}
#pragma mark - Notifications
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token
{
SEL selector = @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application didRegisterForRemoteNotificationsWithDeviceToken:token];
}
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)err
{
SEL selector = @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for(id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application didFailToRegisterForRemoteNotificationsWithError:err];
}
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler
{
SEL selector = @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block NSUInteger subcontractorsLeft = [subcontractorsArray count];
__block UIBackgroundFetchResult fetchResult = UIBackgroundFetchResultNoData;
__block NSObject *lock = [NSObject new];
void (^handler)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult result) {
@synchronized (lock) {
if (result == UIBackgroundFetchResultFailed) {
fetchResult = UIBackgroundFetchResultFailed;
} else if (fetchResult != UIBackgroundFetchResultFailed && result == UIBackgroundFetchResultNewData) {
fetchResult = UIBackgroundFetchResultNewData;
}
subcontractorsLeft--;
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
}
}
};
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
} else {
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:handler];
}
}
}
#pragma mark - Subcontractors
- (void)ensureSubcontractorsAreInitializedAndSorted {
dispatch_once(&onceToken, ^{
subcontractors = [[NSMutableArray alloc] init];
subcontractorsForSelector = [NSMutableDictionary new];
NSArray<EXSingletonModule*> * singletonModules = [[EXModuleRegistryProvider singletonModules] allObjects];
for (EXSingletonModule *singletonModule in singletonModules) {
if ([singletonModule conformsToProtocol:@protocol(UIApplicationDelegate)]) {
[subcontractors addObject:(id<UIApplicationDelegate>)singletonModule];
}
}
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"priority"
ascending:NO];
[subcontractors sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
});
}
- (NSArray<id<UIApplicationDelegate>> *)getSubcontractorsImplementingSelector:(SEL)selector {
[self ensureSubcontractorsAreInitializedAndSorted];
NSString *selectorKey = NSStringFromSelector(selector);
if (subcontractorsForSelector[selectorKey]) {
return subcontractorsForSelector[selectorKey];
}
NSMutableArray<id<UIApplicationDelegate>> *result = [NSMutableArray new];
for (id<UIApplicationDelegate> subcontractor in subcontractors) {
if ([subcontractor respondsToSelector:selector]) {
[result addObject:subcontractor];
}
}
subcontractorsForSelector[selectorKey] = result;
return result;
}
#endif // !TARGET_OS_OSX
@end
@@ -0,0 +1,33 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#pragma once
#import <ExpoModulesCore/Platform.h>
#import <Expo/RCTAppDelegateUmbrella.h>
NS_ASSUME_NONNULL_BEGIN
@class EXReactDelegate;
NS_SWIFT_NAME(ExpoReactRootViewFactory)
@interface EXReactRootViewFactory : RCTRootViewFactory
@property (nonatomic, weak, nullable) EXReactDelegate *reactDelegate;
/**
Initializer for EXAppDelegateWrapper integration
*/
- (instancetype)initWithReactDelegate:(nullable EXReactDelegate *)reactDelegate
configuration:(RCTRootViewFactoryConfiguration *)configuration
turboModuleManagerDelegate:(nullable id)turboModuleManagerDelegate;
/**
Calls super `viewWithModuleName:initialProperties:launchOptions:` from `RCTRootViewFactory`.
*/
- (UIView *)superViewWithModuleName:(NSString *)moduleName
initialProperties:(nullable NSDictionary *)initialProperties
launchOptions:(nullable NSDictionary *)launchOptions;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,55 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Expo/EXReactRootViewFactory.h>
#import <Expo/RCTAppDelegateUmbrella.h>
#import <Expo/Swift.h>
// When `use_frameworks!` is used, the generated Swift header is inside ExpoModulesCore module.
// Otherwise, it's available only locally with double-quoted imports.
#if __has_include(<ExpoModulesCore/ExpoModulesCore-Swift.h>)
#import <ExpoModulesCore/ExpoModulesCore-Swift.h>
#else
#import "ExpoModulesCore-Swift.h"
#endif
@interface RCTRootViewFactory ()
- (NSURL *)bundleURL;
@end
@implementation EXReactRootViewFactory
- (instancetype)initWithReactDelegate:(nullable EXReactDelegate *)reactDelegate
configuration:(RCTRootViewFactoryConfiguration *)configuration
turboModuleManagerDelegate:(nullable id<RCTTurboModuleManagerDelegate>)turboModuleManagerDelegate
{
if (self = [super initWithConfiguration:configuration andTurboModuleManagerDelegate:turboModuleManagerDelegate]) {
self.reactDelegate = reactDelegate;
}
return self;
}
- (UIView *)viewWithModuleName:(NSString *)moduleName
initialProperties:(nullable NSDictionary *)initialProperties
launchOptions:(nullable NSDictionary *)launchOptions
{
if (self.reactDelegate != nil) {
return [self.reactDelegate createReactRootViewWithModuleName:moduleName initialProperties:initialProperties launchOptions:launchOptions];
}
return [super viewWithModuleName:moduleName initialProperties:initialProperties launchOptions:launchOptions];
}
- (UIView *)superViewWithModuleName:(NSString *)moduleName
initialProperties:(nullable NSDictionary *)initialProperties
launchOptions:(nullable NSDictionary *)launchOptions
{
return [super viewWithModuleName:moduleName initialProperties:initialProperties launchOptions:launchOptions];
}
- (NSURL *)bundleURL
{
return [self.reactDelegate bundleURL] ?: [super bundleURL];
}
@end
+288
View File
@@ -0,0 +1,288 @@
import Dispatch
import Foundation
import ExpoModulesCore
import ReactAppDependencyProvider
/**
Note: you cannot subclass Swift from Objective-C, use EXAppDelegateWrapper with Obj-C app delegates
Allows classes extending `ExpoAppDelegateSubscriber` to hook into project's app delegate
by forwarding `UIApplicationDelegate` events to the subscribers.
Keep functions and markers in sync with https://developer.apple.com/documentation/uikit/uiapplicationdelegate
*/
@objc(EXExpoAppDelegate)
open class ExpoAppDelegate: UIResponder, @preconcurrency ReactNativeFactoryProvider, UIApplicationDelegate {
@objc public var factory: RCTReactNativeFactory?
private let defaultModuleName = "main"
private let defaultInitialProps = [AnyHashable: Any]()
public func bindReactNativeFactory(_ factory: RCTReactNativeFactory) {
self.factory = factory
}
public func recreateRootView(
withBundleURL: URL?,
moduleName: String?,
initialProps: [AnyHashable: Any]?,
launchOptions: [AnyHashable: Any]?
) -> UIView {
guard let delegate = self.factory?.delegate,
let rootViewFactory = self.factory?.rootViewFactory else {
fatalError("recreateRootView: Missing factory in ExpoAppDelegate")
}
if delegate.newArchEnabled() {
// chrfalch: rootViewFactory.reactHost is not available here in swift due to the underlying RCTHost type of the property. (todo: check)
assert(rootViewFactory.value(forKey: "reactHost") == nil, "recreateRootViewWithBundleURL: does not support when react instance is created")
} else {
assert(rootViewFactory.bridge == nil, "recreateRootViewWithBundleURL: does not support when react instance is created")
}
let configuration = rootViewFactory.value(forKey: "_configuration") as? RCTRootViewFactoryConfiguration
if let bundleURL = withBundleURL {
configuration?.bundleURLBlock = {
return bundleURL
}
}
let rootView: UIView
if let factory = rootViewFactory as? ExpoReactRootViewFactory {
// When calling `recreateRootViewWithBundleURL:` from `EXReactRootViewFactory`,
// we don't want to loop the ReactDelegate again. Otherwise, it will be an infinite loop.
rootView = factory.superView(
withModuleName: moduleName ?? defaultModuleName,
initialProperties: initialProps,
launchOptions: launchOptions ?? [:]
)
} else {
rootView = rootViewFactory.view(
withModuleName: moduleName ?? defaultModuleName,
initialProperties: initialProps,
launchOptions: launchOptions
)
}
return rootView
}
// MARK: - Initializing the App
#if os(iOS) || os(tvOS)
open func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return ExpoAppDelegateSubscriberManager.application(application, willFinishLaunchingWithOptions: launchOptions)
}
open func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return ExpoAppDelegateSubscriberManager.application(application, didFinishLaunchingWithOptions: launchOptions)
}
#elseif os(macOS)
open func applicationWillFinishLaunching(_ notification: Notification) {
ExpoAppDelegateSubscriberManager.applicationWillFinishLaunching(notification)
}
open func applicationDidFinishLaunching(_ notification: Notification) {
ExpoAppDelegateSubscriberManager.applicationDidFinishLaunching(notification)
}
// TODO: - Configuring and Discarding Scenes
#endif
// MARK: - Responding to App Life-Cycle Events
#if os(iOS) || os(tvOS)
@objc
open func applicationDidBecomeActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberManager.applicationDidBecomeActive(application)
}
@objc
open func applicationWillResignActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberManager.applicationWillResignActive(application)
}
@objc
open func applicationDidEnterBackground(_ application: UIApplication) {
ExpoAppDelegateSubscriberManager.applicationDidEnterBackground(application)
}
open func applicationWillEnterForeground(_ application: UIApplication) {
ExpoAppDelegateSubscriberManager.applicationWillEnterForeground(application)
}
open func applicationWillTerminate(_ application: UIApplication) {
ExpoAppDelegateSubscriberManager.applicationWillTerminate(application)
}
#elseif os(macOS)
@objc
open func applicationDidBecomeActive(_ notification: Notification) {
ExpoAppDelegateSubscriberManager.applicationDidBecomeActive(notification)
}
@objc
open func applicationWillResignActive(_ notification: Notification) {
ExpoAppDelegateSubscriberManager.applicationWillResignActive(notification)
}
@objc
open func applicationDidHide(_ notification: Notification) {
ExpoAppDelegateSubscriberManager.applicationDidHide(notification)
}
open func applicationWillUnhide(_ notification: Notification) {
ExpoAppDelegateSubscriberManager.applicationWillUnhide(notification)
}
open func applicationWillTerminate(_ notification: Notification) {
ExpoAppDelegateSubscriberManager.applicationWillTerminate(notification)
}
#endif
// MARK: - Responding to Environment Changes
#if os(iOS) || os(tvOS)
open func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
ExpoAppDelegateSubscriberManager.applicationDidReceiveMemoryWarning(application)
}
#endif
// TODO: - Managing App State Restoration
// MARK: - Downloading Data in the Background
#if os(iOS) || os(tvOS)
open func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
ExpoAppDelegateSubscriberManager.application(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler)
}
#endif
// MARK: - Handling Remote Notification Registration
open func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
ExpoAppDelegateSubscriberManager.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
open func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
ExpoAppDelegateSubscriberManager.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
}
#if os(iOS) || os(tvOS)
open func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
ExpoAppDelegateSubscriberManager.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
}
#elseif os(macOS)
open func application(
_ application: NSApplication,
didReceiveRemoteNotification userInfo: [String: Any]
) {
ExpoAppDelegateSubscriberManager.application(application, didReceiveRemoteNotification: userInfo)
}
#endif
// MARK: - Continuing User Activity and Handling Quick Actions
open func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
return ExpoAppDelegateSubscriberManager.application(application, willContinueUserActivityWithType: userActivityType)
}
#if os(iOS) || os(tvOS)
open func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return ExpoAppDelegateSubscriberManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
#elseif os(macOS)
open func application(
_ application: NSApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void
) -> Bool {
return ExpoAppDelegateSubscriberManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
#endif
open func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
return ExpoAppDelegateSubscriberManager.application(application, didUpdate: userActivity)
}
open func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
return ExpoAppDelegateSubscriberManager.application(application, didFailToContinueUserActivityWithType: userActivityType, error: error)
}
#if os(iOS)
open func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
ExpoAppDelegateSubscriberManager.application(application, performActionFor: shortcutItem, completionHandler: completionHandler)
}
#endif
// MARK: - Background Fetch
#if os(iOS) || os(tvOS)
open func application(
_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
ExpoAppDelegateSubscriberManager.application(application, performFetchWithCompletionHandler: completionHandler)
}
// TODO: - Interacting With WatchKit
// TODO: - Interacting With HealthKit
#endif
// MARK: - Opening a URL-Specified Resource
#if os(iOS) || os(tvOS)
open func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return ExpoAppDelegateSubscriberManager.application(app, open: url, options: options)
}
#elseif os(macOS)
open func application(_ app: NSApplication, open urls: [URL]) {
ExpoAppDelegateSubscriberManager.application(app, open: urls)
}
#endif
// TODO: - Disallowing Specified App Extension Types
// TODO: - Handling SiriKit Intents
// TODO: - Handling CloudKit Invitations
// MARK: - Managing Interface Geometry
#if os(iOS)
/**
* Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested
* a different orientation.
*/
open func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return ExpoAppDelegateSubscriberManager.application(application, supportedInterfaceOrientationsFor: window)
}
#endif
}
@@ -0,0 +1,138 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import React
public class ExpoReactNativeFactory: RCTReactNativeFactory, ExpoReactNativeFactoryProtocol {
private let defaultModuleName = "main"
private lazy var reactDelegate: ExpoReactDelegate = {
ExpoReactDelegate(
handlers: ExpoAppDelegateSubscriberRepository.reactDelegateHandlers,
reactNativeFactory: self
)
}()
// TODO: Remove check when react-native-macos 0.81 is released
#if !os(macOS)
@objc public override init(delegate: any RCTReactNativeFactoryDelegate) {
let releaseLevel = (Bundle.main.object(forInfoDictionaryKey: "ReactNativeReleaseLevel") as? String)
.flatMap { [
"canary": RCTReleaseLevel.Canary,
"experimental": RCTReleaseLevel.Experimental,
"stable": RCTReleaseLevel.Stable
][$0.lowercased()]
}
?? RCTReleaseLevel.Stable
super.init(delegate: delegate, releaseLevel: releaseLevel)
}
#endif
@MainActor
@objc func createRCTRootViewFactory() -> RCTRootViewFactory {
// Alan: This is temporary. We need to cast to ExpoReactNativeFactoryDelegate here because currently, if you extend RCTReactNativeFactory
// from Swift, customizeRootView will not work on the new arch because the cast to RCTRootView will never
// succeed which breaks expo-splash-screen and react-native-bootsplash.
guard let weakDelegate = self.delegate as? ExpoReactNativeFactoryDelegate else {
fatalError("ExpoReactNativeFactory: delegate is nil.")
}
let bundleUrlBlock: RCTBundleURLBlock = { [weak weakDelegate] in
return weakDelegate?.bundleURL()
}
let configuration = RCTRootViewFactoryConfiguration(
bundleURLBlock: bundleUrlBlock,
newArchEnabled: weakDelegate.newArchEnabled()
)
configuration.createRootViewWithBridge = { bridge, moduleName, initProps in
return weakDelegate.createRootView(with: bridge, moduleName: moduleName, initProps: initProps)
}
configuration.jsRuntimeConfiguratorDelegate = delegate
configuration.createBridgeWithDelegate = { delegate, launchOptions in
weakDelegate.createBridge(with: delegate, launchOptions: launchOptions)
}
configuration.customizeRootView = { rootView in
weakDelegate.customize(rootView)
}
// NOTE(kudo): `sourceURLForBridge` is not referenced intentionally because it does not support New Architecture.
configuration.sourceURLForBridge = nil
configuration.loadSourceForBridgeWithProgress = { bridge, onProgress, onComplete in
weakDelegate.loadSource(for: bridge, onProgress: onProgress, onComplete: onComplete)
}
if weakDelegate.responds(to: #selector(RCTReactNativeFactoryDelegate.extraModules(for:))) {
configuration.extraModulesForBridge = { bridge in
weakDelegate.extraModules(for: bridge)
}
}
if weakDelegate.responds(to: #selector(RCTReactNativeFactoryDelegate.extraLazyModuleClasses(for:))) {
configuration.extraLazyModuleClassesForBridge = { bridge in
weakDelegate.extraLazyModuleClasses(for: bridge)
}
}
if weakDelegate.responds(to: #selector(RCTReactNativeFactoryDelegate.bridge(_:didNotFindModule:))) {
configuration.bridgeDidNotFindModule = { bridge, moduleName in
weakDelegate.bridge(bridge, didNotFindModule: moduleName)
}
}
return ExpoReactRootViewFactory(
reactDelegate: reactDelegate,
configuration: configuration,
turboModuleManagerDelegate: self
)
}
public func recreateRootView(
withBundleURL: URL?,
moduleName: String?,
initialProps: [AnyHashable: Any]?,
launchOptions: [AnyHashable: Any]?
) -> UIView {
guard let delegate = self.delegate else {
fatalError("recreateRootView: Missing RCTReactNativeFactoryDelegate")
}
if RCTIsNewArchEnabled() {
// chrfalch: rootViewFactory.reactHost is not available here in swift due to the underlying RCTHost type of the property. (todo: check)
assert(self.rootViewFactory.value(forKey: "reactHost") == nil, "recreateRootViewWithBundleURL: does not support when react instance is created")
} else {
assert(self.rootViewFactory.bridge == nil, "recreateRootViewWithBundleURL: does not support when react instance is created")
}
let configuration = self.rootViewFactory.value(forKey: "_configuration") as? RCTRootViewFactoryConfiguration
if let bundleURL = withBundleURL {
configuration?.bundleURLBlock = {
return bundleURL
}
}
let rootView: UIView
if let factory = self.rootViewFactory as? ExpoReactRootViewFactory {
// When calling `recreateRootViewWithBundleURL:` from `EXReactRootViewFactory`,
// we don't want to loop the ReactDelegate again. Otherwise, it will be an infinite loop.
rootView = factory.superView(
withModuleName: moduleName ?? defaultModuleName,
initialProperties: initialProps,
launchOptions: launchOptions ?? [:]
)
} else {
rootView = rootViewFactory.view(
withModuleName: moduleName ?? defaultModuleName,
initialProperties: initialProps,
launchOptions: launchOptions
)
}
return rootView
}
}
@@ -0,0 +1,13 @@
import React
open class ExpoReactNativeFactoryDelegate: RCTDefaultReactNativeFactoryDelegate {
open override func customize(_ rootView: UIView) {
ExpoAppDelegateSubscriberRepository.subscribers.forEach { $0.customizeRootView?(rootView) }
}
open override func createRootViewController() -> UIViewController {
return ExpoAppDelegateSubscriberRepository.reactDelegateHandlers.lazy
.compactMap { $0.createRootViewController() }
.first(where: { _ in true }) ?? UIViewController()
}
}
@@ -0,0 +1,29 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
`React-RCTAppDelegate-umbrella.h` requires `USE_HERMES`.
This umbrella wrapper lets us import `React-RCTAppDelegate-umbrella.h` in `Expo.h` without requiring `USE_HERMES` from app project settings.
*/
#if __has_include(<reacthermes/HermesExecutorFactory.h>)
#ifndef USE_HERMES
#define USE_HERMES 1
#define INLINE_USE_HERMES 1
#endif // USE_HERMES
#endif // __has_include(<reacthermes/HermesExecutorFactory.h>)
#if __has_include(<React_RCTAppDelegate/React-RCTAppDelegate-umbrella.h>)
#import <React_RCTAppDelegate/React-RCTAppDelegate-umbrella.h>
#else
#import <React_RCTAppDelegate/React_RCTAppDelegate-umbrella.h>
#endif
#if INLINE_USE_HERMES
#undef USE_HERMES
#undef INLINE_USE_HERMES
#endif // INLINE_USE_HERMES
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
This class loads some preprocessors and pass into `EXAppDefines` of ExpoModulesCore.
*/
@interface EXAppDefinesLoader : NSObject
@end
NS_ASSUME_NONNULL_END
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <Expo/EXAppDefinesLoader.h>
#import <ExpoModulesCore/ExpoModulesCore.h>
#import <ExpoModulesCore/EXAppDefines.h>
#import <React/RCTDefines.h>
@implementation EXAppDefinesLoader
+ (void)load
{
BOOL APP_DEBUG;
[EXAppDefines load:@{
#if DEBUG
@"APP_DEBUG": @(YES),
#else
@"APP_DEBUG": @(NO),
#endif
@"APP_RCT_DEBUG": @(RCT_DEBUG),
@"APP_RCT_DEV": @(RCT_DEV),
#if RCT_NEW_ARCH_ENABLED
@"APP_NEW_ARCH_ENABLED": @(YES),
#else
@"APP_NEW_ARCH_ENABLED": @(NO),
#endif
}];
}
@end
+3
View File
@@ -0,0 +1,3 @@
#import <ExpoModulesCore/ExpoModulesCore.h>
#import <Expo/EXAppDefinesLoader.h>
#import <Expo/EXAppDelegateWrapper.h>
+2
View File
@@ -0,0 +1,2 @@
// Export public API from the modules core, so modules and the AppDelegate can simply `import Expo`.
@_exported import ExpoModulesCore
@@ -0,0 +1,12 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
For callsites to customize network fetch functionalities like having custom `URLSessionConfiguration`.
*/
@objc(EXFetchCustomExtension)
public class ExpoFetchCustomExtension: NSObject {
@MainActor @objc
public static func setCustomURLSessionConfigurationProvider(_ provider: NSURLSessionConfigurationProvider?) {
urlSessionConfigurationProvider = provider
}
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2015-present 650 Industries. All rights reserved.
@preconcurrency import ExpoModulesCore
private let fetchRequestQueue = DispatchQueue(label: "expo.modules.fetch.RequestQueue")
@MainActor internal var urlSessionConfigurationProvider: NSURLSessionConfigurationProvider?
@MainActor public final class ExpoFetchModule: @preconcurrency Module {
private lazy var urlSession = createURLSession()
private let urlSessionDelegate: URLSessionSessionDelegateProxy
public required init(appContext: AppContext) {
urlSessionDelegate = URLSessionSessionDelegateProxy(dispatchQueue: fetchRequestQueue)
super.init(appContext: appContext)
}
public func definition() -> ModuleDefinition {
Name("ExpoFetchModule")
OnDestroy {
urlSession.invalidateAndCancel()
}
// swiftlint:disable:next closure_body_length
Class(NativeResponse.self) {
Constructor {
return NativeResponse(dispatchQueue: fetchRequestQueue)
}
AsyncFunction("startStreaming") { (response: NativeResponse) -> Data? in
return response.startStreaming()
}.runOnQueue(fetchRequestQueue)
AsyncFunction("cancelStreaming") { (response: NativeResponse, _ reason: String) in
response.cancelStreaming()
}.runOnQueue(fetchRequestQueue)
Property("bodyUsed", \.bodyUsed)
Property("_rawHeaders") { (response: NativeResponse) in
return response.responseInit?.headers ?? []
}
Property("status") { (response: NativeResponse) in
return response.responseInit?.status ?? -1
}
Property("statusText") { (response: NativeResponse) in
return response.responseInit?.statusText ?? ""
}
Property("url") { (response: NativeResponse) in
return response.responseInit?.url ?? ""
}
Property("redirected", \.redirected)
AsyncFunction("arrayBuffer") { (response: NativeResponse, promise: Promise) in
response.waitFor(states: [.bodyCompleted]) { _ in
let data = response.sink.finalize()
promise.resolve(data)
}
}.runOnQueue(fetchRequestQueue)
AsyncFunction("text") { (response: NativeResponse, promise: Promise) in
response.waitFor(states: [.bodyCompleted]) { _ in
let data = response.sink.finalize()
let text = String(decoding: data, as: UTF8.self)
promise.resolve(text)
}
}.runOnQueue(fetchRequestQueue)
}
Class(NativeRequest.self) {
Constructor { (nativeResponse: NativeResponse) in
return NativeRequest(response: nativeResponse)
}
AsyncFunction("start") { (request: NativeRequest, url: URL, requestInit: NativeRequestInit, requestBody: Data?, promise: Promise) in
request.start(
urlSession: urlSession,
urlSessionDelegate: urlSessionDelegate,
url: url,
requestInit: requestInit,
requestBody: requestBody
)
request.response.waitFor(states: [.responseReceived, .errorReceived]) { state in
if state == .responseReceived {
promise.resolve()
} else if state == .errorReceived {
promise.reject(request.response.error ?? FetchUnknownException())
}
}
}.runOnQueue(fetchRequestQueue)
AsyncFunction("cancel") { (request: NativeRequest) in
request.cancel(urlSessionDelegate: self.urlSessionDelegate)
}.runOnQueue(fetchRequestQueue)
}
}
private func createURLSession() -> URLSession {
let config: URLSessionConfiguration
if let urlSessionConfigurationProvider, let concreteConfig = urlSessionConfigurationProvider() {
config = concreteConfig
} else {
config = URLSessionConfiguration.default
config.httpShouldSetCookies = true
config.httpCookieAcceptPolicy = .always
config.httpCookieStorage = HTTPCookieStorage.shared
let useWifiOnly = Bundle.main.infoDictionary?["ReactNetworkForceWifiOnly"] as? Bool ?? false
if useWifiOnly {
config.allowsCellularAccess = !useWifiOnly
}
}
return URLSession(configuration: config, delegate: urlSessionDelegate, delegateQueue: nil)
}
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
/**
An URLSessionDataTask wrapper.
*/
internal final class ExpoURLSessionTask: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate, @unchecked Sendable {
private let delegate: ExpoURLSessionTaskDelegate
private var task: URLSessionDataTask?
init(delegate: ExpoURLSessionTaskDelegate) {
self.delegate = delegate
super.init()
}
func start(
urlSession: URLSession,
urlSessionDelegate: URLSessionSessionDelegateProxy,
url: URL,
requestInit: NativeRequestInit,
requestBody: Data?
) {
let request = NSMutableURLRequest(url: url)
URLProtocol.setProperty(requestInit.redirect == .follow, forKey: "shouldFollowRedirects", in: request)
request.httpMethod = requestInit.method
request.timeoutInterval = 0
if requestInit.credentials == .include {
request.httpShouldHandleCookies = true
if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
request.allHTTPHeaderFields = HTTPCookie.requestHeaderFields(with: cookies)
}
} else {
request.httpShouldHandleCookies = false
}
for tuple in requestInit.headers {
request.addValue(tuple[1], forHTTPHeaderField: tuple[0])
}
request.httpBody = requestBody
let task = urlSession.dataTask(with: request as URLRequest)
urlSessionDelegate.addDelegate(task: task, delegate: self)
self.task = task
task.resume()
self.delegate.urlSessionDidStart(self)
}
func cancel(urlSessionDelegate: URLSessionSessionDelegateProxy) {
if let task {
urlSessionDelegate.removeDelegate(task: task)
task.cancel()
}
}
// MARK: - URLSessionTaskDelegate/URLSessionDataDelegate implementations
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void
) {
self.delegate.urlSession(
self,
task: task,
willPerformHTTPRedirection: response,
newRequest: request,
completionHandler: completionHandler)
}
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
) {
self.delegate.urlSession(self, didReceive: response)
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.delegate.urlSession(self, didReceive: data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.delegate.urlSession(self, task: task, didCompleteWithError: error)
}
}
internal protocol ExpoURLSessionTaskDelegate: AnyObject, Sendable {
func urlSessionDidStart(_ session: ExpoURLSessionTask)
func urlSession(_ session: ExpoURLSessionTask, didReceive response: URLResponse)
func urlSession(_ session: ExpoURLSessionTask, didReceive data: Data)
func urlSession(
_ session: ExpoURLSessionTask,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void
)
func urlSession(_ session: ExpoURLSessionTask, task: URLSessionTask, didCompleteWithError error: Error?)
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
internal final class FetchUnknownException: Exception {
override var reason: String {
"Unknown error"
}
}
internal final class FetchRequestCanceledException: Exception {
override var reason: String {
"Fetch request has been canceled"
}
}
internal final class FetchRedirectException: Exception {
override var reason: String {
"Redirect is not allowed when redirect mode is 'error'"
}
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
/**
A SharedObject for request.
*/
internal final class NativeRequest: SharedObject, @unchecked Sendable {
internal let response: NativeResponse
internal let task: ExpoURLSessionTask
init(response: NativeResponse) {
self.response = response
self.task = ExpoURLSessionTask(delegate: self.response)
}
func start(
urlSession: URLSession,
urlSessionDelegate: URLSessionSessionDelegateProxy,
url: URL,
requestInit: NativeRequestInit,
requestBody: Data?
) {
self.response.redirectMode = requestInit.redirect
self.task.start(
urlSession: urlSession,
urlSessionDelegate: urlSessionDelegate,
url: url,
requestInit: requestInit,
requestBody: requestBody
)
}
func cancel(urlSessionDelegate: URLSessionSessionDelegateProxy) {
self.task.cancel(urlSessionDelegate: urlSessionDelegate)
self.response.emitRequestCanceled()
}
}
@@ -0,0 +1,11 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
/**
Enum for RequestInit.credentials.
*/
internal enum NativeRequestCredentials: String, Enumerable {
case include
case omit
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
/**
Record for RequestInit.
*/
internal struct NativeRequestInit: Record {
@Field
var credentials: NativeRequestCredentials = .include
@Field
var headers: [[String]] = []
@Field
var method: String = "GET"
@Field
var redirect: NativeRequestRedirect = .follow
}
+10
View File
@@ -0,0 +1,10 @@
import ExpoModulesCore
/**
Enum for RequestInit.redirect.
*/
internal enum NativeRequestRedirect: String, Enumerable {
case follow
case manual
case error
}
+227
View File
@@ -0,0 +1,227 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
/**
A SharedObject for response.
*/
internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate, @unchecked Sendable {
internal let sink: ResponseSink
private let dispatchQueue: DispatchQueue
private(set) var state: ResponseState = .intialized {
didSet {
dispatchQueue.async { [weak self] in
guard let self else {
return
}
self.stateChangeOnceListeners.removeAll { $0(self.state) == true }
}
}
}
private typealias StateChangeListener = (ResponseState) -> Bool
private var stateChangeOnceListeners: [StateChangeListener] = []
private(set) var responseInit: NativeResponseInit?
private(set) var redirected = false
private(set) var error: Error?
var redirectMode: NativeRequestRedirect = .follow
var bodyUsed: Bool {
return self.sink.bodyUsed
}
init(dispatchQueue: DispatchQueue) {
self.sink = ResponseSink()
self.dispatchQueue = dispatchQueue
}
func startStreaming() -> Data? {
if isInvalidState(.responseReceived, .bodyCompleted) {
return nil
}
if state == .responseReceived {
state = .bodyStreamingStarted
let queuedData = self.sink.finalize()
emit(event: "didReceiveResponseData", arguments: queuedData)
} else if state == .bodyCompleted {
let queuedData = self.sink.finalize()
return queuedData
}
return nil
}
func cancelStreaming() {
if isInvalidState(.bodyStreamingStarted) {
return
}
state = .bodyStreamingCanceled
}
func emitRequestCanceled() {
let error = FetchRequestCanceledException()
self.error = error
if state == .bodyStreamingStarted {
emit(event: "didFailWithError", arguments: error.localizedDescription)
}
state = .errorReceived
emit(event: "readyForJSFinalization")
}
/**
Waits for given states and when it meets the requirement, executes the callback.
*/
func waitFor(states: [ResponseState], callback: @escaping @Sendable (ResponseState) -> Void) {
if states.contains(state) {
callback(state)
return
}
dispatchQueue.async { [weak self] () in
guard let self else {
return
}
self.stateChangeOnceListeners.append { newState in
if states.contains(newState) {
callback(newState)
return true
}
return false
}
}
}
/**
Check valid state machine
*/
private func isInvalidState(_ validStates: ResponseState...) -> Bool {
if validStates.contains(state) {
return false
}
let validStatesString = validStates.map { "\($0.rawValue)" }.joined(separator: ",")
log.error("Invalid state - currentState[\(state.rawValue)] validStates[\(validStatesString)]")
return true
}
/**
Factory of NativeResponseInit
*/
private static func createResponseInit(response: URLResponse) -> NativeResponseInit? {
guard let httpResponse = response as? HTTPURLResponse else {
return NativeResponseInit(
headers: [], status: 200, statusText: "", url: response.url?.absoluteString ?? ""
)
}
let status = httpResponse.statusCode
let statusText = HTTPURLResponse.localizedString(forStatusCode: status)
let headers = httpResponse.allHeaderFields.reduce(into: [[String]]()) { result, header in
if let key = header.key as? String, let value = header.value as? String {
result.append([key, value])
}
}
let url = httpResponse.url?.absoluteString ?? ""
return NativeResponseInit(
headers: headers, status: status, statusText: statusText, url: url
)
}
// MARK: - ExpoURLSessionTaskDelegate implementations
func urlSessionDidStart(_ session: ExpoURLSessionTask) {
if isInvalidState(.intialized) {
return
}
state = .started
}
func urlSession(_ session: ExpoURLSessionTask, didReceive response: URLResponse) {
if isInvalidState(.started) {
return
}
responseInit = Self.createResponseInit(response: response)
state = .responseReceived
}
func urlSession(_ session: ExpoURLSessionTask, didReceive data: Data) {
if isInvalidState(.responseReceived, .bodyStreamingStarted, .bodyStreamingCanceled) {
return
}
if state == .responseReceived {
self.sink.appendBufferBody(data: data)
} else if state == .bodyStreamingStarted {
emit(event: "didReceiveResponseData", arguments: data)
}
// no-op in .bodyStreamingCanceled state
}
func urlSession(
_ session: ExpoURLSessionTask,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void
) {
let shouldFollowRedirects = self.redirectMode == .follow
completionHandler(shouldFollowRedirects ? request : nil)
self.redirected = shouldFollowRedirects
if self.redirectMode == .error {
let error = FetchRedirectException()
self.error = error
if state == .bodyStreamingStarted {
emit(event: "didFailWithError", arguments: error.localizedDescription)
}
state = .errorReceived
emit(event: "readyForJSFinalization")
}
}
func urlSession(_ session: ExpoURLSessionTask, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
if isInvalidState(.started, .responseReceived, .bodyStreamingStarted, .bodyStreamingCanceled) {
return
}
if state == .started,
let urlError = error as? URLError,
urlError.code.rawValue == CFNetworkErrors.cfurlErrorFileDoesNotExist.rawValue,
let url = task.currentRequest?.url,
url.scheme == "file" {
// When requesting a local file that does not exist,
// the `urlSession(_:didReceive:)` method will not be called.
// Instead of throwing an exception, we generate a 404 response.
responseInit = NativeResponseInit(
headers: [], status: 404, statusText: "File not found", url: url.absoluteString)
// First, set the state to .responseReceived, and then to .errorReceived in the next loop.
// This simulates the state transition similar to HTTP requests.
state = .responseReceived
dispatchQueue.async { [weak self] in
guard let self else {
return
}
self.urlSession(session, task: task, didCompleteWithError: error)
}
return
}
if state == .bodyStreamingStarted {
if let error {
emit(event: "didFailWithError", arguments: error.localizedDescription)
} else {
emit(event: "didComplete")
}
}
if let error {
self.error = error
state = .errorReceived
} else {
state = .bodyCompleted
}
emit(event: "readyForJSFinalization")
}
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
Native data for ResponseInit.
*/
internal struct NativeResponseInit {
let headers: [[String]]
let status: Int
let statusText: String
let url: String
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
A data structure to store response body chunks
*/
internal final class ResponseSink {
private var bodyQueue: [Data] = []
private var isFinalized = false
private(set) var bodyUsed = false
func appendBufferBody(data: Data) {
bodyUsed = true
bodyQueue.append(data)
}
func finalize() -> Data {
let size = bodyQueue.reduce(0) { $0 + $1.count }
var result = Data(capacity: size)
while !bodyQueue.isEmpty {
let data = bodyQueue.removeFirst()
result.append(data)
}
bodyUsed = true
isFinalized = true
return result
}
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
States represent for native response.
*/
internal enum ResponseState: Int {
case intialized = 0
case started
case responseReceived
case bodyCompleted
case bodyStreamingStarted
case bodyStreamingCanceled
case errorReceived
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2018-present 650 Industries. All rights reserved.
// The generated swift header may depend on some Objective-C declarations,
// adding dependency imports here to prevent declarations not found errors.
#import <React/RCTHTTPRequestHandler.h>
// When `use_frameworks!` is used, the generated Swift header is inside ExpoModulesCore module.
// Otherwise, it's available only locally with double-quoted imports.
#if __has_include(<Expo/Expo-Swift.h>)
#import <Expo/Expo-Swift.h>
#else
#import "Expo-Swift.h"
#endif
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2025-present 650 Industries. All rights reserved.
import Testing
@testable import Expo
@Suite
struct ExpoAppDelegateTests {
@Test
func `extends UIResponder`() {
// Assert that `ExpoAppDelegate` extends from `UIResponder` so it doesn't regress again in the future.
// Otherwise, the `AppDelegate` would lose its responder behavior such as being able to handle touches and key presses.
#expect(ExpoAppDelegate.self is UIResponder.Type)
}
}