diff --git a/README.md b/README.md index df839be..8448ffd 100644 --- a/README.md +++ b/README.md @@ -148,13 +148,14 @@ export default const App = () => { const { shareIntent } = useShareIntent(); ``` -| attribute | description | example | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `shareIntent.text` | raw text from text/weburl (ios) and text/\* (android) | "`some text`", "`http://example.com`", "`Hey, Click on my link : http://example.com/nickname`" | -| `shareIntent.webUrl` | link extracted from raw text | `null`, "`http://example.com`", "`http://example.com/nickname`" | -| `shareIntent.files` | image / movies / audio / files with name, path, mimetype, size (in octets) and image/video dimensions (width/height/duration) | `[{ path: "file:///local/path/filename", mimeType: "image/jpeg", fileName: "originalFilename.jpg", size: 2567402, width: 800, height: 600 }, { path: "file:///local/path/filename", mimeType: "video/mp4", fileName: "originalFilename.mp4", size: 2567402, width: 800, height: 600, duration: 20000 }]` | -| `shareIntent.meta` | meta object which contains extra information about the share intent | `{ title: "My cool blog article" }` | -| `shareIntent.meta.title` | optional title property sent by other app. Currently only filled on Android | `My cool blog article` | +| attribute | description | example | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `shareIntent.text` | raw text from text/weburl (ios) and text/\* (android) | "`some text`", "`http://example.com`", "`Hey, Click on my link : http://example.com/nickname`" | +| `shareIntent.webUrl` | link extracted from raw text | `null`, "`http://example.com`", "`http://example.com/nickname`" | +| `shareIntent.files` | image / movies / audio / files with name, path, mimetype, size (in octets) and image/video dimensions (width/height/duration) | `[{ path: "file:///local/path/filename", mimeType: "image/jpeg", fileName: "originalFilename.jpg", size: 2567402, width: 800, height: 600 }, { path: "file:///local/path/filename", mimeType: "video/mp4", fileName: "originalFilename.mp4", size: 2567402, width: 800, height: 600, duration: 20000 }]` | +| `shareIntent.meta` | meta object which contains extra information about the share intent | `{ title: "My cool blog article", "og:image": "https://.../image.png" }` | +| `shareIntent.meta.title` | optional title property sent by other app (available on Android and when `NSExtensionActivationSupportsWebPageWithMaxCount` is enabled on iOS) | `My cool blog article` | +| `shareIntent.meta.xxx` | list all webpage metadata available in meta tags `` (iOS only, available with `NSExtensionActivationSupportsWebPageWithMaxCount`) | | #### Customize Content Types in `app.json` @@ -177,16 +178,17 @@ Simply choose content types you need : ], ``` -| Option | Values | -| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Option | Values | +| ----------------------------- | ------------------- | | iosActivationRules | Allow **text** sharing with `"NSExtensionActivationSupportsText": true`
**Url** sharing with `"NSExtensionActivationSupportsWebURLWithMaxCount": 1` and `"NSExtensionActivationSupportsWebPageWithMaxCount": 1`
**Images** sharing with `"NSExtensionActivationSupportsImageWithMaxCount": 1`
**Videos** sharing with `"NSExtensionActivationSupportsMovieWithMaxCount": 1`
**Files and audio** sharing with `"NSExtensionActivationSupportsFileWithMaxCount": 1`
_default value_: `{ "NSExtensionActivationSupportsWebURLWithMaxCount": 1, "NSExtensionActivationSupportsWebPageWithMaxCount": 1 }"`
_More info in apple developper doc [here](https://developer.apple.com/documentation/bundleresources/information_property_list/nsextension/nsextensionattributes/nsextensionactivationrule)_
you can also provide a custom query (ex: `"iosActivationRules": "SUBQUERY (...)"`) | -| iosShareExtensionName | override `CFBundleDisplayName` the extension `info.plist`, also used as extension name for xcode target (ex: `ExpoShareIntent Example Extension`, folder: `ExpoShareIntentExampleExtension`) | -| iosAppGroupIdentifier | custom application group identifier for `com.apple.security.application-groups` (ex: `group.custom.exposhareintent.example`) cf [#94](https://github.com/achorein/expo-share-intent/issues/94) | -| androidIntentFilters | **one file sharing** array of MIME types :`"text/*"` / `"image/*"` / `"video/*"` / `"*/*"`
_default value_: `["text/*"]` (text and url) | -| androidMultiIntentFilters | **multiple files sharing** array of MIME types : `"image/*"` / `"video/*"` / `"audio/*`/ `"*/*"`
_default value_: `[]` | -| androidMainActivityAttributes | _default value_: `{ "android:launchMode": "singleTask" }` | -| disableAndroid | Disable the android share intent. Useful if you want to use a custom implementation. _default value_: `false` | -| disableIOS | Disable the ios share extension. Useful if you want to use a custom implementation (ex: [iOS Custom View](#ios-custom-view-)). _default value_: `false` | +| iosShareExtensionName | override `CFBundleDisplayName` the extension `info.plist`, also used as extension name for xcode target (ex: `ExpoShareIntent Example Extension`, folder: `ExpoShareIntentExampleExtension`) | +| iosAppGroupIdentifier | custom application group identifier for `com.apple.security.application-groups` (ex: `group.custom.exposhareintent.example`) cf [#94](https://github.com/achorein/expo-share-intent/issues/94) | +| androidIntentFilters | **one file sharing** array of MIME types :`"text/*"` / `"image/*"` / `"video/*"` / `"*/*"`
_default value_: `["text/*"]` (text and url) | +| androidMultiIntentFilters | **multiple files sharing** array of MIME types : `"image/*"` / `"video/*"` / `"audio/*`/ `"*/*"`
_default value_: `[]` | +| androidMainActivityAttributes | _default value_: `{ "android:launchMode": "singleTask" }` | +| preprocessorInjectJS | Add javascript to webpage preprocessor before the share extension is called (cf [Accessing a Webpage](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW12)).
Example: preprocessorInjectJS: "metas['og\\:image'] = metas['og\\:image'] || document.querySelector('img#seo-image')?.getAttribute('src')" | +| disableAndroid | Disable the android share intent. Useful if you want to use a custom implementation. _default value_: `false` | +| disableIOS | Disable the ios share extension. Useful if you want to use a custom implementation (ex: [iOS Custom View](#ios-custom-view-)). _default value_: `false` | ### Expo Router diff --git a/example/basic/App.tsx b/example/basic/App.tsx index 6893930..100af85 100644 --- a/example/basic/App.tsx +++ b/example/basic/App.tsx @@ -20,12 +20,14 @@ const WebUrlComponent = ({ shareIntent }: { shareIntent: ShareIntent }) => { source={ shareIntent.meta?.["og:image"] ? { uri: shareIntent.meta?.["og:image"] } - : require("./assets/icon.png") + : undefined } style={[styles.icon, styles.gap, { borderRadius: 5 }]} /> - {shareIntent.meta?.title || ""} + + {shareIntent.meta?.title || ""} + {shareIntent.webUrl} diff --git a/example/basic/app.json b/example/basic/app.json index 93d4135..c9ac493 100644 --- a/example/basic/app.json +++ b/example/basic/app.json @@ -26,11 +26,9 @@ "NSExtensionActivationSupportsFileWithMaxCount": 1 }, "iosShareExtensionName": "ExpoShareIntent Example Extension", - "iosAppGroupIdentifier": "customgroup.expo.modules.exposhareintent.example", "androidIntentFilters": ["text/*", "image/*", "video/*"], "androidMultiIntentFilters": ["image/*"], - "disableIOS": false, - "disableAndroid": false + "preprocessorInjectJS": "metas['og\\:image'] = metas['og\\:image'] || document.querySelector('img#main-image')?.getAttribute('src') || document.querySelector('img[data-old-hires]')?.getAttribute('data-old-hires');" } ], ["expo-updates"] diff --git a/example/expo-router/app/shareintent.tsx b/example/expo-router/app/shareintent.tsx index 44e6b34..78c6fc9 100644 --- a/example/expo-router/app/shareintent.tsx +++ b/example/expo-router/app/shareintent.tsx @@ -19,12 +19,14 @@ const WebUrlComponent = ({ shareIntent }: { shareIntent: ShareIntentType }) => { source={ shareIntent.meta?.["og:image"] ? { uri: shareIntent.meta?.["og:image"] } - : require("../assets/icon.png") + : undefined } style={[styles.icon, styles.gap, { borderRadius: 5 }]} /> - {shareIntent.meta?.title || ""} + + {shareIntent.meta?.title || ""} + {shareIntent.webUrl} diff --git a/example/react-navigation/app/ShareIntentScreen.tsx b/example/react-navigation/app/ShareIntentScreen.tsx index 12d9f38..c33d53d 100644 --- a/example/react-navigation/app/ShareIntentScreen.tsx +++ b/example/react-navigation/app/ShareIntentScreen.tsx @@ -21,12 +21,14 @@ const WebUrlComponent = ({ shareIntent }: { shareIntent: ShareIntent }) => { source={ shareIntent.meta?.["og:image"] ? { uri: shareIntent.meta?.["og:image"] } - : require("../assets/icon.png") + : undefined } style={[styles.icon, styles.gap, { borderRadius: 5 }]} /> - {shareIntent.meta?.title || ""} + + {shareIntent.meta?.title || ""} + {shareIntent.webUrl} diff --git a/plugin/src/ios/writeIosShareExtensionFiles.ts b/plugin/src/ios/writeIosShareExtensionFiles.ts index e62b45e..654a7f3 100644 --- a/plugin/src/ios/writeIosShareExtensionFiles.ts +++ b/plugin/src/ios/writeIosShareExtensionFiles.ts @@ -77,7 +77,7 @@ export async function writeShareExtensionFiles( platformProjectRoot, parameters, ); - const preprocessorContent = getPreprocessorContent(); + const preprocessorContent = getPreprocessorContent(parameters); await fs.promises.writeFile(preprocessorFilePath, preprocessorContent); } @@ -241,7 +241,8 @@ export function getPreprocessorFilePath( ); } -export function getPreprocessorContent() { +export function getPreprocessorContent(parameters: Parameters) { + const injection = parameters.preprocessorInjectJS || ""; return `class ShareExtensionPreprocessor { run({ completionFunction }) { // Extract meta tags and image sources from the document @@ -260,23 +261,7 @@ export function getPreprocessorContent() { } } - if (!metas["og:image"]) { - const mainImage = document.querySelector("img#main-image"); - if (mainImage) { - const src = mainImage.getAttribute("src"); - if (src) { - metas["og:image"] = src; - } - } - - const oldHiresImage = document.querySelector("img[data-old-hires]"); - if (oldHiresImage) { - const oldHires = oldHiresImage.getAttribute("data-old-hires"); - if (oldHires) { - metas["og:image"] = oldHires; - } - } - } + ${injection} // Call the completion function with the extracted data completionFunction({ diff --git a/plugin/src/types.ts b/plugin/src/types.ts index 445b3f5..9e329c4 100644 --- a/plugin/src/types.ts +++ b/plugin/src/types.ts @@ -9,6 +9,7 @@ export type Parameters = { androidIntentFilters?: ("text/*" | "image/*" | "video/*" | "*/*")[]; androidMultiIntentFilters?: ("image/*" | "video/*" | "*/*")[]; disableExperimental?: boolean; + preprocessorInjectJS?: string; disableAndroid?: boolean; disableIOS?: boolean; }; diff --git a/src/ExpoShareIntentModule.types.ts b/src/ExpoShareIntentModule.types.ts index ebff141..5f7dd07 100644 --- a/src/ExpoShareIntentModule.types.ts +++ b/src/ExpoShareIntentModule.types.ts @@ -40,7 +40,7 @@ export type ShareIntentOptions = { onResetShareIntent?: () => void; }; -export type ShareIntentMeta = Record & { +export type ShareIntentMeta = Record & { title?: string; }; @@ -48,7 +48,7 @@ export type ShareIntentMeta = Record & { * Base type for what shared content is common between both platforms. */ interface BaseShareIntent { - meta?: ShareIntentMeta; + meta?: ShareIntentMeta | null; text?: string | null; } @@ -74,6 +74,7 @@ export interface AndroidShareIntent extends BaseShareIntent { */ export interface IosShareIntent extends BaseShareIntent { files?: IosShareIntentFile[]; + weburls?: { url: string; meta: string }[]; type: "media" | "file" | "text" | "weburl"; } diff --git a/src/utils.ts b/src/utils.ts index 9ba64ad..aaa33b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -56,21 +56,33 @@ export const getShareExtensionKey = (options?: ShareIntentOptions) => { // 3: "file", // }; +export function parseJson( + value: string, + defaultValue: T | null = null, +): T | null { + try { + return JSON.parse(value) as T; + } catch (e) { + console.debug(e); + return defaultValue; + } +} + export const parseShareIntent = ( value: string | AndroidShareIntent, options: ShareIntentOptions, ): ShareIntent => { let result = SHAREINTENT_DEFAULTVALUE; if (!value) return result; - let shareIntent; + let shareIntent: IosShareIntent | AndroidShareIntent | null; // ios native module send a raw string of the json, try to parse it if (typeof value === "string") { - shareIntent = JSON.parse(value) as IosShareIntent; // iOS + shareIntent = parseJson(value); // iOS } else { shareIntent = value; // Android } - if (shareIntent.text) { + if (shareIntent?.text) { // Try to find the webURL in the SharedIntent text const webUrl = shareIntent.text.match( @@ -86,13 +98,13 @@ export const parseShareIntent = ( title: shareIntent.meta?.title ?? undefined, }, }; - } else if (shareIntent.weburls?.length) { - const weburl = shareIntent.weburls[0]; + } else if ((shareIntent as IosShareIntent)?.weburls?.length) { + const weburl = (shareIntent as IosShareIntent).weburls![0]; result = { ...SHAREINTENT_DEFAULTVALUE, type: "weburl", webUrl: weburl.url, - meta: JSON.parse(weburl.meta), + meta: parseJson>(weburl.meta, {}), }; } else { // Ensure we got a valid file. some array value are emply