LoopAPNSCarbsView.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. // LoopFollow
  2. // LoopAPNSCarbsView.swift
  3. import HealthKit
  4. import SwiftUI
  5. struct LoopAPNSCarbsView: View {
  6. private typealias AbsorptionPreset = (hours: Int, minutes: Int)
  7. @Environment(\.presentationMode) var presentationMode
  8. @ObservedObject private var quickPickMeals = QuickPickMealsManager.shared
  9. @State private var carbsAmount = HKQuantity(unit: .gram(), doubleValue: 0.0)
  10. @State private var absorptionHours = 3
  11. @State private var absorptionMinutes = 0
  12. @State private var foodType = ""
  13. @State private var consumedDate = Date()
  14. @State private var showDatePickerSheet = false
  15. @State private var showAbsorptionPickerSheet = false
  16. @State private var isLoading = false
  17. @State private var showAlert = false
  18. @State private var alertMessage = ""
  19. @State private var alertType: AlertType = .success
  20. @State private var otpTimeRemaining: Int? = nil
  21. @State private var showTOTPWarning = false
  22. private let otpPeriod: TimeInterval = 30
  23. private let timeAdjustmentStepMinutes = 5
  24. private let maxPastHours = 12
  25. private let maxFutureHours = 1
  26. private let minAllowedAbsorptionTime = 0.5
  27. private let maxAllowedAbsorptionTime = 8.0
  28. private let lollipopStandardAbsorption: AbsorptionPreset = (0, 30)
  29. private let tacoStandardAbsorption: AbsorptionPreset = (3, 0)
  30. private let pizzaStandardAbsorption: AbsorptionPreset = (5, 0)
  31. private var otpTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
  32. @FocusState private var carbsFieldIsFocused: Bool
  33. // Computed property to check if TOTP should be blocked
  34. private var isTOTPBlocked: Bool {
  35. TOTPService.shared.isTOTPBlocked(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
  36. }
  37. private var absorptionTimeValue: Double {
  38. Double(absorptionHours) + (Double(absorptionMinutes) / 60.0)
  39. }
  40. private var absorptionTimeText: String {
  41. if absorptionMinutes == 0 {
  42. return "\(absorptionHours) hr"
  43. }
  44. return "\(absorptionHours) hr \(absorptionMinutes) min"
  45. }
  46. private var absorptionConfirmationText: String {
  47. String(format: "%.1f", absorptionTimeValue)
  48. }
  49. private var minimumAbsorptionPreset: AbsorptionPreset {
  50. let minimumAbsorptionMinutes = Int(minAllowedAbsorptionTime * 60)
  51. return (minimumAbsorptionMinutes / 60, minimumAbsorptionMinutes % 60)
  52. }
  53. private var maximumAbsorptionHours: Int {
  54. Int(maxAllowedAbsorptionTime)
  55. }
  56. private var absorptionValidationMessage: String {
  57. String(
  58. format: "Please enter a valid absorption time between %.1f and %.1f hours",
  59. minAllowedAbsorptionTime,
  60. maxAllowedAbsorptionTime
  61. )
  62. }
  63. private var absorptionMinuteOptions: [Int] {
  64. if absorptionHours == minimumAbsorptionPreset.hours {
  65. return [minimumAbsorptionPreset.minutes]
  66. }
  67. if absorptionHours == maximumAbsorptionHours {
  68. return [0]
  69. }
  70. return [0, 30]
  71. }
  72. private var oldestAcceptedDate: Date {
  73. Date().addingTimeInterval(-TimeInterval(maxPastHours) * 60 * 60)
  74. }
  75. private var latestAcceptedDate: Date {
  76. Date().addingTimeInterval(TimeInterval(maxFutureHours) * 60 * 60)
  77. }
  78. enum AlertType {
  79. case success
  80. case error
  81. case confirmation
  82. }
  83. var body: some View {
  84. NavigationView {
  85. VStack {
  86. Form {
  87. if !quickPickMeals.quickPickMeals.isEmpty {
  88. Section(header: QuickPickSectionHeader(title: "Quick-Pick Meals", infoText: QuickPickSectionHeader.mealInfoText)) {
  89. ScrollView(.horizontal, showsIndicators: false) {
  90. HStack(spacing: 12) {
  91. ForEach(quickPickMeals.quickPickMeals) { meal in
  92. Button {
  93. carbsAmount = HKQuantity(unit: .gram(), doubleValue: meal.carbs)
  94. } label: {
  95. Text("\(Int(meal.carbs))g")
  96. .font(.subheadline.weight(.medium))
  97. .padding(.horizontal, 14)
  98. .padding(.vertical, 8)
  99. .background(Color.accentColor.opacity(0.15))
  100. .foregroundColor(.accentColor)
  101. .cornerRadius(8)
  102. }
  103. .buttonStyle(.plain)
  104. }
  105. }
  106. .padding(.vertical, 4)
  107. }
  108. }
  109. }
  110. Section {
  111. HKQuantityInputView(
  112. label: "Carbs Amount",
  113. quantity: $carbsAmount,
  114. unit: .gram(),
  115. maxLength: 4,
  116. minValue: HKQuantity(unit: .gram(), doubleValue: 1.0),
  117. maxValue: Storage.shared.maxCarbs.value,
  118. isFocused: $carbsFieldIsFocused,
  119. onValidationError: { message in
  120. alertMessage = message
  121. alertType = .error
  122. showAlert = true
  123. }
  124. )
  125. HStack(spacing: 8) {
  126. Text("Time")
  127. Spacer()
  128. Button(action: {
  129. adjustConsumedDate(byMinutes: -timeAdjustmentStepMinutes)
  130. }) {
  131. Image(systemName: "minus")
  132. .font(.subheadline.weight(.semibold))
  133. .foregroundColor(Color(.systemGray6))
  134. .frame(width: 28, height: 28)
  135. .background(Color.blue)
  136. .clipShape(Circle())
  137. }
  138. .buttonStyle(.plain)
  139. Button(action: {
  140. showDatePickerSheet = true
  141. }) {
  142. Text(consumedDate, format: Date.FormatStyle().hour().minute())
  143. .font(.body.monospacedDigit())
  144. .foregroundColor(.primary)
  145. .frame(minWidth: 58)
  146. }
  147. .buttonStyle(.plain)
  148. Button(action: {
  149. adjustConsumedDate(byMinutes: timeAdjustmentStepMinutes)
  150. }) {
  151. Image(systemName: "plus")
  152. .font(.subheadline.weight(.semibold))
  153. .foregroundColor(Color(.systemGray6))
  154. .frame(width: 28, height: 28)
  155. .background(Color.blue)
  156. .clipShape(Circle())
  157. }
  158. .buttonStyle(.plain)
  159. }
  160. HStack(alignment: .center, spacing: 12) {
  161. Text("Food Type")
  162. Spacer()
  163. HStack(spacing: 10) {
  164. Button(action: {
  165. foodType = "🍭"
  166. setAbsorptionTime(hours: lollipopStandardAbsorption.hours, minutes: lollipopStandardAbsorption.minutes)
  167. }) {
  168. Text("🍭")
  169. .font(.title3)
  170. .frame(width: 42, height: 42)
  171. .background(foodType == "🍭" ? Color.white.opacity(0.12) : Color.clear)
  172. .cornerRadius(12)
  173. }
  174. .buttonStyle(.plain)
  175. Button(action: {
  176. foodType = "🌮"
  177. setAbsorptionTime(hours: tacoStandardAbsorption.hours, minutes: tacoStandardAbsorption.minutes)
  178. }) {
  179. Text("🌮")
  180. .font(.title3)
  181. .frame(width: 42, height: 42)
  182. .background(foodType == "🌮" ? Color.white.opacity(0.12) : Color.clear)
  183. .cornerRadius(12)
  184. }
  185. .buttonStyle(.plain)
  186. Button(action: {
  187. foodType = "🍕"
  188. setAbsorptionTime(hours: pizzaStandardAbsorption.hours, minutes: pizzaStandardAbsorption.minutes)
  189. }) {
  190. Text("🍕")
  191. .font(.title3)
  192. .frame(width: 42, height: 42)
  193. .background(foodType == "🍕" ? Color.white.opacity(0.12) : Color.clear)
  194. .cornerRadius(12)
  195. }
  196. .buttonStyle(.plain)
  197. Button(action: {
  198. foodType = "🍽️"
  199. showAbsorptionPickerSheet = true
  200. }) {
  201. Text("🍽️")
  202. .font(.title3)
  203. .frame(width: 42, height: 42)
  204. .background(foodType == "🍽️" ? Color.white.opacity(0.12) : Color.clear)
  205. .cornerRadius(12)
  206. }
  207. .buttonStyle(.plain)
  208. }
  209. }
  210. HStack(spacing: 8) {
  211. Text("Absorption Time")
  212. Image(systemName: "info.circle")
  213. .foregroundColor(.blue)
  214. .font(.subheadline)
  215. Spacer()
  216. Button(action: {
  217. showAbsorptionPickerSheet = true
  218. }) {
  219. Text(absorptionTimeText)
  220. .foregroundColor(.secondary)
  221. }
  222. .buttonStyle(.plain)
  223. }
  224. }
  225. // TOTP Blocking Warning Section
  226. if isTOTPBlocked && showTOTPWarning {
  227. Section {
  228. VStack(alignment: .leading, spacing: 8) {
  229. HStack {
  230. Image(systemName: "exclamationmark.triangle.fill")
  231. .foregroundColor(.orange)
  232. Text("TOTP Code Already Used")
  233. .font(.headline)
  234. .foregroundColor(.orange)
  235. }
  236. Text("This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command.")
  237. .font(.caption)
  238. .foregroundColor(.secondary)
  239. .multilineTextAlignment(.leading)
  240. }
  241. .padding(.vertical, 4)
  242. }
  243. }
  244. Section(header: Text("Security")) {
  245. VStack(alignment: .leading) {
  246. Text("Current OTP Code")
  247. .font(.headline)
  248. if let otpCode = TOTPGenerator.extractOTPFromURL(Storage.shared.loopAPNSQrCodeURL.value) {
  249. HStack {
  250. Text(otpCode)
  251. .font(.system(.body, design: .monospaced))
  252. .foregroundColor(.green)
  253. .padding(.vertical, 4)
  254. .padding(.horizontal, 8)
  255. .background(Color.green.opacity(0.1))
  256. .cornerRadius(4)
  257. Text("(" + (otpTimeRemaining.map { "\($0)s left" } ?? "-") + ")")
  258. .font(.caption)
  259. .foregroundColor(.secondary)
  260. }
  261. } else {
  262. Text("Invalid QR code URL")
  263. .foregroundColor(.red)
  264. }
  265. }
  266. }
  267. }
  268. .safeAreaInset(edge: .bottom) {
  269. Button(action: sendCarbs) {
  270. if isLoading {
  271. HStack {
  272. ProgressView()
  273. .scaleEffect(0.8)
  274. Text("Sending...")
  275. }
  276. .frame(maxWidth: .infinity)
  277. } else {
  278. Text("Send Carbs")
  279. .frame(maxWidth: .infinity)
  280. }
  281. }
  282. .buttonStyle(.borderedProminent)
  283. .controlSize(.large)
  284. .disabled(carbsAmount.doubleValue(for: .gram()) <= 0 || isLoading || isTOTPBlocked)
  285. .padding(.horizontal)
  286. .padding(.vertical, 8)
  287. .background(.bar)
  288. }
  289. .navigationTitle("Carbs")
  290. .navigationBarTitleDisplayMode(.inline)
  291. }
  292. .sheet(isPresented: $showDatePickerSheet) {
  293. NavigationStack {
  294. VStack {
  295. DatePicker(
  296. "Time",
  297. selection: Binding(
  298. get: { consumedDate },
  299. set: { consumedDate = clampedConsumedDate($0) }
  300. ),
  301. in: oldestAcceptedDate ... latestAcceptedDate,
  302. displayedComponents: [.hourAndMinute, .date]
  303. )
  304. .datePickerStyle(.wheel)
  305. .labelsHidden()
  306. .padding()
  307. Spacer()
  308. }
  309. .navigationTitle("Consumption Time")
  310. .navigationBarTitleDisplayMode(.inline)
  311. .toolbar {
  312. ToolbarItem(placement: .confirmationAction) {
  313. Button("Done") {
  314. consumedDate = clampedConsumedDate(consumedDate)
  315. showDatePickerSheet = false
  316. }
  317. }
  318. }
  319. }
  320. .presentationDetents([.medium, .large])
  321. .presentationDragIndicator(.visible)
  322. }
  323. .sheet(isPresented: $showAbsorptionPickerSheet) {
  324. NavigationStack {
  325. VStack {
  326. HStack(spacing: 0) {
  327. Picker("Hours", selection: $absorptionHours) {
  328. ForEach(0 ... maximumAbsorptionHours, id: \.self) { hour in
  329. Text("\(hour) hr")
  330. .tag(hour)
  331. }
  332. }
  333. Picker("Minutes", selection: $absorptionMinutes) {
  334. ForEach(absorptionMinuteOptions, id: \.self) { minute in
  335. Text("\(minute) min")
  336. .tag(minute)
  337. }
  338. }
  339. }
  340. .pickerStyle(.wheel)
  341. .frame(height: 180)
  342. .padding(.horizontal)
  343. Spacer()
  344. }
  345. .navigationTitle("Absorption Time")
  346. .navigationBarTitleDisplayMode(.inline)
  347. .toolbar {
  348. ToolbarItem(placement: .confirmationAction) {
  349. Button("Done") {
  350. normalizeAbsorptionTime()
  351. showAbsorptionPickerSheet = false
  352. }
  353. }
  354. }
  355. }
  356. .presentationDetents([.medium])
  357. .presentationDragIndicator(.visible)
  358. .onAppear {
  359. normalizeAbsorptionTime()
  360. }
  361. .onChange(of: absorptionHours) { _ in
  362. normalizeAbsorptionTime()
  363. }
  364. }
  365. .onAppear {
  366. // Validate APNS setup
  367. let apnsService = LoopAPNSService()
  368. if !apnsService.validateSetup() {
  369. alertMessage = "Loop APNS setup is incomplete. Please configure all required fields in settings."
  370. alertType = .error
  371. showAlert = true
  372. }
  373. quickPickMeals.refresh(
  374. maxCarbs: Storage.shared.maxCarbs.value.doubleValue(for: .gram()),
  375. includeFatProtein: false
  376. )
  377. // Reset timer state so it shows '-' until first tick
  378. otpTimeRemaining = nil
  379. // Don't reset TOTP usage flag here - let the timer handle it
  380. // Validate TOTP state when view appears
  381. _ = isTOTPBlocked
  382. // Add delay before showing TOTP warning to prevent flash after successful send
  383. if isTOTPBlocked {
  384. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  385. showTOTPWarning = true
  386. }
  387. } else {
  388. showTOTPWarning = false
  389. }
  390. }
  391. .onReceive(otpTimer) { _ in
  392. let now = Date().timeIntervalSince1970
  393. let newOtpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
  394. // Check if we've moved to a new TOTP period (when time remaining increases)
  395. if let currentOtpTimeRemaining = otpTimeRemaining,
  396. newOtpTimeRemaining > currentOtpTimeRemaining
  397. {
  398. // New TOTP code generated, reset the usage flag
  399. TOTPService.shared.resetTOTPUsage()
  400. }
  401. // Also check if we're at the very beginning of a new period (when time remaining is close to 30)
  402. if newOtpTimeRemaining >= 29 {
  403. // We're at the start of a new TOTP period, reset the usage flag
  404. TOTPService.shared.resetTOTPUsage()
  405. }
  406. otpTimeRemaining = newOtpTimeRemaining
  407. }
  408. .alert(isPresented: $showAlert) {
  409. switch alertType {
  410. case .success:
  411. return Alert(
  412. title: Text("Success"),
  413. message: Text(alertMessage),
  414. dismissButton: .default(Text("OK")) {
  415. presentationMode.wrappedValue.dismiss()
  416. }
  417. )
  418. case .error:
  419. return Alert(
  420. title: Text("Error"),
  421. message: Text(alertMessage),
  422. dismissButton: .default(Text("OK"))
  423. )
  424. case .confirmation:
  425. let timeFormatter = DateFormatter()
  426. timeFormatter.timeStyle = .short
  427. timeFormatter.dateStyle = .short
  428. return Alert(
  429. title: Text("Confirm Carbs"),
  430. message: Text("Send \(Int(carbsAmount.doubleValue(for: .gram())))g of carbs with \(absorptionConfirmationText)h absorption time at \(timeFormatter.string(from: consumedDate))?"),
  431. primaryButton: .default(Text("Send")) {
  432. sendCarbsConfirmed()
  433. },
  434. secondaryButton: .cancel()
  435. )
  436. }
  437. }
  438. }
  439. }
  440. private func sendCarbs() {
  441. guard carbsAmount.doubleValue(for: .gram()) > 0 else {
  442. alertMessage = "Please enter a valid carb amount"
  443. alertType = .error
  444. showAlert = true
  445. return
  446. }
  447. // Check guardrails
  448. let maxCarbs = Storage.shared.maxCarbs.value.doubleValue(for: .gram())
  449. let carbsValue = carbsAmount.doubleValue(for: .gram())
  450. if carbsValue > maxCarbs {
  451. alertMessage = "Carbs amount (\(Int(carbsValue))g) exceeds the maximum allowed (\(Int(maxCarbs))g). Please reduce the amount."
  452. alertType = .error
  453. showAlert = true
  454. return
  455. }
  456. // Validate time constraints (similar to LoopCaregiver)
  457. let now = Date()
  458. let oldestAcceptedDate = now.addingTimeInterval(-60 * 60 * Double(maxPastHours))
  459. let latestAcceptedDate = now.addingTimeInterval(60 * 60 * Double(maxFutureHours))
  460. if consumedDate < oldestAcceptedDate {
  461. alertMessage = "Time must be within the prior \(maxPastHours) hours"
  462. alertType = .error
  463. showAlert = true
  464. return
  465. }
  466. if consumedDate > latestAcceptedDate {
  467. alertMessage = "Time must be within the next \(maxFutureHours) hour"
  468. alertType = .error
  469. showAlert = true
  470. return
  471. }
  472. alertType = .confirmation
  473. showAlert = true
  474. }
  475. private func sendCarbsConfirmed() {
  476. isLoading = true
  477. // Extract OTP from QR code URL
  478. guard let otpCode = TOTPGenerator.extractOTPFromURL(Storage.shared.loopAPNSQrCodeURL.value) else {
  479. alertMessage = "Invalid QR code URL. Please re-scan the QR code in settings."
  480. alertType = .error
  481. isLoading = false
  482. showAlert = true
  483. return
  484. }
  485. let selectedAbsorptionTime = absorptionTimeValue
  486. guard selectedAbsorptionTime >= minAllowedAbsorptionTime, selectedAbsorptionTime <= maxAllowedAbsorptionTime else {
  487. alertMessage = absorptionValidationMessage
  488. alertType = .error
  489. isLoading = false
  490. showAlert = true
  491. return
  492. }
  493. // Create the APNS payload for carbs with custom time
  494. // We "randomize" the milliseconds to avoid issue with NS which
  495. // doesn't allow entries at the same second.
  496. let adjustedConsumedDate = consumedDate.dateUsingCurrentSeconds()
  497. let payload = LoopAPNSPayload(
  498. type: .carbs,
  499. carbsAmount: carbsAmount.doubleValue(for: .gram()),
  500. absorptionTime: selectedAbsorptionTime,
  501. foodType: foodType.isEmpty ? nil : foodType,
  502. consumedDate: adjustedConsumedDate,
  503. otp: otpCode
  504. )
  505. let apnsService = LoopAPNSService()
  506. apnsService.sendCarbsViaAPNS(payload: payload) { success, errorMessage in
  507. DispatchQueue.main.async {
  508. self.isLoading = false
  509. if success {
  510. let sentCarbs = carbsAmount.doubleValue(for: .gram())
  511. if sentCarbs > 0 {
  512. QuickPickMealsManager.shared.recordMeal(carbs: sentCarbs)
  513. }
  514. // Mark TOTP code as used
  515. TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
  516. let timeFormatter = DateFormatter()
  517. timeFormatter.timeStyle = .short
  518. self.alertMessage = "Carbs sent successfully for \(timeFormatter.string(from: adjustedConsumedDate))!"
  519. self.alertType = .success
  520. LogManager.shared.log(
  521. category: .apns,
  522. message: "Carbs sent - Amount: \(carbsAmount.doubleValue(for: .gram()))g, Absorption: \(absorptionConfirmationText)h, Time: \(adjustedConsumedDate)"
  523. )
  524. } else {
  525. self.alertMessage = errorMessage ?? "Failed to send carbs. Check your Loop APNS configuration."
  526. self.alertType = .error
  527. LogManager.shared.log(
  528. category: .apns,
  529. message: "Failed to send carbs: \(errorMessage ?? "unknown error")"
  530. )
  531. }
  532. self.showAlert = true
  533. }
  534. }
  535. }
  536. private func setAbsorptionTime(hours: Int, minutes: Int) {
  537. absorptionHours = min(max(hours, 0), maximumAbsorptionHours)
  538. absorptionMinutes = minutes
  539. normalizeAbsorptionTime()
  540. }
  541. private func normalizeAbsorptionTime() {
  542. if absorptionTimeValue < minAllowedAbsorptionTime {
  543. absorptionHours = minimumAbsorptionPreset.hours
  544. absorptionMinutes = minimumAbsorptionPreset.minutes
  545. return
  546. }
  547. if absorptionTimeValue >= maxAllowedAbsorptionTime {
  548. absorptionHours = maximumAbsorptionHours
  549. absorptionMinutes = 0
  550. return
  551. }
  552. if !absorptionMinuteOptions.contains(absorptionMinutes) {
  553. absorptionMinutes = absorptionMinuteOptions.first ?? 0
  554. }
  555. }
  556. private func adjustConsumedDate(byMinutes minutes: Int) {
  557. let adjustedDate = Calendar.current.date(byAdding: .minute, value: minutes, to: consumedDate) ?? consumedDate
  558. consumedDate = clampedConsumedDate(adjustedDate)
  559. }
  560. private func clampedConsumedDate(_ date: Date) -> Date {
  561. min(max(date, oldestAcceptedDate), latestAcceptedDate)
  562. }
  563. }
  564. // APNS Payload structure for carbs
  565. struct LoopAPNSPayload {
  566. enum PayloadType {
  567. case carbs
  568. case bolus
  569. }
  570. let type: PayloadType
  571. let carbsAmount: Double?
  572. let absorptionTime: Double?
  573. let foodType: String?
  574. let bolusAmount: Double?
  575. let consumedDate: Date?
  576. let otp: String
  577. init(type: PayloadType, carbsAmount: Double? = nil, absorptionTime: Double? = nil, foodType: String? = nil, bolusAmount: Double? = nil, consumedDate: Date? = nil, otp: String) {
  578. self.type = type
  579. self.carbsAmount = carbsAmount
  580. self.absorptionTime = absorptionTime
  581. self.foodType = foodType
  582. self.bolusAmount = bolusAmount
  583. self.consumedDate = consumedDate
  584. self.otp = otp
  585. }
  586. }