craft-native 0.0.290 → 0.0.293

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.
@@ -177,7 +177,7 @@ export interface CraftAndroidConfig {
177
177
  };
178
178
  }
179
179
  /** The shared health data types (the iOS builder's HealthKit names). */
180
- export type HealthConnectType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate' | 'heartRateVariability' | 'bodyMass' | 'sleep' | 'mindful' | 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat';
180
+ export type HealthConnectType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate' | 'heartRateVariability' | 'bodyMass' | 'sleep' | 'mindful' | 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat' | 'bloodGlucose' | 'insulinDelivery';
181
181
  /**
182
182
  * The Health Connect permissions behind each type, for reading and for
183
183
  * writing. CraftNativeHealth.kt asks for the same ones; a permission the
@@ -480,7 +480,9 @@ var HEALTH_CONNECT_PERMISSIONS = {
480
480
  dietaryEnergy: { read: ["READ_NUTRITION"], write: ["WRITE_NUTRITION"] },
481
481
  dietaryProtein: { read: ["READ_NUTRITION"], write: ["WRITE_NUTRITION"] },
482
482
  dietaryCarbohydrates: { read: ["READ_NUTRITION"], write: ["WRITE_NUTRITION"] },
483
- dietaryFat: { read: ["READ_NUTRITION"], write: ["WRITE_NUTRITION"] }
483
+ dietaryFat: { read: ["READ_NUTRITION"], write: ["WRITE_NUTRITION"] },
484
+ bloodGlucose: { read: ["READ_BLOOD_GLUCOSE"], write: [] },
485
+ insulinDelivery: { read: [], write: [] }
484
486
  };
485
487
  var DEFAULT_HEALTH_CONNECT = { read: ["steps", "heartRate", "activeEnergy", "distance", "workouts"], write: ["workouts"] };
486
488
  function healthConnectPermissions(config) {
@@ -17,6 +17,7 @@ import androidx.health.connect.client.aggregate.AggregationResult
17
17
  import androidx.health.connect.client.feature.ExperimentalMindfulnessSessionApi
18
18
  import androidx.health.connect.client.permission.HealthPermission
19
19
  import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
20
+ import androidx.health.connect.client.records.BloodGlucoseRecord
20
21
  import androidx.health.connect.client.records.DistanceRecord
21
22
  import androidx.health.connect.client.records.ElevationGainedRecord
22
23
  import androidx.health.connect.client.records.ExerciseSessionRecord
@@ -162,6 +163,10 @@ internal class CraftNativeHealth(
162
163
  getDailyStatistics(type, options(1))
163
164
  }
164
165
  "getHeartRateSamples" -> answer(completion) { getHeartRateSamples(options(0)) }
166
+ "getSamples" -> answer(completion) {
167
+ val type = args.opt(0) as? String ?: throw invalid("getSamples needs a type")
168
+ getSamples(type, options(1))
169
+ }
165
170
  "getSleepIntervals" -> answer(completion) { getSleepIntervals(options(0)) }
166
171
  "saveMindfulSession" -> answer(completion) { saveMindfulSession(options(0)) }
167
172
  "getMindfulSessions" -> answer(completion) { getMindfulSessions(options(0)) }
@@ -438,7 +443,8 @@ internal class CraftNativeHealth(
438
443
  /** The types whose every read permission is granted, in the order of TYPES. */
439
444
  private suspend fun grantedTypes(): Any {
440
445
  val granted = client().permissionController.getGrantedPermissions()
441
- return JSONArray(TYPES.filter { type -> granted.containsAll(readPermissions(type)) })
446
+ // A type Health Connect cannot hold (insulin) is never granted.
447
+ return JSONArray(TYPES.filter { type -> readPermissions(type).let { it.isNotEmpty() && granted.containsAll(it) } })
442
448
  }
443
449
 
444
450
  /**
@@ -783,6 +789,53 @@ internal class CraftNativeHealth(
783
789
  return rows
784
790
  }
785
791
 
792
+ /**
793
+ * Every glucose reading or carb entry between two instants, oldest first,
794
+ * as the writing app saved it: its record id (so a sync stores it once),
795
+ * value, source and device. Glucose comes in mg/dL with its meal relation
796
+ * and specimen when the source said. Health Connect has no insulin type,
797
+ * so `insulinDelivery` answers an empty list rather than failing a sync
798
+ * that asks for all three.
799
+ */
800
+ private suspend fun getSamples(type: String, options: JSONObject): Any {
801
+ if (type !in SAMPLE_TYPES) throw invalid("getSamples reads bloodGlucose, insulinDelivery or dietaryCarbohydrates")
802
+ val end = options.instant("endDate") ?: Instant.now()
803
+ val start = options.instant("startDate") ?: end.minus(Duration.ofDays(7))
804
+ val cap = (options.int("limit") ?: 10_000).coerceIn(1, 50_000)
805
+ val rows = JSONArray()
806
+ if (type == "insulinDelivery") return rows
807
+ val client = client()
808
+ fun row(id: String, value: Double, unit: String, from: Instant, to: Instant, metadata: Metadata): JSONObject {
809
+ val origin = metadata.dataOrigin.packageName
810
+ val item = JSONObject()
811
+ .put("id", id)
812
+ .put("value", value)
813
+ .put("unit", unit)
814
+ .put("startDate", from.ms())
815
+ .put("endDate", to.ms())
816
+ .put("sourceName", sourceName(origin))
817
+ .put("sourceBundleId", origin)
818
+ .put("ownApp", origin == context.packageName)
819
+ putDevice(item, metadata.device, withModel = false)
820
+ return item
821
+ }
822
+ if (type == "bloodGlucose") {
823
+ for (record in readAll(client, BloodGlucoseRecord::class, range(start, end), cap = cap)) {
824
+ val item = row(record.metadata.id, record.level.inMilligramsPerDeciliter, "mg/dL", record.time, record.time, record.metadata)
825
+ mealTimeName(record.relationToMeal)?.let { item.put("mealTime", it) }
826
+ specimenName(record.specimenSource)?.let { item.put("specimen", it) }
827
+ rows.put(item)
828
+ }
829
+ return rows
830
+ }
831
+ for (record in readAll(client, NutritionRecord::class, range(start, end), cap = cap)) {
832
+ val grams = record.totalCarbohydrate?.inGrams ?: continue
833
+ if (grams <= 0) continue
834
+ rows.put(row(record.metadata.id, grams, "g", record.startTime, record.endTime, record.metadata))
835
+ }
836
+ return rows
837
+ }
838
+
786
839
  /**
787
840
  * Sleep between two instants, oldest first, as the sources wrote it: each
788
841
  * stage with its name and whether it counts as asleep. A session with no
@@ -996,7 +1049,28 @@ internal class CraftNativeHealth(
996
1049
  "steps", "heartRate", "activeEnergy", "distance", "workouts", "restingHeartRate",
997
1050
  "heartRateVariability", "bodyMass", "sleep", "mindful",
998
1051
  "dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat",
1052
+ "bloodGlucose", "insulinDelivery",
999
1053
  )
1054
+ /** What `getSamples` reads one record at a time. */
1055
+ private val SAMPLE_TYPES = setOf("bloodGlucose", "insulinDelivery", "dietaryCarbohydrates")
1056
+
1057
+ private fun mealTimeName(relation: Int): String? = when (relation) {
1058
+ BloodGlucoseRecord.RELATION_TO_MEAL_BEFORE_MEAL -> "beforeMeal"
1059
+ BloodGlucoseRecord.RELATION_TO_MEAL_AFTER_MEAL -> "afterMeal"
1060
+ BloodGlucoseRecord.RELATION_TO_MEAL_FASTING -> "fasting"
1061
+ BloodGlucoseRecord.RELATION_TO_MEAL_GENERAL -> "general"
1062
+ else -> null
1063
+ }
1064
+
1065
+ private fun specimenName(source: Int): String? = when (source) {
1066
+ BloodGlucoseRecord.SPECIMEN_SOURCE_CAPILLARY_BLOOD -> "capillaryBlood"
1067
+ BloodGlucoseRecord.SPECIMEN_SOURCE_INTERSTITIAL_FLUID -> "interstitialFluid"
1068
+ BloodGlucoseRecord.SPECIMEN_SOURCE_PLASMA -> "plasma"
1069
+ BloodGlucoseRecord.SPECIMEN_SOURCE_SERUM -> "serum"
1070
+ BloodGlucoseRecord.SPECIMEN_SOURCE_TEARS -> "tears"
1071
+ BloodGlucoseRecord.SPECIMEN_SOURCE_WHOLE_BLOOD -> "wholeBlood"
1072
+ else -> null
1073
+ }
1000
1074
  private val DIETARY_TYPES = setOf("dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat")
1001
1075
 
1002
1076
  private val AWAKE_STAGES = setOf(
@@ -1142,6 +1216,8 @@ internal class CraftNativeHealth(
1142
1216
  "sleep" -> setOf(HealthPermission.getReadPermission(SleepSessionRecord::class))
1143
1217
  "mindful" -> setOf(HealthPermission.getReadPermission(MindfulnessSessionRecord::class))
1144
1218
  in DIETARY_TYPES -> setOf(HealthPermission.getReadPermission(NutritionRecord::class))
1219
+ "bloodGlucose" -> setOf(HealthPermission.getReadPermission(BloodGlucoseRecord::class))
1220
+ // Health Connect has no insulin record: nothing to ask for.
1145
1221
  else -> emptySet()
1146
1222
  }
1147
1223
 
@@ -1546,11 +1546,48 @@ export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance'
1546
1546
  /** Food logged by any app, per day: kcal eaten, then grams of protein, carbohydrate and fat. */
1547
1547
  | 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat'
1548
1548
  /** Mindful minutes (meditation, breathing): read with getMindfulSessions, written with saveMindfulSession. */
1549
- | 'mindful';
1549
+ | 'mindful'
1550
+ /** Blood glucose from a CGM or meter, in mg/dL: read with getSamples. Read only. */
1551
+ | 'bloodGlucose'
1552
+ /**
1553
+ * Insulin from a pump, pen or closed loop, in international units, each
1554
+ * marked basal or bolus: read with getSamples. Read only. HealthKit only:
1555
+ * Health Connect has no insulin type, so Android answers none.
1556
+ */
1557
+ | 'insulinDelivery';
1550
1558
  /** The types with one value per day. `sleep` is hours asleep, counted on the day you woke. */
1551
- export type HealthDailyType = Exclude<HealthDataType, 'workouts' | 'mindful'>;
1559
+ export type HealthDailyType = Exclude<HealthDataType, 'workouts' | 'mindful' | 'bloodGlucose' | 'insulinDelivery'>;
1552
1560
  /** Quantity types that can be written with `health.saveData()`. */
1553
- export type HealthQuantityType = Exclude<HealthDataType, 'workouts' | 'sleep' | 'mindful'>;
1561
+ export type HealthQuantityType = Exclude<HealthDataType, 'workouts' | 'sleep' | 'mindful' | 'bloodGlucose' | 'insulinDelivery'>;
1562
+ /** The types `health.getSamples()` reads one sample at a time: glucose, insulin and carbs, as a diabetes app needs them. */
1563
+ export type HealthSampleType = 'bloodGlucose' | 'insulinDelivery' | 'dietaryCarbohydrates';
1564
+ export interface HealthSamplesQuery extends HealthSampleQuery {
1565
+ /** At most this many, oldest first. Defaults to 10000, capped at 50000. Start defaults to 7 days ago. */
1566
+ limit?: number;
1567
+ }
1568
+ /** One glucose reading, insulin dose or carb entry as the writing app saved it. */
1569
+ export interface HealthQuantitySample {
1570
+ /** HealthKit's UUID or Health Connect's record id: stable across reads. */
1571
+ id: string;
1572
+ /** In `unit`: mg/dL for glucose, IU for insulin, g for carbs. */
1573
+ value: number;
1574
+ unit: 'mg/dL' | 'IU' | 'g';
1575
+ /** Epoch milliseconds. */
1576
+ startDate: number;
1577
+ /** Epoch milliseconds; the same as `startDate` for an instant reading. A basal delivery spans its segment. */
1578
+ endDate: number;
1579
+ sourceName: string;
1580
+ sourceBundleId: string;
1581
+ /** True when this app wrote it. */
1582
+ ownApp: boolean;
1583
+ deviceName?: string;
1584
+ /** Insulin only: what the dose was for. */
1585
+ insulinReason?: 'basal' | 'bolus';
1586
+ /** Glucose only: when it was taken relative to a meal, when the source said. */
1587
+ mealTime?: 'beforeMeal' | 'afterMeal' | 'fasting' | 'general';
1588
+ /** Glucose only (Health Connect): what was sampled, when the source said. */
1589
+ specimen?: 'capillaryBlood' | 'interstitialFluid' | 'plasma' | 'serum' | 'tears' | 'wholeBlood';
1590
+ }
1554
1591
  export interface HealthAuthorizationOptions {
1555
1592
  /** `false` asks to read only. Default: also allowed to save workouts. */
1556
1593
  write?: boolean;
@@ -1737,7 +1774,7 @@ export interface HealthLiveWorkoutEvent {
1737
1774
  message?: string;
1738
1775
  }
1739
1776
  /** The health reads and writes a host may or may not have, depending on the app build it runs in. */
1740
- export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveData' | 'saveMindfulSession' | 'getMindfulSessions' | 'startHeartRateUpdates' | 'stopHeartRateUpdates' | 'startWorkout' | 'pauseWorkout' | 'resumeWorkout' | 'endWorkout';
1777
+ export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSamples' | 'getSleepIntervals' | 'saveData' | 'saveMindfulSession' | 'getMindfulSessions' | 'startHeartRateUpdates' | 'stopHeartRateUpdates' | 'startWorkout' | 'pauseWorkout' | 'resumeWorkout' | 'endWorkout';
1741
1778
  export declare const health: {
1742
1779
  /**
1743
1780
  * Whether this app build answers `method`. An app installed before a read
@@ -1760,6 +1797,13 @@ export declare const health: {
1760
1797
  getDailyStatistics(type: HealthDailyType, options?: HealthDataOptions): Promise<HealthDailyValue[]>;
1761
1798
  /** Every heart-rate reading between two instants, oldest first. iOS only for now. */
1762
1799
  getHeartRateSamples(options?: HealthHeartRateQuery): Promise<HealthHeartRateSample[]>;
1800
+ /**
1801
+ * Every glucose reading, insulin dose or carb entry between two instants,
1802
+ * oldest first, each with its own id so a sync can store it once. Needs
1803
+ * the type's read grant. Health Connect has no insulin, so Android answers
1804
+ * an empty list for `insulinDelivery`.
1805
+ */
1806
+ getSamples(type: HealthSampleType, options?: HealthSamplesQuery): Promise<HealthQuantitySample[]>;
1763
1807
  /** Sleep between two instants, oldest first: time in bed and each asleep or awake stage. iOS only for now. */
1764
1808
  getSleepIntervals(options?: HealthSampleQuery): Promise<HealthSleepInterval[]>;
1765
1809
  /**
package/dist/cli.js CHANGED
@@ -2945,7 +2945,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
2945
2945
  `);
2946
2946
  }
2947
2947
  // package.json
2948
- var version = "0.0.290";
2948
+ var version = "0.0.293";
2949
2949
 
2950
2950
  // bin/cli.ts
2951
2951
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -374,7 +374,7 @@ function craftBinaryNotFoundMessage(triedPath) {
374
374
  `);
375
375
  }
376
376
  // package.json
377
- var version = "0.0.290";
377
+ var version = "0.0.293";
378
378
 
379
379
  // src/sdk-version.ts
380
380
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.290";
120
+ var version = "0.0.293";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -3116,6 +3116,22 @@ struct CraftWebView: UIViewRepresentable {
3116
3116
  } else {
3117
3117
  rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
3118
3118
  }
3119
+ // Glucose, insulin and carbs, one sample at a time (CraftHealth.swift).
3120
+ case "getHealthSamples":
3121
+ if config.enableHealthKit {
3122
+ if let dataType = body["type"] as? String {
3123
+ CraftHealth.shared.getSamples(
3124
+ type: dataType,
3125
+ startDate: body["startDate"] as? Double,
3126
+ endDate: body["endDate"] as? Double,
3127
+ limit: body["limit"] as? Int
3128
+ ) { [weak self] in self?.answer(callbackId, $0) }
3129
+ } else {
3130
+ rejectCallback(callbackId, error: "getHealthSamples was called without a type", code: "INVALID_ARGUMENT")
3131
+ }
3132
+ } else {
3133
+ rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
3134
+ }
3119
3135
  case "getHealthSleepIntervals":
3120
3136
  if config.enableHealthKit {
3121
3137
  getHealthSleepIntervals(
@@ -5749,6 +5765,10 @@ struct CraftWebView: UIViewRepresentable {
5749
5765
  options = options || {};
5750
5766
  return craft._invoke('getHealthHeartRateSamples', {startDate: options.startDate, endDate: options.endDate, limit: options.limit});
5751
5767
  },
5768
+ getSamples: function(type, options) {
5769
+ options = options || {};
5770
+ return craft._invoke('getHealthSamples', {type: type, startDate: options.startDate, endDate: options.endDate, limit: options.limit});
5771
+ },
5752
5772
  getSleepIntervals: function(options) {
5753
5773
  options = options || {};
5754
5774
  return craft._invoke('getHealthSleepIntervals', {startDate: options.startDate, endDate: options.endDate});
@@ -106,6 +106,9 @@ final class CraftHealth {
106
106
  getDailyStatistics(type: type, startDate: options(1)["startDate"] as? Double, endDate: options(1)["endDate"] as? Double, completion: completion)
107
107
  case "getHeartRateSamples":
108
108
  getHeartRateSamples(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
109
+ case "getSamples":
110
+ guard let type = args.first as? String else { return completion(.failure(CraftHealthError(message: "getSamples needs a type", code: "INVALID_ARGUMENT"))) }
111
+ getSamples(type: type, startDate: options(1)["startDate"] as? Double, endDate: options(1)["endDate"] as? Double, limit: options(1)["limit"] as? Int, completion: completion)
109
112
  case "getSleepIntervals":
110
113
  getSleepIntervals(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, completion: completion)
111
114
  case "saveMindfulSession":
@@ -196,6 +199,12 @@ final class CraftHealth {
196
199
  readTypes.insert(mindfulType)
197
200
  shareTypes.insert(mindfulType)
198
201
  }
202
+ case "bloodGlucose", "insulinDelivery":
203
+ // A CGM, meter, pump or closed loop's own writes: read
204
+ // only, never written back, so the sheet only asks to read.
205
+ if let diabetesType = Self.healthQuantity(type)?.type {
206
+ readTypes.insert(diabetesType)
207
+ }
199
208
  case "dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat":
200
209
  // What a food log (MyFitnessPal, Lose It!, Cronometer...)
201
210
  // writes to Apple Health. Request the write grant too unless
@@ -404,6 +413,8 @@ final class CraftHealth {
404
413
  case "dietaryProtein": return (HKQuantityType.quantityType(forIdentifier: .dietaryProtein), .gram(), .cumulativeSum)
405
414
  case "dietaryCarbohydrates": return (HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates), .gram(), .cumulativeSum)
406
415
  case "dietaryFat": return (HKQuantityType.quantityType(forIdentifier: .dietaryFatTotal), .gram(), .cumulativeSum)
416
+ case "bloodGlucose": return (HKQuantityType.quantityType(forIdentifier: .bloodGlucose), HKUnit(from: "mg/dL"), .discreteAverage)
417
+ case "insulinDelivery": return (HKQuantityType.quantityType(forIdentifier: .insulinDelivery), .internationalUnit(), .cumulativeSum)
407
418
  default: return nil
408
419
  }
409
420
  }
@@ -613,6 +624,71 @@ final class CraftHealth {
613
624
  healthStore.execute(query)
614
625
  }
615
626
 
627
+ /// The types `getSamples` reads, and the unit each answers in.
628
+ private static let sampleUnits: [String: String] = ["bloodGlucose": "mg/dL", "insulinDelivery": "IU", "dietaryCarbohydrates": "g"]
629
+
630
+ /// Every glucose reading, insulin dose or carb entry between two
631
+ /// instants, oldest first, as the writing app saved it: its UUID (so a
632
+ /// sync stores it once), value, source and device, plus what a diabetes
633
+ /// app needs to tell readings and doses apart: an insulin dose's reason
634
+ /// (basal or bolus) and a glucose reading's meal time.
635
+ func getSamples(type: String, startDate: Double?, endDate: Double?, limit: Int?, completion rawCompletion: @escaping Completion) {
636
+ let completion = completeOnMain(rawCompletion)
637
+ guard let healthStore = healthStore else {
638
+ completion(.failure(CraftHealthError(message: "HealthKit not available")))
639
+ return
640
+ }
641
+ guard let unitName = Self.sampleUnits[type], let spec = Self.healthQuantity(type), let sampleType = spec.type else {
642
+ completion(.failure(CraftHealthError(message: "getSamples reads bloodGlucose, insulinDelivery or dietaryCarbohydrates", code: "INVALID_ARGUMENT")))
643
+ return
644
+ }
645
+
646
+ let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
647
+ let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -7, to: end)!
648
+ let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
649
+ let oldestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
650
+ let cap = max(1, min(limit ?? 10_000, 50_000))
651
+ let ownBundle = Bundle.main.bundleIdentifier
652
+
653
+ let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: cap, sortDescriptors: [oldestFirst]) { _, samples, error in
654
+ if let error = error, (error as? HKError)?.code != .errorNoData {
655
+ completion(.failure(CraftHealthError(message: error.localizedDescription)))
656
+ return
657
+ }
658
+ let rows: [[String: Any]] = (samples as? [HKQuantitySample] ?? []).map { sample in
659
+ let source = sample.sourceRevision.source
660
+ var item: [String: Any] = [
661
+ "id": sample.uuid.uuidString,
662
+ "value": sample.quantity.doubleValue(for: spec.unit),
663
+ "unit": unitName,
664
+ "startDate": sample.startDate.timeIntervalSince1970 * 1000,
665
+ "endDate": sample.endDate.timeIntervalSince1970 * 1000,
666
+ "sourceName": source.name,
667
+ "sourceBundleId": source.bundleIdentifier,
668
+ "ownApp": ownBundle != nil && source.bundleIdentifier == ownBundle,
669
+ ]
670
+ if let device = sample.device?.name { item["deviceName"] = device }
671
+ if let reason = sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? NSNumber {
672
+ switch HKInsulinDeliveryReason(rawValue: reason.intValue) {
673
+ case .basal: item["insulinReason"] = "basal"
674
+ case .bolus: item["insulinReason"] = "bolus"
675
+ default: break
676
+ }
677
+ }
678
+ if let meal = sample.metadata?[HKMetadataKeyBloodGlucoseMealTime] as? NSNumber {
679
+ switch HKBloodGlucoseMealTime(rawValue: meal.intValue) {
680
+ case .preprandial: item["mealTime"] = "beforeMeal"
681
+ case .postprandial: item["mealTime"] = "afterMeal"
682
+ default: break
683
+ }
684
+ }
685
+ return item
686
+ }
687
+ completion(.success(rows))
688
+ }
689
+ healthStore.execute(query)
690
+ }
691
+
616
692
  /// One heart-rate sample as a page sees it, from a read or the stream
617
693
  /// alike: bpm, when, the app and device that wrote it, a series' count,
618
694
  /// lowest and highest, and what the wearer was doing when the watch says.
package/dist/mobile.js CHANGED
@@ -1715,6 +1715,12 @@ var health = {
1715
1715
  throw new Error("Reading heart-rate samples is unavailable");
1716
1716
  return craft.health.getHeartRateSamples(options);
1717
1717
  },
1718
+ async getSamples(type, options = {}) {
1719
+ const craft = getCraftRoot();
1720
+ if (!craft?.health?.getSamples)
1721
+ throw new Error("Reading health samples is unavailable");
1722
+ return craft.health.getSamples(type, options);
1723
+ },
1718
1724
  async getSleepIntervals(options = {}) {
1719
1725
  const craft = getCraftRoot();
1720
1726
  if (!craft?.health?.getSleepIntervals)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.290",
3
+ "version": "0.0.293",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",