@jesushr0013/native-timer 8.0.10 → 8.2.0

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.
@@ -11,23 +11,12 @@ Pod::Spec.new do |s|
11
11
  s.homepage = package['repository']['url'] rescue 'https://github.com/jesherram/native-timer'
12
12
  s.author = package['author']
13
13
  s.source = { :git => 'https://github.com/jesherram/native-timer.git', :tag => s.version.to_s }
14
- s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}', 'ios/LiveActivitiesKit/**/*.{swift,h,m,c,cc,mm,cpp}'
15
- s.public_header_files = 'ios/LiveActivitiesKit/*.h'
14
+ s.source_files = 'ios/Core/**/*.{swift,h,m,c,cc,mm,cpp}', 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
16
15
 
17
16
  s.ios.deployment_target = '15.0'
18
17
  s.dependency 'Capacitor'
19
18
  s.swift_version = '5.1'
20
19
 
21
- # Frameworks condicionais - ActivityKit apenas para iOS 16.2+
22
- # SwiftUICore es necesario como weak_framework porque Xcode 16+ lo separa de SwiftUI
23
- # y en iOS 16.x no existe como framework independiente
24
- s.weak_frameworks = 'WidgetKit', 'SwiftUI', 'ActivityKit', 'SwiftUICore'
25
-
26
- # Linker flags para asegurar weak linking de SwiftUICore
27
- s.pod_target_xcconfig = {
28
- 'OTHER_LDFLAGS' => '$(inherited) -weak_framework SwiftUICore'
29
- }
30
- s.user_target_xcconfig = {
31
- 'OTHER_LDFLAGS' => '$(inherited) -weak_framework SwiftUICore'
32
- }
20
+ # Solo ActivityKit como weak_framework - SwiftUI ya NO se linkea en este target
21
+ s.weak_frameworks = 'ActivityKit'
33
22
  end
@@ -6,15 +6,13 @@ Pod::Spec.new do |s|
6
6
  s.homepage = 'https://github.com/jesherram/native-timer'
7
7
  s.author = { 'Meycagesal' => 'info@meycagesal.com' }
8
8
  s.source = { :git => 'https://github.com/jesherram/native-timer.git', :tag => s.version.to_s }
9
- s.source_files = 'ios/LiveActivitiesKit/**/*.{swift,h,m,c,cc,mm,cpp}'
10
- s.public_header_files = 'ios/LiveActivitiesKit/*.h'
9
+ s.source_files = 'ios/LiveActivities/**/*.{swift,h,m,c,cc,mm,cpp}'
11
10
 
12
11
  s.ios.deployment_target = '15.0'
12
+ s.dependency 'Jesushr0013NativeTimer'
13
13
  s.swift_version = '5.1'
14
14
 
15
- # Frameworks condicionais - ActivityKit apenas para iOS 16.2+
16
- # SwiftUICore es necesario como weak_framework porque Xcode 16+ lo separa de SwiftUI
17
- # y en iOS 16.x no existe como framework independiente
15
+ # Live Activities requiere SwiftUI - weak linking para compatibilidad con iOS < 16.1
18
16
  s.weak_frameworks = 'WidgetKit', 'SwiftUI', 'ActivityKit', 'SwiftUICore'
19
17
 
20
18
  # Linker flags para asegurar weak linking de SwiftUICore
package/Package.swift CHANGED
@@ -7,22 +7,32 @@ let package = Package(
7
7
  products: [
8
8
  .library(
9
9
  name: "Jesushr0013NativeTimer",
10
- targets: ["Jesushr0013NativeTimer"])
10
+ targets: ["Jesushr0013NativeTimer"]),
11
+ .library(
12
+ name: "NativeTimerLiveActivities",
13
+ targets: ["NativeTimerLiveActivities"])
11
14
  ],
12
15
  dependencies: [
13
16
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
14
17
  ],
15
18
  targets: [
19
+ .target(
20
+ name: "NativeTimerCore",
21
+ dependencies: [],
22
+ path: "ios/Core"),
16
23
  .target(
17
24
  name: "Jesushr0013NativeTimer",
18
25
  dependencies: [
26
+ "NativeTimerCore",
19
27
  .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
28
  .product(name: "Cordova", package: "capacitor-swift-pm")
21
29
  ],
22
- path: "ios",
23
- sources: ["Plugin", "LiveActivitiesKit"],
24
- linkerSettings: [
25
- .unsafeFlags(["-weak_framework", "SwiftUICore"])
26
- ])
30
+ path: "ios/Plugin"),
31
+ .target(
32
+ name: "NativeTimerLiveActivities",
33
+ dependencies: [
34
+ "NativeTimerCore"
35
+ ],
36
+ path: "ios/LiveActivities")
27
37
  ]
28
38
  )
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # @jesushr0013/native-timer
2
+
3
+ A Capacitor 8+ plugin for **work shift time tracking** (jornada laboral) with **Android Foreground Service** and **iOS Live Activities** (Dynamic Island + Lock Screen).
4
+
5
+ The widget displays a live "Jornada Activa" (Active Shift) timer designed specifically for tracking work hours.
6
+
7
+ ## Features
8
+
9
+ - **Android**: Persistent foreground service with notification for shift tracking
10
+ - **iOS**: Live Activities with Dynamic Island and Lock Screen widget showing "Jornada Activa" (iOS 16.2+)
11
+ - **iOS**: Compatible with iOS 16.x and 17+ (SwiftUICore weak-linked)
12
+ - Background-safe timer that survives app suspension — ideal for long work shifts
13
+ - Customizable primary color for notifications and widgets
14
+ - Smart notification management (foreground/background aware)
15
+
16
+ ## Requirements
17
+
18
+ | Platform | Minimum Version |
19
+ |----------|----------------|
20
+ | Capacitor | 8.0.0+ |
21
+ | iOS | 16.0+ (Live Activities require 16.2+) |
22
+ | Android | API 26+ (Android 8.0) |
23
+ | Xcode | 16+ |
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install @jesushr0013/native-timer
29
+ npx cap sync
30
+ ```
31
+
32
+ ## iOS Setup
33
+
34
+ ### 1. Add Widget Extension
35
+
36
+ In Xcode, add a **Widget Extension** target to your project for Live Activities support.
37
+
38
+ ### 2. Fix SwiftUICore for iOS 16.x
39
+
40
+ If you compile with Xcode 16+, you **must** add this linker flag to both your App target and Widget Extension target in Xcode:
41
+
42
+ **Build Settings → Other Linker Flags:**
43
+ ```
44
+ -weak_framework SwiftUICore
45
+ ```
46
+
47
+ This prevents a crash on iOS 16.x where `SwiftUICore.framework` doesn't exist as a standalone framework.
48
+
49
+ ### 3. Entitlements
50
+
51
+ Add the `Push Notifications` capability and enable `Supports Live Activities` in your `Info.plist`:
52
+
53
+ ```xml
54
+ <key>NSSupportsLiveActivities</key>
55
+ <true/>
56
+ ```
57
+
58
+ ## Android Setup
59
+
60
+ The plugin automatically manages a Foreground Service. Make sure your `AndroidManifest.xml` includes:
61
+
62
+ ```xml
63
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
64
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ```typescript
70
+ import { NativeTimer } from '@jesushr0013/native-timer';
71
+
72
+ // Start a timer
73
+ // Note: title/body are for the Android notification only.
74
+ // The iOS widget always shows "⚡ Jornada Activa" as the title.
75
+ await NativeTimer.startTimer({
76
+ startTime: Date.now(),
77
+ title: 'Jornada Activa',
78
+ body: 'Timer en marcha...',
79
+ primaryColor: '#0045a5', // optional — widget & notification accent color
80
+ });
81
+
82
+ // Check if timer is running
83
+ const { isRunning } = await NativeTimer.isTimerRunning();
84
+
85
+ // Get elapsed time (ms)
86
+ const { elapsedTime } = await NativeTimer.getElapsedTime();
87
+
88
+ // Update notification text
89
+ await NativeTimer.updateNotification({
90
+ title: 'Still working',
91
+ body: '2h 30min elapsed',
92
+ });
93
+
94
+ // Stop the timer
95
+ await NativeTimer.stopTimer();
96
+ ```
97
+
98
+ ### Live Activities (iOS only)
99
+
100
+ ```typescript
101
+ // Check availability
102
+ const { available } = await NativeTimer.areLiveActivitiesAvailable();
103
+
104
+ if (available) {
105
+ // Start Live Activity
106
+ const { activityId } = await NativeTimer.startLiveActivity({
107
+ title: 'Work Session',
108
+ startTime: new Date().toISOString(),
109
+ elapsedTime: '0 h 0 min',
110
+ status: 'active',
111
+ primaryColor: '#0045a5',
112
+ });
113
+
114
+ // Update Live Activity
115
+ await NativeTimer.updateLiveActivity({
116
+ activityId: activityId!,
117
+ elapsedTime: '1 h 30 min',
118
+ status: 'active',
119
+ });
120
+
121
+ // Stop Live Activity
122
+ await NativeTimer.stopLiveActivity({ activityId: activityId! });
123
+
124
+ // Or stop all at once
125
+ await NativeTimer.stopAllLiveActivities();
126
+ }
127
+ ```
128
+
129
+ ### Foreground State Management
130
+
131
+ ```typescript
132
+ // Tell the plugin when your app goes to background/foreground
133
+ // to manage notifications intelligently
134
+ App.addListener('appStateChange', ({ isActive }) => {
135
+ NativeTimer.setAppForegroundState({ inForeground: isActive });
136
+ });
137
+
138
+ // Reset notification dismissed state
139
+ await NativeTimer.resetNotificationState();
140
+ ```
141
+
142
+ ### Event Listener
143
+
144
+ ```typescript
145
+ // Listen for periodic timer updates
146
+ const listener = await NativeTimer.addListener('timerUpdate', (data) => {
147
+ console.log('Elapsed:', data.elapsedTime, 'ms');
148
+ console.log('Formatted:', data.formattedTime);
149
+ });
150
+
151
+ // Remove all listeners
152
+ await NativeTimer.removeAllListeners();
153
+ ```
154
+
155
+ ## API
156
+
157
+ ### `startTimer(options)`
158
+
159
+ Inicia el timer nativo. En Android crea un Foreground Service con notificación persistente. En iOS prepara el timer interno para Live Activities.
160
+
161
+ | Param | Type | Required | Description |
162
+ |-------|------|----------|-------------|
163
+ | `startTime` | `number` | Yes | Timestamp en milisegundos (ej: `Date.now()`) del inicio de la jornada |
164
+ | `title` | `string` | Yes | Título de la notificación (solo Android) |
165
+ | `body` | `string` | Yes | Cuerpo de la notificación (solo Android) |
166
+ | `primaryColor` | `string` | No | Color hex (ej: `#0045a5`) para la notificación Android y el widget iOS |
167
+
168
+ **Returns:** `Promise<{ success: boolean }>`
169
+
170
+ ---
171
+
172
+ ### `stopTimer()`
173
+
174
+ Detiene el timer, cancela el Foreground Service (Android) y elimina todas las notificaciones pendientes.
175
+
176
+ **Returns:** `Promise<{ success: boolean }>`
177
+
178
+ ---
179
+
180
+ ### `updateNotification(options)`
181
+
182
+ Actualiza el texto de la notificación del timer en Android. En iOS no tiene efecto visible (el widget se actualiza via `updateLiveActivity`).
183
+
184
+ | Param | Type | Required | Description |
185
+ |-------|------|----------|-------------|
186
+ | `title` | `string` | Yes | Nuevo título de la notificación |
187
+ | `body` | `string` | Yes | Nuevo cuerpo de la notificación |
188
+
189
+ **Returns:** `Promise<{ success: boolean }>`
190
+
191
+ ---
192
+
193
+ ### `isTimerRunning()`
194
+
195
+ Comprueba si hay un timer activo en ejecución.
196
+
197
+ **Returns:** `Promise<{ isRunning: boolean }>`
198
+
199
+ ---
200
+
201
+ ### `getElapsedTime()`
202
+
203
+ Obtiene el tiempo transcurrido desde que se inició el timer.
204
+
205
+ **Returns:** `Promise<{ elapsedTime: number }>` — tiempo en milisegundos
206
+
207
+ ---
208
+
209
+ ### `setAppForegroundState(options)`
210
+
211
+ Indica al plugin si la app está en primer o segundo plano. Esto controla si las notificaciones locales se muestran (solo en segundo plano) para no molestar al usuario mientras usa la app.
212
+
213
+ | Param | Type | Required | Description |
214
+ |-------|------|----------|-------------|
215
+ | `inForeground` | `boolean` | Yes | `true` si la app está visible, `false` si está en segundo plano |
216
+
217
+ **Returns:** `Promise<{ success: boolean }>`
218
+
219
+ ---
220
+
221
+ ### `resetNotificationState()`
222
+
223
+ Resetea el estado interno de "notificación descartada". Útil al volver a abrir la app para que las notificaciones puedan mostrarse de nuevo en segundo plano.
224
+
225
+ **Returns:** `Promise<{ success: boolean }>`
226
+
227
+ ---
228
+
229
+ ### `areLiveActivitiesAvailable()` *(iOS only)*
230
+
231
+ Verifica si el dispositivo soporta Live Activities (requiere iOS 16.2+ y que el usuario las tenga habilitadas).
232
+
233
+ **Returns:** `Promise<{ available: boolean }>`
234
+
235
+ ---
236
+
237
+ ### `startLiveActivity(options)` *(iOS only)*
238
+
239
+ Inicia una Live Activity que muestra el timer de jornada en la Dynamic Island (iPhone 14 Pro+) y en la pantalla de bloqueo. El widget muestra "⚡ Jornada Activa" con un contador en tiempo real.
240
+
241
+ | Param | Type | Required | Description |
242
+ |-------|------|----------|-------------|
243
+ | `title` | `string` | Yes | Nombre de la sesión (usado internamente) |
244
+ | `startTime` | `string` | Yes | Fecha/hora de inicio en formato ISO 8601 |
245
+ | `elapsedTime` | `string` | Yes | Tiempo transcurrido formateado (ej: `"1 h 30 min"`) |
246
+ | `status` | `string` | Yes | Estado de la jornada (ej: `"active"`, `"paused"`) |
247
+ | `primaryColor` | `string` | No | Color hex para el widget (default: `#0045a5`) |
248
+
249
+ **Returns:** `Promise<{ success: boolean; activityId?: string }>` — el `activityId` se usa para actualizar o detener la actividad
250
+
251
+ ---
252
+
253
+ ### `updateLiveActivity(options)` *(iOS only)*
254
+
255
+ Actualiza una Live Activity existente con nuevo tiempo transcurrido y estado.
256
+
257
+ | Param | Type | Required | Description |
258
+ |-------|------|----------|-------------|
259
+ | `activityId` | `string` | Yes | ID devuelto por `startLiveActivity` |
260
+ | `elapsedTime` | `string` | Yes | Tiempo transcurrido actualizado (ej: `"2 h 15 min"`) |
261
+ | `status` | `string` | Yes | Estado actual (ej: `"active"`) |
262
+
263
+ **Returns:** `Promise<{ success: boolean }>`
264
+
265
+ ---
266
+
267
+ ### `stopLiveActivity(options)` *(iOS only)*
268
+
269
+ Detiene una Live Activity específica y la elimina de la Dynamic Island y pantalla de bloqueo.
270
+
271
+ | Param | Type | Required | Description |
272
+ |-------|------|----------|-------------|
273
+ | `activityId` | `string` | Yes | ID devuelto por `startLiveActivity` |
274
+
275
+ **Returns:** `Promise<{ success: boolean }>`
276
+
277
+ ---
278
+
279
+ ### `stopAllLiveActivities()` *(iOS only)*
280
+
281
+ Detiene **todas** las Live Activities activas del plugin. Útil para limpieza al cerrar sesión o al detener la jornada.
282
+
283
+ **Returns:** `Promise<{ success: boolean }>`
284
+
285
+ ---
286
+
287
+ ### `addListener('timerUpdate', callback)`
288
+
289
+ Escucha actualizaciones periódicas del timer (cada ~30 segundos).
290
+
291
+ ```typescript
292
+ const listener = await NativeTimer.addListener('timerUpdate', (data) => {
293
+ console.log(data.elapsedTime); // number (ms)
294
+ console.log(data.formattedTime); // string
295
+ });
296
+ ```
297
+
298
+ ---
299
+
300
+ ### `removeAllListeners()`
301
+
302
+ Elimina todos los listeners registrados.
303
+
304
+ **Returns:** `Promise<void>`
305
+
306
+ ## License
307
+
308
+ MIT
@@ -1,2 +1,2 @@
1
- #Tue Dec 09 12:43:54 CET 2025
2
- gradle.version=6.8.3
1
+ #Thu Apr 16 07:13:06 CEST 2026
2
+ gradle.version=8.9
@@ -6,31 +6,6 @@ import UIKit
6
6
  import ActivityKit
7
7
  #endif
8
8
 
9
- @available(iOS 16.2, *)
10
- public struct WorkSessionTimerAttributes: ActivityAttributes {
11
- public struct ContentState: Codable, Hashable {
12
- public let title: String
13
- public let elapsedTime: String
14
- public let status: String
15
- public let startTime: String
16
- public let primaryColor: String
17
-
18
- public init(title: String, elapsedTime: String, status: String, startTime: String, primaryColor: String = "#0045a5") {
19
- self.title = title
20
- self.elapsedTime = elapsedTime
21
- self.status = status
22
- self.startTime = startTime
23
- self.primaryColor = primaryColor
24
- }
25
- }
26
-
27
- public let sessionName: String
28
-
29
- public init(sessionName: String) {
30
- self.sessionName = sessionName
31
- }
32
- }
33
-
34
9
  public class NativeTimerManager {
35
10
  private var startTime: Date?
36
11
  private var timer: Timer?
@@ -0,0 +1,32 @@
1
+ import Foundation
2
+
3
+ #if canImport(ActivityKit)
4
+ import ActivityKit
5
+ #endif
6
+
7
+ #if canImport(ActivityKit)
8
+ @available(iOS 16.1, *)
9
+ public struct WorkSessionTimerAttributes: ActivityAttributes {
10
+ public struct ContentState: Codable, Hashable {
11
+ public let title: String
12
+ public let elapsedTime: String
13
+ public let status: String
14
+ public let startTime: String
15
+ public let primaryColor: String
16
+
17
+ public init(title: String, elapsedTime: String, status: String, startTime: String, primaryColor: String = "#0045a5") {
18
+ self.title = title
19
+ self.elapsedTime = elapsedTime
20
+ self.status = status
21
+ self.startTime = startTime
22
+ self.primaryColor = primaryColor
23
+ }
24
+ }
25
+
26
+ public let sessionName: String
27
+
28
+ public init(sessionName: String) {
29
+ self.sessionName = sessionName
30
+ }
31
+ }
32
+ #endif
@@ -3,7 +3,13 @@ import SwiftUI
3
3
  import WidgetKit
4
4
  import ActivityKit
5
5
 
6
- @available(iOS 16.2, *)
6
+ #if canImport(NativeTimerCore)
7
+ import NativeTimerCore
8
+ #elseif canImport(MeycagesalNativeTimer)
9
+ import MeycagesalNativeTimer
10
+ #endif
11
+
12
+ @available(iOS 16.1, *)
7
13
  public struct NativeTimerWidget: Widget {
8
14
  public var body: some WidgetConfiguration {
9
15
  ActivityConfiguration(for: WorkSessionTimerAttributes.self) { context in
@@ -467,7 +473,7 @@ extension Color {
467
473
  }
468
474
  }
469
475
 
470
- @available(iOS 16.2, *)
476
+ @available(iOS 16.1, *)
471
477
  public struct NativeTimerWidgetBundle: WidgetBundle {
472
478
  public var body: some Widget {
473
479
  NativeTimerWidget()
@@ -1,6 +1,10 @@
1
1
  import Foundation
2
2
  import Capacitor
3
3
 
4
+ #if canImport(NativeTimerCore)
5
+ import NativeTimerCore
6
+ #endif
7
+
4
8
  #if canImport(ActivityKit)
5
9
  import ActivityKit
6
10
  #endif
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesushr0013/native-timer",
3
- "version": "8.0.10",
3
+ "version": "8.2.0",
4
4
  "description": "Plugin nativo para timer con foreground service",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",