@jesushr0013/native-timer 8.1.0 → 8.2.1

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,7 +7,10 @@ let package = Package(
7
7
  products: [
8
8
  .library(
9
9
  name: "Jesushr0013NativeTimer",
10
- targets: ["Jesushr0013NativeTimer"])
10
+ targets: ["Jesushr0013NativeTimer"]),
11
+ .library(
12
+ name: "Jesushr0013NativeTimerLiveActivities",
13
+ targets: ["Jesushr0013NativeTimerLiveActivities"])
11
14
  ],
12
15
  dependencies: [
13
16
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
@@ -20,6 +23,12 @@ let package = Package(
20
23
  .product(name: "Cordova", package: "capacitor-swift-pm")
21
24
  ],
22
25
  path: "ios",
23
- sources: ["Plugin", "LiveActivitiesKit"])
26
+ sources: ["Core", "Plugin"]),
27
+ .target(
28
+ name: "Jesushr0013NativeTimerLiveActivities",
29
+ dependencies: [
30
+ "Jesushr0013NativeTimer"
31
+ ],
32
+ path: "ios/LiveActivities")
24
33
  ]
25
34
  )
package/README.md CHANGED
@@ -4,166 +4,437 @@ A Capacitor 8+ plugin for **work shift time tracking** (jornada laboral) with **
4
4
 
5
5
  The widget displays a live "Jornada Activa" (Active Shift) timer designed specifically for tracking work hours.
6
6
 
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Architecture (v8.2.0+)](#architecture-v820)
13
+ - [Requirements](#requirements)
14
+ - [Installation (New Project)](#installation-new-project)
15
+ - [iOS Setup](#ios-setup)
16
+ - [Android Setup](#android-setup)
17
+ - [Usage](#usage)
18
+ - [API Reference](#api-reference)
19
+ - [Upgrading from v8.1.x to v8.2.0](#upgrading-from-v81x-to-v820)
20
+ - [Troubleshooting](#troubleshooting)
21
+ - [License](#license)
22
+
23
+ ---
24
+
7
25
  ## Features
8
26
 
9
27
  - **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)
28
+ - **iOS**: Live Activities with Dynamic Island and Lock Screen widget showing "Jornada Activa" (iOS 16.2+)
29
+ - **iOS**: Compatible with iOS 15+ SwiftUI is **completely isolated** in a separate target, so there is **no crash on iOS 16**
12
30
  - Background-safe timer that survives app suspension — ideal for long work shifts
13
31
  - Customizable primary color for notifications and widgets
14
32
  - Smart notification management (foreground/background aware)
15
33
 
34
+ ---
35
+
36
+ ## Architecture (v8.2.0+)
37
+
38
+ Starting from v8.2.0, the iOS code is split into **3 independent targets** to prevent the main app from linking SwiftUI/SwiftUICore:
39
+
40
+ ```
41
+ ios/
42
+ ├── Core/ ← NativeTimerCore target
43
+ │ ├── NativeTimerManager.swift (timer logic, ActivityKit management)
44
+ │ └── WorkSessionTimerAttributes.swift (ActivityAttributes model)
45
+ ├── Plugin/ ← Jesushr0013NativeTimer target
46
+ │ └── NativeTimerPlugin.swift (Capacitor bridge)
47
+ └── LiveActivities/ ← NativeTimerLiveActivities target
48
+ └── NativeTimerWidget.swift (SwiftUI widget for Dynamic Island)
49
+ ```
50
+
51
+ | Target | SwiftUI? | What it does |
52
+ |--------|----------|--------------|
53
+ | **NativeTimerCore** | **No** | Timer logic, notification management, `WorkSessionTimerAttributes` model, `NativeTimerManager` class. Safe on iOS 15+. |
54
+ | **Jesushr0013NativeTimer** | **No** | Capacitor plugin bridge (`NativeTimerPlugin`). Depends on Core. Auto-discovered by Capacitor. Safe on iOS 15+. |
55
+ | **NativeTimerLiveActivities** | **Yes** | SwiftUI widget for Dynamic Island and Lock Screen. Depends on Core. Only loaded in Widget Extension target. |
56
+
57
+ > **Why this matters:** In previous versions, the widget's `import SwiftUI` lived in the same target as the plugin, causing `SwiftUICore` to be linked into the main app binary. On iOS 16.0–16.0.x, `SwiftUICore.framework` doesn't exist as a standalone framework, causing an immediate crash at launch. This architecture completely eliminates that problem.
58
+
59
+ ---
60
+
16
61
  ## Requirements
17
62
 
18
63
  | Platform | Minimum Version |
19
64
  |----------|----------------|
20
65
  | Capacitor | 8.0.0+ |
21
- | iOS | 16.0+ (Live Activities require 16.2+) |
66
+ | iOS | 15.0+ (Live Activities require 16.2+) |
22
67
  | Android | API 26+ (Android 8.0) |
23
68
  | Xcode | 16+ |
69
+ | Node.js | 18+ |
24
70
 
25
- ## Installation
71
+ ---
72
+
73
+ ## Installation (New Project)
74
+
75
+ ### Step 1: Install the npm package
26
76
 
27
77
  ```bash
28
78
  npm install @jesushr0013/native-timer
79
+ ```
80
+
81
+ ### Step 2: Sync with native platforms
82
+
83
+ ```bash
29
84
  npx cap sync
30
85
  ```
31
86
 
32
- ## iOS Setup
87
+ This will:
88
+ - Copy the plugin to `node_modules/`
89
+ - Update `ios/App/Podfile` with the plugin's pod dependency
90
+ - Run `pod install` in the `ios/` folder
91
+ - Copy plugin files to the Android project
33
92
 
34
- ### 1. Add Widget Extension
93
+ ---
35
94
 
36
- In Xcode, add a **Widget Extension** target to your project for Live Activities support.
95
+ ## iOS Setup
37
96
 
38
- ### 2. Fix SwiftUICore for iOS 16.x
97
+ ### Step 1: Verify pod installation
39
98
 
40
- If you compile with Xcode 16+, you **must** add this linker flag to both your App target and Widget Extension target in Xcode:
99
+ After `npx cap sync`, verify that the plugin was installed correctly:
41
100
 
42
- **Build Settings → Other Linker Flags:**
43
- ```
44
- -weak_framework SwiftUICore
101
+ ```bash
102
+ cd ios
103
+ pod install
104
+ cd ..
45
105
  ```
46
106
 
47
- This prevents a crash on iOS 16.x where `SwiftUICore.framework` doesn't exist as a standalone framework.
107
+ You should see `Jesushr0013NativeTimer` in the pod list. This pod includes **only** `Core/` + `Plugin/` **no SwiftUI**.
48
108
 
49
- ### 3. Entitlements
109
+ ### Step 2: Enable Live Activities in Info.plist
50
110
 
51
- Add the `Push Notifications` capability and enable `Supports Live Activities` in your `Info.plist`:
111
+ Open `ios/App/App/Info.plist` and add:
52
112
 
53
113
  ```xml
54
114
  <key>NSSupportsLiveActivities</key>
55
115
  <true/>
56
116
  ```
57
117
 
118
+ Or in Xcode: open Info.plist → add row → key `NSSupportsLiveActivities` → type `Boolean` → value `YES`.
119
+
120
+ ### Step 3: Add Widget Extension (required for Live Activities)
121
+
122
+ Live Activities need a **Widget Extension** target in your Xcode project:
123
+
124
+ 1. Open `ios/App/App.xcworkspace` in Xcode
125
+ 2. File → New → Target → **Widget Extension**
126
+ 3. Name it (e.g. `TimerWidgetExtension`)
127
+ 4. Uncheck "Include Configuration App Intent" (not needed)
128
+ 5. Click Finish
129
+
130
+ ### Step 4: Add NativeTimerKit to Widget Extension
131
+
132
+ #### Option A: CocoaPods (recommended for Capacitor projects)
133
+
134
+ Edit your `ios/App/Podfile` and add a target block for the Widget Extension:
135
+
136
+ ```ruby
137
+ target 'App' do
138
+ capacitor_pods
139
+ # ... your existing pods
140
+ end
141
+
142
+ # Add this new block:
143
+ target 'TimerWidgetExtension' do
144
+ pod 'NativeTimerKit'
145
+ end
146
+ ```
147
+
148
+ Then reinstall pods:
149
+
150
+ ```bash
151
+ cd ios
152
+ pod install
153
+ cd ..
154
+ ```
155
+
156
+ #### Option B: Swift Package Manager
157
+
158
+ 1. In Xcode: File → Add Package Dependencies
159
+ 2. Enter: `https://github.com/jesherram/native-timer.git`
160
+ 3. Select version `8.2.0` or higher
161
+ 4. Add `NativeTimerLiveActivities` library to your **Widget Extension** target
162
+
163
+ ### Step 5: Write the Widget Extension code
164
+
165
+ Replace the auto-generated Widget Extension code with:
166
+
167
+ ```swift
168
+ import WidgetKit
169
+ import SwiftUI
170
+ import ActivityKit
171
+
172
+ // CocoaPods imports:
173
+ import MeycagesalNativeTimer // provides WorkSessionTimerAttributes + NativeTimerManager
174
+ import NativeTimerKit // provides NativeTimerWidget (pre-built UI)
175
+
176
+ // If using SPM instead, replace the imports above with:
177
+ // import NativeTimerCore
178
+ // import NativeTimerLiveActivities
179
+
180
+ @main
181
+ struct TimerWidgetBundle: WidgetBundle {
182
+ var body: some Widget {
183
+ if #available(iOS 16.1, *) {
184
+ NativeTimerWidget()
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ That's it! The `NativeTimerWidget` comes pre-built with:
191
+ - Lock Screen banner with gradient background
192
+ - Dynamic Island (expanded, compact, and minimal views)
193
+ - Live timer counter using SwiftUI `.timer` style
194
+ - Customizable accent color via `primaryColor`
195
+
196
+ #### Optional: Build your own custom widget
197
+
198
+ If you want to customize the widget UI, use `WorkSessionTimerAttributes` directly:
199
+
200
+ ```swift
201
+ import ActivityKit
202
+ import SwiftUI
203
+ import WidgetKit
204
+ import MeycagesalNativeTimer // or NativeTimerCore for SPM
205
+
206
+ @available(iOS 16.1, *)
207
+ struct MyCustomTimerWidget: Widget {
208
+ var body: some WidgetConfiguration {
209
+ ActivityConfiguration(for: WorkSessionTimerAttributes.self) { context in
210
+ // Lock Screen UI
211
+ VStack {
212
+ Text(context.state.title)
213
+ Text(context.state.elapsedTime)
214
+ .font(.title.bold())
215
+ }
216
+ .padding()
217
+ } dynamicIsland: { context in
218
+ DynamicIsland {
219
+ DynamicIslandExpandedRegion(.bottom) {
220
+ Text(context.state.elapsedTime)
221
+ .font(.title2.bold())
222
+ }
223
+ } compactLeading: {
224
+ Image(systemName: "timer")
225
+ } compactTrailing: {
226
+ Text(context.state.elapsedTime)
227
+ .font(.caption2)
228
+ } minimal: {
229
+ Image(systemName: "timer")
230
+ }
231
+ }
232
+ }
233
+ }
234
+ ```
235
+
236
+ **Available fields in `context.state`:**
237
+
238
+ | Field | Type | Description |
239
+ |-------|------|-------------|
240
+ | `title` | `String` | Session name |
241
+ | `elapsedTime` | `String` | Formatted elapsed time (e.g. `"1 h 30 min"`) |
242
+ | `status` | `String` | Session status (e.g. `"active"`) |
243
+ | `startTime` | `String` | ISO 8601 start date |
244
+ | `primaryColor` | `String` | Hex color (e.g. `"#0045a5"`) |
245
+
246
+ ### Step 6: No SwiftUICore workaround needed
247
+
248
+ > **v8.2.0+:** Previous versions required adding `-weak_framework SwiftUICore` to the App target's linker flags. **This is no longer necessary.** The plugin target does not link SwiftUI at all.
249
+ >
250
+ > If you have this flag from a previous version, you can safely **remove it** from the App target. Keep it **only** in the Widget Extension target (the `NativeTimerKit` pod handles this automatically).
251
+
252
+ ---
253
+
58
254
  ## Android Setup
59
255
 
60
- The plugin automatically manages a Foreground Service. Make sure your `AndroidManifest.xml` includes:
256
+ ### Step 1: Permissions
257
+
258
+ Make sure your `android/app/src/main/AndroidManifest.xml` includes:
61
259
 
62
260
  ```xml
63
261
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
262
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
64
263
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
65
264
  ```
66
265
 
266
+ ### Step 2: That's it
267
+
268
+ The plugin automatically registers the foreground service and notification channel. No additional native code is needed.
269
+
270
+ ---
271
+
67
272
  ## Usage
68
273
 
274
+ ### Import
275
+
69
276
  ```typescript
70
277
  import { NativeTimer } from '@jesushr0013/native-timer';
278
+ ```
279
+
280
+ ### Basic Timer
71
281
 
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.
282
+ ```typescript
283
+ // Start a work shift timer
75
284
  await NativeTimer.startTimer({
76
- startTime: Date.now(),
77
- title: 'Jornada Activa',
78
- body: 'Timer en marcha...',
79
- primaryColor: '#0045a5', // optional — widget & notification accent color
285
+ startTime: Date.now(), // timestamp in ms
286
+ title: 'Jornada Activa', // Android notification title
287
+ body: 'Timer en marcha...', // Android notification body
288
+ primaryColor: '#0045a5', // optional: accent color
80
289
  });
81
290
 
82
- // Check if timer is running
291
+ // Check if running
83
292
  const { isRunning } = await NativeTimer.isTimerRunning();
84
293
 
85
- // Get elapsed time (ms)
294
+ // Get elapsed time in milliseconds
86
295
  const { elapsedTime } = await NativeTimer.getElapsedTime();
87
296
 
88
- // Update notification text
297
+ // Update the Android notification text
89
298
  await NativeTimer.updateNotification({
90
- title: 'Still working',
91
- body: '2h 30min elapsed',
299
+ title: 'Jornada Activa',
300
+ body: '2h 30min transcurridos',
92
301
  });
93
302
 
94
- // Stop the timer
303
+ // Stop everything
95
304
  await NativeTimer.stopTimer();
96
305
  ```
97
306
 
98
- ### Live Activities (iOS only)
307
+ ### Live Activities (iOS 16.2+ only)
99
308
 
100
309
  ```typescript
101
- // Check availability
310
+ // 1. Check if Live Activities are available
102
311
  const { available } = await NativeTimer.areLiveActivitiesAvailable();
103
312
 
104
313
  if (available) {
105
- // Start Live Activity
314
+ // 2. Start a Live Activity (shows in Dynamic Island + Lock Screen)
106
315
  const { activityId } = await NativeTimer.startLiveActivity({
107
- title: 'Work Session',
108
- startTime: new Date().toISOString(),
109
- elapsedTime: '0 h 0 min',
316
+ title: 'Jornada Laboral',
317
+ startTime: new Date().toISOString(), // ISO 8601 format
318
+ elapsedTime: '0 h 0 min', // formatted string
110
319
  status: 'active',
111
- primaryColor: '#0045a5',
320
+ primaryColor: '#0045a5', // optional
112
321
  });
113
322
 
114
- // Update Live Activity
323
+ // 3. Update the Live Activity periodically
115
324
  await NativeTimer.updateLiveActivity({
116
325
  activityId: activityId!,
117
326
  elapsedTime: '1 h 30 min',
118
327
  status: 'active',
119
328
  });
120
329
 
121
- // Stop Live Activity
330
+ // 4. Stop the Live Activity when the shift ends
122
331
  await NativeTimer.stopLiveActivity({ activityId: activityId! });
123
-
124
- // Or stop all at once
125
- await NativeTimer.stopAllLiveActivities();
126
332
  }
333
+
334
+ // Or stop ALL Live Activities at once (useful for cleanup)
335
+ await NativeTimer.stopAllLiveActivities();
127
336
  ```
128
337
 
129
- ### Foreground State Management
338
+ ### Smart Foreground/Background Management
130
339
 
131
340
  ```typescript
132
- // Tell the plugin when your app goes to background/foreground
133
- // to manage notifications intelligently
341
+ import { App } from '@capacitor/app';
342
+
343
+ // Tell the plugin when the app goes to foreground/background
344
+ // This controls whether local notifications are shown
345
+ // (only shown in background to avoid bothering the user)
134
346
  App.addListener('appStateChange', ({ isActive }) => {
135
347
  NativeTimer.setAppForegroundState({ inForeground: isActive });
136
348
  });
137
349
 
138
- // Reset notification dismissed state
350
+ // Reset the "notification dismissed" state when reopening the app
139
351
  await NativeTimer.resetNotificationState();
140
352
  ```
141
353
 
142
- ### Event Listener
354
+ ### Timer Update Listener
143
355
 
144
356
  ```typescript
145
- // Listen for periodic timer updates
357
+ // Listen for periodic updates (every ~30 seconds)
146
358
  const listener = await NativeTimer.addListener('timerUpdate', (data) => {
147
359
  console.log('Elapsed:', data.elapsedTime, 'ms');
148
360
  console.log('Formatted:', data.formattedTime);
149
361
  });
150
362
 
151
- // Remove all listeners
363
+ // Clean up when done
152
364
  await NativeTimer.removeAllListeners();
153
365
  ```
154
366
 
155
- ## API
367
+ ### Complete Example
368
+
369
+ ```typescript
370
+ import { NativeTimer } from '@jesushr0013/native-timer';
371
+ import { App } from '@capacitor/app';
372
+ import { Capacitor } from '@capacitor/core';
373
+
374
+ let currentActivityId: string | null = null;
375
+
376
+ // --- Start shift ---
377
+ async function startShift() {
378
+ const now = Date.now();
379
+ const color = '#0045a5';
380
+
381
+ // Start the native timer (Android foreground service + iOS timer)
382
+ await NativeTimer.startTimer({
383
+ startTime: now,
384
+ title: 'Jornada Activa',
385
+ body: 'Registrando tu jornada laboral...',
386
+ primaryColor: color,
387
+ });
388
+
389
+ // On iOS, also start a Live Activity
390
+ if (Capacitor.getPlatform() === 'ios') {
391
+ const { available } = await NativeTimer.areLiveActivitiesAvailable();
392
+ if (available) {
393
+ const { activityId } = await NativeTimer.startLiveActivity({
394
+ title: 'Jornada Laboral',
395
+ startTime: new Date(now).toISOString(),
396
+ elapsedTime: '0 h 0 min',
397
+ status: 'active',
398
+ primaryColor: color,
399
+ });
400
+ currentActivityId = activityId ?? null;
401
+ }
402
+ }
403
+ }
404
+
405
+ // --- Stop shift ---
406
+ async function stopShift() {
407
+ await NativeTimer.stopTimer();
408
+
409
+ if (currentActivityId) {
410
+ await NativeTimer.stopLiveActivity({ activityId: currentActivityId });
411
+ currentActivityId = null;
412
+ }
413
+ }
414
+
415
+ // --- Foreground/background awareness ---
416
+ App.addListener('appStateChange', ({ isActive }) => {
417
+ NativeTimer.setAppForegroundState({ inForeground: isActive });
418
+ if (isActive) {
419
+ NativeTimer.resetNotificationState();
420
+ }
421
+ });
422
+ ```
423
+
424
+ ---
425
+
426
+ ## API Reference
156
427
 
157
428
  ### `startTimer(options)`
158
429
 
159
- Inicia el timer nativo. En Android crea un Foreground Service con notificación persistente. En iOS prepara el timer interno para Live Activities.
430
+ Starts the native timer. On Android, creates a Foreground Service with a persistent notification. On iOS, prepares the internal timer for Live Activities.
160
431
 
161
432
  | Param | Type | Required | Description |
162
433
  |-------|------|----------|-------------|
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 |
434
+ | `startTime` | `number` | Yes | Timestamp in milliseconds (e.g. `Date.now()`) |
435
+ | `title` | `string` | Yes | Notification title (Android only) |
436
+ | `body` | `string` | Yes | Notification body (Android only) |
437
+ | `primaryColor` | `string` | No | Hex color (e.g. `"#0045a5"`) for notification and widget accent |
167
438
 
168
439
  **Returns:** `Promise<{ success: boolean }>`
169
440
 
@@ -171,7 +442,7 @@ Inicia el timer nativo. En Android crea un Foreground Service con notificación
171
442
 
172
443
  ### `stopTimer()`
173
444
 
174
- Detiene el timer, cancela el Foreground Service (Android) y elimina todas las notificaciones pendientes.
445
+ Stops the timer, cancels the Foreground Service (Android), and removes all pending notifications.
175
446
 
176
447
  **Returns:** `Promise<{ success: boolean }>`
177
448
 
@@ -179,12 +450,12 @@ Detiene el timer, cancela el Foreground Service (Android) y elimina todas las no
179
450
 
180
451
  ### `updateNotification(options)`
181
452
 
182
- Actualiza el texto de la notificación del timer en Android. En iOS no tiene efecto visible (el widget se actualiza via `updateLiveActivity`).
453
+ Updates the Android notification text. On iOS, use `updateLiveActivity()` instead.
183
454
 
184
455
  | Param | Type | Required | Description |
185
456
  |-------|------|----------|-------------|
186
- | `title` | `string` | Yes | Nuevo título de la notificación |
187
- | `body` | `string` | Yes | Nuevo cuerpo de la notificación |
457
+ | `title` | `string` | Yes | New notification title |
458
+ | `body` | `string` | Yes | New notification body |
188
459
 
189
460
  **Returns:** `Promise<{ success: boolean }>`
190
461
 
@@ -192,7 +463,7 @@ Actualiza el texto de la notificación del timer en Android. En iOS no tiene efe
192
463
 
193
464
  ### `isTimerRunning()`
194
465
 
195
- Comprueba si hay un timer activo en ejecución.
466
+ Checks if a timer is currently active.
196
467
 
197
468
  **Returns:** `Promise<{ isRunning: boolean }>`
198
469
 
@@ -200,19 +471,19 @@ Comprueba si hay un timer activo en ejecución.
200
471
 
201
472
  ### `getElapsedTime()`
202
473
 
203
- Obtiene el tiempo transcurrido desde que se inició el timer.
474
+ Gets the elapsed time since the timer was started.
204
475
 
205
- **Returns:** `Promise<{ elapsedTime: number }>` — tiempo en milisegundos
476
+ **Returns:** `Promise<{ elapsedTime: number }>` — time in milliseconds
206
477
 
207
478
  ---
208
479
 
209
480
  ### `setAppForegroundState(options)`
210
481
 
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.
482
+ Tells the plugin whether the app is in the foreground or background. Controls whether local notifications are displayed (only shown in background).
212
483
 
213
484
  | Param | Type | Required | Description |
214
485
  |-------|------|----------|-------------|
215
- | `inForeground` | `boolean` | Yes | `true` si la app está visible, `false` si está en segundo plano |
486
+ | `inForeground` | `boolean` | Yes | `true` if the app is visible, `false` if backgrounded |
216
487
 
217
488
  **Returns:** `Promise<{ success: boolean }>`
218
489
 
@@ -220,7 +491,7 @@ Indica al plugin si la app está en primer o segundo plano. Esto controla si las
220
491
 
221
492
  ### `resetNotificationState()`
222
493
 
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.
494
+ Resets the internal "notification dismissed" state. Call this when reopening the app so notifications can be shown again in background.
224
495
 
225
496
  **Returns:** `Promise<{ success: boolean }>`
226
497
 
@@ -228,7 +499,7 @@ Resetea el estado interno de "notificación descartada". Útil al volver a abrir
228
499
 
229
500
  ### `areLiveActivitiesAvailable()` *(iOS only)*
230
501
 
231
- Verifica si el dispositivo soporta Live Activities (requiere iOS 16.2+ y que el usuario las tenga habilitadas).
502
+ Checks if the device supports Live Activities (requires iOS 16.2+ and user permission).
232
503
 
233
504
  **Returns:** `Promise<{ available: boolean }>`
234
505
 
@@ -236,29 +507,29 @@ Verifica si el dispositivo soporta Live Activities (requiere iOS 16.2+ y que el
236
507
 
237
508
  ### `startLiveActivity(options)` *(iOS only)*
238
509
 
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.
510
+ Starts a Live Activity showing the shift timer on Dynamic Island (iPhone 14 Pro+) and Lock Screen.
240
511
 
241
512
  | Param | Type | Required | Description |
242
513
  |-------|------|----------|-------------|
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`) |
514
+ | `title` | `string` | Yes | Session name |
515
+ | `startTime` | `string` | Yes | Start date/time in ISO 8601 format |
516
+ | `elapsedTime` | `string` | Yes | Formatted elapsed time (e.g. `"1 h 30 min"`) |
517
+ | `status` | `string` | Yes | Session status (e.g. `"active"`, `"paused"`) |
518
+ | `primaryColor` | `string` | No | Hex color for the widget (default: `"#0045a5"`) |
248
519
 
249
- **Returns:** `Promise<{ success: boolean; activityId?: string }>` — el `activityId` se usa para actualizar o detener la actividad
520
+ **Returns:** `Promise<{ success: boolean; activityId?: string }>`
250
521
 
251
522
  ---
252
523
 
253
524
  ### `updateLiveActivity(options)` *(iOS only)*
254
525
 
255
- Actualiza una Live Activity existente con nuevo tiempo transcurrido y estado.
526
+ Updates an existing Live Activity with new elapsed time and status.
256
527
 
257
528
  | Param | Type | Required | Description |
258
529
  |-------|------|----------|-------------|
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"`) |
530
+ | `activityId` | `string` | Yes | ID returned by `startLiveActivity` |
531
+ | `elapsedTime` | `string` | Yes | Updated elapsed time (e.g. `"2 h 15 min"`) |
532
+ | `status` | `string` | Yes | Current status (e.g. `"active"`) |
262
533
 
263
534
  **Returns:** `Promise<{ success: boolean }>`
264
535
 
@@ -266,11 +537,11 @@ Actualiza una Live Activity existente con nuevo tiempo transcurrido y estado.
266
537
 
267
538
  ### `stopLiveActivity(options)` *(iOS only)*
268
539
 
269
- Detiene una Live Activity específica y la elimina de la Dynamic Island y pantalla de bloqueo.
540
+ Stops a specific Live Activity and removes it from Dynamic Island and Lock Screen.
270
541
 
271
542
  | Param | Type | Required | Description |
272
543
  |-------|------|----------|-------------|
273
- | `activityId` | `string` | Yes | ID devuelto por `startLiveActivity` |
544
+ | `activityId` | `string` | Yes | ID returned by `startLiveActivity` |
274
545
 
275
546
  **Returns:** `Promise<{ success: boolean }>`
276
547
 
@@ -278,7 +549,7 @@ Detiene una Live Activity específica y la elimina de la Dynamic Island y pantal
278
549
 
279
550
  ### `stopAllLiveActivities()` *(iOS only)*
280
551
 
281
- Detiene **todas** las Live Activities activas del plugin. Útil para limpieza al cerrar sesión o al detener la jornada.
552
+ Stops **all** active Live Activities from this plugin. Useful for cleanup on logout or shift end.
282
553
 
283
554
  **Returns:** `Promise<{ success: boolean }>`
284
555
 
@@ -286,7 +557,7 @@ Detiene **todas** las Live Activities activas del plugin. Útil para limpieza al
286
557
 
287
558
  ### `addListener('timerUpdate', callback)`
288
559
 
289
- Escucha actualizaciones periódicas del timer (cada ~30 segundos).
560
+ Listens for periodic timer updates (~30 seconds).
290
561
 
291
562
  ```typescript
292
563
  const listener = await NativeTimer.addListener('timerUpdate', (data) => {
@@ -299,10 +570,111 @@ const listener = await NativeTimer.addListener('timerUpdate', (data) => {
299
570
 
300
571
  ### `removeAllListeners()`
301
572
 
302
- Elimina todos los listeners registrados.
573
+ Removes all registered listeners.
303
574
 
304
575
  **Returns:** `Promise<void>`
305
576
 
577
+ ---
578
+
579
+ ## Upgrading from v8.1.x to v8.2.0
580
+
581
+ ### What changed
582
+
583
+ The iOS code was split from 1 target into 3 separate targets. The main plugin **no longer links SwiftUI**, completely fixing the `SwiftUICore` crash on iOS 16.
584
+
585
+ ### Step-by-step upgrade
586
+
587
+ ```bash
588
+ # 1. Update the plugin
589
+ npm install @jesushr0013/native-timer@8.2.0
590
+
591
+ # 2. Remove local patches (if you had modified the plugin's Swift files locally)
592
+ # Only needed if you used patch-package:
593
+ rm patches/@jesushr0013+native-timer+*.patch # skip if you don't have patches
594
+
595
+ # 3. Sync Capacitor
596
+ npx cap sync ios
597
+
598
+ # 4. Reinstall pods from scratch (important: pod paths changed)
599
+ cd ios
600
+ pod deintegrate
601
+ pod install --repo-update
602
+ cd ..
603
+ ```
604
+
605
+ Then in **Xcode**:
606
+
607
+ 5. **Remove the old linker flag** (if you had it):
608
+ - Select your **App** target → Build Settings → Other Linker Flags
609
+ - **Remove** `-weak_framework SwiftUICore`
610
+ - This flag is no longer needed in the App target
611
+
612
+ 6. **Clean and rebuild**:
613
+ - Product → Clean Build Folder (`Cmd+Shift+K`)
614
+ - Build (`Cmd+B`)
615
+
616
+ 7. **Test on iOS 16 simulator** — the app should launch without any `SwiftUICore` crash.
617
+
618
+ ### Breaking changes for Widget Extension users
619
+
620
+ If your Widget Extension was importing files from the old `LiveActivitiesKit` directory:
621
+
622
+ | Before (v8.1.x) | After (v8.2.0) |
623
+ |------------------|----------------|
624
+ | Files in `ios/LiveActivitiesKit/` | Files split into `ios/Core/` + `ios/LiveActivities/` |
625
+ | Single target compiled everything together | 3 separate targets |
626
+ | `import MeycagesalNativeTimer` (CocoaPods) | `import MeycagesalNativeTimer` + `import NativeTimerKit` (CocoaPods) |
627
+ | N/A (SPM) | `import NativeTimerCore` + `import NativeTimerLiveActivities` (SPM) |
628
+
629
+ ---
630
+
631
+ ## Troubleshooting
632
+
633
+ ### `SwiftUICore not available` crash on iOS 16
634
+
635
+ **Cause:** You are using a version < 8.2.0, or pods were not reinstalled after upgrading.
636
+
637
+ **Fix:**
638
+ ```bash
639
+ npm install @jesushr0013/native-timer@8.2.0
640
+ npx cap sync ios
641
+ cd ios && pod deintegrate && pod install --repo-update && cd ..
642
+ ```
643
+
644
+ ### Live Activities not appearing
645
+
646
+ 1. Check that `NSSupportsLiveActivities` is `true` in `Info.plist`
647
+ 2. Check that the Widget Extension target has `NativeTimerKit` (CocoaPods) or `NativeTimerLiveActivities` (SPM)
648
+ 3. Check that the device is running iOS 16.2+
649
+ 4. Check that Live Activities are enabled in Settings → Your App → Live Activities
650
+
651
+ ### Pod install fails with "Unable to find a specification for NativeTimerKit"
652
+
653
+ Run with `--repo-update` to refresh the pod specs cache:
654
+
655
+ ```bash
656
+ cd ios
657
+ pod install --repo-update
658
+ cd ..
659
+ ```
660
+
661
+ ### Widget Extension shows blank / doesn't load
662
+
663
+ Make sure your Widget Extension's `@main` bundle includes the widget:
664
+
665
+ ```swift
666
+ @main
667
+ struct TimerWidgetBundle: WidgetBundle {
668
+ var body: some Widget {
669
+ if #available(iOS 16.1, *) {
670
+ NativeTimerWidget()
671
+ }
672
+ }
673
+ }
674
+ ```
675
+
676
+ ---
677
+
306
678
  ## License
307
679
 
308
680
  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(Jesushr0013NativeTimer)
7
+ import Jesushr0013NativeTimer
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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesushr0013/native-timer",
3
- "version": "8.1.0",
3
+ "version": "8.2.1",
4
4
  "description": "Plugin nativo para timer con foreground service",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",