-
Notifications
You must be signed in to change notification settings - Fork 3.9k
feat: setup background task from scratch on iOS #53834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e7ca379
82ce553
4c96c52
f181955
cc214f8
4a55c9b
ec9680c
8da06e2
6a06a97
aefe68e
2f6dc68
d4d6a31
5de130d
138ad72
5d2abd1
3a2b3c9
82c42ac
532a007
99bf557
94769ea
a9d5755
e663e60
1b252dd
34813d6
0e504da
a062fc9
49632c5
a8884a0
74d6767
310e010
a1c707e
8a914f3
6276845
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| NEW_EXPENSIFY_URL=https:/$()/new.expensify.com/ | ||
| SECURE_EXPENSIFY_URL=https:/$()/secure.expensify.com/ | ||
| EXPENSIFY_URL=https:/$()/www.expensify.com/ | ||
| EXPENSIFY_PARTNER_NAME=chat-expensify-com | ||
| EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66 | ||
| PUSHER_APP_KEY=268df511a204fbb60884 | ||
| USE_WEB_PROXY=false | ||
| ENVIRONMENT=production | ||
| SEND_CRASH_REPORTS=true | ||
| FB_API_KEY=AIzaSyBrLKgCuo6Vem6Xi5RPokdumssW8HaWBow | ||
| FB_APP_ID=1:1008697809946:web:08de4ecb7656b7235445a3 | ||
| FB_PROJECT_ID=expensify-mobile-app |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| require "json" | ||
|
|
||
| package = JSON.parse(File.read(File.join(__dir__, "package.json"))) | ||
| folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' | ||
|
|
||
| Pod::Spec.new do |s| | ||
| s.name = "expensify-react-native-background-task" | ||
| s.version = package["version"] | ||
| s.summary = package["description"] | ||
| s.homepage = package["homepage"] | ||
| s.license = package["license"] | ||
| s.authors = package["author"] | ||
|
|
||
| s.platforms = { :ios => min_ios_version_supported } | ||
| s.source = { :git => ".git", :tag => "#{s.version}" } | ||
|
|
||
| s.source_files = "ios/**/*.{h,m,mm,cpp}" | ||
|
|
||
| install_modules_dependencies(s) | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| #import <Foundation/Foundation.h> | ||
| #import <BackgroundTasks/BackgroundTasks.h> | ||
| #import <React/RCTBridgeModule.h> | ||
|
|
||
| @interface RNBackgroundTaskManager : NSObject | ||
|
|
||
| @property (nonatomic, copy) void (^ _Nullable taskHandler)(BGTask * _Nonnull); | ||
|
|
||
| + (instancetype _Nullable )shared; | ||
| + (void)setup; | ||
| - (void)setHandlerForIdentifier:(NSString *_Nullable)identifier | ||
| completion:(void (^_Nullable)(BGTask * _Nonnull))handler; | ||
| - (void (^_Nullable)(BGTask * _Nonnull))handlerForIdentifier:(NSString *_Nullable)identifier; | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| #import <BackgroundTasks/BackgroundTasks.h> | ||
|
|
||
| @implementation RNBackgroundTaskManager : NSObject { | ||
| NSMutableDictionary<NSString *, void (^)(BGTask * _Nonnull)> *_handlers; | ||
| } | ||
|
|
||
| + (instancetype)shared { | ||
| static RNBackgroundTaskManager *instance = nil; | ||
| static dispatch_once_t onceToken; | ||
| dispatch_once(&onceToken, ^{ | ||
| instance = [[RNBackgroundTaskManager alloc] init]; | ||
| }); | ||
| return instance; | ||
| } | ||
|
|
||
| - (instancetype)init { | ||
| if (self = [super init]) { | ||
| _handlers = [NSMutableDictionary new]; | ||
| } | ||
| return self; | ||
| } | ||
|
|
||
| - (void)setHandlerForIdentifier:(NSString *)identifier | ||
| completion:(void (^)(BGTask * _Nonnull))handler { | ||
| _handlers[identifier] = handler; | ||
| } | ||
|
|
||
| - (void (^)(BGTask * _Nonnull))handlerForIdentifier:(NSString *)identifier { | ||
| return _handlers[identifier]; | ||
| } | ||
|
|
||
| + (void)setup { | ||
| NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; | ||
|
|
||
| if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { | ||
| NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); | ||
| } else { | ||
| for (NSString *identifier in backgroundIdentifiers) { | ||
| [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier | ||
| usingQueue:nil | ||
| launchHandler:^(BGTask * _Nonnull task) { | ||
| NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", task.identifier); | ||
| void (^handler)(BGTask * _Nonnull) = [[RNBackgroundTaskManager shared] handlerForIdentifier:task.identifier]; | ||
| if (handler) { | ||
| handler(task); | ||
| } | ||
| }]; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #ifdef RCT_NEW_ARCH_ENABLED | ||
| #import "RNReactNativeBackgroundTaskSpec.h" | ||
| #import <BackgroundTasks/BackgroundTasks.h> | ||
|
|
||
| @interface ReactNativeBackgroundTask : NativeReactNativeBackgroundTaskSpecBase <NativeReactNativeBackgroundTaskSpec> | ||
| #else | ||
| #import <React/RCTBridgeModule.h> | ||
| #import <BackgroundTasks/BackgroundTasks.h> | ||
|
|
||
| @interface ReactNativeBackgroundTask : NSObject <RCTBridgeModule> | ||
| #endif | ||
|
|
||
| - (void)defineTask:(NSString *)taskName | ||
| taskExecutor:(RCTResponseSenderBlock)taskExecutor | ||
| resolve:(RCTPromiseResolveBlock)resolve | ||
| reject:(RCTPromiseRejectBlock)reject; | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| #import "ReactNativeBackgroundTask.h" | ||
| #import <UIKit/UIKit.h> | ||
| #import <BackgroundTasks/BackgroundTasks.h> | ||
| #import "RNBackgroundTaskManager.h" | ||
|
|
||
| @implementation ReactNativeBackgroundTask { | ||
| NSMutableDictionary *_taskExecutors; | ||
| } | ||
|
|
||
| RCT_EXPORT_MODULE() | ||
|
|
||
| - (instancetype)init { | ||
| if (self = [super init]) { | ||
| _taskExecutors = [NSMutableDictionary new]; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we need any kind of concurrency safety for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think concurrency protection needed for
These are the only two places accessing |
||
| } | ||
| return self; | ||
| } | ||
|
|
||
| - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier error:(NSError **)outError { | ||
| BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:identifier]; | ||
|
|
||
| // Set earliest begin date to some time in the future | ||
| request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now | ||
|
|
||
| NSError *error = nil; | ||
| BOOL success = [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]; | ||
|
|
||
| if (!success) { | ||
| NSLog(@"[ReactNativeBackgroundTask] Failed to schedule task: %@", error.localizedDescription); | ||
| if (outError != nil) { | ||
| *outError = error; | ||
| } | ||
| } | ||
|
|
||
| return success; | ||
| } | ||
|
|
||
| RCT_EXPORT_METHOD(defineTask:(NSString *)taskName | ||
| taskExecutor:(RCTResponseSenderBlock)taskExecutor | ||
| resolve:(RCTPromiseResolveBlock)resolve | ||
| reject:(RCTPromiseRejectBlock)reject) | ||
| { | ||
| if (taskName == nil) { | ||
| NSLog(@"[ReactNativeBackgroundTask] Failed to define task: taskName is nil"); | ||
| reject(@"ERR_INVALID_TASK_NAME", @"Task name must be provided", nil); | ||
| return; | ||
| } | ||
|
|
||
| if (taskExecutor == nil) { | ||
| NSLog(@"[ReactNativeBackgroundTask] Failed to define task: taskExecutor is nil"); | ||
| reject(@"ERR_INVALID_TASK_EXECUTOR", @"Task executor must be provided", nil); | ||
| return; | ||
| } | ||
|
|
||
| NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; | ||
|
|
||
|
|
||
| if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { | ||
| NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); | ||
| reject(@"ERR_INVALID_TASK_SCHEDULER_IDENTIFIER", @"No background identifiers found or invalid format", nil); | ||
| return; | ||
| } | ||
|
|
||
| NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); | ||
|
|
||
| BOOL allSuccess = YES; | ||
| NSError *taskError = nil; | ||
|
|
||
| for (NSString *identifier in backgroundIdentifiers) { | ||
| [[RNBackgroundTaskManager shared] setHandlerForIdentifier:identifier completion:^(BGTask * _Nonnull task) { | ||
| NSLog(@"[ReactNativeBackgroundTask] Executing background task's handler"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my previous experiences with background handling, we had some sort of expiry handling/time out logic. do you think we need it here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could add a way for force finishing background task programmatically but for our use case I don't think that's necessary. |
||
|
|
||
| // Execute all registered tasks | ||
| [self->_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { | ||
| NSLog(@"[ReactNativeBackgroundTask] Executing task: %@", taskName); | ||
| [self emitOnBackgroundTaskExecution:(taskName)]; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question where is this method coming from? I couldn't quickly find in the Apple docs.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }]; | ||
|
|
||
| NSError *scheduleError = nil; | ||
| [self scheduleNewBackgroundTask:identifier error:&scheduleError]; | ||
|
|
||
| [task setTaskCompletedWithSuccess:YES]; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have a lot of context, but should this be inside the loop?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why in loop? When the execution of one task finish we schedule another execution for the same task |
||
| }]; | ||
|
|
||
| NSError *scheduleError = nil; | ||
| BOOL success = [self scheduleNewBackgroundTask:identifier error:&scheduleError]; | ||
|
|
||
| if (success) { | ||
| _taskExecutors[taskName] = taskExecutor; | ||
| } else { | ||
| allSuccess = NO; | ||
| taskError = scheduleError; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (allSuccess) { | ||
| resolve(@YES); | ||
| } else { | ||
| reject(@"ERR_SCHEDULE_TASK_FAILED", | ||
| taskError.localizedDescription ?: @"Failed to schedule initial background task", | ||
| taskError); | ||
| } | ||
|
|
||
| _taskExecutors[taskName] = taskExecutor; | ||
| } | ||
|
|
||
| // Don't compile this code when we build for the old architecture. | ||
| #ifdef RCT_NEW_ARCH_ENABLED | ||
| - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule: | ||
| (const facebook::react::ObjCTurboModule::InitParams &)params | ||
| { | ||
| return std::make_shared<facebook::react::NativeReactNativeBackgroundTaskSpecJSI>(params); | ||
| } | ||
| #endif | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "@expensify/react-native-background-task", | ||
| "version": "0.0.0", | ||
| "description": "Execute tasks in background", | ||
| "main": "src/index", | ||
| "codegenConfig": { | ||
| "name": "RNReactNativeBackgroundTaskSpec", | ||
| "type": "modules", | ||
| "jsSrcsDir": "src" | ||
| }, | ||
| "author": " <> ()", | ||
| "license": "UNLICENSED", | ||
| "homepage": "#readme", | ||
| "create-react-native-library": { | ||
| "type": "module-mixed", | ||
| "languages": "kotlin-objc", | ||
| "version": "0.44.1" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@szymonrybczak any reason we committed this file to the repo? Normally it's just a temporary file used in production builds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will take a look at this, probably remove it and add it into the .gitignore, thanks for caching this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Working on this in this PR: #56169
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks but I already got to it in this PR: #57058