https://github.com/squareetlabs/capacitor-openwith
A Capacitor plugin that enables Android and iOS applications to handle shared content
https://github.com/squareetlabs/capacitor-openwith
Last synced: 2 months ago
JSON representation
A Capacitor plugin that enables Android and iOS applications to handle shared content
- Host: GitHub
- URL: https://github.com/squareetlabs/capacitor-openwith
- Owner: squareetlabs
- Created: 2025-03-02T10:53:34.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2025-03-02T11:51:31.000Z (3 months ago)
- Last Synced: 2025-03-02T12:20:39.033Z (3 months ago)
- Language: Java
- Size: 102 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
Awesome Lists containing this project
README
# @squareetlabs/capacitor-openwith
Capacitor plugin to handle files and content shared from other apps on Android and iOS.
## Installation
```bash
npm install @squareetlabs/capacitor-openwith
npx cap sync
```## Android Configuration
Add the following permissions to your `android/app/src/main/AndroidManifest.xml`:
```xml
```
## iOS Configuration
Para permitir que tu aplicación reciba archivos compartidos desde otras aplicaciones, necesitas configurar una Share Extension:
### 1. Crear Share Extension
1. Abre tu proyecto en XCode
2. Ve a `File > New > Target`
3. Selecciona "Share Extension" en la sección de iOS
4. Dale un nombre (por ejemplo, "ShareExtension")
5. Asegúrate de que "Embed in Application" esté seleccionado### 2. Configuración de App Groups
1. Selecciona tu proyecto en XCode
2. Selecciona el target principal de tu aplicación
3. Ve a "Signing & Capabilities"
4. Haz clic en "+" y añade "App Groups"
5. Crea un nuevo grupo con el formato: `group.[tu-bundle-identifier]`
6. Repite el proceso para el target de la Share Extension### 3. Configuración del Info.plist de la Share Extension
Añade lo siguiente a tu Info.plist de la Share Extension:
```xml
NSExtensionNSExtensionAttributes
NSExtensionActivationRule
NSExtensionActivationSupportsWebURLWithMaxCount
1
NSExtensionActivationSupportsFileWithMaxCount
1
NSExtensionActivationSupportsText
NSExtensionActivationSupportsImageWithMaxCount
1
NSExtensionActivationSupportsMovieWithMaxCount
1
NSExtensionMainStoryboard
MainInterface
NSExtensionPointIdentifier
com.apple.share-services```
### 4. Implementación del ShareViewController
Reemplaza el contenido del archivo `ShareViewController.swift` con el siguiente código:
```swift
import UIKit
import Social
import MobileCoreServices
import UniformTypeIdentifiersclass ShareViewController: SLComposeServiceViewController {
override func isContentValid() -> Bool {
// Puedes validar si hay contenido, por ejemplo, si el usuario escribió algo
// o si hay attachments. Por simplicidad devolvemos true.
return true
}
override func didSelectPost() {
// Este método se llama cuando el usuario toca el botón de "Post" (o "Compartir").// 1. Obtén el primer NSExtensionItem:
if let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
let attachments = extensionItem.attachments {
// 2. Itera sobre los attachments
for attachment in attachments {
// Verifica si es del tipo URL
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (item, error) in
guard let self = self else { return }
if let url = item as? URL {
// 3. Aquí guardas el contenido como antes
self.saveSharedContent(content: url.absoluteString, type: "url")
}
// Importante: se completa la request
// para que la extensión se cierre
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
// Como probablemente quieras manejar solo un URL, podrías hacer un break aquí
// break
}
// Si quieres más tipos (imágenes, texto, etc.), añade lógica extra
}
} else {
// Si no hay items o no se pudo castear,
// simplemente cierra la extensión
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}
override func configurationItems() -> [Any]! {
// Aquí podrías añadir celdas de configuración en la parte de abajo de la hoja
// (por ejemplo, si quieres un switch para "compartir públicamente" o similar).
// Si no lo necesitas, devuélvelo vacío.
return []
}
private func saveSharedContent(content: String, type: String) {
// Guarda el contenido en UserDefaults compartido
if let userDefaults = UserDefaults(suiteName: "group." + (Bundle.main.bundleIdentifier ?? "")) {
userDefaults.set(content, forKey: "SharedContent")
userDefaults.set(type, forKey: "SharedContentType")
userDefaults.synchronize()
}
}
}
```### 5. Configuración del Bundle Identifier
Asegúrate de que el Bundle Identifier de la Share Extension esté correctamente configurado en el Info.plist:
```xml
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER).share-extension
```### 6. Uso en tu aplicación
Una vez configurado, puedes escuchar los eventos de archivos compartidos en tu aplicación:
```typescript
import { OpenWith } from '@capacitor-community/open-with';OpenWith.addListener('receivedFiles', async (data) => {
console.log('Archivo recibido:', data);
// data.data contiene:
// - uri: URL del archivo
// - type: tipo MIME
// - source: información de la app que compartió (si está disponible)
// - extras: información adicional como título, texto, etc.
});// Inicializar el plugin
await OpenWith.initialize();
```### 7. Solución de Problemas
Si la Share Extension no aparece en el menú compartir:
- Verifica que el App Group esté correctamente configurado en ambos targets
- Asegúrate de que los tipos de archivo que quieres compartir estén incluidos en NSExtensionActivationRule
- Comprueba que la Share Extension esté firmada con el mismo equipo de desarrollo que la aplicación principal
- Limpia y reconstruye el proyecto (Product > Clean Build Folder)
- Desinstala y vuelve a instalar la aplicación en el dispositivo/simulador## API
The plugin provides the following functions:
- `addHandler()`: Adds a handler for shared file events
- `initialize()`: Initializes the plugin
- `setVerbosity({ level: number })`: Configures logging level (0: disabled, 1: enabled)### Interfaces
```typescript
interface SharedData {
// Source application information
source?: {
packageName: string;
applicationName: string;
applicationIcon: string;
};
// Intent action
action?: string;
// MIME type
type?: string;
// Content URI
uri?: string;
// URI scheme
scheme?: string;
// Additional data
extras?: {
text?: string;
htmlText?: string;
subject?: string;
title?: string;
email?: string[];
cc?: string[];
bcc?: string[];
phoneNumber?: string;
latitude?: number;
longitude?: number;
eventTitle?: string;
eventDescription?: string;
eventLocation?: string;
[key: string]: any;
};
// ClipData information
clipData?: {
text?: string;
uri?: string;
htmlText?: string;
}[];
}interface SharedFilesEvent {
data: SharedData;
}
```## Usage
The plugin can be used with various frameworks. Choose your framework below for specific implementation details:- [Angular Implementation](#1-create-a-service-to-handle-the-plugin)
- [React Implementation](#react-example)
- [Vue Implementation](#vue-example)
- [Svelte Implementation](#svelte-example-bonus)### 1. Create a service to handle the plugin
```typescript
// open-with.service.ts
import {Injectable} from '@angular/core';
import {Subject} from 'rxjs';
import {Capacitor} from '@capacitor/core';
import {OpenWith, SharedData, SharedFilesEvent} from '@squareetlabs/capacitor-openwith';@Injectable({
providedIn: 'root'
})
export class OpenWithService {
private filesReceived = new Subject();
public filesReceived$ = this.filesReceived.asObservable();constructor() {
this.init();
}private async init() {
if (!Capacitor.isNativePlatform()) {
console.log('OpenWith only works on native platforms');
return;
}try {
await OpenWith.addHandler(() => {
console.log('OpenWith handler added successfully');
});await OpenWith.setVerbosity({level: 1});
await OpenWith.initialize();await OpenWith.addListener('receivedFiles', (shared: SharedFilesEvent) => {
console.log('Received data:', shared);
if (shared && shared.data) {
this.filesReceived.next(shared.data);
}
});} catch (error) {
console.error('Error initializing OpenWith:', error);
}
}
}
```### 2. Use the service in your component
```typescript
// my-component.ts
import {Component} from '@angular/core';
import {OpenWithService} from './open-with.service';
import {SharedData} from '@squareetlabs/capacitor-openwith';@Component({
selector: 'app-my-component',
template: `
Shared Content
From: {{sharedData.source?.applicationName}}
Type: {{sharedData.type}}
Text: {{sharedData.extras?.text}}
`
})
export class MyComponent {
sharedData: SharedData | null = null;constructor(private openWithService: OpenWithService) {
this.openWithService.filesReceived$.subscribe(
(data: SharedData) => {
this.sharedData = data;
this.handleSharedContent(data);
}
);
}private handleSharedContent(data: SharedData) {
// Example of how to handle different content types
if (data.extras?.text) {
console.log('Shared text:', data.extras.text);
}
if (data.extras?.phoneNumber) {
console.log('Phone number:', data.extras.phoneNumber);
}
if (data.uri) {
console.log('Shared URI:', data.uri);
}
// etc...
}
}
```## Framework Examples
### React Example
```typescript
// OpenWithProvider.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Capacitor } from '@capacitor/core';
import { OpenWith, SharedData } from '@squareetlabs/capacitor-openwith';interface OpenWithContextType {
sharedData: SharedData | null;
}const OpenWithContext = createContext({ sharedData: null });
export const useOpenWith = () => useContext(OpenWithContext);
export const OpenWithProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [sharedData, setSharedData] = useState(null);useEffect(() => {
if (!Capacitor.isNativePlatform()) {
console.log('OpenWith only works on native platforms');
return;
}const initializeOpenWith = async () => {
try {
await OpenWith.addHandler(() => {
console.log('OpenWith handler added successfully');
});await OpenWith.setVerbosity({ level: 1 });
await OpenWith.initialize();await OpenWith.addListener('receivedFiles', (shared) => {
console.log('Received data:', shared);
if (shared && shared.data) {
setSharedData(shared.data);
}
});
} catch (error) {
console.error('Error initializing OpenWith:', error);
}
};initializeOpenWith();
}, []);return (
{children}
);
};// App.tsx
import { OpenWithProvider } from './OpenWithProvider';const App: React.FC = () => {
return (
);
};// SharedContent.tsx
import { useOpenWith } from './OpenWithProvider';const SharedContent: React.FC = () => {
const { sharedData } = useOpenWith();if (!sharedData) return null;
return (
Shared Content
From: {sharedData.source?.applicationName}
Type: {sharedData.type}
Text: {sharedData.extras?.text}
);
};
```### Vue Example
```typescript
// openWithPlugin.ts
import { ref, readonly } from 'vue';
import { Capacitor } from '@capacitor/core';
import { OpenWith, SharedData } from '@squareetlabs/capacitor-openwith';const sharedData = ref(null);
export const useOpenWith = () => {
const initialize = async () => {
if (!Capacitor.isNativePlatform()) {
console.log('OpenWith only works on native platforms');
return;
}try {
await OpenWith.addHandler(() => {
console.log('OpenWith handler added successfully');
});await OpenWith.setVerbosity({ level: 1 });
await OpenWith.initialize();await OpenWith.addListener('receivedFiles', (shared) => {
console.log('Received data:', shared);
if (shared && shared.data) {
sharedData.value = shared.data;
}
});
} catch (error) {
console.error('Error initializing OpenWith:', error);
}
};return {
sharedData: readonly(sharedData),
initialize
};
};// main.ts
import { createApp } from 'vue';
import App from './App.vue';const app = createApp(App);
app.mount('#app');// App.vue
import { onMounted } from 'vue';
import { useOpenWith } from './openWithPlugin';const { sharedData, initialize } = useOpenWith();
onMounted(() => {
initialize();
});
Shared Content
From: {{ sharedData.source?.applicationName }}
Type: {{ sharedData.type }}
Text: {{ sharedData.extras?.text }}
```
### Svelte Example (Bonus)
```typescript
// openWith.ts
import { writable } from 'svelte/store';
import { Capacitor } from '@capacitor/core';
import { OpenWith, SharedData } from '@squareetlabs/capacitor-openwith';export const sharedData = writable(null);
export const initializeOpenWith = async () => {
if (!Capacitor.isNativePlatform()) {
console.log('OpenWith only works on native platforms');
return;
}try {
await OpenWith.addHandler(() => {
console.log('OpenWith handler added successfully');
});await OpenWith.setVerbosity({ level: 1 });
await OpenWith.initialize();await OpenWith.addListener('receivedFiles', (shared) => {
console.log('Received data:', shared);
if (shared && shared.data) {
sharedData.set(shared.data);
}
});
} catch (error) {
console.error('Error initializing OpenWith:', error);
}
};// App.svelte
import { onMount } from 'svelte';
import { sharedData, initializeOpenWith } from './openWith';onMount(() => {
initializeOpenWith();
});{#if $sharedData}
Shared Content
From: {$sharedData.source?.applicationName}
Type: {$sharedData.type}
Text: {$sharedData.extras?.text}
{/if}## Supported Content Types
The plugin can handle various types of shared content:
- Plain text
- URLs
- Images
- Documents
- Contacts
- Locations
- Calendar events
- Phone numbers
- Emails
- Media content
- And more...## Debugging
To enable detailed logging, use:
```typescript
await OpenWith.setVerbosity({level: 1});
```## Supported Platforms
- ✅ Android
- ✅ iOS (coming soon)## License
MIT