From 625ab1d81439481c145829831a6b3593a6ad44aa Mon Sep 17 00:00:00 2001 From: Robert Herber Date: Thu, 11 Sep 2025 19:34:43 +0200 Subject: [PATCH 01/10] Got authorization working all the way --- .gitignore | 3 +- apps/activity-kit-example/app.json | 5 +- .../activity-kit-example/app/(tabs)/index.tsx | 5 + apps/activity-kit-example/package.json | 1 + bun.lock | 64 +++ package.json | 2 +- packages/react-native-alarm-kit/.gitignore | 78 ++++ .../react-native-alarm-kit/.watchmanconfig | 1 + .../NitroAlarmKit.podspec | 31 ++ packages/react-native-alarm-kit/README.md | 38 ++ .../react-native-alarm-kit/babel.config.js | 3 + .../ios/AlarmKitModule.swift | 45 ++ .../ios/AlarmModule.swift | 20 + packages/react-native-alarm-kit/ios/Bridge.h | 8 + .../react-native-alarm-kit/ios/Utils.swift | 32 ++ packages/react-native-alarm-kit/nitro.json | 17 + packages/react-native-alarm-kit/package.json | 67 +++ .../react-native.config.js | 16 + packages/react-native-alarm-kit/src/index.ts | 390 ++++++++++++++++++ .../src/specs/Alarm.nitro.ts | 17 + .../src/specs/AlarmKit.nitro.ts | 75 ++++ packages/react-native-alarm-kit/src/types.ts | 104 +++++ packages/react-native-alarm-kit/tsconfig.json | 27 ++ 23 files changed, 1046 insertions(+), 3 deletions(-) create mode 100644 packages/react-native-alarm-kit/.gitignore create mode 100644 packages/react-native-alarm-kit/.watchmanconfig create mode 100644 packages/react-native-alarm-kit/NitroAlarmKit.podspec create mode 100644 packages/react-native-alarm-kit/README.md create mode 100644 packages/react-native-alarm-kit/babel.config.js create mode 100644 packages/react-native-alarm-kit/ios/AlarmKitModule.swift create mode 100644 packages/react-native-alarm-kit/ios/AlarmModule.swift create mode 100644 packages/react-native-alarm-kit/ios/Bridge.h create mode 100644 packages/react-native-alarm-kit/ios/Utils.swift create mode 100644 packages/react-native-alarm-kit/nitro.json create mode 100644 packages/react-native-alarm-kit/package.json create mode 100644 packages/react-native-alarm-kit/react-native.config.js create mode 100644 packages/react-native-alarm-kit/src/index.ts create mode 100644 packages/react-native-alarm-kit/src/specs/Alarm.nitro.ts create mode 100644 packages/react-native-alarm-kit/src/specs/AlarmKit.nitro.ts create mode 100644 packages/react-native-alarm-kit/src/types.ts create mode 100644 packages/react-native-alarm-kit/tsconfig.json diff --git a/.gitignore b/.gitignore index d95258f..cb031ec 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .DS_Store -generated \ No newline at end of file +generated +/packages/react-native-alarm-kit/android diff --git a/apps/activity-kit-example/app.json b/apps/activity-kit-example/app.json index 8c2bfb9..41c9d4f 100644 --- a/apps/activity-kit-example/app.json +++ b/apps/activity-kit-example/app.json @@ -9,6 +9,9 @@ "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { + "infoPlist": { + "NSAlarmKitUsageDescription": "This app uses ActivityKit to demonstrate Live Activities." + }, "supportsTablet": true, "bundleIdentifier": "com.robertherber.activity-kit-example" }, @@ -40,7 +43,7 @@ "expo-build-properties", { "ios": { - "deploymentTarget": "18.0" + "deploymentTarget": "26.0" } } ], diff --git a/apps/activity-kit-example/app/(tabs)/index.tsx b/apps/activity-kit-example/app/(tabs)/index.tsx index 82b4946..8137f30 100644 --- a/apps/activity-kit-example/app/(tabs)/index.tsx +++ b/apps/activity-kit-example/app/(tabs)/index.tsx @@ -3,6 +3,7 @@ import { Image } from 'expo-image' import * as Notifications from 'expo-notifications' import { useState } from 'react' import { Button, Platform, StyleSheet } from 'react-native' +import AlarmKit from 'react-native-alarm-kit' import { HelloWave } from '@/components/HelloWave' import ParallaxScrollView from '@/components/ParallaxScrollView' import { ThemedText } from '@/components/ThemedText' @@ -50,6 +51,10 @@ export default function HomeScreen() { } title="Push permissions" > + {latestActivityId ? ( + + {latestActivityId ? ( diff --git a/apps/activity-kit-example/targets/widget/WidgetLiveActivity.swift b/apps/activity-kit-example/targets/widget/WidgetLiveActivity.swift index 1a31171..61ea5e3 100644 --- a/apps/activity-kit-example/targets/widget/WidgetLiveActivity.swift +++ b/apps/activity-kit-example/targets/widget/WidgetLiveActivity.swift @@ -4,104 +4,6 @@ import WidgetKit import SwiftUI import NitroActivityKitCore -@available(iOS 26.0, *) -func getAlarmTimeInterval() -> Date { - do { - let addInterval = try AlarmManager.shared.alarms.first(where: { alarm in - alarm.state == .countdown - })?.countdownDuration?.preAlert ?? 10 - - let now = Date(timeIntervalSinceNow: addInterval) - - //now.addTimeInterval(addInterval) - - return now - } - catch { - print("AlarmKit: failed to fetch countdown alarm — \(error)") - return Date(timeIntervalSinceNow: 20) - } - -} - -@available(iOS 26.0, *) -struct WidgetLiveActivityAlarm: Widget { - - var body: some WidgetConfiguration { - - ActivityConfiguration( - for: AlarmAttributes.self) { context in - // Changed VStack to mimic built-in timer style - ZStack { - /*RoundedRectangle(cornerRadius: 18, style: .continuous) - .fill(.ultraThinMaterial)*/ - - HStack(spacing: 8) { - Text("Timer") - .font(.subheadline.weight(.medium)) - .foregroundColor(.secondary) - - Text(getAlarmTimeInterval(), - style: .timer) - .font(.system(size: 36, weight: .medium, design: .monospaced)) - .foregroundColor(.orange) - .minimumScaleFactor(0.5) - .lineLimit(1) - } - .padding() - .activityBackgroundTint(Color.black.opacity(0.1)) - .frame(maxWidth: .infinity, alignment: .leading) - } - .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) - .padding(16) - .activityBackgroundTint(Color.black.opacity(0.1)) - .containerBackground(.ultraThinMaterial, for: .widget) - // .activitySystemActionForegroundColor(Color.black) - - } dynamicIsland: { context in - DynamicIsland { - DynamicIslandExpandedRegion(.leading) { - Text("Leading") - } - DynamicIslandExpandedRegion(.trailing) { - Text("Trailing") - } - DynamicIslandExpandedRegion(.center) { - ZStack { - RoundedRectangle(cornerRadius: 18, style: .continuous) - .fill(.ultraThinMaterial) - - HStack(spacing: 8) { - Text("Timer") - .font(.subheadline.weight(.medium)) - .foregroundColor(.secondary) - Text(getAlarmTimeInterval(), - style: .timer, - ) - .font(.system(size: 36, weight: .medium, design: .monospaced)) - .foregroundColor(.orange) - .minimumScaleFactor(0.5) - .lineLimit(1) - } - .padding() - .frame(maxWidth: .infinity, alignment: .leading) - } - .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) - .padding(16) - } - } compactLeading: { - Text("L") - } compactTrailing: { - Text("Hello") - } minimal: { - Text("Hello") - } - .widgetURL(URL(string: "https://www.expo.dev")) - .keylineTint(Color.red) - } - } -} - struct WidgetLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: ActivityKitModuleAttributes.self) { context in @@ -210,4 +112,3 @@ extension ActivityKitModuleAttributes.ContentState { ActivityKitModuleAttributes.ContentState.smiley ActivityKitModuleAttributes.ContentState.starEyes } - diff --git a/apps/activity-kit-example/targets/widget/WidgetLiveActivityAlarm.swift b/apps/activity-kit-example/targets/widget/WidgetLiveActivityAlarm.swift new file mode 100644 index 0000000..12efa7f --- /dev/null +++ b/apps/activity-kit-example/targets/widget/WidgetLiveActivityAlarm.swift @@ -0,0 +1,111 @@ +import ActivityKit +import AlarmKit +import WidgetKit +import SwiftUI +import NitroActivityKitCore + +@available(iOS 26.0, *) +func getAlarmTimeInterval() -> Date { + do { + let addInterval = try AlarmManager.shared.alarms.first(where: { alarm in + alarm.state == .countdown + })?.countdownDuration?.preAlert ?? 10 + + let now = Date(timeIntervalSinceNow: addInterval) + + // now.addTimeInterval(addInterval) + + return now + } catch { + print("AlarmKit: failed to fetch countdown alarm — \(error)") + return Date(timeIntervalSinceNow: 20) + } + +} + +@available(iOS 26.0, *) +struct WidgetLiveActivityAlarm: Widget { + + var body: some WidgetConfiguration { + + ActivityConfiguration( + for: AlarmAttributes.self) { context in + // Changed VStack to mimic built-in timer style + ZStack { + /*RoundedRectangle(cornerRadius: 18, style: .continuous) + .fill(.ultraThinMaterial)*/ +// context.state.mode <- for checking state + HStack(spacing: 8) { + Text("Timer") + .font(.subheadline.weight(.medium)) + // .glassEffect(.tint(.orange)) + .foregroundColor(.secondary) + + Text(context.attributes.metadata?.getDate("timerFiringAt") ?? Date(), + style: .timer) + .font(.system(size: 36, weight: .medium, design: .monospaced)) + .foregroundColor(.orange) + .minimumScaleFactor(0.5) + // .glassEffect(.tint(.orange)) + .lineLimit(1) + } + .padding() + .activityBackgroundTint(Color.black.opacity(0.1)) + .frame(maxWidth: .infinity, alignment: .leading) + } + .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + .padding(16) + .activityBackgroundTint(Color.black.opacity(0.1)) + .containerBackground(.ultraThinMaterial, for: .widget) + // .activitySystemActionForegroundColor(Color.black) + + } dynamicIsland: { context in + DynamicIsland { + DynamicIslandExpandedRegion(.leading) { + Text("Leading") + .glassEffect() + } + DynamicIslandExpandedRegion(.trailing) { + Text("Trailing") + .glassEffect() + } + DynamicIslandExpandedRegion(.center) { + ZStack { + RoundedRectangle(cornerRadius: 18, style: .continuous) + .fill(.ultraThinMaterial) + + HStack(spacing: 8) { + Text("Timer") + .font(.subheadline.weight(.medium)) + .foregroundColor(.secondary) + .glassEffect() + Text(context.attributes.metadata?.getDate("timerFiringAt") ?? Date(), + style: .timer, + ) + .font(.system(size: 36, weight: .medium, design: .monospaced)) + .foregroundColor(.orange) + .minimumScaleFactor(0.5) + .lineLimit(1) + .glassEffect() + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + .padding(16) + } + } compactLeading: { + Text("L") + .glassEffect() + } compactTrailing: { + Text("Hello") + .glassEffect() + } minimal: { + Text("Hello") + .glassEffect() + } + .widgetURL(URL(string: "https://www.expo.dev")) + .keylineTint(Color.red) + } + } +} diff --git a/packages/react-native-activity-kit/ios/AlarmKitModule.swift b/packages/react-native-activity-kit/ios/AlarmKitModule.swift index f6c00ab..e53e7b8 100644 --- a/packages/react-native-activity-kit/ios/AlarmKitModule.swift +++ b/packages/react-native-activity-kit/ios/AlarmKitModule.swift @@ -2,116 +2,131 @@ import AlarmKit import SwiftUI import NitroModules -@available(iOS 26.0, *) -class AlarmKitModule: HybridAlarmKitModuleSpec { - func requestAuthorization() throws -> NitroModules.Promise { - return Promise.async { - try await withCheckedThrowingContinuation { continuation in - Task { - let result = try await AlarmManager.shared.requestAuthorization() +func createColor(_ color: RGBColor) -> Color { + return Color.init(red: color.red, green: color.green, blue: color.blue) +} - continuation.resume(returning: convertAuthStatus(result)) - } - } - } +@available(iOS 26.0, *) +func createAlarmButtonNullable(_ props: AlarmButtonProps?) -> AlarmButton? { + if let props = props { + return createAlarmButton(props) } - func getPermissionStatus() throws -> AuthStatus { - return convertAuthStatus(AlarmManager.shared.authorizationState) - } + return nil +} - func authorizationUpdates(callback: @escaping (AuthStatus) -> Void) throws { - Task { - for await status in AlarmManager.shared.authorizationUpdates { - callback(convertAuthStatus(status)) - } - } - } +@available(iOS 26.0, *) +func createAlarmButton(_ props: AlarmButtonProps) -> AlarmButton { + return AlarmButton( + text: LocalizedStringResource(stringLiteral: props.text), + textColor: createColor(props.textColor), + systemImageName: props.systemImageName + ) +} - func alarmUpdates(callback: @escaping ([any HybridAlarmSpec]) -> Void) throws { - Task { - for await alarms in AlarmManager.shared.alarmUpdates { - callback(alarms.map { alarm in - return AlarmModule(alarm: alarm) - }) - } - } - } +@available(iOS 26.0, *) +class AlarmKitModule: HybridAlarmKitModuleSpec { + func createCountdown(props: CountdownProps) throws -> NitroModules.Promise { + let alertContent = AlarmPresentation.Alert( + title: LocalizedStringResource(stringLiteral: props.alert.title), + stopButton: createAlarmButton(props.alert.stopButton), + ) - func alarms() throws -> [any HybridAlarmSpec] { - return try AlarmManager.shared.alarms.map { alarm in - return AlarmModule(alarm: alarm) + let countdownContent = AlarmPresentation.Countdown( + title: LocalizedStringResource(stringLiteral: props.countdown.title), + pauseButton: createAlarmButtonNullable(props.countdown.pauseButton), + ) + + var pausedPresentation: AlarmPresentation.Paused? + + if let paused = props.paused { + pausedPresentation = AlarmPresentation.Paused( + title: "Paused", + resumeButton: createAlarmButton(paused.resumeButton), + ) } - } - func createCountdown(alertTitle: String, stopText: String, countdownTitle: String) -> Promise { - let stopButton = AlarmButton( - text: LocalizedStringResource(stringLiteral: stopText), - textColor: Color.black, - systemImageName: "stop.circle" + let presentation = AlarmPresentation( + alert: alertContent, + countdown: countdownContent, + paused: pausedPresentation ) - let secondaryButton = AlarmButton( - text: LocalizedStringResource(stringLiteral: stopText), - textColor: Color.black, - systemImageName: "stop.circle" - ) + let id = UUID() - let alertContent = AlarmPresentation.Alert( - title: LocalizedStringResource(stringLiteral: alertTitle), - stopButton: stopButton, - // secondaryButton: secondaryButton, - // secondaryButtonBehavior: .none - ) + let preAlert = TimeInterval(floatLiteral: props.preAlert) + let postAlert = props.postAlert != nil ? TimeInterval(floatLiteral: props.postAlert!) : nil - let pauseButton = AlarmButton( - text: LocalizedStringResource(stringLiteral: stopText), - textColor: Color.black, - systemImageName: "pause.circle" + let attributes = AlarmAttributes.init( + presentation: presentation, + metadata: try? GenericDictionaryAlarmStruct(state: anyMapToDictionary(props.metadata)), + tintColor: createColor(props.tintColor) ) - let resumeButton = AlarmButton( - text: LocalizedStringResource(stringLiteral: stopText), - textColor: Color.black, - systemImageName: "play.circle" + let alarmConfiguration = AlarmManager.AlarmConfiguration( + countdownDuration: Alarm.CountdownDuration.init( + preAlert: preAlert, + postAlert: postAlert + ), + // schedule: Alarm.Schedule.fixed(Date.now.addingTimeInterval(countdownDuration)), + attributes: attributes, + // stopIntent: Intent.unspecified (alarmID: id), + // secondaryIntent: secondaryIntent(alarmID: id, userInput: userInput) + sound: props.sound != nil ? .named(props.sound!) : .default ) - let countdownContent = AlarmPresentation.Countdown( - title: LocalizedStringResource(stringLiteral: countdownTitle), - pauseButton: pauseButton - ) + return Promise.async { + try await withCheckedThrowingContinuation { continuation in + Task { + do { + let alarm = try await AlarmManager.shared.schedule( + id: id, + configuration: alarmConfiguration + ) + let alarmModule = AlarmModule(alarm: alarm) + continuation.resume(returning: alarmModule) + } catch { + continuation.resume(throwing: error) + } + } + } + } + } - let pausedContent = AlarmPresentation.Paused( - title: "Paused", - resumeButton: resumeButton + func createAlarm(props: AlarmProps) throws -> NitroModules.Promise { + let alertContent = AlarmPresentation.Alert( + title: LocalizedStringResource(stringLiteral: props.alert.title), + stopButton: createAlarmButton(props.alert.stopButton), ) + var pausedPresentation: AlarmPresentation.Paused? + + if let paused = props.paused { + pausedPresentation = AlarmPresentation.Paused( + title: "Paused", + resumeButton: createAlarmButton(paused.resumeButton), + ) + } + let presentation = AlarmPresentation( alert: alertContent, - countdown: countdownContent, - paused: pausedContent + paused: pausedPresentation ) let id = UUID() - let countdownDuration: Double = TimeInterval(floatLiteral: 10) - let attributes = AlarmAttributes.init( presentation: presentation, - metadata: try? GenericDictionaryAlarmStruct(state: [:]), - tintColor: .black + metadata: try? GenericDictionaryAlarmStruct(state: anyMapToDictionary(props.metadata)), + tintColor: createColor(props.tintColor) ) let alarmConfiguration = AlarmManager.AlarmConfiguration( - countdownDuration: Alarm.CountdownDuration.init( - preAlert: countdownDuration, - postAlert: countdownDuration - ), - // schedule: Alarm.Schedule.fixed(Date.now.addingTimeInterval(countdownDuration)), + schedule: Alarm.Schedule.fixed(props.scheduledDate), attributes: attributes, // stopIntent: Intent.unspecified (alarmID: id), // secondaryIntent: secondaryIntent(alarmID: id, userInput: userInput) - sound: .default + sound: props.sound != nil ? .named(props.sound!) : .default ) return Promise.async { @@ -131,4 +146,44 @@ class AlarmKitModule: HybridAlarmKitModuleSpec { } } } + + func requestAuthorization() throws -> NitroModules.Promise { + return Promise.async { + try await withCheckedThrowingContinuation { continuation in + Task { + let result = try await AlarmManager.shared.requestAuthorization() + + continuation.resume(returning: convertAuthStatus(result)) + } + } + } + } + + func getPermissionStatus() throws -> AuthStatus { + return convertAuthStatus(AlarmManager.shared.authorizationState) + } + + func authorizationUpdates(callback: @escaping (AuthStatus) -> Void) throws { + Task { + for await status in AlarmManager.shared.authorizationUpdates { + callback(convertAuthStatus(status)) + } + } + } + + func alarmUpdates(callback: @escaping ([any HybridAlarmSpec]) -> Void) throws { + Task { + for await alarms in AlarmManager.shared.alarmUpdates { + callback(alarms.map { alarm in + return AlarmModule(alarm: alarm) + }) + } + } + } + + func alarms() throws -> [any HybridAlarmSpec] { + return try AlarmManager.shared.alarms.map { alarm in + return AlarmModule(alarm: alarm) + } + } } diff --git a/packages/react-native-activity-kit/src/AlarmKit.ts b/packages/react-native-activity-kit/src/AlarmKit.ts index 0f3ff3f..35e2888 100644 --- a/packages/react-native-activity-kit/src/AlarmKit.ts +++ b/packages/react-native-activity-kit/src/AlarmKit.ts @@ -77,12 +77,26 @@ class AlarmKit { alertTitle: string, stopText: string, countdownTitle: string, + countdownDurationInSeconds: number = 60, ): Promise { - return this.hybridObject.createCountdown( - alertTitle, - stopText, - countdownTitle, - ) + return this.hybridObject.createCountdown({ + tintColor: { red: 0, green: 0.478, blue: 1, alpha: 1 }, + alert: { + title: alertTitle, + stopButton: { + text: stopText, + systemImageName: 'stop.fill', + textColor: { red: 1, green: 1, blue: 1, alpha: 1 }, + }, + }, + countdown: { + title: countdownTitle, + }, + preAlert: countdownDurationInSeconds, + metadata: { + timerFiringAt: Date.now() + countdownDurationInSeconds * 1000, + }, + }) } /*async getPermissionStatus(): Promise { diff --git a/packages/react-native-activity-kit/src/specs/AlarmKit.nitro.ts b/packages/react-native-activity-kit/src/specs/AlarmKit.nitro.ts index d26f63d..22e9c86 100644 --- a/packages/react-native-activity-kit/src/specs/AlarmKit.nitro.ts +++ b/packages/react-native-activity-kit/src/specs/AlarmKit.nitro.ts @@ -1,4 +1,4 @@ -import type { HybridObject } from 'react-native-nitro-modules' +import type { AnyMap, HybridObject } from 'react-native-nitro-modules' import type { Alarm } from './Alarm.nitro' export interface AlarmSound { @@ -63,6 +63,71 @@ export interface AlarmPermissionStatus { status: AuthStatus } +export interface RGBColor { + red: number // 0-255 + green: number // 0-255 + blue: number // 0-255 + alpha?: number // 0-1, default 1 +} + +export interface AlarmButtonProps { + text: string + textColor: RGBColor + systemImageName: string +} + +enum SecondaryButtonBehavior { + countdown = 'countdown', + custom = 'custom', + none = 'none', +} + +export interface AlertPresentation { + title: string + stopButton: AlarmButtonProps + secondaryButton?: AlarmButtonProps + secondaryButtonBehavior?: SecondaryButtonBehavior // default 'none' +} + +export interface CountdownPresentation { + title: string + pauseButton?: AlarmButtonProps +} + +export interface PausedPresentation { + title: string + resumeButton: AlarmButtonProps +} + +export interface CountdownProps { + alert: AlertPresentation + countdown: CountdownPresentation + paused?: PausedPresentation + + tintColor: RGBColor + + sound?: string + + metadata: AnyMap + + preAlert: number // in seconds + postAlert?: number // in seconds +} + +export interface AlarmProps { + alert: AlertPresentation + paused?: PausedPresentation + + tintColor: RGBColor + + sound?: string + + metadata: AnyMap + + // let's just support exact time for now + scheduledDate: Date // timestamp in milliseconds +} + export interface AlarmKitModule extends HybridObject<{ ios: 'swift' }> { // Permission methods requestAuthorization(): Promise @@ -73,9 +138,6 @@ export interface AlarmKitModule extends HybridObject<{ ios: 'swift' }> { alarmUpdates(callback: (alarms: Alarm[]) => void): void alarms(): Alarm[] - createCountdown( - alertTitle: string, - stopText: string, - countdownTitle: string, - ): Promise + createCountdown(props: CountdownProps): Promise + createAlarm(props: AlarmProps): Promise } From 26eaadd8ee3e1e9cd8304e5cbf840437b3b87f83 Mon Sep 17 00:00:00 2001 From: Robert Herber Date: Thu, 18 Sep 2025 17:29:11 +0200 Subject: [PATCH 07/10] feature complete? --- .../activity-kit-example/app/(tabs)/index.tsx | 21 +- .../ios/AlarmKitModule.swift | 174 ++++---- .../ios/AlarmModule.swift | 20 - .../ios/AlarmProxy.swift | 47 +++ .../react-native-activity-kit/src/AlarmKit.ts | 386 +----------------- .../src/specs/Alarm.nitro.ts | 17 - ...rmKit.nitro.ts => AlarmKitModule.nitro.ts} | 12 +- .../src/specs/AlarmProxy.nitro.ts | 23 ++ 8 files changed, 203 insertions(+), 497 deletions(-) delete mode 100644 packages/react-native-activity-kit/ios/AlarmModule.swift create mode 100644 packages/react-native-activity-kit/ios/AlarmProxy.swift delete mode 100644 packages/react-native-activity-kit/src/specs/Alarm.nitro.ts rename packages/react-native-activity-kit/src/specs/{AlarmKit.nitro.ts => AlarmKitModule.nitro.ts} (89%) create mode 100644 packages/react-native-activity-kit/src/specs/AlarmProxy.nitro.ts diff --git a/apps/activity-kit-example/app/(tabs)/index.tsx b/apps/activity-kit-example/app/(tabs)/index.tsx index cd54a52..a1817ef 100644 --- a/apps/activity-kit-example/app/(tabs)/index.tsx +++ b/apps/activity-kit-example/app/(tabs)/index.tsx @@ -1,4 +1,5 @@ import { ActivityKit, AlarmKit } from '@kingstinct/react-native-activity-kit' // Importing the ActivityKit module' +import { SecondaryButtonBehavior } from '@kingstinct/react-native-activity-kit/lib/specs/AlarmKit.nitro' import { Image } from 'expo-image' import * as Notifications from 'expo-notifications' import { useState } from 'react' @@ -57,7 +58,25 @@ export default function HomeScreen() { diff --git a/packages/react-native-activity-kit/ios/AlarmKitModule.swift b/packages/react-native-activity-kit/ios/AlarmKitModule.swift index e53e7b8..d741e68 100644 --- a/packages/react-native-activity-kit/ios/AlarmKitModule.swift +++ b/packages/react-native-activity-kit/ios/AlarmKitModule.swift @@ -3,7 +3,30 @@ import SwiftUI import NitroModules func createColor(_ color: RGBColor) -> Color { - return Color.init(red: color.red, green: color.green, blue: color.blue) + if let alpha = color.alpha { + return Color.init( + red: color.red * 255, + green: color.green * 255, + blue: color.blue * 255, + opacity: alpha + ) + } + return Color.init( + red: color.red * 255, + green: color.green * 255, + blue: color.blue * 255 + ) +} + +func createPausedPresentation(_ paused: PausedPresentation?) -> AlarmPresentation.Paused? { + if let paused = paused { + return AlarmPresentation.Paused( + title: LocalizedStringResource(stringLiteral: paused.title), + resumeButton: createAlarmButton(paused.resumeButton), + ) + } + + return nil } @available(iOS 26.0, *) @@ -24,27 +47,75 @@ func createAlarmButton(_ props: AlarmButtonProps) -> AlarmButton { ) } +func createSecondButtonBehavior(_ behavior: SecondaryButtonBehavior?) -> AlarmPresentation.Alert.SecondaryButtonBehavior? { + if let behavior = behavior { + + switch behavior { + case .countdown: + return .countdown + case .custom: + return .custom + case .none: + return .none + } + } + return nil +} + +func createAlertPresentation(_ alertPresentation: AlertPresentation) -> AlarmPresentation.Alert { + let alertContent = AlarmPresentation.Alert( + title: LocalizedStringResource(stringLiteral: alertPresentation.title), + stopButton: createAlarmButton(alertPresentation.stopButton), + secondaryButton: createAlarmButtonNullable(alertPresentation.secondaryButton), + secondaryButtonBehavior: createSecondButtonBehavior(alertPresentation.secondaryButtonBehavior) + ) + + return alertContent +} + +func scheduleAlarm(alarmConfiguration: AlarmManager.AlarmConfiguration) -> Promise { + return Promise.async { + try await withCheckedThrowingContinuation { continuation in + Task { + do { + let id = UUID() + let alarm = try await AlarmManager.shared.schedule( + id: id, + configuration: alarmConfiguration + ) + let alarmModule = AlarmModule(alarm: alarm) + + continuation.resume(returning: alarmModule) + } catch { + continuation.resume(throwing: error) + } + } + } + } +} + +func createAttributes(presentation: AlarmPresentation, metadata: AnyMap, tintColor: RGBColor) -> AlarmAttributes { + + let attributes = AlarmAttributes.init( + presentation: presentation, + metadata: try? GenericDictionaryAlarmStruct(state: anyMapToDictionary(metadata)), + tintColor: createColor(tintColor) + ) + + return attributes +} + @available(iOS 26.0, *) class AlarmKitModule: HybridAlarmKitModuleSpec { - func createCountdown(props: CountdownProps) throws -> NitroModules.Promise { - let alertContent = AlarmPresentation.Alert( - title: LocalizedStringResource(stringLiteral: props.alert.title), - stopButton: createAlarmButton(props.alert.stopButton), - ) + func createCountdown(props: CountdownProps) throws -> Promise { + let alertContent = createAlertPresentation(props.alert) let countdownContent = AlarmPresentation.Countdown( title: LocalizedStringResource(stringLiteral: props.countdown.title), pauseButton: createAlarmButtonNullable(props.countdown.pauseButton), ) - var pausedPresentation: AlarmPresentation.Paused? - - if let paused = props.paused { - pausedPresentation = AlarmPresentation.Paused( - title: "Paused", - resumeButton: createAlarmButton(paused.resumeButton), - ) - } + let pausedPresentation = createPausedPresentation(props.paused) let presentation = AlarmPresentation( alert: alertContent, @@ -52,17 +123,15 @@ class AlarmKitModule: HybridAlarmKitModuleSpec { paused: pausedPresentation ) - let id = UUID() + let attributes = createAttributes( + presentation: presentation, + metadata: props.metadata, + tintColor: props.tintColor + ) let preAlert = TimeInterval(floatLiteral: props.preAlert) let postAlert = props.postAlert != nil ? TimeInterval(floatLiteral: props.postAlert!) : nil - let attributes = AlarmAttributes.init( - presentation: presentation, - metadata: try? GenericDictionaryAlarmStruct(state: anyMapToDictionary(props.metadata)), - tintColor: createColor(props.tintColor) - ) - let alarmConfiguration = AlarmManager.AlarmConfiguration( countdownDuration: Alarm.CountdownDuration.init( preAlert: preAlert, @@ -70,55 +139,29 @@ class AlarmKitModule: HybridAlarmKitModuleSpec { ), // schedule: Alarm.Schedule.fixed(Date.now.addingTimeInterval(countdownDuration)), attributes: attributes, + // stopIntent: Intent.unspecified (alarmID: id), // secondaryIntent: secondaryIntent(alarmID: id, userInput: userInput) sound: props.sound != nil ? .named(props.sound!) : .default ) - return Promise.async { - try await withCheckedThrowingContinuation { continuation in - Task { - do { - let alarm = try await AlarmManager.shared.schedule( - id: id, - configuration: alarmConfiguration - ) - let alarmModule = AlarmModule(alarm: alarm) - continuation.resume(returning: alarmModule) - } catch { - continuation.resume(throwing: error) - } - } - } - } + return scheduleAlarm(alarmConfiguration: alarmConfiguration) } - func createAlarm(props: AlarmProps) throws -> NitroModules.Promise { - let alertContent = AlarmPresentation.Alert( - title: LocalizedStringResource(stringLiteral: props.alert.title), - stopButton: createAlarmButton(props.alert.stopButton), - ) + func createAlarm(props: AlarmProps) throws -> NitroModules.Promise { + let alertContent = createAlertPresentation(props.alert) - var pausedPresentation: AlarmPresentation.Paused? - - if let paused = props.paused { - pausedPresentation = AlarmPresentation.Paused( - title: "Paused", - resumeButton: createAlarmButton(paused.resumeButton), - ) - } + let pausedPresentation = createPausedPresentation(props.paused) let presentation = AlarmPresentation( alert: alertContent, paused: pausedPresentation ) - let id = UUID() - - let attributes = AlarmAttributes.init( + let attributes = createAttributes( presentation: presentation, - metadata: try? GenericDictionaryAlarmStruct(state: anyMapToDictionary(props.metadata)), - tintColor: createColor(props.tintColor) + metadata: props.metadata, + tintColor: props.tintColor ) let alarmConfiguration = AlarmManager.AlarmConfiguration( @@ -129,22 +172,7 @@ class AlarmKitModule: HybridAlarmKitModuleSpec { sound: props.sound != nil ? .named(props.sound!) : .default ) - return Promise.async { - try await withCheckedThrowingContinuation { continuation in - Task { - do { - let alarm = try await AlarmManager.shared.schedule( - id: id, - configuration: alarmConfiguration - ) - let alarmModule = AlarmModule(alarm: alarm) - continuation.resume(returning: alarmModule) - } catch { - continuation.resume(throwing: error) - } - } - } - } + return scheduleAlarm(alarmConfiguration: alarmConfiguration) } func requestAuthorization() throws -> NitroModules.Promise { @@ -171,7 +199,7 @@ class AlarmKitModule: HybridAlarmKitModuleSpec { } } - func alarmUpdates(callback: @escaping ([any HybridAlarmSpec]) -> Void) throws { + func alarmUpdates(callback: @escaping ([any HybridAlarmProxySpec]) -> Void) throws { Task { for await alarms in AlarmManager.shared.alarmUpdates { callback(alarms.map { alarm in @@ -181,7 +209,7 @@ class AlarmKitModule: HybridAlarmKitModuleSpec { } } - func alarms() throws -> [any HybridAlarmSpec] { + func alarms() throws -> [any HybridAlarmProxySpec] { return try AlarmManager.shared.alarms.map { alarm in return AlarmModule(alarm: alarm) } diff --git a/packages/react-native-activity-kit/ios/AlarmModule.swift b/packages/react-native-activity-kit/ios/AlarmModule.swift deleted file mode 100644 index 9990f29..0000000 --- a/packages/react-native-activity-kit/ios/AlarmModule.swift +++ /dev/null @@ -1,20 +0,0 @@ -import AlarmKit -import NitroModules - -@available(iOS 26.0, *) -class AlarmModule: HybridAlarmSpec { - var id: String - - var state: AlarmState - - var postAlert: Double? - - var preAlert: Double? - - init(alarm: Alarm) { - self.id = alarm.id.uuidString - self.state = convertAlarmState(alarm.state) - self.postAlert = alarm.countdownDuration?.postAlert - self.preAlert = alarm.countdownDuration?.preAlert - } -} diff --git a/packages/react-native-activity-kit/ios/AlarmProxy.swift b/packages/react-native-activity-kit/ios/AlarmProxy.swift new file mode 100644 index 0000000..55bcf6f --- /dev/null +++ b/packages/react-native-activity-kit/ios/AlarmProxy.swift @@ -0,0 +1,47 @@ +import AlarmKit +import NitroModules + +@available(iOS 26.0, *) +class AlarmModule: HybridAlarmProxySpec { + func countdown() throws { + try AlarmManager.shared.countdown(id: alarm.id) + } + + func pause() throws { + try AlarmManager.shared.pause(id: alarm.id) + } + + func resume() throws { + try AlarmManager.shared.resume(id: alarm.id) + } + + func stop() throws { + try AlarmManager.shared.stop(id: alarm.id) + } + + func cancel() throws { + try AlarmManager.shared.cancel(id: alarm.id) + } + + private let alarm: Alarm + + var id: String { + return alarm.id.uuidString + } + + var state: AlarmState { + return convertAlarmState(alarm.state) + } + + var postAlert: Double? { + return alarm.countdownDuration?.postAlert + } + + var preAlert: Double? { + return alarm.countdownDuration?.preAlert + } + + init(alarm: Alarm) { + self.alarm = alarm + } +} diff --git a/packages/react-native-activity-kit/src/AlarmKit.ts b/packages/react-native-activity-kit/src/AlarmKit.ts index 35e2888..d3c2d68 100644 --- a/packages/react-native-activity-kit/src/AlarmKit.ts +++ b/packages/react-native-activity-kit/src/AlarmKit.ts @@ -1,26 +1,17 @@ import { Platform } from 'react-native' import { NitroModules } from 'react-native-nitro-modules' -import type { Alarm } from './specs/Alarm.nitro' -import type { AlarmKitModule, AuthStatus } from './specs/AlarmKit.nitro' -import type { - AlarmConfiguration, - AlarmEventListener, - AlarmEventSubscription, - AlarmKitError, - AlarmPermissionStatus, - AlarmPresentation, - AlarmUpdateRequest, -} from './types' +import type { AlarmKitModule } from './specs/AlarmKitModule.nitro' +import type { AlarmKitError } from './types' import { AlarmErrorCode } from './types' const PLATFORM_IOS = Platform.OS === 'ios' -const MIN_IOS_VERSION = 16 +const MIN_IOS_VERSION = 26 -function checkPlatformSupport(): void { +export function isSupported(): void { if (!PLATFORM_IOS) { throw createError( AlarmErrorCode.UnsupportedPlatform, - 'AlarmKit is only supported on iOS 16.0+', + 'AlarmKit is only supported on iOS 26.0+', ) } @@ -49,369 +40,4 @@ function createError( return error } -class AlarmKit { - private hybridObject: AlarmKitModule - - constructor() { - checkPlatformSupport() - this.hybridObject = - NitroModules.createHybridObject('AlarmKitModule') - } - - // Permission methods - async requestAuthorization(): Promise { - try { - return await this.hybridObject.requestAuthorization() - } catch (error: any) { - throw createError( - AlarmErrorCode.PermissionDenied, - 'Failed to request alarm permissions', - { - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - async createCountdown( - alertTitle: string, - stopText: string, - countdownTitle: string, - countdownDurationInSeconds: number = 60, - ): Promise { - return this.hybridObject.createCountdown({ - tintColor: { red: 0, green: 0.478, blue: 1, alpha: 1 }, - alert: { - title: alertTitle, - stopButton: { - text: stopText, - systemImageName: 'stop.fill', - textColor: { red: 1, green: 1, blue: 1, alpha: 1 }, - }, - }, - countdown: { - title: countdownTitle, - }, - preAlert: countdownDurationInSeconds, - metadata: { - timerFiringAt: Date.now() + countdownDurationInSeconds * 1000, - }, - }) - } - - /*async getPermissionStatus(): Promise { - try { - return await this.hybridObject.getPermissionStatus() - } catch (error: any) { - throw createError( - AlarmErrorCode.SystemError, - 'Failed to get permission status', - { - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - // Alarm management - async scheduleAlarm(configuration: AlarmConfiguration): Promise { - try { - this.validateAlarmConfiguration(configuration) - - const serializedConfig = { - ...configuration, - fireDate: configuration.fireDate.getTime(), - } - - return await this.hybridObject.scheduleAlarm(serializedConfig) - } catch (error: any) { - if (error.code) { - throw error // Re-throw AlarmKitError - } - throw createError( - AlarmErrorCode.SystemError, - 'Failed to schedule alarm', - { - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - async updateAlarm(request: AlarmUpdateRequest): Promise { - try { - const serializedRequest = { - ...request, - fireDate: request.fireDate?.getTime(), - } - - return await this.hybridObject.updateAlarm(serializedRequest) - } catch (error: any) { - throw createError(AlarmErrorCode.SystemError, 'Failed to update alarm', { - originalError: error?.message || 'Unknown error', - }) - } - } - - async cancelAlarm(identifier: string): Promise { - try { - if (!identifier || identifier.trim().length === 0) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Alarm identifier cannot be empty', - ) - } - - return await this.hybridObject.cancelAlarm(identifier) - } catch (error: any) { - if (error.code) { - throw error - } - throw createError( - AlarmErrorCode.AlarmNotFound, - 'Failed to cancel alarm', - { - identifier, - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - async cancelAllAlarms(): Promise { - try { - return await this.hybridObject.cancelAllAlarms() - } catch (error: any) { - throw createError( - AlarmErrorCode.SystemError, - 'Failed to cancel all alarms', - { - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - // Alarm queries - async getScheduledAlarms(): Promise { - try { - const alarms = await this.hybridObject.getScheduledAlarms() - return alarms.map(this.deserializeAlarmPresentation) - } catch (error: any) { - throw createError( - AlarmErrorCode.SystemError, - 'Failed to get scheduled alarms', - { - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - async getAlarm(identifier: string): Promise { - try { - if (!identifier || identifier.trim().length === 0) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Alarm identifier cannot be empty', - ) - } - - const alarm = await this.hybridObject.getAlarm(identifier) - return alarm ? this.deserializeAlarmPresentation(alarm) : null - } catch (error: any) { - if (error.code) { - throw error - } - throw createError(AlarmErrorCode.AlarmNotFound, 'Failed to get alarm', { - identifier, - originalError: error?.message || 'Unknown error', - }) - } - } - - async getAlarmCount(): Promise { - try { - return await this.hybridObject.getAlarmCount() - } catch (error: any) { - throw createError( - AlarmErrorCode.SystemError, - 'Failed to get alarm count', - { - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - // Alarm actions - async dismissAlarm(identifier: string): Promise { - try { - if (!identifier || identifier.trim().length === 0) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Alarm identifier cannot be empty', - ) - } - - return await this.hybridObject.dismissAlarm(identifier) - } catch (error: any) { - if (error.code) { - throw error - } - throw createError( - AlarmErrorCode.AlarmNotFound, - 'Failed to dismiss alarm', - { - identifier, - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - async snoozeAlarm( - identifier: string, - snoozeInterval: number, - ): Promise { - try { - if (!identifier || identifier.trim().length === 0) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Alarm identifier cannot be empty', - ) - } - - if (snoozeInterval <= 0) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Snooze interval must be greater than 0', - ) - } - - return await this.hybridObject.snoozeAlarm(identifier, snoozeInterval) - } catch (error: any) { - if (error.code) { - throw error - } - throw createError( - AlarmErrorCode.AlarmNotFound, - 'Failed to snooze alarm', - { - identifier, - originalError: error?.message || 'Unknown error', - }, - ) - } - } - - // Event subscription - addAlarmFireListener(listener: AlarmEventListener): AlarmEventSubscription { - this.hybridObject.addAlarmFireListener((alarm) => { - listener(this.deserializeAlarmPresentation(alarm)) - }) - - return { - remove: () => { - // In a full implementation, we would track listeners and remove them - // For now, this is a placeholder - }, - } - } - - addAlarmDismissListener( - listener: AlarmEventListener, - ): AlarmEventSubscription { - this.hybridObject.addAlarmDismissListener((alarm) => { - listener(this.deserializeAlarmPresentation(alarm)) - }) - - return { - remove: () => { - // Placeholder - would need proper listener management - }, - } - } - - addAlarmSnoozeListener(listener: AlarmEventListener): AlarmEventSubscription { - this.hybridObject.addAlarmSnoozeListener((alarm) => { - listener(this.deserializeAlarmPresentation(alarm)) - }) - - return { - remove: () => { - // Placeholder - would need proper listener management - }, - } - } - - addAlarmCancelListener(listener: AlarmEventListener): AlarmEventSubscription { - this.hybridObject.addAlarmCancelListener((alarm) => { - listener(this.deserializeAlarmPresentation(alarm)) - }) - - return { - remove: () => { - // Placeholder - would need proper listener management - }, - } - } - - // Private methods - private validateAlarmConfiguration(config: AlarmConfiguration): void { - if (!config.identifier || config.identifier.trim().length === 0) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Alarm identifier is required', - ) - } - - if (!config.title || config.title.trim().length === 0) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Alarm title is required', - ) - } - - if (!(config.fireDate instanceof Date)) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Fire date must be a Date object', - ) - } - - if (config.fireDate <= new Date()) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Fire date must be in the future', - ) - } - - if (config.sound?.volume !== undefined) { - if (config.sound.volume < 0 || config.sound.volume > 1) { - throw createError( - AlarmErrorCode.InvalidConfiguration, - 'Sound volume must be between 0 and 1', - ) - } - } - } - - private deserializeAlarmPresentation(alarm: any): AlarmPresentation { - return { - ...alarm, - configuration: { - ...alarm.configuration, - fireDate: new Date(alarm.configuration.fireDate), - }, - actualFireDate: alarm.actualFireDate - ? new Date(alarm.actualFireDate) - : undefined, - nextFireDate: alarm.nextFireDate - ? new Date(alarm.nextFireDate) - : undefined, - } - }*/ -} - -export default new AlarmKit() +export default NitroModules.createHybridObject('AlarmKitModule') diff --git a/packages/react-native-activity-kit/src/specs/Alarm.nitro.ts b/packages/react-native-activity-kit/src/specs/Alarm.nitro.ts deleted file mode 100644 index 356855a..0000000 --- a/packages/react-native-activity-kit/src/specs/Alarm.nitro.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HybridObject } from 'react-native-nitro-modules' - -export enum AlarmState { - scheduled, - countdown, - paused, - alerting, -} - -export interface Alarm extends HybridObject<{ ios: 'swift' }> { - // Permission methods - id: string - state: AlarmState - - postAlert?: number - preAlert?: number -} diff --git a/packages/react-native-activity-kit/src/specs/AlarmKit.nitro.ts b/packages/react-native-activity-kit/src/specs/AlarmKitModule.nitro.ts similarity index 89% rename from packages/react-native-activity-kit/src/specs/AlarmKit.nitro.ts rename to packages/react-native-activity-kit/src/specs/AlarmKitModule.nitro.ts index 22e9c86..436bce5 100644 --- a/packages/react-native-activity-kit/src/specs/AlarmKit.nitro.ts +++ b/packages/react-native-activity-kit/src/specs/AlarmKitModule.nitro.ts @@ -1,5 +1,5 @@ import type { AnyMap, HybridObject } from 'react-native-nitro-modules' -import type { Alarm } from './Alarm.nitro' +import type { AlarmProxy } from './AlarmProxy.nitro' export interface AlarmSound { name: string @@ -76,7 +76,7 @@ export interface AlarmButtonProps { systemImageName: string } -enum SecondaryButtonBehavior { +export enum SecondaryButtonBehavior { countdown = 'countdown', custom = 'custom', none = 'none', @@ -135,9 +135,9 @@ export interface AlarmKitModule extends HybridObject<{ ios: 'swift' }> { authorizationUpdates(callback: (status: AuthStatus) => void): void - alarmUpdates(callback: (alarms: Alarm[]) => void): void - alarms(): Alarm[] + alarmUpdates(callback: (alarms: AlarmProxy[]) => void): void + alarms(): AlarmProxy[] - createCountdown(props: CountdownProps): Promise - createAlarm(props: AlarmProps): Promise + createCountdown(props: CountdownProps): Promise + createAlarm(props: AlarmProps): Promise } diff --git a/packages/react-native-activity-kit/src/specs/AlarmProxy.nitro.ts b/packages/react-native-activity-kit/src/specs/AlarmProxy.nitro.ts new file mode 100644 index 0000000..16f3284 --- /dev/null +++ b/packages/react-native-activity-kit/src/specs/AlarmProxy.nitro.ts @@ -0,0 +1,23 @@ +import type { HybridObject } from 'react-native-nitro-modules' + +export enum AlarmState { + scheduled, + countdown, + paused, + alerting, +} + +export interface AlarmProxy extends HybridObject<{ ios: 'swift' }> { + // Permission methods + readonly id: string + readonly state: AlarmState + + readonly postAlert?: number + readonly preAlert?: number + + cancel(): void + countdown(): void + pause(): void + resume(): void + stop(): void +} From f7ac085ad5ccd3c7a9925a2198d17cce4a934f85 Mon Sep 17 00:00:00 2001 From: Robert Herber Date: Thu, 18 Sep 2025 23:05:31 +0200 Subject: [PATCH 08/10] remove unused import --- apps/activity-kit-example/app/(tabs)/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/activity-kit-example/app/(tabs)/index.tsx b/apps/activity-kit-example/app/(tabs)/index.tsx index a1817ef..58e99df 100644 --- a/apps/activity-kit-example/app/(tabs)/index.tsx +++ b/apps/activity-kit-example/app/(tabs)/index.tsx @@ -1,5 +1,4 @@ import { ActivityKit, AlarmKit } from '@kingstinct/react-native-activity-kit' // Importing the ActivityKit module' -import { SecondaryButtonBehavior } from '@kingstinct/react-native-activity-kit/lib/specs/AlarmKit.nitro' import { Image } from 'expo-image' import * as Notifications from 'expo-notifications' import { useState } from 'react' From 2ef01762adaeec1bdc5b3a842e1ddf451b1cfcd9 Mon Sep 17 00:00:00 2001 From: Robert Herber Date: Thu, 18 Sep 2025 23:07:17 +0200 Subject: [PATCH 09/10] Update changeset to include alertkit --- .changeset/fuzzy-turkeys-cross.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-turkeys-cross.md diff --git a/.changeset/fuzzy-turkeys-cross.md b/.changeset/fuzzy-turkeys-cross.md new file mode 100644 index 0000000..c9b0d32 --- /dev/null +++ b/.changeset/fuzzy-turkeys-cross.md @@ -0,0 +1,5 @@ +--- +"@kingstinct/react-native-activity-kit": patch +--- + +Add alertkit From cbacecd43e50331a86e3dda877030d19c040a1b7 Mon Sep 17 00:00:00 2001 From: Robert Herber Date: Thu, 18 Sep 2025 23:20:26 +0200 Subject: [PATCH 10/10] bump macos in workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6166ed9..4e2d906 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -145,7 +145,7 @@ jobs: build-ios: # Only run on macOS since we need Xcode - runs-on: macos-15 + runs-on: macos-26 timeout-minutes: 50 steps: