Skip to content

Commit b6a7f3c

Browse files
committed
feat(ios): add ios share intent handling
- Add PrivacyInfo.xcprivacy file to resources for App Store compliance - Implement share intent handling via URL scheme and notifications - Add app lifecycle observers for proper share intent management - Update project settings for React Native and Hermes configuration - Create workspace file for proper CocoaPods integration These changes enable the app to handle share intents from other applications and comply with iOS 17+ privacy requirements for App Store submission.
1 parent 9e2fa1d commit b6a7f3c

9 files changed

Lines changed: 3200 additions & 358 deletions

File tree

README.md

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,32 @@ npm install react-native-nitro-share-intent react-native-nitro-modules
2828
2929
### iOS Setup
3030

31-
1. **Add Share Extension Support** (if needed):
32-
- In your `Info.plist`, add support for the file types you want to handle
33-
- Configure your app to handle the appropriate URL schemes
31+
1. **Add to AppDelegate.swift**:
32+
```swift
33+
import NitroShareIntent
34+
35+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
36+
// Your existing code...
37+
38+
// Notify NitroShareIntent about app launch
39+
NotificationCenter.default.post(name: NSNotification.Name("AppDidFinishLaunching"), object: nil)
40+
41+
return true
42+
}
43+
44+
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
45+
// Handle share intent URLs
46+
NotificationCenter.default.post(
47+
name: NSNotification.Name("ShareIntentReceived"),
48+
object: nil,
49+
userInfo: ["url": url]
50+
)
51+
return true
52+
}
53+
```
3454

35-
2. **Configure App Groups** (for iOS share extensions):
55+
2. **Configure URL Schemes in Info.plist**:
3656
```xml
37-
<!-- Add to your Info.plist -->
3857
<key>CFBundleURLTypes</key>
3958
<array>
4059
<dict>
@@ -52,44 +71,54 @@ npm install react-native-nitro-share-intent react-native-nitro-modules
5271

5372
### Android Setup
5473

55-
1. **Configure Intent Filters** in your `AndroidManifest.xml`:
56-
57-
```xml
58-
<activity
59-
android:name=".MainActivity"
60-
android:exported="true">
61-
62-
<!-- Handle text sharing -->
63-
<intent-filter>
64-
<action android:name="android.intent.action.SEND" />
65-
<category android:name="android.intent.category.DEFAULT" />
66-
<data android:mimeType="text/plain" />
67-
</intent-filter>
68-
69-
<!-- Handle single file sharing -->
70-
<intent-filter>
71-
<action android:name="android.intent.action.SEND" />
72-
<category android:name="android.intent.category.DEFAULT" />
73-
<data android:mimeType="*/*" />
74-
</intent-filter>
75-
76-
<!-- Handle multiple file sharing -->
77-
<intent-filter>
78-
<action android:name="android.intent.action.SEND_MULTIPLE" />
79-
<category android:name="android.intent.category.DEFAULT" />
80-
<data android:mimeType="*/*" />
81-
</intent-filter>
82-
83-
<!-- Handle URL sharing -->
84-
<intent-filter>
85-
<action android:name="android.intent.action.VIEW" />
86-
<category android:name="android.intent.category.DEFAULT" />
87-
<category android:name="android.intent.category.BROWSABLE" />
88-
<data android:scheme="http" />
89-
<data android:scheme="https" />
90-
</intent-filter>
91-
</activity>
92-
```
74+
1. **Add to MainActivity.java/kt**:
75+
```kotlin
76+
import com.margelo.nitro.nitroshareintent.NitroShareIntent
77+
78+
override fun onCreate(savedInstanceState: Bundle?) {
79+
super.onCreate(savedInstanceState)
80+
// Your existing code...
81+
82+
// Handle initial share intent
83+
NitroShareIntent.instance.handleIntent(intent)
84+
}
85+
86+
override fun onNewIntent(intent: Intent) {
87+
super.onNewIntent(intent)
88+
// Handle new share intents
89+
NitroShareIntent.instance.handleIntent(intent)
90+
}
91+
```
92+
93+
2. **Configure Intent Filters in AndroidManifest.xml**:
94+
```xml
95+
<activity
96+
android:name=".MainActivity"
97+
android:exported="true"
98+
android:launchMode="singleTop">
99+
100+
<!-- Handle text sharing -->
101+
<intent-filter>
102+
<action android:name="android.intent.action.SEND" />
103+
<category android:name="android.intent.category.DEFAULT" />
104+
<data android:mimeType="text/plain" />
105+
</intent-filter>
106+
107+
<!-- Handle file sharing -->
108+
<intent-filter>
109+
<action android:name="android.intent.action.SEND" />
110+
<category android:name="android.intent.category.DEFAULT" />
111+
<data android:mimeType="*/*" />
112+
</intent-filter>
113+
114+
<!-- Handle multiple file sharing -->
115+
<intent-filter>
116+
<action android:name="android.intent.action.SEND_MULTIPLE" />
117+
<category android:name="android.intent.category.DEFAULT" />
118+
<data android:mimeType="*/*" />
119+
</intent-filter>
120+
</activity>
121+
```
93122

94123
## 🚀 Quick Start
95124

@@ -248,16 +277,12 @@ import {
248277
const ShareHandler = () => {
249278
useShareIntent((payload: SharePayload) => {
250279
if (ShareIntentUtils.isTextShare(payload)) {
251-
// Handle text share
252280
console.log('Text shared:', payload.text);
253281
} else if (ShareIntentUtils.isFileShare(payload)) {
254-
// Handle file share
255282
if (ShareIntentUtils.isMultipleFileShare(payload)) {
256283
console.log('Multiple files shared:', payload.files?.length);
257284
} else {
258285
console.log('Single file shared:', payload.files?.[0]);
259-
260-
// Check file type
261286
const fileUri = payload.files?.[0];
262287
if (fileUri && ShareIntentUtils.isImageFile(fileUri)) {
263288
console.log("It's an image file!");
@@ -285,7 +310,6 @@ useShareIntent((payload: SharePayload) => {
285310
});
286311
}
287312

288-
// Access file metadata from extras
289313
if (payload.extras) {
290314
console.log('File metadata:', {
291315
fileName: payload.extras.fileName,
@@ -339,8 +363,6 @@ useShareIntent((payload: SharePayload) => {
339363

340364
### Debugging
341365

342-
Enable logging to debug share intent issues:
343-
344366
```typescript
345367
useShareIntent((payload) => {
346368
console.log('Share Intent Debug:', {

android/src/main/java/com/margelo/nitro/nitroshareintent/NitroShareIntent.kt

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
5151
val intent = NitroModules.applicationContext?.currentActivity?.intent
5252

5353
return if (intent != null && isShareIntent(intent)) {
54-
5554
Promise.resolved(processIntent(intent))
5655
} else {
5756
Promise.resolved(null)
@@ -82,12 +81,10 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
8281
}
8382

8483
Intent.ACTION_SEND_MULTIPLE -> {
85-
8684
if (type != null) handleMultipleShare(intent) else null
8785
}
8886

8987
Intent.ACTION_VIEW -> {
90-
9188
intent.dataString?.let { dataString ->
9289
val extras = mutableMapOf("url" to dataString)
9390
SharePayload(
@@ -100,7 +97,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
10097
}
10198

10299
else -> {
103-
104100
null
105101
}
106102
}
@@ -109,7 +105,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
109105
}
110106

111107
private fun handleSingleShare(intent: Intent, type: String): SharePayload? {
112-
113108
return when {
114109
type.startsWith("text/") -> {
115110
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
@@ -127,7 +122,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
127122

128123
else -> {
129124
val fileUri = intent.parcelable<Uri>(Intent.EXTRA_STREAM)
130-
131125
if (fileUri != null) {
132126
val fileInfo = getFileInfo(fileUri)
133127

@@ -142,7 +136,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
142136
}
143137

144138
private fun handleMultipleShare(intent: Intent): SharePayload? {
145-
146139
val fileUris = intent.parcelableArrayList<Uri>(Intent.EXTRA_STREAM)
147140

148141
if (fileUris.isNullOrEmpty()) return null
@@ -155,10 +148,8 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
155148
val filePaths = fileUris.map { uri ->
156149
try {
157150
val fileInfo = getFileInfo(uri)
158-
159151
fileInfo["filePath"] ?: uri.toString()
160152
} catch (_: Exception) {
161-
162153
uri.toString()
163154
}
164155
}.toTypedArray()
@@ -168,15 +159,13 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
168159

169160
@SuppressLint("Range")
170161
private fun getFileInfo(uri: Uri): Map<String, String?> {
171-
172162
NitroModules.applicationContext.let { ctx ->
173163
val resolver: ContentResolver = ctx?.contentResolver ?: return mapOf(
174164
"contentUri" to uri.toString(),
175165
"filePath" to getAbsolutePath(uri),
176166
)
177167
val queryResult = resolver.query(uri, null, null, null, null)
178168
if (queryResult == null) {
179-
180169
return mapOf("filePath" to getAbsolutePath(uri))
181170
}
182171

@@ -190,17 +179,14 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
190179
var mediaHeight: String? = null
191180
var mediaDuration: String? = null
192181

193-
194182
if (mimeType.startsWith("image/")) {
195-
196183
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
197184
BitmapFactory.decodeStream(resolver.openInputStream(uri), null, options)
198185
mediaHeight = options.outHeight.toString()
199186
mediaWidth = options.outWidth.toString()
200187
}
201188

202189
if (mimeType.startsWith("video/")) {
203-
204190
try {
205191
val retriever = MediaMetadataRetriever()
206192
retriever.setDataSource(ctx, uri)
@@ -236,21 +222,17 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
236222

237223

238224
private fun getAbsolutePath(uri: Uri): String? {
239-
240225
NitroModules.applicationContext.let { ctx ->
241226
try {
242227
if (DocumentsContract.isDocumentUri(ctx, uri)) {
243-
244228
if (isExternalStorageDocument(uri)) {
245-
246229
val docId = DocumentsContract.getDocumentId(uri)
247230
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
248231
val type = split[0]
249232
return if ("primary".equals(type, ignoreCase = true)) {
250233
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
251234
} else getDataColumn(uri, null, null)
252235
} else if (isDownloadsDocument(uri)) {
253-
254236
return try {
255237
val id = DocumentsContract.getDocumentId(uri)
256238
val contentUri = ContentUris.withAppendedId(
@@ -262,7 +244,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
262244
getDataColumn(uri, null, null)
263245
}
264246
} else if (isMediaDocument(uri)) {
265-
266247
val docId = DocumentsContract.getDocumentId(uri)
267248
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
268249
val type = split[0]
@@ -278,7 +259,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
278259
return getDataColumn(contentUri, selection, selectionArgs)
279260
}
280261
} else if ("content".equals(uri.scheme, ignoreCase = true)) {
281-
282262
return getDataColumn(uri, null, null)
283263
}
284264

@@ -291,11 +271,9 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
291271
}
292272

293273
private fun getDataColumn(uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
294-
295274
NitroModules.applicationContext.let { ctx ->
296275
val resolver = ctx?.contentResolver
297276
if (uri.authority != null) {
298-
299277
var cursor: Cursor? = null
300278
val column = "_display_name"
301279
val projection = arrayOf(column)
@@ -343,7 +321,6 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
343321
if (cursor != null && cursor.moveToFirst()) {
344322
val columnIndex = cursor.getColumnIndexOrThrow(column)
345323
val result = cursor.getString(columnIndex)
346-
347324
return result
348325
}
349326
} finally {
@@ -355,32 +332,23 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
355332
}
356333

357334
private fun isExternalStorageDocument(uri: Uri): Boolean {
358-
val result = "com.android.externalstorage.documents" == uri.authority
359-
360-
return result
335+
return "com.android.externalstorage.documents" == uri.authority
361336
}
362337

363338
private fun isDownloadsDocument(uri: Uri): Boolean {
364-
val result = "com.android.providers.downloads.documents" == uri.authority
365-
366-
return result
339+
return "com.android.providers.downloads.documents" == uri.authority
367340
}
368341

369342
private fun isMediaDocument(uri: Uri): Boolean {
370-
val result = "com.android.providers.media.documents" == uri.authority
371-
372-
return result
343+
return "com.android.providers.media.documents" == uri.authority
373344
}
374345

375346
private fun isShareIntent(intent: Intent?): Boolean {
376347
if (intent == null) {
377-
378348
return false
379349
}
380350
val action = intent.action
381-
val result = action == Intent.ACTION_SEND || action == Intent.ACTION_SEND_MULTIPLE || action == Intent.ACTION_VIEW
382-
383-
return result
351+
return action == Intent.ACTION_SEND || action == Intent.ACTION_SEND_MULTIPLE || action == Intent.ACTION_VIEW
384352
}
385353

386354
inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? {
@@ -407,11 +375,10 @@ class NitroShareIntent : HybridNitroShareIntentSpec(), ActivityEventListener{
407375
resultCode: Int,
408376
data: Intent?
409377
) {
410-
// TODO Auto-generated method stub
411378
}
412379

413380
override fun onNewIntent(intent: Intent) {
414-
handleIntent(intent)
381+
handleIntent(intent)
415382
}
416383

417384

0 commit comments

Comments
 (0)