# 친구 초대 미션

친구 초대 미션은 [<mark style="color:orange;">\[Client Admin\]</mark>](https://client-admin.bleepy.io/)에서 프로모션 등록 시 설정할 수 있는 게임 미션입니다. \
친구 초대 코드를 공유하여 신규 유저를 초대하면 초대한 유저와 초대받은 유저가 모두 리워드를 받을 수 있습니다.\
언어별 설정 방법이 다르므로 개발 언어를 먼저 확인해 주세요!

***

## Android

런처 Activity 내 선언된 <mark style="color:blue;">`Javascript Interface`</mark> 에 <mark style="color:blue;">`sharedInviteLink`</mark> 케이스를 추가합니다.\
해당 코드 실행 시 OS 공유 시트를 노출 시키게 됩니다.

<table><thead><tr><th width="270">Interface function</th><th>Description</th></tr></thead><tbody><tr><td><mark style="color:orange;"><code>sharedInviteLink()</code></mark></td><td><p></p><p>친구 초대 링크 공유 시 호출되는 함수</p><ul><li><p>parameters</p><ul><li><mark style="color:blue;"><code>inviteCode</code></mark> - 발급된 초대코드</li><li><mark style="color:blue;"><code>infoMessage</code></mark> - 안내 메세지</li><li><mark style="color:blue;"><code>link</code></mark> - 초대 링크</li></ul></li></ul></td></tr></tbody></table>

```kotlin
// 블리피 Launcher Web <-> App 인터페이스 규격
class ExampleWebAppInterface(private val mContext: Context) {
    ...
    @JavascriptInterface
    fun sharedInviteLink(inviteCode: String, infoMessage: String, link: String) {
        // 초대 미션 공유
        val msg = "초대코드: $inviteCode\n$infoMessage\n$link"

        // Android Sharesheet를 통해 텍스트 콘텐츠를 공유합니다.
        val sendIntent: Intent = Intent().apply {
            action = Intent.ACTION_SEND
            putExtra(Intent.EXTRA_TEXT, msg)
            type = "text/plain"
        }
        
        val shareIntent = Intent.createChooser(sendIntent, null)
        mContext.startActivity(shareIntent)
    }
    ...
}
```

***

## iOS

### 1. 링크 공유 시트 노출을 위한 구조체 생성

친구 초대 데이터를 전달 받고 iOS 앱 안에서 공유 시트를 노출 시키는 구조체를 생성합니다.

* 공유 시트 노출을 위해 <mark style="color:blue;">`UIViewControllerRepresentable`</mark> 사용

```swift
// ShareSheet.swift
import SwiftUI
import UIKit

// postMessage로 넘어오는 문자열을 JSON 형태로 파싱하기 위해 선언
// inviteCode - 발급된 초대코드
// infoMessage - Client Admin에 입력된 안내 메세지
// link - Client Admin에 입력된 초대 링크
struct InviteLink: Codable {
    var inviteCode: String
    var infoMessage: String
    var link: String
}

// 공유하기 시트
struct ShareSheet: UIViewControllerRepresentable {
    var items: [Any] // 공유할 항목

    func makeUIViewController(context: Context) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
        // 업데이트가 필요할 경우 여기에 로직 추가
    }
}
```

### 2. WebView 구조체에서 Script Message 핸들링 추가

* <mark style="color:blue;">`WKWebView`</mark> 의 <mark style="color:blue;">`userContentController`</mark> 에서 친구 초대 링크 공유 관련 이벤트 처리 로직을 추가합니다.
* 런처는 <mark style="color:blue;">`postMessage`</mark> 문자열 데이터 형태로 전달하기 때문에 JSON 형태로 디코딩 과정이 필요합니다.
* 디코딩된 데이터를 부모 contentView의 <mark style="color:blue;">`onSharedInviteLink`</mark> 콜백 함수에 전달합니다.

```swift
// LauncherWebView.swift
import WebKit
import SwiftUI

struct LauncherWebView: UIViewRepresentable {
    ...
    // 초대 링크 처리할 콜백 함수
    let onSharedInviteLink: (String, String, String) -> Void
    
    class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
        ...
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            // 런처 친구초대 링크 공유 이벤트 수신 시 처리
            if message.name == "sharedInviteLink" {
                // 메시지의 body는 JSON.stringify 문자열이라 디코딩 필요
                if let jsonString = message.body as? String,
                   let jsonData = jsonString.data(using: .utf8) {
                    do {
                        // JSON 문자열을 ShareSheet 클래스 내 InviteLink 구조체로 디코딩
                        let inviteData = try JSONDecoder().decode(InviteLink.self, from: jsonData)
                        // 부모의 onSharedInviteLink 메서드 호출
                        parent.onSharedInviteLink(inviteData.inviteCode, inviteData.infoMessage, inviteData.link)
                    } catch {
                        print("Failed to decode JSON: (error.localizedDescription)")
                    }
                } else {
                    print("Failed to convert message body to String or Data")
                }
            }
        }
        ...
    }
    
    ...
    func makeUIView(context: Context) -> WKWebView {
        ...
        // javascript에서 전달하는 이벤트 등록
        contentController.add(context.coordinator, name: "sharedInviteLink")
        ...
    }
    ...
    
    static func dismantleUIView(_ uiView: WKWebView, coordinator: Self.Coordinator) {
        uiView.configuration.userContentController.removeScriptMessageHandler(forName: "sharedInviteLink")
    }
}
```

### 3. WebView가 포함된 ContentView 내에서 ShareSheet 클래스 호출

<mark style="color:blue;">`WebView`</mark> 에 실행된 런처에서 친구 초대 링크 공유에 대한 이벤트가 발생하면 <mark style="color:blue;">`ContentView`</mark>에 선언된 <mark style="color:blue;">`onSharedInviteLink`</mark> 콜백 함수가 호출 됩니다.

* <mark style="color:blue;">`shareItems`</mark> state 값에 공유할 내용을 설정합니다.
* <mark style="color:blue;">`isShowingShareSheet`</mark> state 값 변경을 통해 <mark style="color:blue;">`ShareSheet`</mark> 노출 여부를 결정합니다.

```swift
// LauncherContentView.swift
import SwiftUI
import WebKit

struct LauncherContentView: View {
    ...
    // ShareSheet를 표시할 때 사용할 상태 변수
    @State private var isShowingShareSheet = false
    @State private var shareItems: [Any] = []
    
    var body: some View {
        LauncherWebView(
            url: URL(string: "런처 URL"),
            // 친구 초대 링크 공유 이벤트 핸들링 처리
            onSharedInviteLink: { inviteCode, infoMessage, link in
                // 공유할 항목 설정
                shareItems = ["\(infoMessage)\n\n초대코드 : \(inviteCode)\n\n\(link)"]
                // ShareSheet를 표시하도록 상태를 변경
                isShowingShareSheet = true
            },
        )
        .navigationBarBackButtonHidden(true)
        // ShareSheet를 표시하는 시트 추가
        .sheet(isPresented: $isShowingShareSheet) {
            ShareSheet(items: shareItems)
        }
    }
    ...
    
}
```

***

## Flutter

### :handshake: OS 공유 시트 사용 시

#### 1. 링크 공유 패키지 설치

아래 패키지를 설치하면 링크 공유 기능을 수행할 수 있습니다.

* 사용 패키지: <mark style="color:blue;">`share_plus`</mark>
  * 해당 패키지는 플랫폼의 공유 대화상자를 통해 콘텐츠 공유를 가능하게 합니다.

```sh
flutter pub add share_plus
```

#### 2. javascript handler 추가

런처를 사용하는 Screen 내 <mark style="color:blue;">`Javascript Handler`</mark> 에 <mark style="color:blue;">`sharedInviteLink`</mark> 타입을 추가합니다. <mark style="color:blue;">`handlerName`</mark> 값은 <mark style="color:blue;">`BlpLauncher`</mark> 로 설정해야 합니다.

<table><thead><tr><th width="235">Type</th><th>Description</th></tr></thead><tbody><tr><td><mark style="color:orange;"><code>sharedInviteLink</code></mark></td><td><p></p><p>친구 초대 링크 공유 시 호출</p><ul><li><mark style="color:blue;"><code>inviteCode</code></mark> - 발급된 초대코드</li><li><mark style="color:blue;"><code>infoMessage</code></mark> - 안내 메세지</li><li><mark style="color:blue;"><code>link</code></mark> - 초대 링크</li></ul></td></tr></tbody></table>

<pre class="language-dart"><code class="lang-dart">// lib/screens/bleepy_screen.dart
<strong>InAppWebView(
</strong>    key: webViewKey,
    initialUrlRequest:
        URLRequest(url: WebUri(widget.launcherUrl)),
    initialSettings: options,
    onWebViewCreated: (controller) {
        webViewController = controller;
        // 자바스크립트 핸들러 추가
        controller.addJavaScriptHandler(handlerName: 'BlpLauncher', callback: (message) {
            final data = jsonDecode(message[0]);
            final key = data['type'];
            switch (key) {
                ...
                // 추가
                case "sharedInviteLink":
                    sharedInviteLink(data["inviteCode"], data["infoMessage"], data["link"]);
                    break;
                default:
                    break;
            }
        });
    }
)
</code></pre>

#### 3. sharedInviteLink(inviteCode, infoMessage, link)

<mark style="color:blue;">`share_plus`</mark> 패키지를 통해서 친구 초대 링크를 공유하는 기능을 수행합니다.

```dart
// lib/screens/bleepy_screen.dart
import 'package:share_plus/share_plus.dart';
// 친구초대 링크 공유하기
void sharedInviteLink(String infoMessage, String inviteCode, String link) async {
    try {
        // 메세지 예시
        const msg = "초대코드: $inviteCode\n$infoMessage\n$link"
        await Share.share(msg);
    } catch (error: any) {
        // Share API error
    }
}
```

### :handshake: 카카오톡 공유 기능 사용 시

Flutter에서 카카오톡 공유 기능을 위해 추가적인 조치가 필요합니다. 카카오톡 실행 URL 오픈 시 필요한 아래 패키지를 설치해주세요.

```sh
flutter pub add url_launcher
```

<mark style="color:blue;">`flutter_inappwebview`</mark> 내 <mark style="color:blue;">`shouldOverrideUrlLoading`</mark> 에 아래 로직을 추가합니다.

* 기존 앱 존재 시 : 앱 실행
* 미설치 시 : 스토어 이동

```dart
...
import 'dart:io';
import 'package:url_launcher/url_launcher.dart';

InAppWebView(
  ...
  shouldOverrideUrlLoading: (controller, navigationAction) async {
    var uri = navigationAction.request.url!;
  
    if(Platform.isIOS) {
      // iOS - kakao
      if (uri.toString().startsWith("kakaolink://")) {
        if (await canLaunchUrl(uri)) {
          // KakaoTalk is installed, launch it
          await launchUrl(uri, mode: LaunchMode.externalApplication);
        } else {
          // KakaoTalk is not installed, redirect to App Store
          const appStoreUrl = "https://apps.apple.com/app/id362057947";
          await launchUrl(Uri.parse(appStoreUrl), mode: LaunchMode.externalApplication);
        }
  
        return NavigationActionPolicy.CANCEL;
      }
    }
  
    if(Platform.isAndroid) {
      // AOS - kakao
      if (uri.toString().startsWith("intent:")) {
        final fallbackUrl = Uri.tryParse(uri.queryParameters['browser_fallback_url'] ?? '');
  
        if (fallbackUrl != null && await canLaunchUrl(fallbackUrl)) {
          await launchUrl(fallbackUrl, mode: LaunchMode.externalApplication);
        } else {
          final kakaolinkUrl = uri.toString().replaceFirst("intent:", "");
  
          try {
            await launchUrl(Uri.parse(kakaolinkUrl), mode: LaunchMode.externalApplication);
          } catch (e) {
            print("Failed to launch KakaoLink URL: $e");
  
            // KakaoTalk is not installed, redirect to Google Play Store
            const appStoreUrl = "https://play.google.com/store/apps/details?id=com.kakao.talk";
            await launchUrl(Uri.parse(appStoreUrl), mode: LaunchMode.externalApplication);
          }
        }
        return NavigationActionPolicy.CANCEL;
      }
    }
  
    return NavigationActionPolicy.ALLOW;
  },
  ...
)
```

iOS 환경의 경우 <mark style="color:blue;">`Info.plist`</mark> 파일 내 <mark style="color:blue;">`kakaolink`</mark> 값을 추가해주세요.

```xml
// ios/Runner/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>kakaolink</string>
</array>
```

***

## React Native

### :handshake: OS 공유 시트 사용 시

기본 제공되는 <mark style="color:blue;">`Share API`</mark> 를 통해 친구 초대 링크를 공유하는 기능을 수행합니다. \ <mark style="color:blue;">`WebView`</mark> 의 <mark style="color:blue;">`onMessage`</mark> 함수 내에  해당 케이스를 추가합니다.

<table><thead><tr><th width="208">Type</th><th>Description</th></tr></thead><tbody><tr><td><mark style="color:orange;"><code>sharedInviteLink</code></mark></td><td><p></p><p>친구 초대 링크 공유 시 호출되는 함수</p><ul><li><mark style="color:blue;"><code>inviteCode</code></mark> - 발급된 초대코드</li><li><mark style="color:blue;"><code>infoMessage</code></mark> - 안내 메세지</li><li><mark style="color:blue;"><code>link</code></mark> - 초대 링크</li></ul></td></tr></tbody></table>

```typescript
// src/screens/launcher/Launcher.tsx
import {WebView, WebViewMessageEvent} from 'react-native-webview';
export default function Launcher() {
    // WebView에서 보내온 메세지 처리
    const onMessage = (e: WebViewMessageEvent) => {
        const {type, inviteCode, infoMessage, link}
            = JSON.parse(e.nativeEvent.data);
        switch (type) {
            ...
            // 추가
            case 'sharedInviteLink':
                sharedInviteLink(inviteCode, infoMessage, link);
                break;
            }
        };
        return (
            <View>
                <WebView
                    ...
                    onMessage={onMessage}
                    ...
                />
            </View>
        );
    }
}
```

#### 1. sharedInviteLink(inviteCode, infoMessage, link)

<mark style="color:blue;">`Share API`</mark> 를 통해서 친구 초대 링크를 공유하는 기능을 수행합니다.

```typescript
// src/screens/launcher/Launcher.tsx
import { Share } from 'react-native';
// 친구초대 링크 공유하기
const sharedInviteLink = async (inviteCode: string, infoMessage: string, link: string) => {
    try {
        // 메세지 예시
        const msg = '{infoMessage}\n초대코드 : {inviteCode}\n\n{link}'
        await Share.share({
            message: msg,
        });
    } catch (error: any) {
        // Share API error
    }
};
```

### :handshake: 카카오톡 공유 기능 사용 시

React Native 앱으로 개발된 경우, 카카오톡 공유 기능을 위해 추가적인 조치가 필요합니다.\
올바른 URL 스키마 처리를 위해 아래 패키지를 설치해주세요.&#x20;

```sh
npm install react-native-url-polyfill
```

패키지 설치가 완료되면 <mark style="color:blue;">`WebView`</mark> 관련 설정을 추가합니다.

* <mark style="color:orange;">`originWhiteList`</mark>
* <mark style="color:orange;">`domStorageEnabled`</mark>
* <mark style="color:orange;">`onShouldStartLoadWithRequest`</mark>

<mark style="color:blue;">`handleShouldStartLoadWithRequest`</mark> 메서드 내에서 AOS, iOS 플랫폼 별 처리를 진행합니다.\
앱이 설치된 경우는 앱을 실행하고, 앱 미설치 시 스토어로 이동하게 됩니다.

<pre class="language-typescript"><code class="lang-typescript">import 'react-native-url-polyfill/auto';
import {Alert, Linking, Platform} from 'react-native';

export default function Launcher() {
  ...
  const handleShouldStartLoadWithRequest = (event: any): boolean => {
    const url = event.url;

    // 1. Android intent scheme process
    if (Platform.OS === 'android' &#x26;&#x26; url.startsWith('intent:')) {
      const handleAndroidIntent = async () => {
        try {
          const kakaoUrl = url.replace('intent:', '');
          const fallbackUrl = new URLSearchParams(kakaoUrl).get(
            'browser_fallback_url',
          );
          const canOpen = await Linking.canOpenURL(kakaoUrl);

          if (canOpen) {
            // run KakaoTalk
            await Linking.openURL(kakaoUrl);
          } else if (fallbackUrl) {
            // move to fallback URL
            await Linking.openURL(fallbackUrl);
          } else {
            // move to Google Play Store
            const playStoreUrl =
              'https://play.google.com/store/apps/details?id=com.kakao.talk';
            await Linking.openURL(playStoreUrl);
          }
        } catch (error) {
          console.error('Error handling intent URL:', error);
        }
      };

      handleAndroidIntent();
      return false; // WebView load stop
    }
  
    // 2. IOS kakaolink process
    if (Platform.OS === 'ios' &#x26;&#x26; url.startsWith('kakaolink://')) {
      const handleIosKakaoLink = async () => {
        try {
          const canOpen = await Linking.canOpenURL(url);
          if (canOpen) {
            await Linking.openURL(url);
          } else {
            const appStoreUrl = 'https://apps.apple.com/app/id362057947';
            await Linking.openURL(appStoreUrl);
          }
        } catch (error) {
          console.error('Error handling Kakao URL on iOS:', error);
        }
      };
      handleIosKakaoLink();
      return false; // WebView load stop
    }

    return true; // WebView load allow
  };
  ...

<strong>  return (
</strong>    &#x3C;View>
      &#x3C;WebView
        ref={webviewRef}
        source={{
          uri: route.params.url,
        }}
        originWhitelist={['kakaolink', 'intent', 'http', 'https']}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        cacheEnabled={false}
        // 카카오톡 공유하기 처리
        onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
      />
    &#x3C;/View>
  );
}
</code></pre>

### Android 추가 처리

<mark style="color:blue;">`AndroidManifest.xml`</mark> 파일 내 수정이 필요합니다.

* <mark style="color:blue;">`<queries></queries>`</mark> 선언을 통해 <mark style="color:blue;">`kakaolink`</mark> 외부 <mark style="color:blue;">`intents`</mark>를 명시합니다.
* <mark style="color:blue;">`<application>`</mark> 및 <mark style="color:blue;">`<activity>`</mark> 태그 내 <mark style="color:blue;">`android:exported`</mark> 속성 값을 <mark style="color:blue;">`true`</mark>로 설정합니다.

````xml
// android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <!-- Declare queries for external intents -->
    <queries>
      <!-- KakaoTalk-specific intent -->
      <intent>
          <action android:name="android.intent.action.VIEW" />
          <data android:scheme="kakaolink" />
      </intent>
    </queries>

    <application
      ...
      android:exported="true">
      <activity
        ...
        android:exported="true">
        ...
      </activity>
    </application>
</manifest>

```
````

### iOS

<mark style="color:blue;">`Info.plist`</mark> 파일 내 <mark style="color:blue;">`kakaolink`</mark> URL 스키마 추가가 필요합니다.

```xml
// ios/{projectName}/Info.plist
...
<!-- KakaoTalk URL Scheme Support -->
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>kakaolink</string>
</array>
...
```

***

## Web

Web 도메인의 서비스인 경우 블리피 런처에서 기본적으로 [Web Share API](https://developer.mozilla.org/ko/docs/Web/API/Web_Share_API)사용하게 됩니다.

친구 초대 미션 사용 시 임베디드 코드 내 <mark style="color:orange;">유저의 회원가입 시점</mark> 추가 전달이 필요합니다.

<table><thead><tr><th width="153">Type</th><th>Description</th></tr></thead><tbody><tr><td><mark style="color:orange;"><code>signUpAt</code></mark></td><td><p></p><p>신규 유저 판단을 위한 회원가입 일시를 전달해야 합니다.</p><ul><li>timestamp 형식 (10자리)</li><li>ex) 1717143687</li></ul></td></tr></tbody></table>

```html
<!-- {signUpAt} 영역에 로그인된 회원의 회원가입 일시 전달 (timestamp) -->
<div style="height: 100%; max-width: 960px">
  <iframe
    src="https://web-launcher.bleepy.io?userKey={userKey}&secretKey={secretKey}&signUpAt={signUpAt}"
    style="overflow: hidden; width: 100%; height: 100%"
    allowfullscreen
    sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation"
    id="bleepy-iframe"
    allow="web-share;clipboard-read; clipboard-write"
  />
</div>
```

{% hint style="info" %}
개발에 대한 추가 설명이 더 필요하신가요?

"[<mark style="color:orange;">\[Client Admin\]</mark>](https://client-admin.bleepy.io/login) 로그인 → 오른쪽 하단 채널톡 위젯" 클릭 후 개발 카테고리에 문의 남겨주시면 기술 개발팀에서 확인 후 연락드리겠습니다.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://bleepy.gitbook.io/bleepy-developers/game-promotion/addon/invite-mission.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
