A SKAdNetwork é uma estrutura fornecida pela Apple para permitir a medição de campanhas publicitárias de instalação de aplicativos no iOS com privacidade. A SKAdNetwork 4.0 é a versão mais recente da estrutura, que difere da SKAdNetwork 3.0 em vários detalhes importantes.
Use este guia para implementar a SKAdNetwork 4.0 como parte de sua integração com o Singular S2S.
- Saiba mais sobre a SKAdNetwork 4.0 nas Perguntas frequentes sobre a SKAN 4.0.
- Para obter mais informações sobre a SKAdNetwork, consulte o Guia de implementação da SKAdNetwork 3.0 S2S.
Implementação da SKAdNetwork 4.0 S2S
A solução SKAdNetwork da Singular consiste nos seguintes componentes:
- Código do lado do cliente para implementar a SKAdNetwork
- Validação e agregação de postback de todas as redes
- Proteção contra fraudes:
- Validação de assinatura e redução de ID de transação.
- Configuração segura com redes para verificar parâmetros que não são assinados (valor de conversão e geodados).
- Gerenciamento do valor de conversão: oferece a capacidade de configurar dinamicamente no painel da Singular qual atividade pós-instalação deve ser codificada no valor de conversão da SKAdNetwork.
- Relatórios: Traduzir o ID limitado da campanha SKAdNetwork e enriquecer os dados com mais parâmetros e granularidades de marketing.
- Postbacks de parceiros: Fornece o envio de postbacks de SKAdNetwork com um valor de conversão decodificado em eventos e receita, o que é fundamental para sua otimização.
Para os clientes que usam uma integração Singular de servidor para servidor (S2S), a implementação da SKAdNetwork consiste em duas partes principais:
- Implementação do lado do cliente da SKAdNetwork: Essa parte é fundamental para registrar seus aplicativos na SKAdNetwork e gerenciar o valor de conversão da SKAdNetwork de forma inteligente. Isso significa que, ao implementá-la, você poderá otimizar suas campanhas com base nas atribuições de SKAdNetwork e nas atividades pós-instalação associadas.
- Atualização da integração S2S: essa parte é importante para validar e solucionar problemas da implementação no lado do cliente. Ao enriquecer eventos e sessões enviados para a Singular hoje por meio dos endpoints de eventos e sessões S2S da Singular com dados da SKAdNetwork (valores de conversão e carimbos de data/hora de atualização), a Singular pode validar se a implementação foi feita corretamente no lado do seu aplicativo.
Implementação do lado do cliente da SKAdNetwork
A Singular fornece exemplos de código que suportam o registro na SKAdNetwork e o gerenciamento do valor de conversão. Esses exemplos de código são responsáveis pelas seguintes partes:
- Suporte e registro da SKAdNetwork
- Gerenciamento do valor de conversão:
- O código se comunica de forma síncrona com o endpoint da Singular para receber o próximo valor de conversão com base no modelo de conversão configurado. Ele relata eventos/sessões/receita e, em resposta, obtém o próximo valor de conversão, que é um número codificado que representa a atividade pós-instalação que foi configurada para medição no painel da Singular.
- O código também coleta metadados da SKAdnetwork por período de medição, que são usados para validação e cálculo do próximo valor de conversão:
- O registro de data e hora da primeira chamada para a estrutura SKAdNetwork subjacente
- O registro de data e hora da última chamada para a estrutura SKAdNetwork subjacente
- O último valor de postbacks atualizado (tanto Coarse quanto Fine)
- Receita total e receita Admon geradas pelo usuário
Atualização da integração S2S
Obrigatório
Quando você tiver um modelo de conversão ativo, nossos pontos de extremidade S2S começarão a retornar um novo campo int chamado "conversion_value", que conterá o próximo valor a ser atualizado no código do lado do cliente.
Recomendado
Para validar a integração e oferecer suporte a insights adicionais, recomendamos usar nossos exemplos de código para estender sua integração S2S atual. Os endpoints de sessão e evento S2S da Singular já suportam a obtenção de parâmetros SKAdNetwork adicionais, como:
- Valores de conversão P0,P1,P2 fino e grosso
- Valores de conversão anteriores de P0,P1,P2 finos e grosseiros
- Último registro de data e hora de uma chamada para a estrutura SKAdNetwork subjacente
- Primeiro registro de data e hora de uma chamada para a estrutura SKAdNetwork subjacente
- Receita agregada por período de medição de janela (P0, P1, P2)
Fluxo de integração
O diagrama acima ilustra o fluxo da SKAdNetwork para um cliente S2S:
- Primeiro, o código no aplicativo se comunica com um servidor Singular (por meio de um endpoint dedicado para SKAdNetwork) para obter o valor de conversão mais recente de forma síncrona com base em eventos/sessões/receitas que ocorrem no aplicativo e atualizar a estrutura de SKAdNetwork com esse valor.
- Em segundo lugar, o aplicativo enriquece os eventos e sessões existentes com dados da SKAdNetwork, que serão usados posteriormente para validações.
- Quando o aplicativo terminar de atualizar a estrutura de SKAdNetwork com novos valores de conversão e o cronômetro de SKAdNetwork expirar, o postback de SKAdNetwork será enviado para a rede.
- A rede o encaminhará para a Singular (por meio da configuração segura ou da configuração normal).
- A Singular processará o postback da seguinte forma
- Validando sua assinatura
- Decodificação do valor de conversão com base no modelo de conversão configurado
- Enriquecer o postback com informações da rede. Os dados são coletados a partir de integrações com parceiros, unindo o SKAdNetwork e o ID da campanha da rede.
- Envio dos postbacks decodificados ao BI e aos parceiros
Uma observação importante é que as informações da SKAdNetwork, incluindo instalações e eventos decodificados, poderão ser acessadas por meio de um conjunto diferente de relatórios/APIs/tabelas ETL e postbacks para evitar misturá-las com seus conjuntos de dados existentes. Isso é especialmente importante durante as próximas semanas para permitir que você meça e teste a SKAdNetwork lado a lado com sua campanha existente.
Amostras de código iOS nativo da SKAdNetwork
Interface de SKAdNetwork
Essa interface inclui os seguintes componentes de SKAdNetwork:
Registro para SKAdNetwork
- Esse método é responsável por chamar a SKAdNetwork pela primeira vez.
- Ele define o valor de Conversão para P0 como 0
- O método subjacente da API da Apple gera uma notificação se o dispositivo tiver dados de atribuição para esse aplicativo.
Gerenciamentodo valor de conversão
- O valor de conversão é calculado com base na atividade pós-instalação de um dispositivo que é capturado pelos métodos abaixo e um modelo de conversão selecionado que pode ser configurado dinamicamente pelo usuário.
- Os métodos nesta seção são responsáveis por extrair o próximo valor de conversão do endpoint da Singular de acordo com o modelo de conversão selecionado e a atividade pós-instalação relatada (consulte a documentação acima).
- Os métodos abaixo atualizam o valor de conversão com base nas seguintes atividades pós-instalação:
- Sessão: essencial para a medição da retenção de um usuário com SKAdNetwork
- Conversion Event (Evento de conversão): essencial para a medição do evento de conversão pós-instalação com SKAdNetwork
- Evento de receita: Crítico para a medição de receita com SKAdNetwork
//SKANSnippet.h
#import <Foundation/Foundation.h>
@interface SKANSnippet : NSObject
/* Registre-se para atribuição SkAdNetwork.
Você deve chamar esse método o mais rápido possível assim que o aplicativo for iniciado pela primeira vez. Esta função define o valor de conversão como 0 e atualiza o carimbo de data/hora para processamento adicional. */
+ (void)registerAppForAdNetworkAttribution;
/* Para rastrear retenção e coortes, você precisa chamar esse método para cada aplicativo aberto. Ele relata os detalhes da sessão e atualiza o valor de conversão devido a esta sessão, se necessário. O retorno de chamada passado para o método é opcional, você pode usá-lo para executar o código assim que o valor de conversão for atualizado. */
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
/* Para rastrear eventos de conversão com SKAdNetwork, você precisa chamar esse método após cada evento e antes que esse evento seja enviado ao Singular. Ele relata os detalhes do evento e atualiza o valor da conversão devido a esse evento, se necessário. O retorno de chamada passado para o método é opcional, você pode usá-lo para executar o código assim que o valor de conversão for atualizado. */
+ (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
/* Para rastrear receita com SKAdNetwork você precisa chamar esse método antes de cada evento de receita. Ele atualizará a receita total, portanto, quando você chamar 'updateConversionValuesAsync', o novo valor de conversão será determinado de acordo com o valor total da receita.
Observação:
1. Chame esse método antes de chamar 'updateConversionValuesAsync' para garantir que a receita seja atualizada.
2. No caso de repetir um evento, evite chamar este método para que a mesma receita não seja contabilizada duas vezes.*/
+ (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon;
/* Obtém valores atuais finos, grosseiros e bloqueados por janela. salvo no dicionário em "fineValue", "coarseValue", "windowLock". Além disso, contém todos os outros valores relevantes para fins SKAN.*/
// Exemplo:
// {
// "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
Implementação da interface 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" // Chaves SKAN para persistência e solicitações de NSUserDefaults #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 { // Making the lock async so it will not freeze the calling thread 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; } // persist updated conversion values based on the active skan window. + (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; } // Revenues are being accumulated and saved by Ad monetization and non ad monetization events, total sum and break down by skan windows. + (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]; } // Coarse value is being sent on requests and responses as an Int and being translated into the system defined coarse value upon API execution. + (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
Atualização da integração S2S (recomendada)
Atualize sua integração S2S com os seguintes metadados da SKAdNetwork (esses metadados devem ser encaminhados em todas as sessões e eventos relatados à Singular para validação da implementação da SKAdNetwork):
- skan_current_conversion_value: O último valor de conversão fina
- prev_fine_value: O valor anterior da conversão fina
- skan_first_call_to_skadnetwork_timestamp: Registro de data e hora Unix da primeira chamada para a API SkAdNetwork subjacente
- skan_last_call_to_skadnetwork_timestamp: Registro de data e hora Unix da última chamada à API SkAdNetwork subjacente
- p0/1/2_coarse: Os valores grosseiros mais recentes por janela
- p0/1/2_prev_coarse_value: os valores brutos anteriores por janela
- p0/1/2_window_lock: Registro de data e hora Unix da última atualização com bloqueio de janela por janela
- p0/1/2_total_iap_revenue: Total da receita sem monetização de anúncios por janela
- p0/1/2_total_admon_revenue: Total da receita de monetização de anúncios por janela
- skan_total_revenue_by_currency: Total da receita de não monetização de anúncios
- skan_total_admon_revenue_by_currency: Total da receita de monetização de anúncios
A função a seguir mostra como extrair esses valores:
NSDictionary *values = [SKANSnippet getSkanDetails];
Agora, depois de enviar esses parâmetros para o lado do servidor, você pode encaminhá-los por meio de nossos pontos de extremidade de API de servidor para servidor. Para saber mais, pesquise esses parâmetros em nossa referência de API de servidor para servidor.
Exemplo de fluxo do ciclo de vida do aplicativo
// Somente na primeira inicialização do aplicativo
[SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //para o ponto final singular "LAUNCH"
// Após cada sessão ser tratada
[SKANSnippet updateConversionValueAsync:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //para o ponto final singular "LAUNCH"
// Depois de lidar com eventos sem fins lucrativos
[SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"] // para o ponto final singular "EVT"
// Depois de lidar com eventos de receita
[SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"] // para o ponto final singular "EVT"