-
Notifications
You must be signed in to change notification settings - Fork 0
fix: iOS 인스타그램 스토리 공유가 미지원 안내로 막힘 #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
05c9fab
43734c9
2e81a57
496f171
41f5079
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "platforms": ["ios"], | ||
| "ios": { | ||
| "modules": ["InstagramStoryModule"] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| Pod::Spec.new do |s| | ||
| s.name = 'InstagramStory' | ||
| s.version = '1.0.0' | ||
| s.summary = 'iOS 인스타그램 스토리 공유 (전용 pasteboard 키)' | ||
| s.description = '인스타그램이 요구하는 com.instagram.sharedSticker.* pasteboard 키로 이미지를 전달한다.' | ||
| s.author = '' | ||
| s.homepage = 'https://github.com/TeamPiKi/client' | ||
| s.platforms = { :ios => '15.1' } | ||
| s.source = { git: '' } | ||
| s.static_framework = true | ||
|
|
||
| s.dependency 'ExpoModulesCore' | ||
|
|
||
| s.pod_target_xcconfig = { | ||
| 'DEFINES_MODULE' => 'YES', | ||
| 'SWIFT_COMPILATION_MODE' => 'wholemodule' | ||
| } | ||
|
|
||
| s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import ExpoModulesCore | ||
| import UIKit | ||
|
|
||
| /** | ||
| * 인스타그램 스토리 공유 (iOS). | ||
| * | ||
| * 인스타그램은 일반 이미지 붙여넣기가 아니라 `com.instagram.sharedSticker.*` 전용 | ||
| * pasteboard 키를 읽는다. expo-clipboard 로는 이 키를 지정할 수 없어 직접 구현한다. | ||
| * https://developers.facebook.com/docs/instagram-platform/sharing-to-stories/ | ||
| */ | ||
| public class InstagramStoryModule: Module { | ||
| private static let backgroundImageKey = "com.instagram.sharedSticker.backgroundImage" | ||
| private static let scheme = "instagram-stories://share" | ||
| /** 붙여넣기 데이터가 남지 않도록 짧게 만료시킨다 */ | ||
| private static let pasteboardExpirySeconds: TimeInterval = 60 * 5 | ||
|
|
||
| public func definition() -> ModuleDefinition { | ||
| Name("InstagramStory") | ||
|
|
||
| AsyncFunction("shareBackgroundImage") { (base64: String, facebookAppId: String) -> String in | ||
| guard let imageData = Data(base64Encoded: base64) else { | ||
| return "error" | ||
| } | ||
|
|
||
| // App ID 를 붙이지 않으면 인스타그램이 미지원 안내를 띄운다 (2023-01 이후 필수) | ||
| guard let url = URL(string: "\(Self.scheme)?source_application=\(facebookAppId)") else { | ||
| return "error" | ||
| } | ||
|
|
||
| return await MainActor.run { | ||
| guard UIApplication.shared.canOpenURL(url) else { | ||
| return "notInstalled" | ||
| } | ||
|
|
||
| UIPasteboard.general.setItems( | ||
| [[Self.backgroundImageKey: imageData]], | ||
| options: [.expirationDate: Date().addingTimeInterval(Self.pasteboardExpirySeconds)] | ||
| ) | ||
|
|
||
| UIApplication.shared.open(url, options: [:], completionHandler: nil) | ||
|
|
||
| return "success" | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "name": "instagram-story", | ||
| "version": "1.0.0", | ||
| "description": "iOS 인스타그램 스토리 공유 (전용 pasteboard 키)", | ||
| "main": "src/index.ts", | ||
| "private": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import type { ShareInstagramStoryStatusT } from '@piki/core'; | ||
| import Constants from 'expo-constants'; | ||
| import { requireOptionalNativeModule } from 'expo-modules-core'; | ||
|
|
||
| const PLUGIN_PATH = './plugins/withInstagramStoryShare.js'; | ||
|
|
||
| /** | ||
| * app.json 에 등록한 withInstagramStoryShare 플러그인의 facebookAppId 를 읽는다. | ||
| * Info.plist 와 딥링크가 같은 값을 써야 해서 단일 출처로 둔다. | ||
| */ | ||
| export const getFacebookAppId = (): string => { | ||
| const plugins = Constants.expoConfig?.plugins ?? []; | ||
|
|
||
| const entry = plugins.find( | ||
| (plugin): plugin is [string, { facebookAppId?: string }] => | ||
| Array.isArray(plugin) && plugin[0] === PLUGIN_PATH | ||
| ); | ||
|
|
||
| return entry?.[1]?.facebookAppId ?? ''; | ||
| }; | ||
|
|
||
| type InstagramStoryModuleT = { | ||
| shareBackgroundImage: ( | ||
| base64: string, | ||
| facebookAppId: string | ||
| ) => Promise<ShareInstagramStoryStatusT>; | ||
| }; | ||
|
|
||
| /** iOS 전용 모듈 — 안드로이드/미포함 빌드에서는 null */ | ||
| const InstagramStoryModule = requireOptionalNativeModule<InstagramStoryModuleT>('InstagramStory'); | ||
|
|
||
| export const isInstagramStoryModuleAvailable = InstagramStoryModule !== null; | ||
|
|
||
| /** | ||
| * 인스타그램 스토리 편집 화면으로 배경 이미지를 전달한다 (iOS). | ||
| * 모듈이 없거나 App ID 가 비어 있으면 'error' 를 돌려준다. | ||
| */ | ||
| export const shareInstagramStoryBackground = async ( | ||
| base64: string, | ||
| facebookAppId: string | ||
| ): Promise<ShareInstagramStoryStatusT> => { | ||
| if (!InstagramStoryModule || !facebookAppId) return 'error'; | ||
|
|
||
| return InstagramStoryModule.shareBackgroundImage(base64, facebookAppId); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| const { withInfoPlist } = require('@expo/config-plugins'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
pnpm install --frozen-lockfile
pnpm exec eslint apps/app/plugins/withInstagramStoryShare.jsRepository: TeamPiKi/client Length of output: 2860 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- plugin ---'
cat -n apps/app/plugins/withInstagramStoryShare.js
printf '%s\n' '--- nearby config plugins ---'
find apps/app/plugins -maxdepth 1 -type f -print | sort | while read -r file; do
printf '\n### %s\n' "$file"
sed -n '1,12p' "$file"
done
printf '%s\n' '--- ESLint configuration references ---'
rg -n --hidden --glob '!node_modules/**' \
'no-require-imports|no-undef|eslint|withInfoPlist|module\.exports|export default' \
.eslintrc* eslint.config.* package.json apps/app/package.json apps/app/plugins 2>/dev/null || true
printf '%s\n' '--- package scripts ---'
node -e "const p=require('./package.json'); console.log(JSON.stringify(p.scripts,null,2))"Repository: TeamPiKi/client Length of output: 5707 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ESLint configuration ---'
cat -n eslint.config.mjs | sed -n '1,90p'
printf '%s\n' '--- app scripts and lint configuration ---'
node - <<'JS'
const root = require('./package.json');
const app = require('./apps/app/package.json');
console.log('root scripts:', JSON.stringify(root.scripts, null, 2));
console.log('app scripts:', JSON.stringify(app.scripts, null, 2));
JS
printf '%s\n' '--- effective rules for the plugin ---'
pnpm exec eslint --print-config apps/app/plugins/withInstagramStoryShare.js |
node -e "
let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>{
const c=JSON.parse(s);
for (const k of ['no-undef','`@typescript-eslint/no-require-imports`']) {
console.log(k + ':', JSON.stringify(c.rules[k]));
}
});
"
printf '%s\n' '--- all CommonJS plugin entry points ---'
rg -n -U '^const .*require\\(|^module\\.exports' apps/app/pluginsRepository: TeamPiKi/client Length of output: 3825 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- lint results for all Config Plugin files ---'
pnpm exec eslint apps/app/plugins --format stylish || true
printf '%s\n' '--- CommonJS entry points ---'
rg -n '^(const .*require\\(|module\\.exports)' apps/app/plugins
printf '%s\n' '--- Node globals available to ESLint ---'
node - <<'JS'
const globals = require('globals');
console.log(JSON.stringify({
require: globals.node.require,
module: globals.node.module,
}, null, 2));
JS
printf '%s\n' '--- Expo lint command resolution ---'
pnpm --filter piki-app exec expo lint --help 2>&1 | sed -n '1,80p' || trueRepository: TeamPiKi/client Length of output: 2712 Config Plugin 파일에 CommonJS ESLint 예외를 적용하세요.
🧰 Tools🪛 ESLint[error] 1-1: A ( 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| /** | ||
| * iOS 인스타그램 스토리 공유에 필요한 Info.plist 설정. | ||
| * | ||
| * 2023-01 부터 인스타그램이 스토리 공유에 Facebook App ID 를 요구한다. | ||
| * 없으면 인스타그램이 "이 앱은 스토리 공유를 지원하지 않는다" 안내만 띄운다. | ||
| * https://developers.facebook.com/docs/instagram-platform/sharing-to-stories/ | ||
| * | ||
| * `instagram-stories` 스킴은 app.json 의 LSApplicationQueriesSchemes 에 이미 있다. | ||
| */ | ||
| const withInstagramStoryShare = (config, { facebookAppId } = {}) => { | ||
| if (!facebookAppId) { | ||
| // 값이 없으면 조용히 건너뛴다 — 빌드는 되고 스토리 공유만 동작하지 않는다. | ||
| // (환경변수 미설정인 CI/로컬에서 빌드 자체가 깨지지 않도록) | ||
| return config; | ||
| } | ||
|
|
||
| return withInfoPlist(config, config => { | ||
| config.modResults.FacebookAppID = facebookAppId; | ||
|
|
||
| return config; | ||
| }); | ||
| }; | ||
|
|
||
| module.exports = withInstagramStoryShare; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { headers } from 'next/headers'; | ||
|
|
||
| import { isWebview } from '@/utils/webBridge'; | ||
|
|
||
| /** | ||
| * RSC 전용. User-Agent 로 앱(웹뷰) 여부를 판정한다. | ||
| * | ||
| * 클라이언트에서 `useSyncExternalStore` 로 판정하면 hydration 후에야 값이 정해져 | ||
| * 앱 전용 UI 가 뒤늦게 나타난다. 서버에서 미리 내려주면 첫 렌더부터 확정된다. | ||
| */ | ||
| export const getIsApp = async (): Promise<boolean> => { | ||
| const userAgent = (await headers()).get('user-agent'); | ||
|
|
||
| return isWebview(userAgent); | ||
| }; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
멀티라인 매개변수에 후행 쉼표를 추가하세요.
Line 25와 Line 40의 마지막 매개변수 뒤에 ES5 후행 쉼표가 없습니다. TypeScript 스타일 규칙에 맞게 추가하세요.
수정 예시
shareBackgroundImage: ( base64: string, - facebookAppId: string + facebookAppId: string, ) => Promise<ShareInstagramStoryStatusT>; @@ export const shareInstagramStoryBackground = async ( base64: string, - facebookAppId: string + facebookAppId: string, ): Promise<ShareInstagramStoryStatusT> => {Also applies to: 38-41
🤖 Prompt for AI Agents
Source: Coding guidelines