Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/*
2
 * Copyright 2017 Google
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
#import <TargetConditionals.h>
18
#if TARGET_OS_IOS || TARGET_OS_TV
19
 
20
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
21
 
22
#import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
23
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMFetchResponseParser.h"
24
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentData.h"
25
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentDataWithImageURL.h"
26
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageDefinition.h"
27
#import "FirebaseInAppMessaging/Sources/Private/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h"
28
#import "FirebaseInAppMessaging/Sources/Private/Util/FIRIAMTimeFetcher.h"
29
#import "FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.h"
30
 
31
#import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
32
 
33
@interface FIRIAMFetchResponseParser ()
34
@property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
35
@end
36
 
37
@implementation FIRIAMFetchResponseParser
38
 
39
- (instancetype)initWithTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher {
40
  if (self = [super init]) {
41
    _timeFetcher = timeFetcher;
42
  }
43
  return self;
44
}
45
 
46
- (NSArray<FIRIAMMessageDefinition *> *)parseAPIResponseDictionary:(NSDictionary *)responseDict
47
                                                 discardedMsgCount:(NSInteger *)discardCount
48
                                            fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime {
49
  if (fetchWaitTime != nil) {
50
    *fetchWaitTime = nil;  // It would be set to non nil value if it's detected in responseDict
51
    if ([responseDict[@"expirationEpochTimestampMillis"] isKindOfClass:NSString.class]) {
52
      NSTimeInterval nextFetchTimeInResponse =
53
          [responseDict[@"expirationEpochTimestampMillis"] doubleValue] / 1000;
54
      NSTimeInterval fetchWaitTimeInSeconds =
55
          nextFetchTimeInResponse - [self.timeFetcher currentTimestampInSeconds];
56
 
57
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900005",
58
                  @"Detected next fetch epoch time in API response as %f seconds and wait for %f "
59
                   "seconds before next fetch.",
60
                  nextFetchTimeInResponse, fetchWaitTimeInSeconds);
61
 
62
      if (fetchWaitTimeInSeconds > 0.01) {
63
        *fetchWaitTime = @(fetchWaitTimeInSeconds);
64
        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900018",
65
                    @"Fetch wait time calculated from server response is negative. Discard it.");
66
      }
67
    } else {
68
      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900014",
69
                  @"No fetch epoch time detected in API response.");
70
    }
71
  }
72
 
73
  NSArray<NSDictionary *> *messageArray = responseDict[@"messages"];
74
  NSInteger discarded = 0;
75
 
76
  NSMutableArray<FIRIAMMessageDefinition *> *definitions = [[NSMutableArray alloc] init];
77
  for (NSDictionary *nextMsg in messageArray) {
78
    FIRIAMMessageDefinition *nextDefinition =
79
        [self convertToMessageDefinitionWithMessageDict:nextMsg];
80
    if (nextDefinition) {
81
      [definitions addObject:nextDefinition];
82
    } else {
83
      FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM900001",
84
                 @"No definition generated for message node %@", nextMsg);
85
      discarded++;
86
    }
87
  }
88
  FIRLogDebug(
89
      kFIRLoggerInAppMessaging, @"I-IAM900002",
90
      @"%lu message definitions were parsed out successfully and %lu messages are discarded",
91
      (unsigned long)definitions.count, (unsigned long)discarded);
92
 
93
  if (discardCount) {
94
    *discardCount = discarded;
95
  }
96
  return [definitions copy];
97
}
98
 
99
// Return nil if no valid triggering condition can be detected
100
- (NSArray<FIRIAMDisplayTriggerDefinition *> *)parseTriggeringCondition:
101
    (NSArray<NSDictionary *> *)triggerConditions {
102
  if (triggerConditions == nil || triggerConditions.count == 0) {
103
    return nil;
104
  }
105
 
106
  NSMutableArray<FIRIAMDisplayTriggerDefinition *> *triggers = [[NSMutableArray alloc] init];
107
 
108
  for (NSDictionary *nextTriggerCondition in triggerConditions) {
109
    // Handle app_launch and on_foreground cases.
110
    if (nextTriggerCondition[@"fiamTrigger"]) {
111
      if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"ON_FOREGROUND"]) {
112
        [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]];
113
      } else if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"APP_LAUNCH"]) {
114
        [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppLaunchTrigger]];
115
      }
116
    } else if ([nextTriggerCondition[@"event"] isKindOfClass:[NSDictionary class]]) {
117
      NSDictionary *triggeringEvent = (NSDictionary *)nextTriggerCondition[@"event"];
118
      if (triggeringEvent[@"name"]) {
119
        [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc]
120
                                initWithFirebaseAnalyticEvent:triggeringEvent[@"name"]]];
121
      }
122
    }
123
  }
124
 
125
  return [triggers copy];
126
}
127
 
128
// For one element in the restful API response's messages array, convert into
129
// a FIRIAMMessageDefinition object. If the conversion fails, a nil is returned.
130
- (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictionary *)messageNode {
131
  @try {
132
    BOOL isTestMessage = NO;
133
 
134
    id isTestCampaignNode = messageNode[@"isTestCampaign"];
135
    if ([isTestCampaignNode isKindOfClass:[NSNumber class]]) {
136
      isTestMessage = [isTestCampaignNode boolValue];
137
    }
138
 
139
    id payloadNode = messageNode[@"experimentalPayload"] ?: messageNode[@"vanillaPayload"];
140
 
141
    if (![payloadNode isKindOfClass:[NSDictionary class]]) {
142
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900012",
143
                    @"Message payload does not exist or does not represent a dictionary in "
144
                     "message node %@",
145
                    messageNode);
146
      return nil;
147
    }
148
 
149
    NSString *messageID = payloadNode[@"campaignId"];
150
    if (!messageID) {
151
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900010",
152
                    @"messsage id is missing in message node %@", messageNode);
153
      return nil;
154
    }
155
 
156
    NSString *messageName = payloadNode[@"campaignName"];
157
    if (!messageName && !isTestMessage) {
158
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900011",
159
                    @"campaign name is missing in non-test message node %@", messageNode);
160
      return nil;
161
    }
162
 
163
    ABTExperimentPayload *experimentPayload = nil;
164
    NSDictionary *experimentPayloadDictionary = payloadNode[@"experimentPayload"];
165
 
166
    if (experimentPayloadDictionary) {
167
      experimentPayload =
168
          [[ABTExperimentPayload alloc] initWithDictionary:experimentPayloadDictionary];
169
    }
170
 
171
    NSTimeInterval startTimeInSeconds = 0;
172
    NSTimeInterval endTimeInSeconds = 0;
173
    if (!isTestMessage) {
174
      // Parsing start/end times out of non-test messages. They are strings in the
175
      // json response.
176
      id startTimeNode = payloadNode[@"campaignStartTimeMillis"];
177
      if ([startTimeNode isKindOfClass:[NSString class]]) {
178
        startTimeInSeconds = [startTimeNode doubleValue] / 1000.0;
179
      }
180
 
181
      id endTimeNode = payloadNode[@"campaignEndTimeMillis"];
182
      if ([endTimeNode isKindOfClass:[NSString class]]) {
183
        endTimeInSeconds = [endTimeNode doubleValue] / 1000.0;
184
      }
185
    }
186
 
187
    id contentNode = messageNode[@"content"];
188
    if (![contentNode isKindOfClass:[NSDictionary class]]) {
189
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900013",
190
                    @"content node does not exist or does not represent a dictionary in "
191
                     "message node %@",
192
                    messageNode);
193
      return nil;
194
    }
195
 
196
    NSDictionary *content = (NSDictionary *)contentNode;
197
    FIRIAMRenderingMode mode;
198
    UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *secondaryBtnTxtColor,
199
        *titleTextColor;
200
    viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil;
201
 
202
    NSString *title, *body, *imageURLStr, *landscapeImageURLStr, *actionURLStr,
203
        *secondaryActionURLStr, *actionButtonText, *secondaryActionButtonText;
204
    title = body = imageURLStr = landscapeImageURLStr = actionButtonText =
205
        secondaryActionButtonText = actionURLStr = secondaryActionURLStr = nil;
206
 
207
    // TODO: Refactor this giant if-else block into separate parsing methods per message type.
208
    if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) {
209
      NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"];
210
      mode = FIRIAMRenderAsBannerView;
211
 
212
      title = bannerNode[@"title"][@"text"];
213
      titleTextColor = [UIColor firiam_colorWithHexString:bannerNode[@"title"][@"hexColor"]];
214
 
215
      body = bannerNode[@"body"][@"text"];
216
 
217
      imageURLStr = bannerNode[@"imageUrl"];
218
      actionURLStr = bannerNode[@"action"][@"actionUrl"];
219
      viewCardBackgroundColor =
220
          [UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]];
221
 
222
    } else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) {
223
      mode = FIRIAMRenderAsModalView;
224
 
225
      NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"];
226
      title = modalNode[@"title"][@"text"];
227
      titleTextColor = [UIColor firiam_colorWithHexString:modalNode[@"title"][@"hexColor"]];
228
 
229
      body = modalNode[@"body"][@"text"];
230
 
231
      imageURLStr = modalNode[@"imageUrl"];
232
      actionButtonText = modalNode[@"actionButton"][@"text"][@"text"];
233
      btnTxtColor =
234
          [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"text"][@"hexColor"]];
235
      btnBgColor =
236
          [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"buttonHexColor"]];
237
 
238
      actionURLStr = modalNode[@"action"][@"actionUrl"];
239
      viewCardBackgroundColor =
240
          [UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]];
241
    } else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) {
242
      mode = FIRIAMRenderAsImageOnlyView;
243
      NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"];
244
 
245
      imageURLStr = imageOnlyNode[@"imageUrl"];
246
 
247
      if (!imageURLStr) {
248
        FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007",
249
                      @"Image url is missing for image-only message %@", messageNode);
250
        return nil;
251
      }
252
      actionURLStr = imageOnlyNode[@"action"][@"actionUrl"];
253
    } else if ([content[@"card"] isKindOfClass:[NSDictionary class]]) {
254
      mode = FIRIAMRenderAsCardView;
255
      NSDictionary *cardNode = (NSDictionary *)contentNode[@"card"];
256
      title = cardNode[@"title"][@"text"];
257
      titleTextColor = [UIColor firiam_colorWithHexString:cardNode[@"title"][@"hexColor"]];
258
 
259
      body = cardNode[@"body"][@"text"];
260
 
261
      imageURLStr = cardNode[@"portraitImageUrl"];
262
      landscapeImageURLStr = cardNode[@"landscapeImageUrl"];
263
 
264
      viewCardBackgroundColor = [UIColor firiam_colorWithHexString:cardNode[@"backgroundHexColor"]];
265
 
266
      actionButtonText = cardNode[@"primaryActionButton"][@"text"][@"text"];
267
      btnTxtColor = [UIColor
268
          firiam_colorWithHexString:cardNode[@"primaryActionButton"][@"text"][@"hexColor"]];
269
 
270
      secondaryActionButtonText = cardNode[@"secondaryActionButton"][@"text"][@"text"];
271
      secondaryBtnTxtColor = [UIColor
272
          firiam_colorWithHexString:cardNode[@"secondaryActionButton"][@"text"][@"hexColor"]];
273
 
274
      actionURLStr = cardNode[@"primaryAction"][@"actionUrl"];
275
      secondaryActionURLStr = cardNode[@"secondaryAction"][@"actionUrl"];
276
 
277
    } else {
278
      // Unknown message type
279
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003",
280
                    @"Unknown message type in message node %@", messageNode);
281
      return nil;
282
    }
283
 
284
    if (title == nil && mode != FIRIAMRenderAsImageOnlyView) {
285
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004",
286
                    @"Title text is missing in message node %@", messageNode);
287
      return nil;
288
    }
289
 
290
    NSURL *imageURL = [self imageURLFromURLString:imageURLStr];
291
    NSURL *landscapeImageURL = [self imageURLFromURLString:landscapeImageURLStr];
292
    NSURL *actionURL = [self urlFromURLString:actionURLStr];
293
    NSURL *secondaryActionURL = [self urlFromURLString:secondaryActionURLStr];
294
    FIRIAMRenderingEffectSetting *renderEffect =
295
        [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
296
    renderEffect.viewMode = mode;
297
 
298
    if (viewCardBackgroundColor) {
299
      renderEffect.displayBGColor = viewCardBackgroundColor;
300
    }
301
 
302
    if (btnBgColor) {
303
      renderEffect.btnBGColor = btnBgColor;
304
    }
305
 
306
    if (btnTxtColor) {
307
      renderEffect.btnTextColor = btnTxtColor;
308
    }
309
 
310
    if (secondaryBtnTxtColor) {
311
      renderEffect.secondaryActionBtnTextColor = secondaryBtnTxtColor;
312
    }
313
 
314
    if (titleTextColor) {
315
      renderEffect.textColor = titleTextColor;
316
    }
317
 
318
    NSArray<FIRIAMDisplayTriggerDefinition *> *triggersDefinition =
319
        [self parseTriggeringCondition:messageNode[@"triggeringConditions"]];
320
 
321
    if (isTestMessage) {
322
      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008",
323
                    @"A test message with id %@ was parsed successfully.", messageID);
324
      renderEffect.isTestMessage = YES;
325
    } else {
326
      // Triggering definitions should always be present for a non-test message.
327
      if (!triggersDefinition || triggersDefinition.count == 0) {
328
        FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009",
329
                      @"No valid triggering condition is detected in message definition"
330
                       " with id %@",
331
                      messageID);
332
        return nil;
333
      }
334
    }
335
 
336
    FIRIAMMessageContentDataWithImageURL *msgData =
337
        [[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title
338
                                                               messageBody:body
339
                                                          actionButtonText:actionButtonText
340
                                                 secondaryActionButtonText:secondaryActionButtonText
341
                                                                 actionURL:actionURL
342
                                                        secondaryActionURL:secondaryActionURL
343
                                                                  imageURL:imageURL
344
                                                         landscapeImageURL:landscapeImageURL
345
                                                           usingURLSession:nil];
346
 
347
    FIRIAMMessageRenderData *renderData =
348
        [[FIRIAMMessageRenderData alloc] initWithMessageID:messageID
349
                                               messageName:messageName
350
                                               contentData:msgData
351
                                           renderingEffect:renderEffect];
352
    NSDictionary *dataBundle = nil;
353
    id dataBundleNode = messageNode[@"dataBundle"];
354
    if ([dataBundleNode isKindOfClass:[NSDictionary class]]) {
355
      dataBundle = dataBundleNode;
356
    }
357
    if (isTestMessage) {
358
      return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData
359
                                                                    appData:dataBundle
360
                                                          experimentPayload:experimentPayload];
361
    } else {
362
      return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData
363
                                                       startTime:startTimeInSeconds
364
                                                         endTime:endTimeInSeconds
365
                                               triggerDefinition:triggersDefinition
366
                                                         appData:dataBundle
367
                                               experimentPayload:experimentPayload
368
                                                   isTestMessage:NO];
369
    }
370
  } @catch (NSException *e) {
371
    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006",
372
                  @"Error in parsing message node %@ "
373
                   "with error %@",
374
                  messageNode, e);
375
    return nil;
376
  }
377
}
378
 
379
- (nullable NSURL *)imageURLFromURLString:(NSString *)string {
380
  NSURL *url = [self urlFromURLString:string];
381
 
382
  // Image URLs must be valid HTTPS links, according to the Firebase Console.
383
  if (![url.scheme.lowercaseString isEqualToString:@"https"]) return nil;
384
 
385
  return url;
386
}
387
 
388
- (nullable NSURL *)urlFromURLString:(NSString *)string {
389
  NSString *sanitizedString = [self sanitizedURLStringFromString:string];
390
 
391
  if (sanitizedString.length == 0) return nil;
392
 
393
  return [NSURL URLWithString:sanitizedString];
394
}
395
 
396
- (NSString *)sanitizedURLStringFromString:(NSString *)string {
397
  return [string stringByReplacingOccurrencesOfString:@" " withString:@""];
398
}
399
 
400
@end
401
 
402
#endif  // TARGET_OS_IOS || TARGET_OS_TV