mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
fix(share): attach captured card image when sharing to Instagram Stories
Both share modals opened the instagram-stories:// scheme without ever placing the captured PNG on the pasteboard, so Instagram opened an empty story. Replace the manual scheme launch with react-native-share shareSingle(INSTAGRAM_STORIES), which writes the image to UIPasteboard under com.instagram.sharedSticker.backgroundImage on iOS and uses the ADD_TO_STORY intent on Android. Gate the flow on the required Facebook App ID (EXPO_PUBLIC_FACEBOOK_APP_ID) and Instagram being installed, with fallback to the system share sheet, and remove the dead base64 reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f38b6f14fa
commit
04822043ae
11 files changed
+215
-62
No files matched your search
+16
-10
@@ -13,13 +13,19 @@ EXPO_PUBLIC_API_URL=http://127.0.0.1:3003
|
|||||||
# invoking shell. Do not put those credentials in this Expo environment file or
|
# invoking shell. Do not put those credentials in this Expo environment file or
|
||||||
# commit them. The test user must be disposable, onboarding-complete, and have a
|
# commit them. The test user must be disposable, onboarding-complete, and have a
|
||||||
# matching profile in the standalone non-production PostgreSQL database.
|
# matching profile in the standalone non-production PostgreSQL database.
|
||||||
|
|
||||||
# Sentry error tracking (optional - get DSN from https://sentry.io)
|
# Facebook App ID for Instagram Stories sharing (optional). Instagram requires
|
||||||
EXPO_PUBLIC_SENTRY_DSN=
|
# a registered Facebook App ID for the Stories share flow; without it the
|
||||||
|
# Instagram share button falls back to the system share sheet.
|
||||||
# RevenueCat (for in-app subscriptions)
|
# https://developers.facebook.com/docs/instagram/sharing-to-stories/
|
||||||
# Get your API keys from https://app.revenuecat.com
|
EXPO_PUBLIC_FACEBOOK_APP_ID=
|
||||||
EXPO_PUBLIC_REVENUECAT_IOS_API_KEY=
|
|
||||||
EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY=
|
# Sentry error tracking (optional - get DSN from https://sentry.io)
|
||||||
# Entitlement ID from RevenueCat dashboard (defaults to "pro" if not set)
|
EXPO_PUBLIC_SENTRY_DSN=
|
||||||
EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID=Sauci Pro
|
|
||||||
|
# RevenueCat (for in-app subscriptions)
|
||||||
|
# Get your API keys from https://app.revenuecat.com
|
||||||
|
EXPO_PUBLIC_REVENUECAT_IOS_API_KEY=
|
||||||
|
EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY=
|
||||||
|
# Entitlement ID from RevenueCat dashboard (defaults to "pro" if not set)
|
||||||
|
EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID=Sauci Pro
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="com.android.vending.BILLING"/>
|
<uses-permission android:name="com.android.vending.BILLING"/>
|
||||||
<queries>
|
<queries>
|
||||||
|
<package android:name="com.instagram.android"/>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|||||||
+13
-1
@@ -298,7 +298,19 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"./plugins/withWidgetExported.js",
|
"./plugins/withWidgetExported.js",
|
||||||
"./plugins/withNativeWidget.js"
|
"./plugins/withNativeWidget.js",
|
||||||
|
[
|
||||||
|
"react-native-share",
|
||||||
|
{
|
||||||
|
"ios": [
|
||||||
|
"instagram",
|
||||||
|
"instagram-stories"
|
||||||
|
],
|
||||||
|
"android": [
|
||||||
|
"com.instagram.android"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ PODS:
|
|||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleDataTransport (10.1.0):
|
- GoogleDataTransport (10.1.1):
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- GoogleSignIn (9.1.0):
|
- GoogleSignIn (9.1.0):
|
||||||
@@ -3236,6 +3236,34 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- RNShare (12.3.1):
|
||||||
|
- boost
|
||||||
|
- DoubleConversion
|
||||||
|
- fast_float
|
||||||
|
- fmt
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly
|
||||||
|
- RCT-Folly/Fabric
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- SocketRocket
|
||||||
|
- Yoga
|
||||||
- RNSVG (15.12.1):
|
- RNSVG (15.12.1):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -3517,6 +3545,7 @@ DEPENDENCIES:
|
|||||||
- RNPurchases (from `../../../node_modules/react-native-purchases`)
|
- RNPurchases (from `../../../node_modules/react-native-purchases`)
|
||||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||||
- RNScreens (from `../../../node_modules/react-native-screens`)
|
- RNScreens (from `../../../node_modules/react-native-screens`)
|
||||||
|
- RNShare (from `../../../node_modules/react-native-share`)
|
||||||
- RNSVG (from `../../../node_modules/react-native-svg`)
|
- RNSVG (from `../../../node_modules/react-native-svg`)
|
||||||
- RNWorklets (from `../../../node_modules/react-native-worklets`)
|
- RNWorklets (from `../../../node_modules/react-native-worklets`)
|
||||||
- SocketRocket (~> 0.7.1)
|
- SocketRocket (~> 0.7.1)
|
||||||
@@ -3792,6 +3821,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-reanimated"
|
:path: "../node_modules/react-native-reanimated"
|
||||||
RNScreens:
|
RNScreens:
|
||||||
:path: "../../../node_modules/react-native-screens"
|
:path: "../../../node_modules/react-native-screens"
|
||||||
|
RNShare:
|
||||||
|
:path: "../../../node_modules/react-native-share"
|
||||||
RNSVG:
|
RNSVG:
|
||||||
:path: "../../../node_modules/react-native-svg"
|
:path: "../../../node_modules/react-native-svg"
|
||||||
RNWorklets:
|
RNWorklets:
|
||||||
@@ -3804,8 +3835,8 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||||
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||||
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
|
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||||
EXApplication: 1e98d4b1dccdf30627f92917f4b2c5a53c330e5f
|
EXApplication: 1e98d4b1dccdf30627f92917f4b2c5a53c330e5f
|
||||||
EXAV: b60fcf142fae6684d295bc28cd7cfcb3335570ea
|
EXAV: b60fcf142fae6684d295bc28cd7cfcb3335570ea
|
||||||
EXConstants: 18ac927605517285307ea8f7ffe6fe7ca538eb5d
|
EXConstants: 18ac927605517285307ea8f7ffe6fe7ca538eb5d
|
||||||
@@ -3851,10 +3882,10 @@ SPEC CHECKSUMS:
|
|||||||
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
|
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
|
||||||
FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41
|
FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41
|
||||||
FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b
|
FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b
|
||||||
fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6
|
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||||
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||||
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
|
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: a24e58982ab3ba2f64d79613e027fe7f57e88539
|
||||||
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
|
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||||
@@ -3944,6 +3975,7 @@ SPEC CHECKSUMS:
|
|||||||
RNPurchases: ae1f8f522674a7938dd26eba6c103c40d049dfee
|
RNPurchases: ae1f8f522674a7938dd26eba6c103c40d049dfee
|
||||||
RNReanimated: 45bfba06751010ae2f6a1cd8b9f618ac4ce1a12c
|
RNReanimated: 45bfba06751010ae2f6a1cd8b9f618ac4ce1a12c
|
||||||
RNScreens: 2e9c41cd099b1ca50136af8d57c3594214d0086a
|
RNScreens: 2e9c41cd099b1ca50136af8d57c3594214d0086a
|
||||||
|
RNShare: 61cad1e65b8e3788c97ee7c5cac4bcbb25a8df40
|
||||||
RNSVG: 94a1be05fab4043354bcf7104f0f9b0e2231ef05
|
RNSVG: 94a1be05fab4043354bcf7104f0f9b0e2231ef05
|
||||||
RNWorklets: 09a47bd92d3da6e00420a060fcc86e23b745e980
|
RNWorklets: 09a47bd92d3da6e00420a060fcc86e23b745e980
|
||||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>instagram</string>
|
||||||
|
<string>instagram-stories</string>
|
||||||
|
</array>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
"react-native-share": "^12.3.1",
|
||||||
"react-native-svg": "15.12.1",
|
"react-native-svg": "15.12.1",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-view-shot": "^4.0.3",
|
"react-native-view-shot": "^4.0.3",
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Linking } from "react-native";
|
||||||
|
|
||||||
|
const mockShareSingle = jest.fn();
|
||||||
|
const mockIsPackageInstalled = jest.fn();
|
||||||
|
|
||||||
|
jest.mock("react-native-share", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
shareSingle: (...args: unknown[]) => mockShareSingle(...args),
|
||||||
|
isPackageInstalled: (...args: unknown[]) => mockIsPackageInstalled(...args),
|
||||||
|
},
|
||||||
|
Social: { InstagramStories: "instagramstories" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
function loadShareToInstagramStories() {
|
||||||
|
let shareToInstagramStories: (uri: string) => Promise<boolean>;
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
shareToInstagramStories = require("../../lib/instagramShare").shareToInstagramStories;
|
||||||
|
});
|
||||||
|
return shareToInstagramStories!;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("shareToInstagramStories", () => {
|
||||||
|
const originalAppId = process.env.EXPO_PUBLIC_FACEBOOK_APP_ID;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalAppId === undefined) {
|
||||||
|
delete process.env.EXPO_PUBLIC_FACEBOOK_APP_ID;
|
||||||
|
} else {
|
||||||
|
process.env.EXPO_PUBLIC_FACEBOOK_APP_ID = originalAppId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false without opening Instagram when no Facebook App ID is configured", async () => {
|
||||||
|
delete process.env.EXPO_PUBLIC_FACEBOOK_APP_ID;
|
||||||
|
const share = loadShareToInstagramStories();
|
||||||
|
|
||||||
|
await expect(share("file:///tmp/card.png")).resolves.toBe(false);
|
||||||
|
expect(mockShareSingle).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when Instagram is not installed", async () => {
|
||||||
|
process.env.EXPO_PUBLIC_FACEBOOK_APP_ID = "123456";
|
||||||
|
jest.spyOn(Linking, "canOpenURL").mockResolvedValue(false);
|
||||||
|
const share = loadShareToInstagramStories();
|
||||||
|
|
||||||
|
await expect(share("file:///tmp/card.png")).resolves.toBe(false);
|
||||||
|
expect(mockShareSingle).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shares the captured image as the story background when Instagram is available", async () => {
|
||||||
|
process.env.EXPO_PUBLIC_FACEBOOK_APP_ID = "123456";
|
||||||
|
jest.spyOn(Linking, "canOpenURL").mockResolvedValue(true);
|
||||||
|
mockShareSingle.mockResolvedValue({ success: true });
|
||||||
|
const share = loadShareToInstagramStories();
|
||||||
|
|
||||||
|
await expect(share("file:///tmp/card.png")).resolves.toBe(true);
|
||||||
|
expect(mockShareSingle).toHaveBeenCalledWith({
|
||||||
|
social: "instagramstories",
|
||||||
|
appId: "123456",
|
||||||
|
backgroundImage: "file:///tmp/card.png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the native share fails", async () => {
|
||||||
|
process.env.EXPO_PUBLIC_FACEBOOK_APP_ID = "123456";
|
||||||
|
jest.spyOn(Linking, "canOpenURL").mockResolvedValue(true);
|
||||||
|
mockShareSingle.mockRejectedValue(new Error("share failed"));
|
||||||
|
const share = loadShareToInstagramStories();
|
||||||
|
|
||||||
|
await expect(share("file:///tmp/card.png")).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Platform,
|
Platform,
|
||||||
Linking,
|
|
||||||
Share,
|
Share,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
Image,
|
Image,
|
||||||
@@ -23,8 +22,8 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import ViewShot from "react-native-view-shot";
|
import ViewShot from "react-native-view-shot";
|
||||||
import * as Sharing from "expo-sharing";
|
import * as Sharing from "expo-sharing";
|
||||||
import * as FileSystem from "expo-file-system/legacy";
|
|
||||||
import { colors, gradients, radius, typography, spacing, blur } from "../../theme";
|
import { colors, gradients, radius, typography, spacing, blur } from "../../theme";
|
||||||
|
import { shareToInstagramStories } from "../../lib/instagramShare";
|
||||||
|
|
||||||
const logo = require("../../../assets/logo.png");
|
const logo = require("../../../assets/logo.png");
|
||||||
|
|
||||||
@@ -87,31 +86,9 @@ export function SharePreviewModal({
|
|||||||
const handleInstagramShare = async () => {
|
const handleInstagramShare = async () => {
|
||||||
if (!capturedUri) return;
|
if (!capturedUri) return;
|
||||||
|
|
||||||
try {
|
const shared = await shareToInstagramStories(capturedUri);
|
||||||
// Check if Instagram is installed
|
if (!shared) {
|
||||||
const instagramUrl = 'instagram://app';
|
// Instagram Stories sharing unavailable - fall back to the share sheet
|
||||||
const canOpenInstagram = await Linking.canOpenURL(instagramUrl);
|
|
||||||
|
|
||||||
if (canOpenInstagram && Platform.OS === 'ios') {
|
|
||||||
// For iOS, we can use the Instagram Stories URL scheme
|
|
||||||
// First, we need to copy the image to a location Instagram can access
|
|
||||||
const base64 = await FileSystem.readAsStringAsync(capturedUri, {
|
|
||||||
encoding: FileSystem.EncodingType.Base64,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use Instagram Stories URL scheme
|
|
||||||
const instagramStoriesUrl = `instagram-stories://share?source_application=com.sauci.app`;
|
|
||||||
|
|
||||||
// Try to open Instagram Stories with the image
|
|
||||||
// Note: This requires the image to be in the pasteboard on iOS
|
|
||||||
await Linking.openURL(instagramStoriesUrl);
|
|
||||||
} else {
|
|
||||||
// Fallback to regular sharing
|
|
||||||
await handleMoreShare();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Instagram share error:', error);
|
|
||||||
// Fallback to regular sharing
|
|
||||||
await handleMoreShare();
|
await handleMoreShare();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Linking,
|
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
Share,
|
Share,
|
||||||
@@ -16,8 +15,8 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import ViewShot from "react-native-view-shot";
|
import ViewShot from "react-native-view-shot";
|
||||||
import * as Sharing from "expo-sharing";
|
import * as Sharing from "expo-sharing";
|
||||||
import * as FileSystem from "expo-file-system/legacy";
|
|
||||||
import { colors, spacing, typography, blur } from "../../../theme";
|
import { colors, spacing, typography, blur } from "../../../theme";
|
||||||
|
import { shareToInstagramStories } from "../../../lib/instagramShare";
|
||||||
import { Events } from "../../../lib/analytics";
|
import { Events } from "../../../lib/analytics";
|
||||||
import { QuizResultsShareCard } from "./QuizResultsShareCard";
|
import { QuizResultsShareCard } from "./QuizResultsShareCard";
|
||||||
|
|
||||||
@@ -63,22 +62,11 @@ export function QuizShareModal({ visible, onClose, scorePercent }: QuizShareModa
|
|||||||
const handleInstagramShare = async () => {
|
const handleInstagramShare = async () => {
|
||||||
if (!capturedUri) return;
|
if (!capturedUri) return;
|
||||||
|
|
||||||
try {
|
const shared = await shareToInstagramStories(capturedUri);
|
||||||
const instagramUrl = "instagram://app";
|
if (shared) {
|
||||||
const canOpenInstagram = await Linking.canOpenURL(instagramUrl);
|
Events.quizShared();
|
||||||
|
} else {
|
||||||
if (canOpenInstagram && Platform.OS === "ios") {
|
// Instagram Stories sharing unavailable - fall back to the share sheet
|
||||||
await FileSystem.readAsStringAsync(capturedUri, {
|
|
||||||
encoding: FileSystem.EncodingType.Base64,
|
|
||||||
});
|
|
||||||
const instagramStoriesUrl = "instagram-stories://share?source_application=com.sauci.app";
|
|
||||||
await Linking.openURL(instagramStoriesUrl);
|
|
||||||
Events.quizShared();
|
|
||||||
} else {
|
|
||||||
await handleMoreShare();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Instagram share error:", error);
|
|
||||||
await handleMoreShare();
|
await handleMoreShare();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Linking, Platform } from "react-native";
|
||||||
|
import Share, { Social } from "react-native-share";
|
||||||
|
|
||||||
|
// Instagram requires a registered Facebook App ID for the Stories share flow
|
||||||
|
// (https://developers.facebook.com/docs/instagram/sharing-to-stories/).
|
||||||
|
const facebookAppId = process.env.EXPO_PUBLIC_FACEBOOK_APP_ID;
|
||||||
|
|
||||||
|
const INSTAGRAM_ANDROID_PACKAGE = "com.instagram.android";
|
||||||
|
|
||||||
|
async function isInstagramStoriesAvailable(): Promise<boolean> {
|
||||||
|
if (!facebookAppId) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Platform.OS === "ios") {
|
||||||
|
return await Linking.canOpenURL("instagram-stories://share");
|
||||||
|
}
|
||||||
|
if (Platform.OS === "android") {
|
||||||
|
const { isInstalled } = await Share.isPackageInstalled(INSTAGRAM_ANDROID_PACKAGE);
|
||||||
|
return isInstalled;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share a locally captured image (file:// URI) to Instagram Stories as the
|
||||||
|
* story background. Returns true when the share was handed off to Instagram,
|
||||||
|
* false when Instagram Stories sharing is unavailable (not installed, no
|
||||||
|
* Facebook App ID configured, or the user dismissed the native prompt) so
|
||||||
|
* callers can fall back to the system share sheet.
|
||||||
|
*/
|
||||||
|
export async function shareToInstagramStories(imageUri: string): Promise<boolean> {
|
||||||
|
if (!(await isInstagramStoriesAvailable())) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await Share.shareSingle({
|
||||||
|
social: Social.InstagramStories,
|
||||||
|
appId: facebookAppId as string,
|
||||||
|
backgroundImage: imageUri,
|
||||||
|
});
|
||||||
|
return result.success !== false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Instagram Stories share error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10
@@ -381,6 +381,7 @@
|
|||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
"react-native-share": "^12.3.1",
|
||||||
"react-native-svg": "15.12.1",
|
"react-native-svg": "15.12.1",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-view-shot": "^4.0.3",
|
"react-native-view-shot": "^4.0.3",
|
||||||
@@ -25256,6 +25257,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-share": {
|
||||||
|
"version": "12.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-12.3.1.tgz",
|
||||||
|
"integrity": "sha512-mRVRie0qKbtj+jWqJ2POPkeSd8SxnMp6aazFZVmq8ZbkUGtQLENR/L58ky8zH4VUEF/WYRmdBiBHWHVtzOewtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-svg": {
|
"node_modules/react-native-svg": {
|
||||||
"version": "15.12.1",
|
"version": "15.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
|
||||||
|
|||||||
Reference in new issue
Block a user