SKAdNetwork es un framework proporcionado por Apple para permitir una medición respetuosa con la privacidad de las campañas publicitarias de instalación de aplicaciones en iOS.SKAdNetwork 4.0 es la versión más reciente del framework, que difiere de SKAdNetwork 3.0 en varios detalles importantes.
Utilice esta guía para implementar SKAdNetwork 4.0 como parte de su integración Singular S2S.
- Más información sobre SKAdNetwork 4.0 en las preguntas frecuentes sobre SKAN 4.0.
- Para más información sobre SKAdNetwork, consulte la Guía de implementación de SKAdNetwork 3.0 S2S.
Implementación de SKAdNetwork 4.0 S2S
La solución SKAdNetwork de Singular consta de los siguientes componentes:
- Código del lado del cliente para implementar SK AdNetwork
- Validación y agregación postback de todas las redes
- Protección contra el fraude:
- Validación de firmas y deduplicación de ID de transacciones.
- Configuración segura con las redes para verificar los parámetros que no están firmados (valor de conversión y geodatos).
- Gestión del valor de conversión: ofrece la posibilidad de configurar dinámicamente en el panel de control de Singular qué actividad posterior a la instalación debe codificarse en el valor de conversión de SKAdNetwork.
- Elaboración de informes: Traducción del ID de campaña limitado de SKAdNetwork y enriquecimiento de los datos con más parámetros y granularidades de marketing.
- Postbacks de socios: Proporciona el envío de postbacks de SKAdNetwork con un valor de conversión descodificado en eventos e ingresos, lo cual es crítico para su optimización.
Para los clientes que utilizan una integración Singular de servidor a servidor (S2S), la implementación de SKAdNetwork consta de dos partes principales:
- Implementación del lado del cliente de SKAdNetwork: Esta parte es fundamental para registrar sus aplicaciones en SKAdNetwork y gestionar el valor de conversión de SKAdNetwork de forma inteligente. Esto significa que, al implementarla, podrá optimizar sus campañas basándose en las atribuciones de SKAdNetwork y sus actividades post-instalación asociadas.
- Actualización de la integración S2S: Esta parte es importante para validar y solucionar problemas de la implementación del lado del cliente. Al enriquecer los eventos y sesiones enviados hoy a Singular a través de los endpoints de eventos y sesiones S2S de Singular con datos de SKAdNetwork (valores de conversión y marcas de tiempo de actualización), Singular puede validar que la implementación se ha realizado correctamente en el lado de su aplicación.
Implementación del lado del cliente de SKAdNetwork
Singular proporciona ejemplos de código que permiten registrarse en SKAdNetwork y gestionar el valor de conversión. Estos ejemplos de código se encargan de las siguientes partes:
- Soporte y registro de SKAdnetwork
- Gestión del valor de conversión:
- El código se comunica de forma sincrónica con el endpoint de Singular para recibir el siguiente valor de conversión basado en el modelo de conversión configurado. Informa de eventos/sesiones/ingresos y, en respuesta, obtiene el siguiente valor de conversión, que es un número codificado que representa la actividad posterior a la instalación que se configuró para la medición en el cuadro de mandos de Singular.
- El código también recopila metadatos de SKAdnetwork por periodo de medición, que se utilizan tanto para la validación como para el cálculo del siguiente valor de conversión:
- La marca de tiempo de la primera llamada al marco SKAdNetwork subyacente.
- La fecha y hora de la última llamada al marco SKAdNetwork subyacente.
- El último valor de postbacks actualizado (tanto grueso como fino)
- Ingresos totales e ingresos Admon generados por el usuario
Actualización de la integración S2S
Obligatorio
Una vez que tenga un modelo de conversión activo, nuestros endpoints S2S empezarán a devolver un nuevo campo int llamado "conversion_value", que contendrá el siguiente valor a actualizar en el código del lado del cliente.
Recomendado
Para validar la integración y obtener más información, le recomendamos que utilice nuestros ejemplos de código para ampliar su integración S2S actual. Los puntos finales de sesión y eventos S2S de Singular ya admiten la obtención de parámetros SKAdNetwork adicionales como:
- P0,P1,P2 valores de conversión fina y gruesa
- Valores de conversión previos P0,P1,P2 finos y gruesos
- Última marca de tiempo de una llamada al marco SKAdNetwork subyacente
- Primera marca de tiempo de una llamada al marco SKAdNetwork subyacente
- Ingresos agregados por periodo de medición de ventana (P0, P1, P2)
Flujo de integración
El diagrama anterior ilustra el flujo de SKAdNetwork para un cliente S2S:
- En primer lugar, el código de la aplicación se comunica con un servidor Singular (a través de un punto final dedicado para SKAdNetwork) para obtener el último valor de conversión de forma sincrónica en función de los eventos/sesiones/eventos que se producen en la aplicación y actualizar el marco SKAdNetwork con este valor.
- En segundo lugar, la aplicación enriquece los eventos y sesiones existentes con datos de SKAdNetwork, que se utilizarán posteriormente para las validaciones.
- Cuando la aplicación termina de actualizar el marco SKAdNetwork con los nuevos valores de conversión y el temporizador SKAdNetwork expira, el postback SKAdNetwork se envía a la red.
- La red lo reenviará a Singular (a través de la configuración segura o de la configuración normal).
- Singular procesará el postback
- Validando su firma
- Decodificación del valor de conversión basado en el modelo de conversión configurado.
- Enriquecer el postback con información de la red. Los datos se recogen de las integraciones con los socios uniendo el SKAdNetwork y el ID de campaña de la red.
- Envío de los postbacks descodificados a BI y a los socios
Una nota importante es que la información de SKAdNetwork, incluidas las instalaciones y los eventos descodificados, será accesible a través de un conjunto diferente de informes/APIs/ tablas ETL y postbacks para evitar mezclarla con sus conjuntos de datos existentes. Esto es especialmente importante durante las próximas semanas para que pueda medir y probar SKAdNetwork codo con codo con su campaña actual.
Ejemplos de código nativo de SKAdNetwork para iOS
Interfaz SKAdNetwork
Esta interfaz incluye los siguientes componentes de SKAdNetwork:
Registro en SKAdNetwork
- Este método se encarga de llamar a SKAdNetwork por primera vez.
- Establece el valor de conversión de P0 en 0.
- El método subyacente de la API de Apple genera una notificación si el dispositivo tiene datos de atribución para esa aplicación.
Gestióndel valor de conversión
- El valor de conversión se calcula basándose en la actividad posterior a la instalación de un dispositivo que es capturada por los métodos que se indican a continuación y un modelo de conversión seleccionado que puede ser configurado dinámicamente por el usuario.
- Los métodos de esta sección se encargan de extraer el siguiente valor de conversión del endpoint de Singular en función del modelo de conversión seleccionado y de la actividad post-instalación notificada (véase la documentación anterior).
- Los métodos siguientes actualizan el valor de conversión en función de las siguientes actividades posteriores a la instalación:
- Sesión: crítico para la medición de retención de un usuario con SKAdNetwork.
- Evento de conversión: crítico para la medición de eventos de conversión post-instalación con SKAdNetwork
- Evento de ingresos: crítico para la medición de ingresos con SKAdNetwork
//SKANSnippet.h
#import <Foundation/Foundation.h>
@interface SKANSnippet : NSObject
/* Regístrese para la atribución de SkAdNetwork. Debes llamar a este método lo antes posible una vez que la aplicación se inicie por primera vez. Esta función establece el valor de conversión en 0 y actualiza la marca de tiempo para procesamiento adicional.*/
+ (void)registerAppForAdNetworkAttribution;
/* Para realizar un seguimiento de la retención y las cohortes, debe llamar a este método para cada aplicación abierta. Informa los detalles de la sesión y actualiza el valor de conversión debido a esta sesión si es necesario. La devolución de llamada pasada al método es opcional; puede usarla para ejecutar código una vez que se actualice el valor de conversión.*/
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
/* Para realizar un seguimiento de los eventos de conversión con SKAdNetwork, debe llamar a este método después de cada evento y antes de que este evento se envíe a Singular. Informa los detalles del evento y actualiza el valor de conversión debido a este evento si es necesario. La devolución de llamada pasada al método es opcional; puede usarla para ejecutar código una vez que se actualice el valor de conversión.*/
+ (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
/* Para realizar un seguimiento de los ingresos con SKAdNetwork, debe llamar a este método antes de cada evento de ingresos. Actualizará los ingresos totales, por lo que cuando llame a 'updateConversionValuesAsync', el nuevo valor de conversión se determinará según la cantidad total de ingresos.
Nota:
1. Llame a este método antes de llamar a 'updateConversionValuesAsync' para asegurarse de que los ingresos estén actualizados.
2. En caso de volver a intentar un evento, evite llamar a este método para que los mismos ingresos no se cuenten dos veces.*/
+ (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon;
/* Obtiene los valores finos, gruesos y de ventana bloqueada actuales. guardado en el diccionario en "fineValue", "coarseValue", "windowLock". Además, contiene todos los demás valores relevantes para fines SKAN.*/
// Ejemplo:
// {
// "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
Implementación de la interfaz 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" // Claves SKAN para persistencia y solicitudes 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; } // conservar los valores de conversión actualizados según la ventana de skan activa. + (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; } // Los ingresos se acumulan y ahorran mediante eventos de monetización de anuncios y eventos de monetización no publicitaria, suma total y desglosados por ventanas de 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]; } // El valor aproximado se envía en solicitudes y respuestas como un Int y se traduce al valor aproximado definido por el sistema tras la ejecución de la 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
Actualización de la integración S2S (recomendada)
Actualice su integración S2S con los siguientes metadatos de SKAdNetwork (estos metadatos deben enviarse en cada sesión y evento notificado a Singular para la validación de la implementación de SKAdNetwork):
- skan_current_conversion_value: El último valor de conversión fina
- prev_fine_value: El valor de conversión fina anterior
- skan_first_call_to_skadnetwork_timestamp: Marca de tiempo Unix de la primera llamada a la API SkAdNetwork subyacente.
- skan_last_call_to_skadnetwork_timestamp: Marca de tiempo Unix de la última llamada a la API SkAdNetwork subyacente
- p0/1/2_coarse: Los últimos valores gruesos por ventana
- p0/1/2_prev_coarse_value : valores gruesos anteriores por ventana
- p0/1/2_window_lock: Marca de tiempo Unix de la última actualización con bloqueo de ventana por ventana
- p0/1/2_total_iap_revenue: Total de ingresos por monetización no publicitaria por ventana
- p0/1/2_total_admon_revenue: Total de ingresos por monetización de anuncios por ventana
- skan_total_revenue_by_currency: Total de ingresos por monetización no publicitaria
- skan_total_admon_revenue_by_currency: Total de ingresos por monetización de anuncios
La siguiente función muestra cómo extraer estos valores:
NSDictionary *values = [SKANSnippet getSkanDetails];
Ahora, una vez que envíe estos parámetros a su servidor, puede reenviarlos a través de nuestros puntos finales de API de servidor a servidor. Para obtener más información, busque estos parámetros en nuestra referencia de API de servidor a servidor.
Ejemplo de flujo del ciclo de vida de la aplicación
// Solo en el primer lanzamiento de la aplicación
[SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //al punto final singular "LANZAMIENTO"
// Después de cada sesión se maneja
[SKANSnippet updateConversionValueAsync:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //al punto final singular "LANZAMIENTO"
// Después de manejar eventos que no generan ingresos
[SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"] //al punto final singular "EVT"
// Después de manejar eventos de ingresos
[SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"] //al punto final singular "EVT"