Skip to main content

Module 8: Native Features and Device APIs

Sharing and Linking

Share content with other apps and open URLs, deep links, and external applications

🎯 Learning Objectives

  • Use expo-sharing to share files and content
  • Open URLs and external apps with expo-linking
  • Handle incoming deep links
  • Work with the clipboard using expo-clipboard
  • Implement common sharing patterns

Sharing Overview

Mobile apps frequently need to share content with other apps or open external resources. React Native with Expo provides several APIs to handle these scenarios.

Sharing and Linking Ecosystem

flowchart TD
    subgraph YourApp["Your App"]
        A[Content to Share]
        B[URL to Open]
        C[Clipboard Data]
    end
    
    subgraph APIs["Expo APIs"]
        D[expo-sharing]
        E[expo-linking]
        F[expo-clipboard]
    end
    
    subgraph External["External"]
        G[Share Sheet]
        H[Browser/Apps]
        I[System Clipboard]
    end
    
    A --> D --> G
    B --> E --> H
    C --> F --> I
    
    style YourApp fill:#e3f2fd
    style APIs fill:#fff3e0
    style External fill:#e8f5e9

When to Use Each API

Use Case API Example
Share a file or image expo-sharing Share photo to Instagram
Open a website expo-linking Open terms of service
Open another app expo-linking Open Maps for directions
Handle incoming links expo-linking Deep link from email
Copy/paste text expo-clipboard Copy referral code

Installation

# Install all sharing and linking packages
npx expo install expo-sharing expo-linking expo-clipboard

Sharing Files with expo-sharing

The expo-sharing module opens the native share dialog to share files with other apps.

Basic Sharing

import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';

// Check if sharing is available
async function checkSharingAvailable() {
  const isAvailable = await Sharing.isAvailableAsync();
  
  if (!isAvailable) {
    alert('Sharing is not available on this device');
    return false;
  }
  return true;
}

// Share a file
async function shareFile(fileUri: string) {
  if (!(await checkSharingAvailable())) return;
  
  try {
    await Sharing.shareAsync(fileUri);
    console.log('Shared successfully');
  } catch (error) {
    console.error('Error sharing:', error);
  }
}

// Usage
shareFile(FileSystem.documentDirectory + 'myDocument.pdf');

Sharing Options

import * as Sharing from 'expo-sharing';

async function shareWithOptions(fileUri: string) {
  await Sharing.shareAsync(fileUri, {
    // MIME type of the file
    mimeType: 'application/pdf',
    
    // Dialog title (Android only)
    dialogTitle: 'Share this document',
    
    // UTI for iOS (Uniform Type Identifier)
    UTI: 'com.adobe.pdf',
  });
}

// Common MIME types
const mimeTypes = {
  pdf: 'application/pdf',
  png: 'image/png',
  jpg: 'image/jpeg',
  gif: 'image/gif',
  mp4: 'video/mp4',
  mp3: 'audio/mpeg',
  txt: 'text/plain',
  json: 'application/json',
  zip: 'application/zip',
};

Sharing Images

import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library';

// Share an image from the camera roll
async function shareImage(assetUri: string) {
  // For local assets, we can share directly
  await Sharing.shareAsync(assetUri, {
    mimeType: 'image/jpeg',
  });
}

// Share a remote image (must download first)
async function shareRemoteImage(imageUrl: string) {
  // Download to local file system
  const filename = imageUrl.split('/').pop() || 'image.jpg';
  const localUri = FileSystem.cacheDirectory + filename;
  
  const downloadResult = await FileSystem.downloadAsync(
    imageUrl,
    localUri
  );
  
  if (downloadResult.status !== 200) {
    throw new Error('Failed to download image');
  }
  
  // Share the downloaded file
  await Sharing.shareAsync(downloadResult.uri, {
    mimeType: 'image/jpeg',
  });
  
  // Optionally clean up
  await FileSystem.deleteAsync(localUri, { idempotent: true });
}

// Share a base64 image
async function shareBase64Image(base64Data: string) {
  const filename = FileSystem.cacheDirectory + 'shared-image.png';
  
  // Write base64 to file
  await FileSystem.writeAsStringAsync(filename, base64Data, {
    encoding: FileSystem.EncodingType.Base64,
  });
  
  await Sharing.shareAsync(filename, {
    mimeType: 'image/png',
  });
}

Share Component Example

import React, { useState } from 'react';
import { View, Text, Pressable, Image, StyleSheet, Alert } from 'react-native';
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';

interface ShareButtonProps {
  imageUri: string;
  title?: string;
}

export function ShareImageButton({ imageUri, title = 'Share' }: ShareButtonProps) {
  const [isSharing, setIsSharing] = useState(false);

  const handleShare = async () => {
    // Check availability
    const isAvailable = await Sharing.isAvailableAsync();
    if (!isAvailable) {
      Alert.alert('Error', 'Sharing is not available on this device');
      return;
    }

    setIsSharing(true);
    
    try {
      // If it's a remote URL, download first
      if (imageUri.startsWith('http')) {
        const filename = 'shared-image.jpg';
        const localUri = FileSystem.cacheDirectory + filename;
        
        const { uri } = await FileSystem.downloadAsync(imageUri, localUri);
        await Sharing.shareAsync(uri);
        
        // Clean up
        await FileSystem.deleteAsync(uri, { idempotent: true });
      } else {
        // Local file, share directly
        await Sharing.shareAsync(imageUri);
      }
    } catch (error) {
      Alert.alert('Error', 'Failed to share image');
      console.error(error);
    } finally {
      setIsSharing(false);
    }
  };

  return (
    <Pressable
      style={({ pressed }) => [
        styles.shareButton,
        pressed && styles.shareButtonPressed,
        isSharing && styles.shareButtonDisabled,
      ]}
      onPress={handleShare}
      disabled={isSharing}
    >
      <Text style={styles.shareButtonText}>
        {isSharing ? 'Sharing...' : `πŸ“€ ${title}`}
      </Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  shareButton: {
    backgroundColor: '#007AFF',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
  },
  shareButtonPressed: {
    opacity: 0.8,
  },
  shareButtonDisabled: {
    backgroundColor: '#ccc',
  },
  shareButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

⚠️ Sharing Limitations

  • expo-sharing shares files, not plain text or URLs
  • For sharing text/URLs, use the React Native Share API instead
  • The file must be accessible (local file system or downloaded)
  • Some apps may not appear in the share sheet for certain file types

Sharing Text and URLs (React Native Share API)

import { Share, Alert } from 'react-native';

// Share text content
async function shareText(message: string) {
  try {
    const result = await Share.share({
      message: message,
    });
    
    if (result.action === Share.sharedAction) {
      if (result.activityType) {
        // Shared with specific activity type (iOS)
        console.log('Shared via:', result.activityType);
      } else {
        // Shared successfully
        console.log('Shared!');
      }
    } else if (result.action === Share.dismissedAction) {
      // Dismissed (iOS only)
      console.log('Share dismissed');
    }
  } catch (error) {
    Alert.alert('Error', 'Failed to share');
  }
}

// Share a URL
async function shareUrl(url: string, title?: string) {
  try {
    await Share.share({
      message: url,
      title: title, // Android only
      url: url,     // iOS only (will be appended to message)
    });
  } catch (error) {
    console.error(error);
  }
}

// Share with both message and URL
async function shareContent(title: string, message: string, url: string) {
  try {
    await Share.share(
      {
        title,
        message: `${message}\n\n${url}`,
        url, // iOS will use this instead of appending to message
      },
      {
        dialogTitle: title, // Android dialog title
        subject: title,     // Email subject
      }
    );
  } catch (error) {
    console.error(error);
  }
}

// Usage
shareContent(
  'Check out this app!',
  'I found this amazing app that you should try.',
  'https://example.com/app'
);

Opening URLs with expo-linking

The expo-linking module provides utilities for opening URLs, handling deep links, and interacting with other apps.

Opening URLs

import * as Linking from 'expo-linking';

// Open a website in the default browser
async function openWebsite(url: string) {
  const supported = await Linking.canOpenURL(url);
  
  if (supported) {
    await Linking.openURL(url);
  } else {
    console.log(`Cannot open URL: ${url}`);
  }
}

// Open URL without checking (simpler but less safe)
async function openUrl(url: string) {
  try {
    await Linking.openURL(url);
  } catch (error) {
    console.error('Failed to open URL:', error);
  }
}

// Usage
openWebsite('https://reactnative.dev');
openWebsite('https://docs.expo.dev');

Opening Settings

import * as Linking from 'expo-linking';
import { Platform } from 'react-native';

// Open app settings (useful after permission denial)
async function openAppSettings() {
  if (Platform.OS === 'ios') {
    await Linking.openURL('app-settings:');
  } else {
    await Linking.openSettings();
  }
}

// Convenience function
async function openSettings() {
  await Linking.openSettings();
}

Common URL Schemes

import * as Linking from 'expo-linking';

// Phone call
async function makePhoneCall(phoneNumber: string) {
  const url = `tel:${phoneNumber}`;
  await Linking.openURL(url);
}

// Send SMS
async function sendSMS(phoneNumber: string, message?: string) {
  const url = message 
    ? `sms:${phoneNumber}?body=${encodeURIComponent(message)}`
    : `sms:${phoneNumber}`;
  await Linking.openURL(url);
}

// Send email
async function sendEmail(
  to: string,
  subject?: string,
  body?: string
) {
  let url = `mailto:${to}`;
  const params: string[] = [];
  
  if (subject) params.push(`subject=${encodeURIComponent(subject)}`);
  if (body) params.push(`body=${encodeURIComponent(body)}`);
  
  if (params.length > 0) {
    url += `?${params.join('&')}`;
  }
  
  await Linking.openURL(url);
}

// FaceTime (iOS only)
async function startFaceTime(contact: string) {
  await Linking.openURL(`facetime:${contact}`);
}

// Usage examples
makePhoneCall('+1234567890');
sendSMS('+1234567890', 'Hello from the app!');
sendEmail('support@example.com', 'Help Request', 'I need assistance with...');

URL Scheme Reference

Action URL Scheme Example
Website https:// https://example.com
Phone call tel: tel:+1234567890
SMS sms: sms:+1234567890?body=Hello
Email mailto: mailto:test@example.com
App settings app-settings: app-settings: (iOS)

Deep Linking into Other Apps

Many popular apps have their own URL schemes that you can use to open specific content or actions within those apps.

Opening Maps

import * as Linking from 'expo-linking';
import { Platform } from 'react-native';

// Open location in maps app
async function openMaps(
  latitude: number,
  longitude: number,
  label?: string
) {
  const encodedLabel = label ? encodeURIComponent(label) : '';
  
  const url = Platform.select({
    ios: `maps:0,0?q=${latitude},${longitude}${label ? `(${encodedLabel})` : ''}`,
    android: `geo:${latitude},${longitude}?q=${latitude},${longitude}${label ? `(${encodedLabel})` : ''}`,
  });
  
  if (url) {
    await Linking.openURL(url);
  }
}

// Open address in maps
async function openAddress(address: string) {
  const encodedAddress = encodeURIComponent(address);
  
  const url = Platform.select({
    ios: `maps:0,0?q=${encodedAddress}`,
    android: `geo:0,0?q=${encodedAddress}`,
  });
  
  if (url) {
    await Linking.openURL(url);
  }
}

// Open directions
async function openDirections(
  destLat: number,
  destLng: number,
  destLabel?: string
) {
  const destination = `${destLat},${destLng}`;
  
  const url = Platform.select({
    ios: `maps:0,0?daddr=${destination}`,
    android: `google.navigation:q=${destination}`,
  });
  
  if (url) {
    const canOpen = await Linking.canOpenURL(url);
    if (canOpen) {
      await Linking.openURL(url);
    } else {
      // Fallback to web Google Maps
      await Linking.openURL(
        `https://www.google.com/maps/dir/?api=1&destination=${destination}`
      );
    }
  }
}

// Usage
openMaps(37.7749, -122.4194, 'San Francisco');
openAddress('1 Infinite Loop, Cupertino, CA');
openDirections(37.7749, -122.4194);

Opening Social Media Apps

import * as Linking from 'expo-linking';

// Twitter/X
async function openTwitterProfile(username: string) {
  const appUrl = `twitter://user?screen_name=${username}`;
  const webUrl = `https://twitter.com/${username}`;
  
  const canOpenApp = await Linking.canOpenURL(appUrl);
  await Linking.openURL(canOpenApp ? appUrl : webUrl);
}

async function openTweet(tweetId: string) {
  const appUrl = `twitter://status?id=${tweetId}`;
  const webUrl = `https://twitter.com/i/status/${tweetId}`;
  
  const canOpenApp = await Linking.canOpenURL(appUrl);
  await Linking.openURL(canOpenApp ? appUrl : webUrl);
}

// Instagram
async function openInstagramProfile(username: string) {
  const appUrl = `instagram://user?username=${username}`;
  const webUrl = `https://instagram.com/${username}`;
  
  const canOpenApp = await Linking.canOpenURL(appUrl);
  await Linking.openURL(canOpenApp ? appUrl : webUrl);
}

// Facebook
async function openFacebookProfile(profileId: string) {
  const appUrl = `fb://profile/${profileId}`;
  const webUrl = `https://facebook.com/${profileId}`;
  
  const canOpenApp = await Linking.canOpenURL(appUrl);
  await Linking.openURL(canOpenApp ? appUrl : webUrl);
}

// YouTube
async function openYouTubeVideo(videoId: string) {
  const appUrl = `youtube://watch?v=${videoId}`;
  const webUrl = `https://youtube.com/watch?v=${videoId}`;
  
  const canOpenApp = await Linking.canOpenURL(appUrl);
  await Linking.openURL(canOpenApp ? appUrl : webUrl);
}

// LinkedIn
async function openLinkedInProfile(profileId: string) {
  const appUrl = `linkedin://profile/${profileId}`;
  const webUrl = `https://linkedin.com/in/${profileId}`;
  
  const canOpenApp = await Linking.canOpenURL(appUrl);
  await Linking.openURL(canOpenApp ? appUrl : webUrl);
}

// WhatsApp
async function openWhatsAppChat(phoneNumber: string, message?: string) {
  // Phone number should include country code without + or spaces
  const cleanNumber = phoneNumber.replace(/\D/g, '');
  let url = `whatsapp://send?phone=${cleanNumber}`;
  
  if (message) {
    url += `&text=${encodeURIComponent(message)}`;
  }
  
  const canOpen = await Linking.canOpenURL(url);
  if (canOpen) {
    await Linking.openURL(url);
  } else {
    // Web fallback
    await Linking.openURL(
      `https://wa.me/${cleanNumber}${message ? `?text=${encodeURIComponent(message)}` : ''}`
    );
  }
}

Creating a Universal Link Helper

import * as Linking from 'expo-linking';

interface AppLink {
  appUrl: string;
  webUrl: string;
}

async function openWithFallback({ appUrl, webUrl }: AppLink) {
  try {
    const canOpenApp = await Linking.canOpenURL(appUrl);
    
    if (canOpenApp) {
      await Linking.openURL(appUrl);
    } else {
      await Linking.openURL(webUrl);
    }
  } catch (error) {
    // Last resort: try web URL
    await Linking.openURL(webUrl);
  }
}

// Usage
openWithFallback({
  appUrl: 'spotify://album/1234567890',
  webUrl: 'https://open.spotify.com/album/1234567890',
});

πŸ’‘ iOS URL Scheme Queries

On iOS, you must declare URL schemes you want to query in app.json:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "LSApplicationQueriesSchemes": [
          "twitter",
          "instagram",
          "fb",
          "whatsapp",
          "youtube"
        ]
      }
    }
  }
}

Without this, canOpenURL will return false even if the app is installed.

Working with the Clipboard

The expo-clipboard module provides access to the system clipboard for copying and pasting text and images.

Basic Clipboard Operations

import * as Clipboard from 'expo-clipboard';

// Copy text to clipboard
async function copyToClipboard(text: string) {
  await Clipboard.setStringAsync(text);
  console.log('Copied to clipboard!');
}

// Get text from clipboard
async function getFromClipboard(): Promise<string> {
  const text = await Clipboard.getStringAsync();
  return text;
}

// Check if clipboard has content
async function hasClipboardContent(): Promise<boolean> {
  const hasString = await Clipboard.hasStringAsync();
  return hasString;
}

// Usage
await copyToClipboard('Hello, World!');
const pastedText = await getFromClipboard();
console.log('Pasted:', pastedText);

Copy Button Component

import React, { useState, useCallback } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import * as Clipboard from 'expo-clipboard';

interface CopyButtonProps {
  text: string;
  label?: string;
  onCopy?: () => void;
}

export function CopyButton({ text, label = 'Copy', onCopy }: CopyButtonProps) {
  const [copied, setCopied] = useState(false);

  const handleCopy = useCallback(async () => {
    await Clipboard.setStringAsync(text);
    setCopied(true);
    onCopy?.();
    
    // Reset after 2 seconds
    setTimeout(() => setCopied(false), 2000);
  }, [text, onCopy]);

  return (
    <Pressable
      style={({ pressed }) => [
        styles.button,
        copied && styles.buttonCopied,
        pressed && styles.buttonPressed,
      ]}
      onPress={handleCopy}
    >
      <Text style={styles.buttonText}>
        {copied ? 'βœ“ Copied!' : `πŸ“‹ ${label}`}
      </Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#f0f0f0',
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
    flexDirection: 'row',
    alignItems: 'center',
  },
  buttonCopied: {
    backgroundColor: '#d4edda',
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonText: {
    fontSize: 14,
    color: '#333',
  },
});

Copiable Text Component

import React, { useState } from 'react';
import { Text, Pressable, StyleSheet, View } from 'react-native';
import * as Clipboard from 'expo-clipboard';
import * as Haptics from 'expo-haptics';

interface CopiableTextProps {
  children: string;
  style?: object;
  showHint?: boolean;
}

export function CopiableText({ 
  children, 
  style, 
  showHint = true 
}: CopiableTextProps) {
  const [copied, setCopied] = useState(false);

  const handleLongPress = async () => {
    await Clipboard.setStringAsync(children);
    await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  };

  return (
    <View>
      <Pressable onLongPress={handleLongPress}>
        <Text style={[styles.text, style]}>{children}</Text>
      </Pressable>
      
      {showHint && !copied && (
        <Text style={styles.hint}>Long press to copy</Text>
      )}
      
      {copied && (
        <Text style={styles.copiedHint}>Copied!</Text>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  text: {
    fontSize: 16,
    padding: 8,
    backgroundColor: '#f5f5f5',
    borderRadius: 4,
  },
  hint: {
    fontSize: 12,
    color: '#888',
    marginTop: 4,
  },
  copiedHint: {
    fontSize: 12,
    color: '#28a745',
    marginTop: 4,
  },
});

Clipboard with Images (iOS 14+)

import * as Clipboard from 'expo-clipboard';
import * as FileSystem from 'expo-file-system';

// Copy image to clipboard (iOS 14+ only)
async function copyImageToClipboard(imageUri: string) {
  // Read image as base64
  const base64 = await FileSystem.readAsStringAsync(imageUri, {
    encoding: FileSystem.EncodingType.Base64,
  });
  
  await Clipboard.setImageAsync(base64);
}

// Get image from clipboard
async function getImageFromClipboard(): Promise<string | null> {
  const hasImage = await Clipboard.hasImageAsync();
  
  if (!hasImage) {
    return null;
  }
  
  const image = await Clipboard.getImageAsync({ format: 'png' });
  return image?.data ?? null; // Returns base64 string
}

// Check clipboard content type
async function getClipboardContentType() {
  const hasString = await Clipboard.hasStringAsync();
  const hasImage = await Clipboard.hasImageAsync();
  const hasUrl = await Clipboard.hasUrlAsync();
  
  return { hasString, hasImage, hasUrl };
}

Common Sharing Patterns

Here are complete implementations of common sharing scenarios you'll encounter in real apps.

Share Product/Content

import { Share, Alert } from 'react-native';
import * as Linking from 'expo-linking';

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  imageUrl: string;
}

// Generate deep link for your app
function getProductDeepLink(productId: string): string {
  // This should match your app's URL scheme
  return Linking.createURL(`/product/${productId}`);
}

// Share a product
async function shareProduct(product: Product) {
  const deepLink = getProductDeepLink(product.id);
  
  try {
    const result = await Share.share({
      title: product.name,
      message: `Check out ${product.name} - $${product.price}\n\n${product.description}\n\n${deepLink}`,
    });
    
    if (result.action === Share.sharedAction) {
      // Track share event
      analytics.track('product_shared', { productId: product.id });
    }
  } catch (error) {
    Alert.alert('Error', 'Failed to share product');
  }
}

Share App / Invite Friends

import { Share, Platform } from 'react-native';

interface ReferralInfo {
  code: string;
  userId: string;
}

async function shareApp(referral?: ReferralInfo) {
  const appStoreLink = Platform.select({
    ios: 'https://apps.apple.com/app/id123456789',
    android: 'https://play.google.com/store/apps/details?id=com.example.app',
  });
  
  let message = "I've been using this amazing app and thought you'd like it too!";
  
  if (referral) {
    message += `\n\nUse my referral code "${referral.code}" to get 20% off!`;
  }
  
  message += `\n\nDownload here: ${appStoreLink}`;
  
  await Share.share({
    title: 'Check out this app!',
    message,
  });
}

// Referral code component
function ReferralSection({ referralCode }: { referralCode: string }) {
  const handleShare = () => shareApp({ code: referralCode, userId: 'current-user' });
  const handleCopy = async () => {
    await Clipboard.setStringAsync(referralCode);
    Alert.alert('Copied!', 'Referral code copied to clipboard');
  };
  
  return (
    <View style={styles.referralSection}>
      <Text style={styles.referralTitle}>Your Referral Code</Text>
      <View style={styles.codeContainer}>
        <Text style={styles.code}>{referralCode}</Text>
        <Pressable onPress={handleCopy}>
          <Text>πŸ“‹</Text>
        </Pressable>
      </View>
      <Pressable style={styles.shareButton} onPress={handleShare}>
        <Text style={styles.shareButtonText}>Invite Friends</Text>
      </Pressable>
    </View>
  );
}

Contact Support Options

import React from 'react';
import { View, Text, Pressable, StyleSheet, Linking as RNLinking } from 'react-native';
import * as Linking from 'expo-linking';

const SUPPORT_EMAIL = 'support@example.com';
const SUPPORT_PHONE = '+1234567890';
const WHATSAPP_NUMBER = '1234567890';

export function ContactSupport() {
  const handleEmail = async () => {
    const subject = encodeURIComponent('Support Request');
    const body = encodeURIComponent('Please describe your issue:\n\n');
    await Linking.openURL(`mailto:${SUPPORT_EMAIL}?subject=${subject}&body=${body}`);
  };
  
  const handleCall = async () => {
    await Linking.openURL(`tel:${SUPPORT_PHONE}`);
  };
  
  const handleWhatsApp = async () => {
    const message = encodeURIComponent('Hi, I need help with...');
    const url = `whatsapp://send?phone=${WHATSAPP_NUMBER}&text=${message}`;
    
    const canOpen = await Linking.canOpenURL(url);
    if (canOpen) {
      await Linking.openURL(url);
    } else {
      await Linking.openURL(`https://wa.me/${WHATSAPP_NUMBER}?text=${message}`);
    }
  };
  
  const handleChat = () => {
    // Open in-app chat or navigate to chat screen
    navigation.navigate('Chat');
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Contact Support</Text>
      
      <Pressable style={styles.option} onPress={handleEmail}>
        <Text style={styles.optionIcon}>βœ‰οΈ</Text>
        <View>
          <Text style={styles.optionTitle}>Email Us</Text>
          <Text style={styles.optionSubtitle}>{SUPPORT_EMAIL}</Text>
        </View>
      </Pressable>
      
      <Pressable style={styles.option} onPress={handleCall}>
        <Text style={styles.optionIcon}>πŸ“ž</Text>
        <View>
          <Text style={styles.optionTitle}>Call Us</Text>
          <Text style={styles.optionSubtitle}>{SUPPORT_PHONE}</Text>
        </View>
      </Pressable>
      
      <Pressable style={styles.option} onPress={handleWhatsApp}>
        <Text style={styles.optionIcon}>πŸ’¬</Text>
        <View>
          <Text style={styles.optionTitle}>WhatsApp</Text>
          <Text style={styles.optionSubtitle}>Quick response</Text>
        </View>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 16 },
  option: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    backgroundColor: '#f5f5f5',
    borderRadius: 12,
    marginBottom: 12,
  },
  optionIcon: { fontSize: 24, marginRight: 16 },
  optionTitle: { fontSize: 16, fontWeight: '600' },
  optionSubtitle: { fontSize: 14, color: '#666' },
});

Social Links Footer

import React from 'react';
import { View, Pressable, Text, StyleSheet } from 'react-native';
import * as Linking from 'expo-linking';

const SOCIAL_LINKS = [
  { icon: '🐦', name: 'Twitter', url: 'https://twitter.com/yourapp' },
  { icon: 'πŸ“Έ', name: 'Instagram', url: 'https://instagram.com/yourapp' },
  { icon: 'πŸ“˜', name: 'Facebook', url: 'https://facebook.com/yourapp' },
  { icon: 'πŸ’Ό', name: 'LinkedIn', url: 'https://linkedin.com/company/yourapp' },
  { icon: '🌐', name: 'Website', url: 'https://yourapp.com' },
];

export function SocialLinksFooter() {
  const handlePress = async (url: string) => {
    await Linking.openURL(url);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Follow Us</Text>
      <View style={styles.linksRow}>
        {SOCIAL_LINKS.map((link) => (
          <Pressable
            key={link.name}
            style={({ pressed }) => [
              styles.linkButton,
              pressed && styles.linkButtonPressed,
            ]}
            onPress={() => handlePress(link.url)}
            accessibilityLabel={`Open ${link.name}`}
          >
            <Text style={styles.linkIcon}>{link.icon}</Text>
          </Pressable>
        ))}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    padding: 24,
    borderTopWidth: 1,
    borderTopColor: '#eee',
  },
  title: {
    fontSize: 14,
    color: '#666',
    marginBottom: 16,
  },
  linksRow: {
    flexDirection: 'row',
    gap: 16,
  },
  linkButton: {
    width: 48,
    height: 48,
    borderRadius: 24,
    backgroundColor: '#f0f0f0',
    alignItems: 'center',
    justifyContent: 'center',
  },
  linkButtonPressed: {
    backgroundColor: '#e0e0e0',
  },
  linkIcon: {
    fontSize: 24,
  },
});

Hands-On Exercises

Exercise 1: Build a Share Sheet

Create a component that presents multiple sharing options for a piece of content.

Requirements:

  • Share via native share sheet
  • Copy link to clipboard
  • Share to specific apps (WhatsApp, Twitter)
  • Show feedback when copied
Show Solution
import React, { useState } from 'react';
import { View, Text, Pressable, Share, StyleSheet, Modal } from 'react-native';
import * as Clipboard from 'expo-clipboard';
import * as Linking from 'expo-linking';
import * as Haptics from 'expo-haptics';

interface ShareSheetProps {
  visible: boolean;
  onClose: () => void;
  content: {
    title: string;
    message: string;
    url: string;
  };
}

export function ShareSheet({ visible, onClose, content }: ShareSheetProps) {
  const [copied, setCopied] = useState(false);
  
  const fullMessage = `${content.message}\n\n${content.url}`;

  const handleNativeShare = async () => {
    await Share.share({
      title: content.title,
      message: fullMessage,
    });
    onClose();
  };

  const handleCopyLink = async () => {
    await Clipboard.setStringAsync(content.url);
    await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
    setCopied(true);
    setTimeout(() => {
      setCopied(false);
      onClose();
    }, 1500);
  };

  const handleWhatsApp = async () => {
    const url = `whatsapp://send?text=${encodeURIComponent(fullMessage)}`;
    const canOpen = await Linking.canOpenURL(url);
    
    if (canOpen) {
      await Linking.openURL(url);
    } else {
      await Linking.openURL(
        `https://wa.me/?text=${encodeURIComponent(fullMessage)}`
      );
    }
    onClose();
  };

  const handleTwitter = async () => {
    const url = `twitter://post?message=${encodeURIComponent(fullMessage)}`;
    const webUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(fullMessage)}`;
    
    const canOpen = await Linking.canOpenURL(url);
    await Linking.openURL(canOpen ? url : webUrl);
    onClose();
  };

  return (
    <Modal
      visible={visible}
      transparent
      animationType="slide"
      onRequestClose={onClose}
    >
      <Pressable style={styles.overlay} onPress={onClose}>
        <View style={styles.sheet}>
          <View style={styles.handle} />
          
          <Text style={styles.title}>Share</Text>
          
          <View style={styles.options}>
            <ShareOption
              icon="πŸ“€"
              label="Share"
              onPress={handleNativeShare}
            />
            <ShareOption
              icon={copied ? "βœ“" : "πŸ“‹"}
              label={copied ? "Copied!" : "Copy Link"}
              onPress={handleCopyLink}
            />
            <ShareOption
              icon="πŸ’¬"
              label="WhatsApp"
              onPress={handleWhatsApp}
            />
            <ShareOption
              icon="🐦"
              label="Twitter"
              onPress={handleTwitter}
            />
          </View>
          
          <Pressable style={styles.cancelButton} onPress={onClose}>
            <Text style={styles.cancelText}>Cancel</Text>
          </Pressable>
        </View>
      </Pressable>
    </Modal>
  );
}

function ShareOption({ icon, label, onPress }) {
  return (
    <Pressable
      style={({ pressed }) => [
        styles.option,
        pressed && styles.optionPressed,
      ]}
      onPress={onPress}
    >
      <Text style={styles.optionIcon}>{icon}</Text>
      <Text style={styles.optionLabel}>{label}</Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: 'rgba(0,0,0,0.4)',
    justifyContent: 'flex-end',
  },
  sheet: {
    backgroundColor: 'white',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    padding: 20,
    paddingBottom: 40,
  },
  handle: {
    width: 40,
    height: 4,
    backgroundColor: '#ddd',
    borderRadius: 2,
    alignSelf: 'center',
    marginBottom: 20,
  },
  title: {
    fontSize: 18,
    fontWeight: '600',
    textAlign: 'center',
    marginBottom: 20,
  },
  options: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginBottom: 20,
  },
  option: {
    alignItems: 'center',
    padding: 12,
  },
  optionPressed: {
    opacity: 0.6,
  },
  optionIcon: {
    fontSize: 32,
    marginBottom: 8,
  },
  optionLabel: {
    fontSize: 12,
    color: '#666',
  },
  cancelButton: {
    padding: 16,
    alignItems: 'center',
  },
  cancelText: {
    fontSize: 16,
    color: '#007AFF',
  },
});

Exercise 2: Open in Maps with Options

Create a location card that offers multiple map options when tapped.

Requirements:

  • Display location name and address
  • Offer Apple Maps, Google Maps options
  • Get directions option
  • Handle apps not being installed
Show Solution
import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet, ActionSheetIOS, Platform, Alert } from 'react-native';
import * as Linking from 'expo-linking';

interface Location {
  name: string;
  address: string;
  latitude: number;
  longitude: number;
}

export function LocationCard({ location }: { location: Location }) {
  const { name, address, latitude, longitude } = location;

  const openAppleMaps = async (directions = false) => {
    const url = directions
      ? `maps://app?daddr=${latitude},${longitude}`
      : `maps://app?ll=${latitude},${longitude}&q=${encodeURIComponent(name)}`;
    await Linking.openURL(url);
  };

  const openGoogleMaps = async (directions = false) => {
    const appUrl = directions
      ? `comgooglemaps://?daddr=${latitude},${longitude}&directionsmode=driving`
      : `comgooglemaps://?center=${latitude},${longitude}&q=${encodeURIComponent(name)}`;
    
    const webUrl = directions
      ? `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`
      : `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;

    const canOpen = await Linking.canOpenURL(appUrl);
    await Linking.openURL(canOpen ? appUrl : webUrl);
  };

  const showOptions = () => {
    if (Platform.OS === 'ios') {
      ActionSheetIOS.showActionSheetWithOptions(
        {
          options: [
            'Cancel',
            'Open in Apple Maps',
            'Open in Google Maps',
            'Get Directions (Apple Maps)',
            'Get Directions (Google Maps)',
          ],
          cancelButtonIndex: 0,
        },
        async (buttonIndex) => {
          switch (buttonIndex) {
            case 1:
              await openAppleMaps(false);
              break;
            case 2:
              await openGoogleMaps(false);
              break;
            case 3:
              await openAppleMaps(true);
              break;
            case 4:
              await openGoogleMaps(true);
              break;
          }
        }
      );
    } else {
      // Android - use Alert with buttons
      Alert.alert(
        'Open Location',
        'Choose an option',
        [
          { text: 'Cancel', style: 'cancel' },
          { text: 'Google Maps', onPress: () => openGoogleMaps(false) },
          { text: 'Get Directions', onPress: () => openGoogleMaps(true) },
        ]
      );
    }
  };

  return (
    <Pressable
      style={({ pressed }) => [
        styles.card,
        pressed && styles.cardPressed,
      ]}
      onPress={showOptions}
    >
      <Text style={styles.icon}>πŸ“</Text>
      <View style={styles.content}>
        <Text style={styles.name}>{name}</Text>
        <Text style={styles.address}>{address}</Text>
        <Text style={styles.hint}>Tap for directions</Text>
      </View>
      <Text style={styles.arrow}>β€Ί</Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  card: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    backgroundColor: '#fff',
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  cardPressed: {
    opacity: 0.8,
  },
  icon: {
    fontSize: 28,
    marginRight: 12,
  },
  content: {
    flex: 1,
  },
  name: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 4,
  },
  address: {
    fontSize: 14,
    color: '#666',
    marginBottom: 4,
  },
  hint: {
    fontSize: 12,
    color: '#007AFF',
  },
  arrow: {
    fontSize: 24,
    color: '#ccc',
  },
});

Summary

Sharing and linking are essential features that connect your app to the broader mobile ecosystem, enabling users to share content and interact with other apps seamlessly.

🎯 Key Takeaways

  • expo-sharing: Share files through the native share sheet
  • React Native Share: Share text and URLs without files
  • expo-linking: Open URLs, apps, and handle deep links
  • URL schemes: tel:, mailto:, sms:, and app-specific schemes
  • expo-clipboard: Copy and paste text and images
  • Always check availability: Use canOpenURL before opening app URLs
  • Provide fallbacks: Open web URLs when apps aren't installed
  • iOS requires LSApplicationQueriesSchemes: Declare schemes in app.json

In the next lesson, we'll explore the file systemβ€”reading, writing, and managing files on the device.