react-native-advanced-share-intent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +345 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/advancedshareintent/AdvancedShareIntentModule.kt +266 -0
- package/android/src/main/java/com/advancedshareintent/AdvancedShareIntentPackage.kt +16 -0
- package/index.d.ts +2 -0
- package/index.js +92 -0
- package/index.mjs +9 -0
- package/ios/AdvancedShareIntent.h +5 -0
- package/ios/AdvancedShareIntent.m +186 -0
- package/ios/ShareExtension/AdvancedShareIntentShareExtension.swift +363 -0
- package/package.json +70 -0
- package/react-native-advanced-share-intent.podspec +18 -0
- package/react-native.config.js +9 -0
- package/src/index.d.ts +44 -0
- package/src/index.ts +134 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 react-native-advanced-share-intent contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# react-native-advanced-share-intent
|
|
2
|
+
|
|
3
|
+
Lightweight React Native share intent handling for Android and iOS Share Extensions.
|
|
4
|
+
|
|
5
|
+
`react-native-advanced-share-intent` exposes a small typed API for reading content shared into your app from Android share sheets and iOS Share Extensions. It supports cold starts, foreground shares, text, URLs, images, videos, documents, multiple files, metadata, and explicit cleanup.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Android `ACTION_SEND` and `ACTION_SEND_MULTIPLE`
|
|
10
|
+
- iOS Share Extension support with App Groups
|
|
11
|
+
- Cold-start delivery with `getInitialShare()`
|
|
12
|
+
- Foreground delivery with `addShareListener()`
|
|
13
|
+
- Cleanup with `clearSharedData()`
|
|
14
|
+
- Text, URL, image, video, document, and multi-file shares
|
|
15
|
+
- File name, size, MIME type, original URI, and capture date metadata where available
|
|
16
|
+
- iOS Photos asset preservation with `ph://` local identifiers when available
|
|
17
|
+
- TypeScript definitions
|
|
18
|
+
- No runtime dependencies
|
|
19
|
+
- React Native autolinking support
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Using npm:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install react-native-advanced-share-intent
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Using Yarn:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
yarn add react-native-advanced-share-intent
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For iOS, install pods after adding the package:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
cd ios && pod install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rebuild the native app after installation.
|
|
42
|
+
|
|
43
|
+
## Android Setup
|
|
44
|
+
|
|
45
|
+
Add share intent filters to the activity that hosts React Native. Keep `launchMode="singleTask"` so foreground shares arrive through `onNewIntent`.
|
|
46
|
+
|
|
47
|
+
```xml
|
|
48
|
+
<activity
|
|
49
|
+
android:name=".MainActivity"
|
|
50
|
+
android:launchMode="singleTask"
|
|
51
|
+
android:exported="true">
|
|
52
|
+
|
|
53
|
+
<intent-filter>
|
|
54
|
+
<action android:name="android.intent.action.SEND" />
|
|
55
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
56
|
+
<data android:mimeType="text/*" />
|
|
57
|
+
<data android:mimeType="image/*" />
|
|
58
|
+
<data android:mimeType="video/*" />
|
|
59
|
+
<data android:mimeType="application/*" />
|
|
60
|
+
</intent-filter>
|
|
61
|
+
|
|
62
|
+
<intent-filter>
|
|
63
|
+
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
|
64
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
65
|
+
<data android:mimeType="image/*" />
|
|
66
|
+
<data android:mimeType="video/*" />
|
|
67
|
+
<data android:mimeType="application/*" />
|
|
68
|
+
</intent-filter>
|
|
69
|
+
</activity>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Android returns shared files as provider-backed `content://` URIs. The library does not copy large file bytes into JavaScript memory. Pass the URI to your uploader, media pipeline, or a native file-copy step when your app needs a local copy.
|
|
73
|
+
|
|
74
|
+
## iOS Setup
|
|
75
|
+
|
|
76
|
+
iOS share delivery requires a Share Extension and an App Group. The library includes `AdvancedShareIntentShareExtension`, a base extension controller that collects shared items, stores a compact payload in the App Group, and opens the containing app.
|
|
77
|
+
|
|
78
|
+
1. In Xcode, add a Share Extension target.
|
|
79
|
+
2. Enable the same App Group on the main app target and the Share Extension target.
|
|
80
|
+
3. Add a URL scheme to the main app, for example `myapp`.
|
|
81
|
+
4. In your extension target, subclass the included controller:
|
|
82
|
+
|
|
83
|
+
```swift
|
|
84
|
+
import AdvancedShareIntent
|
|
85
|
+
|
|
86
|
+
final class ShareViewController: AdvancedShareIntentShareExtension {
|
|
87
|
+
override var appGroupIdentifier: String {
|
|
88
|
+
"group.com.example.myapp"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override var containingAppScheme: String {
|
|
92
|
+
"myapp"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
5. Configure the App Group from JavaScript before reading initial data:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import ShareIntent from 'react-native-advanced-share-intent';
|
|
101
|
+
|
|
102
|
+
await ShareIntent.setAppGroupIdentifier('group.com.example.myapp');
|
|
103
|
+
await ShareIntent.setContainingAppScheme('myapp');
|
|
104
|
+
|
|
105
|
+
const initialShare = await ShareIntent.getInitialShare();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The extension preserves Photos library items as `ph://` URIs with `localIdentifier` when iOS exposes a `PHAsset`. Other files are copied into the App Group container and returned as `file://` URLs. For long-running uploads, copy or consume those files promptly after receiving the payload, then call `clearSharedData()` to remove cached share-extension files.
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
111
|
+
|
|
112
|
+
Read the share that launched the app:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import ShareIntent from 'react-native-advanced-share-intent';
|
|
116
|
+
|
|
117
|
+
const share = await ShareIntent.getInitialShare();
|
|
118
|
+
|
|
119
|
+
if (share) {
|
|
120
|
+
console.log(share.text);
|
|
121
|
+
console.log(share.files);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Listen for new shares while the app is running:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { useEffect, useState } from 'react';
|
|
129
|
+
import ShareIntent, {
|
|
130
|
+
type ShareIntentPayload,
|
|
131
|
+
} from 'react-native-advanced-share-intent';
|
|
132
|
+
|
|
133
|
+
export function useShareIntent() {
|
|
134
|
+
const [share, setShare] = useState<ShareIntentPayload | null>(null);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
ShareIntent.getInitialShare().then(setShare);
|
|
138
|
+
|
|
139
|
+
const subscription = ShareIntent.addShareListener(setShare);
|
|
140
|
+
return () => subscription.remove();
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
return share;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Clear processed shared data:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
await ShareIntent.clearSharedData();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Named function exports are also available:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import {
|
|
157
|
+
getInitialShare,
|
|
158
|
+
addShareListener,
|
|
159
|
+
clearSharedData,
|
|
160
|
+
} from 'react-native-advanced-share-intent';
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For a compact copyable component, see [`examples/BasicShareIntent.tsx`](examples/BasicShareIntent.tsx).
|
|
164
|
+
|
|
165
|
+
## API Reference
|
|
166
|
+
|
|
167
|
+
### `getInitialShare()`
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
getInitialShare(): Promise<ShareIntentPayload | null>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Returns the share payload that launched the app, or `null` when the app was not opened from a share.
|
|
174
|
+
|
|
175
|
+
### `addShareListener(listener)`
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
addShareListener(listener: ShareIntentListener): EmitterSubscription
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Subscribes to share payloads delivered after the app is already running. Call `subscription.remove()` during cleanup.
|
|
182
|
+
|
|
183
|
+
### `clearSharedData()`
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
clearSharedData(): Promise<void>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Clears the cached share payload and removes iOS App Group files created by the Share Extension.
|
|
190
|
+
|
|
191
|
+
### `setAppGroupIdentifier(identifier)`
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
setAppGroupIdentifier(identifier: string): Promise<void>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
iOS only. Sets the App Group used by the containing app and Share Extension.
|
|
198
|
+
|
|
199
|
+
### `setContainingAppScheme(scheme)`
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
setContainingAppScheme(scheme: string): Promise<void>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
iOS only. Stores the URL scheme used by the Share Extension to reopen the containing app.
|
|
206
|
+
|
|
207
|
+
### Types
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
type ShareIntentPayload = {
|
|
211
|
+
text?: string;
|
|
212
|
+
subject?: string;
|
|
213
|
+
title?: string;
|
|
214
|
+
mimeType?: string;
|
|
215
|
+
files: SharedFile[];
|
|
216
|
+
webUrl?: string;
|
|
217
|
+
isInitial: boolean;
|
|
218
|
+
receivedAt: number;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
type SharedFile = {
|
|
222
|
+
uri: string;
|
|
223
|
+
fileName?: string;
|
|
224
|
+
name?: string;
|
|
225
|
+
mimeType?: string;
|
|
226
|
+
size?: number;
|
|
227
|
+
type: 'text' | 'image' | 'video' | 'document' | 'unknown';
|
|
228
|
+
dateTaken?: number;
|
|
229
|
+
localIdentifier?: string;
|
|
230
|
+
originalUri?: string;
|
|
231
|
+
};
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Example App
|
|
235
|
+
|
|
236
|
+
The example app stays in the GitHub repository so contributors and users can test the native behavior. It is excluded from the npm package through the root `package.json` `files` allowlist.
|
|
237
|
+
|
|
238
|
+
Clone and run the example app with npm:
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
git clone https://github.com/engr-touqeer/react-native-advanced-share-intent
|
|
242
|
+
cd react-native-advanced-share-intent/example
|
|
243
|
+
npm install
|
|
244
|
+
cd ios && pod install
|
|
245
|
+
cd ..
|
|
246
|
+
npm run ios
|
|
247
|
+
npm run android
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Or run it with Yarn:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
git clone https://github.com/engr-touqeer/react-native-advanced-share-intent
|
|
254
|
+
cd react-native-advanced-share-intent/example
|
|
255
|
+
yarn install
|
|
256
|
+
cd ios && pod install
|
|
257
|
+
cd ..
|
|
258
|
+
yarn ios
|
|
259
|
+
yarn android
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The Android example is configured to receive share intents and display the parsed payload. For iOS testing, add a Share Extension target to the example app and follow the iOS setup above with your own App Group and URL scheme.
|
|
263
|
+
|
|
264
|
+
## Publishing
|
|
265
|
+
|
|
266
|
+
Install, build, and inspect the package with npm:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
npm install
|
|
270
|
+
npm run build
|
|
271
|
+
npm pack --dry-run
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Install, build, and inspect the package with Yarn:
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
yarn install
|
|
278
|
+
yarn build
|
|
279
|
+
yarn pack --dry-run
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Publish manually only after reviewing the dry-run output:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
npm login
|
|
286
|
+
npm whoami
|
|
287
|
+
npm publish --access public
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
npm requires either account 2FA or a granular access token with publish access and bypass 2FA enabled. If publish fails with `E403`, confirm 2FA is enabled for your npm account or create a granular npm access token with the required publishing permissions.
|
|
291
|
+
|
|
292
|
+
The npm package is intentionally limited to:
|
|
293
|
+
|
|
294
|
+
```txt
|
|
295
|
+
android/
|
|
296
|
+
ios/
|
|
297
|
+
src/
|
|
298
|
+
index.js
|
|
299
|
+
index.mjs
|
|
300
|
+
index.d.ts
|
|
301
|
+
react-native.config.js
|
|
302
|
+
react-native-advanced-share-intent.podspec
|
|
303
|
+
README.md
|
|
304
|
+
LICENSE
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
This keeps the published package lightweight while preserving the full example app in GitHub.
|
|
308
|
+
|
|
309
|
+
## Lockfile Policy
|
|
310
|
+
|
|
311
|
+
Use one package manager lockfile style in committed changes. This repository uses npm as the primary lockfile source with `package-lock.json`. Yarn is supported for installs and scripts, but `yarn.lock` should not be committed unless the project intentionally switches to Yarn as the primary package manager.
|
|
312
|
+
|
|
313
|
+
## Troubleshooting
|
|
314
|
+
|
|
315
|
+
### The native module is not linked
|
|
316
|
+
|
|
317
|
+
Run `pod install` for iOS, rebuild the native app, and make sure React Native autolinking can see the package.
|
|
318
|
+
|
|
319
|
+
### Android shares do not arrive while the app is open
|
|
320
|
+
|
|
321
|
+
Confirm the host activity uses `android:launchMode="singleTask"` and has the `SEND` or `SEND_MULTIPLE` intent filters for the MIME types you want to support.
|
|
322
|
+
|
|
323
|
+
### iOS returns `null`
|
|
324
|
+
|
|
325
|
+
Confirm the main app and Share Extension use the same App Group, the Share Extension subclasses `AdvancedShareIntentShareExtension`, and JavaScript calls `setAppGroupIdentifier()` before `getInitialShare()`.
|
|
326
|
+
|
|
327
|
+
### Large files are slow or fail to upload
|
|
328
|
+
|
|
329
|
+
The library returns provider or App Group file URIs. Copy, stream, or upload those files from a native-capable file pipeline instead of reading large files into JavaScript memory.
|
|
330
|
+
|
|
331
|
+
## Contributing
|
|
332
|
+
|
|
333
|
+
Issues and pull requests are welcome. Please keep changes focused, preserve Android and iOS share intent behavior, and test with text, one file, and multiple files where possible.
|
|
334
|
+
|
|
335
|
+
Before opening a pull request:
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
npm install
|
|
339
|
+
npm run build
|
|
340
|
+
npm pack --dry-run
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath("com.android.tools.build:gradle:8.7.3")
|
|
13
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
apply plugin: "com.android.library"
|
|
18
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
19
|
+
|
|
20
|
+
android {
|
|
21
|
+
namespace "com.advancedshareintent"
|
|
22
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 35)
|
|
23
|
+
|
|
24
|
+
defaultConfig {
|
|
25
|
+
minSdkVersion safeExtGet("minSdkVersion", 23)
|
|
26
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 35)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
repositories {
|
|
31
|
+
google()
|
|
32
|
+
mavenCentral()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dependencies {
|
|
36
|
+
implementation "com.facebook.react:react-android"
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
package com.advancedshareintent
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.ClipData
|
|
5
|
+
import android.content.ContentResolver
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.database.Cursor
|
|
8
|
+
import android.net.Uri
|
|
9
|
+
import android.os.Bundle
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.provider.OpenableColumns
|
|
13
|
+
import android.provider.MediaStore
|
|
14
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
15
|
+
import com.facebook.react.bridge.Arguments
|
|
16
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
17
|
+
import com.facebook.react.bridge.Promise
|
|
18
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
19
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
20
|
+
import com.facebook.react.bridge.ReactMethod
|
|
21
|
+
import com.facebook.react.bridge.WritableMap
|
|
22
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
23
|
+
|
|
24
|
+
class AdvancedShareIntentModule(
|
|
25
|
+
private val reactContext: ReactApplicationContext
|
|
26
|
+
) : ReactContextBaseJavaModule(reactContext), ActivityEventListener, LifecycleEventListener {
|
|
27
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
28
|
+
private var initialShare: WritableMap? = null
|
|
29
|
+
private var latestShare: WritableMap? = null
|
|
30
|
+
private var hasListeners = false
|
|
31
|
+
|
|
32
|
+
init {
|
|
33
|
+
reactContext.addActivityEventListener(this)
|
|
34
|
+
reactContext.addLifecycleEventListener(this)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun getName(): String = NAME
|
|
38
|
+
|
|
39
|
+
override fun initialize() {
|
|
40
|
+
super.initialize()
|
|
41
|
+
reactContext.currentActivity?.intent?.let { intent ->
|
|
42
|
+
parseShareIntent(intent, true)?.let { payload ->
|
|
43
|
+
initialShare = payload.copy()
|
|
44
|
+
latestShare = payload.copy()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun onNewIntent(intent: Intent) {
|
|
50
|
+
parseShareIntent(intent, false)?.let { payload ->
|
|
51
|
+
latestShare = payload.copy()
|
|
52
|
+
sendEventWhenReady(payload.copy())
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun onActivityResult(
|
|
57
|
+
activity: Activity,
|
|
58
|
+
requestCode: Int,
|
|
59
|
+
resultCode: Int,
|
|
60
|
+
data: Intent?
|
|
61
|
+
) = Unit
|
|
62
|
+
|
|
63
|
+
@ReactMethod
|
|
64
|
+
fun getInitialShare(promise: Promise) {
|
|
65
|
+
try {
|
|
66
|
+
val payload = initialShare ?: reactContext.currentActivity?.intent?.let { parseShareIntent(it, true) }
|
|
67
|
+
initialShare = payload?.copy()
|
|
68
|
+
promise.resolve(payload?.copy())
|
|
69
|
+
} catch (error: Exception) {
|
|
70
|
+
promise.reject("advanced_share_intent_initial_error", error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@ReactMethod
|
|
75
|
+
fun clearSharedData(promise: Promise) {
|
|
76
|
+
initialShare = null
|
|
77
|
+
latestShare = null
|
|
78
|
+
reactContext.currentActivity?.intent?.apply {
|
|
79
|
+
action = null
|
|
80
|
+
type = null
|
|
81
|
+
data = null
|
|
82
|
+
clipData = null
|
|
83
|
+
replaceExtras(Bundle())
|
|
84
|
+
}
|
|
85
|
+
promise.resolve(null)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@ReactMethod
|
|
89
|
+
fun addListener(eventName: String) {
|
|
90
|
+
hasListeners = true
|
|
91
|
+
latestShare?.copy()?.let { sendEventWhenReady(it) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@ReactMethod
|
|
95
|
+
fun removeListeners(count: Int) {
|
|
96
|
+
hasListeners = false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun onHostResume() {
|
|
100
|
+
latestShare?.copy()?.let { sendEventWhenReady(it) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override fun onHostPause() = Unit
|
|
104
|
+
|
|
105
|
+
override fun onHostDestroy() = Unit
|
|
106
|
+
|
|
107
|
+
private fun parseShareIntent(intent: Intent, isInitial: Boolean): WritableMap? {
|
|
108
|
+
val action = intent.action ?: return null
|
|
109
|
+
if (action != Intent.ACTION_SEND && action != Intent.ACTION_SEND_MULTIPLE) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
val mimeType = intent.type ?: "*/*"
|
|
114
|
+
val files = Arguments.createArray()
|
|
115
|
+
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
|
116
|
+
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
|
117
|
+
val title = intent.getStringExtra(Intent.EXTRA_TITLE)
|
|
118
|
+
|
|
119
|
+
collectUris(intent).forEach { uri ->
|
|
120
|
+
grantReadPermission(uri)
|
|
121
|
+
files.pushMap(uriToFileMap(uri, mimeType))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (text.isNullOrBlank() && files.size() == 0) {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Arguments.createMap().apply {
|
|
129
|
+
if (!text.isNullOrBlank()) putString("text", text)
|
|
130
|
+
if (!subject.isNullOrBlank()) putString("subject", subject)
|
|
131
|
+
if (!title.isNullOrBlank()) putString("title", title)
|
|
132
|
+
putString("mimeType", mimeType)
|
|
133
|
+
putArray("files", files)
|
|
134
|
+
putBoolean("isInitial", isInitial)
|
|
135
|
+
putDouble("receivedAt", System.currentTimeMillis().toDouble())
|
|
136
|
+
extractWebUrl(text)?.let { putString("webUrl", it) }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun collectUris(intent: Intent): List<Uri> {
|
|
141
|
+
val uris = LinkedHashSet<Uri>()
|
|
142
|
+
val stream = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
|
143
|
+
if (stream != null) uris.add(stream)
|
|
144
|
+
|
|
145
|
+
val streams = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
|
|
146
|
+
streams?.forEach { uri -> if (uri != null) uris.add(uri) }
|
|
147
|
+
|
|
148
|
+
collectClipData(intent.clipData, uris)
|
|
149
|
+
intent.data?.let { uris.add(it) }
|
|
150
|
+
return uris.toList()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private fun collectClipData(clipData: ClipData?, uris: MutableSet<Uri>) {
|
|
154
|
+
if (clipData == null) return
|
|
155
|
+
for (index in 0 until clipData.itemCount) {
|
|
156
|
+
clipData.getItemAt(index)?.uri?.let { uris.add(it) }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private fun uriToFileMap(uri: Uri, fallbackMimeType: String): WritableMap {
|
|
161
|
+
val resolver = reactContext.contentResolver
|
|
162
|
+
val mimeType = resolver.getType(uri) ?: fallbackMimeType
|
|
163
|
+
val metadata = queryMetadata(resolver, uri)
|
|
164
|
+
|
|
165
|
+
return Arguments.createMap().apply {
|
|
166
|
+
putString("uri", uri.toString())
|
|
167
|
+
putString("type", classifyMimeType(mimeType))
|
|
168
|
+
putString("mimeType", mimeType)
|
|
169
|
+
metadata.name?.let { putString("fileName", it) }
|
|
170
|
+
metadata.name?.let { putString("name", it) }
|
|
171
|
+
metadata.size?.let { putDouble("size", it.toDouble()) }
|
|
172
|
+
metadata.dateTaken?.let { putDouble("dateTaken", it.toDouble()) }
|
|
173
|
+
putString("originalUri", uri.toString())
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun queryMetadata(resolver: ContentResolver, uri: Uri): FileMetadata {
|
|
178
|
+
if (uri.scheme == ContentResolver.SCHEME_FILE) {
|
|
179
|
+
val path = uri.path ?: return FileMetadata(null, null, null)
|
|
180
|
+
val file = java.io.File(path)
|
|
181
|
+
return FileMetadata(file.name, file.takeIf { it.exists() }?.length(), file.takeIf { it.exists() }?.lastModified())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var cursor: Cursor? = null
|
|
185
|
+
return try {
|
|
186
|
+
cursor = resolver.query(
|
|
187
|
+
uri,
|
|
188
|
+
arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, MediaStore.MediaColumns.DATE_TAKEN),
|
|
189
|
+
null,
|
|
190
|
+
null,
|
|
191
|
+
null
|
|
192
|
+
)
|
|
193
|
+
if (cursor != null && cursor.moveToFirst()) {
|
|
194
|
+
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
195
|
+
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
196
|
+
val dateTakenIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_TAKEN)
|
|
197
|
+
FileMetadata(
|
|
198
|
+
name = if (nameIndex >= 0) cursor.getString(nameIndex) else null,
|
|
199
|
+
size = if (sizeIndex >= 0 && !cursor.isNull(sizeIndex)) cursor.getLong(sizeIndex) else null,
|
|
200
|
+
dateTaken = if (dateTakenIndex >= 0 && !cursor.isNull(dateTakenIndex)) cursor.getLong(dateTakenIndex) else null
|
|
201
|
+
)
|
|
202
|
+
} else {
|
|
203
|
+
FileMetadata(uri.lastPathSegment, null, null)
|
|
204
|
+
}
|
|
205
|
+
} catch (_: Exception) {
|
|
206
|
+
FileMetadata(uri.lastPathSegment, null, null)
|
|
207
|
+
} finally {
|
|
208
|
+
cursor?.close()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private fun grantReadPermission(uri: Uri) {
|
|
213
|
+
try {
|
|
214
|
+
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
215
|
+
reactContext.grantUriPermission(reactContext.packageName, uri, flags)
|
|
216
|
+
reactContext.contentResolver.takePersistableUriPermission(uri, flags)
|
|
217
|
+
} catch (_: Exception) {
|
|
218
|
+
// Many providers do not support persisted grants. The temporary share grant is still valid.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private fun sendEventWhenReady(payload: WritableMap, attempt: Int = 0) {
|
|
223
|
+
if (!hasListeners && attempt < MAX_DELIVERY_ATTEMPTS) {
|
|
224
|
+
mainHandler.postDelayed({ sendEventWhenReady(payload.copy(), attempt + 1) }, DELIVERY_RETRY_MS)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!reactContext.hasActiveCatalystInstance()) {
|
|
229
|
+
if (attempt < MAX_DELIVERY_ATTEMPTS) {
|
|
230
|
+
mainHandler.postDelayed({ sendEventWhenReady(payload.copy(), attempt + 1) }, DELIVERY_RETRY_MS)
|
|
231
|
+
}
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
reactContext
|
|
236
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
237
|
+
.emit(EVENT_NAME, payload)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun WritableMap.copy(): WritableMap = Arguments.makeNativeMap(this.toHashMap())
|
|
241
|
+
|
|
242
|
+
private data class FileMetadata(val name: String?, val size: Long?, val dateTaken: Long?)
|
|
243
|
+
|
|
244
|
+
companion object {
|
|
245
|
+
const val NAME = "AdvancedShareIntent"
|
|
246
|
+
private const val EVENT_NAME = "AdvancedShareIntentReceived"
|
|
247
|
+
private const val MAX_DELIVERY_ATTEMPTS = 10
|
|
248
|
+
private const val DELIVERY_RETRY_MS = 500L
|
|
249
|
+
|
|
250
|
+
fun classifyMimeType(mimeType: String?): String {
|
|
251
|
+
val value = mimeType?.lowercase() ?: return "unknown"
|
|
252
|
+
return when {
|
|
253
|
+
value.startsWith("image/") -> "image"
|
|
254
|
+
value.startsWith("video/") -> "video"
|
|
255
|
+
value.startsWith("text/") -> "text"
|
|
256
|
+
value == "text/plain" -> "text"
|
|
257
|
+
else -> "document"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fun extractWebUrl(text: String?): String? {
|
|
262
|
+
if (text.isNullOrBlank()) return null
|
|
263
|
+
return Regex("""https?://\S+""").find(text)?.value
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.advancedshareintent
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class AdvancedShareIntentPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(AdvancedShareIntentModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
package/index.d.ts
ADDED