react-native-mytatva-rn-sdk 1.2.97 → 1.2.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/android/src/main/java/com/mytatvarnsdk/CgmTrackyLibModule.kt +25 -0
  2. package/android/src/main/java/com/mytatvarnsdk/activity/ConnectSensorActivity.kt +164 -18
  3. package/android/src/main/java/com/mytatvarnsdk/activity/HelpActivity.kt +3 -9
  4. package/android/src/main/java/com/mytatvarnsdk/activity/PermissionActivity.kt +8 -16
  5. package/android/src/main/java/com/mytatvarnsdk/activity/PlaceSensorActivity.kt +12 -13
  6. package/android/src/main/java/com/mytatvarnsdk/activity/PlaceTransmitterActivity.kt +12 -13
  7. package/android/src/main/java/com/mytatvarnsdk/activity/SearchTransmitterActivity.kt +9 -14
  8. package/android/src/main/java/com/mytatvarnsdk/activity/SensorConnectSuccessActivity.kt +12 -16
  9. package/android/src/main/java/com/mytatvarnsdk/activity/StartCGMActivity.kt +9 -15
  10. package/android/src/main/java/com/mytatvarnsdk/activity/VideoActivity.kt +96 -50
  11. package/android/src/main/java/com/mytatvarnsdk/network/AuthenticateSDKService.kt +50 -0
  12. package/android/src/main/java/com/mytatvarnsdk/utils/CustomerSupportConfig.kt +137 -0
  13. package/android/src/main/java/com/mytatvarnsdk/utils/ToolbarSupportHelper.kt +83 -0
  14. package/android/src/main/res/layout/activity_connect_sensor.xml +117 -2
  15. package/ios/Custom/CustomTopUIView.swift +25 -0
  16. package/ios/Database/KLTBluetoothManager.m +69 -14
  17. package/ios/MyReactNativeBridge.m +4 -0
  18. package/ios/Support/API.swift +117 -18
  19. package/ios/Support/CustomTopViewSupportHelper.swift +55 -0
  20. package/ios/Support/CustomerSupportConfig.swift +169 -0
  21. package/ios/Support/Global.swift +2 -2
  22. package/ios/Support/GlobalYouTubePlayerView.swift +102 -11
  23. package/ios/TableViewCell/ImageTVC/ImageTVC.swift +147 -15
  24. package/ios/ViewControllers/AttachTransmitterViewController.swift +16 -21
  25. package/ios/ViewControllers/ChatWithExpertViewController.swift +1 -7
  26. package/ios/ViewControllers/ConnectToSensorViewController.swift +213 -34
  27. package/ios/ViewControllers/ConnectToTransmitterViewController.swift +16 -21
  28. package/ios/ViewControllers/ProvidePermissionViewController.swift +16 -21
  29. package/ios/ViewControllers/PutOnTheSensorViewController.swift +16 -21
  30. package/ios/ViewControllers/StartConnectionViewController.swift +18 -25
  31. package/ios/ViewModel/FinalViewModel.swift +2 -2
  32. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- //
1
+ ​//
2
2
  // KLTBluetoothManager.m
3
3
  // KaiLiTe
4
4
  //
@@ -28,6 +28,8 @@
28
28
  // 蓝牙管理状态
29
29
  @property (nonatomic, assign) CBManagerState centralManagerState;
30
30
 
31
+ - (BOOL)klt_safeWriteData:(NSData *)data withType:(CBCharacteristicWriteType)type;
32
+
31
33
  @end
32
34
 
33
35
  @implementation KLTBluetoothManager
@@ -71,6 +73,19 @@
71
73
  return self;
72
74
  }
73
75
 
76
+ /// NSTimer must not be invalidated synchronously from its own fired callback — that
77
+ /// can block the main thread for seconds (Sentry AppHang in -[KLTBluetoothManager endScan]).
78
+ - (void)invalidateConnectTimerSafely {
79
+ NSTimer *timer = _connectTimer;
80
+ _connectTimer = nil;
81
+ if (!timer) {
82
+ return;
83
+ }
84
+ dispatch_async(dispatch_get_main_queue(), ^{
85
+ [timer invalidate];
86
+ });
87
+ }
88
+
74
89
  + (KLTBluetoothManager *)sharedManager {
75
90
  static KLTBluetoothManager *bluetooth;
76
91
  static dispatch_once_t onceToken;
@@ -81,6 +96,21 @@
81
96
  return bluetooth;
82
97
  }
83
98
 
99
+ - (BOOL)klt_safeWriteData:(NSData *)data withType:(CBCharacteristicWriteType)type {
100
+ if (!data || data.length == 0) {
101
+ return NO;
102
+ }
103
+
104
+ CBPeripheral *peripheral = self.connectedPeripheral;
105
+ CBCharacteristic *characteristic = self.writeReadCharacteristic;
106
+ if (!peripheral || peripheral.state != CBPeripheralStateConnected || !characteristic) {
107
+ KLTLog(@"BLE write skipped - peripheral or write characteristic unavailable");
108
+ return NO;
109
+ }
110
+
111
+ [peripheral writeValue:data forCharacteristic:characteristic type:type];
112
+ return YES;
113
+ }
84
114
 
85
115
  #pragma mark - cbcenteralmanager delegate
86
116
 
@@ -476,7 +506,8 @@
476
506
  Device *currentDevice = self.currentDevice;
477
507
  if (KLTLocalSettingManager.shareInstance.canConnectOtherDevice) {
478
508
  // 直接开始自检协议
479
- [self.connectedPeripheral writeValue:[ProtocalTools check_request:self.eDevice] forCharacteristic:self.writeReadCharacteristic type:CBCharacteristicWriteWithoutResponse];
509
+ [self klt_safeWriteData:[ProtocalTools check_request:self.eDevice]
510
+ withType:CBCharacteristicWriteWithoutResponse];
480
511
  } else {
481
512
  // 二次重连 0x22快传批量数据
482
513
  NSInteger maxGlucoseId = [[KLTDatabaseHandler shared] getLatestAndMaxGlucoseIdOfDevice:currentDevice];
@@ -515,7 +546,9 @@
515
546
  }
516
547
 
517
548
  NSData *data = [ProtocalTools pullGlucose_series_request:glucoseId + 1 andNumber:pullNums andEDevice:self.eDevice];
518
- [self.connectedPeripheral writeValue:data forCharacteristic:self.writeReadCharacteristic type:CBCharacteristicWriteWithoutResponse];
549
+ if (![self klt_safeWriteData:data withType:CBCharacteristicWriteWithoutResponse]) {
550
+ return;
551
+ }
519
552
  self.status = BluetoothManagerStatusResendStart;
520
553
  self.isInResend = YES;
521
554
  }
@@ -532,7 +565,8 @@
532
565
  [Notification_Center postNotificationName:@"DidHandleResendData" object:nil];
533
566
 
534
567
  // CT3/CT4发送重置协议这条命令0x11,结束重传状态
535
- [self.connectedPeripheral writeValue:[ProtocalTools reset_request:self.eDevice] forCharacteristic:self.writeReadCharacteristic type:CBCharacteristicWriteWithoutResponse];
568
+ [self klt_safeWriteData:[ProtocalTools reset_request:self.eDevice]
569
+ withType:CBCharacteristicWriteWithoutResponse];
536
570
  } else {
537
571
  for (Glucose *glucose in glucoseList) {
538
572
  ReceiveData *receive = [self handleCommonReceiveGluData:glucose commandText:@"22"];
@@ -553,7 +587,7 @@
553
587
  pullNums = remain_points;
554
588
  }
555
589
  NSData *data = [ProtocalTools pullGlucose_series_request:maxGlucoseId + 1 andNumber:pullNums andEDevice:self.eDevice];
556
- [self.connectedPeripheral writeValue:data forCharacteristic:self.writeReadCharacteristic type:CBCharacteristicWriteWithoutResponse];
590
+ [self klt_safeWriteData:data withType:CBCharacteristicWriteWithoutResponse];
557
591
  }
558
592
  }
559
593
  }
@@ -595,8 +629,7 @@
595
629
  [self.peripherals removeAllObjects];
596
630
  self.connectedPeripheral = nil;
597
631
 
598
- [_connectTimer invalidate];
599
- _connectTimer = nil;
632
+ [self invalidateConnectTimerSafely];
600
633
  [_rescanTimer invalidate];
601
634
  _rescanTimer = nil;
602
635
 
@@ -610,6 +643,14 @@
610
643
  #pragma mark - other method
611
644
 
612
645
  - (void)endScan {
646
+ // endScan is the _connectTimer target. Defer to the next run-loop turn so we never
647
+ // invalidate the firing timer or run stopScan/KVO inside the timer callback.
648
+ dispatch_async(dispatch_get_main_queue(), ^{
649
+ [self performEndScanAfterTimer];
650
+ });
651
+ }
652
+
653
+ - (void)performEndScanAfterTimer {
613
654
  // 20s后扫描设备超时未连上再次尝试连接
614
655
  if (self.currentDevice && BluetoothManagerStatusScanStart == self.status) {
615
656
  for (CBPeripheral *per in self.peripherals) {
@@ -620,7 +661,7 @@
620
661
  }
621
662
  }
622
663
  }
623
-
664
+
624
665
  [self stopScan];
625
666
  self.status = BluetoothManagerStatusTimeOut;
626
667
  }
@@ -630,8 +671,7 @@
630
671
  self.status = BluetoothManagerStatusScanStop;
631
672
  [self.centralManager stopScan];
632
673
 
633
- [_connectTimer invalidate];
634
- _connectTimer = nil;
674
+ [self invalidateConnectTimerSafely];
635
675
  }
636
676
 
637
677
  // 开始扫描
@@ -678,23 +718,38 @@
678
718
  //}
679
719
 
680
720
  - (void)startScan {
721
+ // Defer to the next main-queue run-loop turn so scanForPeripheralsWithServices is
722
+ // never invoked synchronously from an NSTimer callback (Sentry AppHang on iOS).
723
+ // CBCentralManager is bound to the main queue — this does not move BLE off main.
724
+ dispatch_async(dispatch_get_main_queue(), ^{
725
+ [self performStartScan];
726
+ });
727
+ }
728
+
729
+ - (void)performStartScan {
730
+ if (self.status == BluetoothManagerStatusScanStart) {
731
+ KLTLog(@"startScan skipped — scan already in progress");
732
+ return;
733
+ }
734
+
681
735
  self.connectedPeripheral = nil;
682
736
  [self.peripherals removeAllObjects];
683
-
737
+
684
738
  if (self.centralManager.state != CBManagerStatePoweredOn) {
685
739
  NSLog(@"Bluetooth is not powered on. Aborting scan.");
686
740
  // Do not change status here – just wait for BluetoothEnable notification
687
741
  // The UI layer listens for BluetoothEnable and will start scan once BLE is ready.
688
742
  return;
689
743
  }
690
-
744
+
691
745
  NSLog(@"Allen manager start scan %d", __LINE__);
692
746
  self.status = BluetoothManagerStatusScanStart;
693
747
  self.isBgForceConnectDevice = NO;
694
-
748
+
695
749
  NSDictionary *scanOptions = @{ CBCentralManagerScanOptionAllowDuplicatesKey: @(YES) };
696
750
  [self.centralManager scanForPeripheralsWithServices:nil options:scanOptions];
697
-
751
+
752
+ [self invalidateConnectTimerSafely];
698
753
  _connectTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
699
754
  target:self
700
755
  selector:@selector(endScan)
@@ -471,6 +471,8 @@ RCT_EXPORT_METHOD(startCgmTracky:(NSString *)token envType:(NSString *)envType
471
471
 
472
472
  [defaults synchronize];
473
473
 
474
+ [[CustomerSupportConfig shared] fetchCustomerSupportWithToken:token];
475
+
474
476
  dispatch_async(dispatch_get_main_queue(), ^{
475
477
  UIWindow *keyWindow = [UIApplication sharedApplication].delegate.window;
476
478
  UIViewController *rootVC = keyWindow.rootViewController;
@@ -513,6 +515,8 @@ RCT_EXPORT_METHOD(reconnectCgmTracky:(NSString *)token envType:(NSString *)envT
513
515
 
514
516
  [defaults synchronize];
515
517
 
518
+ [[CustomerSupportConfig shared] fetchCustomerSupportWithToken:token];
519
+
516
520
  dispatch_async(dispatch_get_main_queue(), ^{
517
521
  UIWindow *keyWindow = [UIApplication sharedApplication].delegate.window;
518
522
  UIViewController *rootVC = keyWindow.rootViewController;
@@ -23,8 +23,22 @@ let STAGE_ENC_IV = "9Ddyaf6rfywpiTvT"
23
23
 
24
24
 
25
25
  let defaultToken = "ILtFcw+xtbuy8IgsBCSyD6nSpgZd5AOz7T+g3N8Tef/INZi+dxwPJhnBc2kfdq2e8Kw2yayjaKjuji64coUJsK7e4QcF7sqXCp3Cy5S2OVE+hONwCipkrn9d1yjsBWIDqfLK9ModJCWdigDj5ZeSwkWYSSeTEVoN3Dc2Y8cuqKUQvN9ZcNGmdRrXz2oo3rm4EHQrXPo1Ijufm0HfcqzSH/Gh0TC5gFKjde/GEXIm5Z2Ju1/TTXVSJg/zxNuOF4iUKNEh4UIFUYxYpnqlALRCdF75J+9WAZ7/LBGyB1Wrx+D7bsrzRiQ5sCTwxE3TCM3s66lYJizX22VIzYKOIrJ6GTOYGTINHeIJKEPEvFceR+cG54ez15V1r77ErQElevItD3xIGe0uDezfnixJYPxPhSEOPOyIpxUFInXBRdGjllHPIevzBqqHeP76nz+1U7mFNs1kofugN+5huT3/tCifTQqFyWuxD+VIhl0UJxhxPMbBgoryANgKCToWrK2FqdJwBtZKwCj3S+yp0rff0fwrbMXXsYytJyVLLY2oNviHtzDa/kih0AnMA3vEbqYkf1PQ6LZv+AmtpNYHKh7kMDrhVE3wVxH4+ga0+sfuxYgfiZXCoQW7yY0wOZTiFbgvL2TQflDZR6ttK5abtyKYV4gl5KqbFLZW+d4pKLQoZb02NbJc+5h5gAH2VclTJc6fbkd0qIWuXyqGIBEc2uYrNl2v3vmVvbcGevSqb900ozo6SbFccQLjLcEon+okmgBIDPvBEZFj8fZgQV4z8/yzayDcfqZQPc9Wrw8JNrFLCd8emvo7QHFfGttnIsAtmR8Yp3x49sjEn3hFW6nEKl9FRtRT1r3LzHmIdokL5tz3nO/T3FzLn5sqfp5QBJKKK2dgJdmAz0KC6vKtJ0PtVR4shL7pqjoZuJimFObYKyQbQudW7gLH8WjqORNG2dsplOLzDQ0y7nQdQ0tWrLJtnAch0p5PyTXtunlkNWeuzrldkqVMTUPaAtGcEnmS4RFncD3C6sq9V6lpBKLdrh99vnH3EX81DTH2Hrqm0bN5Tl3UqzUJ7n/JQCR3phDmOo+Uk81y3z3ikiGZZpZikN7c3DtSP34Vc+cNUosWrChqj1gLL4Eg/jMDxE4+bdAGr03ALxEAj2uLGEsDaZRuio6e8/iMcA0z3oqhtAxQ0nU1ivAK7cHeddjRFtvrNb0l+TgrcQI9UVffMOoGDEfAQNUpT2+R5dIysw=="
26
- let TOKEN = UserDefaults.standard.string(forKey: "authToken") ?? defaultToken
27
- let envType = UserDefaults.standard.string(forKey: "envType") ?? "uat"
26
+
27
+ func currentAuthToken() -> String {
28
+ UserDefaults.standard.string(forKey: "authToken") ?? defaultToken
29
+ }
30
+
31
+ func currentEnvType() -> String {
32
+ UserDefaults.standard.string(forKey: "envType") ?? "uat"
33
+ }
34
+
35
+ func normalizedCipherText(_ response: String) -> String {
36
+ var text = response.trimmingCharacters(in: .whitespacesAndNewlines)
37
+ if text.hasPrefix("\""), text.hasSuffix("\""), text.count >= 2 {
38
+ text = String(text.dropFirst().dropLast())
39
+ }
40
+ return text
41
+ }
28
42
 
29
43
  /*TatvaEncryptionConfig {
30
44
  const val STAGE_BASE_URL = "https://api-feature2.mytatva.in/api/v8"
@@ -99,7 +113,7 @@ class API {
99
113
  func postCGMData(
100
114
  data: Payload,
101
115
  environment: TatvaEnvironment,
102
- token: String = TOKEN,
116
+ token: String = currentAuthToken(),
103
117
  encryptionKey: String = PROD_ENC_KEY,
104
118
  encryptionIv: String = PROD_ENC_IV,
105
119
  onSuccess: @escaping () -> Void,
@@ -137,7 +151,8 @@ class API {
137
151
  print("===>Decrypted (for verification): \(decrypted)")
138
152
  }
139
153
 
140
- let urlString = (envType.lowercased() == "uat" ? STAGE_BASE_URL : PROD_BASE_URL) + "/cgm/logs"
154
+ let env = currentEnvType()
155
+ let urlString = (env.lowercased() == "uat" ? STAGE_BASE_URL : PROD_BASE_URL) + "/cgm/logs"
141
156
  // let urlString = (environment == .stage ? STAGE_BASE_URL : PROD_BASE_URL) + "/cgm/logs"
142
157
  guard let url = URL(string: urlString) else {
143
158
  throw URLError(.badURL)
@@ -146,7 +161,7 @@ class API {
146
161
  var request = URLRequest(url: url)
147
162
  request.httpMethod = "POST"
148
163
  request.timeoutInterval = 30
149
- request.setValue(envType.lowercased() == "uat" ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
164
+ request.setValue(env.lowercased() == "uat" ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
150
165
  // request.setValue(environment == .stage ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
151
166
  request.setValue(token, forHTTPHeaderField: "token")
152
167
  request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
@@ -172,7 +187,8 @@ class API {
172
187
 
173
188
  if let data = data, let responseStr = String(data: data, encoding: .utf8) {
174
189
  print("===>Server Response: \(responseStr)")
175
- if let decrypted = Crypto.shared.getDecryptedData(cipherText: responseStr, encryptionKey: encryptionKey, encryptionIv: encryptionIv) {
190
+ let cipherText = normalizedCipherText(responseStr)
191
+ if let decrypted = Crypto.shared.getDecryptedData(cipherText: cipherText, encryptionKey: encryptionKey, encryptionIv: encryptionIv) {
176
192
  print("===>Decrypted post cgm data response (for verification): \(decrypted)")
177
193
  }
178
194
  }
@@ -203,9 +219,9 @@ class API {
203
219
  encryptionKey: String = PROD_ENC_KEY,
204
220
  encryptionIv: String = PROD_ENC_IV) {
205
221
  if sensorId.isEmpty { return }
206
- // let envType = UserDefaults.standard.string(forKey: "envType") ?? "uat"
207
- print("envType: \(envType.lowercased())")
208
- let urlString = (envType.lowercased() == "uat" ? STAGE_BASE_URL : PROD_BASE_URL) + "/cgm/connection"
222
+ let env = currentEnvType()
223
+ print("envType: \(env.lowercased())")
224
+ let urlString = (env.lowercased() == "uat" ? STAGE_BASE_URL : PROD_BASE_URL) + "/cgm/connection"
209
225
  //let urlString = (environment == .stage ? STAGE_BASE_URL : PROD_BASE_URL) + "/cgm/connection"
210
226
  let url = URL(string: urlString)!
211
227
  var request = URLRequest(url: url)
@@ -214,13 +230,14 @@ class API {
214
230
 
215
231
  // Set headers
216
232
  request.addValue("text/plain", forHTTPHeaderField: "Content-Type")
217
- request.setValue(envType.lowercased() == "uat" ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
233
+ request.setValue(env.lowercased() == "uat" ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
218
234
  //request.setValue(environment == .stage ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
219
- request.addValue(TOKEN, forHTTPHeaderField: "token")
235
+ let token = currentAuthToken()
236
+ request.addValue(token, forHTTPHeaderField: "token")
220
237
  print("===>url:", urlString)
221
238
  print("===>api-key:", PROD_API_KEY)
222
239
  print("===>httpmMethod:","POST")
223
- print("===>ios token:", TOKEN)
240
+ print("===>ios token:", token)
224
241
  // Set request body
225
242
  let json: [String: Any] = [
226
243
  "sensorId": sensorId,
@@ -259,7 +276,8 @@ class API {
259
276
  }
260
277
  if let responseString = String(data: data, encoding: .utf8) {
261
278
  print("===>Server Response: \(responseString)")
262
- if let decrypted = Crypto.shared.getDecryptedData(cipherText: responseString, encryptionKey: encryptionKey, encryptionIv: encryptionIv) {
279
+ let cipherText = normalizedCipherText(responseString)
280
+ if let decrypted = Crypto.shared.getDecryptedData(cipherText: cipherText, encryptionKey: encryptionKey, encryptionIv: encryptionIv) {
263
281
  print("===>Decrypted connection api response (for verification): \(decrypted)")
264
282
  }
265
283
  }
@@ -330,6 +348,84 @@ class API {
330
348
 
331
349
  }*/
332
350
 
351
+ func fetchCustomerSupportDetails(
352
+ token: String = currentAuthToken(),
353
+ encryptionKey: String = PROD_ENC_KEY,
354
+ encryptionIv: String = PROD_ENC_IV,
355
+ onSuccess: @escaping (String) -> Void,
356
+ onFailure: @escaping () -> Void
357
+ ) {
358
+ guard !token.isEmpty else {
359
+ print("===>fetchCustomerSupportDetails: empty token")
360
+ onFailure()
361
+ return
362
+ }
363
+
364
+ let env = currentEnvType()
365
+ let urlString = (env.lowercased() == "uat" ? STAGE_BASE_URL : PROD_BASE_URL) + "/helper/customer_support_details"
366
+
367
+ guard let url = URL(string: urlString) else {
368
+ print("===>fetchCustomerSupportDetails: Invalid URL")
369
+ onFailure()
370
+ return
371
+ }
372
+
373
+ var request = URLRequest(url: url)
374
+ request.httpMethod = "GET"
375
+ request.setValue(env.lowercased() == "uat" ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
376
+ request.setValue(token, forHTTPHeaderField: "token")
377
+
378
+ print("===>fetchCustomerSupportDetails URL:", urlString)
379
+ print("===>fetchCustomerSupportDetails env:", env)
380
+ print("===>fetchCustomerSupportDetails usingDefaultToken:", token == defaultToken)
381
+
382
+ let task = URLSession.shared.dataTask(with: request) { data, response, error in
383
+ if let error = error {
384
+ print("===>fetchCustomerSupportDetails Error: \(error.localizedDescription)")
385
+ DispatchQueue.main.async { onFailure() }
386
+ return
387
+ }
388
+
389
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
390
+ if statusCode > 0 {
391
+ print("===>fetchCustomerSupportDetails HTTP Status: \(statusCode)")
392
+ }
393
+
394
+ guard let data = data, let responseString = String(data: data, encoding: .utf8) else {
395
+ print("===>fetchCustomerSupportDetails: No data received")
396
+ DispatchQueue.main.async { onFailure() }
397
+ return
398
+ }
399
+
400
+ print("===>fetchCustomerSupportDetails Server Response: \(responseString)")
401
+
402
+ let cipherText = normalizedCipherText(responseString)
403
+ if let decrypted = Crypto.shared.getDecryptedData(
404
+ cipherText: cipherText,
405
+ encryptionKey: encryptionKey,
406
+ encryptionIv: encryptionIv
407
+ ) {
408
+ print("===>fetchCustomerSupportDetails Decrypted: \(decrypted)")
409
+ if statusCode >= 400 {
410
+ print("===>fetchCustomerSupportDetails: API error \(statusCode)")
411
+ DispatchQueue.main.async { onFailure() }
412
+ return
413
+ }
414
+ DispatchQueue.main.async { onSuccess(decrypted) }
415
+ return
416
+ }
417
+
418
+ if responseString.contains("\"code\"") {
419
+ DispatchQueue.main.async { onSuccess(responseString) }
420
+ return
421
+ }
422
+
423
+ print("===>fetchCustomerSupportDetails: Decryption failed")
424
+ DispatchQueue.main.async { onFailure() }
425
+ }
426
+ task.resume()
427
+ }
428
+
333
429
  func verifyDevice(
334
430
  sensorId: String,
335
431
  encryptionKey: String = PROD_ENC_KEY,
@@ -337,7 +433,9 @@ class API {
337
433
  onSuccess: @escaping (Int, Bool, String) -> Void,
338
434
  onFailure: @escaping () -> Void
339
435
  ) {
340
- let urlString = (envType.lowercased() == "uat" ? STAGE_BASE_URL : PROD_BASE_URL) + "/helper/device_verification?deviceId=\(sensorId)&deviceType=CGM&vendor=diasens"
436
+ let env = currentEnvType()
437
+ let token = currentAuthToken()
438
+ let urlString = (env.lowercased() == "uat" ? STAGE_BASE_URL : PROD_BASE_URL) + "/helper/device_verification?deviceId=\(sensorId)&deviceType=CGM&vendor=diasens"
341
439
 
342
440
  guard let url = URL(string: urlString) else {
343
441
  print("===>verifyDevice: Invalid URL")
@@ -347,11 +445,11 @@ class API {
347
445
 
348
446
  var request = URLRequest(url: url)
349
447
  request.httpMethod = "GET"
350
- request.setValue(envType.lowercased() == "uat" ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
351
- request.setValue(TOKEN, forHTTPHeaderField: "token")
448
+ request.setValue(env.lowercased() == "uat" ? STAGE_API_KEY : PROD_API_KEY, forHTTPHeaderField: "api-key")
449
+ request.setValue(token, forHTTPHeaderField: "token")
352
450
 
353
451
  print("===>verifyDevice URL:", urlString)
354
- print("===>verifyDevice token:", TOKEN)
452
+ print("===>verifyDevice token:", token)
355
453
 
356
454
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
357
455
  if let error = error {
@@ -368,7 +466,8 @@ class API {
368
466
 
369
467
  print("===>verifyDevice Server Response: \(responseString)")
370
468
 
371
- guard let decrypted = Crypto.shared.getDecryptedData(cipherText: responseString, encryptionKey: encryptionKey, encryptionIv: encryptionIv) else {
469
+ let cipherText = normalizedCipherText(responseString)
470
+ guard let decrypted = Crypto.shared.getDecryptedData(cipherText: cipherText, encryptionKey: encryptionKey, encryptionIv: encryptionIv) else {
372
471
  print("===>verifyDevice: Decryption failed")
373
472
  DispatchQueue.main.async { onFailure() }
374
473
  return
@@ -0,0 +1,55 @@
1
+ //
2
+ // CustomTopViewSupportHelper.swift
3
+ // MyTatvaCare
4
+ //
5
+
6
+ import UIKit
7
+
8
+ enum CustomTopViewSupportHelper {
9
+ static func configureWhatsapp(customTopView: CustomTopUIView) {
10
+ customTopView.refreshWhatsappVisibility()
11
+ }
12
+
13
+ static func bindDemoAction(
14
+ customTopView: CustomTopUIView,
15
+ isForReconnect: Bool,
16
+ presenter: UIViewController,
17
+ analyticsHandler: (() -> Void)? = nil
18
+ ) {
19
+ customTopView.onDemoTapped = {
20
+ analyticsHandler?()
21
+
22
+ let payload = ["status": "cgm_watch_demo_clicked"]
23
+ NotificationCenter.default.post(
24
+ name: Notification.Name("cgmwatchdemoclicked"),
25
+ object: nil,
26
+ userInfo: payload
27
+ )
28
+
29
+ let webVC = WebViewController()
30
+ webVC.modalPresentationStyle = .formSheet
31
+ webVC.videoURL = URL(string: CustomerSupportConfig.shared.demoVideoURL(isForReconnect: isForReconnect))
32
+ presenter.present(webVC, animated: true, completion: nil)
33
+ }
34
+ }
35
+
36
+ static func bindWhatsappAction(
37
+ customTopView: CustomTopUIView,
38
+ navigationController: UINavigationController?
39
+ ) {
40
+ customTopView.onWhatsAppTapped = {
41
+ let payload = [
42
+ "screen_name": "connection journey page",
43
+ "status": "cgm_WA_support_clicked"
44
+ ]
45
+
46
+ NotificationCenter.default.post(
47
+ name: Notification.Name("cgmWAsupportclicked"),
48
+ object: nil,
49
+ userInfo: payload
50
+ )
51
+
52
+ CustomerSupportConfig.shared.openWhatsApp()
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,169 @@
1
+ //
2
+ // CustomerSupportConfig.swift
3
+ // MyTatvaCare
4
+ //
5
+
6
+ import Foundation
7
+ import UIKit
8
+
9
+ extension Notification.Name {
10
+ static let customerSupportConfigDidUpdate = Notification.Name("customerSupportConfigDidUpdate")
11
+ }
12
+
13
+ @objc public class CustomerSupportConfig: NSObject {
14
+ @objc public static let shared = CustomerSupportConfig()
15
+
16
+ static let defaultWhatsAppNumber = "918511975757"
17
+ static let defaultDemoVideoUrl =
18
+ "https://www.youtube.com/embed/65vl2aE1K_I"
19
+
20
+ private(set) var isLoaded = false
21
+ private(set) var isShowWhatsapp = false
22
+ private(set) var whatsAppNumber = CustomerSupportConfig.defaultWhatsAppNumber
23
+ private(set) var watchCGMDemoUrl = CustomerSupportConfig.defaultDemoVideoUrl
24
+
25
+ private override init() {}
26
+
27
+ func effectiveWhatsAppNumber() -> String {
28
+ whatsAppNumber.isEmpty ? Self.defaultWhatsAppNumber : whatsAppNumber
29
+ }
30
+
31
+ func shouldShowWhatsapp() -> Bool {
32
+ guard isLoaded else { return false }
33
+ return isShowWhatsapp && !effectiveWhatsAppNumber().isEmpty
34
+ }
35
+
36
+ func demoVideoURL(isForReconnect: Bool) -> String {
37
+ watchCGMDemoUrl.isEmpty ? Self.defaultDemoVideoUrl : watchCGMDemoUrl
38
+ }
39
+
40
+ func updateFromResponse(_ response: String) {
41
+ guard let data = response.data(using: .utf8),
42
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
43
+ print("CustomerSupportConfig: failed to parse response, using failure defaults")
44
+ applyFailureDefaults()
45
+ return
46
+ }
47
+
48
+ let code: Int
49
+ if let intCode = json["code"] as? Int {
50
+ code = intCode
51
+ } else if let strCode = json["code"] as? String, let parsed = Int(strCode) {
52
+ code = parsed
53
+ } else {
54
+ code = 0
55
+ }
56
+
57
+ guard code == 1,
58
+ let dataObject = json["data"] as? [String: Any] else {
59
+ print("CustomerSupportConfig: non-success API response, using failure defaults")
60
+ applyFailureDefaults()
61
+ return
62
+ }
63
+
64
+ isShowWhatsapp = parseBool(dataObject["isShowWhatsapp"])
65
+
66
+ if let apiNumber = dataObject["whatsAppNumber"] as? String {
67
+ whatsAppNumber = apiNumber.trimmingCharacters(in: .whitespacesAndNewlines)
68
+ }
69
+ if whatsAppNumber.isEmpty {
70
+ whatsAppNumber = Self.defaultWhatsAppNumber
71
+ }
72
+
73
+ if let apiDemoUrl = dataObject["watchCGMDemoUrl"] as? String {
74
+ watchCGMDemoUrl = apiDemoUrl.trimmingCharacters(in: .whitespacesAndNewlines)
75
+ }
76
+ if watchCGMDemoUrl.isEmpty {
77
+ watchCGMDemoUrl = Self.defaultDemoVideoUrl
78
+ }
79
+ isLoaded = true
80
+
81
+ print("CustomerSupportConfig loaded | showWhatsapp=\(isShowWhatsapp) | demoUrl=\(watchCGMDemoUrl)")
82
+ notifyConfigDidUpdate()
83
+ }
84
+
85
+ private func notifyConfigDidUpdate() {
86
+ DispatchQueue.main.async {
87
+ NotificationCenter.default.post(
88
+ name: .customerSupportConfigDidUpdate,
89
+ object: nil
90
+ )
91
+ }
92
+ }
93
+
94
+ func applyFailureDefaults() {
95
+ isShowWhatsapp = true
96
+ whatsAppNumber = Self.defaultWhatsAppNumber
97
+ watchCGMDemoUrl = Self.defaultDemoVideoUrl
98
+ isLoaded = true
99
+ print("CustomerSupportConfig: failure defaults applied | showWhatsapp=\(isShowWhatsapp) | demoUrl=\(watchCGMDemoUrl)")
100
+ notifyConfigDidUpdate()
101
+ }
102
+
103
+ @objc(fetchCustomerSupportWithToken:)
104
+ public func fetchCustomerSupport(withToken token: String) {
105
+ guard !token.isEmpty else {
106
+ print("CustomerSupportConfig: empty token, using failure defaults")
107
+ DispatchQueue.main.async { [weak self] in
108
+ self?.applyFailureDefaults()
109
+ }
110
+ return
111
+ }
112
+
113
+ API.shared.fetchCustomerSupportDetails(
114
+ token: token,
115
+ onSuccess: { [weak self] response in
116
+ DispatchQueue.main.async {
117
+ self?.updateFromResponse(response)
118
+ }
119
+ },
120
+ onFailure: { [weak self] in
121
+ print("CustomerSupportConfig: API fetch failed, using failure defaults")
122
+ DispatchQueue.main.async {
123
+ self?.applyFailureDefaults()
124
+ }
125
+ }
126
+ )
127
+ }
128
+
129
+ func fetchIfNeeded() {
130
+ guard !isLoaded else { return }
131
+ let token = currentAuthToken()
132
+ guard !token.isEmpty, token != defaultToken else {
133
+ print("CustomerSupportConfig: no valid token, using failure defaults")
134
+ applyFailureDefaults()
135
+ return
136
+ }
137
+ fetchCustomerSupport(withToken: token)
138
+ }
139
+
140
+ func openWhatsApp() {
141
+ let cleanedNumber = effectiveWhatsAppNumber().replacingOccurrences(
142
+ of: "[^0-9]",
143
+ with: "",
144
+ options: .regularExpression
145
+ )
146
+ guard !cleanedNumber.isEmpty,
147
+ let url = URL(string: "https://wa.me/\(cleanedNumber)") else {
148
+ return
149
+ }
150
+
151
+ if UIApplication.shared.canOpenURL(url) {
152
+ UIApplication.shared.open(url, options: [:], completionHandler: nil)
153
+ }
154
+ }
155
+
156
+ private func parseBool(_ value: Any?) -> Bool {
157
+ guard let value else { return false }
158
+ switch value {
159
+ case let bool as Bool:
160
+ return bool
161
+ case let int as Int:
162
+ return int == 1
163
+ case let string as String:
164
+ return string.lowercased() == "true" || string == "1"
165
+ default:
166
+ return false
167
+ }
168
+ }
169
+ }
@@ -14,8 +14,8 @@ enum Enum_stroyboard: String {
14
14
  case Main = "MainCGM"
15
15
  }
16
16
 
17
- var watchCGMVideo = "https://www.youtube.com/embed/r5Zemc4R044?si=RPvayD39VzsZuXsA"
18
- var reconnectionwatchCGMVideo = "https://www.youtube.com/embed/n_pUrFoZ1wQ?si=miYcKnqHo-kXU8OP"
17
+ var watchCGMVideo = "https://www.youtube.com/embed/65vl2aE1K_I"
18
+ var reconnectionwatchCGMVideo = "https://www.youtube.com/embed/65vl2aE1K_I"
19
19
  var applyingTheSensor = "https://www.youtube.com/embed/O01BBxzxGmE?si=SblmJ14boqu7Y4qM"
20
20
  var applyingTheTransmitter = "https://www.youtube.com/embed/6m3pxl5BK9A?si=08OGnUJf2BdvAYfd"
21
21
  var connectionJourneyError = "https://www.youtube.com/embed/mVMBdUeTlhc?si=ODZSiDW96UqyFiZb"