[BETA] SKAdNetwork 4.0 的S2S 集成指南

SKAdNetwork 是苹果公司提供的一个框架,用于对 iOS 上的应用程序安装广告活动进行隐私友好的测量。SKAdNetwork 4.0 是该框架的最新版本,在几个重要细节上与 SKAdNetwork 3.0 有所不同。

使用本指南实施 SKAdNetwork 4.0,将其作为 Singular S2S 集成的一部分。

SKAdNetwork 4.0 S2S 实施

Singular 的 SKAdNetwork 解决方案由以下部分组成:

  • 实施 SKAdNetwork 的客户端代码
  • 来自所有网络的回传验证和聚合
  • 欺诈保护
    • 签名验证和交易 ID 删除。
    • 与网络进行安全设置,以验证未签名的参数(转换值和地理数据)。
  • 转换值管理: 可在 Singular 的仪表板上动态配置哪些安装后活动应编码到 SKAdNetwork 转换值中。
  • 报告:翻译有限的 SKAdNetwork 营销活动 ID,并使用更多营销参数和粒度来丰富数据。
  • 合作伙伴回传: 它为发送 SKAdNetwork 的回帖提供了将转换值解码为事件和收入的功能,这对其优化至关重要。

对于使用服务器到服务器(S2S)Singular 集成的客户,SKAdNetwork 的实施包括两个主要部分:

  1. SKAdNetwork 客户端实施: 这部分对于向 SKAdNetwork 注册应用程序和智能管理 SKAdNetwork 转换值至关重要。这意味着,通过实施该部分,您将能够根据 SKAdNetwork 的属性及其相关的安装后活动优化您的营销活动。
  2. S2S 集成更新:这一部分对于验证客户端实施并排除故障非常重要。通过使用 SKAdNetwork 数据(转换值和更新时间戳)丰富今天通过 Singular 的 S2S 会话和事件端点发送到 Singular 的事件和会话,Singular 可以验证应用程序端的实施是否正确。

SKAdNetwork 客户端实施

Singular 提供了支持注册 SKAdNetwork 和管理转换值的代码示例。这些代码示例负责以下部分:

  1. SKAdNetwork 支持和注册
  2. 转换值管理:
    • 代码与 Singular 的端点同步通信,以根据配置的转换模型接收下一个转换值。 它报告事件/会话/收入,并作为响应,获取下一个转换值,这是一个编码数字,代表在 Singular 的仪表板中配置用于测量的安装后活动。
    • 代码还按测量时间段收集 SKAdnetwork 元数据。 元数据用于验证和计算下一个转换值:
      • 对底层 SKAdNetwork 框架的首次调用时间戳
      • 底层 SKAdNetwork 框架的最后一次调用时间戳
      • 最后更新的回传值(粗略和精细回传值)
      • 用户产生的总收入和广告收入

S2S 集成更新

必须更新

一旦您拥有一个活动的转换模型,我们的 S2S 端点将开始返回一个名为 "conversion_value "的新 int 字段,其中将包含客户端代码中需要更新的下一个值。

建议

为了验证集成并支持更多见解,我们建议使用我们的代码示例来扩展您当前的 S2S 集成。Singular 的 S2S 事件和会话端点已经支持获取额外的 SKAdNetwork 参数,例如

  • P0、P1、P2 的精细和粗略转换值
  • P0、P1、P2 先前的精细和粗略转换值
  • 调用底层 SKAdNetwork 框架的最后时间戳
  • 调用 SKAdNetwork 框架的第一个时间戳
  • 每个窗口测量期的总收入(P0、P1、P2)

集成流程

Screen_Shot_2020-09-16_at_18.59.13.png

上图说明了 S2S 客户的 SKAdNetwork 流程:

  1. 首先,应用程序中的代码与 Singular 服务器(通过 SKAdNetwork 专用端点)通信,根据应用程序中发生的事件/会话/收入事件同步获取最新转换值,并用此值更新 SKAdNetwork 框架。
  2. 其次,应用程序用 SKAdNetwork 数据丰富现有事件和会话,这些数据随后将用于验证。
  3. 一旦应用程序完成用新的转换值更新 SKAdNetwork 框架,且 SKAdNetwork 定时器到期,SKAdNetwork 回传将被发送到网络。
  4. 网络将把它转发给 Singular(通过安全设置或常规设置)。
  5. Singular 将通过以下方式处理回传
    • 验证其签名
    • 根据配置的转换模型解码转换值
    • 用网络信息丰富回传内容。通过加入 SKAdNetwork 和网络活动 ID,从与合作伙伴的集成中收集数据。
    • 将解码后的回帖发送给 BI 和合作伙伴

需要注意的是,SKAdNetwork 信息(包括安装和解码事件)将通过一组不同的报告/API/ETL 表和回传进行访问,以避免与现有数据集混合。在接下来的几周内,这一点尤为重要,因为您可以同时测量和测试 SKAdNetwork 与现有营销活动的效果。

SKAdNetwork 本地 iOS 代码示例

SKAdNetwork 界面

该界面包括以下 SKAdNetwork 组件:

注册 SKAdNetwork

  • 该方法负责首次调用 SKAdNetwork。
  • 它将 P0 的转换值设置为 0
  • 如果设备拥有该应用的归因数据,底层 Apple API 方法会生成通知。

转换值管理

  • 转换值的计算基于以下方法捕获的设备安装后活动和用户可动态配置的选定转换模型。
  • 本节中的方法负责根据选定的转换模型和报告的安装后活动从 Singular 端点提取下一个转换值(请参阅上述文档)。
  • 以下方法根据以下安装后活动更新转换值:
    • 会话:对使用 SKAdNetwork 的用户的保留测量至关重要
    • 转换事件:对于使用 SKAdNetwork 测量安装后转换事件至关重要
    • 收入事件:对于使用 SKAdNetwork 进行收入测量至关重要
//SKANSnippet.h
  
  #import <Foundation/Foundation.h>
  
  @interface SKANSnippet : NSObject
  
  // 注册 SkAdNetwork 归因。
  // 首次启动应用程序后,应尽快调用此方法。
  // 此函数将转换值设置为 0,并更新时间戳,以便进行额外处理。
  + (void)registerAppForAdNetworkAttribution;
  
  // 要跟踪保留率和队列,您需要为每个打开的应用程序调用此方法。
  // 如果需要,它会报告会话详情并更新该会话的转换值。
  // 传递给该方法的回调是可选的,您可以在转换值更新后使用它来运行代码。
  //
  + (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
  
  // 要使用 SKAdNetwork 跟踪转换事件,您需要在每次事件发生后、事件发送到 Singular 之前调用此方法。
  // 如果需要,它会报告事件详情并更新该事件导致的转换值。
  // 传递给该方法的回调是可选的,您可以在转换值更新后使用它来运行代码。
  + (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
  
  // 要使用 SKAdNetwork 跟踪收入,您需要在每次收入事件之前调用此方法。
  // 它将更新总收入,因此当您调用 "updateConversionValuesAsync "时,新的转换值将根据总收入确定。
  // 请注意:
  // 1. 在调用 "updateConversionValuesAsync "之前调用此方法,以确保收入得到更新。
  // 2. 如果要重试一个事件,请避免调用此方法,以免同一收入被计算两次。
  + (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon;
  
  // 获取当前的精细值、粗略值和窗口锁定值,保存在字典中的 "fineValue"、"c coarseValue "和 "windowLock "下。
  // 此外,还包含用于 SKAN 目的的所有其他相关值。
  // 例如 
  // {
  //    "skan_current_conversion_value": 3,
  //    "prev_fine_value": 2,
  //    "skan_first_call_to_skadnetwork_timestamp": 167890942,
  //    "skan_last_call_to_skadnetwork_timestamp": 167831134,
  //    "skan_total_revenue_by_currency": { "USD": 1.2 },
  //    "skan_total_admon_revenue_by_currency": { "USD": 0.8 },
  //    "p0_coarse": 0,
  //    "p1_coarse": 1,
  //    "p2_coarse": nil,
  //    "p0_window_lock": 167890942,
  //    "p1_window_lock": nil,
  //    "p2_window_lock": nil,
  //    "p0_prev_coarse_value": 0,
  //    "p1_prev_coarse_value": 0,
  //    "p2_prev_coarse_value": nil,
  //    "p0_total_iap_revenue": nil,
  //    "p1_total_iap_revenue": nil,
  //    "p2_total_iap_revenue": nil,
  //    "p0_total_admon_revenue": nil,
  //    "p1_total_admon_revenue": nil,
  //    "p2_total_admon_revenue": nil
  // }
  + (NSDictionary *)getSkanDetails;
  
   @end
  

SKAdNetwork 接口实施

//  SKANSnippet.m
  
  #import "SKANSnippet.h"
  #import <StoreKit/SKAdNetwork.h>
  #import <UIKit/UIKit.h>
  
  #define SESSION_EVENT_NAME @"__SESSION__"
  #define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v2/conversion_value"
  
  // 用于 NSUserDefaults 持久性和请求的 SKAN 密钥
  #define CONVERSION_VALUE_KEY @"skan_current_conversion_value"
  #define FIRST_SKAN_CALL_TIMESTAMP @"skan_first_call_to_skadnetwork_timestamp"
  #define LAST_SKAN_CALL_TIMESTAMP @"skan_last_call_to_skadnetwork_timestamp"
  #define TOTAL_REVENUE_BY_CURRENCY @"skan_total_revenue_by_currency"
  #define TOTAL_ADMON_REVENUE_BY_CURRNECY @"skan_total_admon_revenue_by_currency"
  #define SKAN_UPDATED_CONVERSION_VALUE @"conversion_value"
  #define SKAN_UPDATED_COARSE_VALUE @"skan_updated_coarse_value"
  #define SKAN_UPDATED_LOCK_WINDOW_VALUE @"skan_updated_lock_window_value"
  
  #define P0_COARSE @"p0_coarse"
  #define P1_COARSE @"p1_coarse"
  #define P2_COARSE @"p2_coarse"
  #define P0_WINDOW_LOCK_TS @"p0_window_lock"
  #define P1_WINDOW_LOCK_TS @"p1_window_lock"
  #define P2_WINDOW_LOCK_TS @"p2_window_lock"
  
  #define P0_PREV_FINE_VALUE @"prev_fine_value"
  #define P0_PREV_COARSE_VALUE @"p0_prev_coarse_value"
  #define P1_PREV_COARSE_VALUE @"p1_prev_coarse_value"
  #define P2_PREV_COARSE_VALUE @"p2_prev_coarse_value"
  
  #define TOTAL_REVENUE_P0 @"p0_total_iap_revenue"
  #define TOTAL_REVENUE_P1 @"p1_total_iap_revenue"
  #define TOTAL_REVENUE_P2 @"p2_total_iap_revenue"
  #define TOTAL_ADMON_REVENUE_P0 @"p0_total_admon_revenue"
  #define TOTAL_ADMON_REVENUE_P1 @"p1_total_admon_revenue"
  #define TOTAL_ADMON_REVENUE_P2 @"p2_total_admon_revenue"
  
  @implementation SKANSnippet
  
  static NSInteger firstSkan4WindowInSec = 3600 * 24 * 2; //48 hours in sec
  static NSInteger secondSkan4WindowInSec = 3600 * 24 * 7;
  static NSInteger thirdSkan4WindowInSec = 3600 * 24 * 35;
  
  static NSLock *lockObject;
  
  + (void)registerAppForAdNetworkAttribution {
      if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
          return;
      }
      
      if (@available(iOS 16.1, *)) {
          [SKAdNetwork updatePostbackConversionValue:0 completionHandler:nil];
          [SKANSnippet setFirstSkanCallTimestamp];
          [SKANSnippet setLastSkanCallTimestamp];
          [SKANSnippet valuesHasBeenUpdated:@(0) coarseValue:nil lockWindow:NO];
      } 
  }
  
  + (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
      [SKANSnippet updateConversionValuesAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
  }
  
  + (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
          if ([SKANSnippet isSkanWindowOver]) {
              return;
          }
          
          [SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
  }
  
  + (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon {
      // Update total revenues
      if (amount == 0 || !currency ) {
          return;
      }
      
      [SKANSnippet addToTotalRevenue:@(amount) withCurrency:currency isAdmon:admon];
  }
  
  + (NSDictionary *)getSkanDetails {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      NSMutableDictionary *res = [NSMutableDictionary dictionary];
      //current fine
      [res setValue:[[userDefaults valueForKey:CONVERSION_VALUE_KEY] stringValue] forKey:CONVERSION_VALUE_KEY];
      //prev fine
      [res setValue:[[userDefaults valueForKey:P0_PREV_FINE_VALUE] stringValue] forKey:P0_PREV_FINE_VALUE];
      //current coarse
      [res setValue:[[userDefaults valueForKey:P0_COARSE] stringValue] forKey:P0_COARSE];
      [res setValue:[[userDefaults valueForKey:P1_COARSE] stringValue] forKey:P1_COARSE];
      [res setValue:[[userDefaults valueForKey:P2_COARSE] stringValue] forKey:P2_COARSE];
      //prev coarse
      [res setValue:[[userDefaults valueForKey:P0_PREV_COARSE_VALUE] stringValue] forKey:P0_PREV_COARSE_VALUE];
      [res setValue:[[userDefaults valueForKey:P1_PREV_COARSE_VALUE] stringValue] forKey:P1_PREV_COARSE_VALUE];
      [res setValue:[[userDefaults valueForKey:P2_PREV_COARSE_VALUE] stringValue] forKey:P2_PREV_COARSE_VALUE];
      //lock windows ts
      [res setValue:[[userDefaults valueForKey:P0_WINDOW_LOCK_TS] stringValue] forKey:P0_WINDOW_LOCK_TS];
      [res setValue:[[userDefaults valueForKey:P1_WINDOW_LOCK_TS] stringValue] forKey:P1_WINDOW_LOCK_TS];
      [res setValue:[[userDefaults valueForKey:P2_WINDOW_LOCK_TS] stringValue] forKey:P2_WINDOW_LOCK_TS];
      //total revenues
      [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_BY_CURRENCY] forKey:TOTAL_REVENUE_BY_CURRENCY];
      [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_BY_CURRNECY] forKey:TOTAL_ADMON_REVENUE_BY_CURRNECY];
      //revenue per window
      [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P0] forKey:TOTAL_REVENUE_P0];
      [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P1] forKey:TOTAL_REVENUE_P1];
      [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P2] forKey:TOTAL_REVENUE_P2];
      [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P0] forKey:TOTAL_ADMON_REVENUE_P0];
      [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P1] forKey:TOTAL_ADMON_REVENUE_P1];
      [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P2] forKey:TOTAL_ADMON_REVENUE_P2];
      //skan TS
      [res setValue:[[userDefaults valueForKey:LAST_SKAN_CALL_TIMESTAMP] stringValue] forKey:LAST_SKAN_CALL_TIMESTAMP];
      [res setValue:[[userDefaults valueForKey:FIRST_SKAN_CALL_TIMESTAMP] stringValue] forKey:FIRST_SKAN_CALL_TIMESTAMP];
      
      return res;
  }
  
  
  #pragma mark - internal
  + (BOOL)validateValues:(NSNumber *)conversionValue coarse:(NSNumber *)coarseValue{
      if ([conversionValue intValue] < 0 || 63 < [conversionValue intValue]) {
          return NO;
      }
      
      if (coarseValue) {
          if ([coarseValue intValue] > 2 && [coarseValue intValue] < 0) {
              return NO;
          }
      }
      
      return YES;
  }
  
  + (NSURLComponents *)prepareQueryParams:(NSString *)bundleIdentifier eventName:(NSString *)eventName {
      NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
      
      NSString *API_KEY = @"YOUR API KEY";
      NSString *APP_VERSION = @"YOUR APP VERSION";
      NSString *IDFV = @"IDFV";
      NSString *IDFA = @"IDFA";
      
      NSMutableArray *queryItems = [@[
          [NSURLQueryItem queryItemWithName:@"a" value:API_KEY],
          [NSURLQueryItem queryItemWithName:@"v" value:[[UIDevice currentDevice] systemVersion]],
          [NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
          [NSURLQueryItem queryItemWithName:@"app_v" value:APP_VERSION],
          [NSURLQueryItem queryItemWithName:@"n" value:eventName],
          [NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
          [NSURLQueryItem queryItemWithName:@"idfv" value:IDFV],
          [NSURLQueryItem queryItemWithName:@"idfa" value:IDFA]
      ] mutableCopy];
      
      NSDictionary *skanValues = [SKANSnippet getSkanDetails];
      [skanValues enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
          if ([obj isKindOfClass:[NSDictionary class]]) {
              [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:[SKANSnippet dictionaryToJsonString:obj]]];
          } else {
              [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:obj]];
          }
      }];
      
      components.queryItems = queryItems;
      
      return components;
  }
  
  + (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
      if (!lockObject) {
          lockObject = [NSLock new];
      }
      
      @try {
          // 使锁成为异步,这样就不会冻结调用线程
          dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
              
              [lockObject lock];
              NSString *bundleIdentifier = @"YOUR BUNDLE IDENTIFIER";
              NSURLComponents *components = [SKANSnippet prepareQueryParams:bundleIdentifier eventName:eventName];
              
              [[[NSURLSession sharedSession] dataTaskWithURL:components.URL
                                           completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                  if (error) {
                      [lockObject unlock];
                      if (handler) {
                          handler(nil, nil, NO, error);
                      }
                      
                      return;
                  }
                  
                  NSDictionary *parsedResponse = [SKANSnippet jsonDataToDictionary:data];
                  
                  if (!parsedResponse) {
                      [lockObject unlock];
                      if (handler) {
                          handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                   code:0
                                                               userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
                      }
                      
                      return;
                  }
                  
                  NSNumber *conversionValue = [parsedResponse objectForKey:SKAN_UPDATED_CONVERSION_VALUE];
                  NSNumber *coarseValue = [parsedResponse objectForKey:SKAN_UPDATED_COARSE_VALUE];
                  BOOL lockWindow = [[parsedResponse objectForKey:SKAN_UPDATED_LOCK_WINDOW_VALUE] boolValue];
                  
                  
                  if (!conversionValue) {
                      [lockObject unlock];
                      NSString *status = [parsedResponse objectForKey:@"status"];
                      
                      if (!status || ![status isEqualToString:@"ok"]) {
                          if (handler) {
                              NSString *reason = [parsedResponse objectForKey:@"reason"];
                              if (!reason) {
                                  reason = @"Got error from server";
                              }
                              
                              handler(nil, nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                        code:0
                                                                    userInfo:@{NSLocalizedDescriptionKey:reason}]);
                          }
                      }
                      
                      return;
                  }
                  
                  if(![SKANSnippet validateValues:conversionValue coarse:coarseValue]) {
                      if (handler) {
                          handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                   code:0
                                                               userInfo:@{NSLocalizedDescriptionKey:@"Illegal values recieved"}]);
                      }
                      
                      return;
                  }
                  
                  if (![SKANSnippet getFirstSkanCallTimestamp]) {
                      [SKANSnippet setFirstSkanCallTimestamp];
                  }
                  
                  [SKANSnippet setConversionValues:conversionValue coarseValue:coarseValue lockWindow:lockWindow handler:handler];
                  
                  [lockObject unlock];
              }] resume];
          });
      } @catch (id exception) {
          NSLog(@"%@", exception);
      }
  }
  
  + (void)setFirstSkanCallTimestamp {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:FIRST_SKAN_CALL_TIMESTAMP];
      [userDefaults synchronize];
  }
  
  + (void)setLastSkanCallTimestamp {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:LAST_SKAN_CALL_TIMESTAMP];
      [userDefaults synchronize];
  }
  
  + (NSString*)dictionaryToJsonString:(NSDictionary*)dictionary {
      if (!dictionary || [dictionary count] == 0){
          return @"{}";
      }
      
      NSError *error;
      NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
                                                         options:0
                                                           error:&error];
      
      if (error || !jsonData) {
          return @"{}";
      }
      
      return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
  }
  
  + (NSInteger)getFirstSkanCallTimestamp {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      return [userDefaults integerForKey:FIRST_SKAN_CALL_TIMESTAMP];
  }
  
  + (NSInteger)getLastSkanCallTimestamp {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      return [userDefaults integerForKey:LAST_SKAN_CALL_TIMESTAMP];
  }
  
  + (NSInteger)getCurrentUnixTimestamp {
      return [[NSDate date] timeIntervalSince1970];
  }
  
  + (void)setConversionValues:(NSNumber *)conversionValue coarseValue:(NSNumber *)coarse lockWindow:(BOOL)lockWindow handler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
      @try {
          __block void(^skanResultHandler)(NSError * _Nullable error) = ^(NSError * _Nullable error) {
              if (handler) {
                  if (error) {
                      handler(nil, nil, NO, error);
                  } else {
                      handler(conversionValue, coarse, lockWindow, nil);
                  }
              }
              
              [SKANSnippet valuesHasBeenUpdated:conversionValue coarseValue:coarse lockWindow:lockWindow];
          };
          
          if (@available(iOS 16.1, *)) {
              [SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] coarseValue:[SKANSnippet resolveCoarseValueFrom:coarse] lockWindow:lockWindow completionHandler:^(NSError * _Nullable error) {
                  skanResultHandler(error);
              }];
          } else {
              if (@available(iOS 15.4, *)) {
                  [SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] completionHandler:^(NSError * _Nullable error) {
                      skanResultHandler(error);
                  }];
              }
          }
      } @catch (id exception) {
          NSLog(@"%@", exception);
      }
  }
  
  + (NSNumber *)getConversionValue {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      
      if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
          return @(0);
      }
      
      return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
  }
  
  + (NSDictionary*)jsonDataToDictionary:(NSData*)jsonData {
      if (!jsonData) {
          return nil;
      }
      
      NSError *error;
      NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData
                                                                  options:kNilOptions error:&error];
      
      if (error || !parsedData) {
          return nil;
      }
      
      return parsedData;
  }
  
  + (NSInteger)getCurrentSkanWindow {
      NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
      if (timeDiff < firstSkan4WindowInSec) { return 0; }
      if (timeDiff < secondSkan4WindowInSec) { return 1; }
      if (timeDiff < thirdSkan4WindowInSec) { return 2; }
      
      return -1;
  }
  
  // 根据活动 skan 窗口保留更新的转化值。
  + (void)valuesHasBeenUpdated:(NSNumber *)fineValue coarseValue:(NSNumber *)coarseValue lockWindow:(BOOL)lockWindow {
      NSNumber *currentPersistedFineValue;
      NSNumber *currentPersistedCoarseValue;
      NSInteger window = [SKANSnippet getCurrentSkanWindow];
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
          
      switch (window) {
      case 0:
          currentPersistedFineValue = [userDefaults objectForKey:CONVERSION_VALUE_KEY];
          currentPersistedCoarseValue = [userDefaults objectForKey:P0_COARSE];
          [userDefaults setValue:fineValue forKey:CONVERSION_VALUE_KEY];
          [userDefaults setValue:currentPersistedFineValue forKey:P0_PREV_FINE_VALUE];
          [userDefaults setValue:coarseValue forKey:P0_COARSE];
          [userDefaults setValue:currentPersistedCoarseValue forKey:P0_PREV_COARSE_VALUE];
              
          if (lockWindow) {
              [userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P0_WINDOW_LOCK_TS];
          }
              
          break;
      
      case 1:
          currentPersistedCoarseValue = [userDefaults objectForKey:P1_COARSE];
          [userDefaults setValue:coarseValue forKey:P1_COARSE];
          [userDefaults setValue:currentPersistedCoarseValue forKey:P1_PREV_COARSE_VALUE];
              
          if (lockWindow) {
              [userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P1_WINDOW_LOCK_TS];
          }
              
          break;
      
      case 2:
          currentPersistedCoarseValue = [userDefaults objectForKey:P2_COARSE];
          [userDefaults setValue:coarseValue forKey:P2_COARSE];
          [userDefaults setValue:currentPersistedCoarseValue forKey:P2_PREV_COARSE_VALUE];
              
          if (lockWindow) {
              [userDefaults setValue:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P2_WINDOW_LOCK_TS];
          }
              
          break;
      }
      
      [SKANSnippet setLastSkanCallTimestamp];
  }
  
  + (BOOL)isSkanWindowOver {
      NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
      return thirdSkan4WindowInSec <= timeDiff;
  }
  
  // 收入通过广告盈利和非广告盈利事件、总金额和按 skan 窗口细分来累积和保存。
  + (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency isAdmon:(BOOL)isAdmon  {
      NSString *key = isAdmon ? TOTAL_ADMON_REVENUE_BY_CURRNECY : TOTAL_REVENUE_BY_CURRENCY;
      [SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
          
      NSInteger window = [SKANSnippet getCurrentSkanWindow];
      switch (window) {
          case 0:
              key = isAdmon ? TOTAL_ADMON_REVENUE_P0 : TOTAL_REVENUE_P0 ;
              break;
          case 1:
              key = isAdmon ? TOTAL_ADMON_REVENUE_P1 : TOTAL_REVENUE_P1 ;
              break;
              
          case 2:
              key = isAdmon ? TOTAL_ADMON_REVENUE_P2 : TOTAL_REVENUE_P2 ;
              break;
          case -1:
              key = nil;
              return;
      }
      
      [SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
  }
  
  // 粗略值在请求和响应上作为 Int 发送,并在 API 执行时转换为系统定义的粗略值。
  + (NSString *)resolveCoarseValueFrom:(NSNumber *)value {
      if(@available(iOS 16.1, *)) {
          if (!value) {
              return nil;
          }
          
          switch ([value integerValue]) {
              case 0:
                  return SKAdNetworkCoarseConversionValueLow;
              case 1:
                  return SKAdNetworkCoarseConversionValueMedium;
              case 2:
                  return SKAdNetworkCoarseConversionValueHigh;
              default:
                  return nil;
          }
      }
      
      return nil;
  }
  
  + (NSDictionary*)getTotalRevenue:(NSString *)revenueKey {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      
      if (![userDefaults objectForKey:revenueKey]){
          [userDefaults setObject:[NSDictionary dictionary] forKey:revenueKey];
      }
      
      return [userDefaults objectForKey:revenueKey];
  }
  
  + (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency forKey:(NSString *)revenueKey {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      NSMutableDictionary *revenues = [[SKANSnippet getTotalRevenue:revenueKey] mutableCopy];
      NSNumber *currentRevenue = 0;
  
      if ([revenues objectForKey:currency]) {
          currentRevenue = [revenues objectForKey:currency];
      }
  
      currentRevenue = @([currentRevenue floatValue] + [newRevenue floatValue]);
      [revenues setObject:currentRevenue forKey:currency];
      [userDefaults setObject:revenues forKey:revenueKey];
      [userDefaults synchronize];
  }
  
  @end
  

更新 S2S 集成(推荐)

使用以下 SKAdNetwork 元数据更新您的 S2S 集成(该元数据应在报告给 Singular 的每个会话和事件中转发,以便进行 SKAdNetwork 实施验证):

  • SKAN_current_conversion_value:最新的精细转换值
  • prev_fine_value:上一次精细转换值
  • skan_first_call_to_skadnetwork_timestamp:首次调用底层 SkAdNetwork API 的 Unix 时间戳
  • skan_last_call_too_skadnetwork_timestamp:最近一次调用底层 SkAdNetwork API 的 Unix 时间戳
  • p0/1/2_coarse:每个窗口的最新粗略值
  • p0/1/2_prev_coarse_value : 每个窗口之前的粗略值
  • p0/1/2_window_lock:每个窗口最后一次使用窗口锁更新的 Unix 时间戳
  • p0/1/2_total_iap_revenue:每个窗口的非广告货币化收入总计
  • p0/1/2_total_admon_revenue:每个窗口的广告货币化收入总额
  • skan_total_revenue_by_currency:非广告货币化收入总额
  • skan_total_admon_revenue_by_currency:广告货币化收入总额

以下函数展示了如何提取这些值:

NSDictionary *values = [SKANSnippet getSkanDetails];

现在,一旦您将这些参数发送到服务器端,就可以通过我们的服务器到服务器 API 端点转发这些参数。要了解更多信息,请在我们的服务器到服务器 API 参考资料中搜索这些参数。

应用程序生命周期流程示例

// 仅在应用程序首次启动时
  [SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //前往 Singular 的 LAUNCH 端点 // 处理完每个会话后 [SKANSnippet updateConversionValueAsync:handler]; NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //前往 Singular 的 LAUNCH 端点
// 处理无收入事件后 [SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:handler]; NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"] //转到 Singular 的 EVENT 端点
// 处理完收入事件后 [SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"]; [SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"] //转到 Singular 的 EVENT 端点