React Native SDK - 支持深度链接

文件

添加深度链接支持

深度链接可引导用户访问应用程序中的特定内容。当用户在已安装应用的设备上点击深层链接时,应用会直接打开到目标内容,如产品页面或特定体验。

奇异跟踪链接支持标准深度链接(适用于已安装的应用)和延迟深度链接(适用于新安装的应用)。有关详细信息,请参阅深度链接常见问题解答奇异链接常见问题解答

实现路径:本指南涵盖裸 React NativeExpo实现。之所以存在这种区别,是因为 Expo 抽象了原生代码管理以简化 React Native 开发,而裸 React Native 则提供了对原生 iOS 和 Android 文件夹的直接访问。请根据您的项目结构选择合适的实现路径。


要求

先决条件

完成Singular Links 先决条件,为您的应用程序启用深度链接。

注释:

  • 本文假设您的组织正在使用 SingularLinks- Singular 的跟踪链接技术。老客户使用的可能是传统的跟踪链接。
  • 您应用程序的深度链接目的地需要在 Singular 的应用程序页面上进行配置(请参阅配置您的应用程序进行归因跟踪)。

实施 Singular 链接处理程序

SingularLink 处理程序提供一种回调机制,用于在应用程序打开时从 Singular 跟踪链接检索深度链接、延迟深度链接、直通参数和 URL 参数。

可用参数:

  • 深度链接 (_dl):点击链接的用户在应用程序中的目标 URL
  • 延迟深度链接 (_ddl):点击链接后安装应用程序的用户的目标 URL
  • 直通 (_p):通过跟踪链接传递的自定义数据,以获取更多上下文信息
  • urlParameters:来自打开应用程序的奇异链接的查询参数对象。不适用于延迟深度链接方案。

裸 React 原生实现

为直接访问原生 iOS 和 Android 代码的裸 React Native 项目配置深度链接。

iOS 平台配置

更新 iOS AppDelegate

通过在AppDelegate 文件中向 Singular SDK 传递launchOptionsuserActivity 对象,使 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 会将这些信息用于归因跟踪和深层链接导航。


安卓平台配置

更新安卓主活动

修改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 回调

配置withSingularLink 回调,以便在 SDK 初始化过程中处理传入的深度链接和延迟的深度链接数据。

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 回调。有关详细信息,请参阅奇异链接常见问题解答

有关完整的方法文档,请参阅withSingularLink 参考资料


Expo 实现

使用配置文件和自定义插件为 Expo 项目配置深度链接,以自动处理本地代码修改。

应用程序配置

更新 App.json 以实现深度链接

由于 Expo 从本地代码管理中抽象出来,请在app.json 文件中添加特定平台的深度链接配置。

iOS 配置

  • 在 Singular 跟踪链接域中添加associatedDomains功能
  • 添加支持自定义 URL方案的方案(回退)

安卓配置

  • 更新主机值以匹配你的 Singular 链接域(例如,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"
          ]
        }
      ]
    }
  }
}

自定义世博会插件

创建特定平台插件

这些插件文件可在世博会管理工作流程中自动修改 Singular SDK 集成所需的本地代码,无需在每次expo prebuild 后进行手动编辑。

  • iOS 插件:检测 Swift 或 Objective-C AppDelegate,并注入导入语句、设备标识符日志和通用链接处理代码,将属性数据转发至 Singular React Native 桥接器。
  • 安卓插件:检测 Java 或 Kotlin MainActivity 文件,并添加导入、意图记录和onNewIntent方法,以捕获深度链接并将其传递给 Singular 的桥接模块

这两个插件都能确保当用户点击深层链接或通用链接时,原生代码能正确捕获并将数据转发给您的 React Native 挂钩,以便进行归因跟踪。

设置步骤

  1. 进入项目根目录
  2. 创建插件文件夹
  3. 添加以下 JavaScript 插件文件

iOS 插件实现

创建plugins/singular-ios-deeplink-plugin.js ,自动配置 iOS 深度链接。

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;

安卓插件实现

创建plugins/singular-android-deeplink-plugin.js 以自动配置 Android 深度链接。

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"
    ]
  }
}

世博会 SDK 配置

使用与裸 React Native 相同的方法配置 Expo 应用程序中的withSingularLink 回调。有关完整的实现细节,请参阅上面的SDK 配置部分。


处理程序行为

了解链接解析

withSingularLink 处理程序会根据应用程序是刚安装还是已安装而有不同的行为。

全新安装(延迟深度链接)

刚安装时,应用程序启动时不存在打开 URL。Singular 会完成归因,以确定跟踪链接是否包含深度链接或延迟深度链接值。

延迟深度链接流程

  1. 用户点击配置了深度链接值的 Singular 跟踪链接
  2. 用户安装并首次打开应用程序
  3. Singular SDK 向 Singular 服务器发送第一个会话
  4. 归因完成并从跟踪链接中识别深度链接
  5. 深度链接值返回到withSingularLink 处理程序的deeplink 参数中的isDeferred = true

测试延迟深度链接

  1. 从测试设备上卸载应用程序(如果当前已安装)
  2. iOS:重置您的 IDFA。Android:重置您的谷歌广告 ID (GAID)
  3. 点击设备上的 Singular 跟踪链接(确保配置了深度链接值
  4. 安装并打开应用程序

归因应成功完成,延迟的深度链接值将传递到withSingularLink 处理程序。

专业提示在使用不同软件包名称(例如com.example.dev而不是com.example.prod )的开发构建中测试深度链接时,请专门针对开发应用的软件包名称配置跟踪链接。点击测试链接后,直接在设备上安装开发构建(通过 Android Studio 或 Xcode),而不是从应用商店下载生产应用。


已安装(即时深度链接)

当应用程序已安装时,点击奇异链接可使用通用链接(iOS)或安卓应用程序链接技术立即打开应用程序。

即时深度链接流程

  1. 用户点击奇异跟踪链接
  2. 操作系统提供包含整个 Singular 跟踪链接的开放 URL
  3. 在 SDK 初始化过程中,Singular 对 URL 进行解析
  4. Singular 提取deeplinkpassthrough
  5. 通过withSingularLink 处理程序返回isDeferred = false的值

高级功能

直通参数

使用直通参数从跟踪链接点击中获取其他数据。

如果跟踪链接中包含passthrough (_p) 参数,withSingularLink 处理程序的passthrough参数就会包含相应数据。使用该参数可捕获营销活动元数据、用户细分数据或应用程序中需要的任何自定义信息。

传递值示例

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

在 (_p) 参数中带有 URL 编码直通值的跟踪链接示例

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"