Skip to content
Merged
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
39 changes: 28 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ behaviour.
## Setup with Expo (recommended)

The package ships an Expo config plugin that wires up everything for you on
`expo prebuild`: AppDelegate bootstrap (iOS), MainApplication bootstrap
(Android), `Info.plist` permission + background-mode keys, foreground-service
notification factory, the Azure DevOps Maven repo, and the extra Android
permissions (`POST_NOTIFICATIONS`, `FOREGROUND_SERVICE`).
`expo prebuild`: AppDelegate bootstrap + background URL session forwarding
(iOS), MainApplication bootstrap (Android), `Info.plist` permission +
background-mode keys, foreground-service notification factory, the Azure
DevOps Maven repo, and the extra Android permissions (`POST_NOTIFICATIONS`,
`FOREGROUND_SERVICE`).

Add the plugin to `app.json`:

Expand Down Expand Up @@ -118,6 +119,9 @@ func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Must be the FIRST statement: when iOS relaunches the killed app for a
// background location event, the SDK has to re-arm tracking before any
// React Native startup work runs.
MotionTagBootstrap.bootstrap(launchOptions: launchOptions)
// … rest of RN bootstrap …
}
Expand All @@ -127,10 +131,17 @@ func application(
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
MotionTagBootstrap.processBackgroundSessionEvents(
identifier: identifier,
completionHandler: completionHandler
)
if MotionTagBootstrap.handlesBackgroundURLSession(identifier: identifier) {
MotionTagBootstrap.processBackgroundSessionEvents(
identifier: identifier,
completionHandler: completionHandler
)
} else {
// Forward sessions owned by other SDKs (Firebase, …) to their handlers, or
// finish them immediately if nothing else in the app uses background sessions.
// Each session's completion handler must be called exactly once.
completionHandler()
}
}
```

Expand Down Expand Up @@ -158,8 +169,10 @@ import de.motiontag.reactnative.MotionTagBootstrap

override fun onCreate() {
super.onCreate()
loadReactNative(this)
// Init the SDK before React Native loads — the MotionTag SDK requires
// initialisation as early as possible in onCreate.
MotionTagBootstrap.init(this, createNotification())
loadReactNative(this)
}
```

Expand Down Expand Up @@ -309,8 +322,12 @@ A version bump is **not** "just" a version bump when the changelog touches:
`UIBackgroundModes`) → update both the [bare-RN snippet above](#ios--appdelegateswift)
and the Expo plugin's Info.plist injection in `plugin/`.
- iOS bootstrap signature (`MotionTagBootstrap.bootstrap`,
`processBackgroundSessionEvents`) → update `ios/MotionTagBootstrap.swift`, the
README snippet, and the plugin's AppDelegate injection.
`processBackgroundSessionEvents`, `handlesBackgroundURLSession`) → update
`ios/MotionTagBootstrap.swift`, the README snippet, and the plugin's
AppDelegate injection. Note: `handlesBackgroundURLSession` hard-codes the
SDK's background URL session identifier prefixes (`com.motion-tag.` /
`com.motiontag.`) — re-check them against the SDK binary on every iOS SDK
bump (`strings MotionTagSDK | grep -i session`).
- Android manifest permissions or foreground-service contract → update
`android/src/main/AndroidManifest.xml`, the plugin's manifest edits, and the
Android section above.
Expand Down
9 changes: 9 additions & 0 deletions ios/MotionTagBootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ import UIKit
)
}

/// Whether the given background URL session belongs to the MotionTag SDK. Use this in
/// `application(_:handleEventsForBackgroundURLSession:completionHandler:)` to decide whether
/// to forward the event to `processBackgroundSessionEvents(identifier:completionHandler:)` or
/// to the host's other background-session owners (e.g. Expo modules, Firebase) — each session's
/// completion handler must be called exactly once, by exactly one owner.
@objc public static func handlesBackgroundURLSession(identifier: String) -> Bool {
return identifier.hasPrefix("com.motion-tag.") || identifier.hasPrefix("com.motiontag.")
}

/// Forward background URL session events so the SDK can finish background uploads on
/// cold-launch wake-ups. Call from `application(_:handleEventsForBackgroundURLSession:completionHandler:)`.
@objc public static func processBackgroundSessionEvents(
Expand Down
91 changes: 82 additions & 9 deletions plugin/withIosAppDelegate.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,69 @@
const { withAppDelegate } = require('@expo/config-plugins')
const {
mergeContents,
removeContents,
} = require('@expo/config-plugins/build/utils/generateCode')

// MotionTag requires SDK initialisation "as early as possible" in
// application(_:didFinishLaunchingWithOptions:) — when iOS relaunches a
// killed app for a background location event, the SDK must re-arm tracking
// before any React Native / Expo startup work runs.
const BOOTSTRAP_CALL = ` // MotionTag SDK must initialise before React Native starts so background
// wake-ups (after the app is killed) can re-arm tracking.
MotionTagBootstrap.bootstrap(launchOptions: launchOptions)`

// Background uploads run in a background URL session. When iOS wakes the
// (possibly killed) app to deliver its events, they must reach the SDK —
// sessions owned by Expo modules keep going through super.
const BACKGROUND_SESSION_OVERRIDE = ` public override func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
if MotionTagBootstrap.handlesBackgroundURLSession(identifier: identifier) {
MotionTagBootstrap.processBackgroundSessionEvents(
identifier: identifier,
completionHandler: completionHandler
)
} else {
super.application(
application,
handleEventsForBackgroundURLSession: identifier,
completionHandler: completionHandler
)
}
}`

/**
* mergeContents, but tries a list of [anchor, offset] pairs in order so the
* plugin survives small template differences between Expo SDK versions.
*
* Always removes an existing block for the tag first: mergeContents alone is
* content-hash idempotent, so a block whose content is unchanged would stay
* at its old position even when this plugin's anchor moved.
*/
function mergeWithAnchors(src, { tag, newSrc, anchors }) {
src = removeContents({ src, tag }).contents

let lastError
for (const [anchor, offset] of anchors) {
try {
return mergeContents({ tag, src, newSrc, anchor, offset, comment: '//' })
.contents
} catch (error) {
if (error.code !== 'ERR_NO_MATCH') {
throw error
}
lastError = error
}
}
throw new Error(
`[react-native-motiontag] Could not find an insertion point for "${tag}" in AppDelegate.swift. ` +
'Your AppDelegate seems to be heavily customised — add the MotionTag calls manually as shown ' +
`in the package README ("Setup with bare React Native"). Original error: ${lastError.message}`,
)
}

module.exports = function withIosAppDelegate(config) {
return withAppDelegate(config, (cfg) => {
if (cfg.modResults.language !== 'swift') {
Expand All @@ -22,16 +83,28 @@ module.exports = function withIosAppDelegate(config) {
comment: '//',
}).contents

contents = mergeContents({
contents = mergeWithAnchors(contents, {
tag: 'react-native-motiontag-bootstrap',
src: contents,
newSrc:
' MotionTagBootstrap.bootstrap(launchOptions: launchOptions)',
anchor:
/return super\.application\(application, didFinishLaunchingWithOptions: launchOptions\)/,
offset: 0,
comment: '//',
}).contents
newSrc: BOOTSTRAP_CALL,
anchors: [
// First statement of didFinishLaunchingWithOptions in the Expo
// SDK 52+ template — insert the bootstrap call right before it.
[/let delegate = ReactNativeDelegate\(\)/, 0],
// Fallback: the line opening the first `-> Bool {` body in the file,
// which in the Expo template is didFinishLaunchingWithOptions.
[/\)\s*->\s*Bool\s*\{/, 1],
],
})

contents = mergeWithAnchors(contents, {
tag: 'react-native-motiontag-background-session',
newSrc: BACKGROUND_SESSION_OVERRIDE,
anchors: [
// Right after the AppDelegate class declaration, before its first member.
[/class AppDelegate\s*:\s*ExpoAppDelegate\s*\{/, 1],
[/:\s*ExpoAppDelegate\s*\{/, 1],
],
})

cfg.modResults.contents = contents
return cfg
Expand Down
Loading