React Native SDK - ディープリンクのサポート

ドキュメント

ディープリンクサポートの追加

ディープリンクは、ユーザーをアプリ内の特定のコンテンツに誘導します。アプリがインストールされたデバイスでユーザーがディープリンクをタップすると、アプリは製品ページや特定の体験など、目的のコンテンツに直接開きます。

Singularトラッキングリンクは、標準ディープリンク(インストール済みアプリ用)とディファードディープリンク(新規インストール用)の両方をサポートしています。包括的な情報については、ディープリンクFAQと シンギュラーリンクFAQを参照してください。

実装パス:このガイドでは、ベアReact Nativeと Expoの両方の実装をカバーします。ExpoはReact Native開発を簡素化するためにネイティブコード管理を抽象化する一方、ベアReact NativeはネイティブiOSおよびAndroidフォルダへの直接アクセスを提供するため、この区別が存在します。プロジェクトの構造に合わせて適切な実装パスを選択してください。


必要条件

前提条件

アプリのディープリンクを有効にするには、Singular Links Prerequisitesを完了してください。

注意事項


Singularリンクハンドラの実装

SingularLink ハンドラーは、アプリが開いたときに Singular トラッキングリンクからディープリンク、ディファードディープリンク、パススルーパラメータ、URL パラメータを取得するコールバックメカニズムを提供します。

利用可能なパラメータ

  • ディープリンク (_dl):リンクをクリックしたユーザーのアプリ内のリンク先URL
  • ディファードディープリンク(_ddl):リンクをクリックした後にアプリをインストールしたユーザーのリンク先URL
  • パススルー (_p):追加コンテキストのためにトラッキングリンクを通して渡されるカスタムデータ
  • urlParameters:アプリを開いたSingularリンクからのクエリパラメータのオブジェクト。Deferred Deep Linkシナリオには適用されません。

ベアReact Nativeの実装

iOSとAndroidのネイティブコードに直接アクセスできるベアReact Nativeプロジェクト用にディープリンクを設定します。

iOSプラットフォームの構成

iOS AppDelegateの更新

AppDelegate ファイルでlaunchOptionsuserActivity オブジェクトを Singular SDK に渡すことで、Singular SDK が起動関連データを処理し、ディープリンクを処理できるようにします。

AppDelegate.swiftAppDelegate.mm
import Singular

// Deferred Deep link and Short Link support
func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
  
  if let singularBridge = NSClassFromString("SingularBridge") {
    let selector = NSSelectorFromString("startSessionWithLaunchOptions:")
    if singularBridge.responds(to: selector) {
      singularBridge.perform(selector, with: launchOptions, afterDelay: 1)
    }
  }
  
  factory.startReactNative(
    withModuleName: "AppName", // Update with your App Name
    in: window,
    launchOptions: launchOptions
  )

  return true
}

// Universal Links (NSUserActivity)
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
  
  if let url = userActivity.webpageURL {
    print("Singular: Universal link received:", url.absoluteString)
    if let singularBridge = NSClassFromString("SingularBridge") {
      let selector = NSSelectorFromString("startSessionWithUserActivity:")
      if singularBridge.responds(to: selector) {
        singularBridge.perform(selector, with: userActivity, afterDelay: 1)
      }
    }
  }
    
  return RCTLinkingManager.application(
      application,
      continue: userActivity,
      restorationHandler: restorationHandler
    )
}

これらのメソッドは、Singular SDKがアプリの起動方法と起動理由に関する重要な情報を受け取れるようにし、Singularがアトリビューショントラッキングとディープリンクナビゲーションに使用します。


Androidプラットフォームの設定

AndroidのMainActivityを更新する

MainActivity ファイルを修正してIntent オブジェクトを Singular SDK に渡すことで、Singular SDK が起動関連データを処理し、ディープリンクを処理できるようにします。

MainActivity.ktMainActivity.java
// Add as part of the imports at the top of the class
import android.content.Intent
import net.singular.react_native.SingularBridgeModule

// Add to the MainActivity class
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    if (intent.data != null) {
        setIntent(intent)
        SingularBridgeModule.onNewIntent(intent)
    }
}

Intent オブジェクトには、アプリの起動方法と起動理由に関する情報が含まれており、Singular はこれをアトリビューショントラッキングとディープリンクナビゲーションに使用します。


SDKの設定

withSingularLinkコールバックを追加する

SDKの初期化中に、入力されるディープリンクと遅延ディープリンクデータを処理するために、withSingularLink コールバックを設定します。

New ArchitectureOld Architecture
// TurboModule direct API (React Native 0.76+ New Architecture)
import React, { useEffect } from 'react';
import NativeSingular from 'singular-react-native/jsNativeSingular';
import { NativeEventEmitter } from 'react-native';

function App() {
  useEffect(() => {
    // Create config
    const config: SingularConfig = {
      apikey: 'YOUR_SDK_KEY',
      secret: 'YOUR_SDK_SECRET'
      // Note: Deep link handlers are NOT set in config for TurboModule
    };

    NativeSingular.init(config);

    // Set up deep link handler using event emitter
    const emitter = new NativeEventEmitter(NativeSingular);
    const linkListener = emitter.addListener('SingularLinkHandler', (params) => {
      console.log('Singular Link resolved:', params);

      // Extract deep link parameters
      console.log('Singular: Deep link received:', params.deeplink);
      console.log('Singular: Passthrough data:', params.passthrough);
      console.log('Singular: Is deferred:', params.isDeferred);
      console.log('Singular: urlParameters:', params.urlParameters);

      // Handle deep link navigation
    });

    // Clean up event listener on unmount
    return () => {
      linkListener.remove();
    };
  }, []);

  return (
    // Your app components
    null
  );
}

注意: withSingularLink コールバックは、アプリがシンギュラーリンクを介して開いた場合にのみトリガーされます。詳細については、Singular Links FAQを参照してください。

メソッドの完全なドキュメントについては、withSingularLinkリファレンスを参照してください。


Expo の実装

設定ファイルとカスタムプラグインを使用して Expo プロジェクトのディープリンクを設定し、ネイティブコードの変更を自動的に処理します。

アプリの構成

ディープリンク用にApp.jsonを更新する

Expo はネイティブ コード管理から抽象化されているため、app.json ファイルにプラットフォーム固有のディープ リンク設定を追加します。

iOS 構成

  • SingularトラッキングリンクのドメインでassociatedDomains機能を追加します。
  • カスタムURLスキームのサポート(フォールバック)のためにスキームを追加します。

Androidの設定

  • ホスト値をあなたのSingular Linksドメインと一致するように更新します(例:example.sng.link )。
  • 従来のスキームリンクをサポートするために、最後のインテント フィルターでスキームを設定する
app.json
{
  "expo": {
    "name": "MySimpleApp",
    "slug": "mysimpleapp",
    "scheme": "ios-app-scheme",
    "ios": {
      "associatedDomains": [
        "applinks:example.sng.link"
      ]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "http",
              "host": "example.sng.link",
              "pathPrefix": "/A"
            },
            {
              "scheme": "https",
              "host": "example.sng.link",
              "pathPrefix": "/A"
            },
            {
              "scheme": "http",
              "host": "example.sng.link",
              "pathPrefix": "/B"
            },
            {
              "scheme": "https",
              "host": "example.sng.link",
              "pathPrefix": "/B"
            },
            {
              "scheme": "http",
              "host": "example.sng.link",
              "pathPrefix": "/E"
            },
            {
              "scheme": "https",
              "host": "example.sng.link",
              "pathPrefix": "/E"
            },
            {
              "scheme": "http",
              "host": "example.sng.link",
              "pathPrefix": "/F"
            },
            {
              "scheme": "https",
              "host": "example.sng.link",
              "pathPrefix": "/F"
            }
          ],
          "category": [
            "BROWSABLE",
            "DEFAULT"
          ]
        },
        {
          "action": "VIEW",
          "data": [
            {
              "scheme": "android-app-scheme"
            }
          ],
          "category": [
            "BROWSABLE",
            "DEFAULT"
          ]
        }
      ]
    }
  }
}

カスタムExpoプラグイン

プラットフォーム固有のプラグインを作成する

これらのプラグインファイルは、Expoの管理されたワークフローでSingular SDKの統合に必要なネイティブコードの修正を自動化し、expo prebuild ごとに手動で編集する手間を省きます。

  • iOSプラグイン:SwiftまたはObjective-CのAppDelegateを検出し、インポート文、デバイス識別子のロギング、アトリビューションデータをSingular React Nativeブリッジに転送するユニバーサルリンク処理コードを注入します。
  • Androidプラグイン:JavaまたはKotlinのMainActivityファイルを検出し、インポート、インテント・ロギング、ディープリンクをキャプチャしてSingularのブリッジモジュールに渡すonNewIntentメソッドを追加する。

どちらのプラグインも、ユーザーがディープリンクやユニバーサルリンクをタップすると、ネイティブコードがそのデータを適切にキャプチャし、アトリビューショントラッキング用のReact Nativeフックに転送するようにします。

セットアップの手順

  1. プロジェクトのルート・ディレクトリに移動する
  2. pluginsフォルダを作成する
  3. 以下のJavaScriptプラグインファイルを追加します。

iOSプラグインの実装

iOSのディープリンクを自動的に設定するために、plugins/singular-ios-deeplink-plugin.js

singular-ios-deeplink-plugin.js
// plugins/singular-ios-deeplink-plugin.js
const { withAppDelegate } = require('@expo/config-plugins');

const withUniversalSingular = (config) => {
  return withAppDelegate(config, (config) => {
    const { modResults } = config;

    // Detect file type
    const isSwift = modResults.contents.includes('import Expo') || 
                    modResults.contents.includes('class AppDelegate');
    const isObjectiveC = modResults.contents.includes('#import') || 
                         modResults.contents.includes('@implementation');

    console.log(`Detected AppDelegate type: ${isSwift ? 'Swift' : isObjectiveC ? 'Objective-C' : 'Unknown'}`);

    if (isSwift) {
      modResults.contents = applySwiftModifications(modResults.contents);
    } else if (isObjectiveC) {
      modResults.contents = applyObjectiveCModifications(modResults.contents);
    } else {
      console.warn('Unable to detect AppDelegate type - skipping Singular modifications');
    }

    return config;
  });
};

function applySwiftModifications(contents) {
  // Add Singular import
  if (!contents.includes('import Singular')) {
    contents = contents.replace(
      /import ReactAppDependencyProvider/,
      `import ReactAppDependencyProvider
import Singular`
    );
  }

  // Add IDFV logging
  const swiftLaunchPattern = /didFinishLaunchingWithOptions.*?\) -> Bool \{/s;
  if (!contents.includes('UIDevice.current.identifierForVendor')) {
    contents = contents.replace(
      swiftLaunchPattern,
      `didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    print("IDFV", UIDevice.current.identifierForVendor!.uuidString)`
    );
  }

  // Add universal link handling
  if (!contents.includes('Universal link received for Singular')) {
    contents = contents.replace(
      /continue userActivity: NSUserActivity,\s*restorationHandler: @escaping \(\[UIUserActivityRestoring\]\?\) -> Void\s*\) -> Bool \{/,
      `continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
  ) -> Bool {
    if let url = userActivity.webpageURL {
      print("Universal link received for Singular:", url.absoluteString)

      if let singularBridge = NSClassFromString("SingularBridge") {
        let selector = NSSelectorFromString("startSessionWithUserActivity:")
        if singularBridge.responds(to: selector) {
          singularBridge.perform(selector, with: userActivity, afterDelay: 1)
        }
      }
    }`
    );
  }

  return contents;
}

function applyObjectiveCModifications(contents) {
  // Add Singular import
  if (!contents.includes('#import <Singular-React-Native/SingularBridge.h>')) {
    contents = contents.replace(
      /#import "AppDelegate.h"/,
      `#import "AppDelegate.h"
#import <Singular-React-Native/SingularBridge.h>`
    );
  }

  // Add IDFV logging to didFinishLaunchingWithOptions
  const objcLaunchPattern = /- \(BOOL\)application:\(UIApplication \*\)application didFinishLaunchingWithOptions:\(NSDictionary \*\)launchOptions\s*{/;
  if (!contents.includes('NSLog(@"IDFV %@"')) {
    contents = contents.replace(
      objcLaunchPattern,
      `- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  NSLog(@"IDFV %@", [UIDevice currentDevice].identifierForVendor.UUIDString);
  [SingularBridge startSessionWithLaunchOptions:launchOptions];`
    );
  }

  // Add continueUserActivity method if not present
  if (!contents.includes('continueUserActivity')) {
    const methodToAdd = `
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity
    restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
    NSLog(@"Universal link received for Singular: %@", userActivity.webpageURL.absoluteString);
    [SingularBridge startSessionWithUserActivity:userActivity];
    return YES;
}`;

    contents = contents.replace(
      /@end$/,
      `${methodToAdd}
@end`
    );
  }

  return contents;
}

module.exports = withUniversalSingular;

Androidプラグインの実装

Android のディープリンクを自動的に設定するため、plugins/singular-android-deeplink-plugin.js を作成します。

singular-android-deeplink-plugin.js
// plugins/singular-android-deeplink-plugin.js
const { withMainActivity } = require('@expo/config-plugins');

const withSingularAndroid = (config) => {
  // Add MainActivity modifications
  config = withMainActivity(config, (config) => {
    const { modResults } = config;

    // Detect Java vs Kotlin
    const isKotlin = modResults.contents.includes('class MainActivity') && 
                     modResults.contents.includes('override fun');
    const isJava = modResults.contents.includes('public class MainActivity') && 
                   modResults.contents.includes('@Override');

    if (isKotlin) {
      modResults.contents = applyKotlinModifications(modResults.contents);
    } else if (isJava) {
      modResults.contents = applyJavaModifications(modResults.contents);
    }

    return config;
  });

  return config;
};

function applyKotlinModifications(contents) {
  // Add imports
  if (!contents.includes('android.content.Intent')) {
    contents = contents.replace(
      /package com\..*?\n/,
      `$&
import android.content.Intent
import net.singular.react_native.SingularBridgeModule`
    );
  }

  // Add onNewIntent method
  if (!contents.includes('onNewIntent')) {
    const methodToAdd = `
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        if (intent.data != null) {
            setIntent(intent)
            SingularBridgeModule.onNewIntent(intent)
        }
    }`;

    contents = contents.replace(
      /}\s*$/,
      `${methodToAdd}
}`
    );
  }

  return contents;
}

function applyJavaModifications(contents) {
  // Add imports
  if (!contents.includes('android.content.Intent')) {
    contents = contents.replace(
      /package com\..*?;/,
      `$&
import android.content.Intent;
import net.singular.react_native.SingularBridgeModule;`
    );
  }

  // Add onNewIntent method
  if (!contents.includes('onNewIntent')) {
    const methodToAdd = `
    @Override
    public void onNewIntent(Intent intent) {
        if(intent.getData() != null) {
            setIntent(intent);
            super.onNewIntent(intent);
            SingularBridgeModule.onNewIntent(intent);
        }
    }`;

    contents = contents.replace(
      /}\s*$/,
      `${methodToAdd}
}`
    );
  }

  return contents;
}

module.exports = withSingularAndroid;

プラグインの登録

app.json ファイルを更新して、Singular React Native プラグインと一緒に新しいカスタムプラグインを含めます。

app.json
{
  "expo": {
    "plugins": [
      "singular-react-native",
      "./plugins/singular-ios-deeplink-plugin.js",
      "./plugins/singular-android-deeplink-plugin.js"
    ]
  }
}

Expo 用 SDK の設定

素の React Native と同じアプローチを使用して、Expo アプリでwithSingularLink コールバックを構成します。完全な実装の詳細については、上記のSDK構成のセクションを参照してください。


ハンドラーの動作

リンク解決を理解する

withSingularLink ハンドラの動作は、アプリがインストールされたばかりか、既にインストールされているかによって異なります。

新規インストール (ディープリンクの遅延)

新規インストールでは、アプリの起動時に Open URL が存在しません。トラッキングリンクにディープリンクまたはディファードディープリンクの値が含まれているかどうかを判断するために、アトリビューションが完了します。

ディファードディープリンクのフロー

  1. ユーザーがディープリンク値で設定されたSingularトラッキングリンクをクリックする。
  2. ユーザーがアプリをインストールして初めて開く
  3. Singular SDKが最初のセッションをSingularサーバーに送信します。
  4. アトリビューションが完了し、トラッキングリンクからディープリンクが特定される
  5. ディープリンクの値が、isDeferred = truedeeplink パラメータ内のwithSingularLink ハンドラに返される。

ディファードディープリンクのテスト

  1. テストデバイスからアプリをアンインストールする(現在インストールされている場合)
  2. iOS:IDFA をリセットします。Android:Google Advertising ID(GAID)をリセットします。
  3. デバイスからSingularトラッキングリンクをクリックする(ディープリンク値が設定されていることを確認する
  4. アプリをインストールして開く

アトリビューションが正常に完了し、延期されたディープリンク値がwithSingularLink ハンドラーに渡されます。

プロのヒント異なるパッケージ名(例えば、com.example.prod の代わりにcom.example.dev)を使用する開発ビルドでディープリンクをテストする場合、開発アプリのパッケージ名専用にトラッキングリンクを設定してください。テストリンクをクリックした後、アプリストアから本番アプリをダウンロードするのではなく、開発ビルドを直接デバイスにインストールします(Android Studio または Xcode 経由)。


インストール済み(即時ディープリンク)

アプリが既にインストールされている場合、ユニバーサルリンク(iOS)またはAndroid App Linksテクノロジーを使用して、シングラーリンクをクリックするとアプリがすぐに開きます。

即時ディープリンクフロー

  1. ユーザーがSingularトラッキングリンクをクリック
  2. オペレーティングシステムは、Singularトラッキングリンク全体を含むOpen URLを提供します。
  3. SDKの初期化中にSingularがURLを解析します。
  4. Singularはdeeplinkpassthroughの値を抽出します。
  5. 値はisDeferred = falseとともにwithSingularLink ハンドラーを通して返されます。

高度な機能

パススルーパラメータ

パススルーパラメータを使用して、トラッキングリンククリックから追加データを取得します。

passthrough (_p) パラメーターがトラッキングリンクに含まれている場合、withSingularLink ハンドラーのpassthroughパラメーターに対応するデータが含まれます。キャンペーンメタデータ、ユーザーセグメンテーションデータ、またはアプリで必要なカスタム情報の取得に使用します。

パススルー値の例

{
  "utm_source": "Facebook",
  "utm_campaign": "Web-to-App US RON",
  "utm_creative": "Blue CTA for Promo",
  "promo_code": "45k435hg"
}

URL エンコードされたパススルー値を (_p) パラメータに持つトラッキングリンクの例

https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_p=%7B%22utm_source%22%3A%20%22Facebook%22%2C%22utm_campaign%22%3A%20%22Web-to-App%20US%20RON%22%2C%22utm_creative%22%3A%20%22Blue%20CTA%20for%20Promo%22%2C%22promo_code%22%3A%20%2245k435hg%22%7D

withSingularLink ハンドラーが受け取ります:

urlParameters = {"utm_source": "Facebook","utm_campaign": "Web-to-App US RON","utm_creative": "Blue CTA for Promo","promo_code": "45k435hg"}

すべてのクエリパラメータを転送

_forward_params=2 パラメータをトラッキングリンクに追加することで、トラッキングリンクの URL からすべてのクエリパラメータを取得します。

_forward_params=2 がトラッキングリンクに追加されると、すべてのクエリパラメータがwithSingularLink ハンドラのdeeplink パラメータに含まれ、すべてのパラメータを含む完全な URL にアクセスできるようになります。

トラッキングリンクの例

https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_forward_params=2&utm_source=facebook&promo=SALE2024

withSingularLink ハンドラーが受信します:

deeplink = "myapp://product/123?utm_source=facebook&promo=SALE2024"